specguard-ruby 0.3.5 → 0.3.7

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: 9a82ff31aaa51455637e6500a2f4cebda11b50dad52947bd36ce535ada84bc1b
4
- data.tar.gz: 658887600bd0fec337a1d7ddd372b3c481fa761c95ff3c92c25f12fc6e0fcf91
3
+ metadata.gz: f5e2b2a3d9b1a8ac1a2ceb473dd6a20d138a556a020c01361322a94402fe041d
4
+ data.tar.gz: e6ad790808c23a15561ce0f4f9367ee8f7661bf81013d04cde941f8d8faa5ddb
5
5
  SHA512:
6
- metadata.gz: 88589ddb1374aa792be0199049b7bb204b6479850831db80340344f4d624db96dc9b7c1c345d6c95c24bbb6728b4e711ab7a338899a7a815c09e6eb53f47c4b2
7
- data.tar.gz: e11192524e6f248aa16d42c522664863a84894b85987f07ef5f54e5b9c2439bdb6146b0f3c693196d0fcc627f85923f24c5655ed330a9809926f1519802e1e0f
6
+ metadata.gz: 846b95f016d702722fb1e54cedab222b536950cf4c18223c877c9e9d1aa348a34a6ff2627b1b1d4391939f04dbdf3e11651e90e85eb326d98cf5b04436db4b1c
7
+ data.tar.gz: e1a4b80ec07dfc9c63c69c6289f8407b645518da6b95bef7b1c302b206c68060be5ce5f1fd7fc33e9a4880fac97558894b533a116607c3e43156b30f366b72f2
data/README.md CHANGED
@@ -33,7 +33,9 @@ The plugin rides alongside Minitest's own reporters (the suite's output is uncha
33
33
  envelope per process with the same field names the RSpec formatter sends, and never fails the run:
34
34
  a refused or unreachable delivery costs one line on stderr and a line in the local sink. Without
35
35
  `SPECGUARD_API_KEY` nothing is sent — the run is appended to the local development record, exactly
36
- as the RSpec formatter behaves. For a deterministic CI attachment where plugin discovery must not
36
+ as the RSpec formatter behaves. Parameterized tests the `define_method("test_x_#{param}")` loop
37
+ idiom — ship one row per instance, not one row per definition site, so each instance's outcome and
38
+ duration are tracked on their own. For a deterministic CI attachment where plugin discovery must not
37
39
  be assumed, require it explicitly before the run:
38
40
 
39
41
  ```bash
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # May be required before minitest (the README's `-rminitest/specguard_plugin` path):
4
+ # load real minitest, never a bare stub — under discovery the require is a no-op.
5
+ require "minitest"
6
+
3
7
  # Minitest's plugin discovery point. Minitest loads every `minitest/*_plugin.rb`
4
8
  # it can find on the load path (`Minitest.load_plugins` → `Gem.find_files`), so
5
9
  # a gem providing this file needs no require in the consumer's spec files —
@@ -69,6 +69,13 @@ module SpecGuard
69
69
  end
70
70
  end
71
71
 
72
+ # Internal, never on the wire: `record` stashes the result's own method
73
+ # name under this key so `uniquify_ids!` can re-identify colliding rows
74
+ # at delivery time, and `uniquify_ids!` strips it again before the
75
+ # payload is built. The underscore prefix exists so a leak onto the
76
+ # wire would be loud in any payload diff rather than plausible.
77
+ RESULT_NAME_KEY = "_specguard_result_name"
78
+
72
79
  def initialize(configuration: SpecGuard::RSpec.configuration,
73
80
  transport: nil, output: $stderr,
74
81
  annotations: SpecGuard::RSpec::AnnotationLookup.new,
@@ -104,7 +111,12 @@ module SpecGuard
104
111
  # exit code.
105
112
  def record(result)
106
113
  row = row_for(result)
107
- @rows << row if row
114
+ return unless row
115
+
116
+ # Stashed for `uniquify_ids!` (and stripped there) — the one
117
+ # unique-per-result token a Minitest row has. See that method.
118
+ row[RESULT_NAME_KEY] = result.name.to_s
119
+ @rows << row
108
120
  rescue ScriptError, StandardError
109
121
  nil
110
122
  end
@@ -115,6 +127,7 @@ module SpecGuard
115
127
  def report
116
128
  return if @rows.empty?
117
129
 
130
+ uniquify_ids!
118
131
  deliver(payload)
119
132
  rescue ScriptError, StandardError => e
120
133
  warn_once("could not ship test telemetry (#{e.class}: #{e.message}). The test run is unaffected.")
@@ -129,6 +142,35 @@ module SpecGuard
129
142
 
130
143
  private
131
144
 
145
+ # The definition site is NOT unique per result for a generated test:
146
+ # `define_method("test_x_#{param}")` in a loop (or any
147
+ # generate_tests-style helper) stamps every instance with the SAME
148
+ # `source_location` — the define_method call site — so N results share
149
+ # one id, and the platform's ingest upserts
150
+ # `unique_by %i[test_run_id example_id]` silently drops every repeat
151
+ # but the first, taking each dropped row's outcome and duration with
152
+ # it. `@rows` is complete here — every `record` has returned, and
153
+ # parallel mode records from its worker threads through this one shared
154
+ # reporter — so the collision is detectable exactly once, at delivery.
155
+ # Every member of a colliding group is re-identified as
156
+ # `"#{file}:#{line}##{method_name}"`: a method name is unique per
157
+ # class and deterministic, so the suffixed ids are stable across runs.
158
+ # ALL members take the suffix, never just later arrivals — parallel
159
+ # mode's arrival order is fork scheduling, so "the first row keeps the
160
+ # bare id" would make identity depend on it. Rows whose id no other
161
+ # row claims — every plain `def test_` suite among them — keep the id
162
+ # they always had, byte for byte.
163
+ def uniquify_ids!
164
+ counts = Hash.new(0)
165
+ @rows.each { |row| counts[row["id"]] += 1 }
166
+ @rows.each do |row|
167
+ name = row.delete(RESULT_NAME_KEY)
168
+ next unless counts[row["id"]] > 1 && !name.to_s.empty?
169
+
170
+ row["id"] = "#{row["id"]}##{name}"
171
+ end
172
+ end
173
+
132
174
  # The envelope, in the platform's own field names (`Ingest::Payload`:
133
175
  # commit_sha / branch / ci_run_id / duration_seconds / specs) — the same
134
176
  # rule the RSpec formatter states at its `#payload`: this gem's settings
@@ -201,9 +243,14 @@ module SpecGuard
201
243
  intent = annotation_for(file, line_number)
202
244
  {
203
245
  # Minitest has no RSpec-style rerun path or nested ids, so the
204
- # definition site is the identity: stable across runs (which is
205
- # what the platform's per-test identity needs) and unique per
206
- # definition (which is what a row needs).
246
+ # definition site is the row's id the run-local primary key for
247
+ # this one delivered payload, never a cross-run stable identity
248
+ # (cross-run matching remains name + file, the platform's own
249
+ # rule). A definition site is NOT unique per result:
250
+ # `define_method`-generated tests stamp every instance with the
251
+ # same location, so rows can collide on this id within a run, and
252
+ # `uniquify_ids!` is what re-identifies those rows at delivery
253
+ # time. Nothing here promises uniqueness it does not have.
207
254
  "id" => "#{file}:#{line}",
208
255
  "spec_file_path" => file,
209
256
  "file_path" => file,
@@ -196,12 +196,21 @@ module SpecGuard
196
196
  # permits. This path reports nothing and exits 0; the backend
197
197
  # reports a parse failure and exits 1.
198
198
  #
199
- # RATIFIED, and the reason is scope rather than preference. This gem's
200
- # hand-rolled validation logic is slated for REMOVAL by the roadmap that
201
- # owns the binary, not for repair; and closing the gap here would be a
202
- # change to the DEFAULT path, which the slice that introduced
203
- # `ValidatorBackend` explicitly holds fixed. Whoever removes this parser
204
- # closes it by deletion.
199
+ # RATIFIED, and the reason is scope rather than preference.
200
+ # `specguard-lint` validates through the `validate-intent` binary and
201
+ # only the binary, so closing a gap here would be a change to the
202
+ # DEFAULT path, which the slice that introduced `ValidatorBackend`
203
+ # explicitly holds fixed.
204
+ #
205
+ # RETAINED by design, not awaiting deletion. SPGD-96 — the roadmap that
206
+ # owned the binary, and the remover this comment once pointed at —
207
+ # completed 2026-09-06, having carried out the removal it could: the
208
+ # schema-application arm, gone in `c9dca61` ({Linter}'s header records
209
+ # the survivor shape). This input half survived that cutover on purpose:
210
+ # {Scanner.scan_text} is the formatter's `ValidatorError` rescue path
211
+ # ({AnnotationLookup}) and the engine of the suite's validator stub
212
+ # (`spec/support/validator_stub.rb`), and `parse` is its per-token step —
213
+ # removing it would rewrite discovery, not delete dead code.
205
214
  #
206
215
  # Asserted from both sides — what this parser accepts, the convergence,
207
216
  # and both surviving divergences — in
@@ -7,5 +7,5 @@
7
7
  # framework-scoped version constant a lie, so the number moved here and the
8
8
  # rspec module now points at it for back-compat.
9
9
  module SpecGuard
10
- VERSION = "0.3.5"
10
+ VERSION = "0.3.7"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specguard-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - specguard Agent
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-06 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json