lib/mix/tasks/spex.ex

defmodule Mix.Tasks.Spex do
  @moduledoc """
  Mix task that checks saved implementation models for bisimilarity.

  This is the offline verification entry point for `.spex` model files.
  """

  use Mix.Task

  @requirements ["app.config"]

  @doc """
  Runs Spex bisimilarity checks for all loaded implementation models.

  Accepts either no arguments (uses configured `:impl_models_dir`) or one path
  argument to a directory or file containing `.spex` models.
  """
  @impl Mix.Task
  def run(args)

  def run([]) do
    impl_models_dir =
      Application.get_env(:spex, :impl_models_dir) ||
        raise(
          ArgumentError,
          "Missing path to impl models. Please provide it as an argument or configure an " <>
            " :impl_models_dir in the application environment; for details, run " <>
            "`mix help spex`."
        )

    run_checks(impl_models_dir)
  end

  def run([path]) do
    run_checks(path)
  end

  @spec run_checks(Path.t()) :: :ok
  defp run_checks(path) do
    case Spex.ImplModelStore.load(path) do
      {:ok, impl_models} -> do_run_checks(impl_models)
      {:error, error} -> raise error
    end
  end

  @spec do_run_checks([Spex.ImplModel.t()]) :: :ok
  defp do_run_checks(impl_models) when is_list(impl_models) do
    bisimilarity_statuses = Enum.map(impl_models, &check_bisimilarity/1)

    n_total = bisimilarity_statuses |> Enum.count()
    n_not_bisimilar = bisimilarity_statuses |> Stream.filter(&(&1 == false)) |> Enum.count()

    if n_not_bisimilar == 0 do
      IO.puts(
        IO.ANSI.green() <>
          "[Spex] All #{n_total} ImplModels are bisimilar to their specifications." <>
          IO.ANSI.default_color()
      )

      :ok
    else
      IO.puts(
        IO.ANSI.red() <>
          "[Spex] #{n_not_bisimilar} out of #{n_total} ImplModels are " <>
          "#{IO.ANSI.italic()}not#{IO.ANSI.not_italic()} bisimilar to their specifications." <>
          IO.ANSI.default_color()
      )

      exit({:shutdown, 1})
    end
  end

  @spec check_bisimilarity(Spex.ImplModel.t()) :: boolean()
  defp check_bisimilarity(%Spex.ImplModel{} = impl_model) do
    bisimilarity_status = Spex.BisimilarityChecker.bisimilar_to_specification?(impl_model)

    if bisimilarity_status == false do
      IO.puts(
        IO.ANSI.red() <>
          "[Spex] ImplModel is not bisimilar after tests finished:\n" <>
          Spex.ImplModel.serialise(impl_model) <>
          "\n" <>
          IO.ANSI.default_color()
      )
    end

    bisimilarity_status
  end
end