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

ExpressionRepeat mutator #1215

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

sanmai
Copy link
Member

@sanmai sanmai commented Mar 26, 2020

This PR:

  • Adds new ExpressionRepeat mutator, within a new family of Augmentation
  • Covered by tests
  • Tries to walk around statements without obvious side-effects:
    • Constant and scalar assignments
    • Variable assignments (most are false positives)
    • Other statements without instance method calls
  • Doc PR: https://github.com/infection/site/pull/XXX

This mutator duplicates statements with dynamic calls:

 $this->foo();
+$this->foo();

But leaves static and plain function calls alone, even if they're mixed together with a dynamic call. Therefore we can say these new mutations are 100% testable, although it cat be easier to let them be sometimes.

This mutator will show insufficient coverage in cases where one does not verify that a certain method called only once, or exact number of times.

Killing these mutations is a simple as adding ->expects($this->once()) to a mock.

Design drawbacks

Current implementation for the ImpureExpressionVisitor good enough to find impure statements, but may not find statements, that include other impure statements, as impure. This means the mutator creates less mutations than it possibly can otherwise, but at this point it may be sensible to keep the number low.

@sanmai sanmai added the Mutator label Mar 26, 2020
@maks-rafalko maks-rafalko added this to the 0.17.0 milestone Mar 26, 2020
Copy link
Member

@maks-rafalko maks-rafalko left a comment

Choose a reason for hiding this comment

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

Like the idea, one moment to discuss though

<?php

$z = $b->bar() + [1 => $c->foo()];
$z = $b->bar() + [1 => $c->foo()];
Copy link
Member

Choose a reason for hiding this comment

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

Won't we have many new hard-to-kill mutations with mutating any expression?

  1. Probably it's safer to mutate only method calls with side effects? (see MethodCallRemoval)

    for example, mutate this

    $this->foo()
    +$this->foo()

    but not this

    $a = $this->foo() + $this->bar();
    + $a = $this->foo() + $this->bar();

    Or, probably, default to mutating only side-effects method calls but add a setting for this mutator to mutate any expression. Wdyt?

    Now MSI is quite low for such mutator https://monosnap.com/direct/cb98GfVJArUligeK0FLRboh5dQrSa1

  2. Also, what about static calls? Any::foo() - is it mutated? let's add it to tests, as if it is, this will be extremely hard to kill

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point that is should try very hard to be as useful as it can. Let me see if I can limit it to only method-call containing statements.

Copy link
Member Author

Choose a reason for hiding this comment

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

I updated it to mutate only statements with method calls, but without static or function calls. This way we can be certain a mutation can be tested against. Can you confirm on your end?

As I see it, we get:

343 mutations were generated:
     157 mutants were killed
       0 mutants were not covered by tests
     186 covered mutants were not detected
       0 errors were encountered
       0 time outs were encountered

Metrics:
         Mutation Score Indicator (MSI): 45%
         Mutation Code Coverage: 100%
         Covered Code MSI: 45%

I'd say we have far worse mutations, which are often almost impossible to test against.

Mutator Mutations Killed Escaped Errors Timed Out MSI (%s) Covered MSI (%s)
ExpressionRepeat 343 162 181 0 0 47.23 47.23
CastFloat 7 3 4 0 0 42.86 42.86
GreaterThanOrEqualTo 7 3 4 0 0 42.86 42.86
RoundingFamily 14 6 8 0 0 42.86 42.86
UnwrapArrayValues 5 2 3 0 0 40.00 40.00
UnwrapStrToLower 1 0 1 0 0 0.00 0.00
ProtectedVisibility 1 0 1 0 0 0.00 0.00
LessThanOrEqualTo 1 0 1 0 0 0.00 0.00
CastObject 1 0 1 0 0 0.00 0.00

Copy link
Member

@theofidry theofidry left a comment

Choose a reason for hiding this comment

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

I'm not sure to understand: what bugs does it tries to uncover and how are you expected to kill those mutations?

src/Mutator/Augmentation/ExpressionRepeat.php Outdated Show resolved Hide resolved
@sanmai
Copy link
Member Author

sanmai commented Mar 26, 2020

This mutation will show insufficient coverage in cases where one does not verify that a certain method called only once. Killing is a simple as adding ->expects($this->once()) to a mock.

@theofidry
Copy link
Member

theofidry commented Mar 26, 2020

I must say, I'm really not convinced :/

Killing is a simple as adding ->expects($this->once()) to a mock.

There is 3 things bothering me in this argument:

  • Not everyone agrees you should count the number of your mocks. On a personal note, I used to systematically count those, but I found this just makes the tests a lot more coupled to the implementation, increasing the cost of the tests (in terms of efforts to write, maintenance, refactoring) without necessarily providing more value. There is very few cases where I genuinely care, e.g. calls to a third-party API, but those are the exception not the norm.
  • If you don't use a mock, you are out of luck or you need to go out of your way to allow spying on the number of calls made
  • You are asking to update tests: but what bug does it uncover?

You are effectively adding code, and asking the user to test that this code should not be able to be added. To me it is in the same line as creating a dummy interface, adding an interface to a class and asking the users to kill that mutation. And sure you can, it's as simple as adding $this->assertNotInstanceOf(). But does it makes sense still? No.

Now I'm aware I am biased against semantic additions, so let's get another axe of discussion as well. I'm trying your change on PHP-Scoper, here's the difference:

master
$ infection --coverage=build/coverage --skip-initial-tests -j4                                                                                                         
You are running Infection with PCOV enabled.

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

Infection - PHP Mutation Testing Framework dev-master@a402f03e1184dbe18ebee39eae9203e016e2f751


 [WARNING] Skipping the initial test run can be very dangerous.

           It is your responsibility to ensure the tests are in a passing state to begin.

           If this is not done then mutations may report as caught when they are not.




Generate mutants...

Processing source code files: 55/55
.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out

.....M....................M......M.............SSS   (  50 / 1057)
.........SSS.M.S.....MS..SM.....M...M.....M.M..SSS   ( 100 / 1057)
SSSSSSSSSSSM..M.....SSSS.SM............M...SSSSSSS   ( 150 / 1057)
...SSSSSSSSSSSSSSSSSSSSSS..SSSSSSSSSSSSSSSSSSSSSSS   ( 200 / 1057)
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS   ( 250 / 1057)
..M.M.MM..M....S...SS....MMMM...........M.........   ( 300 / 1057)
.....................MMMM............M...........M   ( 350 / 1057)
.M.S........M....M..M.............................   ( 400 / 1057)
..............S...................................   ( 450 / 1057)
M.............SM..................................   ( 500 / 1057)
...........M....................SM.....M..........   ( 550 / 1057)
..................M...M......M..S..S..............   ( 600 / 1057)
..............S.......M.MMM..M..............M.MM..   ( 650 / 1057)
................................M............M.M..   ( 700 / 1057)
..................M.......M.S.M............MMM..MM   ( 750 / 1057)
........................M.M..................M...M   ( 800 / 1057)
......M.....S..S......S..S..MSSSSMM...........SSSS   ( 850 / 1057)
SSS....SMS......S.M...S.....S....S..S......SSSSSSS   ( 900 / 1057)
.SSSSSS.SSSSSSSSSMM...SSS.M.M........MMS.......M..   ( 950 / 1057)
.SSS.......SSSS......MM....SS...................M.   (1000 / 1057)
.......M....................SSSSSSS.........SSSSS.   (1050 / 1057)
.......                                              (1057 / 1057)

1057 mutations were generated:
     765 mutants were killed
     210 mutants were not covered by tests
      82 covered mutants were not detected
       0 errors were encountered
       0 time outs were encountered

Metrics:
         Mutation Score Indicator (MSI): 72%
         Mutation Code Coverage: 80%
         Covered Code MSI: 90%

Please note that some mutants will inevitably be harmless (i.e. false positives).

Time: 1m 29s. Memory: 98.00MB
ExpressionRepeat
$ infection --coverage=build/coverage --skip-initial-tests -j4                                                                                                         
You are running Infection with PCOV enabled.

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

Infection - PHP Mutation Testing Framework dev-pr/2020-03/ExpressionRepeat@95ddae9a20db74830c88e5554bcbb17ecedbaff7


 [WARNING] Skipping the initial test run can be very dangerous.

           It is your responsibility to ensure the tests are in a passing state to begin.

           If this is not done then mutations may report as caught when they are not.




Generate mutants...

Processing source code files: 55/55
.: killed, M: escaped, S: uncovered, E: fatal error, T: timed out

..MMMM..M..M.MM.............M...M....M...MM....M..   (  50 / 1441)
M..........M....M......M..S.M...SSSSS...........M.   ( 100 / 1441)
...SSS....MS.M....M.SSSM...S...M..................   ( 150 / 1441)
.....MM......SSSSSSSSSSSSSSSSSSS...........M....SS   ( 200 / 1441)
SSSSSSSM.SS...M......S...M...........SSSSSSSS....M   ( 250 / 1441)
.MSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS.M.MSSSSSS   ( 300 / 1441)
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS   ( 350 / 1441)
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSMMM.MMMM   ( 400 / 1441)
M.......S....S..SSSSMMMMMMMM..........M........M.M   ( 450 / 1441)
..MMMM....M...........M..M..M.MMMM...M..M.M.M...M.   ( 500 / 1441)
.M...MM.M......M..MM.M.M.M..MMMS.M....M.M....MM...   ( 550 / 1441)
.................MM.MM.MM.....M...................   ( 600 / 1441)
...S...MM...............M...MMMM..............M...   ( 650 / 1441)
M..M..M......M.S....M...M.........................   ( 700 / 1441)
.......M.M............M.........M..M....SM.M...M.M   ( 750 / 1441)
....M...M....M....M.......M...M.....MM.....M..M...   ( 800 / 1441)
S...S....M....M..............M....MMMMSS.........M   ( 850 / 1441)
MMMMM..M..M.....M...M....MMMMMMMM.......M...M..M..   ( 900 / 1441)
..MMM.M..MMM.M...MM.MMM.....M............M.M....MM   ( 950 / 1441)
.M........M..M....M......................M........   (1000 / 1441)
MM...SM....M.......M...MMMM..M.MM.M...M.M..M....M.   (1050 / 1441)
..M.M....M..........M..M.M.M...M..M.M....M...M.M..   (1100 / 1441)
..M....MM..M.M........M........S..SM.M...SM..S.MMM   (1150 / 1441)
.M..M.M.SSSSSSMM...MMM...M....SSSSSSSS.M.MM.S.S...   (1200 / 1441)
MM..SMM...MS.....MS.....SM.S...M....SSSSSSSSMSSSSS   (1250 / 1441)
SSSSSMSSSSSSSSSSS.M....MMSSSSM.M..M...MM.M.MMMMMMM   (1300 / 1441)
M.S.......M....SSSSS.........SSSS......MM......SSS   (1350 / 1441)
....M...M.M............M...M................M.....   (1400 / 1441)
...SSSSSSS..M.......MMM..MSSSSSSS.......M            (1441 / 1441)

1441 mutations were generated:
     876 mutants were killed
     289 mutants were not covered by tests
     276 covered mutants were not detected
       0 errors were encountered
       0 time outs were encountered

Metrics:
         Mutation Score Indicator (MSI): 60%
         Mutation Code Coverage: 79%
         Covered Code MSI: 76%

Please note that some mutants will inevitably be harmless (i.e. false positives).

Time: 2m 1s. Memory: 102.00MB

So the result is:

  • 384 additional mutations (+36%)
    • +111 killed (+15%)
    • +194 covered escaped (+237%)
    • 64% of the covered mutations added were escaped
  • 2min 1s instead of 1min 29s (+32s so +36%)

But those are just indicators we added mutations, it itself it just shows the cost of that new mutator but not the benefits. So I took the extra effort and time to look at the report. And I genuinely looked for cases I might be interested in which I feel my tests where lacking, but I could not find any. So there is two possibilities:

  • The way the mutator works right now is highly inefficient. A few highlights:
         $this->eol = chr(10);
+        $this->eol = chr(10);
// This method is without side-effects
         $whitelistedFunctions = $this->whitelist->getRecordedWhitelistedFunctions();
+        $whitelistedFunctions = $this->whitelist->getRecordedWhitelistedFunctions();
// This method is without side-effects
         $hasNamespacedFunctions = $this->hasNamespacedFunctions($whitelistedFunctions);
+        $hasNamespacedFunctions = $this->hasNamespacedFunctions($whitelistedFunctions);
// This method is not without side-effect, but is idempotent
// this might lead to a slight performance impact, but nothing worth me carrying about
         $dump = $this->cleanAutoload($dump);
+        $dump = $this->cleanAutoload($dump);
         if ($hasNamespacedFunctions) {
             $eol = $this->eol;
+            $eol = $this->eol;
             $original = new FullyQualified($node[0]);
+            $original = new FullyQualified($node[0]);
         self::validateConfigKeys($config);
+        self::validateConfigKeys($config);
  • the kind of bug this mutator can uncover are extremely narrow cases

I'm more than willing to carry on on the debate with an open mind and I'm sure you can find some ways to improve the mutator. In its current form however, this mutator is a red flag to me.

@sanmai sanmai force-pushed the pr/2020-03/ExpressionRepeat branch from 95ddae9 to 0f10ccf Compare March 27, 2020 03:00
@sanmai
Copy link
Member Author

sanmai commented Mar 27, 2020

You are asking to update tests: but what bug does it uncover?

Testing defects, just like any other mutation. That's mutation testing is for, am I not right?

For example, this is a legitimate defect:

 $a = $this->dependency->veryLongExpensiveOperation();
+$a = $this->dependency->veryLongExpensiveOperation();

If you're not testing against these defects, Infection will point this out. It will also remind you with:

Please note that some mutants will inevitably be harmless (i.e. false positives).

@theofidry
Copy link
Member

theofidry commented Mar 27, 2020

I think it should be done the other way around: to ask to justify the existing code rather than justify the potentially added code. So with your example, let's say you have:

$a = $this->dependency->veryLongExpensiveOperation();
$a = $this->dependency->veryLongExpensiveOperation();

I would rather have the process of:

$a = $this->dependency->veryLongExpensiveOperation();
-$a = $this->dependency->veryLongExpensiveOperation();

and then justify that you need it called twice and not once. And eventually even:

-$a = $this->dependency->veryLongExpensiveOperation();

(provided we can do so without creating too many false-positives)

@sanmai
Copy link
Member Author

sanmai commented Mar 27, 2020

IIRC we have this the-other-way-around mutation, and it is awful TBH. For example, not only you won't be testing against these mutations, in many cases you just can't:

- Assert::notNull($example);

They're true false positives.

I think it can also make use of this new SideEffectPredictorVisitor (or similar) to make everyone's lives easier.

@theofidry
Copy link
Member

theofidry commented Mar 27, 2020

IIRC we have this the-other-way-around mutation, and it is awful TBH. For example, not only you won't be testing against these mutations, in many cases you just can't

It's the same with this mutator though: 64% of the generated ones are escaped in PHP-Scoper and 0% of it was something I really wanted to check against (regardless of its feasibility).

So the removal process makes a lot more sense to me, but whether we can pull it off in an efficient way (i.e. not too many false positives) is a different story.

@theofidry
Copy link
Member

I'm not sure to understand SideEffectPredictorVisitor: how can you tell a call is with or without side-effects? It's not because call is dynamic that it has any

@sanmai
Copy link
Member Author

sanmai commented Mar 27, 2020

We can tell that a call most likely without testable side-effects if it is a static call or function call. Therefore we do not mutate them here.

Yeah, it is easy to make this mutator alter statements with static calls, but who we are to punish people using a bad API without DI? I don't think this is where we should stand.

@sanmai sanmai requested a review from BackEndTea April 14, 2020 01:18
@maks-rafalko maks-rafalko modified the milestones: 0.17.0, 0.18.0 Aug 16, 2020
@maks-rafalko maks-rafalko modified the milestones: 0.18.0, 0.19 Oct 17, 2020
@maks-rafalko maks-rafalko modified the milestones: 0.19, 0.20.0, 0.21.0 Oct 28, 2020
@MidnightDesign
Copy link
Sponsor Contributor

I'm not sure I like this mutator. I get that it can be very useful, but I also think that many mutations will be very hard to kill. Consider the following:

final class Foo {
    private ?string $bar = null;
    public function setBar(string $bar): void {
        $this->bar = $bar;
    }
    // ...
}

// Somewhere else:
$foo->setBar('test');

How am I supposed to kill the duplication of $foo->setBar('test');? Foo is final, so I can't use a test double.

What I like about all of the existing mutators is that they don't produce any false positives. At least I haven't found any so far. Everything Infection tells me about is a legitimate problem that I can address. This mutator would introduce false positives I can't do anything about.

Maybe it could be left out of the default mutators and users would have to manually enable it or something..?

@sanmai sanmai marked this pull request as draft January 9, 2021 14:51
@sanmai sanmai force-pushed the pr/2020-03/ExpressionRepeat branch from 1f775fd to 81b26df Compare January 9, 2021 14:52
@maks-rafalko maks-rafalko removed this from the 0.21.0 milestone Feb 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants