Skip to content

test: cover the write rollback's success path - #128

Merged
michael-denyer merged 1 commit into
masterfrom
test/write-buf-rollback-success
Jul 26, 2026
Merged

test: cover the write rollback's success path#128
michael-denyer merged 1 commit into
masterfrom
test/write-buf-rollback-success

Conversation

@michael-denyer

Copy link
Copy Markdown
Owner

Closes the gap #127 surfaced by mutation testing: deleting handle.seek(pos) or
handle.truncate() from _write_buf failed no test.

The rollback's failure was covered
(test_write_buf_deletes_partial_on_non_retryable_rollback_failure). Its success — that a
failed attempt is discarded and the retry writes over clean state — was not. So the two lines
that guarantee a consistent output file after a transient write error were free to disappear
unnoticed.

The test

The first attempt puts more bytes into the stream than the retry will, then fails with a
retryable errno. That setup makes both halves of the rollback load-bearing:

  • Without seek(pos), truncate() cuts at the position left after the debris, so the retried
    line lands after it.
  • Without truncate(), seek(pos) rewinds but the retried line is shorter than the debris, so
    the debris tail survives past the end of the line.

In both cases the writer reports success over a malformed file — which is why the assertion is
on file contents, not on the call sequence.

It bites

Mutant Before this PR After
handle.truncate()pass survived 1 failed
handle.seek(pos)pass survived 1 failed
both → pass survived 1 failed

Each fails on this test's own assertion (Rollback must discard the failed attempt's bytes),
not incidentally somewhere else — checked, because a mutant caught by an unrelated test would
not actually mean this path is covered.

Checks

Check Result
pytest tests/test_incremental_writer.py 30 passed
pytest tests/ 2243 passed, 10 skipped, 3 xfailed
ruff check / ruff format --check clean
pyrefly baseline unchanged at 77 — this adds none

Test-only; no production change.

Closes the gap #127 found by mutation testing: deleting `handle.seek(pos)` or
`handle.truncate()` from `_write_buf` failed no test. The rollback's *failure*
was covered by
test_write_buf_deletes_partial_on_non_retryable_rollback_failure; its success
-- that a failed attempt is discarded and the retry writes over clean state --
was not, so the two lines that guarantee a consistent output file were free to
disappear.

The new test makes the first attempt put more bytes into the stream than the
retry will, then fail with a retryable errno, and asserts on the resulting file
rather than on the call sequence. Both halves of the rollback are load-bearing
under that setup:

- Without `seek(pos)`, `truncate()` cuts at the position left after the debris,
  so the retried line lands after it.
- Without `truncate()`, `seek(pos)` rewinds but the retried line is shorter
  than the debris, so the debris tail survives past the end of the line.

Either way the writer reports success over a malformed file, which is why the
assertion is on content.

Confirmed to bite: with `handle.truncate()` stubbed out, 1 failed; with
`handle.seek(pos)` stubbed out, 1 failed; with both, 1 failed. Each fails on
this test's own assertion ("Rollback must discard the failed attempt's bytes"),
not incidentally elsewhere. Before this test all three mutants survived.

Test-only. `pytest tests/` -- 2243 passed, 10 skipped, 3 xfailed. Baseline
unchanged at 77 entries; this adds none.
@michael-denyer
michael-denyer merged commit 6754910 into master Jul 26, 2026
12 checks passed
@michael-denyer
michael-denyer deleted the test/write-buf-rollback-success branch July 26, 2026 23:59
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