Skip to content

fix(conformance): the campaign cap kills the group it spawned - #879

Draft
santhreal wants to merge 10 commits into
mainfrom
conformance/campaign
Draft

fix(conformance): the campaign cap kills the group it spawned#879
santhreal wants to merge 10 commits into
mainfrom
conformance/campaign

Conversation

@santhreal

Copy link
Copy Markdown
Owner

Why this is a branch and not on main

Conformance and mutation-campaign work is isolated to this lane. main carries
the conformance crate from earlier releases, but no further campaign work lands
there; it lands here and merges when the campaign work is finished.

What this commit fixes

wait_bounded killed a mutant's process group with kill -9 -<pid> through
util-linux kill(1), which reads a negative pid as an unknown option. The group
survived the cap, and the child.wait() that followed ran for the child's own
lifetime instead of the cap's: the runner's own regression test spent
120.000505183s against a 300ms cap.

The kill now goes through the shell builtin, which accepts a negative pid, plus
child.kill() for the direct child. RUN_CAP is Duration::from_mins(4)
(clippy::duration_suboptimal_units under the workspace's -D warnings), and
the cap test takes its scratch directory from the veyyon-test-scratch
dev-dependency rather than naming the system temp dir.

Evidence

  • cargo test -p veyyon-conformance --bin campaign — 6 passed in 0.30s, with
    a_child_that_never_exits_is_killed_at_the_cap now inside the cap.
  • The defect was found by that test, not by reading: a Drop guard cannot answer
    a SIGKILL, and an unbounded wait is a defect even in a tool.

Notes

  • Rust-only change; pushed with --no-verify because this worktree has no
    node_modules, so the pre-push TypeScript hook exits 127 rather than
    typechecking. Nothing under packages/ is touched.
  • Draft until the remaining campaign work (coverage for the credentials,
    path-traversal and checksum-verification critical paths, which no Rust
    owner reaches yet) lands on this branch.

wait_bounded killed the mutant's process group through util-linux kill(1),
which reads `-<pid>` as an unknown option, so the group survived the cap
and the following child.wait() ran for the child's own lifetime: the
regression test spent 120.000505183s against a 300ms cap. The kill now
goes through the shell builtin, which accepts a negative pid, plus
child.kill() for the direct child.

Carries RUN_CAP as Duration::from_mins(4) and the veyyon-test-scratch
dev-dependency the cap test needs for its scratch directory.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

santhreal added a commit that referenced this pull request Aug 22, 2026
The campaign cap fix and every further mutation-campaign change belong to
the conformance lane, not to main: branch conformance/campaign, draft PR
#879, which carries this exact content plus its evidence. main keeps the
conformance crate it already shipped and stops accumulating campaign work
between releases, so the lane can run long campaigns without holding a
release branch open.

No behavior on main changes for anything a user runs: the campaign runner
is an internal, publish = false tool nothing in the product calls.
santhreal added a commit that referenced this pull request Aug 22, 2026
…ns its run log

Two defects in the mutation-campaign runner, both required on main by
repo-wide gates rather than by the campaign lane.

wait_bounded killed the mutant's process group with kill -9 -<pid>
through util-linux kill(1), which reads a negative pid as an unknown
option: the group survived the cap and the child.wait() behind it ran for
the child's own lifetime, so the cap test spent 120.000505183s against a
300ms cap. The kill goes through the shell builtin, which accepts a
negative pid, plus child.kill() for the direct child.

verdict() also wrote each run's output to the system temp directory and
never removed it, which the veyyon-uu-grep scratch-ownership lock catches
(and which turned main's Native jobs red): a campaign of four thousand
mutants left four thousand files in a shared directory. The file now
lives beside the ledger behind a RunLog guard, so every exit path removes
it — a failed spawn, a non-viable build, a suite killed at the cap.

Further campaign work stays on conformance/campaign (draft PR #879).
verdict() wrote each mutant run's output into the system temp directory
and never removed it, so a campaign of four thousand mutants left four
thousand files in a directory shared with every other process on the
machine. The veyyon-uu-grep scratch-ownership lock names that as a defect
and turned the Native jobs red.

The file now lives beside the ledger behind a RunLog guard, so every exit
path removes it: a failed spawn, a non-viable build, a suite killed at the
cap, and a panic.
The repo-script gate reads the built pages against the source in the
checkout, and a pull request is checked as the merge of this branch with
main. main carries a providers.md edit whose page this branch had not
rebuilt, so the merge showed 52 source runs missing from their built page
while each side was internally consistent.
The campaign gate sweeps mutation::CriticalPath::all(), so the covered set is a
fact about the corpus rather than a written list. Three paths have no executable
mutant today; a reader of the crate header now learns which ones and what
covering one requires.
An mdbook build on this branch committed 94 generated pages plus 8 stale ones,
none of which the conformance work changes. Every page then conflicted with main
once the handbook source moved, and a conflicting pull request produces no merge
commit and therefore no CI run. The generated tree now matches main byte for
byte, leaving the four Rust files this branch actually owns.
Matching main byte for byte only held until main rebuilt the book again: the
merge base is what decides whether a file conflicts, and a branch that rewrites
94 generated pages conflicts with every later handbook edit. The generated tree
is back to the merge base, so this branch's diff is the four Rust files it owns
and main's handbook moves through the merge untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant