Skip to content

mutation: a Mull survivor is not evidence of a missing assertion — four sampled survivors all turn the suite red when applied by hand #434

Description

@Yaraslaut

scripts/mutation.sh (morph#405) reports 352 surviving mutants over
include/morph, and morph#431 and morph#432 are written against that list.
The list cannot be read site-by-site. Four survivors, sampled from three
different headers, were applied to the source by hand and every one of them
turns morph_tests red.

Verification status: measured

lane/d-framework at 18c764bc, in the same build tree
(build/mutation-core-forms) and the same configuration the run itself used —
Mull 0.34.0 / mull-ir-frontend-22 / LLVM 22.1.2, clang 22.1.8, -O0 -g,
cxx_all. For each site the mutation Mull describes (cxx_remove_void_call:
delete the call) was applied to the header, the tree rebuilt, and
./tests/morph_tests run. A survivor means the suite passes on the mutant, so
each of these should have stayed green.

site Mull hand-applied
core/remote.hpp:718 + :818reply(wire::encode(makeOk(...))) Survived 34 of 1303 cases fail
forms/forms.hpp:1981emitDeclaredBound(property, "minimum", *meta.minimum) Survived 2 cases fail
core/registry.hpp:709holder->attachIdentity(std::string{primary}) Survived 2 cases fail

Real output, the two remote.hpp reply calls deleted:

/home/yaraslau/repo/morph-wt/340/tests/test_coverage_push95.cpp:572: FAILED:
  {Unknown expression after the reported line}
due to unexpected exception with message:
  envelope decode failed: index 0: no_read_input

/home/yaraslau/repo/morph-wt/340/tests/test_remote_connection_scope.cpp:389: FAILED:
  REQUIRE( regA.await() )
with expansion:
  false

test cases:  1303 |  1269 passed | 34 failed
assertions: 21378 | 21343 passed | 35 failed

forms.hpp's and registry.hpp's, in one build together:

/home/yaraslau/repo/morph-wt/340/tests/test_forms_field_bounds.cpp:104: FAILED:
/home/yaraslau/repo/morph-wt/340/tests/test_forms_field_bounds.cpp:148: FAILED:
/home/yaraslau/repo/morph-wt/340/tests/test_registry_extra.cpp:237: FAILED:
/home/yaraslau/repo/morph-wt/340/tests/test_remote_extra.cpp:674: FAILED:

test cases:  1303 |  1299 passed | 4 failed

The clean tree at the same commit is green: All tests passed (21523 assertions in 1303 test cases).

What this costs the two tickets written against the list

morph#431's headline is "twenty places where the framework's response path can
be deleted and morph_tests stays green"
. It cannot: deleting two of them turns
34 cases red. The premise of that section is false, and it was the section the
ticket said to lead with.

It was tested the long way round first. tests/test_remote_reply_envelopes.cpp
(added on lane/d-framework, 20 cases) reads the full reply envelope for every
control kind dispatchMessage switches on, including both of these sites. The
re-run scored 64.76% and those survivors did not move — which is what
prompted applying the mutation by hand.

Mechanism — inferred, not verified

Every affected site is in a header compiled into many of morph_tests' 105
translation units. Each object carries its own copy of the function and the
linker keeps one. Mull embeds mutants per object (the .mull_mutants section)
and the runner enables one by id; if the copy carrying the enabled mutant is
one the linker discarded, the running program is unmutated and the mutant
scores as a survivor.

That predicts the effect is specific to header-defined code in a multi-TU
binary, which matches where the survivors sit: remote.hpp (126) and
bridge.hpp (85) are 211 of the 352, and cxx_remove_void_call is 148 of
them. It has not been confirmed against Mull's implementation — someone
should read mull-ir-frontend's emission and the runner's patching before this
is treated as understood.

Direction of the error

scripts/mutation.sh's header records a caveat in the other direction —
test_outbox.cpp names a scratch file after a stack address, so concurrent
copies can collide, a collision fails a case, and a failed case reads as a
kill. That remains true and still says the score can be too high.

This one says the score is also too low, and by a larger amount: every
false survivor is a mutant that should have counted as killed. The two are not
in tension — they are different mechanisms — but the second is the one that
decides whether 352 survivors is a backlog or an artefact.

What would change the verdict

  • A sampled survivor that leaves the suite green when hand-applied would be a
    real gap, and would bound how much of the list is affected. Four out of four
    false positives does not prove all 352 are.
  • Reproducing the run with include/morph compiled into a single translation
    unit (a one-TU test harness) would isolate the multi-TU hypothesis directly.
  • If Mull has an option for this — or if a newer release fixes it — the fix is
    a flag in scripts/mutation.sh, not new tests.

Until then, scripts/mutation_survivors.json records the finding next to the
two runs, and morph#431 and morph#432 should be re-scoped: their boundary-shape
findings held up and are closed, their reply(...) shape did not.

🤖 Generated with Claude Code

https://claude.ai/code/session_0154xzWuBMPveLcdeUgydifb

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ciSubsystem: cibugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions