|
| 1 | +# thread safety guide for parallel testing |
| 2 | + |
| 3 | +this document explains how `snap_diff` behaves under rails parallel tests with the `:thread` strategy. |
| 4 | + |
| 5 | +## overview |
| 6 | + |
| 7 | +`snap_diff` is thread safe for parallel test execution as long as global configuration is set before tests run. per thread state is isolated, and shared state is protected where it matters. |
| 8 | + |
| 9 | +## architecture summary |
| 10 | + |
| 11 | +### per-thread assertion registry |
| 12 | + |
| 13 | +each thread gets its own `assertionregistry` stored in thread-local storage: |
| 14 | + |
| 15 | +```ruby |
| 16 | +def registry |
| 17 | + thread.current[:capybara_screenshot_diff_registry] ||= assertionregistry.new |
| 18 | +end |
| 19 | +``` |
| 20 | + |
| 21 | +this prevents cross-thread leakage for assertions and screenshot naming. |
| 22 | + |
| 23 | +### reporters snapshot on notify |
| 24 | + |
| 25 | +reporters are notified using a snapshot protected by a mutex: |
| 26 | + |
| 27 | +```ruby |
| 28 | +def notify_reporters(assertions) |
| 29 | + reporters_snapshot = reporters_mutex.synchronize { reporters.dup } |
| 30 | + reporters_snapshot.each { |reporter| reporter.record(assertions) } |
| 31 | +end |
| 32 | +``` |
| 33 | + |
| 34 | +this ensures a stable list while notifying without forcing a global lock around reporter work. |
| 35 | + |
| 36 | +### html reporter internal lock |
| 37 | + |
| 38 | +the html reporter protects `@failures`, `@total`, and `@finalized` with a mutex so record and finalize can run safely: |
| 39 | + |
| 40 | +```ruby |
| 41 | +@mutex.synchronize do |
| 42 | + return if @finalized |
| 43 | + @total += total |
| 44 | + @failures.concat(failures) |
| 45 | +end |
| 46 | +``` |
| 47 | + |
| 48 | +### screenshot naming isolation |
| 49 | + |
| 50 | +each thread gets its own `screenshotnamer` via the per-thread registry, so counters, sections, and groups do not collide. |
| 51 | + |
| 52 | +### snapshot manager per call |
| 53 | + |
| 54 | +`snapmanager.instance` returns a new instance for each call, avoiding shared mutable state. |
| 55 | + |
| 56 | +## global configuration |
| 57 | + |
| 58 | +configuration uses `mattr_accessor` and should be set once before tests run. do not mutate config during parallel execution. |
| 59 | + |
| 60 | +## parallel test lifecycle |
| 61 | + |
| 62 | +- setup: per-thread registry is created, config is read |
| 63 | +- execution: assertions are added to the thread-local registry |
| 64 | +- teardown: verify and reset operate on the thread-local registry, reporters are notified |
| 65 | +- exit: reporters finalize once per process |
| 66 | + |
| 67 | +## usage examples |
| 68 | + |
| 69 | +```ruby |
| 70 | +parallelize(workers: :number_of_processors, with: :threads) |
| 71 | + |
| 72 | +capybara::screenshot::diff.configure do |screenshot, diff| |
| 73 | + screenshot.window_size = [1280, 1024] |
| 74 | + screenshot.save_path = "doc/screenshots" |
| 75 | + diff.tolerance = 0.001 |
| 76 | +end |
| 77 | +``` |
| 78 | + |
| 79 | +## do and do not |
| 80 | + |
| 81 | +do: |
| 82 | +- set config once in test helper |
| 83 | +- pass per-screenshot options in the call |
| 84 | + |
| 85 | +do not: |
| 86 | +- change global config inside tests |
| 87 | +- manually mutate registry internals |
| 88 | + |
| 89 | +## file system notes |
| 90 | + |
| 91 | +- paths are unique per screenshot name and counter |
| 92 | +- `fileutils.mv` is atomic on most file systems |
| 93 | +- directory creation uses `mkpath` |
0 commit comments