Skip to content

Commit ad46203

Browse files
committed
Update thread safety doc for snap_diff
1 parent cfa66e4 commit ad46203

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

docs/THREAD_SAFETY.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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

Comments
 (0)