specguard-ruby 0.3.29 → 0.3.31
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 +41 -13
- 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 +2 -2
- data/lib/specguard/{rspec → client}/configuration.rb +17 -38
- data/lib/specguard/{rspec → client}/file_selector.rb +1 -1
- data/lib/specguard/{rspec → client}/finding.rb +2 -2
- data/lib/specguard/{rspec → client}/ingest_cli.rb +5 -5
- 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 +1 -1
- data/lib/specguard/{rspec → client}/payload_normalizer.rb +1 -1
- data/lib/specguard/{rspec → client}/scanner.rb +22 -3
- data/lib/specguard/{rspec → client}/transport.rb +54 -6
- data/lib/specguard/{rspec → client}/validator_backend.rb +2 -2
- data/lib/specguard/client.rb +121 -0
- data/lib/specguard/minitest/reporter.rb +26 -17
- data/lib/specguard/rspec/formatter.rb +24 -7
- 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: 14818808e8ef3b696b1b3834ea590734fa398f5fabbef1c5646d03699e844d99
|
|
4
|
+
data.tar.gz: ccce2e2cf09f6b3401284c0f7f3f343185c86fa2d48bc43ca7bbd9e48575910e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7612a5d841ecf6d7b1f675bfe362a5141948c1e227a89d7d90e449c9f3b802b650894e7cd3862bb37a897dcbc77ae6e0d627caa1b3cbde3275b54ebd41beb54
|
|
7
|
+
data.tar.gz: d78f0bea385d4a9ca63d1581648350c76662f7540b0c105dda6a0ebb15c7b3be1be98e7d2ca8f29b5536fac597a4ed155cb5b96f8ff8575e79bfde7292ff0034
|
data/README.md
CHANGED
|
@@ -44,13 +44,26 @@ 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
|
-
directly above an
|
|
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
|
|
54
67
|
comment is shadowed; or trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
|
|
55
68
|
**never** fails on a *missing* one (adoption is opt-in and gradual).
|
|
56
69
|
|
|
@@ -337,8 +350,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
|
|
|
337
350
|
is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
|
|
338
351
|
my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
|
|
339
352
|
but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
|
|
340
|
-
never claims it, separated from its
|
|
341
|
-
|
|
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.
|
|
342
355
|
|
|
343
356
|
## The formatter — `SpecGuard::RSpecFormatter`
|
|
344
357
|
|
|
@@ -441,8 +454,9 @@ the one on the next line.
|
|
|
441
454
|
# SPECGUARD_SHARD_ID / SPECGUARD_OUTPUT_PATH / SPECGUARD_LOCAL_OUTPUT_PATH
|
|
442
455
|
# override any of it.
|
|
443
456
|
#
|
|
444
|
-
# Assign a value here only when it is one neither source can know
|
|
445
|
-
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|
|
|
446
460
|
config.branch = "release/2.0"
|
|
447
461
|
end
|
|
448
462
|
```
|
|
@@ -460,9 +474,18 @@ export SPECGUARD_API_KEY=… # from your repository's settings
|
|
|
460
474
|
export SPECGUARD_TIMEOUT=10 # optional; seconds, applied to connect and read
|
|
461
475
|
```
|
|
462
476
|
|
|
477
|
+
**Using an `sga_` agent key? Also set `SPECGUARD_REPOSITORY_ID`.** An agent key
|
|
478
|
+
covers a set of repositories, so the key cannot say which one a run belongs to;
|
|
479
|
+
the request has to. With `SPECGUARD_REPOSITORY_ID=42` the run is POSTed to
|
|
480
|
+
`<endpoint>/api/v1/repositories/42/ingest` instead of `<endpoint>/api/v1/ingest`
|
|
481
|
+
(`config.repository_id` in Ruby). Under a repository `sgk_` key leave it unset —
|
|
482
|
+
if you do set it, the server ignores it. The id may contain only letters, digits,
|
|
483
|
+
`-` and `_`; anything else is refused before a request is made. A `404` with an
|
|
484
|
+
id configured means no repository with that id is available to the key.
|
|
485
|
+
|
|
463
486
|
```ruby
|
|
464
487
|
# ...or in Ruby, if you would rather not use the environment
|
|
465
|
-
SpecGuard
|
|
488
|
+
SpecGuard.configure do |config|
|
|
466
489
|
config.endpoint = "https://specguard.example.com"
|
|
467
490
|
config.api_key = ENV["SPECGUARD_API_KEY"]
|
|
468
491
|
config.timeout = 10
|
|
@@ -474,7 +497,7 @@ is written to `log/test_results.local.jsonl` — the local development record,
|
|
|
474
497
|
kept apart from the replay queue — so local development needs no opt-out, and a
|
|
475
498
|
fork with no secret configured behaves like a laptop rather than like a broken
|
|
476
499
|
build. The local file's name is configurable via `SPECGUARD_LOCAL_OUTPUT_PATH`
|
|
477
|
-
(or `SpecGuard
|
|
500
|
+
(or `SpecGuard.configure { |c| c.local_output_path = ... }`). The write
|
|
478
501
|
itself is silent when it succeeds — the ordinary case on any machine that can
|
|
479
502
|
create `log/` — and it usually does.
|
|
480
503
|
|
|
@@ -501,6 +524,7 @@ offered to the endpoint and not accepted — so the run can be replayed later wi
|
|
|
501
524
|
```
|
|
502
525
|
SpecGuard: could not deliver test telemetry (HTTP 401 — the API key was not
|
|
503
526
|
accepted). Falling back to log/test_results.jsonl; the test run is unaffected.
|
|
527
|
+
Once the delivery is fixed, replay this run with: bundle exec specguard-ingest log/test_results.jsonl
|
|
504
528
|
```
|
|
505
529
|
|
|
506
530
|
**That line carries the endpoint's own words when it has any.** A `400` refusal
|
|
@@ -513,7 +537,8 @@ SpecGuard: could not deliver test telemetry (HTTP 400 — the endpoint rejected
|
|
|
513
537
|
the payload — spec 3 (spec/orders_spec.rb:9): line_number is required and must
|
|
514
538
|
be a positive integer; spec 7 (spec/orders_spec.rb:31): outcome must be one of
|
|
515
539
|
passed, failed, pending). Falling back to log/test_results.jsonl; the test run
|
|
516
|
-
is unaffected.
|
|
540
|
+
is unaffected. Once the delivery is fixed, replay this run with: bundle exec
|
|
541
|
+
specguard-ingest log/test_results.jsonl
|
|
517
542
|
```
|
|
518
543
|
|
|
519
544
|
It stays **one** line whatever comes back. A systemic problem can have the
|
|
@@ -614,8 +639,9 @@ specguard-ingest: lines 1, 2 carried ci_run_id 17442 and each came back with
|
|
|
614
639
|
test_run_id 41f2c9b8 — the endpoint folded them onto one run
|
|
615
640
|
```
|
|
616
641
|
|
|
617
|
-
It reads the same `SPECGUARD_ENDPOINT`, `SPECGUARD_API_KEY`
|
|
618
|
-
`
|
|
642
|
+
It reads the same `SPECGUARD_ENDPOINT`, `SPECGUARD_API_KEY`, `SPECGUARD_REPOSITORY_ID`
|
|
643
|
+
(for `sga_` agent keys — a replay queue written under one is re-delivered to the same
|
|
644
|
+
repository-scoped route) and `SPECGUARD_TIMEOUT` the formatter does, and sends each line through the same
|
|
619
645
|
transport — so a run that was refused for a rotated key appears on the platform
|
|
620
646
|
once the secret is fixed, with the shard folding described in
|
|
621
647
|
[If you shard your suite](#if-you-shard-your-suite) applying exactly as it would
|
|
@@ -1198,9 +1224,11 @@ export SPECGUARD_ENDPOINT=https://specguard.internal.example.com
|
|
|
1198
1224
|
nothing any other formatter wrote, is read or forwarded.
|
|
1199
1225
|
- **No environment.** SpecGuard's own code reads a fixed list of variables and
|
|
1200
1226
|
no others: the ones named in the envelope table above, which fill
|
|
1201
|
-
`commit_sha`, `branch`, `ci_run_id` and `shard_id`; plus
|
|
1227
|
+
`commit_sha`, `branch`, `ci_run_id` and `shard_id`; plus five that configure
|
|
1202
1228
|
the gem itself rather than describing your suite — `SPECGUARD_ENDPOINT` (where
|
|
1203
|
-
to send the run), `
|
|
1229
|
+
to send the run), `SPECGUARD_REPOSITORY_ID` (which repository an `sga_` agent
|
|
1230
|
+
key's run belongs to — sent as a path segment of the request URL, nothing else),
|
|
1231
|
+
`SPECGUARD_OUTPUT_PATH` (where to write the replay queue —
|
|
1204
1232
|
runs the endpoint refused or could not be reached for), `SPECGUARD_LOCAL_OUTPUT_PATH`
|
|
1205
1233
|
(where to write the local development record when there is no key),
|
|
1206
1234
|
`SPECGUARD_TIMEOUT` (how long to wait), and
|
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
|
|
@@ -358,7 +358,7 @@ module SpecGuard
|
|
|
358
358
|
end
|
|
359
359
|
|
|
360
360
|
# The fence arm of the `:all` empty reason: every matching file the walk
|
|
361
|
-
# found was inside a {SpecGuard::
|
|
361
|
+
# found was inside a {SpecGuard::Client::FileSelector::SKIPPED_DIRECTORIES}
|
|
362
362
|
# directory, so the silence is the fence's, not the tree's.
|
|
363
363
|
def all_fenced_reason(selection)
|
|
364
364
|
n = selection.skipped
|
|
@@ -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
|
#
|
|
@@ -318,6 +318,7 @@ module SpecGuard
|
|
|
318
318
|
LOCAL_OUTPUT_PATH_KEYS = %w[SPECGUARD_LOCAL_OUTPUT_PATH].freeze
|
|
319
319
|
ENDPOINT_KEYS = %w[SPECGUARD_ENDPOINT].freeze
|
|
320
320
|
API_KEY_KEYS = %w[SPECGUARD_API_KEY].freeze
|
|
321
|
+
REPOSITORY_ID_KEYS = %w[SPECGUARD_REPOSITORY_ID].freeze
|
|
321
322
|
TIMEOUT_KEYS = %w[SPECGUARD_TIMEOUT].freeze
|
|
322
323
|
|
|
323
324
|
# The commit the suite ran against. `nil` when nothing said.
|
|
@@ -344,7 +345,9 @@ module SpecGuard
|
|
|
344
345
|
attr_accessor :local_output_path
|
|
345
346
|
# The SpecGuard installation to POST to, scheme and host only:
|
|
346
347
|
# `https://specguard.example.com`. The `/api/v1/ingest` path is the
|
|
347
|
-
# platform's contract, not a setting
|
|
348
|
+
# platform's contract, not a setting — with one exception: when
|
|
349
|
+
# {#repository_id} is set the path is `/api/v1/repositories/<id>/ingest`
|
|
350
|
+
# instead. `nil` when nothing said.
|
|
348
351
|
attr_accessor :endpoint
|
|
349
352
|
# The repository's SpecGuard API key. Present means "deliver over HTTP".
|
|
350
353
|
#
|
|
@@ -354,6 +357,16 @@ module SpecGuard
|
|
|
354
357
|
# nobody can retroactively fix. Send whatever is configured and let a 401
|
|
355
358
|
# be the answer.
|
|
356
359
|
attr_accessor :api_key
|
|
360
|
+
# The repository the run belongs to, sent as a path segment
|
|
361
|
+
# (`/api/v1/repositories/<id>/ingest`). Needed by an `sga_` agent key,
|
|
362
|
+
# which covers a set of repositories and so cannot name the run's
|
|
363
|
+
# repository itself; under an `sgk_` key the platform ignores it. `nil`
|
|
364
|
+
# when unset or blank, which keeps the plain `/api/v1/ingest` path.
|
|
365
|
+
#
|
|
366
|
+
# Deliberately **not** format-checked against the key prefix or a numeric
|
|
367
|
+
# shape (same reasoning as {#api_key}): the platform's 400/403/404 is the
|
|
368
|
+
# answer. {Transport} only refuses ids that are unsafe as a path segment.
|
|
369
|
+
attr_accessor :repository_id
|
|
357
370
|
# Seconds, applied to opening the connection and to reading the response.
|
|
358
371
|
attr_accessor :timeout
|
|
359
372
|
|
|
@@ -374,6 +387,7 @@ module SpecGuard
|
|
|
374
387
|
@local_output_path = first_present(env, LOCAL_OUTPUT_PATH_KEYS) || DEFAULT_LOCAL_OUTPUT_PATH
|
|
375
388
|
@endpoint = first_present(env, ENDPOINT_KEYS)
|
|
376
389
|
@api_key = first_present(env, API_KEY_KEYS)
|
|
390
|
+
@repository_id = first_present(env, REPOSITORY_ID_KEYS)
|
|
377
391
|
@timeout = seconds(first_present(env, TIMEOUT_KEYS)) || DEFAULT_TIMEOUT_SECONDS
|
|
378
392
|
end
|
|
379
393
|
|
|
@@ -433,40 +447,5 @@ module SpecGuard
|
|
|
433
447
|
parsed
|
|
434
448
|
end
|
|
435
449
|
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
450
|
end
|
|
472
451
|
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)
|
|
@@ -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
|
#
|
|
@@ -740,7 +740,7 @@ module SpecGuard
|
|
|
740
740
|
raise UsageError, "no API key is configured (set SPECGUARD_API_KEY)" if blank?(configuration.api_key)
|
|
741
741
|
|
|
742
742
|
transport = Transport.new(endpoint: configuration.endpoint, api_key: configuration.api_key,
|
|
743
|
-
timeout: configuration.timeout)
|
|
743
|
+
timeout: configuration.timeout, repository_id: configuration.repository_id)
|
|
744
744
|
# Asked once, here, so a malformed `SPECGUARD_ENDPOINT` is one exit 2
|
|
745
745
|
# rather than N identical `:failed` lines. `Transport#deliver` would
|
|
746
746
|
# otherwise swallow the same `ArgumentError` once per line and report a
|
|
@@ -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:
|
|
@@ -354,6 +354,18 @@ module SpecGuard
|
|
|
354
354
|
# example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
|
|
355
355
|
COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
|
|
356
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
|
+
|
|
357
369
|
# The line an annotation run may be claimed from: the first example
|
|
358
370
|
# keyword. The lookback rule is `example.metadata[:line_number] - 1`, so
|
|
359
371
|
# only the comment form on the line immediately above an example is
|
|
@@ -367,7 +379,14 @@ module SpecGuard
|
|
|
367
379
|
# example: stacked lines above it (SPGD-897), the trailing form on
|
|
368
380
|
# a group line directly above it (SPGD-1510), and a comment-form run
|
|
369
381
|
# separated from it by one blank/comment line (SPGD-1554).
|
|
370
|
-
|
|
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}/
|
|
371
390
|
|
|
372
391
|
# A blank (whitespace-only) line — one of the two interleaves the
|
|
373
392
|
# separated pass reads. `\n` is whitespace, so a `text.lines` element
|
|
@@ -574,7 +593,7 @@ module SpecGuard
|
|
|
574
593
|
# only ever a missed flag: prose can turn the exemption ON, never a
|
|
575
594
|
# real example OFF. The one-line example's own payload is never
|
|
576
595
|
# consulted, so a payload can never exempt anything.
|
|
577
|
-
EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s
|
|
596
|
+
EXAMPLE_ON_LINE = /(?:\bdo\b|\{|;)\s*#{EXAMPLE_CALL_SOURCE}/
|
|
578
597
|
|
|
579
598
|
# @param text [String] source of one file
|
|
580
599
|
# @param file [String] path to record on each Finding
|
|
@@ -9,7 +9,7 @@ require_relative "configuration"
|
|
|
9
9
|
require_relative "../version"
|
|
10
10
|
|
|
11
11
|
module SpecGuard
|
|
12
|
-
module
|
|
12
|
+
module Client
|
|
13
13
|
# The one HTTP call this gem makes: `POST <endpoint>/api/v1/ingest`,
|
|
14
14
|
# carrying a whole run.
|
|
15
15
|
#
|
|
@@ -127,6 +127,21 @@ module SpecGuard
|
|
|
127
127
|
# Part of the platform's contract, so it is not configurable — the
|
|
128
128
|
# `endpoint` setting is the installation's address and nothing more.
|
|
129
129
|
PATH = "/api/v1/ingest"
|
|
130
|
+
|
|
131
|
+
# The repository-scoped form of the same endpoint (`post "ingest"` nested
|
|
132
|
+
# under `resources :repositories`). It exists because an `sga_` agent key
|
|
133
|
+
# covers a SET of repositories, so the credential cannot name the run's
|
|
134
|
+
# repository and the request has to, as a path segment. `%s` is only ever
|
|
135
|
+
# filled with an id that passed {REPOSITORY_ID_PATTERN}. Under an `sgk_`
|
|
136
|
+
# key the server ignores the segment, so {PATH} stays the default.
|
|
137
|
+
REPOSITORY_PATH = "/api/v1/repositories/%s/ingest"
|
|
138
|
+
|
|
139
|
+
# What a repository id may look like before it becomes a path segment.
|
|
140
|
+
# Deliberately not "digits": the platform's ids are numeric today and the
|
|
141
|
+
# gem must not bake that in. Dots are excluded outright, which is what
|
|
142
|
+
# refuses `..` (percent-escaping would not neutralise a dot segment), and
|
|
143
|
+
# `/`, `?`, `#` can never survive as path structure.
|
|
144
|
+
REPOSITORY_ID_PATTERN = /\A[0-9A-Za-z_-]+\z/
|
|
130
145
|
CONTENT_TYPE = "application/json"
|
|
131
146
|
USER_AGENT = "specguard-ruby/#{SpecGuard::VERSION}"
|
|
132
147
|
CONTENT_ENCODING = "gzip"
|
|
@@ -167,7 +182,12 @@ module SpecGuard
|
|
|
167
182
|
# same reason turned around: a 202 whose body will not parse is still an
|
|
168
183
|
# acceptance, and relabelling it would tell the operator something untrue
|
|
169
184
|
# about a run the platform has already stored. See {#test_run_id}.
|
|
170
|
-
|
|
185
|
+
#
|
|
186
|
+
# `repository_scoped` is true when the request went to the repository-
|
|
187
|
+
# scoped route. It only changes the 404 sentence: under that route a 404
|
|
188
|
+
# means no repository with that id is available to this key, not a wrong
|
|
189
|
+
# URL. Nil/false (every pre-existing `Result.new`) keeps today's wording.
|
|
190
|
+
Result = Struct.new(:outcome, :code, :error, :reasons, :body, :repository_scoped, keyword_init: true) do
|
|
171
191
|
# The status codes worth spelling out, because each implies a different
|
|
172
192
|
# thing for the reader to *do*. A 401 means "rotate or fix the key"; a
|
|
173
193
|
# 400 means "the payload this gem built was refused", which is a bug
|
|
@@ -193,6 +213,10 @@ module SpecGuard
|
|
|
193
213
|
# tells the reader how widespread it is.
|
|
194
214
|
MAX_RENDERED_REASONS = 3
|
|
195
215
|
|
|
216
|
+
# The 404 sentence when a repository id was sent. See `repository_scoped`.
|
|
217
|
+
REPOSITORY_NOT_FOUND_ADVICE =
|
|
218
|
+
"no repository with that id is available to this API key — check SPECGUARD_REPOSITORY_ID"
|
|
219
|
+
|
|
196
220
|
# Ceiling on the spelled-out reasons, before the `and N more` tail,
|
|
197
221
|
# which is appended afterwards so the count can never be the thing that
|
|
198
222
|
# gets truncated away.
|
|
@@ -224,13 +248,19 @@ module SpecGuard
|
|
|
224
248
|
def reason
|
|
225
249
|
case outcome
|
|
226
250
|
when :success then nil
|
|
227
|
-
when :rejected then [+"HTTP #{code}",
|
|
251
|
+
when :rejected then [+"HTTP #{code}", advice, rendered_reasons].compact.join(" — ")
|
|
228
252
|
else "#{error.class}: #{error.message}"
|
|
229
253
|
end
|
|
230
254
|
end
|
|
231
255
|
|
|
232
256
|
private
|
|
233
257
|
|
|
258
|
+
def advice
|
|
259
|
+
return REPOSITORY_NOT_FOUND_ADVICE if code == 404 && repository_scoped
|
|
260
|
+
|
|
261
|
+
ADVICE[code]
|
|
262
|
+
end
|
|
263
|
+
|
|
234
264
|
# The refusal, bounded and flattened to fit on the one line. `nil` when
|
|
235
265
|
# there is nothing readable to add, which is what keeps the bare
|
|
236
266
|
# `HTTP 400 — the endpoint rejected the payload` intact for every body
|
|
@@ -270,9 +300,15 @@ module SpecGuard
|
|
|
270
300
|
# @param timeout [Numeric, String, nil] seconds. Anything that is not a
|
|
271
301
|
# positive finite number falls back to the default rather than raising:
|
|
272
302
|
# a typo in `SPECGUARD_TIMEOUT` must not be able to fail a suite.
|
|
273
|
-
|
|
303
|
+
# @param repository_id [String, Integer, nil] when present, POST to the
|
|
304
|
+
# repository-scoped route an `sga_` agent key needs. nil/blank keeps the
|
|
305
|
+
# `sgk_` path byte-identical. An id outside {REPOSITORY_ID_PATTERN} makes
|
|
306
|
+
# {#uri} raise `ArgumentError`.
|
|
307
|
+
def initialize(endpoint:, api_key:, timeout: Configuration::DEFAULT_TIMEOUT_SECONDS, repository_id: nil)
|
|
274
308
|
@endpoint = endpoint
|
|
275
309
|
@api_key = api_key
|
|
310
|
+
@repository_id = repository_id.to_s.strip
|
|
311
|
+
@repository_id = nil if @repository_id.empty?
|
|
276
312
|
@timeout = sanitize_timeout(timeout)
|
|
277
313
|
end
|
|
278
314
|
|
|
@@ -301,7 +337,8 @@ module SpecGuard
|
|
|
301
337
|
return Result.new(outcome: :success, code: code, body: success_body(response)) if
|
|
302
338
|
response.is_a?(Net::HTTPSuccess)
|
|
303
339
|
|
|
304
|
-
Result.new(outcome: :rejected, code: code, reasons: refusal_reasons(response)
|
|
340
|
+
Result.new(outcome: :rejected, code: code, reasons: refusal_reasons(response),
|
|
341
|
+
repository_scoped: !@repository_id.nil?)
|
|
305
342
|
rescue ScriptError, StandardError => e
|
|
306
343
|
# Connection refused, DNS failure, TLS failure, open/read timeout, a
|
|
307
344
|
# malformed endpoint — one family, one shape. `ScriptError` is in the
|
|
@@ -427,7 +464,7 @@ module SpecGuard
|
|
|
427
464
|
base = @endpoint.to_s.strip
|
|
428
465
|
raise ArgumentError, "no endpoint is configured (set SPECGUARD_ENDPOINT)" if base.empty?
|
|
429
466
|
|
|
430
|
-
parsed = URI.parse(base.sub(%r{/+\z}, "") +
|
|
467
|
+
parsed = URI.parse(base.sub(%r{/+\z}, "") + ingest_path)
|
|
431
468
|
# `URI::HTTPS < URI::HTTP`, so this admits both and rejects the
|
|
432
469
|
# scheme-less `specguard.example.com` that `URI.parse` happily returns
|
|
433
470
|
# as a `URI::Generic` with a nil host — which `Net::HTTP` would then
|
|
@@ -438,6 +475,17 @@ module SpecGuard
|
|
|
438
475
|
parsed
|
|
439
476
|
end
|
|
440
477
|
|
|
478
|
+
def ingest_path
|
|
479
|
+
return PATH if @repository_id.nil?
|
|
480
|
+
|
|
481
|
+
unless REPOSITORY_ID_PATTERN.match?(@repository_id)
|
|
482
|
+
raise ArgumentError,
|
|
483
|
+
"SPECGUARD_REPOSITORY_ID must contain only letters, digits, '-' and '_', got #{@repository_id.inspect}"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
format(REPOSITORY_PATH, @repository_id)
|
|
487
|
+
end
|
|
488
|
+
|
|
441
489
|
def sanitize_timeout(value)
|
|
442
490
|
parsed = Float(value, exception: false)
|
|
443
491
|
return Configuration::DEFAULT_TIMEOUT_SECONDS unless parsed&.finite? && parsed.positive?
|
|
@@ -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,11 +243,12 @@ 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
|
-
timeout: @configuration.timeout
|
|
250
|
+
timeout: @configuration.timeout,
|
|
251
|
+
repository_id: @configuration.repository_id
|
|
252
252
|
)
|
|
253
253
|
end
|
|
254
254
|
|
|
@@ -406,12 +406,21 @@ module SpecGuard
|
|
|
406
406
|
# Report, not promise — see `#fall_back` for why that is the whole of
|
|
407
407
|
# this slice, and why the order above it did not move.
|
|
408
408
|
def sink_clause(path, error)
|
|
409
|
-
return
|
|
409
|
+
return replay_clause(path) if error.nil?
|
|
410
410
|
|
|
411
411
|
"The replay queue #{path} could not be written either " \
|
|
412
412
|
"(#{error.class}: #{error.message}), so this run's telemetry was lost."
|
|
413
413
|
end
|
|
414
414
|
|
|
415
|
+
# The success arm (SPGD-1139): names the configured path (the one
|
|
416
|
+
# `#append` just wrote) and the replay command, word-for-word the
|
|
417
|
+
# formatter's clause. The lost-run arm names no command — nothing to
|
|
418
|
+
# replay.
|
|
419
|
+
def replay_clause(path)
|
|
420
|
+
"Falling back to #{path}; the test run is unaffected. " \
|
|
421
|
+
"Once the delivery is fixed, replay this run with: bundle exec specguard-ingest #{path}"
|
|
422
|
+
end
|
|
423
|
+
|
|
415
424
|
# Once per process, like the formatter's `warn_once`: fifty failing
|
|
416
425
|
# deliveries is one problem, not fifty lines of it.
|
|
417
426
|
def warn_once(message)
|
|
@@ -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,10 +1037,11 @@ 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
|
-
timeout: configuration.timeout
|
|
1043
|
+
timeout: configuration.timeout,
|
|
1044
|
+
repository_id: configuration.repository_id
|
|
1038
1045
|
)
|
|
1039
1046
|
end
|
|
1040
1047
|
|
|
@@ -1196,12 +1203,22 @@ module SpecGuard
|
|
|
1196
1203
|
# which of the two things actually happened — see {#fall_back} for why that
|
|
1197
1204
|
# is the whole of this slice, and why the order above it did not move.
|
|
1198
1205
|
def sink_clause(path, error)
|
|
1199
|
-
return
|
|
1206
|
+
return replay_clause(path) if error.nil?
|
|
1200
1207
|
|
|
1201
1208
|
"The replay queue #{path} could not be written either " \
|
|
1202
1209
|
"(#{error.class}: #{error.message}), so this run's telemetry was lost."
|
|
1203
1210
|
end
|
|
1204
1211
|
|
|
1212
|
+
# The success arm (SPGD-1139): where the run sits AND the command that gets
|
|
1213
|
+
# it back, fix-the-delivery first and replay after — the order the ingest
|
|
1214
|
+
# CLI's own doctrine describes. The lost-run arm above deliberately names no
|
|
1215
|
+
# command: with no queue written there is nothing to replay. Kept
|
|
1216
|
+
# word-for-word identical to the Minitest reporter's clause.
|
|
1217
|
+
def replay_clause(path)
|
|
1218
|
+
"Falling back to #{path}; the test run is unaffected. " \
|
|
1219
|
+
"Once the delivery is fixed, replay this run with: bundle exec specguard-ingest #{path}"
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1205
1222
|
def emit_warning(line)
|
|
1206
1223
|
return if @warned
|
|
1207
1224
|
|
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.31
|
|
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-10-
|
|
11
|
+
date: 2026-10-04 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
|