diff options
19 files changed, 968 insertions, 117 deletions
diff --git a/.gitlab-ci/pipeline.yml b/.gitlab-ci/pipeline.yml index c311b1a03a6..06f5c83acbc 100644 --- a/.gitlab-ci/pipeline.yml +++ b/.gitlab-ci/pipeline.yml @@ -104,7 +104,7 @@ variables: script: - sudo -u www-data -E -H composer run-script drupal-phpunit-upgrade-check # Need to pass this along directly. - - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html --all --ci-parallel-node-index $CI_PARALLEL_NODE_INDEX --ci-parallel-node-total $CI_PARALLEL_NODE_TOTAL + - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --debug-discovery --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html --all --ci-parallel-node-index $CI_PARALLEL_NODE_INDEX --ci-parallel-node-total $CI_PARALLEL_NODE_TOTAL .run-repeat-class-test: &run-repeat-class-test script: @@ -234,7 +234,7 @@ variables: script: - sudo -u www-data -E -H composer run-script drupal-phpunit-upgrade-check # Run a small subset of tests to prove non W3C testing still works. - - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html javascript + - sudo -u www-data -E -H php ./core/scripts/run-tests.sh --debug-discovery --color --keep-results --types "$TESTSUITE" --concurrency "$CONCURRENCY" --repeat "1" --sqlite "./sites/default/files/tests.sqlite" --dburl $SIMPLETEST_DB --url $SIMPLETEST_BASE_URL --verbose --non-html javascript after_script: - sed -i "s#$CI_PROJECT_DIR/##" ./sites/default/files/simpletest/phpunit-*.xml || true diff --git a/core/.phpunit-next.xml b/core/.phpunit-next.xml new file mode 100644 index 00000000000..944ec9dfadd --- /dev/null +++ b/core/.phpunit-next.xml @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="UTF-8"?> + + +<!-- This is a near-copy of phpunit.xml.dist, used to test with upcoming + PHPUnit versions. + + Current differences: + * for PHPUnit 11: removed duplicate directories from <testsuites>, + required in PHPUnit 10, but that cause duplication warnings in + PHPUnit 11. +--> + + +<!-- For how to customize PHPUnit configuration, see core/tests/README.md. --> +<!-- TODO set checkForUnintentionallyCoveredCode="true" once https://www.drupal.org/node/2626832 is resolved. --> +<!-- PHPUnit expects functional tests to be run with either a privileged user + or your current system user. See core/tests/README.md and + https://www.drupal.org/node/2116263 for details. +--> +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + bootstrap="tests/bootstrap.php" + colors="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutChangesToGlobalState="true" + failOnRisky="true" + failOnWarning="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + cacheResult="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" + cacheDirectory=".phpunit.cache"> + <php> + <!-- Set error reporting to E_ALL. --> + <ini name="error_reporting" value="32767"/> + <!-- Do not limit the amount of memory tests take to run. --> + <ini name="memory_limit" value="-1"/> + <!-- Example SIMPLETEST_BASE_URL value: http://localhost --> + <env name="SIMPLETEST_BASE_URL" value=""/> + <!-- Example SIMPLETEST_DB value: mysql://username:password@localhost/database_name#table_prefix --> + <env name="SIMPLETEST_DB" value=""/> + <!-- By default, browser tests will output links that use the base URL set + in SIMPLETEST_BASE_URL. However, if your SIMPLETEST_BASE_URL is an internal + path (such as may be the case in a virtual or Docker-based environment), + you can set the base URL used in the browser test output links to something + reachable from your host machine here. This will allow you to follow them + directly and view the output. --> + <env name="BROWSERTEST_OUTPUT_BASE_URL" value=""/> + <!-- The environment variable SYMFONY_DEPRECATIONS_HELPER is used to configure + the behavior of the deprecation tests. + Drupal core's testing framework is setting this variable to its defaults. + Projects with their own requirements need to manage this variable + explicitly. + --> + <!-- To disable deprecation testing completely uncomment the next line. --> + <!-- <env name="SYMFONY_DEPRECATIONS_HELPER" value="disabled"/> --> + <!-- Deprecation errors can be selectively ignored by specifying a file of + regular expression patterns for exclusion. + Uncomment the line below to specify a custom deprecations ignore file. + NOTE: it may be required to specify the full path to the file to run tests + correctly. + --> + <!-- <env name="SYMFONY_DEPRECATIONS_HELPER" value="ignoreFile=.deprecation-ignore.txt"/> --> + <!-- Example for changing the driver class for mink tests MINK_DRIVER_CLASS value: 'Drupal\FunctionalJavascriptTests\DrupalSelenium2Driver' --> + <env name="MINK_DRIVER_CLASS" value=""/> + <!-- Example for changing the driver args to mink tests MINK_DRIVER_ARGS value: '["http://127.0.0.1:8510"]' --> + <env name="MINK_DRIVER_ARGS" value=""/> + <!-- Example for changing the driver args to webdriver tests MINK_DRIVER_ARGS_WEBDRIVER value: '["chrome", { "goog:chromeOptions": { "w3c": false } }, "http://localhost:4444/wd/hub"]' For using the Firefox browser, replace "chrome" with "firefox" --> + <env name="MINK_DRIVER_ARGS_WEBDRIVER" value=""/> + </php> + <extensions> + <!-- Functional tests HTML output logging. --> + <bootstrap class="Drupal\TestTools\Extension\HtmlLogging\HtmlOutputLogger"> + <!-- The directory where the browser output will be stored. If a relative + path is specified, it will be relative to the current working directory + of the process running the PHPUnit CLI. In CI environments, this can be + overridden by the value set for the "BROWSERTEST_OUTPUT_DIRECTORY" + environment variable. + --> + <parameter name="outputDirectory" value="sites/simpletest/browser_output"/> + <!-- By default browser tests print the individual links in the test run + report. To avoid overcrowding the output in CI environments, you can + set the "verbose" parameter or the "BROWSERTEST_OUTPUT_VERBOSE" + environment variable to "false". In GitLabCI, the output is saved + anyway as an artifact that can be browsed or downloaded from Gitlab. + --> + <parameter name="verbose" value="true"/> + </bootstrap> + <!-- Debug dump() printer. --> + <bootstrap class="Drupal\TestTools\Extension\Dump\DebugDump"> + <parameter name="colors" value="true"/> + <parameter name="printCaller" value="true"/> + </bootstrap> + </extensions> + <testsuites> + <testsuite name="unit-component"> + <directory>tests/Drupal/Tests/Component</directory> + </testsuite> + <testsuite name="unit"> + <directory>tests/Drupal/Tests</directory> + <exclude>tests/Drupal/Tests/Component</exclude> + <directory>modules/**/tests/src/Unit</directory> + <directory>profiles/**/tests/src/Unit</directory> + <directory>themes/**/tests/src/Unit</directory> + <directory>../modules/**/tests/src/Unit</directory> + <directory>../profiles/**/tests/src/Unit</directory> + <directory>../themes/**/tests/src/Unit</directory> + </testsuite> + <testsuite name="kernel"> + <directory>tests/Drupal/KernelTests</directory> + <directory>modules/**/tests/src/Kernel</directory> + <directory>recipes/*/tests/src/Kernel</directory> + <directory>profiles/**/tests/src/Kernel</directory> + <directory>themes/**/tests/src/Kernel</directory> + <directory>../modules/**/tests/src/Kernel</directory> + <directory>../profiles/**/tests/src/Kernel</directory> + <directory>../themes/**/tests/src/Kernel</directory> + </testsuite> + <testsuite name="functional"> + <directory>tests/Drupal/FunctionalTests</directory> + <directory>modules/**/tests/src/Functional</directory> + <directory>profiles/**/tests/src/Functional</directory> + <directory>recipes/*/tests/src/Functional</directory> + <directory>themes/**/tests/src/Functional</directory> + <directory>../modules/**/tests/src/Functional</directory> + <directory>../profiles/**/tests/src/Functional</directory> + <directory>../themes/**/tests/src/Functional</directory> + </testsuite> + <testsuite name="functional-javascript"> + <directory>tests/Drupal/FunctionalJavascriptTests</directory> + <directory>modules/**/tests/src/FunctionalJavascript</directory> + <directory>recipes/*/tests/src/FunctionalJavascript</directory> + <directory>profiles/**/tests/src/FunctionalJavascript</directory> + <directory>themes/**/tests/src/FunctionalJavascript</directory> + <directory>../modules/**/tests/src/FunctionalJavascript</directory> + <directory>../profiles/**/tests/src/FunctionalJavascript</directory> + <directory>../themes/**/tests/src/FunctionalJavascript</directory> + </testsuite> + <testsuite name="build"> + <directory>tests/Drupal/BuildTests</directory> + <directory>modules/**/tests/src/Build</directory> + </testsuite> + </testsuites> + <!-- Settings for coverage reports. --> + <source ignoreSuppressionOfDeprecations="true"> + <include> + <directory>./includes</directory> + <directory>./lib</directory> + <directory>./modules</directory> + <directory>../modules</directory> + <directory>../sites</directory> + </include> + <exclude> + <directory>./lib/Drupal/Component</directory> + <directory>./modules/*/src/Tests</directory> + <directory>./modules/*/tests</directory> + <directory>../modules/*/src/Tests</directory> + <directory>../modules/*/tests</directory> + <directory>../modules/*/*/src/Tests</directory> + <directory>../modules/*/*/tests</directory> + </exclude> + </source> +</phpunit> diff --git a/core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php b/core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php new file mode 100644 index 00000000000..d9737028821 --- /dev/null +++ b/core/lib/Drupal/Core/Test/PhpUnitTestDiscovery.php @@ -0,0 +1,346 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Test; + +use Drupal\Core\Test\Exception\MissingGroupException; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\DataProviderTestSuite; +use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestSuite; +use PHPUnit\TextUI\Configuration\Builder; +use PHPUnit\TextUI\Configuration\TestSuiteBuilder; + +/** + * Discovers available tests using the PHPUnit API. + * + * @internal + */ +class PhpUnitTestDiscovery { + + /** + * The map of legacy test suite identifiers to phpunit.xml ones. + * + * @var array<string,string> + */ + private array $map = [ + 'PHPUnit-FunctionalJavascript' => 'functional-javascript', + 'PHPUnit-Functional' => 'functional', + 'PHPUnit-Kernel' => 'kernel', + 'PHPUnit-Unit' => 'unit', + 'PHPUnit-Unit-Component' => 'unit-component', + 'PHPUnit-Build' => 'build', + ]; + + /** + * The reverse map of legacy test suite identifiers to phpunit.xml ones. + * + * @var array<string,string> + */ + private array $reverseMap; + + /** + * The warnings generated during the discovery. + * + * @var list<string> + */ + private array $warnings = []; + + public function __construct( + private string $configurationFilePath, + ) { + $this->reverseMap = array_flip($this->map); + } + + /** + * Discovers available tests. + * + * @param string|null $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param list<string> $testSuites + * (optional) An array of PHPUnit test suites to filter the discovery for. + * @param string|null $directory + * (optional) Limit discovered tests to a specific directory. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + public function getTestClasses(?string $extension = NULL, array $testSuites = [], ?string $directory = NULL): array { + $this->warnings = []; + + $args = ['--configuration', $this->configurationFilePath]; + + if (!empty($testSuites)) { + // Convert $testSuites from Drupal's legacy syntax to the syntax used in + // phpunit.xml, that is necessary to PHPUnit to be able to apply the + // test suite filter. For example, 'PHPUnit-Unit' to 'unit'. + $tmp = []; + foreach ($testSuites as $i) { + if (!is_string($i)) { + throw new \InvalidArgumentException("Test suite must be a string"); + } + if (str_contains($i, ' ')) { + throw new \InvalidArgumentException("Test suite name '{$i}' is invalid"); + } + $tmp[] = $this->map[$i] ?? $i; + } + $args[] = '--testsuite=' . implode(',', $tmp); + } + + if ($directory !== NULL) { + $args[] = $directory; + } + + $phpUnitConfiguration = (new Builder())->build($args); + + // TestSuiteBuilder calls the test data providers during the discovery. + // Data providers may be changing the Drupal service container, which leads + // to potential issues. We save the current container before running the + // discovery, and in case a change is detected, reset it and raise + // warnings so that developers can tune their data provider code. + if (\Drupal::hasContainer()) { + $container = \Drupal::getContainer(); + $containerObjectId = spl_object_id($container); + } + $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration); + if (isset($containerObjectId) && $containerObjectId !== spl_object_id(\Drupal::getContainer())) { + $this->warnings[] = '*** The service container was changed during the test discovery ***'; + $this->warnings[] = 'Probably a test data provider method called \\Drupal::setContainer.'; + $this->warnings[] = 'Ensure that all the data providers restore the original container before returning data.'; + assert(isset($container)); + \Drupal::setContainer($container); + } + + $list = $directory === NULL ? + $this->getTestList($phpUnitTestSuite, $extension) : + $this->getTestListLimitedToDirectory($phpUnitTestSuite, $extension, $testSuites); + + // Sort the groups and tests within the groups by name. + uksort($list, 'strnatcasecmp'); + foreach ($list as &$tests) { + uksort($tests, 'strnatcasecmp'); + } + + return $list; + } + + /** + * Discovers all class files in all available extensions. + * + * @param string|null $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param string|null $directory + * (optional) Limit discovered tests to a specific directory. + * + * @return array + * A classmap containing all discovered class files; i.e., a map of + * fully-qualified classnames to path names. + */ + public function findAllClassFiles(?string $extension = NULL, ?string $directory = NULL): array { + $testClasses = $this->getTestClasses($extension, [], $directory); + $classMap = []; + foreach ($testClasses as $group) { + foreach ($group as $className => $info) { + $classMap[$className] = $info['file']; + } + } + return $classMap; + } + + /** + * Returns the warnings generated during the discovery. + * + * @return list<string> + * The warnings. + */ + public function getWarnings(): array { + return $this->warnings; + } + + /** + * Returns a list of tests from a TestSuite object. + * + * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite + * The TestSuite object returned by PHPUnit test discovery. + * @param string|null $extension + * The name of an extension to limit discovery to; e.g., 'node'. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + private function getTestList(TestSuite $phpUnitTestSuite, ?string $extension): array { + $list = []; + foreach ($phpUnitTestSuite->tests() as $testSuite) { + foreach ($testSuite->tests() as $testClass) { + if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) { + continue; + } + + $item = $this->getTestClassInfo( + $testClass, + $this->reverseMap[$testSuite->name()] ?? $testSuite->name(), + ); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + } + } + return $list; + } + + /** + * Returns a list of tests from a TestSuite object limited to a directory. + * + * @param \PHPUnit\Framework\TestSuite $phpUnitTestSuite + * The TestSuite object returned by PHPUnit test discovery. + * @param string|null $extension + * The name of an extension to limit discovery to; e.g., 'node'. + * @param list<string> $testSuites + * An array of PHPUnit test suites to filter the discovery for. + * + * @return array<string<array<class-string, array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int}>>> + * An array of test groups keyed by the group name. Each test group is an + * array of test class information arrays as returned by + * ::getTestClassInfo(), keyed by test class. If a test class belongs to + * multiple groups, it will appear under all group keys it belongs to. + */ + private function getTestListLimitedToDirectory(TestSuite $phpUnitTestSuite, ?string $extension, array $testSuites): array { + $list = []; + + // In this case, PHPUnit found a single test class to run tests for. + if ($phpUnitTestSuite->isForTestClass()) { + if ($extension !== NULL && !str_starts_with($phpUnitTestSuite->name(), "Drupal\\Tests\\{$extension}\\")) { + return []; + } + + // Take the test suite name from the class namespace. + $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($phpUnitTestSuite->name()); + if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) { + return []; + } + + $item = $this->getTestClassInfo($phpUnitTestSuite, $testSuite); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + return $list; + } + + // Multiple test classes were found. + $list = []; + foreach ($phpUnitTestSuite->tests() as $testClass) { + if ($extension !== NULL && !str_starts_with($testClass->name(), "Drupal\\Tests\\{$extension}\\")) { + continue; + } + + // Take the test suite name from the class namespace. + $testSuite = 'PHPUnit-' . TestDiscovery::getPhpunitTestSuite($testClass->name()); + if (!empty($testSuites) && !in_array($testSuite, $testSuites, TRUE)) { + continue; + } + + $item = $this->getTestClassInfo($testClass, $testSuite); + + foreach ($item['groups'] as $group) { + $list[$group][$item['name']] = $item; + } + } + return $list; + + } + + /** + * Returns the test class information. + * + * @param \PHPUnit\Framework\Test $testClass + * The test class. + * @param string $testSuite + * The test suite of this test class. + * + * @return array{name: class-string, description: string, group: string|int, groups: list<string|int>, type: string, file: string, tests_count: positive-int} + * The test class information. + */ + private function getTestClassInfo(Test $testClass, string $testSuite): array { + $reflection = new \ReflectionClass($testClass->name()); + + // Let PHPUnit API return the groups, as it will deal transparently with + // annotations or attributes, but skip groups generated by PHPUnit + // internally and starting with a double underscore prefix. + if (RunnerVersion::getMajor() < 11) { + $groups = array_filter($testClass->groups(), function (string $value): bool { + return !str_starts_with($value, '__phpunit'); + }); + } + else { + // In PHPUnit 11+, we need to coalesce the groups from individual tests + // as they may not be available from the test class level (when tests are + // backed by data providers). + $tmp = []; + foreach ($testClass as $test) { + if ($test instanceof DataProviderTestSuite) { + foreach ($test as $testWithData) { + $tmp = array_merge($tmp, $testWithData->groups()); + } + } + else { + $tmp = array_merge($tmp, $test->groups()); + } + } + $groups = array_filter(array_unique($tmp), function (string $value): bool { + return !str_starts_with($value, '__phpunit'); + }); + } + if (empty($groups)) { + throw new MissingGroupException(sprintf('Missing group metadata in test class %s', $testClass->name())); + } + + // Let PHPUnit API return the class coverage information. + $test = $testClass; + while (!$test instanceof TestCase) { + $test = $test->tests()[0]; + } + if (($metadata = $test->valueObjectForEvents()->metadata()->isCoversClass()) && $metadata->isNotEmpty()) { + $description = sprintf('Tests %s.', $metadata->asArray()[0]->className()); + } + elseif (($metadata = $test->valueObjectForEvents()->metadata()->isCoversDefaultClass()) && $metadata->isNotEmpty()) { + $description = sprintf('Tests %s.', $metadata->asArray()[0]->className()); + } + else { + $description = TestDiscovery::parseTestClassSummary($reflection->getDocComment()); + } + + // Find the test cases count. + $count = 0; + foreach ($testClass->tests() as $testCase) { + if ($testCase instanceof TestCase) { + // If it's a straight test method, counts 1. + $count++; + } + else { + // It's a data provider test suite, count 1 per data set provided. + $count += count($testCase->tests()); + } + } + + return [ + 'name' => $testClass->name(), + 'group' => $groups[0], + 'groups' => $groups, + 'type' => $testSuite, + 'description' => $description, + 'file' => $reflection->getFileName(), + 'tests_count' => $count, + ]; + } + +} diff --git a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php index 12aa757e57e..8ab5260aa66 100644 --- a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php +++ b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php @@ -4,9 +4,16 @@ namespace Drupal\Core\Test\RunTests; use PHPUnit\Framework\TestCase; +@trigger_error('Drupal\Core\Test\RunTests\TestFileParser is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); + /** * Parses class names from PHP files without loading them. * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no + * replacement. + * + * @see https://www.drupal.org/node/3447698 + * * @internal */ class TestFileParser { diff --git a/core/lib/Drupal/Core/Test/TestDiscovery.php b/core/lib/Drupal/Core/Test/TestDiscovery.php index 1347d0c583f..468256779b3 100644 --- a/core/lib/Drupal/Core/Test/TestDiscovery.php +++ b/core/lib/Drupal/Core/Test/TestDiscovery.php @@ -7,6 +7,7 @@ use Drupal\Component\Annotation\Reflection\MockFileFinder; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Test\Exception\MissingGroupException; +use PHPUnit\Framework\Attributes\Group; /** * Discovers available tests. @@ -26,6 +27,11 @@ class TestDiscovery { * Statically cached list of test classes. * * @var array + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ protected $testClasses; @@ -149,8 +155,14 @@ class TestDiscovery { * * @todo Remove singular grouping; retain list of groups in 'group' key. * @see https://www.drupal.org/node/2296615 + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * PhpUnitTestDiscovery::getTestClasses() instead. + * + * @see https://www.drupal.org/node/3447698 */ public function getTestClasses($extension = NULL, array $types = [], ?string $directory = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use PhpUnitTestDiscovery::getTestClasses() instead. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if (!isset($extension) && empty($types)) { if (!empty($this->testClasses)) { return $this->testClasses; @@ -175,6 +187,15 @@ class TestDiscovery { catch (MissingGroupException $e) { // If the class name ends in Test and is not a migrate table dump. if (str_ends_with($classname, 'Test') && !str_contains($classname, 'migrate_drupal\Tests\Table')) { + $reflection = new \ReflectionClass($classname); + $groupAttributes = $reflection->getAttributes(Group::class, \ReflectionAttribute::IS_INSTANCEOF); + if (!empty($groupAttributes)) { + $group = '##no-group-annotations'; + $info['group'] = $group; + $info['groups'] = [$group]; + $list[$group][$classname] = $info; + continue; + } throw $e; } // If the class is @group annotation just skip it. Most likely it is an @@ -216,8 +237,14 @@ class TestDiscovery { * @return array * A classmap containing all discovered class files; i.e., a map of * fully-qualified classnames to path names. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use + * PhpUnitTestDiscovery::findAllClassFiles() instead. + * + * @see https://www.drupal.org/node/3447698 */ public function findAllClassFiles($extension = NULL, ?string $directory = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use PhpUnitTestDiscovery::findAllClassFiles() instead. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); $classmap = []; $namespaces = $this->registerTestNamespaces(); if (isset($extension)) { @@ -256,8 +283,14 @@ class TestDiscovery { * * @todo Limit to '*Test.php' files (~10% less files to reflect/introspect). * @see https://www.drupal.org/node/2296635 + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ public static function scanDirectory($namespace_prefix, $path) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if (!str_ends_with($namespace_prefix, '\\')) { throw new \InvalidArgumentException("Namespace prefix for $path must contain a trailing namespace separator."); } @@ -312,8 +345,14 @@ class TestDiscovery { * * @throws \Drupal\Core\Test\Exception\MissingGroupException * If the class does not have a @group annotation. + * + * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is + * no replacement. + * + * @see https://www.drupal.org/node/3447698 */ public static function getTestInfo($classname, $doc_comment = NULL) { + @trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3447698', E_USER_DEPRECATED); if ($doc_comment === NULL) { $reflection = new \ReflectionClass($classname); $doc_comment = $reflection->getDocComment(); @@ -350,7 +389,7 @@ class TestDiscovery { $info['type'] = 'PHPUnit-' . static::getPhpunitTestSuite($classname); if (!empty($annotations['coversDefaultClass'])) { - $info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.'; + $info['description'] = 'Tests ' . ltrim($annotations['coversDefaultClass']) . '.'; } else { $info['description'] = static::parseTestClassSummary($doc_comment); diff --git a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php index 54fdf4b0d21..fe5ff0ca062 100644 --- a/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php +++ b/core/modules/language/tests/src/Kernel/Migrate/d7/MigrateLanguageNegotiationSettingsTest.php @@ -10,6 +10,7 @@ use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase; /** * Tests the migration of language negotiation. * + * @group #slow * @group migrate_drupal_7 */ class MigrateLanguageNegotiationSettingsTest extends MigrateDrupal7TestBase { diff --git a/core/modules/link/tests/src/Functional/LinkFieldUITest.php b/core/modules/link/tests/src/Functional/LinkFieldUITest.php index 694fb6b3677..5c78abc2391 100644 --- a/core/modules/link/tests/src/Functional/LinkFieldUITest.php +++ b/core/modules/link/tests/src/Functional/LinkFieldUITest.php @@ -15,6 +15,7 @@ use Drupal\Tests\field_ui\Traits\FieldUiTestTrait; * Tests link field UI functionality. * * @group link + * @group #slow */ class LinkFieldUITest extends BrowserTestBase { diff --git a/core/modules/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php b/core/modules/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php index 90348cdfdd3..2e9a0977fa3 100644 --- a/core/modules/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php +++ b/core/modules/package_manager/tests/src/Kernel/SupportedReleaseValidatorTest.php @@ -13,7 +13,6 @@ use Drupal\Tests\package_manager\Traits\FixtureUtilityTrait; * @coversDefaultClass \Drupal\package_manager\Validator\SupportedReleaseValidator * @group #slow * @group package_manager - * @group #slow * @internal */ class SupportedReleaseValidatorTest extends PackageManagerKernelTestBase { diff --git a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php index d5cc9759ab1..be5d811fe54 100644 --- a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php +++ b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php @@ -13,6 +13,7 @@ use Drupal\Tests\Traits\Core\PathAliasTestTrait; /** * URL alias migration. * + * @group #slow * @group migrate_drupal_6 */ class MigrateUrlAliasTest extends MigrateDrupal6TestBase { diff --git a/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php b/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php index 6c8c42da59e..82d866e985e 100644 --- a/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php +++ b/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php @@ -77,6 +77,8 @@ class DateFormatAccessControlHandlerTest extends KernelTestBase { * An array of test cases. */ public static function providerTestAccess(): array { + $originalContainer = \Drupal::hasContainer() ? \Drupal::getContainer() : NULL; + $c = new ContainerBuilder(); $cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class); $cache_contexts_manager->assertValidTokens()->willReturn(TRUE); @@ -84,7 +86,7 @@ class DateFormatAccessControlHandlerTest extends KernelTestBase { $c->set('cache_contexts_manager', $cache_contexts_manager); \Drupal::setContainer($c); - return [ + $data = [ 'No permission + unlocked' => [ [], 'unlocked', @@ -122,6 +124,13 @@ class DateFormatAccessControlHandlerTest extends KernelTestBase { AccessResult::allowed()->addCacheContexts(['user.permissions']), ], ]; + + // Restore the original container if needed. + if ($originalContainer) { + \Drupal::setContainer($originalContainer); + } + + return $data; } } diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php index 1d9654dd505..511778daf20 100644 --- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php @@ -10,6 +10,7 @@ use Drupal\node\Entity\Node; /** * Upgrade taxonomy term node associations. * + * @group #slow * @group migrate_drupal_6 */ class MigrateTermNodeTest extends MigrateDrupal6TestBase { diff --git a/core/modules/workflows/tests/src/Kernel/WorkflowAccessControlHandlerTest.php b/core/modules/workflows/tests/src/Kernel/WorkflowAccessControlHandlerTest.php index e46fbcf417b..180edc868f6 100644 --- a/core/modules/workflows/tests/src/Kernel/WorkflowAccessControlHandlerTest.php +++ b/core/modules/workflows/tests/src/Kernel/WorkflowAccessControlHandlerTest.php @@ -124,6 +124,8 @@ class WorkflowAccessControlHandlerTest extends KernelTestBase { * An array of test data. */ public static function checkAccessProvider() { + $originalContainer = \Drupal::hasContainer() ? \Drupal::getContainer() : NULL; + $container = new ContainerBuilder(); $cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class); $cache_contexts_manager->assertValidTokens()->willReturn(TRUE); @@ -131,7 +133,7 @@ class WorkflowAccessControlHandlerTest extends KernelTestBase { $container->set('cache_contexts_manager', $cache_contexts_manager); \Drupal::setContainer($container); - return [ + $data = [ 'Admin view' => [ 'adminUser', 'view', @@ -275,6 +277,13 @@ class WorkflowAccessControlHandlerTest extends KernelTestBase { AccessResult::allowed()->addCacheContexts(['user.permissions']), ], ]; + + // Restore the original container if needed. + if ($originalContainer) { + \Drupal::setContainer($originalContainer); + } + + return $data; } } diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 4545a00cc3a..96f16eed9c7 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -17,14 +17,13 @@ use Drupal\Component\Utility\Timer; use Drupal\Core\Composer\Composer; use Drupal\Core\Database\Database; use Drupal\Core\Test\EnvironmentCleaner; +use Drupal\Core\Test\PhpUnitTestDiscovery; use Drupal\Core\Test\PhpUnitTestRunner; use Drupal\Core\Test\SimpletestTestRunResultsStorage; -use Drupal\Core\Test\RunTests\TestFileParser; use Drupal\Core\Test\TestDatabase; use Drupal\Core\Test\TestRun; use Drupal\Core\Test\TestRunnerKernel; use Drupal\Core\Test\TestRunResultsStorageInterface; -use Drupal\Core\Test\TestDiscovery; use Drupal\BuildTests\Framework\BuildTestBase; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\KernelTests\KernelTestBase; @@ -49,6 +48,10 @@ const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; const SIMPLETEST_SCRIPT_COLOR_YELLOW = 33; // A refreshing cyan. const SIMPLETEST_SCRIPT_COLOR_CYAN = 36; +// A fainting gray. +const SIMPLETEST_SCRIPT_COLOR_GRAY = 90; +// A notable white. +const SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE = "1;97"; // Restricting the chunk of queries prevents memory exhaustion. const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350; @@ -86,12 +89,12 @@ if ($args['list']) { // Display all available tests organized by one @group annotation. echo "\nAvailable test groups & classes\n"; echo "-------------------------------\n\n"; - $test_discovery = new TestDiscovery( - \Drupal::root(), - \Drupal::service('class_loader') - ); + $test_discovery = new PhpUnitTestDiscovery(\Drupal::root() . \DIRECTORY_SEPARATOR . 'core'); try { $groups = $test_discovery->getTestClasses($args['module']); + foreach ($test_discovery->getWarnings() as $warning) { + simpletest_script_print($warning . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION); + } } catch (Exception $e) { error_log((string) $e); @@ -119,11 +122,8 @@ if ($args['list']) { // @see https://www.drupal.org/node/2569585 if ($args['list-files'] || $args['list-files-json']) { // List all files which could be run as tests. - $test_discovery = new TestDiscovery( - \Drupal::root(), - \Drupal::service('class_loader') - ); - // TestDiscovery::findAllClassFiles() gives us a classmap similar to a + $test_discovery = new PhpUnitTestDiscovery(\Drupal::root() . \DIRECTORY_SEPARATOR . 'core'); + // PhpUnitTestDiscovery::findAllClassFiles() gives us a classmap similar to a // Composer 'classmap' array. $test_classes = $test_discovery->findAllClassFiles(); // JSON output is the easiest. @@ -177,6 +177,15 @@ if (!Composer::upgradePHPUnitCheck(Version::id())) { exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } +echo "\n"; +echo "Drupal test run\n\n"; +echo sprintf("Drupal Version: %s\n", \Drupal::VERSION); +echo sprintf("PHP Version: %s\n", \PHP_VERSION); +echo sprintf("PHP Binary: %s\n", $php ?? getenv('_')); +echo sprintf("PHPUnit Version: %s\n", Version::id()); +echo "-------------------------------\n"; +echo "\n"; + $test_list = simpletest_script_get_test_list(); // Try to allocate unlimited time to run the tests. @@ -355,6 +364,11 @@ All arguments are long options. The index of the job in the job set. + --debug-discovery + + If provided, dumps detailed information on the tests selected + for execution, before the execution starts. + <test1>[,<test2>[,<test3> ...]] One or more tests to be run. By default, these are interpreted @@ -427,6 +441,7 @@ function simpletest_script_parse_args() { 'non-html' => FALSE, 'ci-parallel-node-index' => 1, 'ci-parallel-node-total' => 1, + 'debug-discovery' => FALSE, ]; // Override with set values. @@ -919,17 +934,15 @@ function simpletest_script_command(TestRun $test_run, string $test_class): array function simpletest_script_get_test_list() { global $args; - $test_discovery = new TestDiscovery( - \Drupal::root(), - \Drupal::service('class_loader') - ); - $types_processed = empty($args['types']); + $test_discovery = new PhpUnitTestDiscovery(\Drupal::root() . \DIRECTORY_SEPARATOR . 'core'); $test_list = []; $slow_tests = []; if ($args['all'] || $args['module'] || $args['directory']) { try { $groups = $test_discovery->getTestClasses($args['module'], $args['types'], $args['directory']); - $types_processed = TRUE; + foreach ($test_discovery->getWarnings() as $warning) { + simpletest_script_print($warning . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION); + } } catch (Exception $e) { echo (string) $e; @@ -938,30 +951,34 @@ function simpletest_script_get_test_list() { // Ensure that tests marked explicitly as @group #slow are run at the // beginning of each job. if (key($groups) === '#slow') { - $slow_tests = array_keys(array_shift($groups)); + $slow_tests = array_shift($groups); } $not_slow_tests = []; foreach ($groups as $group => $tests) { - $not_slow_tests = array_merge($not_slow_tests, array_keys($tests)); + $not_slow_tests = array_merge($not_slow_tests, $tests); } // Filter slow tests out of the not slow tests and ensure a unique list // since tests may appear in more than one group. - $not_slow_tests = array_unique(array_diff($not_slow_tests, $slow_tests)); + $not_slow_tests = array_diff_key($not_slow_tests, $slow_tests); // If the tests are not being run in parallel, then ensure slow tests run // all together first. if ((int) $args['ci-parallel-node-total'] <= 1 ) { sort_tests_by_type_and_methods($slow_tests); sort_tests_by_type_and_methods($not_slow_tests); - $test_list = array_merge($slow_tests, $not_slow_tests); + $all_tests_list = array_merge($slow_tests, $not_slow_tests); + assign_tests_sequence($all_tests_list); + dump_tests_sequence($all_tests_list, $args); + $test_list = array_keys($all_tests_list); } else { - // Sort all tests by the number of public methods on the test class. - // This is a proxy for the approximate time taken to run the test, - // which is used in combination with @group #slow to start the slowest tests - // first and distribute tests between test runners. + // Sort all tests by the number of test cases on the test class. + // This is used in combination with @group #slow to start the slowest + // tests first and distribute tests between test runners. sort_tests_by_public_method_count($slow_tests); sort_tests_by_public_method_count($not_slow_tests); + $all_tests_list = array_merge($slow_tests, $not_slow_tests); + assign_tests_sequence($all_tests_list); // Now set up a bin per test runner. $bin_count = (int) $args['ci-parallel-node-total']; @@ -975,6 +992,8 @@ function simpletest_script_get_test_list() { $binned_other_tests = place_tests_into_bins($not_slow_tests, $bin_count); $other_tests_for_job = $binned_other_tests[$args['ci-parallel-node-index'] - 1]; $test_list = array_merge($slow_tests_for_job, $other_tests_for_job); + dump_bin_tests_sequence($args['ci-parallel-node-index'], $all_tests_list, $test_list, $args); + $test_list = array_keys($test_list); } } else { @@ -988,6 +1007,9 @@ function simpletest_script_get_test_list() { else { try { $groups = $test_discovery->getTestClasses(NULL, $args['types']); + foreach ($test_discovery->getWarnings() as $warning) { + simpletest_script_print($warning . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION); + } } catch (Exception $e) { echo (string) $e; @@ -1005,19 +1027,24 @@ function simpletest_script_get_test_list() { } elseif ($args['file']) { // Extract test case class names from specified files. - $parser = new TestFileParser(); foreach ($args['test_names'] as $file) { - if (!file_exists($file)) { + if (!file_exists($file) || is_dir($file)) { simpletest_script_print_error('File not found: ' . $file); exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); } - $test_list = array_merge($test_list, $parser->getTestListFromFile($file)); + $fileTests = current($test_discovery->getTestClasses(NULL, [], $file)); + $test_list = array_merge($test_list, $fileTests); } + assign_tests_sequence($test_list); + dump_tests_sequence($test_list, $args); + $test_list = array_keys($test_list); } else { try { $groups = $test_discovery->getTestClasses(NULL, $args['types']); - $types_processed = TRUE; + foreach ($test_discovery->getWarnings() as $warning) { + simpletest_script_print($warning . "\n", SIMPLETEST_SCRIPT_COLOR_EXCEPTION); + } } catch (Exception $e) { echo (string) $e; @@ -1034,22 +1061,15 @@ function simpletest_script_get_test_list() { } // Merge the tests from the groups together. foreach ($args['test_names'] as $group_name) { - $test_list = array_merge($test_list, array_keys($groups[$group_name])); + $test_list = array_merge($test_list, $groups[$group_name]); } + assign_tests_sequence($test_list); + dump_tests_sequence($test_list, $args); // Ensure our list of tests contains only one entry for each test. - $test_list = array_unique($test_list); + $test_list = array_keys($test_list); } } - // If the test list creation does not automatically limit by test type then - // we need to do so here. - if (!$types_processed) { - $test_list = array_filter($test_list, function ($test_class) use ($args) { - $test_info = TestDiscovery::getTestInfo($test_class); - return in_array($test_info['type'], $args['types'], TRUE); - }); - } - if (empty($test_list)) { simpletest_script_print_error('No valid tests were specified.'); exit(SIMPLETEST_SCRIPT_EXIT_FAILURE); @@ -1062,11 +1082,11 @@ function simpletest_script_get_test_list() { * Sort tests by test type and number of public methods. */ function sort_tests_by_type_and_methods(array &$tests): void { - usort($tests, function ($a, $b) { - if (get_test_type_weight($a) === get_test_type_weight($b)) { - return get_test_class_method_count($b) <=> get_test_class_method_count($a); + uasort($tests, function ($a, $b) { + if (get_test_type_weight($a['name']) === get_test_type_weight($b['name'])) { + return $b['tests_count'] <=> $a['tests_count']; } - return get_test_type_weight($b) <=> get_test_type_weight($a); + return get_test_type_weight($b['name']) <=> get_test_type_weight($a['name']); }); } @@ -1083,8 +1103,8 @@ function sort_tests_by_type_and_methods(array &$tests): void { * An array of test class names. */ function sort_tests_by_public_method_count(array &$tests): void { - usort($tests, function ($a, $b) { - return get_test_class_method_count($b) <=> get_test_class_method_count($a); + uasort($tests, function ($a, $b) { + return $b['tests_count'] <=> $a['tests_count']; }); } @@ -1105,28 +1125,46 @@ function get_test_type_weight(string $class): int { } /** - * Get an approximate test method count for a test class. + * Assigns the test sequence. * - * @param string $class - * The test class name. + * @param array $tests + * The array of test class info. */ -function get_test_class_method_count(string $class): int { - $reflection = new \ReflectionClass($class); - $count = 0; - foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { - // If a method uses a dataProvider, increase the count by 20 since data - // providers result in a single method running multiple times. - $comments = $method->getDocComment(); - preg_match_all('#@(.*?)\n#s', $comments, $annotations); - foreach ($annotations[1] as $annotation) { - if (str_starts_with($annotation, 'dataProvider')) { - $count = $count + 20; - continue; - } - } - $count++; +function assign_tests_sequence(array &$tests): void { + $i = 0; + foreach ($tests as &$testInfo) { + $testInfo['sequence'] = ++$i; } - return $count; +} + +/** + * Dumps the list of tests in order of execution after sorting. + * + * @param array $tests + * The array of test class info. + * @param array $args + * The command line arguments. + */ +function dump_tests_sequence(array $tests, array $args): void { + if ($args['debug-discovery'] === FALSE) { + return; + } + echo "Test execution sequence\n"; + echo "-----------------------\n\n"; + echo " Seq Slow? Group Cnt Class\n"; + echo "-----------------------------------------\n"; + $i = 0; + foreach ($tests as $testInfo) { + echo sprintf( + "%4d %5s %15s %4d %s\n", + $testInfo['sequence'], + in_array('#slow', $testInfo['groups']) ? '#slow' : '', + trim_with_ellipsis($testInfo['group'], 15, \STR_PAD_RIGHT), + $testInfo['tests_count'], + trim_with_ellipsis($testInfo['name'], 60, \STR_PAD_LEFT), + ); + } + echo "-----------------------------------------\n\n"; } /** @@ -1149,13 +1187,51 @@ function place_tests_into_bins(array $tests, int $bin_count) { // Create a bin corresponding to each parallel test job. $bins = array_fill(0, $bin_count, []); // Go through each test and add them to one bin at a time. + $i = 0; foreach ($tests as $key => $test) { - $bins[($key % $bin_count)][] = $test; + $bins[($i++ % $bin_count)][$key] = $test; } return $bins; } /** + * Dumps the list of tests in order of execution for a bin. + * + * @param int $bin + * The bin. + * @param array $allTests + * The list of all test classes discovered. + * @param array $tests + * The list of test class to run for this bin. + * @param array $args + * The command line arguments. + */ +function dump_bin_tests_sequence(int $bin, array $allTests, array $tests, array $args): void { + if ($args['debug-discovery'] === FALSE) { + return; + } + echo "Test execution sequence. "; + echo "Tests marked *** will be executed in this PARALLEL BIN #{$bin}.\n"; + echo "-------------------------------------------------------------------------------------\n\n"; + echo "Bin Seq Slow? Group Cnt Class\n"; + echo "--------------------------------------------\n"; + foreach ($allTests as $testInfo) { + $inBin = isset($tests[$testInfo['name']]); + $message = sprintf( + "%s %4d %5s %15s %4d %s\n", + $inBin ? "***" : " ", + $testInfo['sequence'], + in_array('#slow', $testInfo['groups']) ? '#slow' : '', + trim_with_ellipsis($testInfo['group'], 15, \STR_PAD_RIGHT), + $testInfo['tests_count'], + trim_with_ellipsis($testInfo['name'], 60, \STR_PAD_LEFT), + ); + simpletest_script_print($message, $inBin ? SIMPLETEST_SCRIPT_COLOR_BRIGHT_WHITE : SIMPLETEST_SCRIPT_COLOR_GRAY); + } + echo "-------------------------------------------------\n\n"; +} + +/** * Initialize the reporter. */ function simpletest_script_reporter_init(): void { @@ -1170,12 +1246,6 @@ function simpletest_script_reporter_init(): void { 'debug' => 'Log', ]; - echo "\n"; - echo "Drupal test run\n"; - echo "Using PHP Binary: $php\n"; - echo "---------------\n"; - echo "\n"; - // Tell the user about what tests are to be run. if ($args['all']) { echo "All tests will run.\n\n"; @@ -1378,7 +1448,7 @@ function simpletest_script_reporter_display_results(TestRunResultsStorageInterfa function simpletest_script_format_result($result): void { global $args, $results_map, $color; - $summary = sprintf("%-9.9s %9.3fs %-80.80s\n", $results_map[$result->status], $result->time, trim_with_ellipsis($result->function, 80, STR_PAD_LEFT)); + $summary = sprintf("%-9.9s %9.3fs %s\n", $results_map[$result->status], $result->time, trim_with_ellipsis($result->function, 80, STR_PAD_LEFT)); simpletest_script_print($summary, simpletest_script_color_code($result->status)); @@ -1540,12 +1610,12 @@ function simpletest_script_load_messages_by_test_id(TestRunResultsStorageInterfa */ function trim_with_ellipsis(string $input, int $length, int $side): string { if (strlen($input) < $length) { - return str_pad($input, $length, ' ', STR_PAD_RIGHT); + return str_pad($input, $length, ' ', \STR_PAD_RIGHT); } elseif (strlen($input) > $length) { return match($side) { - STR_PAD_RIGHT => substr($input, 0, $length - 3) . '...', - default => '...' . substr($input, -$length + 3), + \STR_PAD_RIGHT => substr($input, 0, $length - 1) . '…', + default => '…' . substr($input, -$length + 1), }; } return $input; diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php new file mode 100644 index 00000000000..feb6addef2a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php @@ -0,0 +1,72 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Test; + +use Drupal\Core\Test\PhpUnitTestDiscovery; +use Drupal\Core\Test\TestDiscovery; +use Drupal\KernelTests\KernelTestBase; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * Tests ::findAllClassFiles() between TestDiscovery and PhpUnitTestDiscovery. + * + * PhpUnitTestDiscovery uses PHPUnit API to build the list of test classes, + * while TestDiscovery uses Drupal legacy code. + */ +#[CoversClass(PhpUnitTestDiscovery::class)] +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] +class PhpUnitApiFindAllClassFilesTest extends KernelTestBase { + + /** + * Checks that Drupal legacy and PHPUnit API based discoveries are equal. + */ + #[DataProvider('argumentsProvider')] + #[IgnoreDeprecations] + public function testEquality(?string $extension = NULL, ?string $directory = NULL): void { + $testDiscovery = new TestDiscovery( + $this->container->getParameter('app.root'), + $this->container->get('class_loader') + ); + $internalList = $testDiscovery->findAllClassFiles($extension, $directory); + + // Location of PHPUnit configuration file. + $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + + $phpUnitTestDiscovery = new PhpUnitTestDiscovery($configurationFilePath); + + $phpUnitList = $phpUnitTestDiscovery->findAllClassFiles($extension, $directory); + + // Downgrade results to make them comparable, working around bugs and + // additions. + // 1. TestDiscovery discovers non-test classes that PHPUnit does not. + $internalList = array_intersect_key($internalList, $phpUnitList); + + $this->assertEquals($internalList, $phpUnitList); + } + + /** + * Provides test data to ::testEquality. + */ + public static function argumentsProvider(): \Generator { + yield 'All tests' => []; + yield 'Extension: system' => ['extension' => 'system']; + yield 'Extension: system, directory' => [ + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php new file mode 100644 index 00000000000..32d21e3e6c3 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Test; + +use Drupal\Core\Test\PhpUnitTestDiscovery; +use Drupal\Core\Test\TestDiscovery; +use Drupal\KernelTests\KernelTestBase; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * Tests ::getTestClasses() between TestDiscovery and PhpPUnitTestDiscovery. + * + * PhpPUnitTestDiscovery uses PHPUnit API to build the list of test classes, + * while TestDiscovery uses Drupal legacy code. + */ +#[CoversClass(PhpUnitTestDiscovery::class)] +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] +class PhpUnitApiGetTestClassesTest extends KernelTestBase { + + /** + * Checks that Drupal legacy and PHPUnit API based discoveries are equal. + */ + #[DataProvider('argumentsProvider')] + #[IgnoreDeprecations] + public function testEquality(array $suites, ?string $extension = NULL, ?string $directory = NULL): void { + $testDiscovery = new TestDiscovery( + $this->container->getParameter('app.root'), + $this->container->get('class_loader') + ); + $internalList = $testDiscovery->getTestClasses($extension, $suites, $directory); + + // Location of PHPUnit configuration file. + $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + + $phpUnitTestDiscovery = new PhpUnitTestDiscovery($configurationFilePath); + + $phpUnitList = $phpUnitTestDiscovery->getTestClasses($extension, $suites, $directory); + + // Downgrade results to make them comparable, working around bugs and + // additions. + // 1. Remove TestDiscovery empty groups. + $internalList = array_filter($internalList); + // 2. Remove TestDiscovery '##no-group-annotations' group. + unset($internalList['##no-group-annotations']); + // 3. Remove 'file' and 'tests_count' keys from PHPUnit results. + foreach ($phpUnitList as &$group) { + foreach ($group as &$testClass) { + unset($testClass['file']); + unset($testClass['tests_count']); + } + } + // 4. Remove from PHPUnit results groups not found by TestDiscovery. + $phpUnitList = array_intersect_key($phpUnitList, $internalList); + // 5. Remove from PHPUnit groups classes not found by TestDiscovery. + foreach ($phpUnitList as $groupName => &$group) { + $group = array_intersect_key($group, $internalList[$groupName]); + } + // 6. Remove from PHPUnit test classes groups not found by TestDiscovery. + foreach ($phpUnitList as $groupName => &$group) { + foreach ($group as $testClassName => &$testClass) { + $testClass['groups'] = array_intersect_key($testClass['groups'], $internalList[$groupName][$testClassName]['groups']); + } + } + + $this->assertEquals($internalList, $phpUnitList); + } + + /** + * Provides test data to ::testEquality. + */ + public static function argumentsProvider(): \Generator { + yield 'All tests' => ['suites' => []]; + yield 'Testsuite: functional-javascript' => ['suites' => ['PHPUnit-FunctionalJavascript']]; + yield 'Testsuite: functional' => ['suites' => ['PHPUnit-Functional']]; + yield 'Testsuite: kernel' => ['suites' => ['PHPUnit-Kernel']]; + yield 'Testsuite: unit' => ['suites' => ['PHPUnit-Unit']]; + yield 'Testsuite: unit-component' => ['suites' => ['PHPUnit-Unit-Component']]; + yield 'Testsuite: build' => ['suites' => ['PHPUnit-Build']]; + yield 'Extension: system' => ['suites' => [], 'extension' => 'system']; + yield 'Extension: system, testsuite: unit' => [ + 'suites' => ['PHPUnit-Unit'], + 'extension' => 'system', + ]; + yield 'Extension: system, directory' => [ + 'suites' => [], + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + yield 'Extension: system, testsuite: unit, directory' => [ + 'suites' => ['PHPUnit-Unit'], + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php index 705981f7507..d8534458049 100644 --- a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php @@ -7,6 +7,8 @@ namespace Drupal\KernelTests\Core\Test; use Drupal\Core\Test\TestDiscovery; use Drupal\KernelTests\KernelTestBase; use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\TextUI\Configuration\Builder; use PHPUnit\TextUI\Configuration\TestSuiteBuilder; use Symfony\Component\Process\Process; @@ -22,11 +24,10 @@ use Symfony\Component\Process\Process; * list thus generated, with the list generated by * \Drupal\Core\Test\TestDiscovery, which is used by run-tests.sh, to ensure * both methods will run the same tests, - * - * @group TestSuites - * @group Test - * @group #slow */ +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] class PhpUnitTestDiscoveryTest extends KernelTestBase { private const TEST_LIST_MISMATCH_MESSAGE = @@ -61,6 +62,7 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase { /** * Tests equality of test discovery between run-tests.sh and PHPUnit CLI. */ + #[IgnoreDeprecations] public function testPhpUnitTestDiscoveryEqualsInternal(): void { // Drupal's test discovery, used by run-tests.sh. $testDiscovery = new TestDiscovery( @@ -76,11 +78,19 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase { $internalList = array_unique($internalList); asort($internalList); + // Location of PHPUnit configuration file. + $configurationFilePath = $this->root . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + // PHPUnit's test discovery - via CLI execution. $process = new Process([ 'vendor/bin/phpunit', '--configuration', - 'core', + $configurationFilePath, '--list-tests-xml', $this->xmlOutputFile, ], $this->root); @@ -113,14 +123,8 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase { // Check against Drupal's discovery. $this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE); - // @todo once PHPUnit 10 is no longer used re-enable the rest of the test. - // @see https://www.drupal.org/project/drupal/issues/3497116 - if (RunnerVersion::getMajor() >= 11) { - $this->markTestIncomplete('On PHPUnit 11+ the test triggers warnings due to phpunit.xml setup. Re-enable in https://www.drupal.org/project/drupal/issues/3497116.'); - } - // PHPUnit's test discovery - via API. - $phpUnitConfiguration = (new Builder())->build(['--configuration', 'core']); + $phpUnitConfiguration = (new Builder())->build(['--configuration', $configurationFilePath]); $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration); $phpUnitApiList = []; foreach ($phpUnitTestSuite->tests() as $testSuite) { diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php index 08fa2eceaf5..d0759a4bf08 100644 --- a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php @@ -203,6 +203,8 @@ class LocalActionManagerTest extends UnitTestCase { } public static function getActionsForRouteProvider() { + $originalContainer = \Drupal::hasContainer() ? \Drupal::getContainer() : NULL; + $cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class); $cache_contexts_manager->assertValidTokens(Argument::any()) ->willReturn(TRUE); @@ -384,6 +386,11 @@ class LocalActionManagerTest extends UnitTestCase { ], ]; + // Restore the original container if needed. + if ($originalContainer) { + \Drupal::setContainer($originalContainer); + } + return $data; } diff --git a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php index 957b2f61f97..9138d54523d 100644 --- a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php +++ b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php @@ -6,12 +6,18 @@ namespace Drupal\Tests\Core\Test\RunTests; use Drupal\Core\Test\RunTests\TestFileParser; use Drupal\Tests\UnitTestCase; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; /** - * @coversDefaultClass \Drupal\Core\Test\RunTests\TestFileParser - * @group Test - * @group RunTests + * Tests for the deprecated TestFileParser class. */ +#[CoversClass(TestFileParser::class)] +#[Group('Test')] +#[Group('RunTest')] +#[IgnoreDeprecations] class TestFileParserTest extends UnitTestCase { public static function provideTestFileContents() { @@ -66,9 +72,9 @@ COMPOUND } /** - * @covers ::parseContents - * @dataProvider provideTestFileContents + * @legacy-covers ::parseContents */ + #[DataProvider('provideTestFileContents')] public function testParseContents($expected, $contents): void { $parser = new TestFileParser(); @@ -78,7 +84,7 @@ COMPOUND } /** - * @covers ::getTestListFromFile + * @legacy-covers ::getTestListFromFile */ public function testGetTestListFromFile(): void { $parser = new TestFileParser(); diff --git a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php index bfbb4ca2e40..0fb55e6c7f8 100644 --- a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php +++ b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php @@ -13,17 +13,23 @@ use Drupal\Core\Test\Exception\MissingGroupException; use Drupal\Core\Test\TestDiscovery; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; /** - * @coversDefaultClass \Drupal\Core\Test\TestDiscovery - * @group Test + * Unit tests for TestDiscovery. */ +#[CoversClass(TestDiscovery::class)] +#[Group('Test')] +#[IgnoreDeprecations] class TestDiscoveryTest extends UnitTestCase { /** - * @covers ::getTestInfo - * @dataProvider infoParserProvider + * @legacy-covers ::getTestInfo */ + #[DataProvider('infoParserProvider')] public function testTestInfoParser($expected, $classname, $doc_comment = NULL): void { $info = TestDiscovery::getTestInfo($classname, $doc_comment); $this->assertEquals($expected, $info); @@ -34,14 +40,14 @@ class TestDiscoveryTest extends UnitTestCase { $tests[] = [ // Expected result. [ - 'name' => static::class, + 'name' => TestDatabaseTest::class, 'group' => 'Test', - 'groups' => ['Test'], - 'description' => 'Tests \Drupal\Core\Test\TestDiscovery.', + 'groups' => ['Test', 'simpletest', 'Template'], + 'description' => 'Tests \Drupal\Core\Test\TestDatabase.', 'type' => 'PHPUnit-Unit', ], // Classname. - static::class, + TestDatabaseTest::class, ]; // A core unit test. @@ -217,7 +223,7 @@ class TestDiscoveryTest extends UnitTestCase { } /** - * @covers ::getTestInfo + * @legacy-covers ::getTestInfo */ public function testTestInfoParserMissingGroup(): void { $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; @@ -232,7 +238,7 @@ EOT; } /** - * @covers ::getTestInfo + * @legacy-covers ::getTestInfo */ public function testTestInfoParserMissingSummary(): void { $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; @@ -311,7 +317,7 @@ EOF; } /** - * @covers ::getTestClasses + * @legacy-covers ::getTestClasses */ public function testGetTestClasses(): void { $this->setupVfsWithTestClasses(); @@ -380,7 +386,7 @@ EOF; } /** - * @covers ::getTestClasses + * @legacy-covers ::getTestClasses */ public function testGetTestClassesWithSelectedTypes(): void { $this->setupVfsWithTestClasses(); @@ -425,7 +431,7 @@ EOF; } /** - * @covers ::getTestClasses + * @legacy-covers ::getTestClasses */ public function testGetTestsInProfiles(): void { $this->setupVfsWithTestClasses(); @@ -454,9 +460,9 @@ EOF; } /** - * @covers ::getPhpunitTestSuite - * @dataProvider providerTestGetPhpunitTestSuite + * @legacy-covers ::getPhpunitTestSuite */ + #[DataProvider('providerTestGetPhpunitTestSuite')] public function testGetPhpunitTestSuite($classname, $expected): void { $this->assertEquals($expected, TestDiscovery::getPhpunitTestSuite($classname)); } @@ -482,7 +488,7 @@ EOF; /** * Ensure that classes are not reflected when the docblock is empty. * - * @covers ::getTestInfo + * @legacy-covers ::getTestInfo */ public function testGetTestInfoEmptyDocblock(): void { // If getTestInfo() performed reflection, it won't be able to find the @@ -497,7 +503,7 @@ EOF; /** * Ensure TestDiscovery::scanDirectory() ignores certain abstract file types. * - * @covers ::scanDirectory + * @legacy-covers ::scanDirectory */ public function testScanDirectoryNoAbstract(): void { $this->setupVfsWithTestClasses(); |