Skip to main content

lib/mix/tasks/firebreak.lockstep.ex

defmodule Mix.Tasks.Firebreak.Lockstep do
  @shortdoc "Generate lockstep concurrency-test scaffolds from cross-tree findings"

  @moduledoc """
  Generates a [lockstep](https://hex.pm/packages/lockstep) `ctest` scaffold per
  synchronous cross-tree crossing firebreak found — the dynamic counterpart to
  `mix firebreak.spec` (which generates TLA+). Where the TLA+ spec *proves* the
  cross-tree `:noproc` failure is reachable in a model, the lockstep scaffold is
  the starting point for a test that reproduces it in the running BEAM.

      mix firebreak.lockstep                  # analyse current project -> firebreak_lockstep/
      mix firebreak.lockstep ../some_app --out scenarios
      mix firebreak.lockstep --no-compile     # static only

  Each scaffold names the two coupled processes, sets up the lockstep harness, and
  marks three TODOs (start the target, drive the call, assert it's handled). It is
  written outside `test/` and `flunk/1`s until completed, so it never breaks your
  suite before you finish and move it in. Requires `{:lockstep, ...}` as a test
  dependency in the target project.
  """

  use Mix.Task

  @impl Mix.Task
  def run(argv) do
    {opts, args} = OptionParser.parse!(argv, strict: [out: :string, compile: :boolean])

    root = List.first(args) || File.cwd!()
    if args == [] and opts[:compile] != false, do: maybe_compile()

    analyze_opts = if opts[:compile] == false, do: [runtime: false], else: []
    out = opts[:out] || "firebreak_lockstep"
    File.mkdir_p!(out)

    scaffolds =
      root
      |> Firebreak.analyze(analyze_opts)
      |> Firebreak.Lockstep.generate()

    Enum.each(scaffolds, fn {name, contents} ->
      File.write!(Path.join(out, name), contents)
    end)

    Mix.shell().info(
      "firebreak: wrote #{length(scaffolds)} lockstep scaffold(s) to #{out}/ " <>
        "(one per synchronous cross-tree crossing)"
    )

    Enum.each(scaffolds, fn {name, _} -> Mix.shell().info("  + #{name}") end)
  end

  defp maybe_compile do
    Mix.Task.run("compile", ["--no-deps-check"])
  rescue
    _ -> :ok
  catch
    _, _ -> :ok
  end
end