rspec-tracer 1.2.3 → 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 +100 -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 +3 -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,103 @@
|
|
|
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
|
+
|
|
1
101
|
## [1.2.3] - 2026-05-06
|
|
2
102
|
|
|
3
103
|
### 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
|
@@ -259,8 +259,11 @@ module RSpecTracer
|
|
|
259
259
|
return if simplecov?
|
|
260
260
|
|
|
261
261
|
require 'coverage'
|
|
262
|
+
return if ::Coverage.running?
|
|
262
263
|
|
|
263
264
|
::Coverage.start
|
|
265
|
+
rescue RuntimeError => e
|
|
266
|
+
RSpecTracer.logger.warn "coverage measurement setup skipped: #{e.message}"
|
|
264
267
|
end
|
|
265
268
|
|
|
266
269
|
def setup_trace_point
|
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
|