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 three bands and returns rather than exits: UsageError and SchemaError 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 reference tool reports every
one: bin/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.
Two validators, one report
#run has exactly one branch on which validator produced the verdicts —
the in-gem Linter, or the Go port shelled out to by ValidatorBackend
when SPECGUARD_VALIDATE_INTENT names a binary. Both arms return
Linter::Results, so the branch closes immediately and the reporting and
exit-code logic below is shared rather than duplicated.
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, an unloadable schema, an
unmet --require-validator. 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, an unloadable schema, 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.
111 112 113 114 115 |
# File 'lib/specguard/rspec/cli.rb', line 111 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.
120 121 122 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 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/specguard/rspec/cli.rb', line 120 def run(argv) = (argv) return EXIT_OK if .nil? # --help / --version already printed # nil unless SPECGUARD_VALIDATE_INTENT names a usable binary, in which # case this raises rather than returning one that is not. Resolved # before anything is selected or scanned, for the same reason the # schema is: "the validator you asked for is not there" must never # surface as a run that checked nothing and called itself clean. backend = ValidatorBackend.resolve(env: @env) # Immediately after resolution and before selection: the line is above # the empty-selection warnings, it costs the `--version` probe at most # once per run, and a run that dies later still said what was about to # validate it. See #report_backend. report_backend(backend) # After the provenance line, so stderr carries what DID validate the # run before the reason that was not good enough, and before selection, # so an unmet assertion selects, scans and reports nothing. # # Being before #select also fixes a precedence, and it was chosen # rather than inherited: `--changed --require-validator foo_spec.rb` # with the variable unset reports the missing backend and never reaches # the "--changed cannot be combined with explicit files" UsageError # below. Both are exit 2, and the backend is the earlier question — # which files to check does not matter when nothing is going to check # them with the implementation that was asked for. require_backend!(backend) if [:require_validator] # Only on the Ruby path. When the backend is active the binary carries # its own schema and this gem's vendored copy governs nothing, so # loading it would let an unrelated packaging accident fail a run that # never reads it. The guard the load provides is not lost — the port # exits 2 on a schema it cannot load, and #run maps that to a # ValidatorError. schema = Schema.load if backend.nil? selection = select() report_selection(selection, json: [:json]) results = check(selection.files, backend: backend, schema: schema) # 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 SchemaError => e # Wording and stream follow bin/validate-intent:861-862 exactly. @stderr.puts "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 |