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
Add pre-filters configuration option #1327
Conversation
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.
This gets the work done, but is not really an acceptable approach.
I suggest having a plugins
string reference, which requires a list<class-string<InfectionPlugin>>
, where:
interface InfectionPlugin
{
// @TODO singleton? I would really rather just talk to the infection container instead of this
public static function instance(): static;
/** @psalm-return null|callable(Mutant): bool */
public function preFilterMutant(): ?callable;
}
Requiring zero-argument-constructor is not really viable
src/Mutant/MutantFiltersHandler.php
Outdated
foreach ($this->filters as $filterClassName) { | ||
Assert::classExists($filterClassName); | ||
|
||
new $filterClassName($mutants); |
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.
This looks exceptionally ugly :D
Can we just have the filter instances injected? They will come with their complex DI setup anyway...
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 is very peculiar indeed.
You mean like with $container->getMutantFiltersHandler()->addFilter(...)
? Would this mean a filter will have own executable? How one could have several filters?
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.
Ideally (yes, below is a callable producing another callable):
/** @psalm-var callable(Container): callable(Mutant): bool $factory */
$container->registerFilter($factory);
Plugin system could then be something like:
interface InfectionPlugin
{
public static function register(Container $container): void;
}
No runtime operation: the plugin can only manipulate or use the infection DIC before it is given to Application
:
infection/src/Console/Application.php
Line 72 in b28f2e2
public function __construct(Container $container) |
Registering plugins is then a matter of having a list<class-string<InfectionPlugin>>
as part of infection.json.dist
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.
Note: we can certainly keep InfectionPlugin
as @internal
and say "it's experimental - expect breakages in minor releases" or such
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.
What will InfectionPlugin::register()
do?
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.
Whatever the plugin needs to do to start up.
For example, in my case I need to get the CWD and find psalm's config file, others may register autoloading, etc.
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.
I mean, does it really need to have an instance of Container?
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.
Yes, because that's how you are going to manipulate and decorate/replace infection components.
|
@Ocramius Please see the updated API. Right now I'm not so sure about giving access to the full Containter object because if your plugin becomes a success, which I hope it will, and if we'll have to break things inside Container, which I have some ideas about, there will be BC breaks, and they'll become upgrade blockers like with have now with PHPUnit 9.3. If we can avoid these, we better do, so if you have an idea exactly what you want to know from the container object, or exactly what you want to do with it, we can make an explicit API just for that, ensuring future BC breaks would be less of an issue. |
Hopefully reviewing later today then 👍 |
@Ocramius I've added exportable interfaces for Container and Mutant. If you need anything specifically from Container or underlying object like Configuration, we can add these now. Test are broken, I'll fix them if we decide this is the right approach. |
9308bcf
to
4d4f6d7
Compare
I'd like to do something about #1283, and this might require more configuration changes, and I really don't want to step on my own toes there and here, so if you might find a moment, what if we can reach a certain decision here? |
I totally missed to check this one as I hoped to do earlier last week, sorry. Will try allocating time for it on Monday/Tuesday this starting week :| |
@@ -101,4 +108,9 @@ public function getTests(): array | |||
{ | |||
return $this->mutation->getAllTests(); | |||
} | |||
|
|||
public function getMutantWrapper(): PublicMutant |
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.
public function getMutantWrapper(): PublicMutant | |
public function getPublicMutant(): PublicMutant |
/** | ||
* @var PublicMutant|null | ||
*/ | ||
private $wrapper; |
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.
private $wrapper; | |
private $publicMutant; |
/** @return Plugin */ | ||
public static function create(Configuration $configuration); |
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.
/** @return Plugin */ | |
public static function create(Configuration $configuration); | |
public static function create(Configuration $configuration): Plugin; |
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.
That's going to be a major headache PHPStan-wise, unfortunately. I know this because the original design had exactly this signature, and, well, suffice to say that return type covariance isn't yet implemented in PHP 7.3.
But I'll try to come back at this problem again.
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.
If we merge the two plugin interfaces that problem will be solved no?
*/ | ||
final class ContainerWrapper implements Configuration |
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 Configuration
appropriate? In the end should not be Container
?
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 should not be Container
, yes. It may include some features on an actual Container
, but I have yet to see why a plugin can't roll their own DI container.
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.
I believe it's not so much a question of whether they have their own DI, but about either exposing some elements of our Container. For example here you could have a Container::registerMutantFilter()
hence you don't need to call the plugin afterwards, you just create it, it will register what it needs to and done.
So maybe an alternative approach is to have this public Container
interface where we have the registerMutantFilter()
and make our container implement that public one: that way we keep one container instance on our side but be very clear on what is exposed to the Plugin API
* | ||
* @psalm-return null|callable(Mutant): bool | ||
*/ | ||
public function getMutantFilter(): ?callable; |
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.
Might be more straightforward to pass an array of such callables instead? This way we can also require the array to be a callbable[]
i.e. not allow 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.
Design-wise it won't matter for us will it be an iterable<callable(Mutant)>
or just a callable. @Ocramius, do you think you'll be better off with a list of callables here?
* @internal | ||
* @final | ||
*/ | ||
class MutantFiltersHandler |
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.
class MutantFiltersHandler | |
class MutantFilterer |
?
* Plugin to filter mutations with external tools. | ||
*/ | ||
interface MutantFilterPlugin extends Plugin | ||
{ |
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.
Maybe for now we can merge MutantFilterPlugin
with Plugin
? It's not too constraining to implement getMutantFilter()
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 can, but that'll cost us BC breaks in the future. For example, there's an idea to make #1326 an internal plugin. It may require a different interface, and a different injection point. Merging these interfaces will create an inconvenience.
@@ -73,6 +75,7 @@ class MutationTestingRunner | |||
public function __construct( | |||
MutantProcessFactory $processFactory, | |||
MutantFactory $mutantFactory, | |||
MutantFiltersHandler $pluginsHandler, |
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.
MutantFiltersHandler $pluginsHandler, | |
MutantFiltersHandler $mutantFilterer, |
In a sense we don't care where it comes from at this point, plugin or elsewhere. I'm even wondering if the two filter()
calls should be moved to such filterers
@Ocramius I really appreciate that! I don't want you spend time reviewing implementation (I don't think I spend enough time polishing it, and there are pending suggestions), but if you can tell that the external interface is sufficient or not, that'll be great. I'm thinking about adding another object to the callback, named, say, |
I quite like that idea and push it even a bit further: class MutantExecutionResult {
}
class MutantExecution {
public readonly Mutant $mutant;
public readonly MutantExecutionResult $result;
}
public function run(iterable $mutations, string $testFrameworkExtraOptions): void
{
$numberOfMutants = IterableCounter::bufferAndCountIfNeeded($mutations, $this->runConcurrently);
$this->eventDispatcher->dispatch(new MutationTestingWasStarted($numberOfMutants));
$processes = take($mutations)
->map(function (Mutation $mutation): Mutant {
return new MutantExecution(
$this->mutantFactory->create($mutation),
new MutantExecutionResult()
);
})
->tap(function (MutantExecution $execution) {
if (!$execution->isCoveredByTest()) {
$execution->result->status = 'notCovered';
}
})
->tap(function (MutantExecution $execution) {
$result = $execution->result;
if ($result->status !== 'pending') {
return;
}
$mutant = $execution->mutant;
$mutatorName = $mutant->getMutation()->getMutatorName();
if (!array_key_exists($mutatorName, $this->ignoreSourceCodeMutatorsMap)) {
return;
}
foreach ($this->ignoreSourceCodeMutatorsMap[$mutatorName] as $sourceCodeRegex) {
if ($this->diffSourceCodeMatcher->matches($mutant->getDiff(), $sourceCodeRegex)) {
$result->status = 'ignored';
return;
}
}
})
->tap(function (MutantExecution $execution) {
if ($execution->mutant->getMutation()->getNominalTestExecutionTime() > $this->timeout) {
$execution->result->status = 'skipped';
}
})
// Note that the commend from the PR still applies: the two previous
// taps could be moved in the handler in some way.
// I would however prefer an API we we can wire the call like so
// Instead of doing a $this->filterer->applyFilters($pipeline);
// Also note that this comes _after_ all the previous call to allow
// the plugins to take advantage of some infection built-in processing
// and correct the information if they see fit before we do any
// actual filtering
->tap($this->pluginsHandler->tap())
->filter(function (MutantExecution $execution) {
$status = $execution->result->status;
$mutant = $execution->mutant;
if ($status === 'notCovered') {
$this->eventDispatcher->dispatch(new MutantProcessWasFinished(
MutantExecutionResult::createFromNonCoveredMutant($mutant)
));
return false;
}
if ($status === 'ignored') {
// Do not appear in the results at all
return false;
}
if ($status === 'skipped') {
$this->eventDispatcher->dispatch(new MutantProcessWasFinished(
MutantExecutionResult::createFromTimeSkippedMutant($mutant)
));
return false;
}
return !$execution->result->executed;
})
->map(function (MutantExecution $execution) use ($testFrameworkExtraOptions): ProcessBearer {
$mutant = $execution->mutant;
$this->fileSystem->dumpFile($mutant->getFilePath(), $mutant->getMutatedCode());
$process = $this->processFactory->createProcessForMutant($mutant, $testFrameworkExtraOptions);
return $process;
})
;
$this->processRunner->run($processes);
$this->eventDispatcher->dispatch(new MutationTestingWasFinished());
} |
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.
I'm really sorry for the delay with the feedback, tough days here in Belarus...
As for me - I agree with several of Theo comments. Design for me looks good, I'm ok with existing architecture.
Let's see what @Ocramius will say as he is the main consumer at the moment.
The only one major issue here is package design principles. I don't say we should do anything right now, let's agree on the architecture first, but then - we will need to move interfaces out of infection/infection
.
Idea is similar to what I described here for Test Framework Adapters.
For example, one of the reasons of moving interfaces to separate packages is to allow developers not to depend on infection/infection
in their plugins.
Currently, Roave/infection-static-analysis-plugin
depends on infection/infection:0.17.5
and will depend on the same package if we merge it as is.
Instead, it would depend on infection/abstract-plugin:^1.0.0
(name is just for example) and would work with any version of infection/infection
that works properly with infection/abstract-plugin:^1.0.0
and so on...
So, 👍 on what is already here
@Ocramius friendly reminder about this PR |
What is the next step of this PR ? |
I think it is time for someone to make a fresh start 😓 |
Few years later I would emphasize @Ocramius' suggestion to let the plugins operate on the container. |
I already started investigating it and have new ideas in mind (the current solution for me is not what we need for Psalm/PHPStan plugins, because we should not pre-filter mutations but kill mutations by SA tool only after tests can not kill them (this is btw what happens on @VincentLanglet I will (hopefully) open RFC somewhere this year, this is the next feature I will work on (read - most prioritized) |
Awesome, glad to hear it's on the todo-list :) |
I got back to this idea. I decided to simplify the things first:
and only then we can think if we really need any "plugin system". If it's only PHPStan and all other virtual potential cases are just imaginary ones, I don't think we need to spend time on it, and we will just integrate PHPStan in the core first, opt-in (TBD) |
This PR:
Premise
The static analysis plugin uses only
$mutant->getFilePath();
to infer whenever a mutation is good to go or not, but there might be additional optimizations to check for new defects, for example if we consider that we know which line we're mutating.Design
To address the needs of this or similar plugins this PR introduces the notion of a plugin.
"plugins"
as FQCN.::create()
MutantFilterPlugin
getMutantFilter()
method returning a callable.Example filter
For this filter to work
infection.json
needs to be updated like so:Future new filters should not require configuration changes since we can infer filter usage by looking at its type.
These filters work with
infection.phar
just as well as with Composer-installed Infection.