diff --git a/core/lib/Drupal/Component/Utility/NestedArray.php b/core/lib/Drupal/Component/Utility/NestedArray.php index 74e631c..995211f 100644 --- a/core/lib/Drupal/Component/Utility/NestedArray.php +++ b/core/lib/Drupal/Component/Utility/NestedArray.php @@ -349,4 +349,26 @@ public static function mergeDeepArray(array $arrays, $preserve_integer_keys = FA return $result; } + /** + * Filters a nested array recursively. + * + * @param array $array + * The filtered nested array + * @param callable|NULL $callable + * The callable to apply for filtering. + * + * @return array + * The filtered array. + */ + public static function filter(array $array, callable $callable = NULL) { + $array = is_callable($callable) ? array_filter($array, $callable) : array_filter($array); + foreach ($array as &$element) { + if (is_array($element)) { + $element = static::filter($element, $callable); + } + } + + return $array; + } + } diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 74d348e..f5b85a5 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -489,9 +489,10 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) { * each module for files matching the PSR-0 standard. Once loaded the test list * is cached and stored in a static variable. * - * @param string $module - * Name of a module. If set then only tests belonging to this module are - * returned. + * @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 with the groups, and then keyed by test classes. @@ -506,8 +507,8 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) { * ); * @endcode */ -function simpletest_test_get_all($module = NULL) { - return \Drupal::service('test_discovery')->getTestClasses($module); +function simpletest_test_get_all($extension = NULL, array $types = []) { + return \Drupal::service('test_discovery')->getTestClasses($extension, $types); } /** diff --git a/core/modules/simpletest/simpletest.services.yml b/core/modules/simpletest/simpletest.services.yml index 56d48ca..8b645de 100644 --- a/core/modules/simpletest/simpletest.services.yml +++ b/core/modules/simpletest/simpletest.services.yml @@ -1,4 +1,4 @@ services: test_discovery: class: Drupal\simpletest\TestDiscovery - arguments: ['@class_loader', '@?cache.discovery'] + arguments: ['@app.root', '@class_loader', '@module_handler', '@?cache.discovery'] diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php index d2c49d7..3538934 100644 --- a/core/modules/simpletest/src/TestDiscovery.php +++ b/core/modules/simpletest/src/TestDiscovery.php @@ -10,9 +10,11 @@ use Doctrine\Common\Annotations\SimpleAnnotationReader; use Doctrine\Common\Reflection\StaticReflectionParser; use Drupal\Component\Annotation\Reflection\MockFileFinder; +use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ExtensionDiscovery; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\simpletest\Exception\MissingGroupException; use PHPUnit_Util_Test; @@ -50,17 +52,37 @@ class TestDiscovery { protected $availableExtensions; /** + * The app root. + * + * @var string + */ + protected $root; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * 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. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * (optional) Backend for caching discovery results. */ - public function __construct($class_loader, CacheBackendInterface $cache_backend = NULL) { + public function __construct($root, $class_loader, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend = NULL) { + $this->root = $root; $this->classLoader = $class_loader; + $this->moduleHandler = $module_handler; $this->cacheBackend = $cache_backend; } @@ -80,15 +102,16 @@ public function registerTestNamespaces() { $existing = $this->classLoader->getPrefixesPsr4(); // Add PHPUnit test namespaces of Drupal core. - $this->testNamespaces['Drupal\\Tests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/Tests']; - $this->testNamespaces['Drupal\\KernelTests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/KernelTests']; - $this->testNamespaces['Drupal\\FunctionalTests\\'] = [DRUPAL_ROOT . '/core/tests/Drupal/FunctionalTests']; + $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 = array(); foreach ($this->getExtensions() as $name => $extension) { $this->availableExtensions[$extension->getType()][$name] = $name; - $base_path = DRUPAL_ROOT . '/' . $extension->getPath(); + $base_path = $this->root . '/' . $extension->getPath(); // Add namespace of disabled/uninstalled extensions. if (!isset($existing["Drupal\\$name\\"])) { @@ -115,11 +138,12 @@ public function registerTestNamespaces() { * * @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 first @group specified in each test's - * PHPDoc comment block, and then keyed by class names. For example: - * @code + * An array of tests keyed by the the group name. + * @code * $groups['block'] => array( * 'Drupal\block\Tests\BlockTest' => array( * 'name' => 'Drupal\block\Tests\BlockTest', @@ -127,15 +151,12 @@ public function registerTestNamespaces() { * 'group' => 'block', * ), * ); - * @endcode - * - * @throws \ReflectionException - * If a discovered test class does not match the expected class name. + * @endcode * * @todo Remove singular grouping; retain list of groups in 'group' key. * @see https://www.drupal.org/node/2296615 */ - public function getTestClasses($extension = NULL) { + public function getTestClasses($extension = NULL, array $types = []) { $reader = new SimpleAnnotationReader(); $reader->addNamespace('Drupal\\simpletest\\Annotation'); @@ -190,13 +211,20 @@ public function getTestClasses($extension = NULL) { } // Allow modules extending core tests to disable originals. - \Drupal::moduleHandler()->alter('simpletest', $list); + $this->moduleHandler->alter('simpletest', $list); if (!isset($extension)) { if ($this->cacheBackend) { $this->cacheBackend->set('simpletest:discovery:classes', $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; } @@ -450,7 +478,7 @@ public static function getPhpunitTestSuite($classname) { * An array of Extension objects, keyed by extension name. */ protected function getExtensions() { - $listing = new ExtensionDiscovery(DRUPAL_ROOT); + $listing = new ExtensionDiscovery($this->root); // Ensure that tests in all profiles are discovered. $listing->setProfileDirectories(array()); $extensions = $listing->scan('module', TRUE); diff --git a/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php b/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php index 509b9ab..21dfad3 100644 --- a/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php +++ b/core/modules/simpletest/tests/src/Unit/TestInfoParsingTest.php @@ -7,8 +7,12 @@ namespace Drupal\Tests\simpletest\Unit; +use Composer\Autoload\ClassLoader; +use Drupal\Core\Extension\Extension; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\simpletest\TestDiscovery; use Drupal\Tests\UnitTestCase; +use org\bovigo\vfs\vfsStream; /** * @coversDefaultClass \Drupal\simpletest\TestDiscovery @@ -256,6 +260,130 @@ public function testTestInfoParserMissingSummary() { $this->assertEmpty($info['description']); } + protected function setupVfsWithTestClasses() { + vfsStream::setup('drupal'); + + $test_file = << [ + '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'], $test_file), + ], + ], + ], + ], + ], + ]); + } + + /** + * @covers ::getTestClasses + */ + public function testGetTestClasses() { + $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_discovery->setExtensions($extensions); + $result = $test_discovery->getTestClasses(); + $this->assertCount(2, $result); + $this->assertEquals([ + 'example' => [ + 'Drupal\Tests\test_module\Functional\FunctionalExampleTest' => [ + 'name' => 'Drupal\Tests\test_module\Functional\FunctionalExampleTest', + 'description' => 'Test description', + 'group' => 'example', + 'type' => 'PHPUnit-Functional' + ], + ], + 'example2' => [ + 'Drupal\Tests\test_module\Functional\FunctionalExampleTest2' => [ + 'name' => 'Drupal\Tests\test_module\Functional\FunctionalExampleTest2', + 'description' => 'Test description', + 'group' => 'example2', + 'type' => 'PHPUnit-Functional' + ], + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'type' => 'PHPUnit-Kernel' + ], + ], + ], $result); + } + + /** + * @covers ::getTestClasses + */ + 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_discovery->setExtensions($extensions); + $result = $test_discovery->getTestClasses(NULL, ['PHPUnit-Kernel']); + $this->assertCount(2, $result); + $this->assertEquals([ + 'example' => [ + ], + 'example2' => [ + 'Drupal\Tests\test_module\Kernel\KernelExampleTest3' => [ + 'name' => 'Drupal\Tests\test_module\Kernel\KernelExampleTest3', + 'description' => 'Test description', + 'group' => 'example2', + 'type' => 'PHPUnit-Kernel' + ], + ], + ], $result); + } + +} + +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; + } + /** * @covers ::getPhpunitTestSuite * @dataProvider providerTestGetPhpunitTestSuite diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index b9c7a01..eba01b9 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -208,6 +208,12 @@ function simpletest_script_help() { Specify the path and the extension (i.e. 'core/modules/user/user.test'). + --types + + Runs just tests from the specified test type, for example + run-tests.sh + (i.e. --types "Simpletest,PHPUnit-Functional") + --directory Run all tests found within the specified file directory. --xml @@ -292,6 +298,7 @@ function simpletest_script_parse_args() { 'module' => NULL, 'class' => FALSE, 'file' => FALSE, + 'types' => [], 'directory' => NULL, 'color' => FALSE, 'verbose' => FALSE, @@ -320,6 +327,10 @@ function simpletest_script_parse_args() { if (is_bool($args[$previous_arg])) { $args[$matches[1]] = TRUE; } + elseif (is_array($args[$previous_arg])) { + $value = array_shift($_SERVER['argv']); + $args[$matches[1]] = array_map('trim', explode(',', $value)); + } else { $args[$matches[1]] = array_shift($_SERVER['argv']); } @@ -894,7 +905,7 @@ function simpletest_script_get_test_list() { $test_list = array(); if ($args['all'] || $args['module']) { try { - $groups = simpletest_test_get_all($args['module']); + $groups = simpletest_test_get_all($args['module'], $args['types']); } catch (Exception $e) { echo (string) $e; @@ -916,7 +927,7 @@ function simpletest_script_get_test_list() { } else { try { - $groups = simpletest_test_get_all(); + $groups = simpletest_test_get_all(NULL, $args['types']); } catch (Exception $e) { echo (string) $e; @@ -1017,7 +1028,7 @@ function simpletest_script_get_test_list() { } else { try { - $groups = simpletest_test_get_all(); + $groups = simpletest_test_get_all(NULL, $args['types']); } catch (Exception $e) { echo (string) $e; diff --git a/core/tests/Drupal/Tests/Component/Utility/NestedArrayTest.php b/core/tests/Drupal/Tests/Component/Utility/NestedArrayTest.php index a120e2a..7964298 100644 --- a/core/tests/Drupal/Tests/Component/Utility/NestedArrayTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/NestedArrayTest.php @@ -259,4 +259,30 @@ public function testMergeOutOfSequenceKeys() { $this->assertSame($expected, $actual, 'drupal_array_merge_deep() ignores numeric key order when merging.'); } + /** + * @covers ::filter + * @dataProvider providerTestFilter + */ + public function testFilter($array, $callable, $expected) { + $this->assertEquals($expected, NestedArray::filter($array, $callable)); + } + + public function providerTestFilter() { + $data = []; + $data['1d-array'] = [ + [0, 1, '', TRUE], NULL, [1 => 1, 3 => TRUE] + ]; + $data['1d-array-callable'] = [ + [0, 1, '', TRUE], function ($element) { return $element === ''; }, [2 => ''] + ]; + $data['2d-array'] = [ + [[0, 1, '', TRUE], [0, 1, 2, 3]], NULL, [0 => [1 => 1, 3 => TRUE], 1 => [1 => 1, 2 => 2, 3 => 3]], + ]; + $data['2d-array-callable'] = [ + [[0, 1, '', TRUE], [0, 1, 2, 3]], function ($element) { return is_array($element) || $element === 3; }, [0 => [], 1 => [3 => 3]], + ]; + + return $data; + } + }