specguard-ruby 0.3.7 → 0.3.9
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/README.md +16 -0
- data/lib/specguard/minitest/reporter.rb +18 -17
- data/lib/specguard/rspec/file_selector.rb +75 -7
- data/lib/specguard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6266c2bcd8775838ab93bbb3963cb007465575ee038f67490161fa64e10e982e
|
|
4
|
+
data.tar.gz: 7799e565338f59d3a76b4b19e19a0877dd172072e772e8cc81909f5d040d3ec7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e811640885a89c54dec7bd69702ca429b0f28f5bfdd7c76fd049d15621da4ce0b48323af914448236877f9bc40df78df642232cda89dbfef5a0adcd85612ed7
|
|
7
|
+
data.tar.gz: 28e11252d3051943734247a9878a50680ee719cff789a1ea53da11041574529ed5d9e3761c5445f33b84b0ae42994ae121d35895ec6c0f33fd2c8045163c329e
|
data/README.md
CHANGED
|
@@ -54,6 +54,22 @@ bundle exec specguard-lint --changed # CI mode: only files in the current diff
|
|
|
54
54
|
bundle exec specguard-lint # one-off audit: every *_spec.rb
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
`--changed` is the CI selection mode: it diffs against the **merge base with
|
|
58
|
+
the default branch** (`origin/HEAD`, then `origin/main`/`origin/master`, then
|
|
59
|
+
their local names) — never a bare working-tree-vs-index `git diff`, which is
|
|
60
|
+
empty on a clean checkout. `--changed=<base>` overrides the base for pipelines
|
|
61
|
+
that know better. In a **shallow** checkout — the default depth-1 `git clone`
|
|
62
|
+
behind `actions/checkout@v4` — the merge base with the default branch is not
|
|
63
|
+
in the clone's history, so the derived base is HEAD itself and the selection
|
|
64
|
+
comes up empty: it still exits `0`, but its stderr note (and `--json`
|
|
65
|
+
`selection.note`) names the checkout as **shallow** and the remedy — fetch the
|
|
66
|
+
default branch (`fetch-depth: 0`) or pass `--changed=<base>` naming a base the
|
|
67
|
+
checkout contains. An explicit `--changed=<base>` that is not in a shallow
|
|
68
|
+
checkout's history is a usage error (exit `2`) naming that cause and the same
|
|
69
|
+
remedy, not the bare "could not diff against" a full clone reports for a
|
|
70
|
+
genuinely bad ref. A selection that comes up empty stays exit `0` and says WHY
|
|
71
|
+
on stderr — the exit contract is unchanged; only the WHY is truthful now.
|
|
72
|
+
|
|
57
73
|
Files are **positional** (`specguard-lint spec/order_spec.rb`); there is no `--source` flag —
|
|
58
74
|
that belongs to `validate-intent`, not to this one.
|
|
59
75
|
|
|
@@ -69,13 +69,6 @@ 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
|
-
|
|
79
72
|
def initialize(configuration: SpecGuard::RSpec.configuration,
|
|
80
73
|
transport: nil, output: $stderr,
|
|
81
74
|
annotations: SpecGuard::RSpec::AnnotationLookup.new,
|
|
@@ -113,9 +106,6 @@ module SpecGuard
|
|
|
113
106
|
row = row_for(result)
|
|
114
107
|
return unless row
|
|
115
108
|
|
|
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
109
|
@rows << row
|
|
120
110
|
rescue ScriptError, StandardError
|
|
121
111
|
nil
|
|
@@ -153,21 +143,32 @@ module SpecGuard
|
|
|
153
143
|
# parallel mode records from its worker threads through this one shared
|
|
154
144
|
# reporter — so the collision is detectable exactly once, at delivery.
|
|
155
145
|
# Every member of a colliding group is re-identified as
|
|
156
|
-
# `"#{file}:#{line}##{method_name}"
|
|
157
|
-
# class
|
|
158
|
-
#
|
|
146
|
+
# `"#{file}:#{line}##{Klass#method_name}"`, suffixed with the row's own
|
|
147
|
+
# class-qualified wire `name` — not the bare method name, which stops
|
|
148
|
+
# distinguishing results the moment TWO classes share one `define_method`
|
|
149
|
+
# call site (Rails' declarative `test "desc"` helper is exactly that
|
|
150
|
+
# shape: one `define_method` inside activesupport, shared by every test
|
|
151
|
+
# class in the suite) and generate identically-named methods. The
|
|
152
|
+
# class-qualified name is unique per result and deterministic, so the
|
|
153
|
+
# suffixed ids are stable across runs. The token is read straight off
|
|
154
|
+
# the row rather than stashed at `record` time: `row["name"]` already IS
|
|
155
|
+
# `"#{result.klass}##{result.name}"`, so a sidecar would be a
|
|
156
|
+
# byte-identical copy of a value already on the wire — redundant state
|
|
157
|
+
# and one more thing that could leak. ALL members take the suffix,
|
|
158
|
+
# never just later arrivals — parallel
|
|
159
159
|
# mode's arrival order is fork scheduling, so "the first row keeps the
|
|
160
160
|
# bare id" would make identity depend on it. Rows whose id no other
|
|
161
161
|
# row claims — every plain `def test_` suite among them — keep the id
|
|
162
|
-
# they always had, byte for byte.
|
|
162
|
+
# they always had, byte for byte. The id remains the run-local primary
|
|
163
|
+
# key for one delivered payload and never a cross-run stable identity:
|
|
164
|
+
# cross-run matching remains name + file, the platform's own rule.
|
|
163
165
|
def uniquify_ids!
|
|
164
166
|
counts = Hash.new(0)
|
|
165
167
|
@rows.each { |row| counts[row["id"]] += 1 }
|
|
166
168
|
@rows.each do |row|
|
|
167
|
-
|
|
168
|
-
next unless counts[row["id"]] > 1 && !name.to_s.empty?
|
|
169
|
+
next unless counts[row["id"]] > 1
|
|
169
170
|
|
|
170
|
-
row["id"] = "#{row["id"]}##{name}"
|
|
171
|
+
row["id"] = "#{row["id"]}##{row["name"]}"
|
|
171
172
|
end
|
|
172
173
|
end
|
|
173
174
|
|
|
@@ -43,6 +43,16 @@ module SpecGuard
|
|
|
43
43
|
# There is no diff base that makes "what changed on this build" non-empty
|
|
44
44
|
# there.
|
|
45
45
|
#
|
|
46
|
+
# A **shallow** checkout — the depth-1 clone CI defaults to
|
|
47
|
+
# (`actions/checkout@v4`) — reaches the same thin base by a third road the
|
|
48
|
+
# ref-resolution above cannot see: every `DEFAULT_BRANCH_REFS` probe
|
|
49
|
+
# *resolves* (the fetched tip is in the clone), but the history behind it
|
|
50
|
+
# is not, so the merge base with the default branch is outside the clone
|
|
51
|
+
# and `merge-base` collapses onto HEAD. The notes below therefore consult
|
|
52
|
+
# `git rev-parse --is-shallow-repository` — lazily and memoized, only
|
|
53
|
+
# where the answer changes the message — and in a shallow clone say that,
|
|
54
|
+
# never "default-branch build".
|
|
55
|
+
#
|
|
46
56
|
# That is why the load-bearing requirement is the **loud empty selection**,
|
|
47
57
|
# not the base. The caller must never be unable to tell "checked 12 files,
|
|
48
58
|
# found no annotations" from "checked 0 files" — {Selection} carries the
|
|
@@ -157,17 +167,21 @@ module SpecGuard
|
|
|
157
167
|
"and no HEAD commit); pass --changed=<base> explicitly"
|
|
158
168
|
end
|
|
159
169
|
|
|
160
|
-
|
|
170
|
+
# One memoized probe per run, shared by the two message branches that
|
|
171
|
+
# consult it (a failed diff, a thin base note).
|
|
172
|
+
is_shallow = shallow_probe(root)
|
|
173
|
+
|
|
174
|
+
files, stats = changed_files(resolved, root, is_shallow)
|
|
161
175
|
|
|
162
176
|
Selection.new(files: files, mode: :changed, base: resolved,
|
|
163
|
-
note: base_note(resolved, base_kind, root), stats: stats)
|
|
177
|
+
note: base_note(resolved, base_kind, root, is_shallow), stats: stats)
|
|
164
178
|
end
|
|
165
179
|
|
|
166
180
|
# Resolves git's repo-root-relative output into paths relative to `root`,
|
|
167
181
|
# dropping (and counting) everything the two scoping rules exclude.
|
|
168
182
|
# @return [[Array<String>, Stats]]
|
|
169
|
-
def changed_files(base, root)
|
|
170
|
-
names = diff_names(base, root)
|
|
183
|
+
def changed_files(base, root, is_shallow)
|
|
184
|
+
names = diff_names(base, root, is_shallow)
|
|
171
185
|
specs = names.select { |name| CHANGED_PATTERNS.any? { |pattern| File.fnmatch?(pattern, name) } }
|
|
172
186
|
|
|
173
187
|
top = toplevel(root)
|
|
@@ -198,9 +212,23 @@ module SpecGuard
|
|
|
198
212
|
# them, and they then fail to open. `-z` makes the output machine-readable:
|
|
199
213
|
# NUL-separated and never `core.quotePath`-quoted, so a non-ASCII path
|
|
200
214
|
# survives intact and a path containing a newline cannot split a record.
|
|
201
|
-
def diff_names(base, root)
|
|
215
|
+
def diff_names(base, root, is_shallow)
|
|
202
216
|
out, ok = git(%W[diff -z --name-only --diff-filter=d #{base} --], root)
|
|
203
|
-
|
|
217
|
+
unless ok
|
|
218
|
+
if is_shallow.call
|
|
219
|
+
# SPGD-1035: in a shallow checkout the overwhelmingly likely cause
|
|
220
|
+
# is that <base> is simply not in the clone's history (git's own
|
|
221
|
+
# stderr says `fatal: bad object` / `unknown revision`), and the
|
|
222
|
+
# fix is a fetch — a cause the bare message never named, leaving
|
|
223
|
+
# "bad ref" and "shallow history" indistinguishable. Non-shallow
|
|
224
|
+
# failures keep the original message.
|
|
225
|
+
raise UsageError,
|
|
226
|
+
"--changed could not diff against #{base.inspect}: this checkout is shallow and " \
|
|
227
|
+
"#{base.inspect} is not in its history — fetch it (fetch-depth: 0, or " \
|
|
228
|
+
"git fetch origin #{base}) or pass a base the checkout contains"
|
|
229
|
+
end
|
|
230
|
+
raise UsageError, "--changed could not diff against #{base.inspect}"
|
|
231
|
+
end
|
|
204
232
|
|
|
205
233
|
out.split("\0").reject(&:empty?)
|
|
206
234
|
end
|
|
@@ -241,20 +269,60 @@ module SpecGuard
|
|
|
241
269
|
# `git diff HEAD`, i.e. the working-tree-vs-HEAD no-op this class exists
|
|
242
270
|
# to avoid. Reporting the first when the second is true would be a
|
|
243
271
|
# confidently wrong explanation.
|
|
244
|
-
|
|
272
|
+
#
|
|
273
|
+
# SPGD-1035: a shallow (depth-limited) checkout reproduces both thin
|
|
274
|
+
# shapes with a third cause those two stories miss — the merge base with
|
|
275
|
+
# the default branch is not in the clone's history at all — so whenever
|
|
276
|
+
# the repo IS shallow the note tells that story and names the remedy
|
|
277
|
+
# (fetch the default branch, or pass a base the checkout contains),
|
|
278
|
+
# retiring the "default-branch build" guess for shallow repos in both
|
|
279
|
+
# shapes.
|
|
280
|
+
def base_note(resolved, base_kind, root, is_shallow)
|
|
281
|
+
shallow_remedy =
|
|
282
|
+
"fetch the default branch (fetch-depth: 0) or pass --changed=<base> naming a base this checkout contains"
|
|
245
283
|
case base_kind
|
|
246
284
|
when :head_fallback
|
|
285
|
+
if is_shallow.call
|
|
286
|
+
return "this checkout is a shallow (depth-limited) clone and no default-branch ref " \
|
|
287
|
+
"(#{DEFAULT_BRANCH_REFS.join(', ')}) is in its history, so the diff base fell " \
|
|
288
|
+
"back to HEAD; --changed can only select uncommitted changes here — #{shallow_remedy}"
|
|
289
|
+
end
|
|
290
|
+
|
|
247
291
|
"no default-branch ref (#{DEFAULT_BRANCH_REFS.join(', ')}) could be found, so the diff base " \
|
|
248
292
|
"fell back to HEAD; --changed can only select uncommitted changes here"
|
|
249
293
|
when :merge_base
|
|
250
294
|
head, ok = git(%w[rev-parse HEAD], root)
|
|
251
295
|
return nil unless ok && head.strip == resolved
|
|
252
296
|
|
|
297
|
+
if is_shallow.call
|
|
298
|
+
return "this checkout is a shallow (depth-limited) clone, so the merge base with the " \
|
|
299
|
+
"default branch is not in its history and the diff base is HEAD itself — only " \
|
|
300
|
+
"uncommitted changes can be selected; #{shallow_remedy}"
|
|
301
|
+
end
|
|
302
|
+
|
|
253
303
|
"the diff base is HEAD itself (this looks like a default-branch build), " \
|
|
254
304
|
"so only uncommitted changes can be selected"
|
|
255
305
|
end
|
|
256
306
|
end
|
|
257
307
|
|
|
308
|
+
# Memoized per-run `git rev-parse --is-shallow-repository` probe.
|
|
309
|
+
# SPGD-1035: shallowness is consulted only in the branches where it
|
|
310
|
+
# changes the message (a derived base of HEAD, a failed diff), so the
|
|
311
|
+
# full-clone happy path makes zero new git invocations and the memo caps
|
|
312
|
+
# the probe at one per run. An unreadable answer (old git without the
|
|
313
|
+
# flag) reads as not shallow, keeping today's messages exactly.
|
|
314
|
+
# @return [Proc] zero-argument; true iff git reports a shallow repository
|
|
315
|
+
def shallow_probe(root)
|
|
316
|
+
cached = nil
|
|
317
|
+
lambda do
|
|
318
|
+
if cached.nil?
|
|
319
|
+
out, ok = git(%w[rev-parse --is-shallow-repository], root)
|
|
320
|
+
cached = ok && out.strip == "true"
|
|
321
|
+
end
|
|
322
|
+
cached
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
258
326
|
def git_repository?(root)
|
|
259
327
|
out, ok = git(%w[rev-parse --is-inside-work-tree], root)
|
|
260
328
|
ok && out.strip == "true"
|
data/lib/specguard/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.9
|
|
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-
|
|
11
|
+
date: 2026-09-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|