Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

MyVal (version 7: prevent sporadic {:/, 0} property-based tests)

MyVal7.livemd

MyVal (version 7: prevent sporadic {:/, 0} property-based tests)

Documentation on @types, @specs, StreamData, & TypeCheck

Types & Specs:

StreamData:

TypeCheck:

Mix.install([
  {:type_check, "~> 0.10.6"},
  {:stream_data, "~> 0.5.0"}
])
defmodule MyVal do
  use TypeCheck
  @enforce_keys [:start_val, :changes, :valid?]
  defstruct [:start_val, changes: [], errors: [], valid?: true]

  @type! start_val :: number()
  @type! my_val_ops :: :+ | :- | :* | :/
  @type! action :: {my_val_ops(), number()}
  @type! actions :: list(action())
  @type! change :: {number(), action()}
  @type! changes :: list(change())
  # @type! first_calc :: {nil, nil}
  # @type! last_calc :: first_calc() | ordinary_calc()
  # @type! operation :: {:+ | :- | :* | :/, number()}
  # @type! first_entry :: {number(), {nil, nil}}
  # @type! my_val_entry :: {number(), operation()}
  # @type! my_val_entry :: {number(), {nil, nil}} | {number(), {:+ | :- | :* | :/, number()}}
  # @type! my_val_initial_entry :: {number(), {nil, nil}}
  # @type! my_val_subsequent_entry :: {number(), {my_val_ops(), number()}}
  # @type! my_val_entry :: my_val_initial_entry() | my_val_subsequent_entry()
  # @type! ordered_entry_list :: nonempty_list(my_val_initial_entry()) | nonempty_list(my_val_entry())
  @type! ordered_entry_list :: nonempty_list()
  @type! t :: %MyVal{start_val: start_val(), changes: changes(), valid?: boolean()}

  @spec! new(number()) :: %MyVal{}
  def new(val), do: %MyVal{start_val: val, changes: [], valid?: true}

  @spec! apply_actions(t(), actions()) :: t() | {:error, t()}
  def apply_actions(%MyVal{valid?: false} = myval, _actions) do
    myval |> wrap_in_already_invalid_error_tuple
  end

  def apply_actions(%MyVal{start_val: sv, changes: _changes} = myval, actions) do
    case any_invalid_action?(actions) do
      true ->
        myval |> invalidate_and_wrap_in_divide_by_zero_error_tuple()

      false ->
        actions
        |> Enum.reduce(myval, fn action, acc -> apply_action(acc, action) end)
    end
  end

  defp any_invalid_action?(actions) do
    Enum.any?(actions, fn {op, operand} -> op == :/ && operand == 0 end)
  end

  defp invalidate_due_to_attempt_to_divide_by_zero(myval) do
    errors =
      ["Attempted to apply an invalid action to this MyVal instance"] ++ Map.get(myval, :errors)

    %{myval | valid?: false, errors: errors}
  end

  defp invalidate_and_wrap_in_divide_by_zero_error_tuple(myval) do
    myval
    |> invalidate_due_to_attempt_to_divide_by_zero()
    |> (fn mv -> {:error, mv} end).()
  end

  defp wrap_in_already_invalid_error_tuple(myval) do
    errors =
      ["Attempted to apply_actions to an invalid MyVal instance"] ++ Map.get(myval, :errors)

    {:error, %{myval | errors: errors}}
  end

  @spec! apply_action(t(), action()) :: t() | {:error, t()}
  def apply_action(%MyVal{valid?: false} = mv, {_op, _operand}) do
    mv |> wrap_in_already_invalid_error_tuple
  end

  def apply_action(%MyVal{valid?: true} = mv, {:+, operand}) do
    add(mv, operand)
  end

  def apply_action(%MyVal{valid?: true} = mv, {:-, operand}) do
    subtract(mv, operand)
  end

  def apply_action(%MyVal{valid?: true} = mv, {:*, operand}) do
    multiply(mv, operand)
  end

  def apply_action(%MyVal{valid?: true} = mv, {:/, 0}) do
    # %{mv | valid?: false, errors: ["Attempted to divide by zero"]}
    # {:error, %{mv | valid?: false, changes: [{:invalid, {:/, 0}}], errors: ["Attempted to divide by zero"]}}
    mv |> invalidate_and_wrap_in_divide_by_zero_error_tuple()
  end

  def apply_action(%MyVal{valid?: true} = mv, {:/, operand}) do
    divide(mv, operand)
  end

  #  initial_value = mv |> val()
  #  actions
  #  |> Enum.reduce(, fn {_val, {op, operand}}, acc ->
  #    apply(Kernel, op, [acc, operand])
  #  end)
  #  %{mv | changes: new_changes}

  @spec! val(t()) :: number() | :error
  def val(%MyVal{valid?: false}), do: :error

  def val(%MyVal{start_val: sv, changes: []}) when is_number(sv), do: sv

  def val(%MyVal{changes: [{hd_val, {_hd_op, _hd_operand}} | _tl]}), do: hd_val

  @spec! peek(t()) :: t() | :error
  def peek(%MyVal{valid?: false}), do: :error

  def peek(%MyVal{changes: [{hd_val, {_hd_op, _hd_operand}} | _tl]} = my_val) do
    IO.inspect(hd_val, label: "current value of MyVal instance")
    my_val
  end

  def peek(%MyVal{start_val: sv, changes: []} = my_val) do
    IO.inspect(sv, label: "current value of MyVal instance")
    my_val
  end

  @spec! add(t(), number()) :: t() | {:error, t()}
  def add(%MyVal{valid?: false} = mv, _operand), do: mv |> wrap_in_already_invalid_error_tuple()

  def add(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
    cur_val = my_val |> val()
    %{my_val | changes: [{cur_val + new_val, {:+, new_val}} | changes]}
  end

  @spec! subtract(t(), number()) :: t() | {:error, t()}
  def subtract(%MyVal{valid?: false} = mv, _operand),
    do: mv |> wrap_in_already_invalid_error_tuple()

  def subtract(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
    cur_val = my_val |> val()
    %{my_val | changes: [{cur_val - new_val, {:-, new_val}} | changes]}
  end

  @spec! multiply(t(), number()) :: t() | {:error, t()}
  def multiply(%MyVal{valid?: false} = mv, _operand),
    do: mv |> wrap_in_already_invalid_error_tuple()

  def multiply(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
    cur_val = my_val |> val()
    %{my_val | changes: [{cur_val * new_val, {:*, new_val}} | changes]}
  end

  @spec! divide(t(), number()) :: t() | {:error, t()}
  def divide(%MyVal{valid?: false} = mv, _operand),
    do: mv |> wrap_in_already_invalid_error_tuple()

  def divide(my_val, 0), do: my_val |> invalidate_and_wrap_in_divide_by_zero_error_tuple()

  def divide(%MyVal{changes: changes} = my_val, new_val) when is_list(changes) do
    cur_val = my_val |> val()
    %{my_val | changes: [{cur_val / new_val, {:/, new_val}} | changes]}
  end

  @spec! view(t()) :: %{val: number(), my_val: t()} | {:error, t()}
  def view(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()

  def view(%MyVal{changes: _changes} = my_val) do
    %{val: MyVal.val(my_val), my_val: my_val}
  end

  @spec! ordered_history(t()) :: ordered_entry_list() | {:error, t()}
  def ordered_history(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()

  def ordered_history(%MyVal{start_val: sv, changes: changes}) do
    changes
    |> Enum.reverse()
    |> List.insert_at(0, {sv, nil})
  end

  @spec! show_history(t()) :: list(String.t()) | {:error, t()}
  def show_history(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()

  def show_history(%MyVal{start_val: sv, changes: changes}) do
    changes
    |> Enum.reverse()
    |> Enum.reduce(
      ["#{sv}"],
      &stringify_next_line/2
    )
    |> Enum.reverse()
  end

  @spec! show_history2(t()) :: list(String.t()) | {:error, t()}
  def show_history2(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()

  def show_history2(%MyVal{start_val: sv, changes: changes}) do
    changes
    |> Enum.reverse()
    |> Enum.reduce(
      ["#{sv}"],
      fn {val, {op, operand}}, acc -> ["#{op} #{operand} = #{val}" | acc] end
    )
    |> Enum.reverse()
  end

  @spec! recalculate(t()) :: number() | {:error, t()}
  def recalculate(%MyVal{valid?: false} = mv), do: mv |> wrap_in_already_invalid_error_tuple()

  def recalculate(%MyVal{start_val: sv, changes: changes} = my_val) do
    actions = changes |> Enum.map(fn {_num, {op, operand}} -> {op, operand} end)

    case any_invalid_action?(actions) do
      true ->
        my_val |> invalidate_and_wrap_in_divide_by_zero_error_tuple()

      false ->
        changes
        |> Enum.reverse()
        |> Enum.reduce(sv, fn {_val, {op, operand}}, acc ->
          apply(Kernel, op, [acc, operand])
        end)
    end
  end

  defp stringify_next_line({_val, {nil, nil}} = entry, acc) do
    [stringify_entry(entry) | acc]
  end

  defp stringify_next_line({_val, {_op, _operand}} = entry, acc) do
    [stringify_entry(entry) | acc]
  end

  defp stringify_entry({val, {nil, nil}}) do
    "#{val}"
  end

  defp stringify_entry({val, {op, operand}}) do
    "#{op} #{operand} = #{val}"
  end
end
ExUnit.start()

defmodule MyValSpecTest do
  use ExUnit.Case, async: true
  use TypeCheck.ExUnit

  spectest(MyVal)
end
defimpl Inspect, for: MyVal do
  def inspect(my_val) do
    # %MyVal{my_value: #{my_val.value}, history: #{my_val.history}}
    """
    #{my_val |> MyVal.val()}
    """
  end
end
defimpl String.Chars, for: MyVal do
  def to_string(my_val) do
    # %MyVal{my_value: #{my_val.value}, history: #{my_val.history}}
    """
    #{my_val |> MyVal.val()}
    """
  end
end
String.Chars.impl_for([])
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.add(8)
|> MyVal.view()

# |> (fn my_val -> %{val: MyVal.val(my_val), my_val: my_val} end).()
# |> inspect()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.view()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.show_history()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.show_history2()
MyVal.new(-8)
|> MyVal.add(11)
|> MyVal.add(4)
|> MyVal.subtract(8)
|> MyVal.add(11)
|> MyVal.multiply(10)
|> MyVal.subtract(10)
|> MyVal.divide(10)
|> MyVal.ordered_history()
MyVal.new(88)
|> MyVal.multiply(17)
|> MyVal.add(1_000_000)
|> MyVal.peek()
|> MyVal.divide(100)
|> MyVal.val()
# MyVal.new("a")
# %MyVal{start_val: -111, changes: [7, 9]} |> MyVal.val()
# %MyVal{changes: [{7, {:+, 8}}]} |> MyVal.val()
# %MyVal{changes: [{nil, {:+, 99}}], valid?: true, errors: []} |> MyVal.val()
# %MyVal{changes: [{7, {nil, 99}}], valid?: true} |> MyVal.val()
# %MyVal{changes: [{7, {:+, nil}}], valid?: true} |> MyVal.val()
MyVal.new(-9)
|> MyVal.add(9)
|> MyVal.add("3")
MyVal.new(-9)
|> MyVal.add(9)
|> MyVal.subtract("3")
MyVal.new(88)
|> MyVal.multiply(17)
|> MyVal.add(1_000_000)
|> MyVal.peek()
|> MyVal.divide(100)
|> MyVal.recalculate()
defmodule MyValTest do
  use ExUnit.Case
  require MyVal

  setup do
    steps = [
      {:add, 1},
      {:divide, 10},
      {:multiply, 2},
      {:subtract, 10}
    ]

    %{starting_value: 99, steps: steps}
  end

  test "silly test" do
    assert 3 == 2 + 1
  end

  test "recalculate and val return the same value", %{starting_value: sv, steps: steps} do
    final_my_value =
      steps
      |> Enum.reduce(
        MyVal.new(sv),
        fn {op, operand}, acc -> apply(MyVal, op, [acc, operand]) end
      )

    assert MyVal.recalculate(final_my_value) == MyVal.val(final_my_value)
  end
end

ExUnit.run()
defmodule MyValTest2 do
  use ExUnit.Case
  use TypeCheck
  require MyVal
  import TypeCheck.Type.StreamData

  setup do
    myval_generator = TypeCheck.Type.build({MyVal.actions()}) |> to_gen()
    %{myval_gen: myval_generator}
  end

  test "generation", %{myval_gen: myval_gen} do
    StreamData.list_of(myval_gen, length: 50) |> Enum.take(1) |> IO.inspect(label: "sample data")
  end
end

ExUnit.run()
MyVal.new(8)
|> MyVal.apply_action({:+, 9})
|> MyVal.apply_action({:-, 19})
MyVal.new(8)
|> MyVal.apply_actions([{:+, 9}, {:-, 19}])
defmodule MyValTest3 do
  use ExUnit.Case
  use TypeCheck
  require MyVal
  import TypeCheck.Type.StreamData

  setup do
    actions_generator = TypeCheck.Type.build(MyVal.action()) |> to_gen()
    # This didn't work because the generated instances weren't valid
    # I'll generate valid instances from more primative building blocks
    # myval_generator = TypeCheck.Type.build(MyVal.t()) |> to_gen()

    %{actions_gen: actions_generator}
  end

  test "attempting to divide by 0 returns :error tuple" do
    result =
      MyVal.new(11)
      |> MyVal.divide(0)

    assert {:error,
            %MyVal{
              valid?: false,
              errors: ["Attempted to apply an invalid action to this MyVal instance"]
            }} = result
  end

  test "attempting to apply_action {:/, 0} returns :error tuple" do
    result =
      MyVal.new(11)
      |> MyVal.apply_action({:/, 0})

    assert {:error,
            %MyVal{
              valid?: false,
              errors: ["Attempted to apply an invalid action to this MyVal instance"]
            }} = result
  end

  test "attempting to apply_actions when one action is {:/, 0} returns :error tuple" do
    result =
      MyVal.new(11)
      |> MyVal.apply_actions([{:+, 7}, {:*, 9}, {:/, 0}, {:-, -11}])

    assert {:error,
            %MyVal{
              valid?: false,
              errors: ["Attempted to apply an invalid action to this MyVal instance"]
            }} = result
  end

  def remove_divide_by_zero_actions(list_of_actions) do
    list_of_actions
    |> Enum.reject(fn {op, operand} -> op == :/ && operand == 0 end)
  end

  test "apply_action vs apply_actions (when no division by zero)", %{actions_gen: actions_gen} do
    max_action_length = 20
    num_tests = 10

    start_vals =
      StreamData.integer(-500..10_000) |> Enum.take(num_tests) |> IO.inspect(label: "start_vals")

    myvals_actions =
      StreamData.list_of(actions_gen, max_length: max_action_length)
      |> Enum.take(num_tests)
      # |> Enum.map(fn list -> Enum.reject(list, fn {op, operand} -> op == :/ && operand == 0 end) end)
      |> Enum.map(&remove_divide_by_zero_actions/1)
      |> IO.inspect(label: "myvals_actions")

    myvals =
      start_vals
      |> Enum.map(&MyVal.new/1)
      |> Enum.zip(myvals_actions)
      |> Enum.map(fn {mv, mvas} -> MyVal.apply_actions(mv, mvas) end)
      |> IO.inspect(label: "myvals")

    actions =
      StreamData.list_of(actions_gen, max_length: max_action_length)
      |> Enum.take(num_tests)
      |> Enum.map(&remove_divide_by_zero_actions/1)
      |> IO.inspect(label: "actions")

    for idx <- 0..(num_tests - 1) do
      actions_applied =
        MyVal.apply_actions(myvals |> Enum.at(idx), actions |> Enum.at(idx))
        |> IO.inspect(label: "actions_applied")

      assert actions_applied |> MyVal.val() == MyVal.recalculate(actions_applied)
    end
  end
end

ExUnit.run()