From 9683891396a3bbdb459f8a17ac526f23d9ed4303 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Wed, 9 Mar 2016 11:38:47 +0000
Subject: [PATCH] Issue #2670978 by dawehner, isntall, jibran, alexpott: Allow
 to run just specific types when running all tests

---
 .../Drupal/Component/Utility/NestedArray.php  |  22 +++
 core/modules/simpletest/simpletest.module     |  11 +-
 .../simpletest/simpletest.services.yml        |   2 +-
 core/modules/simpletest/src/TestDiscovery.php |  58 ++++++--
 .../tests/src/Unit/TestInfoParsingTest.php    | 128 ++++++++++++++++++
 core/scripts/run-tests.sh                     |  17 ++-
 .../Component/Utility/NestedArrayTest.php     |  26 ++++
 7 files changed, 240 insertions(+), 24 deletions(-)

diff --git a/core/lib/Drupal/Component/Utility/NestedArray.php b/core/lib/Drupal/Component/Utility/NestedArray.php
index 74e631cdd067..85b4befa41ed 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 ddd6828413e3..4dc754446e50 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 56d48ca9a452..8b645deb246a 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 d2c49d7b18d2..3538934fa68c 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;
 
@@ -49,18 +51,38 @@ 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 509b9ab8689e..fda12f1a6f30 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 = <<<EOF
+<?php
+
+/**
+ * Test description
+ * @group example
+ */
+class FunctionalExampleTest {}
+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'], $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 b9c7a0170272..eba01b9d1ab2 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       <path>
@@ -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 a120e2a06a06..7964298c9763 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;
+  }
+
 }
-- 
GitLab