diff --git a/core/lib/Drupal/Core/Test/Exception/MissingGroupException.php b/core/lib/Drupal/Core/Test/Exception/MissingGroupException.php new file mode 100644 index 0000000000000000000000000000000000000000..448b4f5b2d75994c2a69cbee89d0ac05363fa6da --- /dev/null +++ b/core/lib/Drupal/Core/Test/Exception/MissingGroupException.php @@ -0,0 +1,11 @@ +<?php + +namespace Drupal\Core\Test\Exception; + +/** + * Exception thrown when a test class is missing an @group annotation. + * + * @see \Drupal\Core\Test\TestDiscovery::getTestClasses() + */ +class MissingGroupException extends \LogicException { +} diff --git a/core/lib/Drupal/Core/Test/TestDiscovery.php b/core/lib/Drupal/Core/Test/TestDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..a8374f92a225c1746db95445fe9a22230712a289 --- /dev/null +++ b/core/lib/Drupal/Core/Test/TestDiscovery.php @@ -0,0 +1,487 @@ +<?php + +namespace Drupal\Core\Test; + +use Doctrine\Common\Reflection\StaticReflectionParser; +use Drupal\Component\Annotation\Reflection\MockFileFinder; +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Test\Exception\MissingGroupException; +use PHPUnit_Util_Test; + +/** + * Discovers available tests. + */ +class TestDiscovery { + + /** + * The class loader. + * + * @var \Composer\Autoload\ClassLoader + */ + protected $classLoader; + + /** + * Statically cached list of test classes. + * + * @var array + */ + protected $testClasses; + + /** + * Cached map of all test namespaces to respective directories. + * + * @var array + */ + protected $testNamespaces; + + /** + * Cached list of all available extension names, keyed by extension type. + * + * @var array + */ + protected $availableExtensions; + + /** + * The app root. + * + * @var string + */ + protected $root; + + /** + * Constructs a new test discovery. + * + * @param string $root + * The app root. + * @param $class_loader + * The class loader. Normally Composer's ClassLoader, as included by the + * front controller, but may also be decorated; e.g., + * \Symfony\Component\ClassLoader\ApcClassLoader. + */ + public function __construct($root, $class_loader) { + $this->root = $root; + $this->classLoader = $class_loader; + } + + /** + * Registers test namespaces of all extensions and core test classes. + * + * @return array + * An associative array whose keys are PSR-4 namespace prefixes and whose + * values are directory names. + */ + public function registerTestNamespaces() { + if (isset($this->testNamespaces)) { + return $this->testNamespaces; + } + $this->testNamespaces = []; + + $existing = $this->classLoader->getPrefixesPsr4(); + + // Add PHPUnit test namespaces of Drupal core. + $this->testNamespaces['Drupal\\Tests\\'] = [$this->root . '/core/tests/Drupal/Tests']; + $this->testNamespaces['Drupal\\KernelTests\\'] = [$this->root . '/core/tests/Drupal/KernelTests']; + $this->testNamespaces['Drupal\\FunctionalTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalTests']; + $this->testNamespaces['Drupal\\FunctionalJavascriptTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalJavascriptTests']; + + $this->availableExtensions = []; + foreach ($this->getExtensions() as $name => $extension) { + $this->availableExtensions[$extension->getType()][$name] = $name; + + $base_path = $this->root . '/' . $extension->getPath(); + + // Add namespace of disabled/uninstalled extensions. + if (!isset($existing["Drupal\\$name\\"])) { + $this->classLoader->addPsr4("Drupal\\$name\\", "$base_path/src"); + } + // Add Simpletest test namespace. + $this->testNamespaces["Drupal\\$name\\Tests\\"][] = "$base_path/src/Tests"; + + // Add PHPUnit test namespaces. + $this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit"; + $this->testNamespaces["Drupal\\Tests\\$name\\Kernel\\"][] = "$base_path/tests/src/Kernel"; + $this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional"; + $this->testNamespaces["Drupal\\Tests\\$name\\FunctionalJavascript\\"][] = "$base_path/tests/src/FunctionalJavascript"; + + // Add discovery for traits which are shared between different test + // suites. + $this->testNamespaces["Drupal\\Tests\\$name\\Traits\\"][] = "$base_path/tests/src/Traits"; + } + + foreach ($this->testNamespaces as $prefix => $paths) { + $this->classLoader->addPsr4($prefix, $paths); + } + + return $this->testNamespaces; + } + + /** + * Discovers all available tests in all extensions. + * + * @param string $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param string[] $types + * An array of included test types. + * + * @return array + * An array of tests keyed by the the group name. If a test is annotated to + * belong to multiple groups, it will appear under all group keys it belongs + * to. + * @code + * $groups['block'] => array( + * 'Drupal\Tests\block\Functional\BlockTest' => array( + * 'name' => 'Drupal\Tests\block\Functional\BlockTest', + * 'description' => 'Tests block UI CRUD functionality.', + * 'group' => 'block', + * 'groups' => ['block', 'group2', 'group3'], + * ), + * ); + * @endcode + * + * @todo Remove singular grouping; retain list of groups in 'group' key. + * @see https://www.drupal.org/node/2296615 + */ + public function getTestClasses($extension = NULL, array $types = []) { + if (!isset($extension) && empty($types)) { + if (!empty($this->testClasses)) { + return $this->testClasses; + } + } + $list = []; + + $classmap = $this->findAllClassFiles($extension); + + // Prevent expensive class loader lookups for each reflected test class by + // registering the complete classmap of test classes to the class loader. + // This also ensures that test classes are loaded from the discovered + // pathnames; a namespace/classname mismatch will throw an exception. + $this->classLoader->addClassMap($classmap); + + foreach ($classmap as $classname => $pathname) { + $finder = MockFileFinder::create($pathname); + $parser = new StaticReflectionParser($classname, $finder, TRUE); + try { + $info = static::getTestInfo($classname, $parser->getDocComment()); + } + catch (MissingGroupException $e) { + // If the class name ends in Test and is not a migrate table dump. + if (preg_match('/Test$/', $classname) && strpos($classname, 'migrate_drupal\Tests\Table') === FALSE) { + throw $e; + } + // If the class is @group annotation just skip it. Most likely it is an + // abstract class, trait or test fixture. + continue; + } + // Skip this test class if it is a Simpletest-based test and requires + // unavailable modules. TestDiscovery should not filter out module + // requirements for PHPUnit-based test classes. + // @todo Move this behavior to \Drupal\simpletest\TestBase so tests can be + // marked as skipped, instead. + // @see https://www.drupal.org/node/1273478 + if ($info['type'] == 'Simpletest') { + if (!empty($info['requires']['module'])) { + if (array_diff($info['requires']['module'], $this->availableExtensions['module'])) { + continue; + } + } + } + + foreach ($info['groups'] as $group) { + $list[$group][$classname] = $info; + } + } + + // Sort the groups and tests within the groups by name. + uksort($list, 'strnatcasecmp'); + foreach ($list as &$tests) { + uksort($tests, 'strnatcasecmp'); + } + + if (!isset($extension) && empty($types)) { + $this->testClasses = $list; + } + + if ($types) { + $list = NestedArray::filter($list, function ($element) use ($types) { + return !(is_array($element) && isset($element['type']) && !in_array($element['type'], $types)); + }); + } + + return $list; + } + + /** + * Discovers all class files in all available extensions. + * + * @param string $extension + * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * + * @return array + * A classmap containing all discovered class files; i.e., a map of + * fully-qualified classnames to pathnames. + */ + public function findAllClassFiles($extension = NULL) { + $classmap = []; + $namespaces = $this->registerTestNamespaces(); + if (isset($extension)) { + // Include tests in the \Drupal\Tests\{$extension} namespace. + $pattern = "/Drupal\\\(Tests\\\)?$extension\\\/"; + $namespaces = array_intersect_key($namespaces, array_flip(preg_grep($pattern, array_keys($namespaces)))); + } + foreach ($namespaces as $namespace => $paths) { + foreach ($paths as $path) { + if (!is_dir($path)) { + continue; + } + $classmap += static::scanDirectory($namespace, $path); + } + } + return $classmap; + } + + /** + * Scans a given directory for class files. + * + * @param string $namespace_prefix + * The namespace prefix to use for discovered classes. Must contain a + * trailing namespace separator (backslash). + * For example: 'Drupal\\node\\Tests\\' + * @param string $path + * The directory path to scan. + * For example: '/path/to/drupal/core/modules/node/tests/src' + * + * @return array + * An associative array whose keys are fully-qualified class names and whose + * values are corresponding filesystem pathnames. + * + * @throws \InvalidArgumentException + * If $namespace_prefix does not end in a namespace separator (backslash). + * + * @todo Limit to '*Test.php' files (~10% less files to reflect/introspect). + * @see https://www.drupal.org/node/2296635 + */ + public static function scanDirectory($namespace_prefix, $path) { + if (substr($namespace_prefix, -1) !== '\\') { + throw new \InvalidArgumentException("Namespace prefix for $path must contain a trailing namespace separator."); + } + $flags = \FilesystemIterator::UNIX_PATHS; + $flags |= \FilesystemIterator::SKIP_DOTS; + $flags |= \FilesystemIterator::FOLLOW_SYMLINKS; + $flags |= \FilesystemIterator::CURRENT_AS_SELF; + $flags |= \FilesystemIterator::KEY_AS_FILENAME; + + $iterator = new \RecursiveDirectoryIterator($path, $flags); + $filter = new \RecursiveCallbackFilterIterator($iterator, function ($current, $file_name, $iterator) { + if ($iterator->hasChildren()) { + return TRUE; + } + // We don't want to discover abstract TestBase classes, traits or + // interfaces. They can be deprecated and will call @trigger_error() + // during discovery. + return substr($file_name, -4) === '.php' && + substr($file_name, -12) !== 'TestBase.php' && + substr($file_name, -9) !== 'Trait.php' && + substr($file_name, -13) !== 'Interface.php'; + }); + $files = new \RecursiveIteratorIterator($filter); + $classes = []; + foreach ($files as $fileinfo) { + $class = $namespace_prefix; + if ('' !== $subpath = $fileinfo->getSubPath()) { + $class .= strtr($subpath, '/', '\\') . '\\'; + } + $class .= $fileinfo->getBasename('.php'); + $classes[$class] = $fileinfo->getPathname(); + } + return $classes; + } + + /** + * Retrieves information about a test class for UI purposes. + * + * @param string $classname + * The test classname. + * @param string $doc_comment + * (optional) The class PHPDoc comment. If not passed in reflection will be + * used but this is very expensive when parsing all the test classes. + * + * @return array + * An associative array containing: + * - name: The test class name. + * - description: The test (PHPDoc) summary. + * - group: The test's first @group (parsed from PHPDoc annotations). + * - groups: All of the test's @group annotations, as an array (parsed from + * PHPDoc annotations). + * - requires: An associative array containing test requirements parsed from + * PHPDoc annotations: + * - module: List of Drupal module extension names the test depends on. + * + * @throws \Drupal\simpletest\Exception\MissingGroupException + * If the class does not have a @group annotation. + */ + public static function getTestInfo($classname, $doc_comment = NULL) { + if ($doc_comment === NULL) { + $reflection = new \ReflectionClass($classname); + $doc_comment = $reflection->getDocComment(); + } + $info = [ + 'name' => $classname, + ]; + $annotations = []; + // Look for annotations, allow an arbitrary amount of spaces before the + // * but nothing else. + preg_match_all('/^[ ]*\* \@([^\s]*) (.*$)/m', $doc_comment, $matches); + if (isset($matches[1])) { + foreach ($matches[1] as $key => $annotation) { + // For historical reasons, there is a single-value 'group' result key + // and a 'groups' key as an array. + if ($annotation === 'group') { + $annotations['groups'][] = $matches[2][$key]; + } + if (!empty($annotations[$annotation])) { + // Only @group is allowed to have more than one annotation, in the + // 'groups' key. Other annotations only have one value per key. + continue; + } + $annotations[$annotation] = $matches[2][$key]; + } + } + + if (empty($annotations['group'])) { + // Concrete tests must have a group. + throw new MissingGroupException(sprintf('Missing @group annotation in %s', $classname)); + } + $info['group'] = $annotations['group']; + $info['groups'] = $annotations['groups']; + + // Sort out PHPUnit-runnable tests by type. + if ($testsuite = static::getPhpunitTestSuite($classname)) { + $info['type'] = 'PHPUnit-' . $testsuite; + } + else { + $info['type'] = 'Simpletest'; + } + + if (!empty($annotations['coversDefaultClass'])) { + $info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.'; + } + else { + $info['description'] = static::parseTestClassSummary($doc_comment); + } + if (isset($annotations['dependencies'])) { + $info['requires']['module'] = array_map('trim', explode(',', $annotations['dependencies'])); + } + + return $info; + } + + /** + * Parses the phpDoc summary line of a test class. + * + * @param string $doc_comment + * + * @return string + * The parsed phpDoc summary line. An empty string is returned if no summary + * line can be parsed. + */ + public static function parseTestClassSummary($doc_comment) { + // Normalize line endings. + $doc_comment = preg_replace('/\r\n|\r/', '\n', $doc_comment); + // Strip leading and trailing doc block lines. + $doc_comment = substr($doc_comment, 4, -4); + + $lines = explode("\n", $doc_comment); + $summary = []; + // Add every line to the summary until the first empty line or annotation + // is found. + foreach ($lines as $line) { + if (preg_match('/^[ ]*\*$/', $line) || preg_match('/^[ ]*\* \@/', $line)) { + break; + } + $summary[] = trim($line, ' *'); + } + return implode(' ', $summary); + } + + /** + * Parses annotations in the phpDoc of a test class. + * + * @param \ReflectionClass $class + * The reflected test class. + * + * @return array + * An associative array that contains all annotations on the test class; + * typically including: + * - group: A list of @group values. + * - requires: An associative array of @requires values; e.g.: + * - module: A list of Drupal module dependencies that are required to + * exist. + * + * @see PHPUnit_Util_Test::parseTestMethodAnnotations() + * @see http://phpunit.de/manual/current/en/incomplete-and-skipped-tests.html#incomplete-and-skipped-tests.skipping-tests-using-requires + */ + public static function parseTestClassAnnotations(\ReflectionClass $class) { + $annotations = PHPUnit_Util_Test::parseTestMethodAnnotations($class->getName())['class']; + + // @todo Enhance PHPUnit upstream to allow for custom @requires identifiers. + // @see PHPUnit_Util_Test::getRequirements() + // @todo Add support for 'PHP', 'OS', 'function', 'extension'. + // @see https://www.drupal.org/node/1273478 + if (isset($annotations['requires'])) { + foreach ($annotations['requires'] as $i => $value) { + list($type, $value) = explode(' ', $value, 2); + if ($type === 'module') { + $annotations['requires']['module'][$value] = $value; + unset($annotations['requires'][$i]); + } + } + } + return $annotations; + } + + /** + * Determines the phpunit testsuite for a given classname, based on namespace. + * + * @param string $classname + * The test classname. + * + * @return string|false + * The testsuite name or FALSE if its not a phpunit test. + */ + public static function getPhpunitTestSuite($classname) { + if (preg_match('/Drupal\\\\Tests\\\\(\w+)\\\\(\w+)/', $classname, $matches)) { + // This could be an extension test, in which case the first match will be + // the extension name. We assume that lower-case strings are module names. + if (strtolower($matches[1]) == $matches[1]) { + return $matches[2]; + } + return 'Unit'; + } + // Core tests. + elseif (preg_match('/Drupal\\\\(\w*)Tests\\\\/', $classname, $matches)) { + if ($matches[1] == '') { + return 'Unit'; + } + return $matches[1]; + } + return FALSE; + } + + /** + * Returns all available extensions. + * + * @return \Drupal\Core\Extension\Extension[] + * An array of Extension objects, keyed by extension name. + */ + protected function getExtensions() { + $listing = new ExtensionDiscovery($this->root); + // Ensure that tests in all profiles are discovered. + $listing->setProfileDirectories([]); + $extensions = $listing->scan('module', TRUE); + $extensions += $listing->scan('profile', TRUE); + $extensions += $listing->scan('theme', TRUE); + return $extensions; + } + +} diff --git a/core/lib/Drupal/Core/Test/TestRunnerKernel.php b/core/lib/Drupal/Core/Test/TestRunnerKernel.php index b615ce356fa11fb06d5bbb61b90077fe2096d1f9..963a6c4e4d8628fc2fbba46e157e4eae9f72969d 100644 --- a/core/lib/Drupal/Core/Test/TestRunnerKernel.php +++ b/core/lib/Drupal/Core/Test/TestRunnerKernel.php @@ -70,7 +70,11 @@ public function boot() { $this->getContainer()->get('module_handler')->loadAll(); - $this->getContainer()->get('test_discovery')->registerTestNamespaces(); + $test_discovery = new TestDiscovery( + $this->getContainer()->get('app.root'), + $this->getContainer()->get('class_loader') + ); + $test_discovery->registerTestNamespaces(); // Register stream wrappers. $this->getContainer()->get('stream_wrapper_manager')->register(); diff --git a/core/modules/simpletest/simpletest.services.yml b/core/modules/simpletest/simpletest.services.yml index 326cf38f2d371f3093ba82f3a57f40ff3e415fa9..241519795798a63bee19e260ea7f2bcc683afa7f 100644 --- a/core/modules/simpletest/simpletest.services.yml +++ b/core/modules/simpletest/simpletest.services.yml @@ -1,5 +1,7 @@ services: test_discovery: + # @todo Change this service so that it uses \Drupal\Core\Test\TestDiscovery + # in https://www.drupal.org/node/1667822 class: Drupal\simpletest\TestDiscovery arguments: ['@app.root', '@class_loader', '@module_handler'] cache_context.test_discovery: diff --git a/core/modules/simpletest/src/Exception/MissingGroupException.php b/core/modules/simpletest/src/Exception/MissingGroupException.php index 85a0696ca16cbfc5e6b95148b75ba4b091876fc2..c320304c9e1a42114d75e51f1fa9e0d5096c8496 100644 --- a/core/modules/simpletest/src/Exception/MissingGroupException.php +++ b/core/modules/simpletest/src/Exception/MissingGroupException.php @@ -2,8 +2,17 @@ namespace Drupal\simpletest\Exception; +@trigger_error(__NAMESPACE__ . '\\MissingGroupException is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\Exception\MissingGroupException instead. See https://www.drupal.org/node/2949692', E_USER_DEPRECATED); + +use Drupal\Core\Test\Exception\MissingGroupException as CoreMissingGroupException; + /** * Exception thrown when a simpletest class is missing an @group annotation. + * + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use + * \Drupal\Core\Test\Exception\MissingGroupException instead. + * + * @see https://www.drupal.org/node/2949692 */ -class MissingGroupException extends \LogicException { +class MissingGroupException extends CoreMissingGroupException { } diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php index fccb7893fd5fb9f87be51df09ac74b3e26c0d243..9856e7fe4b38e9b9ff98a9482f496f5f96fc3926 100644 --- a/core/modules/simpletest/src/TestBase.php +++ b/core/modules/simpletest/src/TestBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; use Drupal\Core\Test\TestDatabase; +use Drupal\Core\Test\TestDiscovery; use Drupal\Core\Test\TestSetupTrait; use Drupal\Core\Utility\Error; use Drupal\Tests\AssertHelperTrait as BaseAssertHelperTrait; diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php index 39079e17f65f3835e89f17cec472e83afd2d6f6c..8ce8b7de464cbdc75b7257956733131c6e463382 100644 --- a/core/modules/simpletest/src/TestDiscovery.php +++ b/core/modules/simpletest/src/TestDiscovery.php @@ -2,53 +2,27 @@ namespace Drupal\simpletest; +@trigger_error(__NAMESPACE__ . '\\TestDiscovery is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDiscovery instead. See https://www.drupal.org/node/2949692', E_USER_DEPRECATED); + use Doctrine\Common\Reflection\StaticReflectionParser; use Drupal\Component\Annotation\Reflection\MockFileFinder; use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\simpletest\Exception\MissingGroupException; -use PHPUnit_Util_Test; +use Drupal\Core\Test\Exception\MissingGroupException; +use Drupal\Core\Test\TestDiscovery as CoreTestDiscovery; /** * Discovers available tests. + * + * This class provides backwards compatibility for code which uses the legacy + * \Drupal\simpletest\TestDiscovery. + * + * @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use + * \Drupal\Core\Test\TestDiscovery instead. + * + * @see https://www.drupal.org/node/2949692 */ -class TestDiscovery { - - /** - * The class loader. - * - * @var \Composer\Autoload\ClassLoader - */ - protected $classLoader; - - /** - * Statically cached list of test classes. - * - * @var array - */ - protected $testClasses; - - /** - * Cached map of all test namespaces to respective directories. - * - * @var array - */ - protected $testNamespaces; - - /** - * Cached list of all available extension names, keyed by extension type. - * - * @var array - */ - protected $availableExtensions; - - /** - * The app root. - * - * @var string - */ - protected $root; +class TestDiscovery extends CoreTestDiscovery { /** * The module handler. @@ -70,66 +44,17 @@ class TestDiscovery { * The module handler. */ public function __construct($root, $class_loader, ModuleHandlerInterface $module_handler) { - $this->root = $root; - $this->classLoader = $class_loader; + parent::__construct($root, $class_loader); $this->moduleHandler = $module_handler; } - /** - * Registers test namespaces of all extensions and core test classes. - * - * @return array - * An associative array whose keys are PSR-4 namespace prefixes and whose - * values are directory names. - */ - public function registerTestNamespaces() { - if (isset($this->testNamespaces)) { - return $this->testNamespaces; - } - $this->testNamespaces = []; - - $existing = $this->classLoader->getPrefixesPsr4(); - - // Add PHPUnit test namespaces of Drupal core. - $this->testNamespaces['Drupal\\Tests\\'] = [$this->root . '/core/tests/Drupal/Tests']; - $this->testNamespaces['Drupal\\KernelTests\\'] = [$this->root . '/core/tests/Drupal/KernelTests']; - $this->testNamespaces['Drupal\\FunctionalTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalTests']; - $this->testNamespaces['Drupal\\FunctionalJavascriptTests\\'] = [$this->root . '/core/tests/Drupal/FunctionalJavascriptTests']; - - $this->availableExtensions = []; - foreach ($this->getExtensions() as $name => $extension) { - $this->availableExtensions[$extension->getType()][$name] = $name; - - $base_path = $this->root . '/' . $extension->getPath(); - - // Add namespace of disabled/uninstalled extensions. - if (!isset($existing["Drupal\\$name\\"])) { - $this->classLoader->addPsr4("Drupal\\$name\\", "$base_path/src"); - } - // Add Simpletest test namespace. - $this->testNamespaces["Drupal\\$name\\Tests\\"][] = "$base_path/src/Tests"; - - // Add PHPUnit test namespaces. - $this->testNamespaces["Drupal\\Tests\\$name\\Unit\\"][] = "$base_path/tests/src/Unit"; - $this->testNamespaces["Drupal\\Tests\\$name\\Kernel\\"][] = "$base_path/tests/src/Kernel"; - $this->testNamespaces["Drupal\\Tests\\$name\\Functional\\"][] = "$base_path/tests/src/Functional"; - $this->testNamespaces["Drupal\\Tests\\$name\\FunctionalJavascript\\"][] = "$base_path/tests/src/FunctionalJavascript"; - - // Add discovery for traits which are shared between different test - // suites. - $this->testNamespaces["Drupal\\Tests\\$name\\Traits\\"][] = "$base_path/tests/src/Traits"; - } - - foreach ($this->testNamespaces as $prefix => $paths) { - $this->classLoader->addPsr4($prefix, $paths); - } - - return $this->testNamespaces; - } - /** * Discovers all available tests in all extensions. * + * This method is a near-duplicate of + * \Drupal\Core\Tests\TestDiscovery::getTestClasses(). It exists so that we + * can provide a BC invocation of hook_simpletest_alter(). + * * @param string $extension * (optional) The name of an extension to limit discovery to; e.g., 'node'. * @param string[] $types @@ -149,9 +74,6 @@ public function registerTestNamespaces() { * ), * ); * @endcode - * - * @todo Remove singular grouping; retain list of groups in 'group' key. - * @see https://www.drupal.org/node/2296615 */ public function getTestClasses($extension = NULL, array $types = []) { if (!isset($extension) && empty($types)) { @@ -225,277 +147,4 @@ public function getTestClasses($extension = NULL, array $types = []) { return $list; } - /** - * Discovers all class files in all available extensions. - * - * @param string $extension - * (optional) The name of an extension to limit discovery to; e.g., 'node'. - * - * @return array - * A classmap containing all discovered class files; i.e., a map of - * fully-qualified classnames to pathnames. - */ - public function findAllClassFiles($extension = NULL) { - $classmap = []; - $namespaces = $this->registerTestNamespaces(); - if (isset($extension)) { - // Include tests in the \Drupal\Tests\{$extension} namespace. - $pattern = "/Drupal\\\(Tests\\\)?$extension\\\/"; - $namespaces = array_intersect_key($namespaces, array_flip(preg_grep($pattern, array_keys($namespaces)))); - } - foreach ($namespaces as $namespace => $paths) { - foreach ($paths as $path) { - if (!is_dir($path)) { - continue; - } - $classmap += static::scanDirectory($namespace, $path); - } - } - return $classmap; - } - - /** - * Scans a given directory for class files. - * - * @param string $namespace_prefix - * The namespace prefix to use for discovered classes. Must contain a - * trailing namespace separator (backslash). - * For example: 'Drupal\\node\\Tests\\' - * @param string $path - * The directory path to scan. - * For example: '/path/to/drupal/core/modules/node/tests/src' - * - * @return array - * An associative array whose keys are fully-qualified class names and whose - * values are corresponding filesystem pathnames. - * - * @throws \InvalidArgumentException - * If $namespace_prefix does not end in a namespace separator (backslash). - * - * @todo Limit to '*Test.php' files (~10% less files to reflect/introspect). - * @see https://www.drupal.org/node/2296635 - */ - public static function scanDirectory($namespace_prefix, $path) { - if (substr($namespace_prefix, -1) !== '\\') { - throw new \InvalidArgumentException("Namespace prefix for $path must contain a trailing namespace separator."); - } - $flags = \FilesystemIterator::UNIX_PATHS; - $flags |= \FilesystemIterator::SKIP_DOTS; - $flags |= \FilesystemIterator::FOLLOW_SYMLINKS; - $flags |= \FilesystemIterator::CURRENT_AS_SELF; - $flags |= \FilesystemIterator::KEY_AS_FILENAME; - - $iterator = new \RecursiveDirectoryIterator($path, $flags); - $filter = new \RecursiveCallbackFilterIterator($iterator, function ($current, $file_name, $iterator) { - if ($iterator->hasChildren()) { - return TRUE; - } - // We don't want to discover abstract TestBase classes, traits or - // interfaces. They can be deprecated and will call @trigger_error() - // during discovery. - return substr($file_name, -4) === '.php' && - substr($file_name, -12) !== 'TestBase.php' && - substr($file_name, -9) !== 'Trait.php' && - substr($file_name, -13) !== 'Interface.php'; - }); - $files = new \RecursiveIteratorIterator($filter); - $classes = []; - foreach ($files as $fileinfo) { - $class = $namespace_prefix; - if ('' !== $subpath = $fileinfo->getSubPath()) { - $class .= strtr($subpath, '/', '\\') . '\\'; - } - $class .= $fileinfo->getBasename('.php'); - $classes[$class] = $fileinfo->getPathname(); - } - return $classes; - } - - /** - * Retrieves information about a test class for UI purposes. - * - * @param string $classname - * The test classname. - * @param string $doc_comment - * (optional) The class PHPDoc comment. If not passed in reflection will be - * used but this is very expensive when parsing all the test classes. - * - * @return array - * An associative array containing: - * - name: The test class name. - * - description: The test (PHPDoc) summary. - * - group: The test's first @group (parsed from PHPDoc annotations). - * - groups: All of the test's @group annotations, as an array (parsed from - * PHPDoc annotations). - * - requires: An associative array containing test requirements parsed from - * PHPDoc annotations: - * - module: List of Drupal module extension names the test depends on. - * - * @throws \Drupal\simpletest\Exception\MissingGroupException - * If the class does not have a @group annotation. - */ - public static function getTestInfo($classname, $doc_comment = NULL) { - if ($doc_comment === NULL) { - $reflection = new \ReflectionClass($classname); - $doc_comment = $reflection->getDocComment(); - } - $info = [ - 'name' => $classname, - ]; - $annotations = []; - // Look for annotations, allow an arbitrary amount of spaces before the - // * but nothing else. - preg_match_all('/^[ ]*\* \@([^\s]*) (.*$)/m', $doc_comment, $matches); - if (isset($matches[1])) { - foreach ($matches[1] as $key => $annotation) { - // For historical reasons, there is a single-value 'group' result key - // and a 'groups' key as an array. - if ($annotation === 'group') { - $annotations['groups'][] = $matches[2][$key]; - } - if (!empty($annotations[$annotation])) { - // Only @group is allowed to have more than one annotation, in the - // 'groups' key. Other annotations only have one value per key. - continue; - } - $annotations[$annotation] = $matches[2][$key]; - } - } - - if (empty($annotations['group'])) { - // Concrete tests must have a group. - throw new MissingGroupException(sprintf('Missing @group annotation in %s', $classname)); - } - $info['group'] = $annotations['group']; - $info['groups'] = $annotations['groups']; - - // Sort out PHPUnit-runnable tests by type. - if ($testsuite = static::getPhpunitTestSuite($classname)) { - $info['type'] = 'PHPUnit-' . $testsuite; - } - else { - $info['type'] = 'Simpletest'; - } - - if (!empty($annotations['coversDefaultClass'])) { - $info['description'] = 'Tests ' . $annotations['coversDefaultClass'] . '.'; - } - else { - $info['description'] = static::parseTestClassSummary($doc_comment); - } - if (isset($annotations['dependencies'])) { - $info['requires']['module'] = array_map('trim', explode(',', $annotations['dependencies'])); - } - - return $info; - } - - /** - * Parses the phpDoc summary line of a test class. - * - * @param string $doc_comment - * - * @return string - * The parsed phpDoc summary line. An empty string is returned if no summary - * line can be parsed. - */ - public static function parseTestClassSummary($doc_comment) { - // Normalize line endings. - $doc_comment = preg_replace('/\r\n|\r/', '\n', $doc_comment); - // Strip leading and trailing doc block lines. - $doc_comment = substr($doc_comment, 4, -4); - - $lines = explode("\n", $doc_comment); - $summary = []; - // Add every line to the summary until the first empty line or annotation - // is found. - foreach ($lines as $line) { - if (preg_match('/^[ ]*\*$/', $line) || preg_match('/^[ ]*\* \@/', $line)) { - break; - } - $summary[] = trim($line, ' *'); - } - return implode(' ', $summary); - } - - /** - * Parses annotations in the phpDoc of a test class. - * - * @param \ReflectionClass $class - * The reflected test class. - * - * @return array - * An associative array that contains all annotations on the test class; - * typically including: - * - group: A list of @group values. - * - requires: An associative array of @requires values; e.g.: - * - module: A list of Drupal module dependencies that are required to - * exist. - * - * @see PHPUnit_Util_Test::parseTestMethodAnnotations() - * @see http://phpunit.de/manual/current/en/incomplete-and-skipped-tests.html#incomplete-and-skipped-tests.skipping-tests-using-requires - */ - public static function parseTestClassAnnotations(\ReflectionClass $class) { - $annotations = PHPUnit_Util_Test::parseTestMethodAnnotations($class->getName())['class']; - - // @todo Enhance PHPUnit upstream to allow for custom @requires identifiers. - // @see PHPUnit_Util_Test::getRequirements() - // @todo Add support for 'PHP', 'OS', 'function', 'extension'. - // @see https://www.drupal.org/node/1273478 - if (isset($annotations['requires'])) { - foreach ($annotations['requires'] as $i => $value) { - list($type, $value) = explode(' ', $value, 2); - if ($type === 'module') { - $annotations['requires']['module'][$value] = $value; - unset($annotations['requires'][$i]); - } - } - } - return $annotations; - } - - /** - * Determines the phpunit testsuite for a given classname, based on namespace. - * - * @param string $classname - * The test classname. - * - * @return string|false - * The testsuite name or FALSE if its not a phpunit test. - */ - public static function getPhpunitTestSuite($classname) { - if (preg_match('/Drupal\\\\Tests\\\\(\w+)\\\\(\w+)/', $classname, $matches)) { - // This could be an extension test, in which case the first match will be - // the extension name. We assume that lower-case strings are module names. - if (strtolower($matches[1]) == $matches[1]) { - return $matches[2]; - } - return 'Unit'; - } - // Core tests. - elseif (preg_match('/Drupal\\\\(\w*)Tests\\\\/', $classname, $matches)) { - if ($matches[1] == '') { - return 'Unit'; - } - return $matches[1]; - } - return FALSE; - } - - /** - * Returns all available extensions. - * - * @return \Drupal\Core\Extension\Extension[] - * An array of Extension objects, keyed by extension name. - */ - protected function getExtensions() { - $listing = new ExtensionDiscovery($this->root); - // Ensure that tests in all profiles are discovered. - $listing->setProfileDirectories([]); - $extensions = $listing->scan('module', TRUE); - $extensions += $listing->scan('profile', TRUE); - $extensions += $listing->scan('theme', TRUE); - return $extensions; - } - } diff --git a/core/modules/simpletest/src/Tests/BrokenSetUpTest.php b/core/modules/simpletest/src/Tests/BrokenSetUpTest.php index 28fd31beb3653d2194064a32831e670afbd1f0c9..fab44d00c820b62e54c823ec02bbe344f998f433 100644 --- a/core/modules/simpletest/src/Tests/BrokenSetUpTest.php +++ b/core/modules/simpletest/src/Tests/BrokenSetUpTest.php @@ -13,6 +13,8 @@ * properly are skipped. * * @group WebTestBase + * @group legacy + * * @see \Drupal\simpletest\WebTestBase */ class BrokenSetUpTest extends WebTestBase { diff --git a/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php b/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php index ae9d6f7291ceb3dcef6f36af1505300154ea8980..4bf7c7b6cc0ddf678e298a6e757f495f90164c7e 100644 --- a/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php +++ b/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php @@ -9,6 +9,7 @@ * * @group simpletest * @group WebTestBase + * @group legacy */ class MissingCheckedRequirementsTest extends WebTestBase { diff --git a/core/modules/simpletest/src/Tests/SimpleTestTest.php b/core/modules/simpletest/src/Tests/SimpleTestTest.php index c26fbdcbfeec9e77cad04c85db53b53dedb370cd..ea10bcef71249be41b572b9ed2578d3c47c45afb 100644 --- a/core/modules/simpletest/src/Tests/SimpleTestTest.php +++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php @@ -14,6 +14,7 @@ * * @group simpletest * @group WebTestBase + * @group legacy */ class SimpleTestTest extends WebTestBase { diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index 0d7cd953ae3c262faeb8a6b30163b98d556ccc36..3f0ab2ff131f20198671541f4ba0d3a28f7bb8ba 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -14,6 +14,7 @@ use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\Test\AssertMailTrait; use Drupal\Core\Test\FunctionalTestSetupTrait; +use Drupal\Core\Test\TestDiscovery; use Drupal\Core\Url; use Drupal\KernelTests\AssertContentTrait as CoreAssertContentTrait; use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; @@ -705,7 +706,7 @@ protected function curlHeaderCallback($curlHandler, $header) { if (getenv('SYMFONY_DEPRECATIONS_HELPER') !== 'disabled') { $message = (string) $parameters[0]; $test_info = TestDiscovery::getTestInfo(get_called_class()); - if ($test_info['group'] !== 'legacy' && !in_array($message, DeprecationListenerTrait::getSkippedDeprecations())) { + if (!in_array('legacy', $test_info['groups']) && !in_array($message, DeprecationListenerTrait::getSkippedDeprecations())) { call_user_func_array([&$this, 'error'], $parameters); } } diff --git a/core/modules/simpletest/tests/src/Functional/OtherInstallationProfileTestsTest.php b/core/modules/simpletest/tests/src/Functional/OtherInstallationProfileTestsTest.php index 972495f575962b1ac41c22aeb600c7503d82b5d7..8a9ba18281a0359696291d328b982ee285054721 100644 --- a/core/modules/simpletest/tests/src/Functional/OtherInstallationProfileTestsTest.php +++ b/core/modules/simpletest/tests/src/Functional/OtherInstallationProfileTestsTest.php @@ -3,11 +3,14 @@ namespace Drupal\Tests\simpletest\Functional; use Drupal\Tests\BrowserTestBase; +use Drupal\Core\Url; /** * Verifies that tests in other installation profiles are found. * * @group simpletest + * @group legacy + * * @see \Drupal\simpletest\Tests\InstallationProfileModuleTestsTest */ class OtherInstallationProfileTestsTest extends BrowserTestBase { @@ -51,11 +54,13 @@ protected function setUp() { /** * Tests that tests located in another installation profile appear. + * + * @expectedDeprecation Drupal\simpletest\TestDiscovery is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDiscovery instead. See https://www.drupal.org/node/2949692 */ public function testOtherInstallationProfile() { // Assert the existence of a test in a different installation profile than // the current. - $this->drupalGet('admin/config/development/testing'); + $this->drupalGet(Url::fromRoute('simpletest.test_form')); $this->assertText('Tests Standard installation profile expectations.'); // Assert the existence of a test for a module in a different installation diff --git a/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php b/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php index f91cb6d5077dc6cf229445d0a70e328d029c44b2..9103cb8aad6dab712f5591ad342109610b7b2cd9 100644 --- a/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php +++ b/core/modules/simpletest/tests/src/Functional/SimpletestUiTest.php @@ -12,6 +12,7 @@ * * @group #slow * @group simpletest + * @group legacy */ class SimpletestUiTest extends BrowserTestBase { @@ -30,6 +31,8 @@ protected function setUp() { /** * Tests that unit, kernel, and functional tests work through the UI. + * + * @expectedDeprecation Drupal\simpletest\TestDiscovery is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDiscovery instead. See https://www.drupal.org/node/2949692 */ public function testTestingThroughUI() { $url = Url::fromRoute('simpletest.test_form'); diff --git a/core/modules/simpletest/tests/src/Functional/ThroughUITest.php b/core/modules/simpletest/tests/src/Functional/ThroughUITest.php index 48737433f366d208d907548f1ff1eb411342a654..1e325e4c55241656442b5adf996ffbada2fc8297 100644 --- a/core/modules/simpletest/tests/src/Functional/ThroughUITest.php +++ b/core/modules/simpletest/tests/src/Functional/ThroughUITest.php @@ -10,6 +10,7 @@ * @see \Drupal\simpletest\Tests::testTestingThroughUI() * * @group simpletest + * @group legacy */ class ThroughUITest extends BrowserTestBase { diff --git a/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php b/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php index 25b2af92e4398aba6c4f7ba6fa96994ccb990409..00d83581cfd66f1c5160975fe56f69df90bbff79 100644 --- a/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php +++ b/core/modules/simpletest/tests/src/Kernel/Cache/Context/TestDiscoveryCacheContextTest.php @@ -8,6 +8,7 @@ /** * @group simpletest + * @group legacy */ class TestDiscoveryCacheContextTest extends KernelTestBase { @@ -18,6 +19,8 @@ class TestDiscoveryCacheContextTest extends KernelTestBase { /** * Tests that test context hashes are unique. + * + * @expectedDeprecation Drupal\simpletest\TestDiscovery is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDiscovery instead. See https://www.drupal.org/node/2949692 */ public function testContext() { // Mock test discovery. diff --git a/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php b/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php index 384f8c1b9b36a79d04a379dc05a5303a4edc0255..36c3d43dc7c2af336579e304813d7d64b25e1a42 100644 --- a/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php +++ b/core/modules/simpletest/tests/src/Kernel/SimpletestDeprecationTest.php @@ -3,9 +3,10 @@ namespace Drupal\Tests\simpletest\Kernel; use Drupal\KernelTests\KernelTestBase; +use Drupal\simpletest\TestDiscovery; /** - * Verify deprecation of simpletest. + * Verify deprecations within the simpletest module. * * @group simpletest * @group legacy @@ -25,4 +26,12 @@ public function testDeprecatedFunctions() { simpletest_classloader_register(); } + /** + * @expectedDeprecation Drupal\simpletest\TestDiscovery is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\Core\Test\TestDiscovery instead. See https://www.drupal.org/node/2949692 + * @expectedDeprecation The "test_discovery" service relies on the deprecated "Drupal\simpletest\TestDiscovery" class. It should either be deprecated or its implementation upgraded. + */ + public function testDeprecatedServices() { + $this->assertInstanceOf(TestDiscovery::class, $this->container->get('test_discovery')); + } + } diff --git a/core/modules/simpletest/tests/src/Kernel/TestDiscoveryDeprecationTest.php b/core/modules/simpletest/tests/src/Kernel/TestDiscoveryDeprecationTest.php index 6d4f44888a046f8d6dc182d015512c45d2791eff..c9c781d278cabaa32bbcf7f770c22709507bb07a 100644 --- a/core/modules/simpletest/tests/src/Kernel/TestDiscoveryDeprecationTest.php +++ b/core/modules/simpletest/tests/src/Kernel/TestDiscoveryDeprecationTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\simpletest\Kernel; use Drupal\KernelTests\KernelTestBase; +use Drupal\simpletest\TestDiscovery; /** * @group simpletest @@ -22,11 +23,13 @@ class TestDiscoveryDeprecationTest extends KernelTestBase { * @covers ::getTestClasses */ public function testHookSimpletestAlter() { + $test_discovery = $this->container->get('test_discovery'); + + $this->assertEquals(TestDiscovery::class, get_class($test_discovery)); + // The simpletest_test module implements hook_simpletest_alter(), which // should trigger a deprecation error during getTestClasses(). - $this->assertNotEmpty( - $this->container->get('test_discovery')->getTestClasses() - ); + $this->assertNotEmpty($test_discovery->getTestClasses()); } } diff --git a/core/modules/simpletest/tests/src/Unit/TestDiscoveryTest.php b/core/modules/simpletest/tests/src/Unit/TestDiscoveryTest.php index 588260acc512a991e0dbc0b3e1e422c82d30d2ab..477f74fbe732e04edf0a0752f5f4144cdc8b065c 100644 --- a/core/modules/simpletest/tests/src/Unit/TestDiscoveryTest.php +++ b/core/modules/simpletest/tests/src/Unit/TestDiscoveryTest.php @@ -7,296 +7,17 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\simpletest\Exception\MissingGroupException; use Drupal\simpletest\TestDiscovery; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; /** * @coversDefaultClass \Drupal\simpletest\TestDiscovery - * @group simpletest - */ -class TestDiscoveryTest extends UnitTestCase { - - /** - * @covers ::getTestInfo - * @dataProvider infoParserProvider - */ - public function testTestInfoParser($expected, $classname, $doc_comment = NULL) { - $info = TestDiscovery::getTestInfo($classname, $doc_comment); - $this->assertEquals($expected, $info); - } - - public function infoParserProvider() { - // A module provided unit test. - $tests[] = [ - // Expected result. - [ - 'name' => static::class, - 'group' => 'simpletest', - 'groups' => ['simpletest'], - 'description' => 'Tests \Drupal\simpletest\TestDiscovery.', - 'type' => 'PHPUnit-Unit', - ], - // Classname. - static::class, - ]; - - // A core unit test. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\Tests\Core\DrupalTest', - 'group' => 'DrupalTest', - 'groups' => ['DrupalTest'], - 'description' => 'Tests \Drupal.', - 'type' => 'PHPUnit-Unit', - ], - // Classname. - 'Drupal\Tests\Core\DrupalTest', - ]; - - // Functional PHPUnit test. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\FunctionalTests\BrowserTestBaseTest', - 'group' => 'browsertestbase', - 'groups' => ['browsertestbase'], - 'description' => 'Tests BrowserTestBase functionality.', - 'type' => 'PHPUnit-Functional', - ], - // Classname. - 'Drupal\FunctionalTests\BrowserTestBaseTest', - ]; - - // kernel PHPUnit test. - $tests['phpunit-kernel'] = [ - // Expected result. - [ - 'name' => '\Drupal\Tests\file\Kernel\FileItemValidationTest', - 'group' => 'file', - 'groups' => ['file'], - 'description' => 'Tests that files referenced in file and image fields are always validated.', - 'type' => 'PHPUnit-Kernel', - ], - // Classname. - '\Drupal\Tests\file\Kernel\FileItemValidationTest', - ]; - - // Simpletest classes can not be autoloaded in a PHPUnit test, therefore - // provide a docblock. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'group' => 'simpletest', - 'groups' => ['simpletest'], - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @group simpletest - */ - ", - ]; - - // Test with a different amount of leading spaces. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'group' => 'simpletest', - 'groups' => ['simpletest'], - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @group simpletest - */ - */ - ", - ]; - - // Make sure that a "* @" inside a string does not get parsed as an - // annotation. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'group' => 'simpletest', - 'groups' => ['simpletest'], - 'description' => 'Tests the Simpletest UI internal browser. * @', - 'type' => 'Simpletest', - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. * @ - * - * @group simpletest - */ - ", - ]; - - // Multiple @group annotations. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'group' => 'Test', - 'groups' => ['Test', 'simpletest'], - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @group Test - * @group simpletest - */ - ", - ]; - - // A great number of @group annotations. - $tests['many-group-annotations'] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'group' => 'Test', - 'groups' => ['Test', 'simpletest', 'another', 'more', 'many', 'enough', 'whoa'], - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @group Test - * @group simpletest - * @group another - * @group more - * @group many - * @group enough - * @group whoa - */ - ", - ]; - - // @dependencies annotation. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - 'requires' => ['module' => ['test']], - 'group' => 'simpletest', - 'groups' => ['simpletest'], - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @dependencies test - * @group simpletest - */ - ", - ]; - - // Multiple @dependencies annotation. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'description' => 'Tests the Simpletest UI internal browser.', - 'type' => 'Simpletest', - 'requires' => ['module' => ['test', 'test1', 'test2']], - 'group' => 'simpletest', - 'groups' => ['simpletest'], - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. - * - * @dependencies test, test1, test2 - * @group simpletest - */ - ", - ]; - - // Multi-line summary line. - $tests[] = [ - // Expected result. - [ - 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', - 'description' => 'Tests the Simpletest UI internal browser. And the summary line continues an there is no gap to the annotation.', - 'type' => 'Simpletest', - 'group' => 'simpletest', - 'groups' => ['simpletest'], - ], - // Classname. - 'Drupal\simpletest\Tests\ExampleSimpleTest', - // Doc block. - "/** - * Tests the Simpletest UI internal browser. And the summary line continues an - * there is no gap to the annotation. * * @group simpletest + * @group legacy */ - ", - ]; - return $tests; - } - - /** - * @covers ::getTestInfo - */ - public function testTestInfoParserMissingGroup() { - $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; - $doc_comment = <<<EOT -/** - * Bulk delete storages and fields, and clean up afterwards. - */ -EOT; - $this->expectException(MissingGroupException::class); - $this->expectExceptionMessage('Missing @group annotation in Drupal\KernelTests\field\BulkDeleteTest'); - TestDiscovery::getTestInfo($classname, $doc_comment); - } - - /** - * @covers ::getTestInfo - */ - public function testTestInfoParserMissingSummary() { - $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; - $doc_comment = <<<EOT -/** - * @group field - */ -EOT; - $info = TestDiscovery::getTestInfo($classname, $doc_comment); - $this->assertEmpty($info['description']); - } +class TestDiscoveryTest extends UnitTestCase { protected function setupVfsWithTestClasses() { vfsStream::setup('drupal'); @@ -363,19 +84,34 @@ class FunctionalExampleTest {} } /** - * @covers ::getTestClasses + * Mock a TestDiscovery object to return specific extension values. */ - public function testGetTestClasses() { - $this->setupVfsWithTestClasses(); + protected function getTestDiscoveryMock($app_root, $extensions) { $class_loader = $this->prophesize(ClassLoader::class); $module_handler = $this->prophesize(ModuleHandlerInterface::class); - $test_discovery = new TestTestDiscovery('vfs://drupal', $class_loader->reveal(), $module_handler->reveal()); + $test_discovery = $this->getMockBuilder(TestDiscovery::class) + ->setConstructorArgs([$app_root, $class_loader->reveal(), $module_handler->reveal()]) + ->setMethods(['getExtensions']) + ->getMock(); + + $test_discovery->expects($this->any()) + ->method('getExtensions') + ->willReturn($extensions); + return $test_discovery; + } + + /** + * @covers ::getTestClasses + */ + public function testGetTestClasses() { + $this->setupVfsWithTestClasses(); $extensions = [ 'test_module' => new Extension('vfs://drupal', 'module', 'modules/test_module/test_module.info.yml'), ]; - $test_discovery->setExtensions($extensions); + $test_discovery = $this->getTestDiscoveryMock('vfs://drupal', $extensions); + $result = $test_discovery->getTestClasses(); $this->assertCount(3, $result); $this->assertEquals([ @@ -421,16 +157,12 @@ public function testGetTestClasses() { */ public function testGetTestClassesWithSelectedTypes() { $this->setupVfsWithTestClasses(); - $class_loader = $this->prophesize(ClassLoader::class); - $module_handler = $this->prophesize(ModuleHandlerInterface::class); - - $test_discovery = new TestTestDiscovery('vfs://drupal', $class_loader->reveal(), $module_handler->reveal()); - $extensions = [ 'test_module' => new Extension('vfs://drupal', 'module', 'modules/test_module/test_module.info.yml'), 'test_profile_module' => new Extension('vfs://drupal', 'profile', 'profiles/test_profile/modules/test_profile_module/test_profile_module.info.yml'), ]; - $test_discovery->setExtensions($extensions); + $test_discovery = $this->getTestDiscoveryMock('vfs://drupal', $extensions); + $result = $test_discovery->getTestClasses(NULL, ['PHPUnit-Kernel']); $this->assertCount(4, $result); $this->assertEquals([ @@ -495,80 +227,4 @@ public function testGetTestsInProfiles() { $this->assertEquals($expected, $result); } - /** - * @covers ::getPhpunitTestSuite - * @dataProvider providerTestGetPhpunitTestSuite - */ - public function testGetPhpunitTestSuite($classname, $expected) { - $this->assertEquals($expected, TestDiscovery::getPhpunitTestSuite($classname)); - } - - public function providerTestGetPhpunitTestSuite() { - $data = []; - $data['simpletest-webtest'] = ['\Drupal\rest\Tests\NodeTest', FALSE]; - $data['simpletest-kerneltest'] = ['\Drupal\hal\Tests\FileNormalizeTest', FALSE]; - $data['module-unittest'] = [static::class, 'Unit']; - $data['module-kerneltest'] = ['\Drupal\KernelTests\Core\Theme\TwigMarkupInterfaceTest', 'Kernel']; - $data['module-functionaltest'] = ['\Drupal\FunctionalTests\BrowserTestBaseTest', 'Functional']; - $data['module-functionaljavascripttest'] = ['\Drupal\Tests\toolbar\FunctionalJavascript\ToolbarIntegrationTest', 'FunctionalJavascript']; - $data['core-unittest'] = ['\Drupal\Tests\ComposerIntegrationTest', 'Unit']; - $data['core-unittest2'] = ['Drupal\Tests\Core\DrupalTest', 'Unit']; - $data['core-unittest3'] = ['Drupal\Tests\Scripts\TestSiteApplicationTest', 'Unit']; - $data['core-kerneltest'] = ['\Drupal\KernelTests\KernelTestBaseTest', 'Kernel']; - $data['core-functionaltest'] = ['\Drupal\FunctionalTests\ExampleTest', 'Functional']; - $data['core-functionaljavascripttest'] = ['\Drupal\FunctionalJavascriptTests\ExampleTest', 'FunctionalJavascript']; - - return $data; - } - - /** - * Ensure that classes are not reflected when the docblock is empty. - * - * @covers ::getTestInfo - */ - public function testGetTestInfoEmptyDocblock() { - // If getTestInfo() performed reflection, it won't be able to find the - // class we asked it to analyze, so it will throw a ReflectionException. - // We want to make sure it didn't do that, because we already did some - // analysis and already have an empty docblock. getTestInfo() will throw - // MissingGroupException because the annotation is empty. - $this->expectException(MissingGroupException::class); - TestDiscovery::getTestInfo('Drupal\Tests\simpletest\ThisTestDoesNotExistTest', ''); - } - - /** - * Ensure TestDiscovery::scanDirectory() ignores certain abstract file types. - * - * @covers ::scanDirectory - */ - public function testScanDirectoryNoAbstract() { - $this->setupVfsWithTestClasses(); - $files = TestDiscovery::scanDirectory('Drupal\\Tests\\test_module\\Kernel\\', vfsStream::url('drupal/modules/test_module/tests/src/Kernel')); - $this->assertNotEmpty($files); - $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTestBase', $files); - $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTrait', $files); - $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleInterface', $files); - $this->assertArrayHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTest3', $files); - } - -} - -class TestTestDiscovery extends TestDiscovery { - - /** - * @var \Drupal\Core\Extension\Extension[] - */ - protected $extensions = []; - - public function setExtensions(array $extensions) { - $this->extensions = $extensions; - } - - /** - * {@inheritdoc} - */ - protected function getExtensions() { - return $this->extensions; - } - } diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 9b39ae46948471495332dddc15f0ba93702e183c..7b2a0088b6f79f7da2e9d6ac16719c2ae4fe1eca 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -18,7 +18,7 @@ use Drupal\Core\Test\TestRunnerKernel; use Drupal\simpletest\Form\SimpletestResultsForm; use Drupal\simpletest\TestBase; -use Drupal\simpletest\TestDiscovery; +use Drupal\Core\Test\TestDiscovery; use PHPUnit\Framework\TestCase; use PHPUnit\Runner\Version; use Symfony\Component\HttpFoundation\Request; @@ -65,6 +65,8 @@ echo "\nAvailable test groups & classes\n"; echo "-------------------------------\n\n"; try { + // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for + // hook_simpletest_alter(). $groups = \Drupal::service('test_discovery')->getTestClasses($args['module']); } catch (Exception $e) { @@ -95,6 +97,8 @@ // List all files which could be run as tests. $test_discovery = NULL; try { + // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for + // hook_simpletest_alter(). $test_discovery = \Drupal::service('test_discovery'); } catch (Exception $e) { @@ -995,6 +999,8 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) { function simpletest_script_get_test_list() { global $args; + // @todo Use \Drupal\Core\Test\TestDiscovery when we no longer need BC for + // hook_simpletest_alter(). /** $test_discovery \Drupal\simpletest\TestDiscovery */ $test_discovery = \Drupal::service('test_discovery'); $types_processed = empty($args['types']); diff --git a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..65266c9d8af8dde2a88260fd7e3f6a49b9b76365 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php @@ -0,0 +1,565 @@ +<?php + +namespace Drupal\Tests\Core\Test; + +use Composer\Autoload\ClassLoader; +use Drupal\Core\DependencyInjection\Container; +use Drupal\Core\DrupalKernel; +use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Test\Exception\MissingGroupException; +use Drupal\Core\Test\TestDiscovery; +use Drupal\Tests\UnitTestCase; +use org\bovigo\vfs\vfsStream; + +/** + * @coversDefaultClass \Drupal\Core\Test\TestDiscovery + * @group Test + */ +class TestDiscoveryTest extends UnitTestCase { + + /** + * @covers ::getTestInfo + * @dataProvider infoParserProvider + */ + public function testTestInfoParser($expected, $classname, $doc_comment = NULL) { + $info = TestDiscovery::getTestInfo($classname, $doc_comment); + $this->assertEquals($expected, $info); + } + + public function infoParserProvider() { + // A module provided unit test. + $tests[] = [ + // Expected result. + [ + 'name' => static::class, + 'group' => 'Test', + 'groups' => ['Test'], + 'description' => 'Tests \Drupal\Core\Test\TestDiscovery.', + 'type' => 'PHPUnit-Unit', + ], + // Classname. + static::class, + ]; + + // A core unit test. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\Tests\Core\DrupalTest', + 'group' => 'DrupalTest', + 'groups' => ['DrupalTest'], + 'description' => 'Tests \Drupal.', + 'type' => 'PHPUnit-Unit', + ], + // Classname. + 'Drupal\Tests\Core\DrupalTest', + ]; + + // Functional PHPUnit test. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\FunctionalTests\BrowserTestBaseTest', + 'group' => 'browsertestbase', + 'groups' => ['browsertestbase'], + 'description' => 'Tests BrowserTestBase functionality.', + 'type' => 'PHPUnit-Functional', + ], + // Classname. + 'Drupal\FunctionalTests\BrowserTestBaseTest', + ]; + + // kernel PHPUnit test. + $tests['phpunit-kernel'] = [ + // Expected result. + [ + 'name' => '\Drupal\Tests\file\Kernel\FileItemValidationTest', + 'group' => 'file', + 'groups' => ['file'], + 'description' => 'Tests that files referenced in file and image fields are always validated.', + 'type' => 'PHPUnit-Kernel', + ], + // Classname. + '\Drupal\Tests\file\Kernel\FileItemValidationTest', + ]; + + // Simpletest classes can not be autoloaded in a PHPUnit test, therefore + // provide a docblock. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'group' => 'simpletest', + 'groups' => ['simpletest'], + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @group simpletest + */ + ", + ]; + + // Test with a different amount of leading spaces. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'group' => 'simpletest', + 'groups' => ['simpletest'], + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @group simpletest + */ + */ + ", + ]; + + // Make sure that a "* @" inside a string does not get parsed as an + // annotation. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'group' => 'simpletest', + 'groups' => ['simpletest'], + 'description' => 'Tests the Simpletest UI internal browser. * @', + 'type' => 'Simpletest', + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. * @ + * + * @group simpletest + */ + ", + ]; + + // Multiple @group annotations. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'group' => 'Test', + 'groups' => ['Test', 'simpletest'], + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @group Test + * @group simpletest + */ + ", + ]; + + // A great number of @group annotations. + $tests['many-group-annotations'] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'group' => 'Test', + 'groups' => ['Test', 'simpletest', 'another', 'more', 'many', 'enough', 'whoa'], + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @group Test + * @group simpletest + * @group another + * @group more + * @group many + * @group enough + * @group whoa + */ + ", + ]; + + // @dependencies annotation. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + 'requires' => ['module' => ['test']], + 'group' => 'simpletest', + 'groups' => ['simpletest'], + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @dependencies test + * @group simpletest + */ + ", + ]; + + // Multiple @dependencies annotation. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'description' => 'Tests the Simpletest UI internal browser.', + 'type' => 'Simpletest', + 'requires' => ['module' => ['test', 'test1', 'test2']], + 'group' => 'simpletest', + 'groups' => ['simpletest'], + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. + * + * @dependencies test, test1, test2 + * @group simpletest + */ + ", + ]; + + // Multi-line summary line. + $tests[] = [ + // Expected result. + [ + 'name' => 'Drupal\simpletest\Tests\ExampleSimpleTest', + 'description' => 'Tests the Simpletest UI internal browser. And the summary line continues an there is no gap to the annotation.', + 'type' => 'Simpletest', + 'group' => 'simpletest', + 'groups' => ['simpletest'], + ], + // Classname. + 'Drupal\simpletest\Tests\ExampleSimpleTest', + // Doc block. + "/** + * Tests the Simpletest UI internal browser. And the summary line continues an + * there is no gap to the annotation. + * + * @group simpletest + */ + ", + ]; + return $tests; + } + + /** + * @covers ::getTestInfo + */ + public function testTestInfoParserMissingGroup() { + $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; + $doc_comment = <<<EOT +/** + * Bulk delete storages and fields, and clean up afterwards. + */ +EOT; + $this->expectException(MissingGroupException::class); + $this->expectExceptionMessage('Missing @group annotation in Drupal\KernelTests\field\BulkDeleteTest'); + TestDiscovery::getTestInfo($classname, $doc_comment); + } + + /** + * @covers ::getTestInfo + */ + public function testTestInfoParserMissingSummary() { + $classname = 'Drupal\KernelTests\field\BulkDeleteTest'; + $doc_comment = <<<EOT +/** + * @group field + */ +EOT; + $info = TestDiscovery::getTestInfo($classname, $doc_comment); + $this->assertEmpty($info['description']); + } + + protected function setupVfsWithTestClasses() { + vfsStream::setup('drupal'); + + $test_file = <<<EOF +<?php + +/** + * Test description + * @group example + */ +class FunctionalExampleTest {} +EOF; + + $test_profile_info = <<<EOF +name: Testing +type: profile +core: 8.x +EOF; + + $test_module_info = <<<EOF +name: Testing +type: module +core: 8.x +EOF; + + vfsStream::create([ + 'modules' => [ + 'test_module' => [ + 'tests' => [ + 'src' => [ + 'Functional' => [ + 'FunctionalExampleTest.php' => $test_file, + 'FunctionalExampleTest2.php' => str_replace(['FunctionalExampleTest', '@group example'], ['FunctionalExampleTest2', '@group example2'], $test_file), + ], + 'Kernel' => [ + 'KernelExampleTest3.php' => str_replace(['FunctionalExampleTest', '@group example'], ['KernelExampleTest3', "@group example2\n * @group kernel\n"], $test_file), + 'KernelExampleTestBase.php' => str_replace(['FunctionalExampleTest', '@group example'], ['KernelExampleTestBase', '@group example2'], $test_file), + 'KernelExampleTrait.php' => str_replace(['FunctionalExampleTest', '@group example'], ['KernelExampleTrait', '@group example2'], $test_file), + 'KernelExampleInterface.php' => str_replace(['FunctionalExampleTest', '@group example'], ['KernelExampleInterface', '@group example2'], $test_file), + ], + ], + ], + ], + ], + 'profiles' => [ + 'test_profile' => [ + 'test_profile.info.yml' => $test_profile_info, + 'modules' => [ + 'test_profile_module' => [ + 'test_profile_module.info.yml' => $test_module_info, + 'tests' => [ + 'src' => [ + 'Kernel' => [ + 'KernelExampleTest4.php' => str_replace(['FunctionalExampleTest', '@group example'], ['KernelExampleTest4', '@group example3'], $test_file), + ], + ], + ], + ], + ], + ], + ], + ]); + } + + /** + * @covers ::getTestClasses + */ + public function testGetTestClasses() { + $this->setupVfsWithTestClasses(); + $extensions = [ + 'test_module' => new Extension('vfs://drupal', 'module', 'modules/test_module/test_module.info.yml'), + ]; + $test_discovery = $this->getTestDiscoveryMock('vfs://drupal', $extensions); + + $result = $test_discovery->getTestClasses(); + $this->assertCount(3, $result); + $this->assertEquals([ + 'example' => [ + 'Drupal\Tests\test_module\Functional\FunctionalExampleTest' => [ + 'name' => 'Drupal\Tests\test_module\Functional\FunctionalExampleTest', + 'description' => 'Test description', + 'group' => 'example', + 'groups' => ['example'], + 'type' => 'PHPUnit-Functional', + ], + ], + 'example2' => [ + 'Drupal\Tests\test_module\Functional\FunctionalExampleTest2' => [ + 'name' => 'Drupal\Tests\test_module\Functional\FunctionalExampleTest2', + 'description' => 'Test description', + 'group' => 'example2', + 'groups' => ['example2'], + 'type' => 'PHPUnit-Functional', + ], + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'groups' => ['example2', 'kernel'], + 'type' => 'PHPUnit-Kernel', + ], + ], + 'kernel' => [ + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'groups' => ['example2', 'kernel'], + 'type' => 'PHPUnit-Kernel', + ], + ], + ], $result); + } + + /** + * Mock a TestDiscovery object to return specific extension values. + */ + protected function getTestDiscoveryMock($app_root, $extensions) { + $class_loader = $this->prophesize(ClassLoader::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $test_discovery = $this->getMockBuilder(TestDiscovery::class) + ->setConstructorArgs([$app_root, $class_loader->reveal(), $module_handler->reveal()]) + ->setMethods(['getExtensions']) + ->getMock(); + + $test_discovery->expects($this->any()) + ->method('getExtensions') + ->willReturn($extensions); + + return $test_discovery; + } + + /** + * @covers ::getTestClasses + */ + public function testGetTestClassesWithSelectedTypes() { + $this->setupVfsWithTestClasses(); + $extensions = [ + 'test_module' => new Extension('vfs://drupal', 'module', 'modules/test_module/test_module.info.yml'), + 'test_profile_module' => new Extension('vfs://drupal', 'profile', 'profiles/test_profile/modules/test_profile_module/test_profile_module.info.yml'), + ]; + $test_discovery = $this->getTestDiscoveryMock('vfs://drupal', $extensions); + + $result = $test_discovery->getTestClasses(NULL, ['PHPUnit-Kernel']); + $this->assertCount(4, $result); + $this->assertEquals([ + 'example' => [], + 'example2' => [ + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'groups' => ['example2', 'kernel'], + 'type' => 'PHPUnit-Kernel', + ], + ], + 'kernel' => [ + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'groups' => ['example2', 'kernel'], + 'type' => 'PHPUnit-Kernel', + ], + ], + 'example3' => [ + 'Drupal\Tests\test_profile_module\Kernel\KernelExampleTest4' => [ + 'name' => 'Drupal\Tests\test_profile_module\Kernel\KernelExampleTest4', + 'description' => 'Test description', + 'group' => 'example3', + 'groups' => ['example3'], + 'type' => 'PHPUnit-Kernel', + ], + ], + ], $result); + } + + /** + * @covers ::getTestClasses + */ + public function testGetTestsInProfiles() { + $this->setupVfsWithTestClasses(); + $class_loader = $this->prophesize(ClassLoader::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $container = new Container(); + $container->set('kernel', new DrupalKernel('prod', new ClassLoader())); + $container->set('site.path', 'sites/default'); + \Drupal::setContainer($container); + + $test_discovery = new TestDiscovery('vfs://drupal', $class_loader->reveal(), $module_handler->reveal()); + + $result = $test_discovery->getTestClasses(NULL, ['PHPUnit-Kernel']); + $expected = [ + 'example3' => [ + 'Drupal\Tests\test_profile_module\Kernel\KernelExampleTest4' => [ + 'name' => 'Drupal\Tests\test_profile_module\Kernel\KernelExampleTest4', + 'description' => 'Test description', + 'group' => 'example3', + 'groups' => ['example3'], + 'type' => 'PHPUnit-Kernel', + ], + ], + ]; + $this->assertEquals($expected, $result); + } + + /** + * @covers ::getPhpunitTestSuite + * @dataProvider providerTestGetPhpunitTestSuite + */ + public function testGetPhpunitTestSuite($classname, $expected) { + $this->assertEquals($expected, TestDiscovery::getPhpunitTestSuite($classname)); + } + + public function providerTestGetPhpunitTestSuite() { + $data = []; + $data['simpletest-webtest'] = ['\Drupal\rest\Tests\NodeTest', FALSE]; + $data['simpletest-kerneltest'] = ['\Drupal\hal\Tests\FileNormalizeTest', FALSE]; + $data['module-unittest'] = [static::class, 'Unit']; + $data['module-kerneltest'] = ['\Drupal\KernelTests\Core\Theme\TwigMarkupInterfaceTest', 'Kernel']; + $data['module-functionaltest'] = ['\Drupal\FunctionalTests\BrowserTestBaseTest', 'Functional']; + $data['module-functionaljavascripttest'] = ['\Drupal\Tests\toolbar\FunctionalJavascript\ToolbarIntegrationTest', 'FunctionalJavascript']; + $data['core-unittest'] = ['\Drupal\Tests\ComposerIntegrationTest', 'Unit']; + $data['core-unittest2'] = ['Drupal\Tests\Core\DrupalTest', 'Unit']; + $data['core-unittest3'] = ['Drupal\Tests\Scripts\TestSiteApplicationTest', 'Unit']; + $data['core-kerneltest'] = ['\Drupal\KernelTests\KernelTestBaseTest', 'Kernel']; + $data['core-functionaltest'] = ['\Drupal\FunctionalTests\ExampleTest', 'Functional']; + $data['core-functionaljavascripttest'] = ['\Drupal\FunctionalJavascriptTests\ExampleTest', 'FunctionalJavascript']; + + return $data; + } + + /** + * Ensure that classes are not reflected when the docblock is empty. + * + * @covers ::getTestInfo + */ + public function testGetTestInfoEmptyDocblock() { + // If getTestInfo() performed reflection, it won't be able to find the + // class we asked it to analyze, so it will throw a ReflectionException. + // We want to make sure it didn't do that, because we already did some + // analysis and already have an empty docblock. getTestInfo() will throw + // MissingGroupException because the annotation is empty. + $this->expectException(MissingGroupException::class); + TestDiscovery::getTestInfo('Drupal\Tests\simpletest\ThisTestDoesNotExistTest', ''); + } + + /** + * Ensure TestDiscovery::scanDirectory() ignores certain abstract file types. + * + * @covers ::scanDirectory + */ + public function testScanDirectoryNoAbstract() { + $this->setupVfsWithTestClasses(); + $files = TestDiscovery::scanDirectory('Drupal\\Tests\\test_module\\Kernel\\', vfsStream::url('drupal/modules/test_module/tests/src/Kernel')); + $this->assertNotEmpty($files); + $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTestBase', $files); + $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTrait', $files); + $this->assertArrayNotHasKey('Drupal\Tests\test_module\Kernel\KernelExampleInterface', $files); + $this->assertArrayHasKey('Drupal\Tests\test_module\Kernel\KernelExampleTest3', $files); + } + +} diff --git a/core/tests/TestSuites/TestSuiteBase.php b/core/tests/TestSuites/TestSuiteBase.php index 41ed61622b779da4006ec3cf758522519688a1ca..06cfc537e967c6e5e47233d8a13b99004e92d574 100644 --- a/core/tests/TestSuites/TestSuiteBase.php +++ b/core/tests/TestSuites/TestSuiteBase.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\TestSuites; -use Drupal\simpletest\TestDiscovery; +use Drupal\Core\Test\TestDiscovery; use PHPUnit\Framework\TestSuite; /**