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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c0ed3fbdb4a082d56a4d83a528dff26bd174abeb28cf1669dc5dd79de521cee
4
- data.tar.gz: ebc2fbdfde4c6ac82072b5464f5dc0441d73efd24b4410ed96d3892ea765f1a9
3
+ metadata.gz: 5a5c7bccaf770c4b630f3aafbe2965e7a88eebf912d4e5f331eb6001613e0559
4
+ data.tar.gz: cd2c8bb4ea28fa5c70807e8ca7e9249e16a7313f9e17662e3b76d20cc26a91ab
5
5
  SHA512:
6
- metadata.gz: 1c28a6a997cf74dbb8d910972d9dd8e0b6e9f36aaacc9670e8a50a6bf7c0d4100f6cf421befcba8349750c3ba179a05b86d2a0cbdf740280d490d4bd42c37512
7
- data.tar.gz: ce834032d235ea1e2775fb127ac040d6f12f560e7cf9babe5fd208a68cb5dd7260a2ac1f2af78b2a782a622902d9b8291adb2ca339837de70b9f12259e05a456
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
@@ -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
- data = {
9
- example_group: example.example_group.name,
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(&:formatted_inclusion_location)
14
- }.merge(example_location(example))
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
- data.merge(example_id: Digest::MD5.hexdigest(data.to_json))
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 :example_location, :example_rerun_location, :location_file_name
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTracer
4
- VERSION = '1.2.3'
4
+ VERSION = '1.2.4'
5
5
  end
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.3
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.3
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