diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md index 1cb07da782b6..39ae3a881b3b 100644 --- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md +++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.4.0 +----- + + * added a `CoverageListener` to enhance the code coverage report + 3.3.0 ----- diff --git a/src/Symfony/Bridge/PhpUnit/CoverageListener.php b/src/Symfony/Bridge/PhpUnit/CoverageListener.php new file mode 100644 index 000000000000..e6b4e7ec98b8 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/CoverageListener.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use PHPUnit\Framework\BaseTestListener; +use PHPUnit\Framework\Test; +use Symfony\Bridge\PhpUnit\Legacy\CoverageListenerTrait; + +if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { + class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListener', 'Symfony\Bridge\PhpUnit\CoverageListener'); +// Using an early return instead of a else does not work when using the PHPUnit +// phar due to some weird PHP behavior (the class gets defined without executing +// the code before it and so the definition is not properly conditional) +} else { + /** + * CoverageListener adds `@covers ` on each test suite when possible + * to make the code coverage more accurate. + * + * @author Grégoire Pineau + */ + class CoverageListener extends BaseTestListener + { + private $trait; + + public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) + { + $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound); + } + + public function startTest(Test $test) + { + $this->trait->startTest($test); + } + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListener.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListener.php new file mode 100644 index 000000000000..022782851576 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListener.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +/** + * CoverageListener adds `@covers ` on each test suite when possible + * to make the code coverage more accurate. + * + * @author Grégoire Pineau + * + * @internal + */ +class CoverageListener extends \PHPUnit_Framework_BaseTestListener +{ + private $trait; + + public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) + { + $this->trait = new CoverageListenerTrait($sutFqcnResolver, $warningOnSutNotFound); + } + + public function startTest(\PHPUnit_Framework_Test $test) + { + $this->trait->startTest($test); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php new file mode 100644 index 000000000000..0a1603e646cb --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +use PHPUnit\Framework\Test; +use PHPUnit\Framework\Warning; + +/** + * PHP 5.3 compatible trait-like shared implementation. + * + * @author Grégoire Pineau + * + * @internal + */ +class CoverageListenerTrait +{ + private $sutFqcnResolver; + private $warningOnSutNotFound; + private $warnings; + + public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) + { + $this->sutFqcnResolver = $sutFqcnResolver; + $this->warningOnSutNotFound = $warningOnSutNotFound; + $this->warnings = array(); + } + + public function startTest($test) + { + $annotations = $test->getAnnotations(); + + $ignoredAnnotations = array('covers', 'coversDefaultClass', 'coversNothing'); + + foreach ($ignoredAnnotations as $annotation) { + if (isset($annotations['class'][$annotation]) || isset($annotations['method'][$annotation])) { + return; + } + } + + $sutFqcn = $this->findSutFqcn($test); + if (!$sutFqcn) { + if ($this->warningOnSutNotFound) { + $message = 'Could not find the tested class.'; + // addWarning does not exist on old PHPUnit version + if (method_exists($test->getTestResultObject(), 'addWarning') && class_exists(Warning::class)) { + $test->getTestResultObject()->addWarning($test, new Warning($message), 0); + } else { + $this->warnings[] = sprintf("%s::%s\n%s", get_class($test), $test->getName(), $message); + } + } + + return; + } + + $testClass = \PHPUnit\Util\Test::class; + if (!class_exists($testClass, false)) { + $testClass = \PHPUnit_Util_Test::class; + } + + $r = new \ReflectionProperty($testClass, 'annotationCache'); + $r->setAccessible(true); + + $cache = $r->getValue(); + $cache = array_replace_recursive($cache, array( + get_class($test) => array( + 'covers' => array($sutFqcn), + ), + )); + $r->setValue($testClass, $cache); + } + + private function findSutFqcn($test) + { + if ($this->sutFqcnResolver) { + $resolver = $this->sutFqcnResolver; + + return $resolver($test); + } + + $class = get_class($test); + + $sutFqcn = str_replace('\\Tests\\', '\\', $class); + $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); + + if (!class_exists($sutFqcn)) { + return; + } + + return $sutFqcn; + } + + public function __destruct() + { + if (!$this->warnings) { + return; + } + + echo "\n"; + + foreach ($this->warnings as $key => $warning) { + echo sprintf("%d) %s\n", ++$key, $warning); + } + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php new file mode 100644 index 000000000000..216b860a4bc2 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php @@ -0,0 +1,35 @@ +markTestSkipped('This test cannot be run on Windows.'); + } + + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('This test cannot be run on HHVM.'); + } + + $dir = __DIR__.'/../Tests/Fixtures/coverage'; + $php = PHP_BINARY; + $phpunit = $_SERVER['argv'][0]; + + exec("$php -d zend_extension=xdebug.so $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text", $output); + $output = implode("\n", $output); + $this->assertContains('Foo', $output); + + exec("$php -d zend_extension=xdebug.so $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text", $output); + $output = implode("\n", $output); + $this->assertNotContains('Foo', $output); + $this->assertContains("SutNotFoundTest::test\nCould not find the tested class.", $output); + $this->assertNotContains("CoversTest::test\nCould not find the tested class.", $output); + $this->assertNotContains("CoversDefaultClassTest::test\nCould not find the tested class.", $output); + $this->assertNotContains("CoversNothingTest::test\nCould not find the tested class.", $output); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist new file mode 100644 index 000000000000..1984359ebdd7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-with-listener.xml.dist @@ -0,0 +1,32 @@ + + + + + + + tests + + + + + + src + + + + + + + + true + + + + diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist new file mode 100644 index 000000000000..620153593376 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/phpunit-without-listener.xml.dist @@ -0,0 +1,23 @@ + + + + + + + tests + + + + + + src + + + diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Bar.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Bar.php new file mode 100644 index 000000000000..b50305a40263 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Bar.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpUnitCoverageTest; + +class Bar +{ + private $foo; + + public function __construct(Foo $foo) + { + $this->foo = $foo; + } + + public function barZ() + { + $this->foo->fooZ(); + + return 'bar'; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Foo.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Foo.php new file mode 100644 index 000000000000..f811ae70b10a --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/src/Foo.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpUnitCoverageTest; + +class Foo +{ + public function fooZ() + { + return 'foo'; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarTest.php new file mode 100644 index 000000000000..b49fc706a9cf --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpUnitCoverageTest\Tests; + +use PHPUnit\Framework\TestCase; + +class BarTest extends TestCase +{ + public function testBar() + { + if (!class_exists('PhpUnitCoverageTest\Foo')) { + $this->markTestSkipped('This test is not part of the main Symfony test suite. It\'s here to test the CoverageListener.'); + } + + $foo = new \PhpUnitCoverageTest\Foo(); + $bar = new \PhpUnitCoverageTest\Bar($foo); + + $this->assertSame('bar', $bar->barZ()); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php new file mode 100644 index 000000000000..d764638d0495 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversDefaultClassTest.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PHPUnit\Framework\TestCase; + +/** + * @coversDefaultClass \DateTime + */ +class CoversDefaultClassTest extends TestCase +{ + public function test() + { + $this->assertTrue(true); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversNothingTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversNothingTest.php new file mode 100644 index 000000000000..e60ea97e57bb --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversNothingTest.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + */ +class CoversNothingTest extends TestCase +{ + public function test() + { + $this->assertTrue(true); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversTest.php new file mode 100644 index 000000000000..f6d3406046d8 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/CoversTest.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PHPUnit\Framework\TestCase; + +class CoversTest extends TestCase +{ + /** + * @covers \DateTime + */ + public function test() + { + $this->assertTrue(true); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFindTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFindTest.php new file mode 100644 index 000000000000..934ee77dc187 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFindTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use PHPUnit\Framework\TestCase; + +class SutNotFoundTest extends TestCase +{ + public function test() + { + $this->assertTrue(true); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php new file mode 100644 index 000000000000..9647a8658d21 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +require __DIR__.'/../src/Bar.php'; +require __DIR__.'/../src/Foo.php'; + +require __DIR__.'/../../../../Legacy/CoverageListenerTrait.php'; +if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { + require __DIR__.'/../../../../Legacy/CoverageListener.php'; +} +require __DIR__.'/../../../../CoverageListener.php';