lib/quint_connect/quint_cli.ex

defmodule QuintConnect.QuintCLI do
  @moduledoc """
  Builds `quint run` / `quint test` argument lists and environment for `System.cmd/3`.
  """

  alias QuintConnect.Config

  @type mode :: :run | :test

  @doc """
  argv for `System.cmd(executable, argv, opts)` — **does not** include the executable name.

  `out_itf` should include `{seq}` so Quint can emit multiple traces.
  """
  @spec argv(String.t(), String.t(), Config.t(), keyword()) :: [String.t()]
  def argv(spec_path, out_itf_path, %Config{} = config, opts \\ []) do
    mode = Keyword.get(opts, :mode, :run)
    base_args(mode, spec_path) ++ mode_args(mode, out_itf_path, config)
  end

  @doc """
  Environment list for `System.cmd/3`.
  """
  @spec merged_env(Config.t()) :: [{String.t(), String.t()}]
  def merged_env(%Config{} = config) do
    base = System.get_env() |> Map.to_list()

    base
    |> put_env_if("QUINT_SEED", config.seed)
    |> put_env_if("QUINT_VERBOSE", verbosity_string(config.verbosity))
  end

  defp base_args(:test, spec_path), do: ["test", spec_path]
  defp base_args(:run, spec_path), do: ["run", spec_path]

  defp mode_args(:test, out_itf_path, config) do
    []
    |> append_seed(config.seed)
    |> append_pair("--match", "^#{config.test_name}$")
    |> append_pair("--max-samples", Config.trace_count(config, :test))
    |> append_pair("--out-itf", out_itf_path)
    |> append_verbosity(config.verbosity || 0)
    |> append_if_present("--main", config.main_module)
  end

  defp mode_args(:run, out_itf_path, config) do
    []
    |> append_seed(config.seed)
    |> append_pair("--max-samples", Config.trace_count(config, :run))
    |> append_pair("--n-traces", Config.trace_count(config, :run))
    |> append_pair("--out-itf", out_itf_path)
    |> maybe_append("--mbt", config.mbt)
    |> append_verbosity(config.verbosity || 0)
    |> append_if_present("--init", config.init_action)
    |> append_if_present("--step", config.step_action)
    |> append_if_present("--max-steps", config.max_steps)
    |> append_if_present("--main", config.main_module)
    |> append_if_present("--backend", config.backend)
  end

  defp maybe_append(acc, _flag, false), do: acc
  defp maybe_append(acc, flag, true), do: acc ++ [flag]
  defp append_if_present(acc, _flag, nil), do: acc
  defp append_if_present(acc, flag, value), do: append_pair(acc, flag, value)
  defp append_pair(acc, flag, value), do: acc ++ [flag, to_string(value)]

  defp append_seed(acc, nil), do: acc
  defp append_seed(acc, seed), do: append_pair(acc, "--seed", seed)

  defp append_verbosity(acc, nil), do: acc
  defp append_verbosity(acc, v) when is_integer(v), do: append_pair(acc, "--verbosity", v)

  defp append_verbosity(acc, v) when is_binary(v) do
    case Integer.parse(v) do
      {n, _} -> append_pair(acc, "--verbosity", n)
      :error -> append_pair(acc, "--verbosity", v)
    end
  end

  defp put_env_if(env, _key, nil), do: env
  defp put_env_if(env, key, value), do: List.keystore(env, key, 0, {key, value})

  defp verbosity_string(nil), do: nil
  defp verbosity_string(v) when is_integer(v), do: Integer.to_string(v)
  defp verbosity_string(v) when is_binary(v), do: v
end