lib/chaperon/action/run_scenario.ex

defmodule Chaperon.Action.RunScenario do
  @moduledoc """
  Action that runs a `Chaperon.Scenario` module from the current session.
  """

  defstruct scenario: nil,
            config: %{},
            scheduler: :local,
            id: nil,
            pid: nil

  @type t :: %__MODULE__{
          scenario: Chaperon.Scenario.t(),
          config: map,
          scheduler: scheduler,
          id: String.t(),
          pid: pid
        }

  alias __MODULE__
  alias Chaperon.Scenario

  @type scenario :: module | Scenario.t()
  @type scheduler :: :local | :cluster

  def new(scenario, config, scheduler) do
    %RunScenario{
      scenario: scenario |> as_scenario,
      config: config,
      scheduler: scheduler
    }
  end

  @spec as_scenario(scenario) :: Scenario.t()
  defp as_scenario(scenario_mod) when is_atom(scenario_mod), do: %Scenario{module: scenario_mod}
  defp as_scenario(s = %Scenario{}), do: s
end

defimpl Chaperon.Actionable, for: Chaperon.Action.RunScenario do
  alias Chaperon.Worker
  use Chaperon.Session.Logging

  import Chaperon.Session,
    only: [
      set_config: 2,
      merge: 2,
      reset_action_metadata: 1,
      add_metric: 3
    ]

  import Chaperon.Timing

  def run(%{scheduler: scheduler, scenario: scenario, config: config}, session) do
    scenario_config =
      config
      |> Map.merge(%{merge_scenario_sessions: true})

    start = timestamp()

    scenario_session =
      case {scheduler, session.config[:execute_nested_scenario]} do
        {:cluster, _} ->
          schedule_cluster_worker(scenario, scenario_config, session)

        {_, :random_node} ->
          # The code below runs the nested scenario on a random worker node
          # in the cluster. This can be alot slower if the amount of nested
          # scenarios being run is high.
          schedule_cluster_worker(scenario, scenario_config, session)

        _ ->
          # In cases with a high amount of nested scenarios being executed per
          # running session, running the nested scenario inside the current
          # session's process is going to be alot faster and have less
          # communication overhead. Also, this won't ensure the nested scenario
          # times out after a configured worker timeout. Instead, the execution
          # time of the nested scenario will be added to this session's
          # execution time.
          schedule_local_worker(scenario, scenario_config, session)
      end

    merge_scenario_sessions = session.config[:merge_scenario_sessions]

    scenario_session =
      case scenario_session do
        nil ->
          session
          |> log_error(
            "Trying to merge scenario_session that failed (Worker task most likely timed out): #{
              inspect(scenario)
            }"
          )

        _ ->
          session
          |> merge_scenario_session(scenario_session)
          |> add_metric({:run_scenario, scenario.module}, timestamp() - start)
      end

    merged_session =
      scenario_session
      |> set_config(merge_scenario_sessions: merge_scenario_sessions)

    {:ok, merged_session}
  end

  defp schedule_cluster_worker(scenario, scenario_config, session) do
    scenario
    |> Worker.start_nested(
      session |> Chaperon.Session.fork(),
      scenario_config
    )
    |> Worker.await(:infinity)
  end

  defp schedule_local_worker(scenario, scenario_config, session) do
    Chaperon.Scenario.execute_nested(
      scenario,
      session |> reset_action_metadata,
      scenario_config
    )
  end

  def abort(action = %{pid: pid}, session) do
    # TODO
    send(pid, :abort)
    {:ok, action, session}
  end

  defp merge_scenario_session(session, scenario_session) do
    %{
      session
      | config: Map.merge(session.config, scenario_session.config),
        assigned: Map.merge(session.assigned, scenario_session.assigned),
        cookies: scenario_session.cookies
    }
    |> merge(scenario_session)
  end
end

defimpl String.Chars, for: Chaperon.Action.RunScenario do
  alias Chaperon.Action.RunScenario

  def to_string(%RunScenario{scenario: scenario}) do
    "RunScenario[#{scenario.module}]"
  end
end