Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup mutation testing #5788

Merged
merged 1 commit into from Apr 2, 2024

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Apr 1, 2024

lets see whether this works now, since the test-suite is stable on random order


taking inspiration from

@staabm
Copy link
Contributor Author

staabm commented Apr 1, 2024

//cc @maks-rafalko @Wirone in case someone has ideas / sees room for improvement.

..never did a infection setup before.

since phpunit already works with psalm, I think the way forward would be to also integrate it with https://github.com/roave/infection-static-analysis-plugin ?

Copy link

codecov bot commented Apr 1, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.17%. Comparing base (8d485f1) to head (a89a476).

❗ Current head a89a476 differs from pull request most recent head f8d8c75. Consider uploading reports for the commit f8d8c75 to get more accurate results

Additional details and impacted files
@@            Coverage Diff            @@
##               main    #5788   +/-   ##
=========================================
  Coverage     90.17%   90.17%           
  Complexity     6582     6582           
=========================================
  Files           693      693           
  Lines         19940    19942    +2     
=========================================
+ Hits          17980    17982    +2     
  Misses         1960     1960           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On
tools: none

- name: Install infection
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not use Composer to manage tool dependencies.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that roave-infection-static-analysis-plugin can only be installed using Composer. However, installing Infection and the plugin using Composer for every run feels wrong. May I suggest that we create a tools/infection directory, put a composer.json file there that pulls in what we need, and then put the entire directory (composer.json, composer.lock, vendor) under version control? Then, just like with all other development tools, everything we need is available after a clone/checkout.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can live with this approach, at least for now.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, we need to install Infection and the plugin into an empty directory with a separate composer.json / composer.lock combo. Otherwise the actual composer.json / composer.lock used in this repository will interfere, as can be seen here https://github.com/sebastianbergmann/phpunit/actions/runs/8511186285/job/23310422850.

I know it's not helpful, but I truly hate installing development tools using Composer :-) I really hope that one day Infection will have a plugin system and will allow plugins packaged as PHARs. And that roave/infection-static-analysis-plugin will be made available that way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all fairness, Infection to this day still autoload the project's code as it relies on reflection. Could be solved by leveraging BetterReflection or publishing a scoped PHAR like PHPStan does, but that's still work to do.

But if you can live with that risk then using a separate composer.json is still fine.

@sebastianbergmann
Copy link
Owner

I doubt that mutation testing of PHPUnit will be anything less than very frustrating. We should start introducing it in its dependencies and work our way up from there.

@sebastianbergmann sebastianbergmann added the type/tests Tests! Yes, PHPUnit, too, needs tests. label Apr 1, 2024
@maks-rafalko
Copy link
Contributor

I doubt that mutation testing of PHPUnit will be anything less than very frustrating.

Could you please elaborate on that? I'm really interested in understanding the reasons, as recently we proofed that Infection can be executed for any codebase of any size and any testsuite size with specific options (already used in this PR). Hopefully your feedback will give us some ideas on how to improve MT/Infection, if it doesn't suite PHPUnit's needs.

Not sure if we all on the same page (I recommend reading this comment), but in a nutshell: we integrated Infection to PHP-CS-Fixer in such a way that it only runs on PRs for the changed lines, adding warnings in the GitHub diff view, helping reviewers understanding where we have bad tests. And in case of PHP-CS-Fixer it doesn't fail the build, specifically to not frustrate contributors. So just informative goal for now.

We should start introducing it in its dependencies and work our way up from there.

Do you have an idea where it should be added first? Happy to help/contribute.

@sebastianbergmann
Copy link
Owner

I doubt that mutation testing of PHPUnit will be anything less than very frustrating.
Could you please elaborate on that?

There is a "right now" missing at the end of my sentence :) Maybe I am too pessimistic, and very likely I am traumatized from trying to get Infection to work on PHPUnit itself a long time ago (when Infection was not as mature as it is today and PHPUnit's test suite was less robust), but when I saw this PR last night my feeling was: "I am not looking forward to see disheartening results from Infection every time it is run".

I'm really interested in understanding the reasons, as recently we proofed that Infection can be executed for any codebase of any size

I do not believe there to be a shortcoming in Infection. While we are making steady progress to modernize PHPUnit's code base, either by cleaning up code inside this repository or by moving code from this repository into dependencies, we are nowhere near a situation where everything is testable in small tests.

The doubt I expressed last night is not about "Infection is not ready", but rather "this code base is not yet in a shape where mutation testing makes sense". Of course, I might be wrong. We will only find out if we try.

Hopefully your feedback will give us some ideas on how to improve MT/Infection, if it doesn't suite PHPUnit's needs.

As I explained above, I do not think that Infection is not suited for PHPUnit's needs.

Not sure if we all on the same page (I recommend reading this comment), but in a nutshell: we integrated Infection to PHP-CS-Fixer in such a way that it only runs on PRs for the changed lines, adding warnings in the GitHub diff view, helping reviewers understanding where we have bad tests. And in case of PHP-CS-Fixer it doesn't fail the build, specifically to not frustrate contributors. So just informative goal for now.

Makes sense to me (not failing the build, limiting to unit test suite, limiting to covered code), thank you for taking the time to explain!

We should start introducing it in its dependencies and work our way up from there.
Do you have an idea where it should be added first? Happy to help/contribute.

I am not opposed to add Infection to PHPUnit's pipeline as proposed by this PR. But we (eventually) need it also for

@theofidry
Copy link
Contributor

FWIW an approach that I found can also work is to leave it as an optional build: you still get the feedback in the pull request, so you can easily review whether you want to address those points or not. It can be noisy at times but I find it being a nice middleground when working with a problematic codebase and when you don't have any bandwidth to deal with false positives or reworking the code up the the ideal standard.

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

after some back and forth, I think I found the simplest step forward with mutation testing.

we only run the mutation on the PRs diffs and do only use a plain infection.phar for now.
this means we also don't need composer to install the tooling in CI.

adding roave/infection-static-analysis-plugin to the mix is a task for a separate PR. but lets see whether this process works as is for now.

@sebastianbergmann if you are fine with the results here, I will do a final cleanup and remove the test-only code-changes to get it ready to merge (squash everything togehter).

@theofidry @maks-rafalko thx for your feedback and support. now its time for the final round of input to get the first babystep done :-)

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

for now the result of the mutation testing github action run is rendered as github annotations in the "files changed" tab, see e.g.

grafik

@sebastianbergmann
Copy link
Owner

if you are fine with the results here

LGTM (I'll take care of managing the PHAR after merge)

- name: Run mutation testing
run: |
git fetch origin $GITHUB_BASE_REF
php tools/infection.phar --threads=max --map-source-class-to-test --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF --ignore-msi-with-no-mutations --only-covered
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can't use --map-source-class-to-test for this repository, as we don't have here 1-to-1 relation between test file name and source class file name.

With this option, Infection tries to find test file just by adding *Test postfix to the source class file name, but it won't work here from what I see because

  • tests can have multiple CoversClass() attributes
  • tests can have no CoversClass() attribute
  • CoversClass() can have source class with the name not matching naming strategy where we just remove *Test postfix to get the source class file name

Thus, it will lead to much less tests being executed for particular changed line than needed, which will lead to more escaped mutants.

Example:

#[CoversClass(TestIdFilterIterator::class)]
#[CoversClass(TestSuiteIterator::class)]
#[Small]
final class TestIdFilterIteratorTest extends TestCase

Here we have 2 CoversClass(), but with current --map-source-class-to-test implementation Infection would run this test only when TestIdFilterIterator is mutated, because test file name is named just by adding *Test postfix. But it would not run this test file if TestSuiteIterator is mutated, because naming strategy is not the same. As I said above, we need to implement this feature at Infection to "understand" CoversClass() in a more advanced way. That's why I recommend to remove --map-source-class-to-test so all the unit tests will be executed and Infection will understand which tests cover changed lines from coverage report.

A bit of theory here: https://infection.github.io/guide/command-line-options.html#map-source-class-to-test

we can (should) add another strategy at Infection level to find test files by analyzing their CoversClass(...) attributes, but this is yet to be done. Let's handle this in the future as separate PRs.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can (should) add another strategy at Infection level to find test files by analyzing their CoversClass(...) attributes

Good idea!

@maks-rafalko
Copy link
Contributor

Besides the comment about --map-source-class-to-test it looks good to me!

It can be noisy at times [...]

yeah, and there is a toggle to hide/show annotations that can help during reviewing if one want to see the code without noise, and then enable it back to see warnings from psalm/phpstan/infection

image

@staabm staabm changed the title WIP: Setup mutation testing Setup mutation testing Apr 2, 2024
@staabm staabm marked this pull request as ready for review April 2, 2024 11:21
@sebastianbergmann sebastianbergmann merged commit f689597 into sebastianbergmann:main Apr 2, 2024
24 checks passed
@sebastianbergmann
Copy link
Owner

Thank you!

@staabm staabm deleted the infection-test branch April 2, 2024 11:26
@sebastianbergmann
Copy link
Owner

sebastianbergmann commented Apr 2, 2024

Infection reports "126 covered mutants were not detected" for PR #5784, but here only 10 annotations from Infection show up.

Scrolling through https://github.com/sebastianbergmann/phpunit/pull/5784/files, I also only see those 10.

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

Infection reports "126 covered mutants were not detected" for PR #5784

github shows only 10 annotations at a time per job. thats expected behaviour.

@sebastianbergmann
Copy link
Owner

github shows only 10 annotations at a time per job. thats expected behaviour.

Okay, thanks.

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

@maks-rafalko @theofidry we currently see a huge portion of mutations running into "runtime error" or "uncovered" result.

Generate mutants...

Processing source code files: 894/894
.: killed, M: escaped, U: uncovered, E: fatal error, X: syntax error, T: timed out, S: skipped, I: ignored

UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (   50 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  100 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  150 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  200 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  250 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  300 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  350 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  400 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  450 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  500 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  550 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  600 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  650 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  700 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  750 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  800 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  850 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  900 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   (  950 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1000 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1050 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1100 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1150 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1200 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1250 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1300 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1350 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1400 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1450 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1500 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1550 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1600 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1650 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1700 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1750 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1800 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1850 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1900 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 1950 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2000 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2050 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2100 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2150 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2200 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2250 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2300 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2350 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2400 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2450 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2500 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2550 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2600 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2650 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2700 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2750 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2800 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2850 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2900 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 2950 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3000 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3050 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3100 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3150 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3200 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3250 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3300 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3350 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3400 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3450 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3500 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3550 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3600 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3650 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3700 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3750 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3800 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3850 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3900 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 3950 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4000 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4050 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4100 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4150 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4200 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4250 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4300 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4350 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4400 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4450 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4500 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4550 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4600 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4650 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4700 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4750 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4800 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4850 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4900 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU   ( 4950 / 10352)
UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUEEEEEEE   ( 5000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5350 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5400 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5450 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5500 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5550 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5600 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5650 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5700 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5750 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5800 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5850 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5900 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 5950 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6350 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6400 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6450 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6500 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6550 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6600 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6650 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6700 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6750 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6800 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6850 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6900 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 6950 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7350 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7400 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7450 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7500 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7550 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7600 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7650 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE..EEEEEEEEEEEEEEEE   ( 7700 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7750 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7800 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7850 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7900 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 7950 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8350 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8400 / 10352)
EEEEEEEEEEEETEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8450 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8500 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8550 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8600 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8650 / 10352)
EEEEEEEEEEEEEEEEEEEEEE.....EMEEMEEEEEE.EEEEEEEEEEE   ( 8700 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8750 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8800 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8850 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8900 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 8950 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9350 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9400 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9450 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9500 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9550 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9600 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9650 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9700 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9750 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9800 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9850 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9900 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   ( 9950 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10000 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10050 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10100 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10150 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10200 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10250 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10300 / 10352)
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE   (10350 / 10352)
EE                                                   (10352 / 10352)

did we miss something? how to debug this situation?

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

the result report also looks strange

grafik

@theofidry
Copy link
Contributor

For reference: https://github.com/sebastianbergmann/phpunit/actions/runs/8522069785/job/23341606147

I can't take a look at it atm but most of my guesses are off since --only-covered is passed and infection is running the initial test suite

@Wirone
Copy link

Wirone commented Apr 2, 2024

@staabm did you run full mutation tests before? I am wondering if it was like this from the beginning or if it started to act like this after the merge 🙂. In PHP-CS-Fixer I had to disable some mutators for some files because they were causing weird side effects. Is that output (with uncovered / error results) from CI or local run?

PS. Thanks for mentioning me, glad to be included in working group 🥰. Unfortunately I had a busy day today, couldn't take a look earlier.

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

The above result is of a full run of the latest main branch in my local machine.

Sebastian had the same result on his machine

@bdsl
Copy link

bdsl commented Apr 2, 2024

since phpunit already works with psalm, I think the way forward would be to also integrate it with https://github.com/roave/infection-static-analysis-plugin ?

I think probably not. PHPUnit has a Psalm baseline file. The static analysis plugin reports mutants as killed whenever there is a baseline-suppressed psalm issue in the same PHP file. See Roave/infection-static-analysis-plugin#484

@maks-rafalko
Copy link
Contributor

maks-rafalko commented Apr 2, 2024

did we miss something? how to debug this situation?

how to get detailed logs is explained here: https://infection.github.io/guide/how-to.html#How-to-debug-Infection - it will show which command line and phpunit's output is there for each mutant, will help to understand Errors

--noop is also a debug option, described here: https://infection.github.io/guide/command-line-options.html#noop

I will try to look into it tonight or tomorrow asap. Could you please post a command line you use to run Infection locally?

@staabm
Copy link
Contributor Author

staabm commented Apr 2, 2024

Locally I used

php ./tools/infection --threads=max

On macos sonoma with php 8.2.x

@maks-rafalko
Copy link
Contributor

maks-rafalko commented Apr 2, 2024

Ok, I found many interesting things after debugging.

we currently see a huge portion of mutations running into "runtime error" or "uncovered" result.

First of all, we shouldn't run Infection as

php ./tools/infection --threads=max

for 2 reasons:

  1. we have testFrameworkOptions": "--testsuite=unit",, so by default we run only unit testsuite. That's why we see many Uncovered mutants, we basically don't run all the tests
  2. I think for now we should run Infection with --only-covered, as we are probably interested in covered MSI, not just MSI.

Now, let's see why you had so many Errors and almost nothing except them.

In other project/repositories, PHPUnit is being installed as a composer dependency or used as PHAR, in both cases this is not the project's code and PHPUnit's code is not being mutated.

Here, in this repository, we have the opposite case: we mutate the PHPUnit's code that is being executed for tests. So we mutate PHPUnit iteself and then use it in Infection (mutated version!).

Because of that, since Infection relies on many PHPUnit's features, it produces weird things because PHPUnit's code is mutated (works not as expected) 🤣

For example:

  • we see many X (syntax errors) because we mutate src/Framework/MockObject/Generator/.* files, and generated code is just invalid PHP code
  • we see many Errors because we mutate src/Util/Xml/Xml.php which then incorrectly reads/writes utf8 xml files (from what I understood)
  • we see many Errors because we mutate src/Runner/ResultCache/.* files. Infection highly relies on result cache to speed up the things. And when Result Cache writer is mutated, it then can't read files written by itself, leading to many Errors

For all the cases above, there is a simple solution - I just excluded those files from mutation process (see patch below). This is what @Wirone wrote about here.

Patch:

diff --git a/infection.json5.dist b/infection.json5.dist
index c234afb51..4dcf7b3c7 100644
--- a/infection.json5.dist
+++ b/infection.json5.dist
@@ -1,9 +1,17 @@
 {
-    "$schema": "vendor/infection/infection/resources/schema.json",
+    "$schema": "https://raw.githubusercontent.com/infection/infection/0.28.1/resources/schema.json",
     "testFrameworkOptions": "--testsuite=unit",
     "source": {
         "directories": [
             "src"
+        ],
+        "excludes": [
+            // causes errors during loading xml fies
+            "Util/Xml/Xml.php",
+            // causes syntax errors since invalid PHP code is generated
+            "{Framework/MockObject/Generator/.*}",
+            // causes issues with saving and reading result cache json files, failing phpunit
+            "{Runner/ResultCache/.*}",
         ]
     },
     "mutators": {

After these changes, Infection runs are consistent

How did I debug it?

By adding text logger

diff --git a/infection.json5.dist b/infection.json5.dist
index 4dcf7b3c7..aa91779be 100644
--- a/infection.json5.dist
+++ b/infection.json5.dist
@@ -16,5 +16,8 @@
     },
     "mutators": {
         "@default": true,
+    },
+    logs: {
+        text: "infection.log"
     }
 }

and running

./tools/infection --threads=max --only-covered --debug --log-verbosity=all

Then analyzing generated infection.log. See docs about how to debug Infection integration.


The last thing I want to mention, that after Infection run, sometimes there is not killed process that eats my CPU:

image

I didn't find the cause yet. But if you have an idea based on the image above, please let me know.

Created PR to apply the changes above: #5792

@yguedidi
Copy link

yguedidi commented Apr 3, 2024

Very interesting! Thank you!

Just curious about the issues you mentioned that led to excluding rules: wouldn't running unit tests with Infection using a PHAR, not altered, version of PHPUnit solve the issue?

@sebastianbergmann
Copy link
Owner

wouldn't running unit tests with Infection using a PHAR, not altered, version of PHPUnit solve the issue?

This might work, nice idea.

I just tried this by building a snapshot PHAR (ant phar-snapshot) and then running PHPUnit's unit test suite with it:

$ build/artifacts/phpunit-snapshot.phar --no-progress --testsuite unit                                  
PHPUnit 11.1-gb18d5f2fe3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.4
Configuration: /usr/local/src/phpunit/phpunit.xml

Time: 00:01.449, Memory: 58.86 MB

There was 1 PHPUnit error:

1) PHPUnit\Metadata\RequirementTest::testVersionRequirementCanBeCheckedUsingVersionConstraint
The data provider specified for PHPUnit\Metadata\RequirementTest::testVersionRequirementCanBeCheckedUsingVersionConstraint is invalid
PHPUnit\Metadata\Version\ConstraintRequirement::__construct(): Argument #1 ($constraint) must be of type PHPUnit\PharIo\Version\VersionConstraint, PharIo\Version\ExactVersionConstraint given, called in /usr/local/src/phpunit/tests/unit/Metadata/Version/RequirementTest.php on line 38
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Metadata/Api/DataProvider.php:107
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Metadata/Api/DataProvider.php:60
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Framework/TestBuilder.php:41
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Framework/TestSuite.php:400
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Framework/TestSuite.php:110
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Framework/TestSuite.php:173
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/Framework/TestSuite.php:198
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/TextUI/Configuration/Xml/TestSuiteMapper.php:79
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/TextUI/Configuration/TestSuiteBuilder.php:60
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/TextUI/Application.php:227
phar:///usr/local/src/phpunit/build/artifacts/phpunit-snapshot.phar/phpunit/TextUI/Application.php:106

/usr/local/src/phpunit/tests/unit/Metadata/Version/RequirementTest.php:61

ERRORS!
Tests: 3033, Assertions: 9336, Errors: 1.

The error we run into is caused by a type mismatch due to the nature of the scoped PHAR: in testVersionRequirementCanBeCheckedUsingVersionConstraint(), the code under test expects an PharIo\Version\VersionConstraint instance (from one of PHPUnit's dependencies). Due to the scoped PHAR, PHPUnit\PharIo\Version\VersionConstraint is used instead.

This can be worked around using this patch ...

diff --git a/tests/unit/Metadata/Version/RequirementTest.php b/tests/unit/Metadata/Version/RequirementTest.php
index a1b0a8f02..38f08e2cb 100644
--- a/tests/unit/Metadata/Version/RequirementTest.php
+++ b/tests/unit/Metadata/Version/RequirementTest.php
@@ -11,7 +11,6 @@
 
 use PharIo\Version\VersionConstraintParser;
 use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\Attributes\Small;
 use PHPUnit\Framework\Attributes\UsesClass;
@@ -29,26 +28,6 @@
 #[Group('metadata')]
 final class RequirementTest extends TestCase
 {
-    public static function constraintProvider(): array
-    {
-        return [
-            [
-                true,
-                '1.0.0',
-                new ConstraintRequirement(
-                    (new VersionConstraintParser)->parse('1.0.0'),
-                ),
-            ],
-        ];
-    }
-
-    public static function comparisonProvider(): array
-    {
-        return [
-            [true, '1.0.0', new ComparisonRequirement('1.0.0', new VersionComparisonOperator('='))],
-        ];
-    }
-
     public function testCanBeCreatedFromStringWithVersionConstraint(): void
     {
         $requirement = Requirement::from('^1.0');
@@ -57,10 +36,12 @@ public function testCanBeCreatedFromStringWithVersionConstraint(): void
         $this->assertSame('^1.0', $requirement->asString());
     }
 
-    #[DataProvider('constraintProvider')]
-    public function testVersionRequirementCanBeCheckedUsingVersionConstraint(bool $expected, string $version, ConstraintRequirement $requirement): void
+    #[Group('exclude-during-mutation-testing')]
+    public function testVersionRequirementCanBeCheckedUsingVersionConstraint(): void
     {
-        $this->assertSame($expected, $requirement->isSatisfiedBy($version));
+        $requirement = new ConstraintRequirement((new VersionConstraintParser)->parse('1.0.0'));
+
+        $this->assertTrue($requirement->isSatisfiedBy('1.0.0'));
     }
 
     public function testCanBeCreatedFromStringWithSimpleComparison(): void
@@ -71,10 +52,11 @@ public function testCanBeCreatedFromStringWithSimpleComparison(): void
         $this->assertSame('>= 1.0', $requirement->asString());
     }
 
-    #[DataProvider('comparisonProvider')]
-    public function testVersionRequirementCanBeCheckedUsingSimpleComparison(bool $expected, string $version, ComparisonRequirement $requirement): void
+    public function testVersionRequirementCanBeCheckedUsingSimpleComparison(): void
     {
-        $this->assertSame($expected, $requirement->isSatisfiedBy($version));
+        $requirement = new ComparisonRequirement('1.0.0', new VersionComparisonOperator('='));
+
+        $this->assertTrue($requirement->isSatisfiedBy('1.0.0'));
     }
 
     public function testCannotBeCreatedFromInvalidString(): void

... and invoking PHPUnit's test runner like so:

$ build/artifacts/phpunit-snapshot.phar --no-progress --testsuite unit --exclude-group exclude-during-mutation-testing
PHPUnit 11.1-gb18d5f2fe3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.4
Configuration: /usr/local/src/phpunit/phpunit.xml

Time: 00:01.247, Memory: 58.86 MB

OK (3033 tests, 9336 assertions)

I then changed Infection's configuration like so:

diff --git a/infection.json5.dist b/infection.json5.dist
index c234afb51..8c04624c8 100644
--- a/infection.json5.dist
+++ b/infection.json5.dist
@@ -1,6 +1,9 @@
 {
     "$schema": "vendor/infection/infection/resources/schema.json",
-    "testFrameworkOptions": "--testsuite=unit",
+    "phpUnit": {
+        "customPath": "build/artifacts/phpunit-snapshot.phar"
+    },
+    "testFrameworkOptions": "--testsuite=unit --exclude-group=exclude-during-mutation-testing",
     "source": {
         "directories": [
             "src"

And invoked Infection like so:

$ ./tools/infection --threads=max --ignore-msi-with-no-mutations --only-covered --logger-html=infection.html

    ____      ____          __  _
   /  _/___  / __/__  _____/ /_(_)___  ____
   / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \
 _/ // / / / __/  __/ /__/ /_/ / /_/ / / / /
/___/_/ /_/_/  \___/\___/\__/_/\____/_/ /_/

#StandWithUkraine

Infection - PHP Mutation Testing Framework version 0.28.1

[notice] You are running Infection with PCOV enabled.

Running initial test suite...

PHPUnit version: 11.1-gb18d5f2fe3

 2879 [============================] 7 secs
In NoLineExecuted.php line 12:
                                                                                                 
  No line of code was executed during tests. This could be due to "@covers" annotations or your  
  PHPUnit filters not being set up correctly.                                                    

I verified using ./build/artifacts/phpunit-snapshot.phar --testsuite unit --exclude-group exclude-during-mutation-testing --coverage-html /tmp/coverage that indeed no code coverage is collected.

And then it dawned on me: the tests are exercising the code of PHPUnit that is in the PHAR, the code of PHPUnit that is in the src directory. Hence no code coverage.

I suspect that humbug/php-scoper#347 might help here: if all code of PHPUnit that is marked @internal would also be scoped then, when using PHPUnit PHAR on PHPUnit's own test suite, would exercise the code of PHPUnit that is 1) in the src directory and 2) marked @internal.

@sebastianbergmann
Copy link
Owner

First of all, we shouldn't run Infection as

php ./tools/infection --threads=max

We actually use

./tools/infection --threads=max --git-diff-lines --git-diff-base=origin/$GITHUB_BASE_REF --ignore-msi-with-no-mutations --only-covered

in the GitHub Actions pipeline. This is also what I used locally.

@sebastianbergmann
Copy link
Owner

The doubt I expressed last night is not about "Infection is not ready", but rather "this code base is not yet in a shape where mutation testing makes sense". Of course, I might be wrong. We will only find out if we try.

I can now better (maybe? at least differently) express what I was "feeling" the other day: mutating the test runner while running tests for the test runner is a very special edge case :-)

sebastianbergmann pushed a commit that referenced this pull request Apr 3, 2024
@theofidry
Copy link
Contributor

Here, in this repository, we have the opposite case: we mutate the PHPUnit's code that is being executed for tests. So we mutate PHPUnit iteself and then use it in Infection (mutated version!).

That's a big oversight from our part and is the root of the issue.

Mutation testing is absolutely useless if you cannot trust that the test runner is behaving correctly. If it is it could very well mean that the mutation is seen as covered when it is not.

The fix however is easy, but looks like I didn't refresh my window correctly @sebastianbergmann already found the solution which is to get a clean test runner for infection.

Now though the flaws of partial scoping are showing. I'll look into it a bit more as I'm not certain addressing this point:

I suspect that humbug/php-scoper#347 might help here: if all code of PHPUnit that is marked @internal would also be scoped then, when using PHPUnit PHAR on PHPUnit's own test suite, would exercise the code of PHPUnit that is 1) in the src directory and 2) marked @internal.

Is enough. If it is though we could actually fix it a more elegant way.

@staabm
Copy link
Contributor Author

staabm commented Apr 3, 2024

Alternatively it might make sense to use a dedicated and more aggressive scoping config for the mutation-testing case only?

@theofidry
Copy link
Contributor

theofidry commented Apr 3, 2024

Ok so looking a bit more into it there is a bit more work necessary.

Conceptually PHPUnit can be split in two parts: the runner and the API.

  • The runner is what would be executed to run the tests: loads the test, apply its filtering and co. and run the relevant tests.
  • The API is what the user would use to declare their PHPUnit tests or extensions, and what the runner would consume when loading the user tests.

For the sake of the argument I will continue on the train of we are using a PHPUnit PHAR to execute the tests with Infection. Technically it doesn't need to be a PHAR, but I think it's a more relatable scenario and easier to understand as well.

When bundling PHPUnit into a scoped PHAR, for it to work nicely, we should get the runner part fully scoped. This might not be the case yet, in which case that is still work to be done.

The API however, should not as it is shared between the user code. Note that this means the runner may consume an incompatible API, so it requires to have check in places for graceful failures. For example a PHPUnit 11 runner will probably not understand a PHPUnit 9 API.

Now if we go back to our use case with Infection, as mentioned earlier you need a reliable test executor. As a result, the shared API cannot be mutated as it would affect the executor.

Conclusion

  • The runner should be fully scoped
  • The shared API should not be scoped at all
  • The shared API should not be mutated at all

How to achieve this?

I explained in humbug/php-scoper#347 why I'm not too keen to implement this in PHP-Scoper directly and looking at the above it is also clear that it won't be enough.

I am assuming from humbug/php-scoper#347 that the intent was to mark all of the runner code with @internal, and keep all of the rest as excluded. To be frank it is reaching a point where I feel having separate packages would be easier but let's assume this is not desired (and we should be able to find a solution without anyway).

In this case, a similar approach as what is required for scoping Wordpress plugins would be adequate (see https://github.com/humbug/php-scoper/blob/main/docs/further-reading.md#wordpress-support). For example, a separate tool could be created to check the PHPUnit codebase and collect the exposed symbols (i.e. the symbols that are not marked as @internal – assuming internal=runner and not-internal=shared API) and dump them.

From there:

  • The dumped public symbols can be used in the PHP-Scoper config to scope everything but leave the exposed symbols as excluded.
  • Ensure all of those public symbols' tests are not part of the test suite executed by Infection (either by configuring a dedicated test suite in the PHPUnit config or via a group).

@sebastianbergmann
Copy link
Owner

The dumped public symbols can be used in the PHP-Scoper config to scope everything but leave the exposed symbols as excluded.

https://github.com/sebastianbergmann/phpunit/blob/main/build/scripts/print-public-code-units.php is a script that prints the names of units of code that are considered public.

assuming internal=runner and not-internal=shared API

It's not as simple as that: parts of the runner are not @internal as they are used by the developers of wrappers around / extensions for the test runner. For instance, the value objects that represent the event system's events are not @internal. The object mentioned in #5792 (comment) is such a value object from the event system.

@sebastianbergmann
Copy link
Owner

The dumped public symbols can be used in the PHP-Scoper config to scope everything but leave the exposed symbols as excluded.

Does excluded-classes work with interfaces, traits, enumerations, too?

@sebastianbergmann
Copy link
Owner

@theofidry I started experimenting with excluded-classes and excluded-functions here: 05f57ad

@theofidry
Copy link
Contributor

@sebastianbergmann it should, but i can't recall if I went through with it or not

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type/tests Tests! Yes, PHPUnit, too, needs tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants