Repository navigation
Close 68 escaped mutants across metadata, SQL and table gateway - #188
Merged
simon-mundy merged 5 commits intoSep 29, 2026
Merged
simon-mundy merged 5 commits into
simon-mundy merged 5 commits into
Conversation
Escaped mutants drop from 79 to 11 and Covered MSI rises from 97% to 99%. The largest cluster was one root cause: AbstractSourceTest seeds the data property by reflection before calling a getter, so the getter's own loader call is unobservable and Infection deletes it for free. LazyRecordingSource seeds only inside loadXData(), delegates to its parent (which keeps the parent's protected visibility observable from a subclass) and records each call, so dropping a loader call from a getter now empties the result. That plus two genuine gaps in the same file - constraint keys matching on only half the table/constraint pair, and the hierarchy builder auto-vivifying nulls - accounts for 33 of them. Elsewhere: Values and Identifiers had no test file at all; the predicates round-tripped setSpecification() without checking getExpressionData() honoured it; the length columns never covered a value long enough for the array_splice insert to displace anything; and Integer's display width, new in php-db#185, never saw a zero width or a values array long enough to notice a removal. Twelve mutants are verified equivalents rather than gaps and are ignored in infection.json5.dist with reasons: the six loader guards short-circuit an idempotent prepareDataHierarchy(), getColumns() delegates to getColumnNames() which reloads the same data, implode() discards the keys array_fill() sets, a RowGatewayInterface is caught by the next guard anyway, and stopPropagation() has an empty body. The 11 remaining escapes are all exception-message concatenation. Killing them means pinning exact wording, which buys brittleness rather than safety, so they are left visible rather than suppressed.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Takes the last 11 escaped mutants and the GitHub annotations that came with them. Covered MSI reaches 100%. Three of the four sites already asserted their message, but with a substring short enough that every Concat and ConcatOperandRemoval mutant slipped under it - reordering the operands or dropping one left the assertion passing. Widening them to the whole message closes that. The wording is not the brittle part. Both RowGatewayFeature messages are short phrases wrapped around self::class and RowPrototypeResultSet::class, so a class rename flows through the assertion on its own and only a deliberate rewording breaks it, which is when being told is useful. VunerablePlatformQuoteException had expectException at four call sites and no message assertion anywhere, so it gains a focused test. Its placeholders sit in the first fragment, so the assertion pins that the message names the platform and method before it explains the risk. The only mutants left in the suite are the two pre-existing timeouts in AbstractResultSet.
The two timeouts in AbstractResultSet are infinite-loop mutants on valid(): one turns the && into ||, the other negates the whole condition, and both leave valid() unable to return false, so any foreach over a buffered result set spins until Infection reaps it. Only one covering test reached that branch - the foreach at line 159, the sole test that calls buffer() before iterating. Everywhere else buffer() is never called, the buffer is null, and is_array() short-circuits so neither mutant is observable. That made a whole branch depend on one loop. These two cases drive valid() directly on a buffered set, via a helper that walks the data source to its end with current()/next() rather than a loop, so a valid() that never goes false cannot spin inside them. Applying each mutant by hand confirms they catch it: the || fails the past-the-end case, the negation fails both. The mutants stay classified as timeouts, because the foreach tests still hang earlier in the same process. What changes is that the branch no longer rests on those tests: if they were removed or stopped iterating, these two would kill the mutants outright instead of letting them escape.
The exception refactor landed on 0.6.x while this branch was open, rewriting most of the test files it touches. - RowGatewayFeatureTest takes 0.6.x throughout: the assertions now render RuntimeException's template constants instead of repeating the literals, and use self:: rather than $this-> - ContainerExceptionTest keeps both sides. 0.6.x added tests for the four new named constructors; this branch's assertion that forService() sets no code survives alongside them - VunerablePlatformQuoteExceptionTest is deleted. It tested a class core#194 removed, and git merged the new file in without noticing the subject had gone Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
testSetStateWithCustomAdapterName asserted only that a delegator came back,
which is true whichever way the coalesce falls, so the Coalesce mutant on
__set_state() survived: AdapterInterface::class is never null, and no test
looked at which service the restored delegator asks for.
$adapterName is protected readonly, so the test now observes it through
behaviour, invoking the delegator against a container that expects has('custom').
Applying the mutant fails the test; that was the last escaped mutant in the
package.
Signed-off-by: Simon Mundy <simon.mundy@peptolab.com>
tyrsson
approved these changes
Sep 29, 2026
simon-mundy
added a commit
that referenced
this pull request
Sep 29, 2026
Drops 18 entries for issues that no longer exist: the seven per-component ExceptionInterface files deleted in PR #190, four exception classes that are now final, three resolved unhandled-thrown-type and mixed-argument sites, and the unfulfilled-expect and prefer-test-attribute entries settled by the mutation coverage work in PR #188. Suppressed issue totals are unchanged at 3948 lint and 1215 analyzer, so no new findings are masked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to the mutation run on #185, which still showed 79 escaped mutants at 97% Covered MSI.
Tests and Infection config only — no
src/changes.The root cause behind 33 of them
AbstractSourceTest::setMockData()seeds$this->databy reflection before calling a getter, so the getter's ownloadXData()call is a no-op and Infection deletes it for free (MethodCallRemoval). The same seeding hidesprotected → privateon the loaders, because nothing in the suite calls them from a subclass —IncompleteSourceonly overrides one.LazyRecordingSourceseeds only insideloadXData(), delegates toparent::first, and records each call:protected → private→parent::loadXData()fatals from the subclassTwo genuine logic gaps in the same file came out with it:
getConstraintKeys()matched on either half of the table/constraint pair rather than both, andprepareDataHierarchy()could leave auto-vivifiednulls where the oldassertArrayHasKeyassertion couldn't tell the difference.Everything else
AbstractSourcelazy-load, constraint-key matching, hierarchy shapeSql\Argument\Values/Identifiers— neither had a test fileFeatureSet(both namespaces),MetadataFeature,SequenceFeature,RowGatewayFeaturesetSpecification()was round-tripped but never honoured ingetExpressionData()AbstractLengthColumn—array_splicedisplacement,setLength()defaultIntegerdisplay width (new in #185) — zero width, splice displacementColumnObject::setErratas,ConfigProvider::getDependencies,ContainerExceptioncodesrc/Sql/Ddl/Column/Integer.phpwas at 92% on0.6.xafter #185 merged; it is now 100%, 26/26.Equivalent mutants, ignored with reasons
Twelve are false positives, not gaps. They are listed in
infection.json5.distwith a comment each rather than papered over with a test:prepareDataHierarchy(), which is itself idempotent, so falling through changes nothinggetColumns()delegates togetColumnNames(), which loads the same dataimplode()discards the keysarray_fill()sets, so its start index is unobservableRowGatewayInterfaceis neither null nor a string, so the next guard returns for the same inputsstopPropagation()has an empty bodyException messages
Three diagnostic messages are built by concatenation, which generated 11
ConcatandConcatOperandRemovalmutants. Three of the four call sites already asserted their message, but with a substring short enough that every mutant slipped under it — reordering the operands or dropping one left the assertion passing.Widening them to the whole message closes all 11. The wording is not the brittle part: both
RowGatewayFeaturemessages are short phrases wrapped aroundself::classandRowPrototypeResultSet::class, so a class rename flows through the assertion on its own.VunerablePlatformQuoteExceptionhadexpectExceptionat four call sites and no message assertion anywhere, so it gains a focused test.The only mutants left in the whole suite are the 2 pre-existing timeouts in
AbstractResultSet.php:237, untouched by this PR.