sieve: an implementation that cannot be run is not a failing implementation - #50
Merged
Merged
Conversation
…tation
Pointing --impl at a command that does not exist produced a FAIL report with
~35 high-severity checks, each tagged with a bug class that was never exercised
and each detailing only "SUT exited with code 1". That is a confident verdict
about code that never executed, and it does not say how to fix it.
Two changes, both at the point where the truth was being discarded.
A preflight keygen before the battery. If the SUT cannot be spawned, dies, or
never answers, the report carries one harness category and overall: "ERROR".
ERROR outranks FAIL, because if the implementation never ran then nothing else
in the report is a statement about it. A SUT that starts and replies
{"ok": false} is deliberately NOT an error: it is alive and speaking the
protocol, so its refusals are a real conformance signal and the battery runs.
And the cause is now reported. SutCrashError already captured the child's
stderr, but all 14 category call sites threw it away and reported
(err as Error).message, which is only ever "exited with code N". They now go
through describeSutError, which quotes the diagnostic part of the dump by
anchoring on the first and last lines that name a failure: Node buries
"Error: Cannot find module" between an internal loader frame and its version
banner, while Python puts the exception last, so neither end alone is reliable.
A run against a missing file now reports
Error: Cannot find module '/repo/my-impl.js'
instead of 35 crypto defects.
Additive: overall gains a third value, three symbols are exported, no existing
field changes shape. CLI exit codes are untouched (only PASS exits 0), so
existing CI gates behave exactly as before.
The preflight spent one extra keygen on every healthy run to answer a question the run itself already answers. The runner now counts protocol responses: zero at the end means the SUT never said anything the battery could judge, so the results are artefacts and the harness category replaces them. Same reports, no cost on a healthy SUT. A SUT that dies part-way now also keeps its results, which is correct: by then it had answered, so those failures are real. The one case with no crash and no stderr, a SUT that spawns and never replies, says exactly that rather than dressing up silence as a diagnosis.
…nnable-sut # Conflicts: # CHANGELOG.md
…cking) The pattern opened with \w*(?:Error|Exception) to catch prefixed names like ModuleNotFoundError. That backtracks quadratically, and it is applied to the stderr of an untrusted child process, so a SUT could emit one 64 KiB word (the stderr cap) and hang the harness meant to be judging it. Matching "error" as a plain substring catches the same names with no backtracking. It over-matches slightly, which costs a less apt quote and never a wrong verdict. Node, Python and spawn-failure diagnostics are unchanged. Also fixes the prettier formatting CI flagged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Not for auto-merge — pqc-tools merges by hand.
What went wrong
A conformance run on
dandelionlabs-io/leonacostaok-blogreported this:Every one of those is false.
--implpointed atnode ./my-impl.js, which does not exist in that repo, so nothing was ever tested. The report named AF-02 and AF-05 bug classes against code that never executed, and the 35 details between them did not contain the words "cannot find module".Reproduce on
main:What this changes
A new
ERRORverdict, decided from the response count. The runner already tracks every protocol response it decodes. Zero of them at the end of a run means the SUT never did anything the battery could judge, so every failure recorded against it is an artefact. Those results are replaced by a singleharnesscategory andoverall: "ERROR".ERRORoutranksFAILbecause if the implementation never ran, nothing else in the report is a statement about it.Two cases are deliberately not errors, and both are tested:
{"ok": false}to everything. It is alive and speaking the protocol, so its refusals are a genuine conformance signal and the full battery runs.This costs nothing on a healthy run: it reads a counter rather than spending a probe request. (An earlier revision of this PR did issue a preflight
keygen; that was wasted work and is gone.)The cause is now reported.
SutCrashErroralready captured the child's stderr, but all 14 category call sites discarded it and reported(err as Error).message, which is only everexited with code N. They now go through a new exporteddescribeSutError.Picking the right stderr line needs care: Node buries
Error: Cannot find modulebetween an internal loader frame and its version banner, while Python opens withTracebackand puts the exception last. Neither end alone works, so it anchors on the first and last lines that name a failure:The last of those is the one case with no crash and no stderr. It says so plainly rather than dressing up silence as a diagnosis.
Same run as the bug report, after:
35 findings to 1, and it names the file.
Known trade-off
A SUT that spawns but never replies still times out on every probe before the run concludes, which with the default 10s timeout and 32 iterations is slow. That is unchanged from
main— it is not a regression, just not something this PR fixes. A crashed process is unaffected: it rejects immediately, so the whole run takes ~170ms.Compatibility
Additive, SemVer minor.
SieveReport.overallgains a third value; three symbols are exported (describeSutError,HARNESS_CATEGORY,Verdict); no existing field changes shape.CLI exit codes are unchanged. Only
PASSexits 0, soERRORexits 1 exactly asFAILdid and existing CI gates behave identically. Consumers that want to tell them apart read.overall. I chose not to add a distinct exit code for that reason — happy to if you would rather have one.Verified
npm run build,npm run lint,npm testacross all 8 packages: 1257 tests, 0 failures.docs/API.mdanddocs/api-surface.jsonregenerated for the three new exports.New
packages/sieve/test/unrunnable-sut.test.tscovers: ERROR plus a single harness check and no invented bug classes; the detail naming the missing module; a live-but-broken SUT still getting a normal FAIL verdict; a hung SUT reporting the silence honestly; ERROR outranking FAIL; the Node-vs-Python stderr selection; and a guard that a run needing no requests sends none, so nothing probes the SUT speculatively.Related
The website ingests Sieve reports and had to be defended separately, since it must keep working against published Sieve versions that predate this: quantakrypto/website#115 treats
counts.pass == 0as "never ran", records it asfailedrather than a verdict, and keeps it out of badges and the posture series.