Skip to content

docs: tell the truth about parallel test execution - #252

Merged
pftg merged 1 commit into
masterfrom
docs/parallel-execution-truth
Aug 24, 2026
Merged

docs: tell the truth about parallel test execution#252
pftg merged 1 commit into
masterfrom
docs/parallel-execution-truth

Conversation

@pftg

@pftg pftg commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Two docs made claims that a measurement contradicts. Since the code fix is deferred to 2.1, documentation truth is the whole 2.0 mitigation, so it has to be right.

What was wrong

docs/reporters.md said that in parallel environments "each worker writes to the same file — the last worker's results will be in the report." That is true only for one-process-per-worker runners. Under Rails' default parallelize(workers: N), which forks, no report is written at all — and the [snap_diff] N screenshots compared … summary line disappears with it.

Mechanism: Minitest skips its after_run hooks in a forked child (Minitest.allow_fork is false by default — minitest 5.27.0 lib/minitest.rb:64, guard at :79), and ::Minitest.after_run is our only finalize trigger (lib/snap_diff/integrations/minitest.rb:69). The four workers hold every record and never finalize; the parent finalizes having recorded nothing.

docs/thread_safety.md was stale on specifics: it claimed SnapManager "returns a new instance for each call" (it has been fiber-memoized since D7, lib/snap_diff/snap_manager.rb:112), documented a registry method now named SnapDiff.session, and never mentioned process parallelism at all — so a reader finished it believing all parallelism was fine.

Evidence

A harness (scratch project, path:-referencing this repo) drove the real matcher → snap manager → git baseline checkout → comparison → registry → reporter path with 8 test classes × 8 tests. The browser was the only stubbed layer.

Mode Runs Result
Serial (control) 8 failures, report written with all 8
with: :threads, distinct names 6/6 8 failures, 0 errors, report complete — same failure set as serial, entries in a different order
workers: 4, distinct names 10/10 8 failures, correct; report=NO every time; all .diff.png artifacts present
Two concurrent processes, 4 failures each report contains 4 of 8 — last writer wins
Colliding names, concurrent 10/10 5–25 loud errors per run plus 18–34 of 64 comparisons silently skipped and passing green

Both documented workarounds were run, not reasoned about:

  • parallelize_teardown { Reporting.finalize! } + per-worker output_path in parallelize_setup → all 8 failures preserved across per-worker files, twice.
  • PARALLEL_WORKERS=1 → single complete report.

The tempting third option — a per-worker save_path — is explicitly called out as wrong: save_path also determines where baselines are read from (SnapDiff.config.screenshot_area), so it would point comparisons at an empty baseline directory.

Scope

Only docs/reporters.md (its parallel section) and docs/thread_safety.md (rewritten). No code, no CHANGELOG, no other docs — the 2.0.0 CHANGELOG entry for this limitation belongs to the release-readiness lane.

Also new in thread_safety.md: the SnapDiff::Vcs git-root memo added by #250 (unsynchronized process-global Hash; idempotent values, but a plain Hash is not a concurrent container on JRuby), and a plain warning that screenshot names must be unique across tests.

standardrb lib test — 151 files, no offenses.

🤖 Generated with Claude Code

https://claude.ai/code/session_014BQJX6eWzBj2UTm5zQsjEs

Summary by Sourcery

Correct the parallel testing documentation so it accurately describes report generation, shared state, screenshot naming requirements, and mitigations for each execution mode.

New Features:

  • Document parallel test execution behavior and supported workarounds for restoring complete HTML reports under forked Rails workers.

Enhancements:

  • Correct and consolidate the parallelism and thread-safety guidance to reflect fiber-local state, process-shared resources, reporter lifecycle behavior, and configuration constraints.
  • Document the risks of colliding screenshot names and the concurrency characteristics of repository-root memoization.

Documentation:

  • Update reporter documentation with accurate outcomes for serial, threaded, forked, and multi-process test execution.
  • Rewrite the thread-safety guide with current APIs, state-sharing guidance, unique naming requirements, and parallel test lifecycle details.

The parallel-execution notes were wrong in a way that mattered: reporters.md
promised a report that Rails' default parallel mode never writes, and
thread_safety.md described internals that had since changed.

Measured with a harness driving the real matcher/registry/reporter path
(4 workers, repeated runs, both parallelization modes):

- `parallelize(workers: N)` — Rails' default — writes NO report at all, and
  prints no summary line. Minitest skips `after_run` hooks in a forked child
  (`Minitest.allow_fork` is false), so the workers that hold the results never
  finalize and the parent that finalizes recorded nothing. reporters.md
  claimed "the last worker's results will be in the report"; there is no
  report. Test results and on-disk artifacts are unaffected.
- `parallelize(with: :threads)` — the JRuby default — is genuinely clean:
  same failures and counts as a serial run.
- One process per worker (parallel_tests, RSpec, CI sharding) is where
  last-writer-wins actually applies.

Both replacement workarounds are verified, not inferred: finalizing from
`parallelize_teardown` with a per-worker `output_path` preserves every
failure, and `PARALLEL_WORKERS=1` restores the single complete report. The
tempting `save_path` variant is called out as wrong — it also relocates the
baselines.

thread_safety.md additionally claimed SnapManager returns a fresh instance per
call (it is fiber-memoized), documented a `registry` method now named
`SnapDiff.session`, and never mentioned processes at all. It now carries the
Vcs git-root memo, and a warning that screenshot names must be unique across
tests: colliding names under concurrency silently skip comparisons — 18 to 34
of 64 assertions per measured run passed green having compared nothing.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @pftg, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 48 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73b724c9-5d1a-4eb2-9b9a-53c01edf7112

📥 Commits

Reviewing files that changed from the base of the PR and between d5b7a9e and b516afd.

📒 Files selected for processing (2)
  • docs/reporters.md
  • docs/thread_safety.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Updates documentation to accurately describe SnapDiff behavior under various parallel test execution modes, especially Rails’ forking parallelization, and adds guidance on safe configuration, shared state, and unique screenshot naming.

Sequence diagram for report finalization under parallel test modes

sequenceDiagram
    participant Suite as Test suite
    participant Worker as Test worker
    participant Reporter as HTML reporter
    participant File as Report file

    alt Serial or thread parallelism
        Suite->>Worker: Run screenshot assertions
        Worker->>Reporter: record(assertions)
        Suite->>Reporter: finalize!
        Reporter->>File: Write complete report
    else Rails forked parallelism
        Suite->>Worker: Fork and run assertions
        Worker->>Reporter: record(assertions)
        Note over Worker: Minitest.allow_fork is false
        Worker--xReporter: after_run is skipped
        Suite->>Reporter: finalize!
        Reporter->>File: Write no report
    else One process per worker
        Suite->>Worker: Run assertions in each process
        Worker->>Reporter: record(assertions)
        Worker->>File: Write report on finalize
        Note over File: Last process to finish overwrites earlier results
    end
Loading

Flow diagram for restoring reports under Rails forked parallelism

flowchart TD
    A["Rails forks test workers"] --> B["parallelize_setup configures a worker-specific output_path"]
    B --> C["Workers run screenshot assertions"]
    C --> D["parallelize_teardown calls SnapDiff::Reporting.finalize!"]
    D --> E["Each worker writes its own HTML report"]
    C --> F["Set PARALLEL_WORKERS=1 instead"]
    F --> G["One process writes one complete report"]
Loading

File-Level Changes

Change Details Files
Clarified reporter behavior and failure/report handling under different parallel execution strategies, and documented mitigations for Rails’ forked parallelism.
  • Replaced single-note description of parallel reporting with a dedicated "Parallel test runs" section explaining how finalize hooks interact with different runners.
  • Added a mode table summarizing when HTML reports are written or lost under serial, threaded, forked, and multi-process/sharded execution.
  • Documented that Rails’ parallelize(workers: N) prevents the HTML report and summary line from being written but preserves all image artifacts and test failures.
  • Added guidance for restoring reports under forked parallelism using parallelize_setup/parallelize_teardown and per-worker output_path reporters.
  • Explicitly warned against using per-worker save_path, explaining that it also controls baseline read locations and would cause all screenshots to be treated as new.
  • Linked to the thread/parallel safety doc for deeper explanation of shared state across modes.
docs/reporters.md
Rewrote the thread safety guide into a broader parallel and thread-safety document that details shared vs isolated state, configuration rules, and screenshot naming requirements across concurrency modes.
  • Replaced the old thread-only focus with a summarized matrix of suite modes (serial, threaded, forked, multi-process) vs correctness and HTML report behavior.
  • Documented two core rules for safe parallel usage: configuration must be set once per process before tests run, and screenshot names must be globally unique.
  • Added a state table describing each important mutable structure (config, session, SnapManager, reporters, HTML totals, deprecation memo, VCS memo, screenshot files), its scope (per process/fiber), and its concurrency protection.
  • Introduced a detailed section explaining how non-unique screenshot names cause dangerous baseline file collisions, including silent comparison skipping in parallel runs, and recommended naming strategies.
  • Clarified configuration best practices, including setting global config only at startup and using per-assertion options instead of mutating shared config mid-run.
  • Outlined the per-fiber test lifecycle (setup, execution, teardown, end-of-suite) and how framework end-of-suite hooks finalize reporters, noting forked-worker exceptions and cross-references to the reporter doc.
docs/thread_safety.md

Possibly linked issues

  • #unknown: The PR directly documents several open decisions and thread-safety behaviors identified by the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@pftg
pftg merged commit c7fa2a6 into master Aug 24, 2026
2 checks passed
@pftg
pftg deleted the docs/parallel-execution-truth branch August 24, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant