specguard-ruby 0.3.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/ci.yml +49 -0
- data/.github/workflows/release.yml +100 -0
- data/LICENSE +21 -0
- data/README.md +1036 -0
- data/Rakefile +28 -0
- data/assets/built-with-yatfa.png +0 -0
- data/bin/specguard-ingest +40 -0
- data/bin/specguard-lint +34 -0
- data/lib/minitest/specguard_plugin.rb +25 -0
- data/lib/specguard/minitest/reporter.rb +244 -0
- data/lib/specguard/rspec/annotation_lookup.rb +363 -0
- data/lib/specguard/rspec/annotation_scanner.rb +190 -0
- data/lib/specguard/rspec/cli.rb +375 -0
- data/lib/specguard/rspec/configuration.rb +472 -0
- data/lib/specguard/rspec/file_selector.rb +276 -0
- data/lib/specguard/rspec/finding.rb +74 -0
- data/lib/specguard/rspec/formatter.rb +1110 -0
- data/lib/specguard/rspec/ingest_cli.rb +942 -0
- data/lib/specguard/rspec/ingest_reporter.rb +280 -0
- data/lib/specguard/rspec/json_reporter.rb +169 -0
- data/lib/specguard/rspec/linter.rb +156 -0
- data/lib/specguard/rspec/payload_normalizer.rb +143 -0
- data/lib/specguard/rspec/scanner.rb +235 -0
- data/lib/specguard/rspec/schemas/open-test-intent.v1.json +15 -0
- data/lib/specguard/rspec/transport.rb +449 -0
- data/lib/specguard/rspec/validator_backend.rb +1450 -0
- data/lib/specguard/rspec/version.rb +14 -0
- data/lib/specguard/rspec.rb +71 -0
- data/lib/specguard/version.rb +11 -0
- data/script/bump-version.sh +128 -0
- metadata +96 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module SpecGuard
|
|
6
|
+
module RSpec
|
|
7
|
+
# `specguard-lint`'s command line, and the whole of the exit contract.
|
|
8
|
+
#
|
|
9
|
+
# == The contract, and the reason it needs defending
|
|
10
|
+
#
|
|
11
|
+
# 0 every annotation checked is valid (including "there were none")
|
|
12
|
+
# 1 at least one annotation is malformed
|
|
13
|
+
# 2 the linter could not do its job — misuse, or the tool itself broken
|
|
14
|
+
#
|
|
15
|
+
# Ruby does not give you this for free; it actively works against it.
|
|
16
|
+
# `ruby -e 'raise "boom"'` exits **1**, and so does an uncaught
|
|
17
|
+
# `OptionParser::InvalidOption`. So on the obvious implementation, every
|
|
18
|
+
# internal failure lands on the one code the contract has already spent on
|
|
19
|
+
# "an annotation is malformed":
|
|
20
|
+
#
|
|
21
|
+
# * `specguard-lint --chnaged` — a typo — would exit 1, and CI would
|
|
22
|
+
# report a malformed annotation that does not exist;
|
|
23
|
+
# * a vendored schema missing from the packaged gem would exit 1, the
|
|
24
|
+
# same false accusation, in the field, on someone else's machine.
|
|
25
|
+
#
|
|
26
|
+
# The project has shipped this defect shape repeatedly — SPGD-35, SPGD-52,
|
|
27
|
+
# SPGD-56 — but always as **false green**: a gate reporting success having
|
|
28
|
+
# checked nothing. This is its inverse, **false red**: a tool failure
|
|
29
|
+
# wearing the costume of a content failure. Both are the same underlying
|
|
30
|
+
# bug, a gate whose failure states are indistinguishable, and for a linter
|
|
31
|
+
# the exit code *is* the product.
|
|
32
|
+
#
|
|
33
|
+
# {#run} therefore rescues in two bands and returns rather than exits:
|
|
34
|
+
# {UsageError} and {ValidatorError} are named, and everything else that is
|
|
35
|
+
# not a deliberate interruption is caught by a backstop. Exit 1 is produced
|
|
36
|
+
# in exactly one place — a failed {Linter::Result} — so it means that and
|
|
37
|
+
# nothing else.
|
|
38
|
+
#
|
|
39
|
+
# `Interrupt`, `SignalException` and `SystemExit` are deliberately *not*
|
|
40
|
+
# caught. Ctrl-C must stay Ctrl-C; mapping it to "the linter is broken"
|
|
41
|
+
# would be its own small lie.
|
|
42
|
+
#
|
|
43
|
+
# == All failures, not the first
|
|
44
|
+
#
|
|
45
|
+
# SPGD-12 §1 step 4 says the linter "exits 1 on the *first* malformed
|
|
46
|
+
# annotation". Its own exit-code table, one paragraph later, says "1 | One
|
|
47
|
+
# or more annotations are malformed", and the validator reports every one:
|
|
48
|
+
# `validate-intent --source broken_intent_spec.rb` emits 5 FAIL blocks. Stopping at the first would turn one file into five CI
|
|
49
|
+
# round-trips. Reporting all of them is the ratified behaviour (human
|
|
50
|
+
# decision recorded on SPGD-82); the "first" wording is a known spec defect
|
|
51
|
+
# with a correction filed against SPGD-12 §1 and the SPGD-73 roadmap text.
|
|
52
|
+
#
|
|
53
|
+
# == One validator, one report
|
|
54
|
+
#
|
|
55
|
+
# Since the SPGD-867 cutover there is exactly one validator: the
|
|
56
|
+
# `validate-intent` binary {ValidatorBackend} resolves (an explicit
|
|
57
|
+
# `SPECGUARD_VALIDATE_INTENT` path, or the first-run auto-install). The
|
|
58
|
+
# Ruby hand-rolled validation arm is gone — when no binary can be resolved
|
|
59
|
+
# the run exits 2 naming both remediations, and it NEVER silently
|
|
60
|
+
# validates some other way. The verdicts arrive as {Linter::Result}s, and
|
|
61
|
+
# the reporting and exit-code logic is shared with nothing because there
|
|
62
|
+
# is nothing to share it with.
|
|
63
|
+
#
|
|
64
|
+
# Because both arms produce the same bytes, the run has to SAY which one it
|
|
65
|
+
# was, or the answer is unrecoverable from the output — see
|
|
66
|
+
# {#report_backend}, which states it in one line on stderr on both arms.
|
|
67
|
+
# That line is the only thing the default configuration gained: stdout, the
|
|
68
|
+
# exit code and every finding are what they were before this file learned
|
|
69
|
+
# about a second validator.
|
|
70
|
+
#
|
|
71
|
+
# The backend's failure modes are exit 2 by construction: {ValidatorError}
|
|
72
|
+
# is rescued beside {UsageError}, which is what makes "the binary you named
|
|
73
|
+
# is missing" read as `specguard-lint: error: …` rather than reaching the
|
|
74
|
+
# backstop and reading as an `internal error:`. Either way it is a 2, and
|
|
75
|
+
# that is the property that matters — a broken tool must not borrow the
|
|
76
|
+
# code that means "your annotations are malformed".
|
|
77
|
+
#
|
|
78
|
+
# == Two renderers, and what `--json` does NOT touch
|
|
79
|
+
#
|
|
80
|
+
# `--json` (SPGD-305) replaces the human report on stdout with one JSON
|
|
81
|
+
# document over the very same {Linter::Result} list — see {JSONReporter}.
|
|
82
|
+
# It is a renderer, so the three things that are the contract are untouched
|
|
83
|
+
# by it: the exit code (the decision below is one expression, evaluated on
|
|
84
|
+
# both paths), stderr (the provenance line and every warning are byte-for-
|
|
85
|
+
# byte what they were), and the default path (without the flag, stdout is
|
|
86
|
+
# what it was, pinned as a regression lock in
|
|
87
|
+
# `spec/specguard/rspec/regression_targets_spec.rb`).
|
|
88
|
+
#
|
|
89
|
+
# No exit-2 path emits a document, and that is a decision rather than an
|
|
90
|
+
# omission. Every `rescue` below means the linter produced NO VERDICTS —
|
|
91
|
+
# bad flags, `--changed` outside a repository, a validator that could not
|
|
92
|
+
# be resolved or produced no verdict. A document is a report about what was
|
|
93
|
+
# checked; emitting `{"ok": false, "findings": []}` for a run that checked
|
|
94
|
+
# nothing would hand a stdout-reading consumer the project's signature
|
|
95
|
+
# defect — an empty clean-looking report standing in for "could not check" —
|
|
96
|
+
# and dressing it as structure would make it *more* convincing, not less.
|
|
97
|
+
# Those runs write prose to stderr, where diagnostics about the linter
|
|
98
|
+
# already live, and say what happened with the exit code.
|
|
99
|
+
class CLI
|
|
100
|
+
BANNER = "Usage: specguard-lint [options] [files...]"
|
|
101
|
+
|
|
102
|
+
# Every annotation checked was valid — or there were none to check.
|
|
103
|
+
# "Lint, don't require": a missing annotation is never an error.
|
|
104
|
+
EXIT_OK = 0
|
|
105
|
+
# One or more annotations are malformed. The only code produced by
|
|
106
|
+
# inspecting content, and the only path that reaches it is a failed
|
|
107
|
+
# {Linter::Result}.
|
|
108
|
+
EXIT_MALFORMED = 1
|
|
109
|
+
# The linter could not do its job: bad flags, `--changed` outside a git
|
|
110
|
+
# repository, a validator that could not be resolved, or an unexpected
|
|
111
|
+
# internal error.
|
|
112
|
+
EXIT_MISUSE = 2
|
|
113
|
+
|
|
114
|
+
def initialize(stdout: $stdout, stderr: $stderr, env: ENV)
|
|
115
|
+
@stdout = stdout
|
|
116
|
+
@stderr = stderr
|
|
117
|
+
@env = env
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @param argv [Array<String>]
|
|
121
|
+
# @return [Integer] 0, 1 or 2 — never anything else, and never by
|
|
122
|
+
# letting an exception reach the shell
|
|
123
|
+
def run(argv)
|
|
124
|
+
options = parse_options(argv)
|
|
125
|
+
return EXIT_OK if options.nil? # --help / --version already printed
|
|
126
|
+
|
|
127
|
+
# Resolved — or the run has already exited 2 — before anything is
|
|
128
|
+
# selected or scanned, for the same reason it always was: "the
|
|
129
|
+
# validator could not be obtained" must never surface as a run that
|
|
130
|
+
# checked nothing and called itself clean. There is no Ruby arm to
|
|
131
|
+
# fall back to, so a resolution failure IS the run's failure.
|
|
132
|
+
backend = ValidatorBackend.resolve(env: @env)
|
|
133
|
+
|
|
134
|
+
# One line per run naming the implementation that produced the
|
|
135
|
+
# verdicts. Since the cutover there is exactly one implementation.
|
|
136
|
+
report_backend(backend)
|
|
137
|
+
|
|
138
|
+
selection = select(options)
|
|
139
|
+
report_selection(selection, json: options[:json])
|
|
140
|
+
|
|
141
|
+
results = backend.check(selection.files)
|
|
142
|
+
|
|
143
|
+
# Computed once, here, and handed to whichever renderer runs. `--json`
|
|
144
|
+
# is a second renderer over this list, not a second code path: the exit
|
|
145
|
+
# code below is the same expression it always was, and the document's
|
|
146
|
+
# `ok` is derived FROM it rather than recomputed from the findings, so
|
|
147
|
+
# the two renderers cannot disagree about whether the run passed.
|
|
148
|
+
code = results.any?(&:failed?) ? EXIT_MALFORMED : EXIT_OK
|
|
149
|
+
report_results(results, files: selection.count, json: options[:json], ok: code == EXIT_OK)
|
|
150
|
+
|
|
151
|
+
code
|
|
152
|
+
rescue UsageError, ValidatorError => e
|
|
153
|
+
@stderr.puts "specguard-lint: error: #{e.message}"
|
|
154
|
+
EXIT_MISUSE
|
|
155
|
+
rescue ScriptError, StandardError => e
|
|
156
|
+
# The backstop that makes exit 1 mean one thing. Anything reaching here
|
|
157
|
+
# is a bug in the linter, not a verdict about anyone's annotations, so
|
|
158
|
+
# it is a 2 and it says so in those words.
|
|
159
|
+
@stderr.puts "specguard-lint: internal error: #{e.class}: #{e.message}"
|
|
160
|
+
EXIT_MISUSE
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# One line per run naming the implementation that produced the verdicts.
|
|
166
|
+
# Since the SPGD-867 cutover there is exactly one implementation — the
|
|
167
|
+
# resolved `validate-intent` binary — and the line says which one,
|
|
168
|
+
# including its identity and schema contract, so "which validator did
|
|
169
|
+
# this CI job actually run" stays answerable from the output.
|
|
170
|
+
#
|
|
171
|
+
# It is on STDERR: the findings and the two `checked …` lines are the
|
|
172
|
+
# product and are pinned byte-for-byte; a line about the linter's own
|
|
173
|
+
# configuration belongs where the warnings are.
|
|
174
|
+
def report_backend(backend)
|
|
175
|
+
@stderr.puts "specguard-lint: #{backend.provenance}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Naming files and asking for the diff are contradictory instructions,
|
|
179
|
+
# and honouring the first while dropping the second silently is the same
|
|
180
|
+
# class of quiet no-op this tool exists to remove: `--changed` would
|
|
181
|
+
# appear to have been applied. That is misuse — exit 2.
|
|
182
|
+
def select(options)
|
|
183
|
+
if options[:files].any?
|
|
184
|
+
if options[:changed]
|
|
185
|
+
raise UsageError,
|
|
186
|
+
"--changed cannot be combined with explicit files; drop one " \
|
|
187
|
+
"(named files are checked as given, --changed derives them from the diff)"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
return FileSelector::Selection.new(files: options[:files], mode: :explicit)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
FileSelector.select(changed: options[:changed], base: options[:base], root: options[:root])
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The honest-reporting half of the `--changed` fix: the selected-file
|
|
197
|
+
# count is always stated, and an empty selection is loud on stderr, so
|
|
198
|
+
# "checked 12 files, found nothing" can never be mistaken for
|
|
199
|
+
# "checked nothing". The exit code is not the lever here — the contract
|
|
200
|
+
# fixes 0 for "no annotations" — which is exactly why the warning has to
|
|
201
|
+
# carry the weight.
|
|
202
|
+
#
|
|
203
|
+
# Under `--json` the count is not dropped, it MOVES: stdout carries one
|
|
204
|
+
# document and nothing else, and this line's number is that document's
|
|
205
|
+
# `summary.files`. The warnings are diagnostics about the linter rather
|
|
206
|
+
# than findings, so they stay on stderr exactly as they are — a run that
|
|
207
|
+
# selected nothing is still loud, in both renderers.
|
|
208
|
+
def report_selection(selection, json:)
|
|
209
|
+
if selection.empty?
|
|
210
|
+
@stderr.puts "specguard-lint: warning: selected 0 spec files — #{empty_reason(selection)}"
|
|
211
|
+
@stderr.puts "specguard-lint: warning: #{selection.note}" if selection.note
|
|
212
|
+
elsif !json
|
|
213
|
+
@stdout.puts "specguard-lint: checked #{selection.count} spec file#{'s' unless selection.count == 1}" \
|
|
214
|
+
"#{" changed since #{selection.base}" if selection.mode == :changed}" \
|
|
215
|
+
"#{" under #{Dir.pwd}" if selection.mode == :all}"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# `:explicit` is absent by construction — an explicit Selection is only
|
|
220
|
+
# built from a non-empty file list, so it can never be empty.
|
|
221
|
+
def empty_reason(selection)
|
|
222
|
+
case selection.mode
|
|
223
|
+
when :changed then changed_empty_reason(selection)
|
|
224
|
+
else "no *_spec.rb found under #{Dir.pwd}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Names the filter that actually emptied the selection. Saying "nothing in
|
|
229
|
+
# the diff matched *_spec.rb" when a spec file demonstrably changed —
|
|
230
|
+
# just not under this directory — is worse than saying nothing: it reads
|
|
231
|
+
# as a conclusion and stops the reader looking.
|
|
232
|
+
def changed_empty_reason(selection)
|
|
233
|
+
stats = selection.stats
|
|
234
|
+
base = selection.base
|
|
235
|
+
|
|
236
|
+
return "nothing in the diff against #{base} matched *_spec.rb" if stats.nil?
|
|
237
|
+
|
|
238
|
+
if stats.changed.zero?
|
|
239
|
+
"nothing changed against #{base}"
|
|
240
|
+
elsif stats.spec_matches.zero?
|
|
241
|
+
"#{stats.changed} file#{'s' unless stats.changed == 1} changed against #{base}, " \
|
|
242
|
+
"none matching *_spec.rb"
|
|
243
|
+
else
|
|
244
|
+
changed_excluded_reason(stats, base)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# `outside_root` and `unreadable` are independent counters over disjoint
|
|
249
|
+
# branches of the same partition (`file_selector.rb`), so both can be
|
|
250
|
+
# positive at once. Naming only the first one found is the failure the
|
|
251
|
+
# comment above forbids: the reader does the arithmetic, sees that
|
|
252
|
+
# `spec_matches - outside_root` files are unaccounted for, and concludes
|
|
253
|
+
# they were checked. They were not — the selection is empty. So the
|
|
254
|
+
# clauses are additive, and a selection emptied by both causes says so.
|
|
255
|
+
def changed_excluded_reason(stats, base)
|
|
256
|
+
matched = "#{stats.spec_matches} changed spec file#{'s' unless stats.spec_matches == 1} against #{base}"
|
|
257
|
+
|
|
258
|
+
# Nothing outside the root: `unreadable` is then the only filter left
|
|
259
|
+
# that can have emptied the selection, so it needs no count of its own.
|
|
260
|
+
return "#{matched} could not be read" unless stats.outside_root.positive?
|
|
261
|
+
|
|
262
|
+
reason = "#{matched}, but #{stats.outside_root} #{stats.outside_root == 1 ? 'is' : 'are'} " \
|
|
263
|
+
"outside #{Dir.pwd} (--changed selects only files under the current directory)"
|
|
264
|
+
reason += " and #{stats.unreadable} could not be read" if stats.unreadable.positive?
|
|
265
|
+
reason
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The FAIL block, in the shape `bin/validate-intent --source` emits: two
|
|
269
|
+
# spaces after `FAIL`, an em-dash before an extraction problem, eight
|
|
270
|
+
# spaces and `-> ` before each schema reason.
|
|
271
|
+
#
|
|
272
|
+
# FAIL spec/order_spec.rb:9
|
|
273
|
+
# -> <root>: missing required property 'entity'
|
|
274
|
+
# FAIL spec/order_spec.rb:28 — unterminated object literal (...)
|
|
275
|
+
#
|
|
276
|
+
# Two deliberate differences from the reference's `--source` mode, which
|
|
277
|
+
# is a fixture self-test rather than a linter:
|
|
278
|
+
#
|
|
279
|
+
# * no `PASS` line per valid annotation — a CI linter that prints a
|
|
280
|
+
# line per healthy annotation buries the failures in a large repo.
|
|
281
|
+
# The summary line carries the count instead, so "checked nothing"
|
|
282
|
+
# is still impossible to mistake for "all clean".
|
|
283
|
+
# * findings go to **stdout**, where the reference puts them and where
|
|
284
|
+
# lint findings conventionally go. Diagnostics about the linter
|
|
285
|
+
# itself (warnings, misuse, schema failure) stay on stderr.
|
|
286
|
+
#
|
|
287
|
+
# That second bullet used to end "…and the exit code — not the stream —
|
|
288
|
+
# is the machine-readable signal." It justified the STREAM SPLIT, which
|
|
289
|
+
# stands unchanged; but read as a claim about the tool it is now false,
|
|
290
|
+
# and it was already dated when it was written. It comes from SPGD-82,
|
|
291
|
+
# before the Go port grew `--json` (SPGD-102), and a 3-valued exit code
|
|
292
|
+
# was then genuinely the only structured thing a consumer could read.
|
|
293
|
+
# SPGD-305 gives this renderer a sibling: with `--json`, stdout carries
|
|
294
|
+
# one JSON document — file, line, kind, errors — and the exit code is one
|
|
295
|
+
# signal of two rather than the only one. Nothing moved off stderr to make
|
|
296
|
+
# that true; see {JSONReporter}.
|
|
297
|
+
#
|
|
298
|
+
# == Two renderers, one result list
|
|
299
|
+
#
|
|
300
|
+
# The partition below is computed ONCE and handed to whichever renderer
|
|
301
|
+
# runs. Both the text summary line and the document's `summary` are
|
|
302
|
+
# statements about the same numbers, and a linter whose two renderers can
|
|
303
|
+
# disagree about how much it checked is worse than one that only prints
|
|
304
|
+
# prose: the disagreement is unfalsifiable from outside the process.
|
|
305
|
+
def report_results(results, files:, json:, ok:)
|
|
306
|
+
annotations, unread = results.partition(&:line_scoped?)
|
|
307
|
+
|
|
308
|
+
if json
|
|
309
|
+
@stdout.puts JSONReporter.render(results, files: files, annotations: annotations.length, ok: ok)
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
results.reject(&:ok?).each { |result| report_failure(result) }
|
|
314
|
+
|
|
315
|
+
@stdout.puts summary_line(annotations, unread)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# The summary line exists for one reason — so "checked nothing" can never
|
|
319
|
+
# read as "all clean" — which makes overstating what was inspected its
|
|
320
|
+
# own kind of lie. A file that could not be opened contributed no
|
|
321
|
+
# annotation, so counting its Result as one would report "checked 12
|
|
322
|
+
# @intent annotations, 12 malformed" having read none of them. Unread
|
|
323
|
+
# files get their own clause: neither folded into the annotation count
|
|
324
|
+
# nor dropped from the report.
|
|
325
|
+
def summary_line(annotations, unread)
|
|
326
|
+
line = "specguard-lint: checked #{annotations.length} @intent annotation" \
|
|
327
|
+
"#{'s' unless annotations.length == 1}, #{annotations.count(&:failed?)} malformed"
|
|
328
|
+
return line if unread.empty?
|
|
329
|
+
|
|
330
|
+
"#{line}; #{unread.length} file#{'s' unless unread.length == 1} could not be read"
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def report_failure(result)
|
|
334
|
+
if result.problem
|
|
335
|
+
@stdout.puts "FAIL #{result.location} — #{result.problem}"
|
|
336
|
+
else
|
|
337
|
+
@stdout.puts "FAIL #{result.location}"
|
|
338
|
+
result.reasons.each { |reason| @stdout.puts " -> #{reason}" }
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def parse_options(argv)
|
|
343
|
+
options = { changed: false, base: nil, root: Dir.pwd, files: [], json: false }
|
|
344
|
+
|
|
345
|
+
parser = OptionParser.new do |o|
|
|
346
|
+
o.banner = BANNER
|
|
347
|
+
o.on("--changed[=BASE]",
|
|
348
|
+
"Only spec files changed against BASE (default: the merge base with the default branch)") do |base|
|
|
349
|
+
options[:changed] = true
|
|
350
|
+
options[:base] = base
|
|
351
|
+
end
|
|
352
|
+
o.on("--json", "Emit one JSON document on stdout instead of the human report") do
|
|
353
|
+
options[:json] = true
|
|
354
|
+
end
|
|
355
|
+
o.on("-v", "--version", "Print the version and exit") do
|
|
356
|
+
@stdout.puts "specguard-ruby #{VERSION}"
|
|
357
|
+
return nil
|
|
358
|
+
end
|
|
359
|
+
o.on("-h", "--help", "Print this help and exit") do
|
|
360
|
+
@stdout.puts o
|
|
361
|
+
return nil
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
options[:files] = parser.parse(argv)
|
|
366
|
+
options
|
|
367
|
+
rescue OptionParser::ParseError => e
|
|
368
|
+
# Uncaught, this is the single likeliest way a user sees a false
|
|
369
|
+
# "malformed annotation": OptionParser raises, Ruby exits 1. Retyping
|
|
370
|
+
# it is what makes `--chnaged` a 2.
|
|
371
|
+
raise UsageError, e.message
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|