lib/runbox/runtime/stage/sandbox/test_runner.ex

defmodule Runbox.Runtime.Stage.Sandbox.TestRunner do
  @moduledoc """
  For new unit testing of scenarios it is recommended to use functions in
  `Runbox.Runtime.Stage.Sandbox` directly. This module should only help to
  convert old scenarios integration tests (using Solutions.TestRunner) to unit
  tests. For examples of such conversion see tests in the scenarios master
  branch.
  """

  alias Runbox.Runtime.Stage.Sandbox
  import ExUnit.Assertions

  @doc """
  Runs an unit test according to the `test_def` definition.

  Function accepts the same `test_def` definition as `Solutions.TestRunner.run_test/1`
  with few differences:

    * Only `run_options.scenario_id`, `run_options.start_from`, `messages` and `expected_actions`
      properties are used, other properties are ignored.

    * Instead of output log format `expected_actions` are `Toolbox.Scenario.OutputAction`
      structures, for example

      ```
      %{
        action: :add_asset,
        params: %{attributes: %{}, id: "/diamonds/O"},
        result: "nil",
        serialization_vector: ["/diamonds/O"],
        side_effects: false,
        timestamp: 1
      }
      ```

      must be changed to:

      ```
      %OutputAction{
        type: :add_asset,
        body: %Asset{attributes: %{}, id: "/diamonds/O"},
        svector: ["/diamonds/O"],
        timestamp: 1
      }
      ```

  To see differences take a look at `deduplication_diamond_test.exs` in scenarios and
  the same test in solutions.
  """
  def run_test(test_def) do
    opts =
      if test_def.run_options[:start_from] do
        [start_from: test_def.run_options.start_from]
      else
        []
      end

    {:ok, oas} = Sandbox.execute_run(test_def.messages, test_def.run_options.scenario_id, opts)
    assert_output_actions!(oas, test_def[:expected_actions])
  end

  defp assert_output_actions!(got_actions, expected_actions) when is_function(expected_actions) do
    expected_actions.(got_actions)
    :ok
  end

  defp assert_output_actions!(got_actions, expected_actions) when is_list(expected_actions) do
    missing = set_difference(expected_actions, got_actions)
    unexpected = set_difference(got_actions, expected_actions)
    assert {missing, unexpected} == {[], []}
    :ok
  end

  defp set_difference(a, b), do: MapSet.to_list(MapSet.difference(MapSet.new(a), MapSet.new(b)))
end