Skip to content

Architecture

mkuch edited this page Jul 29, 2026 · 1 revision

Architecture

PoolWatch is divided into collection, target discovery, analysis, and rendering. The core report model is independent of pytest's terminal and file formats.

Data flow

pytest public hooks
        │
        ▼
SessionCollector ──► TestInterval + PhaseInterval
        │
        ├──► target discovery
        │
        ▼
sweep-line analysis ──► PoolWatchReport
                               │
                 ┌─────────────┼─────────────┐
                 ▼             ▼             ▼
              terminal        JSON          HTML

Plugin entry point

The package declares:

[project.entry-points.pytest11]
poolwatch = "pytest_poolwatch.plugin"

Pytest discovers the plugin after installation. pytest_configure registers a per-session runtime only when PoolWatch is enabled and the process is not an xdist worker.

This keeps controller-side state isolated and prevents each xdist worker from writing competing artifacts.

Public hooks

The runtime observes:

  • pytest_sessionstart
  • pytest_collection_finish
  • pytest_xdist_node_collection_finished when available
  • pytest_runtest_logstart
  • pytest_runtest_logreport
  • pytest_runtest_logfinish
  • pytest_sessionfinish
  • pytest_terminal_summary

No private scheduler API is required.

Collection

SessionCollector groups phase reports by worker and node ID. Repeated test protocols become numbered attempts.

For a complete attempt, report phase timestamps are authoritative:

started_at  = min(phase.started_at)
finished_at = max(phase.finished_at)

Lifecycle timestamps are fallback data for an attempt that never emitted phase reports. Such a session-ended interval is marked incomplete.

Outcome precedence is:

  1. any failed phase;
  2. call-phase outcome;
  3. skipped phase;
  4. last observed phase;
  5. unknown.

Target discovery

Explicit configuration always wins. Without it, the discovery layer recognizes:

  • serial pytest and pytest-asyncio;
  • numeric or observed xdist workers;
  • max_asyncio_tasks for standalone pytest-asyncio-cooperative;
  • dynamic asyncio grouping as an observed-peak fallback.

The xdist and cooperative schedulers are not combined. If both plugins are installed, xdist takes precedence because their runtest-loop implementations cannot be used together reliably.

Analysis

The analyzer sorts interval boundaries and performs a sweep-line pass. Its main complexity is O(n log n) for n attempts because of sorting; the sweep itself is linear in the number of unique boundaries.

The result is an immutable PoolWatchReport containing:

  • target metadata;
  • summary metrics;
  • timeline points;
  • merged underfill windows;
  • finalized test attempts.

See Metrics and Diagnosis for formulas.

Rendering

Renderers receive only PoolWatchReport and do not inspect pytest state.

Terminal

Produces compact summary lines plus diagnosis guidance.

JSON

Maps the immutable model into a schema-versioned dictionary and serializes it as UTF-8 JSON.

HTML

Builds a self-contained document with embedded CSS and SVG step charts. Dynamic node IDs and labels are escaped before interpolation.

Both file renderers use a temporary sibling and atomic replacement.

Source layout

src/pytest_poolwatch/
├── analysis.py    # sweep-line metrics and underfill windows
├── collector.py   # pytest report normalization
├── models.py      # immutable report data
├── plugin.py      # pytest hooks and runtime
├── reporting.py   # terminal, JSON, and HTML renderers
├── settings.py    # CLI and ini configuration
└── targets.py     # scheduler capacity discovery

Tests are separated into analysis, collector, reporting, target-discovery, and subprocess integration coverage.

Clone this wiki locally