Architecture

SpecTracer is a lean Python CLI with no heavy infrastructure. Four modules orchestrate a simple pipeline: collect → parse → link → aggregate → render.

Pipeline Overview

  .feature                JUnit XML             Cucumber JSON
     |                        |                       |
     v                        v                       v
  FeatureParser          JunitParser           CucumberParser
     |                        |                       |
     +-----------─────+─────────+
                 |                       |
              Scenario[]           TestResult[]
                 |                       |
                 +--------> Linker <--------+
                               |
                     LinkedScenarioView[]
                               |
                          Aggregator
                               |
                     AggregatedData{views, stats, health}
                               |
                          HtmlRenderer
                               |
                          report.html

Modules

📄

models.py

Core data classes: Scenario, TestResult, RequiredLayer, ScenarioView. 40 lines — the entire type system.

🔍

parsers.py

Three parsers: Gherkin (line-by-line), JUnit XML (ElementTree), Cucumber JSON (json module). Total: 177 lines.

🔗

linker.py

OR-based tag matching. Links scenarios to test results by shared tags. 17 lines — the simplest module.

📊

aggregator.py

Computes coverage stats, layer breakdowns, feature trees, health checks, and unlinked results. 188 lines of pure logic.

💻

renderers.py

Jinja2 template with 845 lines of embedded HTML/CSS/JS. Produces the self-contained report file.

cli.py

Orchestrator: loads config, collects files, runs pipeline, writes output. 131 lines.

Parsers

FeatureParser

Reads .feature files line-by-line. No heavy Gherkin parser dependency — just enough to extract:

  • Feature: lines for the feature name
  • Scenario names (line following Scenario:)
  • Tags on lines above scenarios (distinguishes linking tags from @require-* tags)
  • Given/When/Then steps for display in the report

Default behavior: Scenarios without any @require-* tags default to @require-e2e.

JunitParser

Parses JUnit XML using xml.etree.ElementTree. Reads tags from three locations to support different frameworks:

  • name attribute on <testcase>
  • classname attribute on <testcase>
  • <properties><property> elements inside <testcase>

Handles <failure>, <error>, and <skipped> child elements for status classification.

CucumberParser

Parses Cucumber JSON output. Handles both array-of-scenarios and single-object formats. Extracts:

  • Scenario-level tags array for linking
  • Scenario name and status
  • Duration (handles nanosecond-to-second conversion)

Linker

The linker is the simplest but most important module. It performs OR-based tag matching:

Linking logic (abridged)
def link(scenarios, results):
    for scenario in scenarios:
        linked = [
            r for r in results
            if any(tag in r.tags for tag in scenario.linking_tags)
        ]
        yield ScenarioView(scenario, linked)

Aggregator

The ReportAggregator transforms linked data into report views. All methods are static:

MethodOutput
build_views()Per-scenario, per-layer result groups with required-layer status
coverage_stats()Total tested/untested counts with percentage
feature_breakdown()Per-feature coverage stats
layer_stats()Per-layer test counts, durations, percentages, visual widths
failure_breakdown()Failures grouped by feature/scenario with stack traces
health_checks()All four health check statuses (progress, pyramid, E2E time, unlinked)
unlinked_results()Test results with no matching scenario

Renderer

The HtmlRenderer uses Jinja2 to produce a single self-contained HTML file. Everything is inlined:

The report file has zero external dependencies — it works offline, on any OS, in any modern browser.

Data Flow

Here's exactly what happens when you run uv run python build_pyramid.py:

  1. Config loadingcli.py discovers and reads the JSON config file. Validates required keys (features, output).
  2. File collectionFileCollector resolves glob patterns and directory recursion for feature files, JUnit XML, and Cucumber JSON paths.
  3. Parsing — Each file type is parsed by its dedicated parser. Scenarios and test results are extracted into typed data classes.
  4. LinkingResultLinker cross-references scenario tags against test result tags. OR logic: any shared tag creates a link.
  5. AggregationReportAggregator computes all stats, views, breakdowns, and health checks from the linked data.
  6. RenderingHtmlRenderer feeds the aggregated data into a Jinja2 template and writes the output file.

Tag Resolution Order

When parsing a .feature file, tags are resolved as follows:

  1. All tags on lines immediately above a Scenario: line are collected
  2. Tags matching the pattern @require-(unit|integration|e2e)(:\w+)? are classified as layer requirements
  3. All remaining tags become linking tags
  4. If zero requirement tags were found, the scenario defaults to @require-e2e
ℹ Feature-level tags are not inherited Tags placed on the Feature: line are ignored by SpecTracer. Only scenario-level tags participate in linking and requirement checking.

Testing Strategy (Dogfooding)

SpecTracer tests itself using its own input formats — an approach called outside-in dogfooding.

LayerFrameworkOutput FormatFeeds Tool As
E2EbehaveCucumber JSONe2e
UnitpytestJUnit XMLunit
IntegrationpytestJUnit XMLintegration

The CI pipeline runs all three layers, collects the outputs, and feeds them into SpecTracer to generate a self-report — a coverage report for SpecTracer itself. Every feature starts as a .feature file, validated by a behave E2E scenario before implementation.

Dogfooding CI pipeline
uv run behave features/ --format json -o reports/e2e.json
uv run pytest tests/unit --junitxml=reports/unit.xml
uv run pytest tests/integration --junitxml=reports/int.xml

uv run python build_pyramid.py
# Output: reports/spectracer-report.html — a self-report
✓ What gets measured gets managed SpecTracer's own self-report is the team's quality dashboard. If the tool's coverage drops, we fix it before shipping new features.