Skip to content

Close 68 escaped mutants across metadata, SQL and table gateway - #188

Merged
simon-mundy merged 5 commits into
php-db:0.6.xfrom
simon-mundy:test/mutation-escapes-metadata-and-sql
Sep 29, 2026
Merged

simon-mundy merged 5 commits into
php-db:0.6.xfrom
simon-mundy:test/mutation-escapes-metadata-and-sql

Conversation

@simon-mundy

@simon-mundy simon-mundy commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Follow-up to the mutation run on #185, which still showed 79 escaped mutants at 97% Covered MSI.

Before After
Escaped mutants 79 0
Covered Code MSI 97% 100%
Mutation Code Coverage 100% 100%

Tests and Infection config only — no src/ changes.

The root cause behind 33 of them

AbstractSourceTest::setMockData() seeds $this->data by reflection before calling a getter, so the getter's own loadXData() call is a no-op and Infection deletes it for free (MethodCallRemoval). The same seeding hides protected → private on the loaders, because nothing in the suite calls them from a subclass — IncompleteSource only overrides one.

LazyRecordingSource seeds only inside loadXData(), delegates to parent:: first, and records each call:

  • delete a loader call from a getter → the data is never there → the getter throws or returns empty
  • protected → private → parent::loadXData() fatals from the subclass
  • repeated reads → the recorded count proves the result is cached, not reloaded

Two genuine logic gaps in the same file came out with it: getConstraintKeys() matched on either half of the table/constraint pair rather than both, and prepareDataHierarchy() could leave auto-vivified nulls where the old assertArrayHasKey assertion couldn't tell the difference.

Everything else

Area Killed
AbstractSource lazy-load, constraint-key matching, hierarchy shape 33
Sql\Argument\Values / Identifiers — neither had a test file 4
FeatureSet (both namespaces), MetadataFeature, SequenceFeature, RowGatewayFeature 10
Predicates — setSpecification() was round-tripped but never honoured in getExpressionData() 4
AbstractLengthColumn — array_splice displacement, setLength() default 4
Integer display width (new in #185) — zero width, splice displacement 2
ColumnObject::setErratas, ConfigProvider::getDependencies, ContainerException code 3
Verified equivalents, ignored in config 12

src/Sql/Ddl/Column/Integer.php was at 92% on 0.6.x after #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.dist with a comment each rather than papered over with a test:

  • the six loader guards short-circuit prepareDataHierarchy(), which is itself idempotent, so falling through changes nothing
  • getColumns() delegates to getColumnNames(), which loads the same data
  • implode() discards the keys array_fill() sets, so its start index is unobservable
  • a RowGatewayInterface is neither null nor a string, so the next guard returns for the same inputs
  • stopPropagation() has an empty body

Exception messages

Three diagnostic messages are built by concatenation, which generated 11 Concat and ConcatOperandRemoval mutants. 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 RowGatewayFeature messages are short phrases wrapped around self::class and RowPrototypeResultSet::class, so a class rename flows through the assertion on its own. VunerablePlatformQuoteException had expectException at 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.

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

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

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.
@simon-mundy
simon-mundy requested a review from tyrsson September 28, 2026 03:31
@simon-mundy simon-mundy self-assigned this Sep 28, 2026
@simon-mundy simon-mundy added qa Improvements in quality assurance of the project next minor Target next minor release. labels Sep 28, 2026
@simon-mundy simon-mundy added this to the 0.6.0 milestone Sep 28, 2026
@github-project-automation github-project-automation Bot moved this to Todo in @phpdb Sep 28, 2026
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>
@simon-mundy
simon-mundy merged commit fe52934 into php-db:0.6.x Sep 29, 2026
15 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

next minor Target next minor release. qa Improvements in quality assurance of the project

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants