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..da901e8 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -492,6 +492,8 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) { * @param string $module * Name of a module. If set then only tests belonging to this module are * returned. + * @param string[] $exclude_groups + * (optional) An array of excluded groups. * * @return array[] * An array of tests keyed with the groups, and then keyed by test classes. @@ -506,7 +508,7 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) { * ); * @endcode */ -function simpletest_test_get_all($module = NULL) { +function simpletest_test_get_all($module = NULL, array $exclude_groups = []) { return \Drupal::service('test_discovery')->getTestClasses($module); } 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..233f05b 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,15 @@ 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->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,6 +137,8 @@ public function registerTestNamespaces() { * * @param string $extension * (optional) The name of an extension to limit discovery to; e.g., 'node'. + * @param string[] $exclude_types + * (optional) An array of excluded test types. * * @return array * An array of tests keyed by the first @group specified in each test's @@ -135,7 +159,7 @@ public function registerTestNamespaces() { * @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 $exclude_types = []) { $reader = new SimpleAnnotationReader(); $reader->addNamespace('Drupal\\simpletest\\Annotation'); @@ -190,13 +214,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 ($exclude_types) { + $list = NestedArray::filter($list, function ($element) use ($exclude_types) { + return !(is_array($element) && isset($element['type']) && in_array($element['type'], $exclude_types)); + }); + } + return $list; } @@ -450,7 +481,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..e7f9f9c 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,136 @@ 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 testGetTestClassesWithExcludedTypes() { + $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' => [ + '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' + ], + ], + ], $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..926d254 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -208,6 +208,11 @@ function simpletest_script_help() { Specify the path and the extension (i.e. 'core/modules/user/user.test'). + --exclude-types + + Runs all tests, but excluding a certain type. Together with --all, + its for example to exclude javascript tests from being executed. + --directory Run all tests found within the specified file directory. --xml @@ -292,6 +297,7 @@ function simpletest_script_parse_args() { 'module' => NULL, 'class' => FALSE, 'file' => FALSE, + 'exclude-types' => [], 'directory' => NULL, 'color' => FALSE, 'verbose' => FALSE, @@ -320,6 +326,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 +904,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['exclude-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; + } + }