Skip to content

Coverage not detected in closures? #1736

@Ocramius

Description

@Ocramius
Question Answer
Infection version 0.26.14
Test Framework version PHPUnit 9.5.25
PHP version 8.1.10
Platform Nixos
Github Repo -

Infection seems to not detect coverage inside closures. For example, in https://github.com/Roave/BackwardCompatibilityCheck/actions/runs/3221773917/jobs/5270116570 :

Not Covered mutants:
====================

1) /github/workspace/src/Changes.php:58    [M] Foreach_

--- Original
+++ New
@@ @@
         $instance = new self([]);
         $instance->bufferedChanges = [];
         $instance->unBufferedChanges = (function () use($other) : Generator {
-            foreach ($this as $change) {
+            foreach (array() as $change) {
                 (yield $change);
             }
             foreach ($other as $change) {


According to my PHPUnit run, I have 100% code coverage:

phpdbg -qrr -dmemory_limit=10G ./vendor/bin/phpunit --coverage-text

<SNIP>

Time: 00:13.629, Memory: 100.00 MB

OK (736 tests, 1058 assertions)


Code Coverage Report:       
  2022-10-10 19:46:59       
                            
 Summary:                   
  Classes: 100.00% (97/97)  
  Methods: 100.00% (252/252)
  Lines:   100.00% (915/915)

The behavior seems to be consistent across multiple projects.

phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
   bootstrap="test/bootstrap.php"
   colors="true"
   convertDeprecationsToExceptions="true"
>
   <testsuites>
       <testsuite name="unit">
           <directory>./test/unit</directory>
       </testsuite>
       <testsuite name="end to end">
           <directory>./test/e2e</directory>
       </testsuite>
   </testsuites>

   <coverage processUncoveredFiles="true">
       <include>
           <directory suffix=".php">./src</directory>
       </include>
   </coverage>

   <php>
       <ini name="error_reporting" value="E_ALL"/>
   </php>
</phpunit>
Output with issue
Running phpdbg -qrr ./vendor/bin/roave-infection-static-analysis-plugin

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

#StandWithUkraine

Infection - PHP Mutation Testing Framework version 0.26.14

Notice:  You are running Infection with phpdbg enabled.

Running initial test suite...

PHPUnit version: 9.5.25

.: killed, M: escaped, U: uncovered, E: fatal error, X: syntax error, T: timed out, S: skipped, I: ignored


Generate mutants...

Processing source code files: 0
.....................................U....UUU.....   (   50)
.UUUU.............................................   (  100)
..................................................   (  150)
..................................................   (  200)
..................................................   (  250)
............................UUUUUU................   (  300)
......................E...........UUUUUUU.........   (  350)
.............................................UU...   (  400)
.UU.....

408 mutations were generated:
    382 mutants were killed
      0 mutants were configured to be ignored
     25 mutants were not covered by tests
      0 covered mutants were not detected
      1 errors were encountered
      0 syntax errors were encountered
      0 time outs were encountered
      0 mutants required more time than configured

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

Note: to see escaped mutants run Infection with "--show-mutations" or configure file loggers.

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

Timed Out mutants:
==================

Skipped mutants:
================

Not Covered mutants:
====================

1) /github/workspace/src/Changes.php:58    [M] Foreach_

--- Original
+++ New
@@ @@
        $instance = new self([]);
        $instance->bufferedChanges = [];
        $instance->unBufferedChanges = (function () use($other) : Generator {
-            foreach ($this as $change) {
+            foreach (array() as $change) {
                (yield $change);
            }
            foreach ($other as $change) {


2) /github/workspace/src/Changes.php:62    [M] Foreach_

--- Original
+++ New
@@ @@
            foreach ($this as $change) {
                (yield $change);
            }
-            foreach ($other as $change) {
+            foreach (array() as $change) {
                (yield $change);
            }
        })();


3) /github/workspace/src/CompareClasses.php:35    [M] LogicalOr

--- Original
+++ New
@@ @@
    public function __invoke(Reflector $definedSymbols, Reflector $pastSourcesWithDependencies, Reflector $newSourcesWithDependencies) : Changes
    {
        $definedApiClassNames = Dict\map(Dict\filter($definedSymbols->reflectAllClasses(), function (ReflectionClass $class) : bool {
-            return !($class->isAnonymous() || $this->isInternalDocComment($class->getDocComment()));
+            return !($class->isAnonymous() && $this->isInternalDocComment($class->getDocComment()));
        }), static function (ReflectionClass $class) : string {
            return $class->getName();
        });


4) /github/workspace/src/CompareClasses.php:35    [M] LogicalNot

--- Original
+++ New
@@ @@
    public function __invoke(Reflector $definedSymbols, Reflector $pastSourcesWithDependencies, Reflector $newSourcesWithDependencies) : Changes
    {
        $definedApiClassNames = Dict\map(Dict\filter($definedSymbols->reflectAllClasses(), function (ReflectionClass $class) : bool {
-            return !($class->isAnonymous() || $this->isInternalDocComment($class->getDocComment()));
+            return $class->isAnonymous() || $this->isInternalDocComment($class->getDocComment());
        }), static function (ReflectionClass $class) : string {
            return $class->getName();
        });


5) /github/workspace/src/DetectChanges/BCBreak/ClassBased/ConstantRemoved.php:35    [M] LogicalOr

--- Original
+++ New
@@ @@
    private function accessibleConstants(ReflectionClass $class) : array
    {
        return Dict\filter($class->getConstants(), static function (ReflectionClassConstant $constant) : bool {
-            return $constant->isPublic() || $constant->isProtected();
+            return $constant->isPublic() && $constant->isProtected();
        });
    }
}


6) /github/workspace/src/DetectChanges/BCBreak/ClassBased/MethodRemoved.php:48    [M] LogicalOr

--- Original
+++ New
@@ @@
    private function accessibleMethods(ReflectionClass $class) : array
    {
        $methods = Vec\filter($class->getMethods(), function (ReflectionMethod $method) : bool {
-            return ($method->isPublic() || $method->isProtected()) && !$this->isInternalDocComment($method->getDocComment());
+            return $method->isPublic() && $method->isProtected() && !$this->isInternalDocComment($method->getDocComment());
        });
        return Dict\associate(Vec\map($methods, static function (ReflectionMethod $method) : string {
            return $method->getName();


7) /github/workspace/src/DetectChanges/BCBreak/ClassBased/MethodRemoved.php:48    [M] LogicalAnd

--- Original
+++ New
@@ @@
    private function accessibleMethods(ReflectionClass $class) : array
    {
        $methods = Vec\filter($class->getMethods(), function (ReflectionMethod $method) : bool {
-            return ($method->isPublic() || $method->isProtected()) && !$this->isInternalDocComment($method->getDocComment());
+            return $method->isPublic() || $method->isProtected() || !$this->isInternalDocComment($method->getDocComment());
        });
        return Dict\associate(Vec\map($methods, static function (ReflectionMethod $method) : string {
            return $method->getName();


8) /github/workspace/src/DetectChanges/BCBreak/ClassBased/MethodRemoved.php:50    [M] LogicalNot

--- Original
+++ New
@@ @@
    private function accessibleMethods(ReflectionClass $class) : array
    {
        $methods = Vec\filter($class->getMethods(), function (ReflectionMethod $method) : bool {
-            return ($method->isPublic() || $method->isProtected()) && !$this->isInternalDocComment($method->getDocComment());
+            return ($method->isPublic() || $method->isProtected()) && $this->isInternalDocComment($method->getDocComment());
        });
        return Dict\associate(Vec\map($methods, static function (ReflectionMethod $method) : string {
            return $method->getName();


9) /github/workspace/src/DetectChanges/BCBreak/ClassBased/PropertyRemoved.php:47    [M] LogicalOr

--- Original
+++ New
@@ @@
    {
        $classIsOpen = !$class->isFinal();
        return Dict\filter($class->getProperties(), function (ReflectionProperty $property) use($classIsOpen) : bool {
-            return ($property->isPublic() || $classIsOpen && $property->isProtected()) && !$this->isInternalDocComment($property->getDocComment());
+            return $property->isPublic() && ($classIsOpen && $property->isProtected()) && !$this->isInternalDocComment($property->getDocComment());
        });
    }
    private function isInternalDocComment(string|null $comment) : bool


10) /github/workspace/src/DetectChanges/BCBreak/ClassBased/PropertyRemoved.php:47    [M] LogicalAnd

--- Original
+++ New
@@ @@
    {
        $classIsOpen = !$class->isFinal();
        return Dict\filter($class->getProperties(), function (ReflectionProperty $property) use($classIsOpen) : bool {
-            return ($property->isPublic() || $classIsOpen && $property->isProtected()) && !$this->isInternalDocComment($property->getDocComment());
+            return $property->isPublic() || $classIsOpen && $property->isProtected() || !$this->isInternalDocComment($property->getDocComment());
        });
    }
    private function isInternalDocComment(string|null $comment) : bool


11) /github/workspace/src/DetectChanges/BCBreak/ClassBased/PropertyRemoved.php:48    [M] LogicalAnd

--- Original
+++ New
@@ @@
    {
        $classIsOpen = !$class->isFinal();
        return Dict\filter($class->getProperties(), function (ReflectionProperty $property) use($classIsOpen) : bool {
-            return ($property->isPublic() || $classIsOpen && $property->isProtected()) && !$this->isInternalDocComment($property->getDocComment());
+            return ($property->isPublic() || ($classIsOpen || $property->isProtected())) && !$this->isInternalDocComment($property->getDocComment());
        });
    }
    private function isInternalDocComment(string|null $comment) : bool


12) /github/workspace/src/DetectChanges/BCBreak/ClassBased/PropertyRemoved.php:49    [M] LogicalNot

--- Original
+++ New
@@ @@
    {
        $classIsOpen = !$class->isFinal();
        return Dict\filter($class->getProperties(), function (ReflectionProperty $property) use($classIsOpen) : bool {
-            return ($property->isPublic() || $classIsOpen && $property->isProtected()) && !$this->isInternalDocComment($property->getDocComment());
+            return ($property->isPublic() || $classIsOpen && $property->isProtected()) && $this->isInternalDocComment($property->getDocComment());
        });
    }
    private function isInternalDocComment(string|null $comment) : bool


13) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] ArrayItemRemoval

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
        });
    }
}


14) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] Concat

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . ' - ' . "\n";
        });
    }
}


15) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] ConcatOperandRemoval

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
        });
    }
}


16) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] ConcatOperandRemoval

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return ' - ' . "\n";
        });
    }
}


17) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] Concat

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return ' - ' . "\n" . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']);
        });
    }
}


18) /github/workspace/src/Formatter/MarkdownPipedToSymfonyConsoleFormatter.php:66    [M] ConcatOperandRemoval

--- Original
+++ New
@@ @@
    private function convertFilteredChangesToMarkdownBulletList(Closure $filterFunction, Change ...$changes) : array
    {
        return Vec\map(Vec\filter($changes, $filterFunction), static function (Change $change) : string {
-            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']) . "\n";
+            return ' - ' . Str\replace_every(Str\trim($change->__toString()), ['ADDED: ' => '', 'CHANGED: ' => '', 'REMOVED: ' => '', 'SKIPPED: ' => '']);
        });
    }
}


19) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:42    [M] MethodCallRemoval

--- Original
+++ New
@@ @@
        Psl\invariant(Filesystem\is_file($installationPath . '/composer.json'), 'Could not locate composer.json within installation path.');
        $this->runInDirectory(function () use($installationPath, $includeDevelopmentDependencies) : void {
            $installer = ($this->makeComposerInstaller)($installationPath);
-            // Some defaults needed for this specific implementation:
-            $installer->setDevMode($includeDevelopmentDependencies);
+            
            $installer->setDumpAutoloader(false);
            /**
             * @psalm-suppress DeprecatedMethod we will keep using the deprecated API until the next major release


20) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:43    [M] FalseValue

--- Original
+++ New
@@ @@
            $installer = ($this->makeComposerInstaller)($installationPath);
            // Some defaults needed for this specific implementation:
            $installer->setDevMode($includeDevelopmentDependencies);
-            $installer->setDumpAutoloader(false);
+            $installer->setDumpAutoloader(true);
            /**
             * @psalm-suppress DeprecatedMethod we will keep using the deprecated API until the next major release
             *                 of composer, as we otherwise need to re-design how an {@see Installer} is constructed.


21) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:43    [M] MethodCallRemoval

--- Original
+++ New
@@ @@
            $installer = ($this->makeComposerInstaller)($installationPath);
            // Some defaults needed for this specific implementation:
            $installer->setDevMode($includeDevelopmentDependencies);
-            $installer->setDumpAutoloader(false);
+            
            /**
             * @psalm-suppress DeprecatedMethod we will keep using the deprecated API until the next major release
             *                 of composer, as we otherwise need to re-design how an {@see Installer} is constructed.


22) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:48    [M] FalseValue

--- Original
+++ New
@@ @@
             * @psalm-suppress DeprecatedMethod we will keep using the deprecated API until the next major release
             *                 of composer, as we otherwise need to re-design how an {@see Installer} is constructed.
             */
-            $installer->setRunScripts(false);
+            $installer->setRunScripts(true);
            $installer->setPlatformRequirementFilter(new IgnoreAllPlatformRequirementFilter());
            $installer->run();
        }, $installationPath);


23) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:48    [M] MethodCallRemoval

--- Original
+++ New
@@ @@
            // Some defaults needed for this specific implementation:
            $installer->setDevMode($includeDevelopmentDependencies);
            $installer->setDumpAutoloader(false);
-            /**
-             * @psalm-suppress DeprecatedMethod we will keep using the deprecated API until the next major release
-             *                 of composer, as we otherwise need to re-design how an {@see Installer} is constructed.
-             */
-            $installer->setRunScripts(false);
+            
            $installer->setPlatformRequirementFilter(new IgnoreAllPlatformRequirementFilter());
            $installer->run();
        }, $installationPath);


24) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:49    [M] MethodCallRemoval

--- Original
+++ New
@@ @@
             *                 of composer, as we otherwise need to re-design how an {@see Installer} is constructed.
             */
            $installer->setRunScripts(false);
-            $installer->setPlatformRequirementFilter(new IgnoreAllPlatformRequirementFilter());
+            
            $installer->run();
        }, $installationPath);
        $astLocator = new ReplaceSourcePathOfLocatedSources($this->astLocator, $installationPath);


25) /github/workspace/src/LocateDependencies/LocateDependenciesViaComposer.php:51    [M] MethodCallRemoval

--- Original
+++ New
@@ @@
             */
            $installer->setRunScripts(false);
            $installer->setPlatformRequirementFilter(new IgnoreAllPlatformRequirementFilter());
-            $installer->run();
+            
        }, $installationPath);
        $astLocator = new ReplaceSourcePathOfLocatedSources($this->astLocator, $installationPath);
        return new AggregateSourceLocator([new PhpInternalSourceLocator($astLocator, new ReflectionSourceStubber()), (new MakeLocatorForInstalledJson())($installationPath, $astLocator)]);
Warning:  Dashboard report has not been sent: The current process is a pull request build

Time: 1m 28s. Memory: 0.17GB
Error: ] The minimum required MSI percentage should be 100%, but actual is      
        93.87%. Improve your tests!                                            

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions