-
-
Notifications
You must be signed in to change notification settings - Fork 154
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
Reduce "Initial Tests Run" stage time by running only covering test files #1919
Reduce "Initial Tests Run" stage time by running only covering test files #1919
Conversation
…e the time for Initial Tests Run
0f1547d
to
b8256b5
Compare
array_map( | ||
static fn (SplFileInfo $sourceFile): string => sprintf('%sTest', $sourceFile->getBasename('.' . $sourceFile->getExtension())), | ||
$this->filteredSourceFilesToMutate, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 10 lines actually the real implementation of the feature that needs attention.
as I wrote in the description, this is a simple
strategy that maps SourceClass
to SourceClassTest
6138a30
to
ddc9d72
Compare
…ble "Initial Tests Run" test files filtering Implements #1451
ddc9d72
to
e675584
Compare
src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php
Outdated
Show resolved
Hide resolved
src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php
Outdated
Show resolved
Hide resolved
src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php
Outdated
Show resolved
Hide resolved
src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php
Outdated
Show resolved
Hide resolved
/** | ||
* @internal | ||
*/ | ||
final class MapSourceClassToTestStrategy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use enums for this purposes.
And to avoid nullable values technically we could use NONE
as an option instead of NULL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought about it. But intentionally didn't use Enums because we have all the "enum-like" classes as usual classes with constants. So it would be strange to have somewhere enums and somewhere classes with constants - I decided to be consistent here.
If we decide to use enums - we can replace all the classes at once as a separate PR. Currently, 0 enums are in the code base.
iterator_to_array($this->sourceFileFilter->filter($this->infectionConfig->getSourceFiles())) | ||
)); | ||
/** @var list<SplFileInfo> $files */ | ||
$files = iterator_to_array($this->sourceFileFilter->filter($this->infectionConfig->getSourceFiles())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this change (that is a bit noisy as it is affecting a few method signatures / phpstan / mutations) be extracted?
static fn (SplFileInfo $file) => $file->getRealPath(), | ||
iterator_to_array($this->sourceFileFilter->filter($this->infectionConfig->getSourceFiles())) | ||
)); | ||
/** @var list<SplFileInfo> $files */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is that phpdoc really necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
short answer: yes, because deeper in the stack we have return type iterable<SplFileInfo|Trace>
and here we need to specify it.
In other word - this type was here, and removing of this @var
produces a phpstan error.
I tried quickly to replace iterable<SplFileInfo|Trace>
with iterable<T>
in the stack - but that produces too many errors that need to be analyzed separately. Something wrong with types across those classes.
@@ -70,7 +72,8 @@ public static function create( | |||
array $sourceDirectories, | |||
bool $skipCoverage, | |||
bool $executeOnlyCoveringTestCases = false, | |||
array $filteredSourceFilesToMutate = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated, but we could update the CS fixer to append the trailing commas all the time to avoid this sort of diff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, extracted to #1920
@@ -123,6 +141,34 @@ public function buildForMutant(string $configPath, string $extraOptions, array $ | |||
return $options; | |||
} | |||
|
|||
private function mapSourceClassToTestClass(SplFileInfo $sourceFile): string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be static
/** | ||
* @return list<string> | ||
*/ | ||
private function prepareArgumentsAndOptions(string $configPath, string $extraOptions): array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be static
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not a common practice in Infection's codebase, so I decided to leave it as is.
If we decide to use static methods every time there is no $this
in it - let's discuss it and probably integrate a fixer? But I wonder what's the benefit and whether it worth doing it.
# Conflicts: # src/Command/RunCommand.php # src/Configuration/Configuration.php # src/Configuration/ConfigurationFactory.php # src/Container.php # src/TestFramework/Factory.php # src/TestFramework/PhpUnit/Adapter/PestAdapterFactory.php # src/TestFramework/PhpUnit/Adapter/PhpUnitAdapterFactory.php # src/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilder.php # tests/benchmark/Tracing/provide-traces-closure.php # tests/phpunit/Configuration/ConfigurationAssertions.php # tests/phpunit/Configuration/ConfigurationFactoryTest.php # tests/phpunit/Configuration/ConfigurationTest.php # tests/phpunit/ContainerTest.php # tests/phpunit/TestFramework/PhpUnit/CommandLine/ArgumentsAndOptionsBuilderTest.php
Wanted to share: |
This PR:
--map-source-class-to-test
option site#259Originally requested here: #1451
Idea explanation
If we have X tests files and run Infection with for example
infection --git-diff-lines
and only 1 file is updated/added - it does not make sense to run all the X tests to generate coverage if we know that only 1 file of X covers the source code file.In practice, PHPUnit even has different methods that help define which test file covers which class:
requireCoverageMetadata
configuration attribute (previouslyforceCoversAnnotation="true"
)CoversClass
attribute /@covers
annotation and othersWith the following code:
we can say that
Calculator
class is covered byCalculatorTest
test class, and vice-versa. So, from Infection point of view, when we use--git-diff-lines
and someone creates PR with updatedCalculator
class, we can run onlyCalculatorTest
and skip other X test files (as they anyway don't add any useful coverage), saving a lot of time.Things to note
--map-source-class-to-test
, so it's opt-inExample with PHP-CS-Fixer
After maintainers of PHP-CS-Fixer decided to integrate Infeciton into their pipeline, I was analysing why "Initial Tests Run" stage takes so much time when even only 1 file is changed in PR.
It turned out, that PHP-CS-Fixer has ~24,000 unit tests, that all are executed when we change 1 source file.
At the same time, PHP-CS-Fixer has
@covers
annotation on all test classes, and we can 100% map which test class covers which source class, and run only covering test classes instead of all ~24,000 tests.Before:
After:
Difference:
So, for a simple PR with 1 file chaged, now "Initital Tests Run" runs 41 tests by 12 seconds instead of 24,768 tests by 4 minutes.
Technical comments
I said the current implementation is intentionally simple, meaning the following: currently, we are not finding which test class covers particular changed file by finding all the
CoversClass
attributes /@covers
annotations, but just make an assumption that if you haveCalculator
class, then test that covers it is namedCalculatorTest
.Actually this is true for Infection, this is how we map Source Class to Test Class:
infection/tests/phpunit/AutoReview/SourceTestClassNameScheme.php
Lines 57 to 64 in eb4842d
and this is also true for PHP-CS-Fixer, they also have such mapping from Source Class to Test Class:
https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/3b3bfe90fe8d5d920328a1f36288d2dfacf5d6b3/tests/AutoReview/ProjectCodeTest.php#L73-L78
Since we now know that
Calculator
class is covered byCalculatorTest
test class, we can very simply build a--filter
option for "Initial Tests Run" stage, just by configuring regex:I've named this "strategy" of mapping source class to test class as
simple
. Later, if needed, we can implement more complex non-standard strategies, e.g. the one that will phisically parse all files fromtests
folder and find those classes that haveCoversClass(Calculator::class)
, if we need more strict mapping.But I intentionally didn't do it at this point.