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