Skip to content

fix(infection): skip mutations to #[TestInline] attribute arguments (#159) - #261

Draft
rossaddison wants to merge 3 commits into
php-testo:1.xfrom
rossaddison:fix/159-infection-skips-test-inline-attributes
Draft

fix(infection): skip mutations to #[TestInline] attribute arguments (#159)#261
rossaddison wants to merge 3 commits into
php-testo:1.xfrom
rossaddison:fix/159-infection-skips-test-inline-attributes

Conversation

@rossaddison

Copy link
Copy Markdown
Contributor

Problem

When Infection mutates values inside a #[TestInline(arguments: [...], result: ...)] attribute it is mutating test data, not production logic.

Every such mutation is "killed" — Assert::same catches the changed expected value — but the kills are semantically meaningless: they verify that the assertion mechanism works, not that the source code handles a production code mutation correctly. This inflates the mutation score with noise and wastes CI runner time.

Concrete example in plugin/inline/tests/Self/StaticMethods.php:

#[TestInline(arguments: [1, 1], result: 2)]   // ← Infection: result: 2 → 3 (IncrementInteger)
public static function publicSum(int $a, int $b): int
{
    return $a + $b;   // ← this is the actual production code worth mutating
}

When result: 2 is changed to result: 3, Assert::same(2, 3) fails — the mutant is "killed", but no useful information was gained about the production code.

Fix

Add global-ignoreSourceCodeByRegex to infection.json:

"mutators": {
    "global-ignoreSourceCodeByRegex": [
        "#\[TestInline"
    ]
}

Infection skips any mutation whose source line contains #[TestInline. This covers:

  • All #[TestInline(...)] attribute lines in plugin/inline/tests/Self/ fixtures
  • Any production code that uses the attribute inline on its methods

Method bodies on adjacent lines are unaffected and continue to be mutated normally — so regressions in publicSum, privateMul, etc. are still caught.

Verification

All 23 inline plugin tests pass unchanged. The fix is a pure config addition with no code changes.

Fixes #159

…hp-testo#159)

When Infection mutates values inside a #[TestInline(arguments: [...],
result: ...)] attribute it is mutating test data, not production logic.
Every such mutation is "killed" (Assert::same catches the wrong expected
value), but the kills are semantically meaningless — they verify that the
assertion works, not that the source code handles a mutation correctly.
This inflates the mutation score with noise and wastes CI runner time.

Add `global-ignoreSourceCodeByRegex` to infection.json so Infection skips
any mutation whose source line contains `#[TestInline`. This covers all
single-line attribute declarations in both the Self-test fixtures under
plugin/inline/tests/Self/ and any production code that uses the attribute.
Method bodies on adjacent lines are unaffected and continue to be mutated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rossaddison
rossaddison requested a review from a team as a code owner July 6, 2026 13:16
@rossaddison

Copy link
Copy Markdown
Contributor Author

Summary

What the fix does: adds global-ignoreSourceCodeByRegex to infection.json with the pattern #\[TestInline. Infection then skips any mutation whose source line contains #[TestInline — so attribute argument values (result: 2, arguments: [1, 1]) are never mutated. Method bodies on the lines immediately below are untouched and continue to be mutated normally.

One line change to one config file.

…is mirrored

tests/Application/Stub/EmptyRun/ is intentionally empty — it is the test
fixture for EmptyRunTest, which asserts that a Testo run over an empty
directory yields Status::Risky with zero tests collected.

Git does not track empty directories, and bin/build-phpunit.php only copies
*.php files when populating the tests/PhpUnit/ mirror, so the mirror never
contained tests/PhpUnit/Application/Stub/EmptyRun/. The mirrored EmptyRunTest
resolved __DIR__ . '/../../Stub/EmptyRun' to that missing path and threw
InvalidArgumentException: File or directory not found — aborting Infection's
initial PHPUnit test run on every CI push to 1.x.

Add .placeholder.php (no namespace, no classes, no tests) to the source
directory. The build script copies it verbatim into the mirror, which creates
the required directory. Testo's FinderConfig still discovers zero tests there,
so Status::Risky is reported and the assertion holds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…urceCodeByRegex

Specifying "mutators": {} without "@default" treats the block as an
allowlist, so all default mutators were silently disabled — producing
0 mutations and an MSI failure.  Adding "@default": true restores the
full default mutator set while the global regex filter still skips
lines containing #[TestInline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rossaddison

Copy link
Copy Markdown
Contributor Author

Fix: 0 mutations generated → MSI failure

Root cause

Adding a "mutators": {} key to infection.json without "@default": true turns the block into an explicit allowlist. Infection saw no selected mutators and generated 0 mutations, which caused the 50% MSI threshold to fail immediately.

What happened

The original infection.json had no "mutators" key at all, so Infection ran all default mutators. PR #261 added:

"mutators": {
    "global-ignoreSourceCodeByRegex": ["#\[TestInline"]
}

global-ignoreSourceCodeByRegex is a configuration option, not a mutator selection — but because the mutators key was now present with no mutator entries, Infection treated it as "run zero mutators plus apply this filter". Result: 0 mutations, 0% MSI.

Fix

Add "@default": true to re-enable the full default mutator set alongside the regex filter:

"mutators": {
    "@default": true,
    "global-ignoreSourceCodeByRegex": ["#\[TestInline"]
}

Also included in this branch

  • tests/Application/Stub/EmptyRun/.placeholder.php — a no-op PHP file so that bin/build-phpunit.php creates tests/PhpUnit/Application/Stub/EmptyRun/ during the PHPUnit mirror build. Without it, EmptyRunTest throws InvalidArgumentException: File or directory not found when Infection runs the PHPUnit mirror suite, blocking Infection from starting at all (pre-existing CI failure on 1.x).

@roxblnfk

roxblnfk commented Jul 6, 2026

Copy link
Copy Markdown
Member

We discussed the issue there infection/infection#3053 (comment)
The PR ignores mutations only for this repo.
But I'm not sure that it's possible to resolve the issue for all the Testo users right now.

@roxblnfk
roxblnfk marked this pull request as draft July 24, 2026 10:38
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.

Infections mutates TestInline attributes

2 participants