# `Macaw.Host.Ctl.Recorder`
[🔗](https://github.com/dcoai/macaw_host/blob/v0.1.4/lib/macaw/host/ctl/recorder.ex#L1)

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
    end

`install/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.

# `calls`

```elixir
@spec calls() :: [tuple()]
```

Every call so far, in order: `{exe, args, opts}`, `{:sleep, ms}` or `{:prompt, question}`.

# `docker_calls`

```elixir
@spec docker_calls() :: [[String.t()]]
```

The argv of each docker invocation, in order.

# `home!`

```elixir
@spec home!(Path.t(), String.t()) :: {Path.t(), Path.t()}
```

A host under `tmp_dir`: `home/data/` and a script `script_name` naming
`home` as its data directory. `{script, home}`.

# `install`

```elixir
@spec install(
  [{list() | (String.t(), [String.t()] -&gt; 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 `""`).

# `load!`

```elixir
@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.

# `script!`

```elixir
@spec script!(Path.t(), Path.t() | nil) :: Path.t()
```

Writes a minimal script at `path`: its `Ctl.Home` naming `data_dir`, the
only part of the file the loaded modules read from disk.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
