From 2745ac0679334c1346512afbcda5a1a569b83feb Mon Sep 17 00:00:00 2001 From: Sebastian Bergmann Date: Wed, 23 Sep 2020 13:58:25 +0200 Subject: [PATCH] Closes #4464 --- .psalm/baseline.xml | 4 +- ChangeLog-9.4.md | 4 ++ src/Framework/TestSuite.php | 15 +++++- src/TextUI/CliArguments/Builder.php | 17 +++++++ src/TextUI/CliArguments/Configuration.php | 48 ++++++++++++++++++- src/TextUI/CliArguments/Mapper.php | 8 ++++ src/TextUI/Command.php | 5 ++ src/TextUI/Help.php | 8 ++-- src/TextUI/TestRunner.php | 29 ++++++++++- src/Util/Test.php | 21 ++++++++ src/Util/TestDox/XmlResultPrinter.php | 3 +- .../cli/_files/output-cli-help-color.txt | 10 ++-- .../cli/_files/output-cli-usage.txt | 8 ++-- .../covers-annotation-based-filter.phpt | 23 +++++++++ .../phpunit.xml | 9 ++++ .../src/AnnotationFilter.php | 14 ++++++ .../tests/AnnotationFilterTest.php | 31 ++++++++++++ .../uses-annotation-based-filter.phpt | 23 +++++++++ 18 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 tests/end-to-end/coverage-annotation-based-filter/covers-annotation-based-filter.phpt create mode 100644 tests/end-to-end/coverage-annotation-based-filter/phpunit.xml create mode 100644 tests/end-to-end/coverage-annotation-based-filter/src/AnnotationFilter.php create mode 100644 tests/end-to-end/coverage-annotation-based-filter/tests/AnnotationFilterTest.php create mode 100644 tests/end-to-end/coverage-annotation-based-filter/uses-annotation-based-filter.phpt diff --git a/.psalm/baseline.xml b/.psalm/baseline.xml index 1e8a176f62b..898cfbfbdae 100644 --- a/.psalm/baseline.xml +++ b/.psalm/baseline.xml @@ -1666,7 +1666,9 @@ - + + addFilter + addFilter addFilter addFilter addFilter diff --git a/ChangeLog-9.4.md b/ChangeLog-9.4.md index 0d18fc3762f..452e5840dee 100644 --- a/ChangeLog-9.4.md +++ b/ChangeLog-9.4.md @@ -4,6 +4,10 @@ All notable changes of the PHPUnit 9.4 release series are documented in this fil ## [9.4.0] - 2020-10-02 +### Added + +* [#4464](https://github.com/sebastianbergmann/phpunit/issues/4464): Filter based on covered (`@covers`) / used (`@uses`) units of code + ### Changed * The PHPUnit XML configuration generator (that is invoked using the `--generate-configuration` CLI option) now asks for a cache directory (default: `.phpunit.cache`) diff --git a/src/Framework/TestSuite.php b/src/Framework/TestSuite.php index 36bc79c056d..3ddccc3a3e8 100644 --- a/src/Framework/TestSuite.php +++ b/src/Framework/TestSuite.php @@ -280,8 +280,8 @@ public function addTest(Test $test, $groups = []): void $groups = $test->getGroups(); } - if (empty($groups)) { - $groups = ['default']; + if ($this->containsOnlyVirtualGroups($groups)) { + $groups[] = 'default'; } foreach ($groups as $group) { @@ -906,4 +906,15 @@ private function clearCaches(): void $this->providedTests = null; $this->requiredTests = null; } + + private function containsOnlyVirtualGroups(array $groups): bool + { + foreach ($groups as $group) { + if (strpos($group, '__phpunit_') !== 0) { + return false; + } + } + + return true; + } } diff --git a/src/TextUI/CliArguments/Builder.php b/src/TextUI/CliArguments/Builder.php index f7f4afd7d22..4321fc25583 100644 --- a/src/TextUI/CliArguments/Builder.php +++ b/src/TextUI/CliArguments/Builder.php @@ -9,6 +9,7 @@ */ namespace PHPUnit\TextUI\CliArguments; +use function array_map; use function array_merge; use function class_exists; use function explode; @@ -60,6 +61,8 @@ final class Builder 'generate-configuration', 'globals-backup', 'group=', + 'covers=', + 'uses=', 'help', 'resolve-dependencies', 'ignore-dependencies', @@ -179,6 +182,8 @@ public function fromParameters(array $parameters, array $additionalLongOptions): $generateConfiguration = null; $migrateConfiguration = null; $groups = null; + $testsCovering = null; + $testsUsing = null; $help = null; $includePath = null; $iniSettings = []; @@ -374,6 +379,16 @@ public function fromParameters(array $parameters, array $additionalLongOptions): break; + case '--covers': + $testsCovering = array_map('strtolower', explode(',', $option[1])); + + break; + + case '--uses': + $testsUsing = array_map('strtolower', explode(',', $option[1])); + + break; + case '--test-suffix': $testSuffixes = explode(',', $option[1]); @@ -811,6 +826,8 @@ public function fromParameters(array $parameters, array $additionalLongOptions): $generateConfiguration, $migrateConfiguration, $groups, + $testsCovering, + $testsUsing, $help, $includePath, $iniSettings, diff --git a/src/TextUI/CliArguments/Configuration.php b/src/TextUI/CliArguments/Configuration.php index d51e17b764b..bef40ac4a59 100644 --- a/src/TextUI/CliArguments/Configuration.php +++ b/src/TextUI/CliArguments/Configuration.php @@ -242,6 +242,16 @@ final class Configuration */ private $groups; + /** + * @var null|string[] + */ + private $testsCovering; + + /** + * @var null|string[] + */ + private $testsUsing; + /** * @var ?bool */ @@ -460,7 +470,7 @@ final class Configuration /** * @param null|int|string $columns */ - public function __construct(?string $argument, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticAttributes, ?bool $beStrictAboutChangesToGlobalState, ?bool $beStrictAboutResourceUsageDuringSmallTests, ?string $bootstrap, ?bool $cacheResult, ?string $cacheResultFile, ?bool $checkVersion, ?string $colors, $columns, ?string $configuration, ?string $coverageClover, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, ?bool $warmCoverageCache, ?bool $debug, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $disallowTodoAnnotatedTests, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?array $extensions, ?array $unavailableExtensions, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?string $filter, ?bool $generateConfiguration, ?bool $migrateConfiguration, ?array $groups, ?bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, ?bool $listGroups, ?bool $listSuites, ?bool $listTests, ?string $listTestsXml, ?string $loader, ?bool $noCoverage, ?bool $noExtensions, ?bool $noInteraction, ?bool $noLogging, ?string $printer, ?bool $processIsolation, ?int $randomOrderSeed, ?int $repeat, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?bool $stopOnDefect, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $teamcityLogfile, ?array $testdoxExcludeGroups, ?array $testdoxGroups, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?string $testdoxXmlFile, ?array $testSuffixes, ?string $testSuite, array $unrecognizedOptions, ?string $unrecognizedOrderBy, ?bool $useDefaultConfiguration, ?bool $verbose, ?bool $version, ?array $coverageFilter, ?string $xdebugFilterFile) + public function __construct(?string $argument, ?string $atLeastVersion, ?bool $backupGlobals, ?bool $backupStaticAttributes, ?bool $beStrictAboutChangesToGlobalState, ?bool $beStrictAboutResourceUsageDuringSmallTests, ?string $bootstrap, ?bool $cacheResult, ?string $cacheResultFile, ?bool $checkVersion, ?string $colors, $columns, ?string $configuration, ?string $coverageClover, ?string $coverageCrap4J, ?string $coverageHtml, ?string $coveragePhp, ?string $coverageText, ?bool $coverageTextShowUncoveredFiles, ?bool $coverageTextShowOnlySummary, ?string $coverageXml, ?bool $pathCoverage, ?string $coverageCacheDirectory, ?bool $warmCoverageCache, ?bool $debug, ?int $defaultTimeLimit, ?bool $disableCodeCoverageIgnore, ?bool $disallowTestOutput, ?bool $disallowTodoAnnotatedTests, ?bool $enforceTimeLimit, ?array $excludeGroups, ?int $executionOrder, ?int $executionOrderDefects, ?array $extensions, ?array $unavailableExtensions, ?bool $failOnEmptyTestSuite, ?bool $failOnIncomplete, ?bool $failOnRisky, ?bool $failOnSkipped, ?bool $failOnWarning, ?string $filter, ?bool $generateConfiguration, ?bool $migrateConfiguration, ?array $groups, ?array $testsCovering, ?array $testsUsing, ?bool $help, ?string $includePath, ?array $iniSettings, ?string $junitLogfile, ?bool $listGroups, ?bool $listSuites, ?bool $listTests, ?string $listTestsXml, ?string $loader, ?bool $noCoverage, ?bool $noExtensions, ?bool $noInteraction, ?bool $noLogging, ?string $printer, ?bool $processIsolation, ?int $randomOrderSeed, ?int $repeat, ?bool $reportUselessTests, ?bool $resolveDependencies, ?bool $reverseList, ?bool $stderr, ?bool $strictCoverage, ?bool $stopOnDefect, ?bool $stopOnError, ?bool $stopOnFailure, ?bool $stopOnIncomplete, ?bool $stopOnRisky, ?bool $stopOnSkipped, ?bool $stopOnWarning, ?string $teamcityLogfile, ?array $testdoxExcludeGroups, ?array $testdoxGroups, ?string $testdoxHtmlFile, ?string $testdoxTextFile, ?string $testdoxXmlFile, ?array $testSuffixes, ?string $testSuite, array $unrecognizedOptions, ?string $unrecognizedOrderBy, ?bool $useDefaultConfiguration, ?bool $verbose, ?bool $version, ?array $coverageFilter, ?string $xdebugFilterFile) { $this->argument = $argument; $this->atLeastVersion = $atLeastVersion; @@ -507,6 +517,8 @@ public function __construct(?string $argument, ?string $atLeastVersion, ?bool $b $this->generateConfiguration = $generateConfiguration; $this->migrateConfiguration = $migrateConfiguration; $this->groups = $groups; + $this->testsCovering = $testsCovering; + $this->testsUsing = $testsUsing; $this->help = $help; $this->includePath = $includePath; $this->iniSettings = $iniSettings; @@ -1283,6 +1295,40 @@ public function groups(): array return $this->groups; } + public function hasTestsCovering(): bool + { + return $this->testsCovering !== null; + } + + /** + * @throws Exception + */ + public function testsCovering(): array + { + if ($this->testsCovering === null) { + throw new Exception; + } + + return $this->testsCovering; + } + + public function hasTestsUsing(): bool + { + return $this->testsUsing !== null; + } + + /** + * @throws Exception + */ + public function testsUsing(): array + { + if ($this->testsUsing === null) { + throw new Exception; + } + + return $this->testsUsing; + } + public function hasHelp(): bool { return $this->help !== null; diff --git a/src/TextUI/CliArguments/Mapper.php b/src/TextUI/CliArguments/Mapper.php index cda3e1f633f..468adbed0ab 100644 --- a/src/TextUI/CliArguments/Mapper.php +++ b/src/TextUI/CliArguments/Mapper.php @@ -124,6 +124,14 @@ public function mapToLegacyArray(Configuration $arguments): array $result['excludeGroups'] = $arguments->excludeGroups(); } + if ($arguments->hasTestsCovering()) { + $result['testsCovering'] = $arguments->testsCovering(); + } + + if ($arguments->hasTestsUsing()) { + $result['testsUsing'] = $arguments->testsUsing(); + } + if ($arguments->hasTestSuffixes()) { $result['testSuffixes'] = $arguments->testSuffixes(); } diff --git a/src/TextUI/Command.php b/src/TextUI/Command.php index 82054ec9420..41d3d32eab9 100644 --- a/src/TextUI/Command.php +++ b/src/TextUI/Command.php @@ -32,6 +32,7 @@ use function sort; use function sprintf; use function stream_resolve_include_path; +use function strpos; use function trim; use function version_compare; use PharIo\Manifest\ApplicationName; @@ -651,6 +652,10 @@ private function handleListGroups(TestSuite $suite, bool $exit): int sort($groups); foreach ($groups as $group) { + if (strpos($group, '__phpunit_') === 0) { + continue; + } + printf( ' - %s' . PHP_EOL, $group diff --git a/src/TextUI/Help.php b/src/TextUI/Help.php index c4e79849253..ea5ebc9ae2a 100644 --- a/src/TextUI/Help.php +++ b/src/TextUI/Help.php @@ -60,14 +60,16 @@ final class Help ], 'Test Selection Options' => [ - ['arg' => '--filter ', 'desc' => 'Filter which tests to run'], + ['arg' => '--list-suites', 'desc' => 'List available test suites'], ['arg' => '--testsuite ', 'desc' => 'Filter which testsuite to run'], + ['arg' => '--list-groups', 'desc' => 'List available test groups'], ['arg' => '--group ', 'desc' => 'Only runs tests from the specified group(s)'], ['arg' => '--exclude-group ', 'desc' => 'Exclude tests from the specified group(s)'], - ['arg' => '--list-groups', 'desc' => 'List available test groups'], - ['arg' => '--list-suites', 'desc' => 'List available test suites'], + ['arg' => '--covers ', 'desc' => 'Only runs tests annotated with "@covers "'], + ['arg' => '--uses ', 'desc' => 'Only runs tests annotated with "@uses "'], ['arg' => '--list-tests', 'desc' => 'List available tests'], ['arg' => '--list-tests-xml ', 'desc' => 'List available tests in XML format'], + ['arg' => '--filter ', 'desc' => 'Filter which tests to run'], ['arg' => '--test-suffix ', 'desc' => 'Only search for test in files with specified suffix(es). Default: Test.php,.phpt'], ], diff --git a/src/TextUI/TestRunner.php b/src/TextUI/TestRunner.php index 6813468eb84..0479e64e986 100644 --- a/src/TextUI/TestRunner.php +++ b/src/TextUI/TestRunner.php @@ -13,6 +13,7 @@ use const PHP_SAPI; use const PHP_VERSION; use function array_diff; +use function array_map; use function assert; use function class_exists; use function count; @@ -1118,7 +1119,9 @@ private function processSuiteFilters(TestSuite $suite, array $arguments): void { if (!$arguments['filter'] && empty($arguments['groups']) && - empty($arguments['excludeGroups'])) { + empty($arguments['excludeGroups']) && + empty($arguments['testsCovering']) && + empty($arguments['testsUsing'])) { return; } @@ -1138,6 +1141,30 @@ private function processSuiteFilters(TestSuite $suite, array $arguments): void ); } + if (!empty($arguments['testsCovering'])) { + $filterFactory->addFilter( + new ReflectionClass(IncludeGroupFilterIterator::class), + array_map( + static function (string $name): string { + return '__phpunit_covers_' . $name; + }, + $arguments['testsCovering'] + ) + ); + } + + if (!empty($arguments['testsUsing'])) { + $filterFactory->addFilter( + new ReflectionClass(IncludeGroupFilterIterator::class), + array_map( + static function (string $name): string { + return '__phpunit_uses_' . $name; + }, + $arguments['testsUsing'] + ) + ); + } + if ($arguments['filter']) { $filterFactory->addFilter( new ReflectionClass(NameFilterIterator::class), diff --git a/src/Util/Test.php b/src/Util/Test.php index b3de7279724..d86ffb64a34 100644 --- a/src/Util/Test.php +++ b/src/Util/Test.php @@ -34,6 +34,8 @@ use function sprintf; use function strncmp; use function strpos; +use function strtolower; +use function trim; use function version_compare; use PHPUnit\Framework\Assert; use PHPUnit\Framework\CodeCoverageException; @@ -439,6 +441,20 @@ public static function getGroups(string $className, ?string $methodName = ''): a } } + foreach (['method', 'class'] as $element) { + if (isset($annotations[$element]['covers'])) { + foreach ($annotations[$element]['covers'] as $coversTarget) { + $groups[] = ['__phpunit_covers_' . self::canonicalizeName($coversTarget)]; + } + } + + if (isset($annotations[$element]['uses'])) { + foreach ($annotations[$element]['uses'] as $usesTarget) { + $groups[] = ['__phpunit_uses_' . self::canonicalizeName($usesTarget)]; + } + } + } + return array_unique(array_merge([], ...$groups)); } @@ -753,4 +769,9 @@ private static function mergeArraysRecursively(array $a, array $b): array return $a; } + + private static function canonicalizeName(string $name): string + { + return strtolower(trim($name, '\\')); + } } diff --git a/src/Util/TestDox/XmlResultPrinter.php b/src/Util/TestDox/XmlResultPrinter.php index 7a8d7d76920..1514ec1407a 100644 --- a/src/Util/TestDox/XmlResultPrinter.php +++ b/src/Util/TestDox/XmlResultPrinter.php @@ -12,6 +12,7 @@ use function array_filter; use function get_class; use function implode; +use function strpos; use DOMDocument; use DOMElement; use PHPUnit\Framework\AssertionFailedError; @@ -159,7 +160,7 @@ public function endTest(Test $test, float $time): void $groups = array_filter( $test->getGroups(), static function ($group) { - return !($group === 'small' || $group === 'medium' || $group === 'large'); + return !($group === 'small' || $group === 'medium' || $group === 'large' || strpos($group, '__phpunit_') === 0); } ); diff --git a/tests/end-to-end/cli/_files/output-cli-help-color.txt b/tests/end-to-end/cli/_files/output-cli-help-color.txt index 17d4ac38965..ec5323adf5a 100644 --- a/tests/end-to-end/cli/_files/output-cli-help-color.txt +++ b/tests/end-to-end/cli/_files/output-cli-help-color.txt @@ -37,15 +37,19 @@ --no-logging  Ignore logging configuration Test Selection Options: - --filter   Filter which tests to run + --list-suites  List available test suites --testsuite   Filter which testsuite to run + --list-groups  List available test groups --group   Only runs tests from the specified group(s) --exclude-group   Exclude tests from the specified group(s) - --list-groups  List available test groups - --list-suites  List available test suites + --covers   Only runs tests annotated with "@covers + " + --uses   Only runs tests annotated with "@uses + " --list-tests  List available tests --list-tests-xml   List available tests in XML format + --filter   Filter which tests to run --test-suffix   Only search for test in files with specified suffix(es). Default: Test.php,.phpt diff --git a/tests/end-to-end/cli/_files/output-cli-usage.txt b/tests/end-to-end/cli/_files/output-cli-usage.txt index 8d6b90a2478..498a4eadc74 100644 --- a/tests/end-to-end/cli/_files/output-cli-usage.txt +++ b/tests/end-to-end/cli/_files/output-cli-usage.txt @@ -31,14 +31,16 @@ Logging Options: Test Selection Options: - --filter Filter which tests to run + --list-suites List available test suites --testsuite Filter which testsuite to run + --list-groups List available test groups --group Only runs tests from the specified group(s) --exclude-group Exclude tests from the specified group(s) - --list-groups List available test groups - --list-suites List available test suites + --covers Only runs tests annotated with "@covers " + --uses Only runs tests annotated with "@uses " --list-tests List available tests --list-tests-xml List available tests in XML format + --filter Filter which tests to run --test-suffix Only search for test in files with specified suffix(es). Default: Test.php,.phpt Test Execution Options: diff --git a/tests/end-to-end/coverage-annotation-based-filter/covers-annotation-based-filter.phpt b/tests/end-to-end/coverage-annotation-based-filter/covers-annotation-based-filter.phpt new file mode 100644 index 00000000000..9f8cb84e7c6 --- /dev/null +++ b/tests/end-to-end/coverage-annotation-based-filter/covers-annotation-based-filter.phpt @@ -0,0 +1,23 @@ +--TEST-- +phpunit --covers 'PHPUnit\TestFixture\AnnotationFilter' +--FILE-- + + + + + tests + + + diff --git a/tests/end-to-end/coverage-annotation-based-filter/src/AnnotationFilter.php b/tests/end-to-end/coverage-annotation-based-filter/src/AnnotationFilter.php new file mode 100644 index 00000000000..43c4c0f776d --- /dev/null +++ b/tests/end-to-end/coverage-annotation-based-filter/src/AnnotationFilter.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture; + +final class AnnotationFilter +{ +} diff --git a/tests/end-to-end/coverage-annotation-based-filter/tests/AnnotationFilterTest.php b/tests/end-to-end/coverage-annotation-based-filter/tests/AnnotationFilterTest.php new file mode 100644 index 00000000000..7125d2c921e --- /dev/null +++ b/tests/end-to-end/coverage-annotation-based-filter/tests/AnnotationFilterTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture; + +use PHPUnit\Framework\TestCase; + +final class AnnotationFilterTest extends TestCase +{ + /** + * @covers \PHPUnit\TestFixture\AnnotationFilter + */ + public function testOne(): void + { + $this->assertTrue(true); + } + + /** + * @uses \PHPUnit\TestFixture\AnnotationFilter + */ + public function testTwo(): void + { + $this->assertTrue(true); + } +} diff --git a/tests/end-to-end/coverage-annotation-based-filter/uses-annotation-based-filter.phpt b/tests/end-to-end/coverage-annotation-based-filter/uses-annotation-based-filter.phpt new file mode 100644 index 00000000000..15b84fa34a4 --- /dev/null +++ b/tests/end-to-end/coverage-annotation-based-filter/uses-annotation-based-filter.phpt @@ -0,0 +1,23 @@ +--TEST-- +phpunit --covers 'PHPUnit\TestFixture\AnnotationFilter' +--FILE-- +