Class: SpecGuard::RSpec::ValidatorBackend::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/specguard/rspec/validator_backend.rb

Overview

Invokes the binary and turns its report into Linter::Results.

Constant Summary collapse

KINDS =

kind -> the gem's equivalent. no-match has no equivalent of its own and folds into KIND_READ; see the module comment.

{
  "schema" => Finding::KIND_SCHEMA,
  "extraction" => Finding::KIND_EXTRACTION,
  "parse" => Finding::KIND_PARSE,
  "read" => Finding::KIND_READ,
  "no-match" => Finding::KIND_READ
}.freeze
NON_ANNOTATION_KINDS =

Kinds that are a statement about a FILE rather than about an annotation site. They contribute nothing to summary.annotations, exactly as CLI#summary_line keeps unread files out of the gem's own annotation count.

%w[read no-match].freeze
MAX_ARG_BYTES =

A full audit passes every spec file in the repository — thousands of arguments on a large suite — and an argument vector has two separate kernel limits on Linux: ARG_MAX caps the TOTAL vector (a quarter of the stack rlimit, so typically ~2 MiB) and MAX_ARG_STRLEN caps each SINGLE argument at 128 KiB. Exceeding either is E2BIG. This budget is for the first, and is deliberately well under the typical value rather than derived from it: the limit is not portable (it is smaller on some kernels and much smaller on other Unixes), and there is nothing to gain from crowding it. The batches are an implementation detail: #check concatenates their findings in argument order, so the caller still gets one list and CLI still prints one selection line and one summary line.

96 * 1024
MAX_BATCH_FILES =

A second, independent bound. Some kernels cap the argument count (and MAX_ARG_BYTES alone would allow ~90k one-character paths).

1_000
IDENTITY_MAX_BYTES =

The --version probe's own guard rail, and the only thing it asserts about the answer's SHAPE. The identity is passed through verbatim, so the question is not "is this the format I expect" — a future build may word it differently and still be telling the truth — but "can this be rendered as the one line CLI promises per run". A binary answering --version with a report document, a stack trace, or a megabyte of anything is answering a different question, and its output would break the line rather than fill it.

200
SCHEMA_DIGEST_PATTERN =

The one token of the identity line this file interprets, matched exactly as cmd/validate-intent/version.go writes it: the literal schema sha256: followed by 64 hex digits. Everything around it stays opaque — the version, the toolchain, the platform and anything a future build appends are still the binary's business.

Anchored at both ends. Without the trailing boundary a 65-digit token would match its first 64 and be compared as if it were a digest; with it, such a token matches nothing and the run lands in the "not reported" band, which is the right answer for a line this gem cannot read rather than one it disagrees with.

Case-insensitive, and the capture is compared downcased. Go's hex.EncodeToString is lower case and Digest::SHA256#hexdigest is too, so today this changes nothing; if a future build shouts, that is a spelling of the same digest and must not be reported as divergence.

/\bschema sha256:(\h{64})\b/i
SCHEMA_SOURCE_FLAG =

The flag that answers what a run ENFORCES. Named as a constant for the reason schemaSourceFlag is one on the other side of the seam: the probe passes it, the diagnostics quote it, and the specs assert the argument vector — three places that must spell it the same way.

"--schema-source"
SCHEMA_SOURCE_PATTERN =

--schema-source writes ONE line: schema <origin> sha256:<64-hex>, where the origin is either an absolute path or the literal <embedded schema> (cmd/validate-intent/schemasource.go, SchemaSourceLine).

This is a SECOND pattern rather than a reuse of SCHEMA_DIGEST_PATTERN, and the difference is not cosmetic: that one requires schema IMMEDIATELY followed by sha256:, which is how --version writes it, and the origin interposed here means it matches this surface never. Reusing it would have made every probe unreadable, every run fall back to the carried digest, and nothing go red — see the module comment.

Anchored to the WHOLE line, unlike the identity token. There, the digest is one token inside a line that is otherwise the binary's own business; here the line IS the answer, so anything around it means this is not the surface being read and the run belongs in the unavailable band. Both captures matter: the digest is compared, and the origin is what lets #provenance say WHICH schema was enforced rather than that one was.

The trailing anchor is what makes the digest the LAST sha256: token on the line rather than the first — the same reading Go's own comment prescribes for the shell (${line##* } "yields sha256:<hex> whatever the origin contains"). A path may contain spaces, and one may even be NAMED like this tail; ending the match at the end of the line is what keeps the answer the last token in both cases. (The origin is also captured greedily, but that is not what decides it: only one token can end the line.)

/\Aschema (?<origin>\S(?:.*\S)?) sha256:(?<digest>\h{64})\z/i
SCHEMA_SOURCE_MAX_BYTES =

The --schema-source probe's renderability budget, and the reason it is not IDENTITY_MAX_BYTES. This line carries a filesystem path, which POSIX allows up to PATH_MAX (4096 on Linux) all by itself, so the 200-byte budget that suits a one-line version string would reject legitimate answers from correctly-installed binaries. It still bounds the line, for the same reason the other one does: a binary answering with a report document or a megabyte of anything is answering a different question, and its output would break the line rather than fill it.

8 * 1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Runner

Returns a new instance of Runner.

Parameters:

  • path (String)

    the validate-intent binary



500
501
502
# File 'lib/specguard/rspec/validator_backend.rb', line 500

def initialize(path)
  @path = path
end

Instance Attribute Details

#enforced_schemaHash{Symbol => String}? (readonly)

What --schema-source answered: {origin:, digest:}, or nil when the binary could not answer it — a build predating open-test-intent slice 19, a schema that exists and will not load, or output this cannot read. Populated by #verify! beside #identity.

The digest is the schema a run STARTED at that moment would enforce. It is a second process from #identity, so the two are not guaranteed to describe the same instant — see the module comment for what that does and does not cost.

Returns:

  • (Hash{Symbol => String}, nil)


497
498
499
# File 'lib/specguard/rspec/validator_backend.rb', line 497

def enforced_schema
  @enforced_schema
end

#identityString? (readonly)

The binary's own --version line, or nil when it could not report one. Populated by #verify!; nil before it runs, which is why nothing constructs a Runner without it (see SpecGuard::RSpec::ValidatorBackend.resolve).

Assigned unconditionally rather than memoized: a second #verify! re-probes, in step with the file checks it sits among, which also re-run. "Once per run" is a property of the single #verify! call SpecGuard::RSpec::ValidatorBackend.resolve makes, not of a cache here.

Returns:

  • (String, nil)


464
465
466
# File 'lib/specguard/rspec/validator_backend.rb', line 464

def identity
  @identity
end

#pathObject (readonly)

Returns the value of attribute path.



452
453
454
# File 'lib/specguard/rspec/validator_backend.rb', line 452

def path
  @path
end

#schema_contractSymbol? (readonly)

The result of the schema-contract comparison, one of:

:enforced      — the binary reported the schema its runs LOAD, and
               it is ours. The strong arm: this one is an answer
               about the contract a verdict would be produced
               under, not about the bytes the artifact carries
:matched       — `--schema-source` did not answer, so the carried
               digest was compared instead, and it is ours
:unreported    — it named itself but carries no digest token
:unidentified  — it could not name itself at all
:unreadable    — we could not read our own vendored schema

There is no :diverged member: both comparisons raise out of #verify! on a difference, so no Runner ever exists holding one. Populated by #verify! beside #identity, and nil before it runs for the same reason.

Returns:

  • (Symbol, nil)


484
485
486
# File 'lib/specguard/rspec/validator_backend.rb', line 484

def schema_contract
  @schema_contract
end

Instance Method Details

#check(paths) ⇒ Array<Linter::Result>

Returns in argument order.

Parameters:

  • paths (Enumerable<String>)

    spec files, as the caller named them

Returns:

Raises:



561
562
563
564
565
566
567
568
569
# File 'lib/specguard/rspec/validator_backend.rb', line 561

def check(paths)
  paths = paths.to_a
  # `validate-intent --source` with no argument is a usage error (exit
  # 2) — and there is nothing to ask it. `CLI#report_selection` has
  # already warned about the empty selection.
  return [] if paths.empty?

  batch(paths).flat_map { |group| check_batch(group) }
end

#provenanceString

The active arm of CLI's one-line-per-run provenance statement.

The identity half is the binary's own words, uninterpreted. The path is spelled exactly as every diagnostic in this file spells it, so the line naming the validator and any error about it name the same thing.

The clause after it is the gem's own, because it is a statement about a COMPARISON the gem made and not about the binary. Each of the five states it can report is worded distinctly: three of them mean the contract was not checked, for three different reasons, and collapsing them into one sentence would leave an operator unable to tell a build too old to answer from an installation missing its own schema. The remaining two are both "checked and clean" and must NOT be collapsed either — one of them answers what the run enforces and the other only what the binary carries, which is the whole distinction this file turns on.

Composed from three pieces rather than branched on, because the identity and the schema contract are answers to two different questions and every combination of them is reachable: a binary can fail --version and answer --schema-source, and the line then has to say both things rather than the first one only.

Returns:

  • (String)


553
554
555
556
# File 'lib/specguard/rspec/validator_backend.rb', line 553

def provenance
  "validated by #{@identity || 'the binary'} at #{@path} (#{ENV_VAR})" \
    "#{identity_clause}#{schema_contract_clause}"
end

#verify!Object

Fails before anything is selected or scanned, so "the backend you asked for is not there" is never mistaken for a verdict about anyone's annotations.

The two probes ride along for the placement rather than for the check: asking here is what makes them once-per-run and ahead of selection. Neither can fail the run — see the module comment.

The schema-contract comparison reads the answers the probes already obtained, so it costs no third process, and it CAN fail the run — see the module comment for why that one band belongs in exit 2 while the identity itself does not.

Raises:



518
519
520
521
522
523
524
525
526
527
# File 'lib/specguard/rspec/validator_backend.rb', line 518

def verify!
  raise ValidatorError, "#{describe} does not exist#{path_hint}" unless File.exist?(@path)
  raise ValidatorError, "#{describe} is not a file" unless File.file?(@path)
  raise ValidatorError, "#{describe} is not executable" unless File.executable?(@path)

  @identity = probe_identity
  @enforced_schema = probe_schema_source
  @schema_contract = verify_schema_contract!
  self
end