README.md

# ScenarioTracer

Ready-to-use ExUnit and JSON-backed scenario tracing built on top of [ExTracer](../ex_tracer/README.md). ScenarioTracer wires together AST scanning, runtime trace collection, and report generation into a single Mix task abstraction — so you can extract living documentation from your existing test suite with minimal configuration.

## Features

- ExUnit formatter that captures test outcomes and durations at runtime
- JSON file-based trace collector and trace store (no external services required)
- `MixTask` behaviour for building project-specific trace extraction pipelines
- Built-in support for ExUnit and StreamData test blocks
- Produces `ExTracer.Report` structs ready for further processing or export

## Installation

```elixir
def deps do
  [
    {:scenario_tracer, "~> 0.1"},
    {:jason, "~> 1.4"}
  ]
end
```

## Quick Start

### 1. Record runtime traces

Add `ScenarioTracer.ExUnitFormatter` to your ExUnit configuration so test outcomes are captured to disk:

```elixir
# test/test_helper.exs
ExUnit.start()

ExUnit.configure(
  formatters: [ExUnit.CLIFormatter, ScenarioTracer.ExUnitFormatter],
  # Pass trace_dir via ExUnit opts or application config
  trace_dir: Path.join(File.cwd!(), "traces")
)
```

Or start the formatter manually with options:

```elixir
{:ok, _pid} = ScenarioTracer.ExUnitFormatter.start_link(trace_dir: "traces/")
```

After `mix test`, a JSON file per test suite will appear in `traces/`.

### 2. Define a Mix task

Implement `ScenarioTracer.MixTask` to describe how your project's sources should be wired:

```elixir
defmodule Mix.Tasks.Scenarios.Extract do
  use Mix.Task
  @behaviour ScenarioTracer.MixTask

  @impl ScenarioTracer.MixTask
  def project_root, do: File.cwd!()

  @impl ScenarioTracer.MixTask
  def adapters, do: [MyApp.Tracer.ResourceAdapter]

  @impl ScenarioTracer.MixTask
  def frameworks, do: [ScenarioTracer.TestFrameworks.ExUnit]

  @impl ScenarioTracer.MixTask
  def trace_dir(root), do: Path.join(root, "traces")

  @impl ScenarioTracer.MixTask
  def node_source(root) do
    # Return a list of maps describing your application nodes
    MyApp.Tracer.NodeBuilder.build(root)
  end

  @impl ScenarioTracer.MixTask
  def lookup_builder(root, nodes, runtime) do
    %ExTracer.Lookup{
      by_id:   Map.new(nodes, &{&1.id, &1}),
      aliases: MyApp.Tracer.AliasMap.build(root),
      code:    MyApp.Tracer.CodeIndex.build(nodes),
      runtime: runtime
    }
  end

  @impl Mix.Task
  def run(_args) do
    report = ScenarioTracer.MixTask.run(__MODULE__)
    IO.puts("Extracted #{length(report.scenarios)} scenarios")
    IO.puts("Coverage: #{report.coverage.coverage_pct}%")
  end
end
```

Run with:

```shell
mix scenarios.extract
```

## Behaviours

### `ScenarioTracer.MixTask`

| Callback | Returns | Purpose |
|---|---|---|
| `project_root/0` | `String.t()` | Absolute path to project root |
| `adapters/0` | `[module()]` | `ExTracer.Adapter` implementations |
| `frameworks/0` | `[module()]` | `ExTracer.TestFramework` implementations |
| `trace_dir/1` | `String.t()` | Directory containing JSON trace files |
| `node_source/1` | `[map()]` | Application node descriptors |
| `lookup_builder/3` | `ExTracer.Lookup.t()` | Builds the cross-reference index |

### `ScenarioTracer.ExUnitFormatter`

A GenServer that hooks into ExUnit's event stream. Handles:

- `:suite_started` — initializes trace collection state
- `:test_started` — records test start time
- `:test_finished` — records outcome, duration, and any failure message

### `ScenarioTracer.TraceCollector.JsonFile`

Implements `ExTracer.TraceCollector`. Writes one JSON file per test module to `trace_dir`. Each file contains an array of event maps:

```json
[
  {
    "scenario_id": "myapp-accounts-user-creates-a-user",
    "test_name": "creates a user with valid params",
    "outcome": "passed",
    "duration_ms": 42,
    "captured_at": "2026-05-06T10:00:00Z"
  }
]
```

### `ScenarioTracer.TraceStore.JsonFile`

Implements `ExTracer.TraceStore`. Loads all JSON files from `trace_dir` and indexes them by scenario ID. Provides fuzzy test-name matching to handle minor formatting differences between compile-time and runtime names.

## Built-in Test Frameworks

| Module | Recognized macros |
|---|---|
| `ScenarioTracer.TestFrameworks.ExUnit` | `test/2`, `test/3` |
| `ScenarioTracer.TestFrameworks.StreamData` | `property/2`, `property/3` |

## License

MIT