A recording stand-in for a generated script's Ctl.World, so an app's
tests drive every <app>-ctl command without docker on the box and
assert each command's exact docker argv.
It ships in lib/ rather than test/support/ because a dependency's
test support is not compiled into the app that depends on it: this way
every app's tests use the one recorder that matches the generated
Ctl.World, instead of copying it. It has no dependencies and does
nothing until install/2 names it in the calling process.
# test/test_helper.exs: once per VM, before any test runs, since the
# script's modules (`Ctl`, `Ctl.Env` …) are global.
Macaw.Host.Ctl.Recorder.load!("container/my_app-ctl")
# a test, with `@moduletag :tmp_dir`
alias Macaw.Host.Ctl.Recorder
test "start runs the container", %{tmp_dir: tmp_dir} do
{script, _home} = Recorder.home!(tmp_dir, "my_app-ctl")
Recorder.install([{["docker"], {"", 0}}])
Ctl.dispatch(["start"], %{}, script)
run = Enum.find(Recorder.docker_calls(), &match?(["run", "-d" | _], &1))
assert ["run", "-d", "--name", "my_app" | _] = run
endinstall/2 takes the canned answers: a list of {matcher, reply}, the
matcher a list the command's [exe | args] must start with, or a
function of (exe, args), and the reply {output, status}. The first
matching entry answers; anything unmatched is {"", 1}, a docker that
says no. Every call is recorded, with its options (where a secret's value
travels, in :env). Everything lives in the calling process's
dictionary, so async tests do not see each other's calls.
Summary
Functions
Every call so far, in order: {exe, args, opts}, {:sleep, ms} or {:prompt, question}.
The argv of each docker invocation, in order.
A host under tmp_dir: home/data/ and a script script_name naming
home as its data directory. {script, home}.
Makes this process's Ctl.World the recorder. Options: :answer, what
prompt/1 and prompt_secret/1 answer (default "").
Loads a generated script's modules into this VM without running it:
:ctl_no_main keeps its last line from calling main/1. Loading the
same path again is a no-op; a script is loaded once per VM, as its
modules are global.
Writes a minimal script at path: its Ctl.Home naming data_dir, the
only part of the file the loaded modules read from disk.
Functions
@spec calls() :: [tuple()]
Every call so far, in order: {exe, args, opts}, {:sleep, ms} or {:prompt, question}.
@spec docker_calls() :: [[String.t()]]
The argv of each docker invocation, in order.
A host under tmp_dir: home/data/ and a script script_name naming
home as its data directory. {script, home}.
@spec install( [{list() | (String.t(), [String.t()] -> boolean()), {String.t(), integer()}}], keyword() ) :: :ok
Makes this process's Ctl.World the recorder. Options: :answer, what
prompt/1 and prompt_secret/1 answer (default "").
@spec load!(Path.t()) :: :ok
Loads a generated script's modules into this VM without running it:
:ctl_no_main keeps its last line from calling main/1. Loading the
same path again is a no-op; a script is loaded once per VM, as its
modules are global.
Writes a minimal script at path: its Ctl.Home naming data_dir, the
only part of the file the loaded modules read from disk.