Skip to content

mutate: every test process held to its share of the memory - #20

Merged
aledbf merged 1 commit into
mainfrom
mutate-memory-bound
Sep 30, 2026
Merged

aledbf merged 1 commit into
mainfrom
mutate-memory-bound

Conversation

@aledbf

@aledbf aledbf commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #19.

An edit that flips the only exit of a loop that appends (spin#199's Ancestors, == becomes !=) allocates gigabytes a second; -test.timeout bounds time, not memory, and on the CI runner the whole job went, and the Gate with it.

What changes

  • A bound per test process and everything it starts. One poller reads /proc every 100 ms for all watched processes (a scan is ~2 ms, so one per process would cost a fifth of a core at -j 10) and SIGKILLs the whole tree past its limit. The tree matters: under go test the test binary is the go command's child and does not die with it.
  • Applied wherever tests run: Ask (the schema binary), Run (an edit built alone) and Cover. Builds are not bounded.
  • Reported as refused, named with the bound, like a timeout: a new Exceeded outcome, counted with the refused:
    refused   internal/mutate/bound.go:111 (stopOver): < becomes >= (its tests held over 2.1 GiB, and were stopped)
    
    (from this change's own task mutate).
  • -mem flag in GiB. By default (0) each job gets MemAvailable / -j, so the jobs together cannot pass what was free when the run started; a negative value removes the bound. Share takes the memory beside the jobs (internal package).

Tests (no network)

  • The /proc/meminfo and /proc/<pid>/stat parsers on fixed input, including a process name with parentheses and spaces.
  • Share's memory share, a value given, and no bound.
  • End to end on a fixture with the issue's loop, through the schema binary (via RunAll, checking the report line) and built alone: the package as it is passes under the same bound, compiler included; the edit is Exceeded; and no test binary is left running. The fixture stops at 1 GiB and sleeps, so a broken bound fails the test instead of taking the machine. Checked to fail with the kill disabled, with only the root killed, and with the comparison inverted.

task test, task lint and task mutate (86 refused, 0 survived) pass locally.

Not covered

  • Linux only: with no /proc there is no bound.
  • A binary killed by a signal it did not get from mutate (the kernel's OOM killer, say) is still counted refused without a reason; with the default share mutate should stop it first.

Each repository takes it with go get github.com/spin-stack/go-tools@<commit>.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…it that passes it refused

An edit that flips the only exit of a loop that appends allocates gigabytes a second, long before
-test.timeout fires: on a CI runner it took the whole job, and the Gate, with it. Each test
process and what it starts are measured from /proc and stopped past their share of the memory
free when the run starts (-mem to set it), and the edit is reported refused, named with the bound.

Fixes #19

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@aledbf
aledbf merged commit 7fefec3 into main Sep 30, 2026
1 check passed
@aledbf
aledbf deleted the mutate-memory-bound branch September 30, 2026 21:47
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.

mutate: a mutant that allocates without bound takes the CI runner's job down with it

1 participant