ScenarioTracer

Ready-to-use ExUnit and JSON-backed scenario tracing built on top of ExTracer. 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

Installation

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:

# 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:

{: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:

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:

mix scenarios.extract

Behaviours

ScenarioTracer.MixTask

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

ScenarioTracer.ExUnitFormatter

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

ScenarioTracer.TraceCollector.JsonFile

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

[
  {
    "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.ExUnittest/2, test/3
ScenarioTracer.TestFrameworks.StreamDataproperty/2, property/3

License

MIT