Skip to content

Keep completed results when --fail-fast aborts a run - #4744

Open
ivmat wants to merge 7 commits into
model-checking:mainfrom
ivmat:fix-fail-fast-collect
Open

Keep completed results when --fail-fast aborts a run#4744
ivmat wants to merge 7 commits into
model-checking:mainfrom
ivmat:fix-fail-fast-collect

Conversation

@ivmat

@ivmat ivmat commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

check_all_harnesses collected results with collect::<Result<Vec<_>>>(), which short-circuits on the first error. A --fail-fast abort was such an error, so every result that had already completed was dropped. The summary then contradicted the per-harness output above it, and --export-json under-reported the run the same way.

Completed results now accumulate in a shared vector as harnesses finish. The abort signal carries no payload; the failing harness records its result like any other. Results are re-sorted into harness order after the parallel loop, since completion order is nondeterministic.

The parallel fail-fast UI test pinned the old behavior: "1 failures, 1 total" for ten failing harnesses under --jobs 4. With completed results retained, those counts depend on thread scheduling, so the test becomes a script-based test asserting the stable properties: the run aborts early (fewer than ten run) and every counted harness is a failure. The sequential UI test is unchanged: it aborts on its first harness, so its pinned summary stays correct. A new sequential script-based test proves retention deterministically ("1 successfully verified, 1 failures, 2 total"); it fails on main.

Testing: cargo test -p kani-driver, rustfmt, and clippy are clean; the two new script-based tests and the existing stop_at_single_fail UI test pass.

Resolves #4729

@ivmat
ivmat requested a review from a team as a code owner August 18, 2026 05:52
@feliperodri feliperodri added the [I] Refactoring / Clean Up Refactoring or cleaning up of existing code label Aug 18, 2026
@feliperodri
feliperodri requested a balanced review from Copilot August 18, 2026 14:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Retains completed harness results when --fail-fast aborts verification, keeping summaries and JSON exports accurate.

Changes:

  • Accumulates parallel results safely and restores harness ordering.
  • Replaces schedule-dependent UI expectations with script-based checks.
  • Adds a deterministic sequential regression test.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
kani-driver/src/harness_runner.rs Retains and sorts completed results.
tests/ui/multiple-harnesses/stop_at_single_fail/fail_fast_test_parallel.expected Removes obsolete fixed summary.
tests/script-based-pre/fail_fast_parallel/fixture.rs Updates parallel fixture documentation.
tests/script-based-pre/fail_fast_parallel/early_abort.sh Checks parallel fail-fast behavior.
tests/script-based-pre/fail_fast_parallel/early_abort.expected Adds expected script result.
tests/script-based-pre/fail_fast_parallel/config.yml Configures parallel regression test.
tests/script-based-pre/fail_fast_keeps_completed/keeps_completed.sh Checks deterministic result retention.
tests/script-based-pre/fail_fast_keeps_completed/keeps_completed.expected Adds expected script result.
tests/script-based-pre/fail_fast_keeps_completed/fixture.rs Adds passing and failing harnesses.
tests/script-based-pre/fail_fast_keeps_completed/config.yml Configures sequential regression test.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread kani-driver/src/harness_runner.rs
Comment thread tests/script-based-pre/fail_fast_parallel/early_abort.sh

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diagnosis and fix are right, and this is the direction I suggested in #4729 — the accumulator + re-sort is exactly it. Nice catch on the sort in particular: the old collect::<Result<Vec>> on an indexed rayon iterator was order-preserving, so without it you'd have silently changed export ordering for every parallel run, not just fail-fast ones. I also checked the determinism argument for the new sequential test and it holds (sort_harnesses_by_loc reverses on start line, jobs() defaults to 1 thread), and the existing sequential UI test really is unaffected.

Blocking on the first inline comment; the rest are cheap.

One more that I can't anchor inline because the file isn't in the diff: schema_utils.rs:362-365 says verification_results.results "is in completion order", which is why entries need harness_id. After this PR the order is deterministic. The conclusion still stands (sorted-by-loc != harness-metadata order) but the stated reason is now wrong — please reword.

Regression suites are still running, so I haven't seen an end-to-end green.

Comment thread kani-driver/src/harness_runner.rs Outdated
Comment thread kani-driver/src/harness_runner.rs Outdated
Err(err) => return Err(err),
};

// Completion order under parallelism is nondeterministic; restore harness order.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order is fixed here, but the set is still schedule-dependent, and that's a user-visible contract change: --export-json is no longer reproducible for --fail-fast --jobs N, so CI consumers diffing exports will see churn run to run.

I think that's the right behavior (it's the honest one), but it needs to be written down somewhere. --fail-fast has zero mentions in docs/ and a one-line help string (args/mod.rs:261). Please add a sentence there: in-flight harnesses finish and get counted, so counts vary under --jobs.

exit 1
fi

python3 - "${EXPORT_FILE}" << 'EOF'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong suite for this half. Every other --export-json test lives in tests/json-handler/, so nobody auditing export coverage will find this. Also, unlike its siblings there, it never runs scripts/validate_json_export.py — so nothing checks that the completed_with_fail_fast document still validates structurally.

Suggest splitting: keep the summary assertions here, move the export assertions to tests/json-handler/fail-fast/ and run the validator.

fi

# The run must abort early: strictly fewer than all ten harnesses.
if [[ "${TOTAL}" -ge 10 ]]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging, not blocking: this turns a pinned test into a timing test. Realistically safe — 4 threads means ~4 chunks and full() is checked before each item, so TOTAL lands near 4 — but the bound is doing no work if scheduling ever changes.

The VERDICTS == TOTAL check below is the load-bearing assertion and holds under any schedule. Fine to keep the < 10 guard, just don't count it as the regression check.

ivmat added 7 commits August 24, 2026 20:47
`check_all_harnesses` collected with `collect::<Result<Vec<_>>>()`,
which short-circuits on the first error. A `--fail-fast` abort was such
an error, so every harness that had already completed was dropped. The
summary then contradicted the per-harness output above it, and the
`--export-json` file under-reported the run the same way.

Accumulate completed results in a shared vector instead. The abort
signal carries no payload; the failing harness records its result like
any other. Results are re-sorted into harness order after the parallel
loop, since completion order is nondeterministic.

The parallel fail-fast UI test pinned the dropped-results behavior
("1 failures, 1 total" with ten failing harnesses under `--jobs 4`).
With completed results retained, its counts depend on thread
scheduling, so it becomes a script-based test asserting the stable
properties: the run aborts early, every counted harness is a failure,
and the summary total equals the number of verdicts printed above it.
The sequential UI test is unchanged: it aborts on its first harness,
so its pinned summary stays correct. A new sequential script-based
test covers result retention deterministically, in both the summary
and the --export-json file.

Resolves model-checking#4729
The abort was raised as an `Err(FailFastAbort)` through the same channel
`try_for_each` uses for genuine errors, and `try_for_each` surfaces only
one error. A harness failing for a real reason at the same time as another
tripped the abort could therefore have its error discarded, leaving the
run to report an ordinary fail-fast stop.

Signal the abort with an `AtomicBool` instead, so the error channel carries
genuine errors only and every one of them propagates. Harnesses that have
not started check the latch and return without running; those already in
flight finish and record their results, as before. The latch is read after
the parallel region joins, which orders the stores before the read.
`verification_results.results` was described as being in completion order.
It is not, and was not before this branch either: rayon's collect returns
results in input order, and this branch now sorts them explicitly. What
the comment is warning about still holds, since the two arrays are built
from different orderings and need not agree, so say that instead.
The latch had no test of its own. `harness_runner.rs` carried no unit
tests at all, and the script-based tests cover result retention rather
than which signal takes the error channel, so reverting the latch left
every test on the branch passing.

Lift the parallel run into `run_until_abort`, generic over the unit of
work, so the policy can be driven directly: the abort and error paths are
what the reviewer asked about, and neither needed a harness or a solver to
exercise. `check_all_harnesses` supplies the same closure it ran inline.

The tests fix the permutations that matter: an abort reported through the
flag and never through the error channel, a genuine error raised by one
unit while another aborts concurrently, an error on its own, units after
an abort not starting, and completed payloads kept in input order.
Reverting the latch to an error-channel abort fails four of them,
including the concurrent case.
The test asserting that a genuine error is not displaced by a concurrent
abort claimed that if rayon ran both units on one thread "the assertion
still holds". It does not: forcing the pool to one thread makes the test
fail, because the aborting unit latches the abort before the erroring unit
is ever visited, so there is no error left to propagate. Rayon is not
obliged to split a two-element iterator, so that was a latent CI flake
sitting inside the receipt for the fix.

Latch the abort only once the aborting unit has seen its sibling start.
Serialized, it declines to abort and the erroring unit runs afterwards, so
the assertion holds instead of failing on a scheduling accident; in
parallel the two still overlap and the test means what it says.

Measured, not assumed: with the pool forced to one thread the test now
passes, and reverting the latch to an error-channel abort still fails four
of the six, the concurrent case among them, on five runs out of five.

Also drop two comment claims that outran the code: the cancellation check
is a `Relaxed` load and so is best effort rather than a guarantee that a
unit will not start, and `verification_results.results` is sorted into
`sort_harnesses_by_loc` order rather than left in scheduling order.
Help string: `--fail-fast` documented only that it stops the run. It also
changes which harnesses get reported, because harnesses already running
when one fails still finish and are counted, so the set varies under
`--jobs N`. That is a user-visible contract for anyone diffing exports,
and the flag's whole documentation was the one line. `docs/` has no
fail-fast section to extend, so this stays in the help string.

Test split: the export assertions lived in the script-based summary test,
away from the rest of the export coverage in tests/json-handler/, and
were the only ones there that never ran validate_json_export.py. Moved to
tests/json-handler/fail-fast/, which follows the multiple-harnesses
layout and validates the document before asserting on it, so an aborted
run's export is now checked structurally as well. The summary test keeps
the rendered-summary half.

early_abort.sh: the comments presented the `< 10` bound as the regression
check. It is a smoke check that the abort happened, and it is a statement
about scheduling; VERDICTS == TOTAL is the assertion that reproduces
model-checking#4729 and holds under any schedule. Relabelled both, changed neither.

Also: the ordering step had no test that could fail. Every runner test
uses a one-thread pool, where units complete in index order and the sort
is a no-op, so deleting it broke nothing. Lifted it into `in_input_order`
and drove it with a shuffled input, which is the case `--jobs N` produces.
The test dropped set -e because the kani run under test exits non-zero by
design, but that also discarded the validator pipeline's status: pipefail
alone only reports it, nothing consumed it, so a structurally invalid
export could not fail the test. Exempt only the kani invocation from
set -e, the way failed-verification/test.sh already does.
@ivmat
ivmat force-pushed the fix-fail-fast-collect branch from 9acff28 to bc25cdf Compare August 25, 2026 16:19
@ivmat
ivmat requested a review from a team as a code owner August 25, 2026 16:19
@ivmat

ivmat commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

@feliperodri done, all items covered and retested.

here's a summary per review item:

  1. Err channel (blocking): the abort is now an AtomicBool latch in check_all_harnesses — the
    closure loads it first and returns Ok(()) if set; a fail-fast failure stores it and returns
    Ok(()); Err is reserved for instrument_model, synthesize_loop_contracts and check_harness,
    so run_result? propagates a genuine error unconditionally. FailFastAbort and its
    Error/Display impls are gone. The cost is as the review predicted: rayon no longer
    short-circuits at the producer, so remaining harnesses are visited and each pays one relaxed
    atomic load instead of a CBMC invocation; no result entry is pushed for them, so summary and
    export still count only harnesses that ran. The latch sits behind a small generic seam
    (run_until_abort) purely so the abort/error precedence is unit-tested without CBMC in the
    loop — reverting the latch makes those tests fail.

  2. Schedule-dependent counts: the help string on fail_fast now states that in-flight harnesses
    finish and are counted, so counts vary under --jobs N. docs/ has no fail-fast section at all
    today (grep -rn fail-fast docs/ is empty), so none was created in this PR; if a prose
    paragraph is wanted, docs/src/usage.md under "Common command line flags" is the natural home
    and it can be added here rather than in a follow-up.

  3. Test split: tests/script-based-pre/fail_fast_keeps_completed/ now asserts only the rendered
    summary. The export half moved to tests/json-handler/fail-fast/ — same two-harness fixture
    under --fail-fast --export-json, then scripts/validate_json_export.py, then the counter and
    per-harness assertions (executed 2 / successful 1 / failed 1, z_passes Success, a_fails
    Failure). It follows the multiple-harnesses layout, set -eu plus pipefail with only the kani
    invocation exempted (as failed-verification/test.sh does), so a failed validation fails the
    test — the aborted run's document now gets an enforced structural check it did not have.

  4. The < 10 bound: comments in early_abort.sh relabelled — it is a smoke check that the abort
    happened at all; VERDICTS == TOTAL is the regression assertion, reproduces --fail-fast discards results for harnesses that already completed #4729's symptom,
    and holds under any schedule. Both checks unchanged.

  5. schema_utils.rs comment: reworded — the array is built in harness-metadata order while
    verification_results.results is in sort_harnesses_by_loc (source-location) order, so
    position still does not correlate the two and harness_id remains what makes an entry
    attributable.

Also folded, from the earlier Copilot comments (never answered in-thread): the export case is
the json-handler test above; the summary-vs-verdicts comparison is the VERDICTS == TOTAL check.

One change beyond the threads: the ordering step was extracted into in_input_order and
unit-tested on a shuffled input — the sort was previously witnessed by no test, since every
runner test uses a one-thread pool where completion order is already input order.

Local regression at the pushed commit: unit tests green (87 in kani-driver) and 15 compiletest
suites with 0 failures, including json-handler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[I] Refactoring / Clean Up Refactoring or cleaning up of existing code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--fail-fast discards results for harnesses that already completed

3 participants