specguard-ruby 0.3.28 → 0.3.30
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 +23 -8
- data/bin/specguard-ingest +6 -6
- data/bin/specguard-lint +4 -4
- data/lib/specguard/{rspec → client}/annotation_lookup.rb +4 -4
- data/lib/specguard/{rspec → client}/annotation_scanner.rb +1 -1
- data/lib/specguard/{rspec → client}/cli.rb +14 -11
- data/lib/specguard/{rspec → client}/configuration.rb +2 -37
- data/lib/specguard/{rspec → client}/file_selector.rb +1 -1
- data/lib/specguard/{rspec → client}/finding.rb +7 -4
- data/lib/specguard/{rspec → client}/ingest_cli.rb +4 -4
- data/lib/specguard/{rspec → client}/ingest_reporter.rb +1 -1
- data/lib/specguard/{rspec → client}/json_reporter.rb +1 -1
- data/lib/specguard/{rspec → client}/linter.rb +6 -4
- data/lib/specguard/{rspec → client}/payload_normalizer.rb +1 -1
- data/lib/specguard/{rspec → client}/scanner.rb +89 -13
- data/lib/specguard/{rspec → client}/transport.rb +1 -1
- data/lib/specguard/{rspec → client}/validator_backend.rb +2 -2
- data/lib/specguard/client.rb +121 -0
- data/lib/specguard/minitest/reporter.rb +14 -15
- data/lib/specguard/rspec/formatter.rb +11 -5
- data/lib/specguard/rspec.rb +31 -62
- data/lib/specguard/version.rb +1 -1
- metadata +18 -17
- /data/lib/specguard/{rspec → client}/schemas/open-test-intent.v1.json +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 916a7326516ee737b20d0a0790fd6b6a0f1abe56404083ad67fe61d7d7fb5c46
|
|
4
|
+
data.tar.gz: 0b39ff94f46fb053ea659cb5f69f878bf86a3b04ed1e0e630394769bed0d6589
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d721e5d2d50648b3dfc7cf22ebbd2af16a9edd422feffa80b6d32d1092860c145ef6598919a802e3341a3eaa9fea6341fa26cab71f8abef33e0ff9d74d127b6
|
|
7
|
+
data.tar.gz: 77a24a2798088706388f6ef2ba7c6d4bd648d4fd152b1dcf55eb9e391409a0ece0ec3046d6b0b14bfaa24db03e68dac7e89d4eef340d6c34ba6b9512acdfda7a
|
data/README.md
CHANGED
|
@@ -44,13 +44,27 @@ be assumed, require it explicitly before the run:
|
|
|
44
44
|
bundle exec ruby -rminitest/specguard_plugin -e 'Minitest.extensions << "specguard"; load ARGV[0]' test/your_suite.rb
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Configure it from Ruby, if the environment is not the right place, through the framework-neutral
|
|
48
|
+
`SpecGuard.configure` — in a `test_helper.rb`, before the run starts:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
SpecGuard.configure do |config|
|
|
52
|
+
config.branch = "release/2.0"
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`SpecGuard::RSpec.configure` is the same call under its RSpec-era spelling: both return the one
|
|
57
|
+
configuration object the reporter and the formatter read, so either works from either framework.
|
|
58
|
+
See [Shipping the run to SpecGuard](#shipping-the-run-to-specguard) for the settings.
|
|
59
|
+
|
|
47
60
|
## The linter — `specguard-lint`
|
|
48
61
|
|
|
49
62
|
Validates `# @intent:` annotations in changed (or all) `*_spec.rb` files against the OpenTestIntent
|
|
50
63
|
JSON Schema. Exits `1` on a malformed annotation — or a well-formed but unreachable one (stacked
|
|
51
64
|
above another comment-form `@intent:` line, so the one-line lookback never claims it; separated
|
|
52
|
-
from its `it` by one blank or ordinary comment line, which puts it out of the lookback's reach;
|
|
53
|
-
|
|
65
|
+
from its example (`it`, `specify`, `xit`, `fit`, `example`, …) by one blank or ordinary comment line, which puts it out of the lookback's reach;
|
|
66
|
+
directly above an example that carries its own trailing `@intent:`, which extraction prefers, so the
|
|
67
|
+
comment is shadowed; or trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
|
|
54
68
|
**never** fails on a *missing* one (adoption is opt-in and gradual).
|
|
55
69
|
|
|
56
70
|
```bash
|
|
@@ -336,8 +350,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
|
|
|
336
350
|
is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
|
|
337
351
|
my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
|
|
338
352
|
but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
|
|
339
|
-
never claims it, separated from its
|
|
340
|
-
`describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
|
|
353
|
+
never claims it, separated from its example by one blank or ordinary comment line, shadowed by its
|
|
354
|
+
example's own trailing `@intent:`, or trailing on a `describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
|
|
341
355
|
|
|
342
356
|
## The formatter — `SpecGuard::RSpecFormatter`
|
|
343
357
|
|
|
@@ -440,8 +454,9 @@ the one on the next line.
|
|
|
440
454
|
# SPECGUARD_SHARD_ID / SPECGUARD_OUTPUT_PATH / SPECGUARD_LOCAL_OUTPUT_PATH
|
|
441
455
|
# override any of it.
|
|
442
456
|
#
|
|
443
|
-
# Assign a value here only when it is one neither source can know
|
|
444
|
-
SpecGuard::RSpec.configure
|
|
457
|
+
# Assign a value here only when it is one neither source can know
|
|
458
|
+
# (`SpecGuard::RSpec.configure` is the same call, under its RSpec-era name):
|
|
459
|
+
SpecGuard.configure do |config|
|
|
445
460
|
config.branch = "release/2.0"
|
|
446
461
|
end
|
|
447
462
|
```
|
|
@@ -461,7 +476,7 @@ export SPECGUARD_TIMEOUT=10 # optional; seconds, applied to connect and
|
|
|
461
476
|
|
|
462
477
|
```ruby
|
|
463
478
|
# ...or in Ruby, if you would rather not use the environment
|
|
464
|
-
SpecGuard
|
|
479
|
+
SpecGuard.configure do |config|
|
|
465
480
|
config.endpoint = "https://specguard.example.com"
|
|
466
481
|
config.api_key = ENV["SPECGUARD_API_KEY"]
|
|
467
482
|
config.timeout = 10
|
|
@@ -473,7 +488,7 @@ is written to `log/test_results.local.jsonl` — the local development record,
|
|
|
473
488
|
kept apart from the replay queue — so local development needs no opt-out, and a
|
|
474
489
|
fork with no secret configured behaves like a laptop rather than like a broken
|
|
475
490
|
build. The local file's name is configurable via `SPECGUARD_LOCAL_OUTPUT_PATH`
|
|
476
|
-
(or `SpecGuard
|
|
491
|
+
(or `SpecGuard.configure { |c| c.local_output_path = ... }`). The write
|
|
477
492
|
itself is silent when it succeeds — the ordinary case on any machine that can
|
|
478
493
|
create `log/` — and it usually does.
|
|
479
494
|
|
data/bin/specguard-ingest
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
# the suite already finished, instead of re-running the suite.
|
|
10
10
|
#
|
|
11
11
|
# Exit codes are the contract, and they are `specguard-lint`'s reasoning
|
|
12
|
-
# transferred (see SpecGuard::
|
|
12
|
+
# transferred (see SpecGuard::Client::IngestCLI):
|
|
13
13
|
#
|
|
14
14
|
# 0 every line was accepted
|
|
15
15
|
# 1 at least one line was refused by the endpoint
|
|
16
16
|
# 2 this tool could not do its job — misuse, or the tool itself broken
|
|
17
17
|
#
|
|
18
|
-
# SpecGuard::
|
|
18
|
+
# SpecGuard::Client::IngestCLI#run is written to *return* one of those three and
|
|
19
19
|
# never to raise. The `require`s below are the one thing outside its reach, so
|
|
20
20
|
# they get the same treatment here: a gem that cannot even load is the tool
|
|
21
21
|
# being broken, which is a 2. Left bare, Ruby would exit 1 and report that the
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
|
|
24
24
|
# Allow running from a source checkout without bundler by putting the local
|
|
25
25
|
# lib/ on the load path. When the gem is installed (or run under
|
|
26
|
-
# `bundle exec`), specguard/
|
|
26
|
+
# `bundle exec`), specguard/client resolves through the normal load path.
|
|
27
27
|
source_lib = File.expand_path("../lib", __dir__)
|
|
28
28
|
$LOAD_PATH.unshift(source_lib) unless $LOAD_PATH.include?(source_lib)
|
|
29
29
|
|
|
30
30
|
begin
|
|
31
|
-
require "specguard/
|
|
31
|
+
require "specguard/client"
|
|
32
32
|
# By its own path, deliberately. `net/http`, `uri` and `zlib` are not on the
|
|
33
33
|
# linter's load chain and this command is the reason to pay for them.
|
|
34
|
-
require "specguard/
|
|
34
|
+
require "specguard/client/ingest_cli"
|
|
35
35
|
rescue ScriptError, StandardError => e
|
|
36
36
|
warn "specguard-ingest: error: could not load specguard-ruby: #{e.class}: #{e.message}"
|
|
37
37
|
exit 2
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
exit SpecGuard::
|
|
40
|
+
exit SpecGuard::Client::IngestCLI.new.run(ARGV)
|
data/bin/specguard-lint
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# 1 one or more annotations are malformed
|
|
12
12
|
# 2 the linter could not do its job — misuse, or the tool itself broken
|
|
13
13
|
#
|
|
14
|
-
# SpecGuard::
|
|
14
|
+
# SpecGuard::Client::CLI#run is written to *return* one of those three and never
|
|
15
15
|
# to raise; see its class comment for why that takes deliberate effort in Ruby.
|
|
16
16
|
# The `require` below is the one thing outside its reach, so it gets the same
|
|
17
17
|
# treatment here: a gem that cannot even load is the linter being broken, which
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
|
|
21
21
|
# Allow running from a source checkout without bundler by putting the local
|
|
22
22
|
# lib/ on the load path. When the gem is installed (or run under
|
|
23
|
-
# `bundle exec`), specguard/
|
|
23
|
+
# `bundle exec`), specguard/client resolves through the normal load path.
|
|
24
24
|
source_lib = File.expand_path("../lib", __dir__)
|
|
25
25
|
$LOAD_PATH.unshift(source_lib) unless $LOAD_PATH.include?(source_lib)
|
|
26
26
|
|
|
27
27
|
begin
|
|
28
|
-
require "specguard/
|
|
28
|
+
require "specguard/client"
|
|
29
29
|
rescue ScriptError, StandardError => e
|
|
30
30
|
warn "specguard-lint: error: could not load specguard-ruby: #{e.class}: #{e.message}"
|
|
31
31
|
exit 2
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
exit SpecGuard::
|
|
34
|
+
exit SpecGuard::Client::CLI.new.run(ARGV)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# with what?*
|
|
5
5
|
#
|
|
6
6
|
# It is the formatter's half of the annotation story. The linter's half already
|
|
7
|
-
# exists and is not duplicated here — {SpecGuard::
|
|
7
|
+
# exists and is not duplicated here — {SpecGuard::Client::Scanner} finds every
|
|
8
8
|
# `@intent:` in a file, captures its payload string-aware and parses it, and
|
|
9
9
|
# the `validate-intent` binary decides whether the result is valid. This class
|
|
10
10
|
# consumes both. Writing a second extractor would guarantee that the tool
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
# platform eventually disagree about what an annotation *is*.
|
|
13
13
|
#
|
|
14
14
|
# Requiring the linter's chain from here is safe in the direction that matters:
|
|
15
|
-
# `lib/specguard/
|
|
15
|
+
# `lib/specguard/client.rb` does not require `rspec/core`, so pulling it in from
|
|
16
16
|
# the formatter's side keeps `bin/specguard-lint` loadable on a machine with no
|
|
17
17
|
# RSpec installed. spec/specguard/rspec/formatter_loading_spec.rb pins that in
|
|
18
18
|
# both directions.
|
|
19
|
-
require_relative "../
|
|
19
|
+
require_relative "../client"
|
|
20
20
|
|
|
21
21
|
module SpecGuard
|
|
22
|
-
module
|
|
22
|
+
module Client
|
|
23
23
|
# == The lookback rule (SPGD-12 §2)
|
|
24
24
|
#
|
|
25
25
|
# `example.metadata[:line_number]` is the line the `it` is on. An annotation
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
|
|
5
5
|
module SpecGuard
|
|
6
|
-
module
|
|
6
|
+
module Client
|
|
7
7
|
# `specguard-lint`'s command line, and the whole of the exit contract.
|
|
8
8
|
#
|
|
9
9
|
# == The contract, and the reason it needs defending
|
|
@@ -126,9 +126,10 @@ module SpecGuard
|
|
|
126
126
|
# "Lint, don't require": a missing annotation is never an error.
|
|
127
127
|
EXIT_OK = 0
|
|
128
128
|
# One or more annotations are malformed — or well-formed but
|
|
129
|
-
# unreachable (SPGD-900/SPGD-1510/SPGD-1554: stacked above
|
|
130
|
-
# comment-form `@intent:` line so the one-line lookback never
|
|
131
|
-
# separated from its example by one blank/comment line,
|
|
129
|
+
# unreachable (SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560: stacked above
|
|
130
|
+
# another comment-form `@intent:` line so the one-line lookback never
|
|
131
|
+
# claims it, separated from its example by one blank/comment line,
|
|
132
|
+
# shadowed by its example's own trailing `@intent:`, or trailing on
|
|
132
133
|
# an example-group line no example's extraction can reach — a dead
|
|
133
134
|
# contract in every case, and still a failure). The only code produced by
|
|
134
135
|
# inspecting content, and the only path that reaches it is a failed
|
|
@@ -168,13 +169,15 @@ module SpecGuard
|
|
|
168
169
|
|
|
169
170
|
results = backend.check(selection.files)
|
|
170
171
|
|
|
171
|
-
# The structural pass (SPGD-900, SPGD-1510, SPGD-1554): annotations
|
|
172
|
+
# The structural pass (SPGD-900, SPGD-1510, SPGD-1554, SPGD-1560): annotations
|
|
172
173
|
# that are valid in isolation but can never be extracted — stacked
|
|
173
174
|
# consecutive comment-form `@intent:` lines, where the one-line
|
|
174
175
|
# lookback (SPGD-12 §2) claims only the line just above the example,
|
|
175
176
|
# comment-form runs separated from their example by one blank/comment
|
|
176
|
-
# line,
|
|
177
|
-
#
|
|
177
|
+
# line, comment-form lines shadowed by their example's own trailing
|
|
178
|
+
# `@intent:` (extraction is own-line-first), and `@intent:` tokens
|
|
179
|
+
# trailing on an example-group line, which no extraction rule can ever
|
|
180
|
+
# reach. These are Linter::Results like any
|
|
178
181
|
# other, so both renderers and the exit code pick them up with no
|
|
179
182
|
# second path to keep in step.
|
|
180
183
|
results += unreachable_results(selection.files)
|
|
@@ -355,7 +358,7 @@ module SpecGuard
|
|
|
355
358
|
end
|
|
356
359
|
|
|
357
360
|
# The fence arm of the `:all` empty reason: every matching file the walk
|
|
358
|
-
# found was inside a {SpecGuard::
|
|
361
|
+
# found was inside a {SpecGuard::Client::FileSelector::SKIPPED_DIRECTORIES}
|
|
359
362
|
# directory, so the silence is the fence's, not the tree's.
|
|
360
363
|
def all_fenced_reason(selection)
|
|
361
364
|
n = selection.skipped
|
|
@@ -482,15 +485,15 @@ module SpecGuard
|
|
|
482
485
|
#
|
|
483
486
|
# The boundary is the set-difference the partition above already
|
|
484
487
|
# supports: a file counts as covered when it yielded any line-scoped
|
|
485
|
-
# result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554
|
|
488
|
+
# result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560
|
|
486
489
|
# unreachable findings are line-scoped too and require an annotation to
|
|
487
490
|
# exist, so they cover their file) or when it could not be read at all
|
|
488
491
|
# (KIND_READ → `unread`). An unread file is NOT a zero-annotation file:
|
|
489
492
|
# it already gets its own summary clause and FAIL line, and calling it
|
|
490
493
|
# annotation-free would be the same overstatement `summary_line` refuses
|
|
491
494
|
# to make. Unreachable findings need an annotation to exist — a stacked
|
|
492
|
-
# comment-form pair, a separated comment-form run,
|
|
493
|
-
# token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
|
|
495
|
+
# comment-form pair, a separated comment-form run, a shadowed comment-form
|
|
496
|
+
# run, or an `@intent:` token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
|
|
494
497
|
# non-bare file and no third subtraction is needed.
|
|
495
498
|
#
|
|
496
499
|
# It is a NOTE on stderr, never a warning and never an exit code: "lint,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SpecGuard
|
|
4
|
-
module
|
|
4
|
+
module Client
|
|
5
5
|
# Where the checkout is sitting — which commit, and which branch — asked of
|
|
6
6
|
# git directly.
|
|
7
7
|
#
|
|
@@ -120,7 +120,7 @@ module SpecGuard
|
|
|
120
120
|
# The escape hatch stays open on top of all of it, for a value neither
|
|
121
121
|
# source can know:
|
|
122
122
|
#
|
|
123
|
-
# SpecGuard
|
|
123
|
+
# SpecGuard.configure do |config|
|
|
124
124
|
# config.branch = "release/2.0"
|
|
125
125
|
# end
|
|
126
126
|
#
|
|
@@ -433,40 +433,5 @@ module SpecGuard
|
|
|
433
433
|
parsed
|
|
434
434
|
end
|
|
435
435
|
end
|
|
436
|
-
|
|
437
|
-
class << self
|
|
438
|
-
# The process-wide formatter configuration.
|
|
439
|
-
#
|
|
440
|
-
# Built on first use rather than at load time so that a `configure` block
|
|
441
|
-
# in a `spec_helper.rb` and a variable exported by a CI job describe the
|
|
442
|
-
# same object regardless of which the interpreter reached first.
|
|
443
|
-
#
|
|
444
|
-
# @return [Configuration]
|
|
445
|
-
def configuration
|
|
446
|
-
@configuration ||= Configuration.new
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# Configure the formatter.
|
|
450
|
-
#
|
|
451
|
-
# SpecGuard::RSpec.configure do |config|
|
|
452
|
-
# config.branch = "release/2.0"
|
|
453
|
-
# end
|
|
454
|
-
#
|
|
455
|
-
# @yieldparam configuration [Configuration]
|
|
456
|
-
# @return [Configuration] the configuration, block or no block
|
|
457
|
-
def configure
|
|
458
|
-
yield configuration if block_given?
|
|
459
|
-
configuration
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Drop the memoized configuration so the next read re-seeds from ENV.
|
|
463
|
-
# Exists for tests, and for the rare caller that changes the environment
|
|
464
|
-
# after this file was loaded.
|
|
465
|
-
#
|
|
466
|
-
# @return [void]
|
|
467
|
-
def reset_configuration!
|
|
468
|
-
@configuration = nil
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
436
|
end
|
|
472
437
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SpecGuard
|
|
4
|
-
module
|
|
4
|
+
module Client
|
|
5
5
|
# One `@intent:` annotation, located and (where possible) parsed.
|
|
6
6
|
#
|
|
7
7
|
# Exactly one of `intent` and `problem` is non-nil:
|
|
@@ -25,7 +25,7 @@ module SpecGuard
|
|
|
25
25
|
#
|
|
26
26
|
# NOTE: this is a subclass of an anonymous `Data.define` rather than a
|
|
27
27
|
# `Data.define do ... end` block, because a constant assigned inside that
|
|
28
|
-
# block binds to the *lexically* enclosing module (SpecGuard::
|
|
28
|
+
# block binds to the *lexically* enclosing module (SpecGuard::Client) rather
|
|
29
29
|
# than to the value class — so KIND_EXTRACTION below would silently not be
|
|
30
30
|
# `Finding::KIND_EXTRACTION`.
|
|
31
31
|
class Finding < Data.define(:file, :line, :intent, :problem, :kind)
|
|
@@ -49,7 +49,7 @@ module SpecGuard
|
|
|
49
49
|
KIND_SCHEMA = :schema
|
|
50
50
|
|
|
51
51
|
# The annotation is well-formed in isolation but can never be EXTRACTED.
|
|
52
|
-
#
|
|
52
|
+
# Four shapes, all dead the moment they are written:
|
|
53
53
|
#
|
|
54
54
|
# * a comment-form `@intent:` stacked above another comment-form
|
|
55
55
|
# `@intent:` line, so the one-line lookback (SPGD-12 §2 —
|
|
@@ -61,7 +61,10 @@ module SpecGuard
|
|
|
61
61
|
# it, and the example beneath it silently ingests unannotated;
|
|
62
62
|
# * a comment-form `@intent:` run separated from its example by ONE
|
|
63
63
|
# blank or ordinary comment line (SPGD-1554): the one-line lookback
|
|
64
|
-
# reads only that interleave, so the whole run is out of reach
|
|
64
|
+
# reads only that interleave, so the whole run is out of reach;
|
|
65
|
+
# * a comment-form `@intent:` directly above an example whose OWN line
|
|
66
|
+
# also carries a trailing `@intent:` (SPGD-1560): extraction is
|
|
67
|
+
# own-line-first, so the example's own annotation shadows the comment.
|
|
65
68
|
#
|
|
66
69
|
# Whichever shape, the contract is dead metadata: counted by the linter,
|
|
67
70
|
# discarded by extraction. Produced by {Scanner}'s structural pass, not
|
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "optparse"
|
|
5
5
|
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../client"
|
|
7
7
|
require_relative "ingest_reporter"
|
|
8
8
|
require_relative "transport"
|
|
9
9
|
|
|
10
10
|
# `specguard-ingest`'s command line — the other end of `log/test_results.jsonl`.
|
|
11
11
|
#
|
|
12
|
-
# == Why this file is not on `require "specguard/
|
|
12
|
+
# == Why this file is not on `require "specguard/client"`'s chain
|
|
13
13
|
#
|
|
14
14
|
# `specguard-lint` runs on machines that never make a network call, and putting
|
|
15
15
|
# this file on the umbrella's chain would put `net/http`, `uri` and `zlib` on
|
|
16
16
|
# the linter's load path to serve a command it never invokes. So it is loaded by
|
|
17
17
|
# its own path, exactly as the formatter is and for the same reason —
|
|
18
|
-
# `require "specguard/
|
|
18
|
+
# `require "specguard/client/ingest_cli"`, which `bin/specguard-ingest` does.
|
|
19
19
|
module SpecGuard
|
|
20
|
-
module
|
|
20
|
+
module Client
|
|
21
21
|
# Replays a saved run: reads a `log/test_results.jsonl` back and re-delivers
|
|
22
22
|
# each line through the {Transport} this gem already ships.
|
|
23
23
|
#
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module SpecGuard
|
|
6
|
-
module
|
|
6
|
+
module Client
|
|
7
7
|
# The shared verdict shape both renderers and the exit code derive from.
|
|
8
8
|
#
|
|
9
9
|
# == Where this sits after SPGD-867 (the cutover)
|
|
@@ -40,15 +40,17 @@ module SpecGuard
|
|
|
40
40
|
# `Finding::KIND_UNREACHABLE` — an annotation that is well-formed in
|
|
41
41
|
# isolation but can never be extracted, so the one-line lookback
|
|
42
42
|
# (SPGD-12 §2) never claims it — is also reported as a failure, and
|
|
43
|
-
# therefore also exits 1.
|
|
43
|
+
# therefore also exits 1. Four shapes reach this kind: a comment-form
|
|
44
44
|
# `@intent:` stacked above another comment-form `@intent:` line (SPGD-900),
|
|
45
45
|
# an `@intent:` trailing on an example-group line, which is no
|
|
46
46
|
# example's own and not comment-only (SPGD-1510), and a comment-form
|
|
47
47
|
# `@intent:` run separated from its example by one blank or ordinary
|
|
48
|
-
# comment line (SPGD-1554)
|
|
48
|
+
# comment line (SPGD-1554), and a comment-form `@intent:` directly above an
|
|
49
|
+
# example whose own line carries a trailing `@intent:`, which own-line-first
|
|
50
|
+
# extraction prefers (SPGD-1560). Nothing about any of them is malformed; the
|
|
49
51
|
# contract is dead metadata, counted by the linter and discarded by
|
|
50
52
|
# extraction, and the structural pass that flags them (SPGD-900,
|
|
51
|
-
# SPGD-1510, SPGD-1554) reports it loudly rather than letting it pass as
|
|
53
|
+
# SPGD-1510, SPGD-1554, SPGD-1560) reports it loudly rather than letting it pass as
|
|
52
54
|
# clean.
|
|
53
55
|
module Linter
|
|
54
56
|
# One annotation's verdict. `problem` is set when discovery could not
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module SpecGuard
|
|
6
|
-
module
|
|
6
|
+
module Client
|
|
7
7
|
# Runs the discovery pipeline over source files and returns {Finding}s.
|
|
8
8
|
#
|
|
9
9
|
# Pipeline, per `@intent:` token:
|
|
@@ -261,7 +261,7 @@ module SpecGuard
|
|
|
261
261
|
# ({AnnotationLookup}, SPGD-12 §2) is positional: an annotation reaches
|
|
262
262
|
# an example only through its own line (the trailing form, and only when
|
|
263
263
|
# that line IS the example's) or through the comment-form line
|
|
264
|
-
# IMMEDIATELY ABOVE it.
|
|
264
|
+
# IMMEDIATELY ABOVE it. Four shapes fall outside both and are dead the
|
|
265
265
|
# moment they are written — silently discarded by extraction while this
|
|
266
266
|
# pipeline counts them as valid annotations and the run exits 0:
|
|
267
267
|
#
|
|
@@ -283,7 +283,15 @@ module SpecGuard
|
|
|
283
283
|
# the example, which is the interleave — so the annotation is out of
|
|
284
284
|
# reach and every line of the run is dead.
|
|
285
285
|
#
|
|
286
|
-
#
|
|
286
|
+
# * the SHADOWED annotation (SPGD-1560, ported from specguard-ts
|
|
287
|
+
# SPGD-1556): a comment-form `@intent:` run directly above an example
|
|
288
|
+
# whose OWN line also carries a trailing `@intent: {`. Extraction is
|
|
289
|
+
# own-line-first ({AnnotationLookup::Index#intent_for} returns the
|
|
290
|
+
# example's own annotation before it ever consults the line above),
|
|
291
|
+
# so the comment is shadowed and its LAST line is dead. (Any earlier
|
|
292
|
+
# lines of the run are the stacked arm's.)
|
|
293
|
+
#
|
|
294
|
+
# All four are reported loudly, not skipped — the same stance
|
|
287
295
|
# {AnnotationScanner} takes for NO_PAYLOAD.
|
|
288
296
|
#
|
|
289
297
|
# A comment-form annotation line is a comment-only line
|
|
@@ -336,10 +344,28 @@ module SpecGuard
|
|
|
336
344
|
"discarded at extraction. Move it directly above the example (or merge it into " \
|
|
337
345
|
"the line that is)"
|
|
338
346
|
|
|
347
|
+
UNREACHABLE_SHADOWED_ANNOTATION =
|
|
348
|
+
"unreachable annotation: the example directly below carries its own trailing @intent:, " \
|
|
349
|
+
"and extraction is own-line-first (SPGD-12 §2), so this comment-form annotation is " \
|
|
350
|
+
"shadowed and silently discarded. Delete this comment, or merge it into the trailing " \
|
|
351
|
+
"payload"
|
|
352
|
+
|
|
339
353
|
# A comment-only line carrying an `@intent:` token — the only form an
|
|
340
354
|
# example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
|
|
341
355
|
COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
|
|
342
356
|
|
|
357
|
+
# The one source of the example-call vocabulary, shared by
|
|
358
|
+
# {EXAMPLE_LINE} and {EXAMPLE_ON_LINE}. `it`/`specify` are matched on
|
|
359
|
+
# `\b` alone; the other eight keywords also carry a `(?![:.])`
|
|
360
|
+
# lookahead, which is load-bearing: `example.run` (the around-hook
|
|
361
|
+
# idiom), `example.metadata[:x]` and `focus: true` are not example
|
|
362
|
+
# definitions and must never anchor. (`\b` already excludes `fitness`
|
|
363
|
+
# and `example_group`, a GROUP_LINE keyword.) `skip` and `pending` are
|
|
364
|
+
# deliberately EXCLUDED: at line start they are the runtime call inside
|
|
365
|
+
# an example body, not an example definition.
|
|
366
|
+
EXAMPLE_CALL_SOURCE =
|
|
367
|
+
"(?:(?:it|specify)\\b|(?:example|focus|fexample|fit|fspecify|xexample|xit|xspecify)\\b(?![:.]))"
|
|
368
|
+
|
|
343
369
|
# The line an annotation run may be claimed from: the first example
|
|
344
370
|
# keyword. The lookback rule is `example.metadata[:line_number] - 1`, so
|
|
345
371
|
# only the comment form on the line immediately above an example is
|
|
@@ -353,7 +379,14 @@ module SpecGuard
|
|
|
353
379
|
# example: stacked lines above it (SPGD-897), the trailing form on
|
|
354
380
|
# a group line directly above it (SPGD-1510), and a comment-form run
|
|
355
381
|
# separated from it by one blank/comment line (SPGD-1554).
|
|
356
|
-
|
|
382
|
+
#
|
|
383
|
+
# The vocabulary is every example method RSpec core defines
|
|
384
|
+
# (`example focus fexample fit fspecify xexample xit xspecify`, besides
|
|
385
|
+
# `it`/`specify`): lookup is keyed by `example.metadata[:line_number]`, so
|
|
386
|
+
# an annotation stacked/separated/shadowed above `xit` is exactly as dead
|
|
387
|
+
# as one above `it`. Both this anchor and {EXAMPLE_ON_LINE} are built
|
|
388
|
+
# from {EXAMPLE_CALL_SOURCE}, so anchor and exemption cannot drift.
|
|
389
|
+
EXAMPLE_LINE = /\A\s*#{EXAMPLE_CALL_SOURCE}/
|
|
357
390
|
|
|
358
391
|
# A blank (whitespace-only) line — one of the two interleaves the
|
|
359
392
|
# separated pass reads. `\n` is whitespace, so a `text.lines` element
|
|
@@ -370,14 +403,18 @@ module SpecGuard
|
|
|
370
403
|
# @return [Array<Finding>] the structural findings for one file, in line
|
|
371
404
|
# order: one per comment-form `@intent:` line in a stacked run (see
|
|
372
405
|
# {stacked_findings_in_text}), one per line of a separated run (see
|
|
373
|
-
# {separated_findings_in_text})
|
|
374
|
-
# trailing
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
406
|
+
# {separated_findings_in_text}), one per run whose example carries its own
|
|
407
|
+
# trailing annotation (see {own_line_shadow_findings_in_text}) plus one
|
|
408
|
+
# per group line carrying the trailing form (see
|
|
409
|
+
# {group_line_findings_in_text}). The four passes are disjoint by
|
|
410
|
+
# construction: the stacked pass flags all but the LAST line of a run
|
|
411
|
+
# when an example directly follows, the shadow pass flags ONLY that last
|
|
412
|
+
# line (and only when the example also carries `@intent: {`), the
|
|
413
|
+
# separated pass needs a blank or comment-only line after the run (one
|
|
414
|
+
# slot cannot be both that and an example), and the group pass reads
|
|
415
|
+
# only lines that are not comment-only — so the merge cannot double-flag
|
|
416
|
+
# a line, and the sort restores the file-then-line order
|
|
417
|
+
# {unreachable_findings} promises.
|
|
381
418
|
def unreachable_findings_in_text(text, file:)
|
|
382
419
|
# A file that is not valid UTF-8 is reported once, loudly, by the
|
|
383
420
|
# backend as a read failure; nothing positional can be said about it,
|
|
@@ -386,6 +423,7 @@ module SpecGuard
|
|
|
386
423
|
|
|
387
424
|
(stacked_findings_in_text(text, file: file) +
|
|
388
425
|
separated_findings_in_text(text, file: file) +
|
|
426
|
+
own_line_shadow_findings_in_text(text, file: file) +
|
|
389
427
|
group_line_findings_in_text(text, file: file)).sort_by(&:line)
|
|
390
428
|
end
|
|
391
429
|
|
|
@@ -473,6 +511,44 @@ module SpecGuard
|
|
|
473
511
|
findings
|
|
474
512
|
end
|
|
475
513
|
|
|
514
|
+
# @param text [String] source of one file
|
|
515
|
+
# @param file [String] path to record on each Finding
|
|
516
|
+
# @return [Array<Finding>] one per maximal run of comment-form
|
|
517
|
+
# `@intent:` lines whose following line is an example
|
|
518
|
+
# ({EXAMPLE_LINE}) that ALSO carries a trailing `@intent: {`
|
|
519
|
+
# ({INTENT_WITH_PAYLOAD}) — reported on the run's LAST line, the one
|
|
520
|
+
# the lookback would otherwise claim. Extraction is own-line-first
|
|
521
|
+
# (SPGD-12 §2), so the example's own annotation always wins and that
|
|
522
|
+
# comment is dead. Earlier lines of a longer run are the stacked arm's
|
|
523
|
+
# ({stacked_findings_in_text}); each dead line is flagged exactly once.
|
|
524
|
+
def own_line_shadow_findings_in_text(text, file:)
|
|
525
|
+
# Same UTF-8 contract as the stacked pass above.
|
|
526
|
+
return [] unless text.valid_encoding?
|
|
527
|
+
|
|
528
|
+
lines = text.lines
|
|
529
|
+
i = 0
|
|
530
|
+
findings = []
|
|
531
|
+
|
|
532
|
+
while i < lines.length
|
|
533
|
+
unless COMMENT_INTENT_LINE.match?(lines[i])
|
|
534
|
+
i += 1
|
|
535
|
+
next
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
i += 1 while i < lines.length && COMMENT_INTENT_LINE.match?(lines[i])
|
|
539
|
+
run_end = i # exclusive; lines[run_end] is the line after the run
|
|
540
|
+
|
|
541
|
+
example = lines[run_end]
|
|
542
|
+
next if example.nil?
|
|
543
|
+
next unless EXAMPLE_LINE.match?(example) && INTENT_WITH_PAYLOAD.match?(example)
|
|
544
|
+
|
|
545
|
+
findings << Finding.new(file: file, line: run_end, problem: UNREACHABLE_SHADOWED_ANNOTATION,
|
|
546
|
+
kind: Finding::KIND_UNREACHABLE)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
findings
|
|
550
|
+
end
|
|
551
|
+
|
|
476
552
|
# RSpec's example-group keywords, at the start of a line and optionally
|
|
477
553
|
# `RSpec.`-prefixed. A line matching this hosts a GROUP, not an example
|
|
478
554
|
# — and {AnnotationLookup}'s extraction can claim an annotation only
|
|
@@ -517,7 +593,7 @@ module SpecGuard
|
|
|
517
593
|
# only ever a missed flag: prose can turn the exemption ON, never a
|
|
518
594
|
# real example OFF. The one-line example's own payload is never
|
|
519
595
|
# consulted, so a payload can never exempt anything.
|
|
520
|
-
EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s
|
|
596
|
+
EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*#{EXAMPLE_CALL_SOURCE}/
|
|
521
597
|
|
|
522
598
|
# @param text [String] source of one file
|
|
523
599
|
# @param file [String] path to record on each Finding
|
|
@@ -7,7 +7,7 @@ require "net/http"
|
|
|
7
7
|
require "open3"
|
|
8
8
|
|
|
9
9
|
module SpecGuard
|
|
10
|
-
module
|
|
10
|
+
module Client
|
|
11
11
|
# The Go validator backend: `validate-intent --source --json`, and since
|
|
12
12
|
# SPGD-867 the ONLY validator `specguard-lint` has.
|
|
13
13
|
#
|
|
@@ -36,7 +36,7 @@ module SpecGuard
|
|
|
36
36
|
# consumers are `Formatter` and `Transport`, and `CLI` does not reference
|
|
37
37
|
# it at all — so threading the lint backend through it would make the
|
|
38
38
|
# linter depend on the telemetry object solely to read one env var. It also
|
|
39
|
-
# memoizes process-wide (`SpecGuard
|
|
39
|
+
# memoizes process-wide (`SpecGuard.configuration`), with
|
|
40
40
|
# `reset_configuration!` existing only for tests, which is a poor fit for
|
|
41
41
|
# something a spec wants to vary per example. {CLI} takes an `env:` instead
|
|
42
42
|
# and asks this module, so the seam is one hash away in a test and one
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
|
|
5
|
+
module SpecGuard
|
|
6
|
+
module Client
|
|
7
|
+
# The Ruby client for SpecGuard.
|
|
8
|
+
#
|
|
9
|
+
# The linter is complete end to end: it finds `@intent:` annotations in
|
|
10
|
+
# spec files, captures each payload string-aware, normalizes PROTOCOL.md
|
|
11
|
+
# §1's permissive syntax into strict JSON, validates the result against the
|
|
12
|
+
# vendored OpenTestIntent schema, and reports violations in the reference
|
|
13
|
+
# tool's own grammar under the 0/1/2 exit contract (see {CLI}).
|
|
14
|
+
#
|
|
15
|
+
# Everything under `SpecGuard::Client` is framework-free: the RSpec
|
|
16
|
+
# formatter and the Minitest reporter are the framework halves of the
|
|
17
|
+
# client and both build on it. Neither is required from here — `rspec`
|
|
18
|
+
# is a development dependency of this gem, not a runtime one, and
|
|
19
|
+
# `bin/specguard-lint` loads this file on machines that may have no RSpec
|
|
20
|
+
# at all. Requiring `rspec/core` from this chain would turn a missing test
|
|
21
|
+
# framework into a broken linter. Load the formatter by its own path when
|
|
22
|
+
# you want it — `require "specguard/rspec/formatter"` — and see that file
|
|
23
|
+
# for the opt-in wiring.
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
|
|
26
|
+
# A source line could not be scanned (unterminated string or object
|
|
27
|
+
# literal). Internal to the scanner: it is caught and turned into a
|
|
28
|
+
# {Finding}'s `problem` rather than reaching a caller.
|
|
29
|
+
class ScanError < Error; end
|
|
30
|
+
|
|
31
|
+
# The tool was invoked in a way that cannot work — `--changed` outside a git
|
|
32
|
+
# repository, say. Distinct from "the annotations are bad": the exit-code
|
|
33
|
+
# contract makes this a 2, and it is typed here so the CLI can map it
|
|
34
|
+
# without re-deriving the distinction from an error message.
|
|
35
|
+
class UsageError < Error; end
|
|
36
|
+
|
|
37
|
+
# The Go validator backend could not produce a verdict — the binary named
|
|
38
|
+
# by `SPECGUARD_VALIDATE_INTENT`, or the default binary the backend
|
|
39
|
+
# resolves when that is blank, is missing, will not execute, exited with a
|
|
40
|
+
# code that is not a verdict, or emitted something this cannot read as a
|
|
41
|
+
# report.
|
|
42
|
+
#
|
|
43
|
+
# Typed separately from {UsageError} because the two are different
|
|
44
|
+
# accusations ("you invoked me wrongly" vs "the tool I was told to use is
|
|
45
|
+
# broken"), and rescued *beside* it in {CLI#run} because the answer to both
|
|
46
|
+
# is the same and non-negotiable: exit 2, never 1. See {ValidatorBackend}.
|
|
47
|
+
class ValidatorError < Error; end
|
|
48
|
+
|
|
49
|
+
# The vendored canonical OpenTestIntent schema, copied byte-for-byte from
|
|
50
|
+
# open-test-intent so this gem has **no cross-repo runtime dependency**.
|
|
51
|
+
# It ships packaged (it lives under `lib/`, which the gemspec includes);
|
|
52
|
+
# the spec fixtures deliberately do not, so nothing here may be
|
|
53
|
+
# load-bearing at runtime.
|
|
54
|
+
#
|
|
55
|
+
# Since the SPGD-867 cutover nothing VALIDATES against this copy — every
|
|
56
|
+
# verdict comes from the `validate-intent` binary's own compiled-in
|
|
57
|
+
# schema. It survives because {ValidatorBackend}'s schema-contract check
|
|
58
|
+
# digests it at runtime and refuses a binary that would enforce different
|
|
59
|
+
# bytes: the two halves of the seam must keep meeting somewhere, and this
|
|
60
|
+
# file is where the gem's half lives.
|
|
61
|
+
SCHEMA_PATH = File.expand_path("client/schemas/open-test-intent.v1.json", __dir__).freeze
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module SpecGuard
|
|
66
|
+
class << self
|
|
67
|
+
# The process-wide client configuration — one object shared by the RSpec
|
|
68
|
+
# formatter, the Minitest reporter and the CLIs, whatever framework a
|
|
69
|
+
# consumer runs.
|
|
70
|
+
#
|
|
71
|
+
# Built on first use rather than at load time so that a `configure` block
|
|
72
|
+
# in a `spec_helper.rb` / `test_helper.rb` and a variable exported by a CI
|
|
73
|
+
# job describe the same object regardless of which the interpreter reached
|
|
74
|
+
# first.
|
|
75
|
+
#
|
|
76
|
+
# {SpecGuard::RSpec.configure} (the documented RSpec entry point) delegates
|
|
77
|
+
# here and returns this same object — there is exactly one configuration,
|
|
78
|
+
# not one per entry point.
|
|
79
|
+
#
|
|
80
|
+
# @return [Client::Configuration]
|
|
81
|
+
def configuration
|
|
82
|
+
# Loaded here, not at the top of this file: the linter's chain
|
|
83
|
+
# (`require "specguard/client"`) must stay free of the formatter's
|
|
84
|
+
# configuration/transport half — formatter_loading_spec pins it.
|
|
85
|
+
require_relative "client/configuration"
|
|
86
|
+
@configuration ||= Client::Configuration.new
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Configure the client.
|
|
90
|
+
#
|
|
91
|
+
# SpecGuard.configure do |config|
|
|
92
|
+
# config.branch = "release/2.0"
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# @yieldparam configuration [Client::Configuration]
|
|
96
|
+
# @return [Client::Configuration] the configuration, block or no block
|
|
97
|
+
def configure
|
|
98
|
+
yield configuration if block_given?
|
|
99
|
+
configuration
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Drop the memoized configuration so the next read re-seeds from ENV.
|
|
103
|
+
# Exists for tests, and for the rare caller that changes the environment
|
|
104
|
+
# after this file was loaded.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
107
|
+
def reset_configuration!
|
|
108
|
+
@configuration = nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
require_relative "client/finding"
|
|
114
|
+
require_relative "client/annotation_scanner"
|
|
115
|
+
require_relative "client/payload_normalizer"
|
|
116
|
+
require_relative "client/scanner"
|
|
117
|
+
require_relative "client/file_selector"
|
|
118
|
+
require_relative "client/linter"
|
|
119
|
+
require_relative "client/json_reporter"
|
|
120
|
+
require_relative "client/validator_backend"
|
|
121
|
+
require_relative "client/cli"
|
|
@@ -4,11 +4,11 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
6
|
# Configuration and Transport are the framework-free halves of the client
|
|
7
|
-
# (env resolution, wire delivery, the never-raise contract)
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
require_relative "../
|
|
7
|
+
# (env resolution, wire delivery, the never-raise contract), under
|
|
8
|
+
# `SpecGuard::Client`. Required here rather than assumed because nothing else
|
|
9
|
+
# loads them on a minitest-only consumer's machine.
|
|
10
|
+
require_relative "../client"
|
|
11
|
+
require_relative "../client/configuration"
|
|
12
12
|
|
|
13
13
|
# The annotation lookup is the third framework-free half: what counts as a
|
|
14
14
|
# test's annotation is a property of the source line, not of the framework
|
|
@@ -17,20 +17,19 @@ require_relative "../rspec/configuration"
|
|
|
17
17
|
# already warns that a second extractor guarantees scanner and reporter
|
|
18
18
|
# eventually disagree about what an annotation is). Requiring it pulls the
|
|
19
19
|
# linter's chain — the same direction-safe require the formatter itself makes;
|
|
20
|
-
# `specguard/
|
|
20
|
+
# `specguard/client` does not require `rspec/core`, so a minitest-only machine
|
|
21
21
|
# stays loadable.
|
|
22
|
-
require_relative "../
|
|
22
|
+
require_relative "../client/annotation_lookup"
|
|
23
23
|
|
|
24
24
|
# The Minitest half of `specguard-ruby`. Everything the RSpec formatter knows
|
|
25
25
|
# about the platform — the envelope's field names, the transport's
|
|
26
26
|
# never-raise contract, the switch that a missing key is — is framework-free
|
|
27
|
-
# knowledge that already lives in `SpecGuard::
|
|
28
|
-
# `SpecGuard::
|
|
29
|
-
# ship first, neither containing any RSpec). This adapter contributes only the
|
|
27
|
+
# knowledge that already lives in `SpecGuard::Client::Configuration` and
|
|
28
|
+
# `SpecGuard::Client::Transport`. This adapter contributes only the
|
|
30
29
|
# part that is genuinely Minitest's: turning `Minitest::Result` objects into
|
|
31
30
|
# the same row shape the RSpec formatter emits, at the moment Minitest hands
|
|
32
31
|
# them to a reporter — and attaching each test's annotation the same way the
|
|
33
|
-
# formatter does, through the shared {SpecGuard::
|
|
32
|
+
# formatter does, through the shared {SpecGuard::Client::AnnotationLookup} and
|
|
34
33
|
# its one-line lookback (SPGD-12 §2), so a Minitest row carries the same
|
|
35
34
|
# `status` / `intent` pair a RSpec row does.
|
|
36
35
|
#
|
|
@@ -78,7 +77,7 @@ module SpecGuard
|
|
|
78
77
|
end
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
def initialize(configuration: SpecGuard
|
|
80
|
+
def initialize(configuration: SpecGuard.configuration,
|
|
82
81
|
transport: nil, output: $stderr,
|
|
83
82
|
annotations: nil,
|
|
84
83
|
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
@@ -108,7 +107,7 @@ module SpecGuard
|
|
|
108
107
|
# binds one root, hands it to the default lookup, and pins rspec-core's
|
|
109
108
|
# lazily memoized relativization regex to the same moment.)
|
|
110
109
|
root = self.class.repo_root
|
|
111
|
-
@annotations = annotations || SpecGuard::
|
|
110
|
+
@annotations = annotations || SpecGuard::Client::AnnotationLookup.new(root: root)
|
|
112
111
|
@clock = clock
|
|
113
112
|
@rows = []
|
|
114
113
|
@started_at = nil
|
|
@@ -244,8 +243,8 @@ module SpecGuard
|
|
|
244
243
|
end
|
|
245
244
|
|
|
246
245
|
def transport_for
|
|
247
|
-
require_relative "../
|
|
248
|
-
SpecGuard::
|
|
246
|
+
require_relative "../client/transport"
|
|
247
|
+
SpecGuard::Client::Transport.new(
|
|
249
248
|
endpoint: @configuration.endpoint,
|
|
250
249
|
api_key: @configuration.api_key,
|
|
251
250
|
timeout: @configuration.timeout
|
|
@@ -40,13 +40,19 @@ require "rspec/core/formatters/base_formatter"
|
|
|
40
40
|
require "json"
|
|
41
41
|
require "fileutils"
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# `specguard/rspec` is what defines `SpecGuard::RSpec` — the namespace whose
|
|
44
|
+
# existence is the shadowing hazard documented below, and the home of the
|
|
45
|
+
# documented `SpecGuard::RSpec.configure` entry point. It is required here so
|
|
46
|
+
# that both stay true however this file is reached, now that the client chain
|
|
47
|
+
# no longer pulls it in.
|
|
48
|
+
require_relative "../rspec"
|
|
49
|
+
require_relative "../client/configuration"
|
|
50
|
+
require_relative "../client/transport"
|
|
45
51
|
# Brings the linter's discovery chain with it (Scanner, Finding, Schema). That
|
|
46
52
|
# direction is safe — `specguard/rspec` does not require `rspec/core`, so the
|
|
47
53
|
# linter stays loadable without RSpec; it is only the reverse that would break
|
|
48
54
|
# packaging.
|
|
49
|
-
require_relative "annotation_lookup"
|
|
55
|
+
require_relative "../client/annotation_lookup"
|
|
50
56
|
|
|
51
57
|
module SpecGuard
|
|
52
58
|
# == A NOTE ON CONSTANT RESOLUTION — read this before editing
|
|
@@ -338,7 +344,7 @@ module SpecGuard
|
|
|
338
344
|
root = self.class.repo_root
|
|
339
345
|
::RSpec::Core::Metadata.relative_path_regex
|
|
340
346
|
@error_stream = error_stream
|
|
341
|
-
@annotations = annotations || SpecGuard::
|
|
347
|
+
@annotations = annotations || SpecGuard::Client::AnnotationLookup.new(root: root)
|
|
342
348
|
@specs = []
|
|
343
349
|
@warned = false
|
|
344
350
|
# Stamped here rather than from a `start` hook on purpose. `:start` is not
|
|
@@ -1031,7 +1037,7 @@ module SpecGuard
|
|
|
1031
1037
|
end
|
|
1032
1038
|
|
|
1033
1039
|
def transport_for(configuration)
|
|
1034
|
-
SpecGuard::
|
|
1040
|
+
SpecGuard::Client::Transport.new(
|
|
1035
1041
|
endpoint: configuration.endpoint,
|
|
1036
1042
|
api_key: configuration.api_key,
|
|
1037
1043
|
timeout: configuration.timeout
|
data/lib/specguard/rspec.rb
CHANGED
|
@@ -1,72 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "rspec/version"
|
|
4
|
+
require_relative "client"
|
|
4
5
|
|
|
5
6
|
module SpecGuard
|
|
7
|
+
# The RSpec-facing entry point of the gem.
|
|
8
|
+
#
|
|
9
|
+
# The framework-free client — scanner, linter, CLIs, configuration,
|
|
10
|
+
# transport — lives under {SpecGuard::Client}; the one component that is
|
|
11
|
+
# genuinely RSpec's is {SpecGuard::RSpecFormatter}
|
|
12
|
+
# (`require "specguard/rspec/formatter"`). What remains here is the
|
|
13
|
+
# documented `SpecGuard::RSpec.configure` entry point, kept working for
|
|
14
|
+
# anybody who configures the client from a `spec_helper.rb`.
|
|
15
|
+
#
|
|
16
|
+
# It is a thin delegate, not a second configuration: all three methods
|
|
17
|
+
# forward to {SpecGuard.configure} / {SpecGuard.configuration} /
|
|
18
|
+
# {SpecGuard.reset_configuration!}, so `SpecGuard::RSpec.configure` and
|
|
19
|
+
# `SpecGuard.configure` return the very same {Client::Configuration} object.
|
|
20
|
+
# A Minitest consumer has no reason to say `RSpec` and should configure
|
|
21
|
+
# through `SpecGuard.configure`.
|
|
6
22
|
module RSpec
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# tool's own grammar under the 0/1/2 exit contract (see {CLI}).
|
|
14
|
-
#
|
|
15
|
-
# The RSpec formatter (`SpecGuard::RSpecFormatter`) is the other half of
|
|
16
|
-
# the client, and it is deliberately **not** required from here: `rspec` is
|
|
17
|
-
# a development dependency of this gem, not a runtime one, and
|
|
18
|
-
# `bin/specguard-lint` loads this file on machines that may have no RSpec
|
|
19
|
-
# at all. Requiring `rspec/core` from this chain would turn a missing test
|
|
20
|
-
# framework into a broken linter. Load it by its own path when you want it
|
|
21
|
-
# — `require "specguard/rspec/formatter"` — and see that file for the
|
|
22
|
-
# opt-in wiring.
|
|
23
|
-
class Error < StandardError; end
|
|
23
|
+
class << self
|
|
24
|
+
# @yieldparam configuration [Client::Configuration]
|
|
25
|
+
# @return [Client::Configuration] the same object {SpecGuard.configure} returns
|
|
26
|
+
def configure(&)
|
|
27
|
+
SpecGuard.configure(&)
|
|
28
|
+
end
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
# @return [Client::Configuration]
|
|
31
|
+
def configuration
|
|
32
|
+
SpecGuard.configuration
|
|
33
|
+
end
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# The Go validator backend could not produce a verdict — the binary named
|
|
37
|
-
# by `SPECGUARD_VALIDATE_INTENT`, or the default binary the backend
|
|
38
|
-
# resolves when that is blank, is missing, will not execute, exited with a
|
|
39
|
-
# code that is not a verdict, or emitted something this cannot read as a
|
|
40
|
-
# report.
|
|
41
|
-
#
|
|
42
|
-
# Typed separately from {UsageError} because the two are different
|
|
43
|
-
# accusations ("you invoked me wrongly" vs "the tool I was told to use is
|
|
44
|
-
# broken"), and rescued *beside* it in {CLI#run} because the answer to both
|
|
45
|
-
# is the same and non-negotiable: exit 2, never 1. See {ValidatorBackend}.
|
|
46
|
-
class ValidatorError < Error; end
|
|
47
|
-
|
|
48
|
-
# The vendored canonical OpenTestIntent schema, copied byte-for-byte from
|
|
49
|
-
# open-test-intent so this gem has **no cross-repo runtime dependency**.
|
|
50
|
-
# It ships packaged (it lives under `lib/`, which the gemspec includes);
|
|
51
|
-
# the spec fixtures deliberately do not, so nothing here may be
|
|
52
|
-
# load-bearing at runtime.
|
|
53
|
-
#
|
|
54
|
-
# Since the SPGD-867 cutover nothing VALIDATES against this copy — every
|
|
55
|
-
# verdict comes from the `validate-intent` binary's own compiled-in
|
|
56
|
-
# schema. It survives because {ValidatorBackend}'s schema-contract check
|
|
57
|
-
# digests it at runtime and refuses a binary that would enforce different
|
|
58
|
-
# bytes: the two halves of the seam must keep meeting somewhere, and this
|
|
59
|
-
# file is where the gem's half lives.
|
|
60
|
-
SCHEMA_PATH = File.expand_path("rspec/schemas/open-test-intent.v1.json", __dir__).freeze
|
|
35
|
+
# @return [void]
|
|
36
|
+
def reset_configuration!
|
|
37
|
+
SpecGuard.reset_configuration!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
61
40
|
end
|
|
62
41
|
end
|
|
63
|
-
|
|
64
|
-
require_relative "rspec/finding"
|
|
65
|
-
require_relative "rspec/annotation_scanner"
|
|
66
|
-
require_relative "rspec/payload_normalizer"
|
|
67
|
-
require_relative "rspec/scanner"
|
|
68
|
-
require_relative "rspec/file_selector"
|
|
69
|
-
require_relative "rspec/linter"
|
|
70
|
-
require_relative "rspec/json_reporter"
|
|
71
|
-
require_relative "rspec/validator_backend"
|
|
72
|
-
require_relative "rspec/cli"
|
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.30
|
|
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-
|
|
11
|
+
date: 2026-10-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -47,24 +47,25 @@ files:
|
|
|
47
47
|
- bin/specguard-ingest
|
|
48
48
|
- bin/specguard-lint
|
|
49
49
|
- lib/minitest/specguard_plugin.rb
|
|
50
|
+
- lib/specguard/client.rb
|
|
51
|
+
- lib/specguard/client/annotation_lookup.rb
|
|
52
|
+
- lib/specguard/client/annotation_scanner.rb
|
|
53
|
+
- lib/specguard/client/cli.rb
|
|
54
|
+
- lib/specguard/client/configuration.rb
|
|
55
|
+
- lib/specguard/client/file_selector.rb
|
|
56
|
+
- lib/specguard/client/finding.rb
|
|
57
|
+
- lib/specguard/client/ingest_cli.rb
|
|
58
|
+
- lib/specguard/client/ingest_reporter.rb
|
|
59
|
+
- lib/specguard/client/json_reporter.rb
|
|
60
|
+
- lib/specguard/client/linter.rb
|
|
61
|
+
- lib/specguard/client/payload_normalizer.rb
|
|
62
|
+
- lib/specguard/client/scanner.rb
|
|
63
|
+
- lib/specguard/client/schemas/open-test-intent.v1.json
|
|
64
|
+
- lib/specguard/client/transport.rb
|
|
65
|
+
- lib/specguard/client/validator_backend.rb
|
|
50
66
|
- lib/specguard/minitest/reporter.rb
|
|
51
67
|
- lib/specguard/rspec.rb
|
|
52
|
-
- lib/specguard/rspec/annotation_lookup.rb
|
|
53
|
-
- lib/specguard/rspec/annotation_scanner.rb
|
|
54
|
-
- lib/specguard/rspec/cli.rb
|
|
55
|
-
- lib/specguard/rspec/configuration.rb
|
|
56
|
-
- lib/specguard/rspec/file_selector.rb
|
|
57
|
-
- lib/specguard/rspec/finding.rb
|
|
58
68
|
- lib/specguard/rspec/formatter.rb
|
|
59
|
-
- lib/specguard/rspec/ingest_cli.rb
|
|
60
|
-
- lib/specguard/rspec/ingest_reporter.rb
|
|
61
|
-
- lib/specguard/rspec/json_reporter.rb
|
|
62
|
-
- lib/specguard/rspec/linter.rb
|
|
63
|
-
- lib/specguard/rspec/payload_normalizer.rb
|
|
64
|
-
- lib/specguard/rspec/scanner.rb
|
|
65
|
-
- lib/specguard/rspec/schemas/open-test-intent.v1.json
|
|
66
|
-
- lib/specguard/rspec/transport.rb
|
|
67
|
-
- lib/specguard/rspec/validator_backend.rb
|
|
68
69
|
- lib/specguard/rspec/version.rb
|
|
69
70
|
- lib/specguard/version.rb
|
|
70
71
|
- script/bump-version.sh
|
|
File without changes
|