Class: SpecGuard::RSpec::CLI
- Inherits:
-
Object
- Object
- SpecGuard::RSpec::CLI
- Defined in:
- lib/specguard/rspec/cli.rb
Overview
specguard-lint's command line, and the whole of the exit contract.
The contract, and the reason it needs defending
0 every annotation checked is valid (including "there were none")
1 at least one annotation is malformed
2 the linter could not do its job — misuse, or the tool itself broken
Ruby does not give you this for free; it actively works against it.
ruby -e 'raise "boom"' exits 1, and so does an uncaught
OptionParser::InvalidOption. So on the obvious implementation, every
internal failure lands on the one code the contract has already spent on
"an annotation is malformed":
* `specguard-lint --chnaged` — a typo — would exit 1, and CI would
report a malformed annotation that does not exist;
* a vendored schema missing from the packaged gem would exit 1, the
same false accusation, in the field, on someone else's machine.
The project has shipped this defect shape repeatedly — SPGD-35, SPGD-52, SPGD-56 — but always as false green: a gate reporting success having checked nothing. This is its inverse, false red: a tool failure wearing the costume of a content failure. Both are the same underlying bug, a gate whose failure states are indistinguishable, and for a linter the exit code is the product.
#run therefore rescues in two bands and returns rather than exits: UsageError and ValidatorError are named, and everything else that is not a deliberate interruption is caught by a backstop. Exit 1 is produced in exactly one place — a failed Linter::Result — so it means that and nothing else.
Interrupt, SignalException and SystemExit are deliberately not
caught. Ctrl-C must stay Ctrl-C; mapping it to "the linter is broken"
would be its own small lie.
All failures, not the first
SPGD-12 §1 step 4 says the linter "exits 1 on the first malformed
annotation". Its own exit-code table, one paragraph later, says "1 | One
or more annotations are malformed", and the validator reports every one:
validate-intent --source broken_intent_spec.rb emits 5 FAIL blocks. Stopping at the first would turn one file into five CI
round-trips. Reporting all of them is the ratified behaviour (human
decision recorded on SPGD-82); the "first" wording is a known spec defect
with a correction filed against SPGD-12 §1 and the SPGD-73 roadmap text.
One validator, one report
Since the SPGD-867 cutover there is exactly one validator: the
validate-intent binary ValidatorBackend resolves (an explicit
SPECGUARD_VALIDATE_INTENT path, or the first-run auto-install). The
Ruby hand-rolled validation arm is gone — when no binary can be resolved
the run exits 2 naming both remediations, and it NEVER silently
validates some other way. The verdicts arrive as Linter::Results, and
the reporting and exit-code logic is shared with nothing because there
is nothing to share it with.
Because both arms produce the same bytes, the run has to SAY which one it was, or the answer is unrecoverable from the output — see #report_backend, which states it in one line on stderr on both arms. That line is the only thing the default configuration gained: stdout, the exit code and every finding are what they were before this file learned about a second validator.
The backend's failure modes are exit 2 by construction: ValidatorError
is rescued beside UsageError, which is what makes "the binary you named
is missing" read as specguard-lint: error: … rather than reaching the
backstop and reading as an internal error:. Either way it is a 2, and
that is the property that matters — a broken tool must not borrow the
code that means "your annotations are malformed".
Two renderers, and what --json does NOT touch
--json (SPGD-305) replaces the human report on stdout with one JSON
document over the very same Linter::Result list — see JSONReporter.
It is a renderer, so the three things that are the contract are untouched
by it: the exit code (the decision below is one expression, evaluated on
both paths), stderr (the provenance line and every warning are byte-for-
byte what they were), and the default path (without the flag, stdout is
what it was, pinned as a regression lock in
spec/specguard/rspec/regression_targets_spec.rb).
No exit-2 path emits a document, and that is a decision rather than an
omission. Every rescue below means the linter produced NO VERDICTS —
bad flags, --changed outside a repository, a validator that could not
be resolved or produced no verdict. A document is a report about what was
checked; emitting {"ok": false, "findings": []} for a run that checked
nothing would hand a stdout-reading consumer the project's signature
defect — an empty clean-looking report standing in for "could not check" —
and dressing it as structure would make it more convincing, not less.
Those runs write prose to stderr, where diagnostics about the linter
already live, and say what happened with the exit code.
Constant Summary collapse
- BANNER =
"Usage: specguard-lint [options] [files...]"- EXIT_OK =
Every annotation checked was valid — or there were none to check. "Lint, don't require": a missing annotation is never an error.
0- EXIT_MALFORMED =
One or more annotations are malformed. The only code produced by inspecting content, and the only path that reaches it is a failed Linter::Result.
1- EXIT_MISUSE =
The linter could not do its job: bad flags,
--changedoutside a git repository, a validator that could not be resolved, or an unexpected internal error. 2
Instance Method Summary collapse
-
#initialize(stdout: $stdout, stderr: $stderr, env: ENV) ⇒ CLI
constructor
A new instance of CLI.
-
#run(argv) ⇒ Integer
0, 1 or 2 — never anything else, and never by letting an exception reach the shell.
Constructor Details
#initialize(stdout: $stdout, stderr: $stderr, env: ENV) ⇒ CLI
Returns a new instance of CLI.
114 115 116 117 118 |
# File 'lib/specguard/rspec/cli.rb', line 114 def initialize(stdout: $stdout, stderr: $stderr, env: ENV) @stdout = stdout @stderr = stderr @env = env end |
Instance Method Details
#run(argv) ⇒ Integer
Returns 0, 1 or 2 — never anything else, and never by letting an exception reach the shell.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/specguard/rspec/cli.rb', line 123 def run(argv) = (argv) return EXIT_OK if .nil? # --help / --version already printed # Resolved — or the run has already exited 2 — before anything is # selected or scanned, for the same reason it always was: "the # validator could not be obtained" must never surface as a run that # checked nothing and called itself clean. There is no Ruby arm to # fall back to, so a resolution failure IS the run's failure. backend = ValidatorBackend.resolve(env: @env) # One line per run naming the implementation that produced the # verdicts. Since the cutover there is exactly one implementation. report_backend(backend) selection = select() report_selection(selection, json: [:json]) results = backend.check(selection.files) # Computed once, here, and handed to whichever renderer runs. `--json` # is a second renderer over this list, not a second code path: the exit # code below is the same expression it always was, and the document's # `ok` is derived FROM it rather than recomputed from the findings, so # the two renderers cannot disagree about whether the run passed. code = results.any?(&:failed?) ? EXIT_MALFORMED : EXIT_OK report_results(results, files: selection.count, json: [:json], ok: code == EXIT_OK) code rescue UsageError, ValidatorError => e @stderr.puts "specguard-lint: error: #{e.}" EXIT_MISUSE rescue ScriptError, StandardError => e # The backstop that makes exit 1 mean one thing. Anything reaching here # is a bug in the linter, not a verdict about anyone's annotations, so # it is a 2 and it says so in those words. @stderr.puts "specguard-lint: internal error: #{e.class}: #{e.}" EXIT_MISUSE end |