specguard-rspec 0.2.1
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 +7 -0
- data/.github/workflows/release.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +735 -0
- data/Rakefile +28 -0
- data/assets/built-with-yatfa.png +0 -0
- data/bin/specguard-lint +34 -0
- data/lib/specguard/rspec/annotation_lookup.rb +286 -0
- data/lib/specguard/rspec/annotation_scanner.rb +144 -0
- data/lib/specguard/rspec/cli.rb +466 -0
- data/lib/specguard/rspec/configuration.rb +459 -0
- data/lib/specguard/rspec/file_selector.rb +276 -0
- data/lib/specguard/rspec/finding.rb +63 -0
- data/lib/specguard/rspec/formatter.rb +919 -0
- data/lib/specguard/rspec/json_reporter.rb +163 -0
- data/lib/specguard/rspec/linter.rb +134 -0
- data/lib/specguard/rspec/payload_normalizer.rb +139 -0
- data/lib/specguard/rspec/scanner.rb +217 -0
- data/lib/specguard/rspec/schema.rb +122 -0
- data/lib/specguard/rspec/schemas/open-test-intent.v1.json +15 -0
- data/lib/specguard/rspec/transport.rb +310 -0
- data/lib/specguard/rspec/validator_backend.rb +1071 -0
- data/lib/specguard/rspec/version.rb +7 -0
- data/lib/specguard/rspec/violation_renderer.rb +372 -0
- data/lib/specguard/rspec.rb +70 -0
- data/script/bump-version.sh +115 -0
- metadata +88 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpecGuard
|
|
4
|
+
module RSpec
|
|
5
|
+
# Where the checkout is sitting — which commit, and which branch — asked of
|
|
6
|
+
# git directly.
|
|
7
|
+
#
|
|
8
|
+
# This is the *last* resort for both, consulted only when no environment
|
|
9
|
+
# variable named the answer.
|
|
10
|
+
#
|
|
11
|
+
# The two questions carry different stakes and the same honesty
|
|
12
|
+
# requirement. `commit_sha` is the one envelope field the platform refuses
|
|
13
|
+
# a run without (`Ingest::Payload#validate_commit_sha` rejects a blank one
|
|
14
|
+
# and the whole POST comes back 400, every example discarded), so a run
|
|
15
|
+
# that cannot name its commit is a run whose telemetry is lost entirely —
|
|
16
|
+
# not one with a gap in it. A nil `branch`, by contrast, is *accepted*: the
|
|
17
|
+
# platform stores it, and renders it as "not reported". Which is precisely
|
|
18
|
+
# why a guess is worse here than a gap — see {BRANCH_COMMAND}.
|
|
19
|
+
#
|
|
20
|
+
# Three properties, all load-bearing, and both questions have all three:
|
|
21
|
+
#
|
|
22
|
+
# * It never raises. `git` may not be installed at all, in which case
|
|
23
|
+
# `IO.popen` raises `Errno::ENOENT` before a subprocess ever exists.
|
|
24
|
+
# * It never prints. `git rev-parse HEAD` outside a repository writes
|
|
25
|
+
# "fatal: not a git repository" to stderr, and a telemetry tool that
|
|
26
|
+
# graffitis somebody's CI log with a git error has already failed.
|
|
27
|
+
# * It answers once per process. Each result is memoized *separately*
|
|
28
|
+
# because a subprocess is expensive relative to everything else here,
|
|
29
|
+
# and neither answer can change mid-run.
|
|
30
|
+
module GitCheckout
|
|
31
|
+
COMMIT_SHA_COMMAND = %w[git rev-parse HEAD].freeze
|
|
32
|
+
|
|
33
|
+
# `symbolic-ref`, deliberately, and **not** `rev-parse --abbrev-ref HEAD`.
|
|
34
|
+
#
|
|
35
|
+
# On a detached checkout `--abbrev-ref` *succeeds*, exit 0, printing the
|
|
36
|
+
# literal string `"HEAD"`. `actions/checkout` detaches by default, so that
|
|
37
|
+
# command would report `branch: "HEAD"` for a large share of CI runs — a
|
|
38
|
+
# value the platform stores, renders in its Branch column, and groups by,
|
|
39
|
+
# indistinguishable from a repository that genuinely has a branch called
|
|
40
|
+
# `HEAD`. It is the wrong answer wearing the costume of a right one.
|
|
41
|
+
#
|
|
42
|
+
# `git symbolic-ref --short -q HEAD` asks the question actually being
|
|
43
|
+
# asked: *what branch is HEAD a symbolic reference to?* Detached, there is
|
|
44
|
+
# no answer, and it says so the honest way — no output, exit 1, and `-q`
|
|
45
|
+
# keeps it silent while doing it. {.resolve}'s existing `$?.success?`
|
|
46
|
+
# guard turns that straight into `nil`, which is what a detached checkout
|
|
47
|
+
# should report and what the platform already knows how to render.
|
|
48
|
+
BRANCH_COMMAND = %w[git symbolic-ref --short -q HEAD].freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
# @return [String, nil] the checked-out commit, or nil for any reason
|
|
52
|
+
# at all — no git, no repository, an empty repository, a broken HEAD.
|
|
53
|
+
def commit_sha
|
|
54
|
+
return @commit_sha if defined?(@commit_sha)
|
|
55
|
+
|
|
56
|
+
@commit_sha = resolve(COMMIT_SHA_COMMAND)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [String, nil] the branch the checkout is on, or nil for any
|
|
60
|
+
# reason at all — no git, no repository, and notably a **detached
|
|
61
|
+
# HEAD**, which is nil rather than the string "HEAD". See
|
|
62
|
+
# {BRANCH_COMMAND}.
|
|
63
|
+
def branch
|
|
64
|
+
return @branch if defined?(@branch)
|
|
65
|
+
|
|
66
|
+
@branch = resolve(BRANCH_COMMAND)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Drop the memoized answers — *both* of them. For tests, and for the
|
|
70
|
+
# rare caller that changes directory into a different checkout
|
|
71
|
+
# mid-process, where the commit and the branch have equally gone stale.
|
|
72
|
+
#
|
|
73
|
+
# @return [void]
|
|
74
|
+
def reset!
|
|
75
|
+
remove_instance_variable(:@commit_sha) if defined?(@commit_sha)
|
|
76
|
+
remove_instance_variable(:@branch) if defined?(@branch)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Shared by both questions so that the two guards below are written
|
|
83
|
+
# once and cannot drift apart — they are the whole of the never-raises
|
|
84
|
+
# and never-prints contract.
|
|
85
|
+
#
|
|
86
|
+
# @param command [Array<String>] argv, never a shell string
|
|
87
|
+
# @return [String, nil]
|
|
88
|
+
def resolve(command)
|
|
89
|
+
# `err: IO::NULL` is the "never prints" half: outside a repository
|
|
90
|
+
# either command writes the same "fatal: not a git repository" to
|
|
91
|
+
# stderr, and the exit status is the only part of that we want.
|
|
92
|
+
output = IO.popen(command, err: IO::NULL, &:read)
|
|
93
|
+
# A non-zero exit is a real answer for {BRANCH_COMMAND} rather than
|
|
94
|
+
# only an error: `symbolic-ref -q` exits 1 on a detached HEAD, and
|
|
95
|
+
# "no branch" is exactly what that should mean.
|
|
96
|
+
return nil unless $?&.success? # rubocop:disable Style/SpecialGlobalVars
|
|
97
|
+
|
|
98
|
+
value = output.to_s.strip
|
|
99
|
+
value.empty? ? nil : value
|
|
100
|
+
rescue ScriptError, StandardError
|
|
101
|
+
# `Errno::ENOENT` (no git binary) is the common one; the broad rescue
|
|
102
|
+
# is deliberate, because there is no failure here worth a raise.
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# What {SpecGuard::RSpecFormatter} needs to know about the run it is
|
|
109
|
+
# watching: which checkout produced it, and where to send what was captured.
|
|
110
|
+
#
|
|
111
|
+
# == Why the environment is the default source
|
|
112
|
+
#
|
|
113
|
+
# `commit_sha` and `branch` are facts about the *checkout*, not about the
|
|
114
|
+
# suite, and every CI provider already publishes them. Reading them from
|
|
115
|
+
# ENV means the common case — a CI job on any of the five major providers —
|
|
116
|
+
# needs no configuration at all. When no provider named either, both fall
|
|
117
|
+
# through to {GitCheckout}, which asks git itself — so a laptop run and a
|
|
118
|
+
# hand-rolled container report their checkout too, without being told to.
|
|
119
|
+
#
|
|
120
|
+
# The escape hatch stays open on top of all of it, for a value neither
|
|
121
|
+
# source can know:
|
|
122
|
+
#
|
|
123
|
+
# SpecGuard::RSpec.configure do |config|
|
|
124
|
+
# config.branch = "release/2.0"
|
|
125
|
+
# end
|
|
126
|
+
#
|
|
127
|
+
# An explicitly assigned value always wins; the ENV names and the git
|
|
128
|
+
# fallback are only consulted to seed the defaults.
|
|
129
|
+
#
|
|
130
|
+
# == Why an unset value is nil rather than an error
|
|
131
|
+
#
|
|
132
|
+
# Nothing here may ever be the reason a test run fails (see
|
|
133
|
+
# {SpecGuard::RSpecFormatter}'s never-block-CI contract). Somebody running
|
|
134
|
+
# `bundle exec rspec` on a laptop has no `GITHUB_SHA`, and that is not a
|
|
135
|
+
# misconfiguration to shout about — it is a run whose envelope honestly
|
|
136
|
+
# records "unknown commit". A blank or whitespace-only variable is treated
|
|
137
|
+
# the same as an unset one, because CI providers routinely export `""` for
|
|
138
|
+
# a variable that does not apply to the current event.
|
|
139
|
+
#
|
|
140
|
+
# == Transport, and what an unset `api_key` means
|
|
141
|
+
#
|
|
142
|
+
# `endpoint`, `api_key` and `timeout` configure the POST to SpecGuard's
|
|
143
|
+
# ingest endpoint. `api_key` is the switch: set it and the run is delivered
|
|
144
|
+
# over HTTP, leave it unset and the run is appended to `output_path` exactly
|
|
145
|
+
# as before. Local development is therefore the default, and it needs no
|
|
146
|
+
# opt-out — which is the whole reason the switch is the credential rather
|
|
147
|
+
# than a separate `enabled` flag nobody would remember to turn off.
|
|
148
|
+
#
|
|
149
|
+
# There is deliberately **no default endpoint**. SpecGuard is self-hostable
|
|
150
|
+
# and there is no address that is right for everybody; guessing one would
|
|
151
|
+
# mean a misconfigured project POSTing its test names to whatever happens to
|
|
152
|
+
# answer at that name.
|
|
153
|
+
class Configuration
|
|
154
|
+
# Relative to the working directory the suite was started from, which is
|
|
155
|
+
# the project root for every normal `bundle exec rspec` invocation.
|
|
156
|
+
DEFAULT_OUTPUT_PATH = "log/test_results.jsonl"
|
|
157
|
+
|
|
158
|
+
# Applied to opening the connection and to reading the response.
|
|
159
|
+
#
|
|
160
|
+
# `Net::HTTP`'s own defaults are 60s each, which is up to two minutes
|
|
161
|
+
# added to every CI run against a hung endpoint — squarely against the
|
|
162
|
+
# roadmap's "a 20,000-example run must not be meaningfully slowed". Ten
|
|
163
|
+
# seconds is the number the client-gem spec settles on, and it is the
|
|
164
|
+
# whole budget: there are **no retries**, because a retry doubles the
|
|
165
|
+
# worst case for telemetry that is explicitly allowed to be lost.
|
|
166
|
+
DEFAULT_TIMEOUT_SECONDS = 10
|
|
167
|
+
|
|
168
|
+
# Checked in order, first non-blank wins. The `SPECGUARD_`-prefixed name
|
|
169
|
+
# comes first everywhere so a project can always override whatever its CI
|
|
170
|
+
# provider decided to export.
|
|
171
|
+
#
|
|
172
|
+
# The provider list is not decoration. `commit_sha` is the one field the
|
|
173
|
+
# platform rejects a run over, so a list that only knew `GITHUB_SHA` meant
|
|
174
|
+
# every GitLab, CircleCI, Buildkite and Jenkins run 400ing as a matter of
|
|
175
|
+
# course — the *default* outcome on four of the five providers this gem is
|
|
176
|
+
# likely to meet.
|
|
177
|
+
COMMIT_SHA_KEYS = %w[
|
|
178
|
+
SPECGUARD_COMMIT_SHA
|
|
179
|
+
GITHUB_SHA
|
|
180
|
+
CI_COMMIT_SHA
|
|
181
|
+
CIRCLE_SHA1
|
|
182
|
+
BUILDKITE_COMMIT
|
|
183
|
+
GIT_COMMIT
|
|
184
|
+
].freeze
|
|
185
|
+
|
|
186
|
+
# The same providers, in the same order. `GITHUB_REF_NAME` is the short
|
|
187
|
+
# name (`main`), not the full ref (`refs/heads/main`) that `GITHUB_REF`
|
|
188
|
+
# carries; Jenkins's `GIT_BRANCH` is passed through as it comes
|
|
189
|
+
# (`origin/main` on many setups), because `branch` is a free-form string
|
|
190
|
+
# to the platform and inventing a normalization here would be this gem
|
|
191
|
+
# guessing at somebody's ref layout.
|
|
192
|
+
BRANCH_KEYS = %w[
|
|
193
|
+
SPECGUARD_BRANCH
|
|
194
|
+
GITHUB_REF_NAME
|
|
195
|
+
CI_COMMIT_REF_NAME
|
|
196
|
+
CIRCLE_BRANCH
|
|
197
|
+
BUILDKITE_BRANCH
|
|
198
|
+
GIT_BRANCH
|
|
199
|
+
].freeze
|
|
200
|
+
|
|
201
|
+
# The id the CI provider gave the *build*, which every shard of a sharded
|
|
202
|
+
# run shares.
|
|
203
|
+
#
|
|
204
|
+
# This is the field that makes a 20,000-example suite land as one run.
|
|
205
|
+
# Nobody runs a suite that size in a single process: under
|
|
206
|
+
# `parallel_tests`, Knapsack or a CI matrix each shard loads this
|
|
207
|
+
# formatter and POSTs its own slice, and every shard carries the same
|
|
208
|
+
# `commit_sha` and `branch` — so the platform had nothing to tell four
|
|
209
|
+
# shards of one run apart from four separate runs, and recorded four
|
|
210
|
+
# `TestRun` rows each holding a quarter of the denominator. Annotations
|
|
211
|
+
# tend to cluster in whatever area a team is currently working on rather
|
|
212
|
+
# than spreading evenly across shards, so the headline ratio then moved
|
|
213
|
+
# from re-run to re-run without the suite changing at all.
|
|
214
|
+
#
|
|
215
|
+
# == It identifies the run, and deliberately survives a re-run
|
|
216
|
+
#
|
|
217
|
+
# These ids are stable across re-attempts of the same build, and that is
|
|
218
|
+
# documented behaviour rather than an accident. GitHub Actions, verbatim:
|
|
219
|
+
# `GITHUB_RUN_ID` is *"A unique number for each workflow run within a
|
|
220
|
+
# repository. This number does not change if you re-run the workflow
|
|
221
|
+
# run."* (`GITHUB_RUN_ATTEMPT` is the value that increments per attempt.)
|
|
222
|
+
# Buildkite retries a job inside the same `BUILDKITE_BUILD_ID`, bumping
|
|
223
|
+
# `BUILDKITE_RETRY_COUNT`; GitLab retries a job inside the same
|
|
224
|
+
# `CI_PIPELINE_ID`.
|
|
225
|
+
#
|
|
226
|
+
# **The attempt is deliberately not part of this id**, and that is the
|
|
227
|
+
# correction this field carries over its first implementation. Pressing
|
|
228
|
+
# "re-run failed jobs" — the mainline recovery gesture for a sharded
|
|
229
|
+
# suite, because re-running all 20,000 examples for one flaky shard is
|
|
230
|
+
# the thing sharding exists to avoid — re-runs a *subset* of shards. Had
|
|
231
|
+
# the attempt been folded in, that subset would have formed a brand-new
|
|
232
|
+
# run holding only the shards that happened to be retried, which is the
|
|
233
|
+
# split denominator this whole field exists to abolish, one gesture
|
|
234
|
+
# further down. Keeping the id stable instead lets a retried shard land
|
|
235
|
+
# back on the run it belongs to and *replace* its own previous slice.
|
|
236
|
+
#
|
|
237
|
+
# That replacement is what {SHARD_ID_KEYS} is for, and the two fields are
|
|
238
|
+
# only correct together: without a shard identity the platform can add a
|
|
239
|
+
# re-delivered slice but cannot recognise it, so a re-run inflates the
|
|
240
|
+
# denominator instead of refreshing it.
|
|
241
|
+
#
|
|
242
|
+
# Same providers in the same order as above, and the same "unset is nil,
|
|
243
|
+
# never an error" rule — a laptop run has none of these, and the platform
|
|
244
|
+
# treats a run with no id as its own run, which is exactly right.
|
|
245
|
+
#
|
|
246
|
+
# The requirement on whatever a provider exports is only this: every shard
|
|
247
|
+
# of one run reads the same value, and no *other* run reads it. A project
|
|
248
|
+
# whose CI layout does not satisfy that for the variable below — Jenkins
|
|
249
|
+
# `BUILD_TAG` interpolates `JOB_NAME`, which some matrix layouts vary per
|
|
250
|
+
# axis — sets `SPECGUARD_RUN_ID` itself, which wins over all of them.
|
|
251
|
+
RUN_ID_KEYS = %w[
|
|
252
|
+
SPECGUARD_RUN_ID
|
|
253
|
+
GITHUB_RUN_ID
|
|
254
|
+
CI_PIPELINE_ID
|
|
255
|
+
CIRCLE_WORKFLOW_ID
|
|
256
|
+
BUILDKITE_BUILD_ID
|
|
257
|
+
BUILD_TAG
|
|
258
|
+
].freeze
|
|
259
|
+
|
|
260
|
+
# Which shard of the run this process is.
|
|
261
|
+
#
|
|
262
|
+
# The platform keys each slice of a run by this, so a shard that reports
|
|
263
|
+
# twice — a retried job, a re-run of the whole build — replaces its own
|
|
264
|
+
# previous numbers rather than adding to them. Without it, ingest can
|
|
265
|
+
# only ever add, and "re-run failed jobs" reports a suite larger than the
|
|
266
|
+
# suite.
|
|
267
|
+
#
|
|
268
|
+
# It only has to be unique *within one run*; it is never compared across
|
|
269
|
+
# runs, so a bare parallel-process index is a perfectly good value.
|
|
270
|
+
#
|
|
271
|
+
# `nil` is allowed and is not an error: the slice is still counted, it
|
|
272
|
+
# just cannot be recognised if it arrives twice. See the "anonymous
|
|
273
|
+
# contribution" note in the platform's `Ingest::RunRecorder` for exactly
|
|
274
|
+
# what that costs.
|
|
275
|
+
#
|
|
276
|
+
# == Two traps that are the reason this is not a plain key list
|
|
277
|
+
#
|
|
278
|
+
# 1. `parallel_tests` — by far the most likely way a Ruby suite is
|
|
279
|
+
# sharded — sets `TEST_ENV_NUMBER` to the **empty string** for its
|
|
280
|
+
# first process (`''`, `'2'`, `'3'`, … so that the first process uses
|
|
281
|
+
# the plain `yourproject_test` database). A first-non-blank rule would
|
|
282
|
+
# therefore skip straight past it and leave process 1, and only
|
|
283
|
+
# process 1, anonymous — the single hardest version of this bug to
|
|
284
|
+
# see, because three shards of four would be idempotent. Presence of
|
|
285
|
+
# the variable is the signal here, not its value, so a set-but-empty
|
|
286
|
+
# `TEST_ENV_NUMBER` resolves to `"1"`. See {#resolve_shard_id}.
|
|
287
|
+
#
|
|
288
|
+
# 2. GitHub Actions exports **no** per-leg index for a `matrix:` job.
|
|
289
|
+
# `GITHUB_JOB` is the job's id as written in the workflow YAML and is
|
|
290
|
+
# identical across every leg of the matrix, so it is not in this list —
|
|
291
|
+
# it would make all N shards claim to be the same shard, and the run
|
|
292
|
+
# would keep only whichever finished last. A matrix-sharded suite sets
|
|
293
|
+
# `SPECGUARD_SHARD_ID` from the matrix value itself, e.g.
|
|
294
|
+
# `SPECGUARD_SHARD_ID: ${{ matrix.shard }}`.
|
|
295
|
+
#
|
|
296
|
+
# The same rule applies to any nesting: `parallel_tests` *inside* a
|
|
297
|
+
# matrix leg makes `TEST_ENV_NUMBER` repeat across legs, so those
|
|
298
|
+
# projects set `SPECGUARD_SHARD_ID` to something that composes both.
|
|
299
|
+
SHARD_ID_KEYS = %w[
|
|
300
|
+
SPECGUARD_SHARD_ID
|
|
301
|
+
TEST_ENV_NUMBER
|
|
302
|
+
CI_NODE_INDEX
|
|
303
|
+
CIRCLE_NODE_INDEX
|
|
304
|
+
BUILDKITE_PARALLEL_JOB
|
|
305
|
+
].freeze
|
|
306
|
+
|
|
307
|
+
# The one key in {SHARD_ID_KEYS} whose empty value is meaningful rather
|
|
308
|
+
# than absent. See trap 1 above.
|
|
309
|
+
BLANK_MEANS_FIRST_SHARD_KEY = "TEST_ENV_NUMBER"
|
|
310
|
+
|
|
311
|
+
OUTPUT_PATH_KEYS = %w[SPECGUARD_OUTPUT_PATH].freeze
|
|
312
|
+
ENDPOINT_KEYS = %w[SPECGUARD_ENDPOINT].freeze
|
|
313
|
+
API_KEY_KEYS = %w[SPECGUARD_API_KEY].freeze
|
|
314
|
+
TIMEOUT_KEYS = %w[SPECGUARD_TIMEOUT].freeze
|
|
315
|
+
|
|
316
|
+
# The commit the suite ran against. `nil` when nothing said.
|
|
317
|
+
attr_accessor :commit_sha
|
|
318
|
+
# The branch the suite ran on. `nil` when nothing said — including a
|
|
319
|
+
# detached checkout, which has no branch to name and says so rather than
|
|
320
|
+
# inventing one. See {GitCheckout::BRANCH_COMMAND}.
|
|
321
|
+
attr_accessor :branch
|
|
322
|
+
# The CI provider's id for the build this process is one shard of.
|
|
323
|
+
# `nil` when nothing said, which is how a laptop run spells "I am my own
|
|
324
|
+
# run". See {RUN_ID_KEYS}.
|
|
325
|
+
attr_accessor :run_id
|
|
326
|
+
# Which shard of that build this process is. `nil` when nothing said.
|
|
327
|
+
# See {SHARD_ID_KEYS}.
|
|
328
|
+
attr_accessor :shard_id
|
|
329
|
+
# Where the run's JSON payload is appended, one object per line — the
|
|
330
|
+
# local sink when there is no API key, and the fallback when delivery
|
|
331
|
+
# fails.
|
|
332
|
+
attr_accessor :output_path
|
|
333
|
+
# The SpecGuard installation to POST to, scheme and host only:
|
|
334
|
+
# `https://specguard.example.com`. The `/api/v1/ingest` path is the
|
|
335
|
+
# platform's contract, not a setting. `nil` when nothing said.
|
|
336
|
+
attr_accessor :endpoint
|
|
337
|
+
# The repository's SpecGuard API key. Present means "deliver over HTTP".
|
|
338
|
+
#
|
|
339
|
+
# Deliberately **not** format-checked. The platform mints `sgk_`-prefixed
|
|
340
|
+
# keys today; a client that gated on that prefix would start rejecting
|
|
341
|
+
# valid keys the day the platform rotated it, from a version of the gem
|
|
342
|
+
# nobody can retroactively fix. Send whatever is configured and let a 401
|
|
343
|
+
# be the answer.
|
|
344
|
+
attr_accessor :api_key
|
|
345
|
+
# Seconds, applied to opening the connection and to reading the response.
|
|
346
|
+
attr_accessor :timeout
|
|
347
|
+
|
|
348
|
+
# @param env [#[]] the environment to seed defaults from. Injectable so
|
|
349
|
+
# the mapping can be tested without mutating the process's own ENV.
|
|
350
|
+
# @param git [#commit_sha, #branch] the checkout to fall back to when no
|
|
351
|
+
# variable named the commit or the branch. Injectable for the same
|
|
352
|
+
# reason, and so a test does not silently pick up the sha or the branch
|
|
353
|
+
# of whatever repository it runs in.
|
|
354
|
+
def initialize(env: ENV, git: GitCheckout)
|
|
355
|
+
@commit_sha = first_present(env, COMMIT_SHA_KEYS) || blank_to_nil(git.commit_sha)
|
|
356
|
+
# `||` short-circuits, which is the whole of "do not spawn a subprocess
|
|
357
|
+
# when a provider already answered".
|
|
358
|
+
@branch = first_present(env, BRANCH_KEYS) || blank_to_nil(git.branch)
|
|
359
|
+
@run_id = first_present(env, RUN_ID_KEYS)
|
|
360
|
+
@shard_id = resolve_shard_id(env)
|
|
361
|
+
@output_path = first_present(env, OUTPUT_PATH_KEYS) || DEFAULT_OUTPUT_PATH
|
|
362
|
+
@endpoint = first_present(env, ENDPOINT_KEYS)
|
|
363
|
+
@api_key = first_present(env, API_KEY_KEYS)
|
|
364
|
+
@timeout = seconds(first_present(env, TIMEOUT_KEYS)) || DEFAULT_TIMEOUT_SECONDS
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
private
|
|
368
|
+
|
|
369
|
+
# {SHARD_ID_KEYS} in order, first non-blank wins — except that a
|
|
370
|
+
# {BLANK_MEANS_FIRST_SHARD_KEY} which is *set* counts as a hit even when
|
|
371
|
+
# it is empty, and resolves to `"1"`.
|
|
372
|
+
#
|
|
373
|
+
# `parallel_tests` numbers its processes `''`, `'2'`, `'3'`, … , so its
|
|
374
|
+
# first process is the one case where the empty string is a real answer
|
|
375
|
+
# rather than "nothing said". Reading it as "nothing said" would leave
|
|
376
|
+
# exactly one shard of a run anonymous while its siblings were
|
|
377
|
+
# identified, and the platform would then double-count that shard alone
|
|
378
|
+
# on a re-run — a wrong denominator that moves by a fifth of a suite and
|
|
379
|
+
# points at nothing.
|
|
380
|
+
#
|
|
381
|
+
# `--first-is-1` / `PARALLEL_TEST_FIRST_IS_1` make `parallel_tests` emit
|
|
382
|
+
# a literal `"1"` for that process, which the ordinary path already
|
|
383
|
+
# handles and which lands on the same value, so both configurations agree
|
|
384
|
+
# on what shard 1 is called.
|
|
385
|
+
def resolve_shard_id(env)
|
|
386
|
+
SHARD_ID_KEYS.each do |key|
|
|
387
|
+
value = env[key]
|
|
388
|
+
next if value.nil?
|
|
389
|
+
|
|
390
|
+
stripped = value.to_s.strip
|
|
391
|
+
return stripped unless stripped.empty?
|
|
392
|
+
return "1" if key == BLANK_MEANS_FIRST_SHARD_KEY
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def first_present(env, keys)
|
|
399
|
+
keys.each do |key|
|
|
400
|
+
value = env[key].to_s.strip
|
|
401
|
+
return value unless value.empty?
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def blank_to_nil(value)
|
|
408
|
+
string = value.to_s.strip
|
|
409
|
+
string.empty? ? nil : string
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# A garbage `SPECGUARD_TIMEOUT` falls back to the default rather than
|
|
413
|
+
# raising or, worse, being coerced to `0` by `String#to_i` — which
|
|
414
|
+
# `Net::HTTP` reads as "time out immediately" and would turn a typo into
|
|
415
|
+
# a run that never delivers anything.
|
|
416
|
+
def seconds(value)
|
|
417
|
+
parsed = Float(value, exception: false)
|
|
418
|
+
return nil unless parsed&.finite? && parsed.positive?
|
|
419
|
+
|
|
420
|
+
parsed
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
class << self
|
|
425
|
+
# The process-wide formatter configuration.
|
|
426
|
+
#
|
|
427
|
+
# Built on first use rather than at load time so that a `configure` block
|
|
428
|
+
# in a `spec_helper.rb` and a variable exported by a CI job describe the
|
|
429
|
+
# same object regardless of which the interpreter reached first.
|
|
430
|
+
#
|
|
431
|
+
# @return [Configuration]
|
|
432
|
+
def configuration
|
|
433
|
+
@configuration ||= Configuration.new
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Configure the formatter.
|
|
437
|
+
#
|
|
438
|
+
# SpecGuard::RSpec.configure do |config|
|
|
439
|
+
# config.branch = "release/2.0"
|
|
440
|
+
# end
|
|
441
|
+
#
|
|
442
|
+
# @yieldparam configuration [Configuration]
|
|
443
|
+
# @return [Configuration] the configuration, block or no block
|
|
444
|
+
def configure
|
|
445
|
+
yield configuration if block_given?
|
|
446
|
+
configuration
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Drop the memoized configuration so the next read re-seeds from ENV.
|
|
450
|
+
# Exists for tests, and for the rare caller that changes the environment
|
|
451
|
+
# after this file was loaded.
|
|
452
|
+
#
|
|
453
|
+
# @return [void]
|
|
454
|
+
def reset_configuration!
|
|
455
|
+
@configuration = nil
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|