lib/runbox/scenario_release.ex

defmodule Runbox.ScenarioRelease do
  @moduledoc """
  Module for inspecting a scenario release.
  """

  alias Runbox.Scenario
  alias Runbox.ScenarioTemplate
  alias Runbox.Slave

  defmodule SlaveFunc do
    require Logger

    @moduledoc """
    Functions which are called on the scenario slave via RPC from the master
    node.
    """

    alias Runbox.Scenario.TemplateInspector

    @spec release_info([module] | nil) :: [Scenario.t()]
    def release_info(modules \\ nil) do
      modules_to_scenarios(modules || release_modules())
    end

    @doc """
    Returns list of scenarios in release.
    """
    @spec scenarios :: [scenario_id :: String.t()]
    def scenarios do
      release_modules()
      |> Enum.filter(&Scenario.manifest_module?/1)
      |> Enum.flat_map(fn manifest_mod ->
        case scenario_manifest_info(manifest_mod) do
          {:ok, manifest} ->
            [manifest.id]

          {:error, _} ->
            Logger.warning("Ignoring manifest module #{inspect(manifest_mod)}")
            []
        end
      end)
    end

    defp release_modules do
      scenario_app = Application.fetch_env!(:runbox, :scenario_app)

      case :application.get_key(scenario_app, :modules) do
        {:ok, modules} -> modules
        :undefined -> []
      end
    end

    defp modules_to_scenarios(modules) do
      modules
      |> Enum.filter(&Scenario.manifest_module?/1)
      |> Enum.flat_map(fn manifest_mod ->
        case scenario_manifest_info(manifest_mod) do
          {:ok, manifest} ->
            opts = scenario_opts(manifest_mod)
            templates = scenario_templates(modules, manifest_mod, manifest)

            [
              %Scenario{
                manifest: manifest,
                opts: opts,
                templates: templates
              }
            ]

          {:error, _} ->
            Logger.warning("Ignoring manifest module #{inspect(manifest_mod)}")
            []
        end
      end)
    end

    defp scenario_templates(modules, manifest_mod, manifest) do
      template_modules = Enum.filter(modules, &Scenario.template_module?(&1, manifest_mod))
      %{template_inspector: inspector_mod} = Scenario.get_impl_for(manifest.type)

      Enum.flat_map(template_modules, fn mod ->
        if valid_template?(inspector_mod, mod) do
          info = template_info(inspector_mod, mod)
          [%ScenarioTemplate{module: mod, info: info}]
        else
          []
        end
      end)
    end

    defp scenario_manifest_info(manifest_mod) do
      {:ok, manifest_mod.get_info()}
    catch
      kind, value ->
        Logger.warning(
          "Error reading info of manifest #{inspect(manifest_mod)}: #{inspect({kind, value})}"
        )

        {:error, value}
    end

    defp scenario_opts(manifest_mod) do
      if Code.ensure_loaded?(manifest_mod) && function_exported?(manifest_mod, :on_start, 0) do
        %{on_start: {manifest_mod, :on_start, []}}
      else
        %{}
      end
    end

    defp valid_template?(inspector_mod, template_mod) do
      TemplateInspector.valid_template?(inspector_mod, template_mod)
    end

    defp template_info(inspector_mod, template_mod) do
      TemplateInspector.template_info(inspector_mod, template_mod)
    end
  end

  @doc """
  Retrieves information about scenarios in a release.
  """
  @spec release_info(String.t()) :: {:ok, [Scenario.t()]} | {:error, term()}
  def release_info(release_dir) do
    Slave.with_slave(release_dir, fn slave ->
      Slave.call(slave, SlaveFunc, :release_info, [])
    end)
  end
end