rspec-tracer 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +125 -0
- data/lib/rspec_tracer/example.rb +110 -6
- data/lib/rspec_tracer/remote_cache/cache.rb +2 -0
- data/lib/rspec_tracer/version.rb +1 -1
- data/lib/rspec_tracer.rb +121 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a5c7bccaf770c4b630f3aafbe2965e7a88eebf912d4e5f331eb6001613e0559
|
|
4
|
+
data.tar.gz: cd2c8bb4ea28fa5c70807e8ca7e9249e16a7313f9e17662e3b76d20cc26a91ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a5889f5a017183e40e63b56c10b6abf2309dff7b18ca941c2f36552e947b508ca3ac144290143a44b2bf78c7f76569eba96d1d580f69eb9a2d2a138e3c004ba
|
|
7
|
+
data.tar.gz: 79ff93be56d9f9bee3c397b0d78ef0cb4114b5c746879d9afcbf6372d2eb55a8950b75f772372c78aaea3852a12a0750a1098758cf343c41c9caa86b485ac054
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
+
## [1.2.4] - 2026-05-17
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **`example_id` was unstable across runs — a long-standing bug since
|
|
6
|
+
v1.0.0.** `RSpecTracer::Example.from` built the MD5 identity hash
|
|
7
|
+
over a payload that included `example_group.name` (RSpec's
|
|
8
|
+
generated class name, which carries a load-order-dependent `_2` /
|
|
9
|
+
`_3` suffix when two spec files share a `describe` name) and the
|
|
10
|
+
example's `line_number`. The same example got a different id
|
|
11
|
+
depending on rspec's file load order — silently thrashing the
|
|
12
|
+
cache and breaking the always-re-run guarantees for failed,
|
|
13
|
+
pending, and flaky examples. A no-op blank-line edit above the
|
|
14
|
+
example flipped the id with the same effect.
|
|
15
|
+
|
|
16
|
+
The digest now uses a stable subset: the describe block's
|
|
17
|
+
*description* string (the user-supplied text, not RSpec's
|
|
18
|
+
generated class name), the example's `description`,
|
|
19
|
+
`full_description`, `shared_group` inclusion locations with the
|
|
20
|
+
trailing `:LINE` stripped, and `file_name`. `line_number` /
|
|
21
|
+
`rerun_file_name` / `rerun_line_number` still ride along in the
|
|
22
|
+
returned Hash for the HTML and JSON reporters' location columns,
|
|
23
|
+
but no longer enter the digest. Contract: **rename = new identity;
|
|
24
|
+
restructure = same identity.**
|
|
25
|
+
|
|
26
|
+
Unnamed examples (`it { }`, `specify { }`, `example { }`) needed
|
|
27
|
+
an extra step. RSpec's pre-run `description` for an unnamed
|
|
28
|
+
example is the line-bearing `"example at <path>:<line>"`
|
|
29
|
+
fallback, which would have re-leaked the line number straight
|
|
30
|
+
into the digest. The digest substitutes a line-independent
|
|
31
|
+
positional discriminator: the example's ordinal among the
|
|
32
|
+
*unnamed* examples of its group. Stability is preserved across
|
|
33
|
+
blank-line edits and across adding or renaming *named* siblings;
|
|
34
|
+
reordering the unnamed examples re-keys them (the documented
|
|
35
|
+
trade-off — give the example an explicit description for a fully
|
|
36
|
+
reorder-stable id).
|
|
37
|
+
|
|
38
|
+
**Upgrade note**: the cache file format is unchanged, so a v1.2.3
|
|
39
|
+
cache loads cleanly through v1.2.4 code. But every cached
|
|
40
|
+
`example_id` was computed against the old payload and the new
|
|
41
|
+
lookups will not match them, so the first run after upgrade
|
|
42
|
+
treats every example as `:no_cache` and re-runs the full suite.
|
|
43
|
+
Warm caches resume from the second run onward.
|
|
44
|
+
|
|
45
|
+
Surfaced by 2.0.0.pre.1 field testing against third-party Rails
|
|
46
|
+
apps. Fixed upstream in
|
|
47
|
+
[#209](https://github.com/avmnu-sng/rspec-tracer/pull/209) and
|
|
48
|
+
[#211](https://github.com/avmnu-sng/rspec-tracer/pull/211).
|
|
49
|
+
|
|
50
|
+
- **`RSpecTracer.start` crashed when the user pre-started
|
|
51
|
+
`::Coverage`.** Users opting into branch coverage typically call
|
|
52
|
+
`Coverage.start(lines: true, branches: true)` before loading
|
|
53
|
+
rspec-tracer. The tracer's `setup_coverage` then called bare
|
|
54
|
+
`::Coverage.start` with no `running?` guard, raising
|
|
55
|
+
`RuntimeError: coverage measurement is already setup` and taking
|
|
56
|
+
the tracer down at boot.
|
|
57
|
+
|
|
58
|
+
Add a `Coverage.running?` predicate guard before the start call,
|
|
59
|
+
plus a defensive `RuntimeError` rescue with a `logger.warn` for
|
|
60
|
+
the case where the predicate returns false but `start` still
|
|
61
|
+
raises. Graceful degradation: coverage measurement is skipped
|
|
62
|
+
with a visible warn, the rest of the tracer pipeline continues.
|
|
63
|
+
|
|
64
|
+
Partial port of upstream
|
|
65
|
+
[#207](https://github.com/avmnu-sng/rspec-tracer/pull/207); the
|
|
66
|
+
`coverage_modes` config DSL half is 2.0-only.
|
|
67
|
+
|
|
68
|
+
- **`remote_cache` success paths were silent at default log level.**
|
|
69
|
+
`RemoteCache::Cache#download` and `#upload` returned without
|
|
70
|
+
emitting anything after the underlying AWS calls succeeded, so a
|
|
71
|
+
successful `rake rspec_tracer:remote_cache:download` produced
|
|
72
|
+
zero output. Users couldn't tell from CI logs whether the cache
|
|
73
|
+
was actually restored or whether the run was cold.
|
|
74
|
+
|
|
75
|
+
Emit a single `RSpecTracer.logger.info` line on each operation's
|
|
76
|
+
success path: `"rspec-tracer remote_cache: restored cache from
|
|
77
|
+
<sha>"` on download, `"rspec-tracer remote_cache: uploaded cache
|
|
78
|
+
to <ref>"` on upload.
|
|
79
|
+
|
|
80
|
+
Partial port of upstream
|
|
81
|
+
[#201](https://github.com/avmnu-sng/rspec-tracer/pull/201); the
|
|
82
|
+
cross-branch-fallback qualifier is specific to 2.0's multi-tier
|
|
83
|
+
cache model and not applicable here.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **`rspec-tracer.gemspec` now declares `rubygems_mfa_required`** so
|
|
88
|
+
RubyGems enforces MFA on every publish. Packaging metadata only;
|
|
89
|
+
no behaviour change for users of the gem. Mirrors upstream
|
|
90
|
+
[#214](https://github.com/avmnu-sng/rspec-tracer/pull/214).
|
|
91
|
+
|
|
92
|
+
- **`.github/workflows/release.yml` tag-trigger pattern tightened
|
|
93
|
+
to the strict `'v[0-9]+.[0-9]+.[0-9]+'` form** (1.x is
|
|
94
|
+
final-release only). The previous loose `'v*.*.*'` form would
|
|
95
|
+
have matched 4-segment shapes like `v2.0.0.rc.1` accidentally
|
|
96
|
+
because `*` greedy-matches dots; the strict per-segment digit
|
|
97
|
+
class rejects any non-digit. Documentation block above the
|
|
98
|
+
pattern explains the GHA filter-pattern flavor for future
|
|
99
|
+
maintainers. Workflow-only; no user-visible change.
|
|
100
|
+
|
|
101
|
+
## [1.2.3] - 2026-05-06
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- **Parallel-tests purge race when a sibling worker is still mid-flush**
|
|
106
|
+
— the elected worker trusted only `parallel_tests`'s pid-file barrier
|
|
107
|
+
(`ParallelTests.wait_for_other_processes_to_finish`), which under
|
|
108
|
+
specific scheduling/I/O timing on GHA Linux x86_64 can return while a
|
|
109
|
+
sibling's `parallel_tests_N/` dir hasn't fully flushed. The elected
|
|
110
|
+
then merged + purged, racing the in-progress sibling. Symptoms:
|
|
111
|
+
intermittent leftover `parallel_tests_N/` dir post-purge AND/OR
|
|
112
|
+
silently dropped peer caches in the merge.
|
|
113
|
+
|
|
114
|
+
Backport of upstream PR
|
|
115
|
+
[#168](https://github.com/avmnu-sng/rspec-tracer/pull/168). Adds a
|
|
116
|
+
filesystem barrier layered on top of the pid-file wait. Each worker
|
|
117
|
+
writes a `.rspec_tracer_boot` marker at `RSpecTracer.start` time and
|
|
118
|
+
a `.rspec_tracer_done` marker as the first step of its at_exit tasks;
|
|
119
|
+
the elected worker waits for every booted peer's `.done` to
|
|
120
|
+
materialize before proceeding to merge + purge. Two independent
|
|
121
|
+
signals (pid file + filesystem) must agree before the elected worker
|
|
122
|
+
declares the peer set stable. Bounded at 5 s with a graceful warn for
|
|
123
|
+
crashed peers — their dirs are purged regardless of completion state,
|
|
124
|
+
and the merge accepts whatever's on disk.
|
|
125
|
+
|
|
1
126
|
## [1.2.2] - 2026-05-04
|
|
2
127
|
|
|
3
128
|
### Fixed
|
data/lib/rspec_tracer/example.rb
CHANGED
|
@@ -1,19 +1,122 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpecTracer
|
|
4
|
+
# Builds the identity-hash payload (`:example_id`-keyed Hash) that
|
|
5
|
+
# RSpec::RunnerHook attaches to every example pre-run.
|
|
6
|
+
#
|
|
7
|
+
# == Identity stability contract
|
|
8
|
+
#
|
|
9
|
+
# `example_id` is the MD5 of a stable subset of the payload:
|
|
10
|
+
# `example_group` (the describe block's *description* string),
|
|
11
|
+
# `description`, `full_description`, `shared_group` (inclusion
|
|
12
|
+
# locations with the trailing line number stripped), and
|
|
13
|
+
# `file_name`. The contract, in one line: *rename = new identity;
|
|
14
|
+
# restructure = same identity.*
|
|
15
|
+
#
|
|
16
|
+
# Identity is PRESERVED when:
|
|
17
|
+
# - blank lines or comments are added/removed around the example
|
|
18
|
+
# - examples are reordered within a describe block (named
|
|
19
|
+
# examples only - see "Unnamed examples" below)
|
|
20
|
+
# - a sibling describe / example in the same file is renamed
|
|
21
|
+
# - the example body or its hooks (`before`, `let`) are edited -
|
|
22
|
+
# the file digest still triggers the re-run
|
|
23
|
+
#
|
|
24
|
+
# Identity CHANGES (one cold "No cache" run) when:
|
|
25
|
+
# - the file is renamed or moved
|
|
26
|
+
# - the `describe` / `it` / shared-example name is changed
|
|
27
|
+
# - the example moves to a different describe block
|
|
28
|
+
#
|
|
29
|
+
# == Unnamed examples (`it { }`, `specify { }`, `example { }`)
|
|
30
|
+
#
|
|
31
|
+
# An example with no description string has no stable name to hash,
|
|
32
|
+
# and identity must be computed pre-run (for the filter decision),
|
|
33
|
+
# before RSpec generates a matcher-derived description. The only
|
|
34
|
+
# line-independent signal RSpec exposes pre-run is position, so an
|
|
35
|
+
# unnamed example's identity is derived from its ordinal among the
|
|
36
|
+
# *unnamed* examples of its group. (RSpec's `description` for an
|
|
37
|
+
# unnamed example is the line-bearing `"example at <path>:<line>"`
|
|
38
|
+
# fallback, which would otherwise leak the line number straight
|
|
39
|
+
# back into the digest.)
|
|
40
|
+
#
|
|
41
|
+
# For unnamed examples the contract above is amended: identity is
|
|
42
|
+
# still PRESERVED across blank-line / comment edits, sibling
|
|
43
|
+
# renames, and adding or removing *named* siblings - but it CHANGES
|
|
44
|
+
# when the unnamed examples are reordered, or one is inserted or
|
|
45
|
+
# removed ahead of it. Give an example an explicit description
|
|
46
|
+
# (`it 'does X' do`) for a fully reorder-stable identity.
|
|
47
|
+
#
|
|
48
|
+
# `line_number` / `rerun_file_name` / `rerun_line_number` stay in
|
|
49
|
+
# the returned Hash for the reporter location columns, but are
|
|
50
|
+
# DELIBERATELY EXCLUDED from the digest - a no-op edit that shifts
|
|
51
|
+
# line numbers must not invalidate the cache. `example_group` uses
|
|
52
|
+
# `example_group.description` (the user's string) rather than
|
|
53
|
+
# `example_group.name`: RSpec's generated class name carries a
|
|
54
|
+
# load-order-dependent `_2` / `_3` suffix when two files share a
|
|
55
|
+
# describe name, which would otherwise flip the id across runs.
|
|
4
56
|
module Example
|
|
5
57
|
module_function
|
|
6
58
|
|
|
59
|
+
# Identity-keyed cache of `<example_group> => Array<unnamed sibling>`
|
|
60
|
+
# populated lazily by `unnamed_description`. A group with N unnamed
|
|
61
|
+
# examples computes the sibling list once per group rather than N
|
|
62
|
+
# times. Memory is bounded by the live example_group set, which
|
|
63
|
+
# RSpec retains for the run lifetime anyway.
|
|
64
|
+
@unnamed_siblings_cache = {}.compare_by_identity
|
|
65
|
+
|
|
7
66
|
def from(example)
|
|
8
|
-
|
|
9
|
-
|
|
67
|
+
location = example_location(example)
|
|
68
|
+
identity = {
|
|
69
|
+
example_group: example.example_group.description,
|
|
10
70
|
description: example.description,
|
|
11
71
|
full_description: example.full_description,
|
|
12
72
|
shared_group: example.metadata[:shared_group_inclusion_backtrace]
|
|
13
|
-
.map(
|
|
14
|
-
|
|
73
|
+
.map { |frame| frame.formatted_inclusion_location.sub(/:\d+\z/, '') },
|
|
74
|
+
file_name: location[:file_name]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
identity
|
|
78
|
+
.merge(location)
|
|
79
|
+
.merge(example_id: Digest::MD5.hexdigest(digest_identity(example, identity).to_json))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The Hash actually fed to the MD5. For a named example this is
|
|
83
|
+
# `identity` unchanged. For an unnamed example RSpec's
|
|
84
|
+
# `description` is the line-bearing `"example at <path>:<line>"`
|
|
85
|
+
# fallback, so `description` is swapped for a line-independent
|
|
86
|
+
# positional discriminator before hashing. The returned/stored
|
|
87
|
+
# payload still carries RSpec's `description` / `full_description`
|
|
88
|
+
# untouched - only the digest input differs.
|
|
89
|
+
def digest_identity(example, identity)
|
|
90
|
+
return identity unless unnamed?(example)
|
|
91
|
+
|
|
92
|
+
identity.merge(description: unnamed_description(example))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# True when the example has no explicit description string. RSpec's
|
|
96
|
+
# raw `metadata[:description]` is `""` for `it { }` / `specify { }`
|
|
97
|
+
# / `example { }`; the `description` *method* would instead return
|
|
98
|
+
# the line-bearing fallback, so the raw metadata value is what
|
|
99
|
+
# cleanly tells named from unnamed.
|
|
100
|
+
def unnamed?(example)
|
|
101
|
+
example.metadata[:description].to_s.strip.empty?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Line-independent identity discriminator for an unnamed example:
|
|
105
|
+
# its 0-based ordinal among the *unnamed* examples of its group.
|
|
106
|
+
# Stable across blank-line edits and across adding or renaming
|
|
107
|
+
# *named* siblings; changes only when the unnamed examples are
|
|
108
|
+
# reordered or one is inserted/removed ahead of it. The string
|
|
109
|
+
# takes a Ruby-inspect-style `#<...>` form so that even if a user
|
|
110
|
+
# description literally matched, full_description would still
|
|
111
|
+
# differ between the unnamed example (`"<group> "`) and the named
|
|
112
|
+
# one (`"<group> #<...>"`), keeping digest collisions structurally
|
|
113
|
+
# impossible.
|
|
114
|
+
def unnamed_description(example)
|
|
115
|
+
group = example.example_group
|
|
116
|
+
unnamed_siblings = @unnamed_siblings_cache[group] ||=
|
|
117
|
+
group.examples.select { |sibling| unnamed?(sibling) }
|
|
15
118
|
|
|
16
|
-
|
|
119
|
+
"#<rspec-tracer unnamed example #{unnamed_siblings.index(example)}>"
|
|
17
120
|
end
|
|
18
121
|
|
|
19
122
|
def example_location(example)
|
|
@@ -53,6 +156,7 @@ module RSpecTracer
|
|
|
53
156
|
RSpecTracer::SourceFile.file_name(file_path)
|
|
54
157
|
end
|
|
55
158
|
|
|
56
|
-
private_class_method :
|
|
159
|
+
private_class_method :digest_identity, :unnamed?, :unnamed_description,
|
|
160
|
+
:example_location, :example_rerun_location, :location_file_name
|
|
57
161
|
end
|
|
58
162
|
end
|
|
@@ -19,11 +19,13 @@ module RSpecTracer
|
|
|
19
19
|
|
|
20
20
|
@aws.download_file(@cache_sha, 'last_run.json')
|
|
21
21
|
@aws.download_dir(@cache_sha, last_run_id)
|
|
22
|
+
RSpecTracer.logger.info "rspec-tracer remote_cache: restored cache from #{@cache_sha}"
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def upload
|
|
25
26
|
@aws.upload_file(@repo.branch_ref, 'last_run.json')
|
|
26
27
|
@aws.upload_dir(@repo.branch_ref, last_run_id)
|
|
28
|
+
RSpecTracer.logger.info "rspec-tracer remote_cache: uploaded cache to #{@repo.branch_ref}"
|
|
27
29
|
|
|
28
30
|
file_name = File.join(RSpecTracer.cache_path, 'branch_refs.json')
|
|
29
31
|
|
data/lib/rspec_tracer/version.rb
CHANGED
data/lib/rspec_tracer.rb
CHANGED
|
@@ -30,6 +30,17 @@ require_relative 'rspec_tracer/time_formatter'
|
|
|
30
30
|
require_relative 'rspec_tracer/version'
|
|
31
31
|
|
|
32
32
|
module RSpecTracer
|
|
33
|
+
# Filesystem barrier markers, layered on top of parallel_tests's
|
|
34
|
+
# pid-file wait to defend against the GHA-observed race where the
|
|
35
|
+
# gem's `wait_for_other_processes_to_finish` returns while a sibling
|
|
36
|
+
# worker hasn't fully flushed its `parallel_tests_N/` dir yet. Each
|
|
37
|
+
# worker writes BOOT at setup-time and DONE as the first step of its
|
|
38
|
+
# at_exit tasks; the elected worker waits for every booted peer's
|
|
39
|
+
# DONE marker (deadline-bounded) before proceeding to merge + purge.
|
|
40
|
+
PARALLEL_TESTS_BOOT_MARKER_FILENAME = '.rspec_tracer_boot'
|
|
41
|
+
PARALLEL_TESTS_DONE_MARKER_FILENAME = '.rspec_tracer_done'
|
|
42
|
+
PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS = 5
|
|
43
|
+
|
|
33
44
|
class << self
|
|
34
45
|
attr_accessor :running, :pid, :no_examples, :duplicate_examples
|
|
35
46
|
|
|
@@ -186,6 +197,29 @@ module RSpecTracer
|
|
|
186
197
|
RSpecTracer.logger.error "Failed to load parallel tests (Error: #{e.message})"
|
|
187
198
|
ensure
|
|
188
199
|
track_parallel_tests_test_env_number
|
|
200
|
+
parallel_tests_touch_boot!
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Per-worker boot marker. Source-of-truth for "this worker booted
|
|
204
|
+
# past `RSpecTracer.start`", consumed by the elected worker's
|
|
205
|
+
# finalize-time peer enumeration. Idempotent; failures are warned
|
|
206
|
+
# and absorbed (boot-marker write must never block test execution).
|
|
207
|
+
def parallel_tests_touch_boot!
|
|
208
|
+
return unless parallel_tests?
|
|
209
|
+
|
|
210
|
+
FileUtils.mkdir_p(RSpecTracer.cache_path)
|
|
211
|
+
File.write(
|
|
212
|
+
File.join(RSpecTracer.cache_path, PARALLEL_TESTS_BOOT_MARKER_FILENAME),
|
|
213
|
+
JSON.generate(
|
|
214
|
+
pid: Process.pid,
|
|
215
|
+
test_env_number: ENV.fetch('TEST_ENV_NUMBER', ''),
|
|
216
|
+
started_at: Time.now.utc.iso8601
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
rescue StandardError => e
|
|
220
|
+
RSpecTracer.logger.warn(
|
|
221
|
+
"RSpec tracer: failed to write boot marker (#{e.class}: #{e.message})"
|
|
222
|
+
)
|
|
189
223
|
end
|
|
190
224
|
|
|
191
225
|
def track_parallel_tests_test_env_number
|
|
@@ -225,8 +259,11 @@ module RSpecTracer
|
|
|
225
259
|
return if simplecov?
|
|
226
260
|
|
|
227
261
|
require 'coverage'
|
|
262
|
+
return if ::Coverage.running?
|
|
228
263
|
|
|
229
264
|
::Coverage.start
|
|
265
|
+
rescue RuntimeError => e
|
|
266
|
+
RSpecTracer.logger.warn "coverage measurement setup skipped: #{e.message}"
|
|
230
267
|
end
|
|
231
268
|
|
|
232
269
|
def setup_trace_point
|
|
@@ -315,6 +352,15 @@ module RSpecTracer
|
|
|
315
352
|
end
|
|
316
353
|
|
|
317
354
|
def run_parallel_tests_exit_tasks
|
|
355
|
+
# Every worker — elected or not — drops its `.done` marker as the
|
|
356
|
+
# first thing in finalize so the elected worker's
|
|
357
|
+
# `parallel_tests_wait_for_peer_done_markers!` can observe it.
|
|
358
|
+
# Non-elected workers stop here; the elected worker proceeds to
|
|
359
|
+
# the merge + purge sequence (gated by `parallel_tests_executed?`,
|
|
360
|
+
# which now layers the peer-done barrier on top of the existing
|
|
361
|
+
# pid-file wait).
|
|
362
|
+
parallel_tests_touch_done!
|
|
363
|
+
|
|
318
364
|
return unless parallel_tests_executed?
|
|
319
365
|
|
|
320
366
|
merge_parallel_tests_reports
|
|
@@ -324,6 +370,26 @@ module RSpecTracer
|
|
|
324
370
|
purge_parallel_tests_reports
|
|
325
371
|
end
|
|
326
372
|
|
|
373
|
+
# Per-worker done marker. Written by every worker (elected or not)
|
|
374
|
+
# as the first step of `run_parallel_tests_exit_tasks`. Pairs with
|
|
375
|
+
# the boot marker for the elected worker's peer-done barrier:
|
|
376
|
+
# presence of `.done` means "this worker has signalled completion
|
|
377
|
+
# of its own writes"; absence (with `.boot` present) means "still
|
|
378
|
+
# mid-flush or crashed". Idempotent; failures are warned + absorbed.
|
|
379
|
+
def parallel_tests_touch_done!
|
|
380
|
+
return unless parallel_tests?
|
|
381
|
+
|
|
382
|
+
FileUtils.mkdir_p(RSpecTracer.cache_path)
|
|
383
|
+
File.write(
|
|
384
|
+
File.join(RSpecTracer.cache_path, PARALLEL_TESTS_DONE_MARKER_FILENAME),
|
|
385
|
+
Time.now.utc.iso8601
|
|
386
|
+
)
|
|
387
|
+
rescue StandardError => e
|
|
388
|
+
RSpecTracer.logger.warn(
|
|
389
|
+
"RSpec tracer: failed to write done marker (#{e.class}: #{e.message})"
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
327
393
|
def merge_parallel_tests_reports
|
|
328
394
|
return unless parallel_tests_executed?
|
|
329
395
|
|
|
@@ -423,9 +489,64 @@ module RSpecTracer
|
|
|
423
489
|
|
|
424
490
|
ParallelTests.wait_for_other_processes_to_finish
|
|
425
491
|
|
|
492
|
+
# Belt-and-suspenders barrier: pid-file said everyone's done, but
|
|
493
|
+
# the gem's `wait_for_other_processes_to_finish` has been observed
|
|
494
|
+
# on GHA Linux x86_64 to return while a sibling's `parallel_tests_N/`
|
|
495
|
+
# is still mid-flush. Cross-check via the `.boot`/`.done` filesystem
|
|
496
|
+
# markers before declaring the peer set stable. Idempotent: once
|
|
497
|
+
# all peers have flushed, subsequent calls just glob, find nothing
|
|
498
|
+
# missing, and return.
|
|
499
|
+
parallel_tests_wait_for_peer_done_markers!
|
|
500
|
+
|
|
426
501
|
true
|
|
427
502
|
end
|
|
428
503
|
|
|
504
|
+
# Block until every peer that wrote `.boot` has also written `.done`,
|
|
505
|
+
# or the deadline elapses. Polled at 50ms — fine enough to close the
|
|
506
|
+
# typical "barrier returned a tick early" case within a poll or two,
|
|
507
|
+
# coarse enough not to dominate CPU.
|
|
508
|
+
#
|
|
509
|
+
# On timeout we log a warn and proceed: a peer that never wrote
|
|
510
|
+
# `.done` either crashed (then its dir is orphan content; the
|
|
511
|
+
# subsequent purge cleans it) or is genuinely hung (the elected
|
|
512
|
+
# can't fix that — we choose merge correctness over indefinite wait).
|
|
513
|
+
def parallel_tests_wait_for_peer_done_markers!
|
|
514
|
+
base_dir = File.dirname(RSpecTracer.cache_path)
|
|
515
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS
|
|
516
|
+
|
|
517
|
+
loop do
|
|
518
|
+
missing = parallel_tests_peer_dirs_missing_done(base_dir)
|
|
519
|
+
return if missing.empty?
|
|
520
|
+
|
|
521
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
522
|
+
RSpecTracer.logger.warn(
|
|
523
|
+
'RSpec tracer: peers booted without finishing within ' \
|
|
524
|
+
"#{PARALLEL_TESTS_PEER_DONE_DEADLINE_SECONDS}s: #{missing.inspect}; " \
|
|
525
|
+
'proceeding (peer dirs will be purged regardless of completion state)'
|
|
526
|
+
)
|
|
527
|
+
return
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
sleep 0.05
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Set difference of `.boot`-bearing peer dirs and `.done`-bearing
|
|
535
|
+
# peer dirs under `base_dir`. A returned entry means "this peer
|
|
536
|
+
# registered but has not signalled completion yet" — either still
|
|
537
|
+
# mid-flush or crashed.
|
|
538
|
+
def parallel_tests_peer_dirs_missing_done(base_dir)
|
|
539
|
+
boot_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_BOOT_MARKER_FILENAME)
|
|
540
|
+
done_dirs = parallel_tests_peer_dirs_with_marker(base_dir, PARALLEL_TESTS_DONE_MARKER_FILENAME)
|
|
541
|
+
boot_dirs - done_dirs
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def parallel_tests_peer_dirs_with_marker(base_dir, marker_filename)
|
|
545
|
+
Dir.glob(File.join(base_dir, 'parallel_tests_*', marker_filename)).map do |path|
|
|
546
|
+
File.dirname(path)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
429
550
|
# Elects the worker that performs the per-run merge. Delegates to
|
|
430
551
|
# `::ParallelTests.first_process?`, which returns true iff
|
|
431
552
|
# `TEST_ENV_NUMBER.to_i <= 1` — i.e. for exactly one worker
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-tracer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abhimanyu Singh
|
|
@@ -111,9 +111,10 @@ licenses:
|
|
|
111
111
|
- MIT
|
|
112
112
|
metadata:
|
|
113
113
|
homepage_uri: https://github.com/avmnu-sng/rspec-tracer
|
|
114
|
-
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.2.
|
|
114
|
+
source_code_uri: https://github.com/avmnu-sng/rspec-tracer/tree/v1.2.4
|
|
115
115
|
changelog_uri: https://github.com/avmnu-sng/rspec-tracer/blob/main/CHANGELOG.md
|
|
116
116
|
bug_tracker_uri: https://github.com/avmnu-sng/rspec-tracer/issues
|
|
117
|
+
rubygems_mfa_required: 'true'
|
|
117
118
|
rdoc_options: []
|
|
118
119
|
require_paths:
|
|
119
120
|
- lib
|