diff --git a/core/modules/action/src/Tests/ConfigurationTest.php b/core/modules/action/src/Tests/ConfigurationTest.php
index 97448ae..cfd35a5 100644
--- a/core/modules/action/src/Tests/ConfigurationTest.php
+++ b/core/modules/action/src/Tests/ConfigurationTest.php
@@ -11,7 +11,12 @@
 use Drupal\simpletest\WebTestBase;
 
 /**
- * Actions configuration.
+ * Tests UI CRUD configuration for complex actions.
+ *
+ * @group Action
+ * @requires module action
+ *
+ * @todo Remove the above @requires tag. Added for demo purposes only.
  */
 class ConfigurationTest extends WebTestBase {
 
@@ -22,14 +27,6 @@ class ConfigurationTest extends WebTestBase {
    */
   public static $modules = array('action');
 
-  public static function getInfo() {
-    return array(
-      'name' => 'Actions configuration',
-      'description' => 'Tests complex actions configuration by adding, editing, and deleting a complex action.',
-      'group' => 'Action',
-    );
-  }
-
   /**
    * Tests configuration of advanced actions through administration interface.
    */
diff --git a/core/modules/block/custom_block/tests/src/Menu/CustomBlockLocalTasksTest.php b/core/modules/block/custom_block/tests/src/Menu/CustomBlockLocalTasksTest.php
index d04ad15..c5c42bb 100644
--- a/core/modules/block/custom_block/tests/src/Menu/CustomBlockLocalTasksTest.php
+++ b/core/modules/block/custom_block/tests/src/Menu/CustomBlockLocalTasksTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\custom_block\Tests\Menu;
 
+use Drupal\Core\Extension\Extension;
 use Drupal\Tests\Core\Menu\LocalTaskIntegrationTest;
 
 /**
@@ -31,6 +32,29 @@ public function setUp() {
       'custom_block' => 'core/modules/block/custom_block',
     );
     parent::setUp();
+
+    // @see \Drupal\block\Plugin\Derivative\ThemeLocalTask
+    $config_factory = $this->getConfigFactoryStub(array(
+      'system.theme' => array(
+        'default' => 'stark',
+      ),
+    ));
+    \Drupal::getContainer()->set('config.factory', $config_factory);
+
+    $list = array(
+      'stark' => new Extension('theme', 'core/themes/stark.info.yml'),
+    );
+    $list['stark']->status = 1;
+    $list['stark']->info['name'] = 'Stark';
+
+    $theme_handler = $this->getMockBuilder('Drupal\Core\Extension\ThemeHandlerInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $theme_handler
+      ->expects($this->any())
+      ->method('listInfo')
+      ->will($this->returnValue($list));
+    \Drupal::getContainer()->set('theme_handler', $theme_handler);
   }
 
   /**
diff --git a/core/modules/simpletest/css/simpletest.module.css b/core/modules/simpletest/css/simpletest.module.css
index 611cc4a..71f7eea 100644
--- a/core/modules/simpletest/css/simpletest.module.css
+++ b/core/modules/simpletest/css/simpletest.module.css
@@ -4,7 +4,7 @@
   width: 1em;
 }
 th.simpletest-test-label {
-  width: 16em;
+  width: 40%;
 }
 
 .simpletest-image {
diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php
index dad9a39..e5251b7 100644
--- a/core/modules/simpletest/simpletest.api.php
+++ b/core/modules/simpletest/simpletest.api.php
@@ -14,9 +14,9 @@
  * Alter the list of tests.
  *
  * @param $groups
- *   A two dimension array, the first key is the test group (as defined in
- *   getInfo) the second is the name of the class and the value is the return
- *   value of the getInfo method.
+ *   A two dimensional array, the first key is the test group, the second is the
+ *   name of the test class, and the value is in associative array containing
+ *   'name', 'description', 'group', and 'requires' keys.
  */
 function hook_simpletest_alter(&$groups) {
   // An alternative session handler module would not want to run the original
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index bec96c4..1654c3a 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -5,6 +5,7 @@
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Render\Element;
 use Drupal\simpletest\TestBase;
+use Drupal\simpletest\TestDiscovery;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Process\PhpExecutableFinder;
 
@@ -88,40 +89,34 @@ function _simpletest_format_summary_line($summary) {
  *
  * @param $test_list
  *   List of tests to run.
- * @param $reporter
- *   Which reporter to use. Allowed values are: text, xml, html and drupal,
- *   drupal being the default.
  *
  * @return string
  *   The test ID.
  */
-function simpletest_run_tests($test_list, $reporter = 'drupal') {
+function simpletest_run_tests($test_list) {
   $test_id = db_insert('simpletest_test_id')
     ->useDefaults(array('test_id'))
     ->execute();
 
-  $phpunit_tests = isset($test_list['UnitTest']) ? $test_list['UnitTest'] : array();
-  if ($phpunit_tests) {
+  if (!empty($test_list['phpunit'])) {
     $phpunit_results = simpletest_run_phpunit_tests($test_id, $phpunit_tests);
     simpletest_process_phpunit_results($phpunit_results);
   }
 
-  if (!array_key_exists('WebTest', $test_list) || empty($test_list['WebTest'])) {
-    // Early return if there are no WebTests to run.
+  // Early return if there are no further tests to run.
+  if (empty($test_list['simpletest'])) {
     return $test_id;
   }
 
-  // Contine with SimpleTests only.
-  $test_list = $test_list['WebTest'];
+  // Continue with SimpleTests only.
+  $test_list = $test_list['simpletest'];
 
   // Clear out the previous verbose files.
   file_unmanaged_delete_recursive('public://simpletest/verbose');
 
   // Get the info for the first test being run.
-  $first_test = array_shift($test_list);
-  $first_instance = new $first_test();
-  array_unshift($test_list, $first_test);
-  $info = $first_instance->getInfo();
+  $first_test = reset($test_list);
+  $info = TestDiscovery::getTestInfo(new \ReflectionClass($first_test));
 
   $batch = array(
     'title' => t('Running tests'),
@@ -295,7 +290,7 @@ function _simpletest_batch_operation($test_list_init, $test_id, &$context) {
   $test = new $test_class($test_id);
   $test->run();
   $size = count($test_list);
-  $info = $test->getInfo();
+  $info = TestDiscovery::getTestInfo(new \ReflectionClass($test));
 
   \Drupal::moduleHandler()->invokeAll('test_finished', array($test->results));
 
@@ -422,8 +417,8 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
  *   returned.
  *
  * @return array[]
- *   An array of tests keyed with the groups specified in each of the tests'
- *   getInfo() methods and then keyed by the test classes. For example:
+ *   An array of tests keyed with the groups, and then keyed by test classes.
+ *   For example:
  *   @code
  *     $groups['Block'] => array(
  *       'BlockTestCase' => array(
@@ -435,159 +430,14 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
  *   @endcode
  */
 function simpletest_test_get_all($module = NULL) {
-  static $all_groups = array();
-  $cid = "simpletest:$module";
-
-  if (!isset($all_groups[$cid])) {
-    $all_groups[$cid] = array();
-    $groups = &$all_groups[$cid];
-    // Register namespaces (extensions are not necessarily enabled).
-    simpletest_classloader_register();
-
-    // Load test information from cache if available, otherwise retrieve the
-    // information from each tests getInfo() method.
-    if ($cache = \Drupal::cache()->get($cid)) {
-      $groups = $cache->data;
-    }
-    else {
-      // Select all PSR-0 classes in the Tests namespace of all modules.
-      $listing = new ExtensionDiscovery();
-      // Ensure that tests in all profiles are discovered.
-      $listing->setProfileDirectories(array());
-      $all_data = $listing->scan('module', TRUE);
-      // If module is set then we keep only that one module.
-      if (isset($module)) {
-        $all_data = array(
-          $module => $all_data[$module],
-        );
-      }
-      else {
-        $all_data += $listing->scan('profile', TRUE);
-        $all_data += $listing->scan('theme', TRUE);
-      }
-
-      // Scan all extension folders for class files.
-      $classes = array();
-      foreach ($all_data as $name => $data) {
-
-        // Build the directory in which simpletest test classes would reside.
-        $tests_dir = DRUPAL_ROOT . '/' . $data->getPath() . '/src/Tests';
-
-        // Check if the directory exists.
-        if (!is_dir($tests_dir)) {
-          // This extension has no directory for simpletest cases.
-          continue;
-        }
-
-        // Scan the directory for class files.
-        $files = file_scan_directory($tests_dir, '/\.php$/');
-        if (empty($files)) {
-          // No class files found.
-          continue;
-        }
-
-        // Convert the file names into the namespaced class names.
-        $strlen = strlen($tests_dir) + 1;
-        $namespace = 'Drupal\\' . $name . '\Tests\\';
-        foreach ($files as $file) {
-          $classes[] = $namespace . str_replace('/', '\\', substr($file->uri, $strlen, -4));
-        }
-      }
-
-      // Check that each class has a getInfo() method and store the information
-      // in an array keyed with the group specified in the test information.
-      $groups = array();
-      foreach ($classes as $class) {
-        // Test classes need to implement getInfo() to be valid.
-        if (class_exists($class) && method_exists($class, 'getInfo')) {
-          $reflectionClass = new ReflectionClass($class);
-          // Skip abstract classes and interfaces.
-          if ($reflectionClass->isInstantiable()) {
-            $reflectionMethod = new ReflectionMethod($class, 'getInfo');
-            $declaringClass = $reflectionMethod->getDeclaringClass()->getName();
-            // Avoid testing intermediate classes which do not implement the
-            // method.
-            if ($class == $declaringClass) {
-              $info = call_user_func(array($class, 'getInfo'));
-            }
-            else {
-              continue;
-            }
-          }
-          else {
-            continue;
-          }
-          // If this test class requires a non-existing module, skip it.
-          if (!empty($info['dependencies'])) {
-            foreach ($info['dependencies'] as $dependency) {
-              if (!isset($dependency_data[$dependency])) {
-                continue 2;
-              }
-            }
-          }
-
-          $groups[$info['group']][$class] = $info;
-        }
-      }
-
-      // Sort the groups and tests within the groups by name.
-      uksort($groups, 'strnatcasecmp');
-      foreach ($groups as &$tests) {
-        uksort($tests, 'strnatcasecmp');
-      }
-
-      // Allow modules extending core tests to disable originals.
-      \Drupal::moduleHandler()->alter('simpletest', $groups);
-      \Drupal::cache()->set($cid, $groups);
-    }
-  }
-  return $all_groups[$cid];
+  return \Drupal::service('test_discovery')->getTestClasses();
 }
 
 /**
  * Registers namespaces for disabled modules.
  */
 function simpletest_classloader_register() {
-  // Use the same cache prefix as simpletest_test_get_all().
-  $cid = "simpletest::all";
-  $types = array(
-    'theme_engine',
-    'module',
-    'theme',
-    'profile',
-  );
-
-  if ($cache = \Drupal::cache()->get($cid)) {
-    $extensions = $cache->data;
-  }
-  else {
-    $listing = new ExtensionDiscovery();
-    // Ensure that tests in all profiles are discovered.
-    $listing->setProfileDirectories(array());
-    $extensions = array();
-    foreach ($types as $type) {
-      foreach ($listing->scan($type, TRUE) as $name => $file) {
-        $extensions[$type][$name] = $file->getPathname();
-      }
-    }
-    \Drupal::cache()->set($cid, $extensions);
-  }
-
-  $classloader = drupal_classloader();
-  foreach ($types as $type) {
-    foreach ($extensions[$type] as $name => $uri) {
-      drupal_classloader_register($name, dirname($uri));
-      $classloader->addPsr4('Drupal\\' . $name . '\\Tests\\', array(
-        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/Drupal/' . $name . '/Tests',
-        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/src',
-      ));
-      // While being there, prime drupal_get_filename().
-      drupal_get_filename($type, $name, $uri);
-    }
-  }
-
-  // Register the core test directory so we can find \Drupal\UnitTestCase.
-  $classloader->add('Drupal\\Tests', DRUPAL_ROOT . '/core/tests');
+  \Drupal::service('test_discovery')->registerTestNamespaces();
 }
 
 /**
@@ -748,59 +598,6 @@ function simpletest_mail_alter(&$message) {
 }
 
 /**
- * Gets PHPUnit classes.
- *
- * @param string $module
- *   Name of a module. If set then only tests belonging to this module is
- *   returned.
- *
- * @return array
- *   Returns an array of test classes.
- *
- * @throws \RuntimeException
- *   This is thrown when anything is wrong with a test.
- */
-function simpletest_phpunit_get_available_tests($module = NULL) {
-  // Try to load the class names array from cache.
-  $cid = 'simpletest_phpunit:' . $module;
-  if ($cache = \Drupal::cache()->get($cid)) {
-    $test_classes = $cache->data;
-  }
-  else {
-    if ($module) {
-      $prefix = 'Drupal\\' . $module . '\\';
-      $n = strlen($prefix);
-    }
-    // If there was no cached data available we have to find the tests.
-    // Load the PHPUnit configuration file, which tells us where to find the
-    // tests.
-    $phpunit_config = simpletest_phpunit_configuration_filepath();
-    $configuration = PHPUnit_Util_Configuration::getInstance($phpunit_config);
-    // Find all the tests and get a list of unique class names.
-    $test_suite = $configuration->getTestSuiteConfiguration(NULL);
-    $test_classes = array();
-    foreach ($test_suite as $test) {
-      // PHPUnit returns a warning message if something is wrong with a test,
-      // throw an exception to avoid an error when trying to call getInfo() on
-      // this.
-      if ($test instanceof PHPUnit_Framework_Warning) {
-        throw new RuntimeException($test->getMessage());
-      }
-
-      $name = get_class($test);
-      if (!array_key_exists($name, $test_classes) && (!$module || substr($name, 0, $n) == $prefix)) {
-        $test_classes[$name] = $test->getInfo();
-      }
-    }
-
-    // Since we have recalculated, we now need to store the new data into cache.
-    \Drupal::cache()->set($cid, $test_classes);
-  }
-
-  return $test_classes;
-}
-
-/**
  * Converts PHPUnit's JUnit XML output to an array.
  *
  * @param $test_id
diff --git a/core/modules/simpletest/simpletest.services.yml b/core/modules/simpletest/simpletest.services.yml
new file mode 100644
index 0000000..b6c75e2
--- /dev/null
+++ b/core/modules/simpletest/simpletest.services.yml
@@ -0,0 +1,3 @@
+services:
+  test_discovery:
+    class: Drupal\simpletest\TestDiscovery
diff --git a/core/modules/simpletest/src/Form/SimpletestTestForm.php b/core/modules/simpletest/src/Form/SimpletestTestForm.php
index e3876a5..a85f941 100644
--- a/core/modules/simpletest/src/Form/SimpletestTestForm.php
+++ b/core/modules/simpletest/src/Form/SimpletestTestForm.php
@@ -27,6 +27,20 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, array &$form_state) {
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Run tests'),
+      '#tableselect' => TRUE,
+      '#button_type' => 'primary',
+    );
+
+    // Do not needlessly re-execute a full test discovery if the user input
+    // already contains an explicit list of test classes to run.
+    if (!empty($form_state['input']['tests'])) {
+      return $form;
+    }
+
     // JavaScript-only table filters.
     $form['filters'] = array(
       '#type' => 'container',
@@ -96,9 +110,6 @@ public function buildForm(array $form, array &$form_state) {
 
     // Generate the list of tests arranged by group.
     $groups = simpletest_test_get_all();
-    $groups['PHPUnit'] = simpletest_phpunit_get_available_tests();
-    $form_state['storage']['PHPUnit'] = $groups['PHPUnit'];
-
     foreach ($groups as $group => $tests) {
       $form['tests'][$group] = array(
         '#attributes' => array('class' => array('simpletest-group')),
@@ -150,10 +161,7 @@ public function buildForm(array $form, array &$form_state) {
         );
         $form['tests'][$class]['description'] = array(
           '#prefix' => '<div class="description">',
-          '#markup' => String::format('@description (@class)', array(
-            '@description' => $info['description'],
-            '@class' => $class,
-          )),
+          '#markup' => String::checkPlain($info['description']),
           '#suffix' => '</div>',
           '#wrapper_attributes' => array(
             'class' => array('simpletest-test-description', 'table-filter-text-source'),
@@ -162,15 +170,6 @@ public function buildForm(array $form, array &$form_state) {
       }
     }
 
-    // Action buttons.
-    $form['actions'] = array('#type' => 'actions');
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => $this->t('Run tests'),
-      '#tableselect' => TRUE,
-      '#button_type' => 'primary',
-    );
-
     $form['clean'] = array(
       '#type' => 'fieldset',
       '#title' => $this->t('Clean test environment'),
@@ -190,16 +189,31 @@ public function buildForm(array $form, array &$form_state) {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, array &$form_state) {
+    // Test discovery does not run upon form submission.
     simpletest_classloader_register();
 
-    $phpunit_all = array_keys($form_state['storage']['PHPUnit']);
+    // This form accepts arbitrary user input for 'tests'.
+    // An invalid value will cause the $class_name lookup below to die with a
+    // fatal error. Regular user access mechanisms to this form are intact.
+    // The only validation effectively being skipped is the validation of
+    // available checkboxes vs. submitted checkboxes.
+    // @todo Refactor Form API to allow to POST values without constructing the
+    //   entire form more easily, BUT retaining routing access security and
+    //   retaining Form API CSRF #token security validation, and without having
+    //   to rely on form caching.
+    if (empty($form_state['values']['tests']) && !empty($form_state['input']['tests'])) {
+      $form_state['values']['tests'] = $form_state['input']['tests'];
+    }
 
     $tests_list = array();
     foreach ($form_state['values']['tests'] as $class_name => $value) {
-      // Since class_exists() will likely trigger an autoload lookup,
-      // we do the fast check first.
-      if ($value === $class_name && class_exists($class_name)) {
-        $test_type = in_array($class_name, $phpunit_all) ? 'UnitTest' : 'WebTest';
+      if ($value === $class_name) {
+        if (is_subclass_of($class_name, 'PHPUnit_Framework_TestCase')) {
+          $test_type = 'phpunit';
+        }
+        else {
+          $test_type = 'simpletest';
+        }
         $tests_list[$test_type][] = $class_name;
       }
     }
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index 023cf8e..133e931 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -200,27 +200,6 @@ public function __construct($test_id = NULL) {
   }
 
   /**
-   * Provides meta information about this test case, such as test name.
-   *
-   * @return array
-   *   An array of untranslated strings with the following keys:
-   *   - name: An overview of what is tested by the class; for example, "User
-   *     access rules".
-   *   - description: One sentence describing the test, starting with a verb.
-   *   - group: The human-readable name of the module ("Node", "Statistics"), or
-   *     the human-readable name of the Drupal facility tested (e.g. "Form API"
-   *     or "XML-RPC").
-   */
-  public static function getInfo() {
-    // PHP does not allow us to declare this method as abstract public static,
-    // so we simply throw an exception here if this has not been implemented by
-    // a child class.
-    throw new \RuntimeException(String::format('@class must implement \Drupal\simpletest\TestBase::getInfo().', array(
-      '@class' => get_called_class(),
-    )));
-  }
-
-  /**
    * Performs setup tasks before each individual test method is run.
    */
   abstract protected function setUp();
diff --git a/core/modules/simpletest/src/TestDiscovery.php b/core/modules/simpletest/src/TestDiscovery.php
new file mode 100644
index 0000000..dac7a43
--- /dev/null
+++ b/core/modules/simpletest/src/TestDiscovery.php
@@ -0,0 +1,390 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\simpletest\TestDiscovery.
+ */
+
+namespace Drupal\simpletest;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use PHPUnit_Framework_TestSuite;
+use PHPUnit_Framework_Warning;
+use PHPUnit_Util_Test;
+
+/**
+ * Discovers available tests.
+ */
+class TestDiscovery {
+
+  /**
+   * Cached map of all test namespaces to respective directories.
+   *
+   * @var array
+   */
+  protected $testNamespaces;
+
+  /**
+   * Cached list of all discovered test classes.
+   *
+   * @var array
+   */
+  protected $testClasses;
+
+  /**
+   * Cached list of available extension names, keyed by extension type.
+   *
+   * @var array
+   */
+  protected $availableExtensions;
+
+  /**
+   * Registers test namespaces of all available extensions.
+   *
+   * @return array
+   *   An associative array whose keys are PSR-4 namespace prefixes and whose
+   *   values are directory names.
+   *
+   * @todo Inject class loader.
+   */
+  public function registerTestNamespaces() {
+    if (isset($this->testNamespaces)) {
+      return $this->testNamespaces;
+    }
+    $this->testNamespaces = array();
+
+    $loader = drupal_classloader();
+    $existing = $loader->getPrefixesPsr4();
+    $old = array();
+    $new = array();
+
+    // Add PHPUnit test namespace of Drupal core.
+    $new['Drupal\\Tests\\'] = DRUPAL_ROOT . '/core/tests/Drupal/Tests';
+
+    $this->availableExtensions = array();
+    foreach ($this->getExtensions() as $name => $extension) {
+      $this->availableExtensions[$extension->getType()][$name] = $name;
+
+      $base_namespace = "Drupal\\$name\\";
+      $base_path = DRUPAL_ROOT . '/' . $extension->getPath();
+
+      // Add namespace of disabled/uninstalled extensions.
+      if (!isset($existing[$base_namespace])) {
+        $new[$base_namespace] = "$base_path/src";
+      }
+      else {
+        $old[$base_namespace] = reset($existing[$base_namespace]);
+      }
+      // Add PHPUnit test namespace.
+      // @todo Move PHPUnit namespace of extensions into Drupal\Tests\$name.
+      // @see https://drupal.org/node/2260121
+      $new[$base_namespace . 'Tests\\'] = "$base_path/tests/src";
+
+      // While being there, prime drupal_get_filename().
+      // @todo Remove this.
+      drupal_get_filename($extension->getType(), $name, $extension->getPathname());
+    }
+
+    foreach ($new as $prefix => $path) {
+      $loader->addPsr4($prefix, $path);
+    }
+
+    $this->testNamespaces = $new + $old;
+    return $this->testNamespaces;
+  }
+
+  /**
+   * Discovers all available tests in all extensions.
+   *
+   * @return array
+   *   An array of tests keyed by the @group specified in each of test's
+   *   phpDoc comment block, and then keyed by class names. For example:
+   *   @code
+   *     $groups['Block'] => array(
+   *       'Drupal\block\Tests\BlockTest' => array(
+   *         'name' => 'Drupal\block\Tests\BlockTest',
+   *         'description' => 'Tests block UI CRUD functionality.',
+   *         'group' => 'Block',
+   *       ),
+   *     );
+   *   @endcode
+   *
+   * @todo Remove singular grouping; retain list of groups in 'group' key.
+   * @todo Add dedicated groups 'Kernel' + 'Web' complementing 'PHPUnit'.
+   */
+  public function getTestClasses() {
+    if (isset($this->testClasses)) {
+      return $this->testClasses;
+    }
+    $this->testClasses = array();
+
+    $classmap = $this->findTestClasses();
+
+    // Prevent expensive class loader lookups for each reflected test class by
+    // registering the complete classmap of test classes to the class loader.
+    $loader = drupal_classloader();
+    $loader->addClassMap($classmap);
+
+    foreach ($classmap as $classname => $pathname) {
+      try {
+        $class = new \ReflectionClass($classname);
+      }
+      catch (\ReflectionException $e) {
+        // Re-throw with expected pathname.
+        // Note: This exception is not caught anywhere. Developers should
+        // immediately see a fatal error.
+        $message = $e->getMessage() . " in expected $pathname";
+        throw new \ReflectionException($message, $e->getCode(), $e);
+      }
+      // Skip abstract classes and interfaces.
+      if (!$class->isInstantiable()) {
+        continue;
+      }
+      // Skip non-test classes.
+      $phpunit = FALSE;
+      if (!$class->isSubclassOf('Drupal\simpletest\TestBase') && !$phpunit = $class->isSubclassOf('PHPUnit_Framework_TestCase')) {
+        continue;
+      }
+      $info = static::getTestInfo($class);
+
+      // Skip this test class if it requires unavailable modules.
+      // @todo PHPUnit skips tests with unmet requirements when executing a test
+      //   suite (instead of excluding them altogether). Refactor the test
+      //   runner to follow that approach.
+      if (!empty($info['requires']['module'])) {
+        if (array_diff($info['requires']['module'], $this->availableExtensions['module'])) {
+          continue;
+        }
+      }
+      // @todo Check whether PHPUnit's native validation is really required.
+      if ($phpunit) {
+        $suite = new PHPUnit_Framework_TestSuite($class);
+        // PHPUnit returns a warning if something is wrong with a test.
+        if ($suite instanceof PHPUnit_Framework_Warning) {
+          throw new \RuntimeException($test->getMessage());
+        }
+      }
+
+      $this->testClasses[$info['group']][$classname] = $info;
+    }
+
+    // Sort the groups and tests within the groups by name.
+    uksort($this->testClasses, 'strnatcasecmp');
+    foreach ($this->testClasses as &$tests) {
+      uksort($tests, 'strnatcasecmp');
+    }
+
+    // Allow modules extending core tests to disable originals.
+    \Drupal::moduleHandler()->alter('simpletest', $this->testClasses);
+
+    return $this->testClasses;
+  }
+
+  /**
+   * Discovers all test classes in all available extensions.
+   *
+   * @return array
+   *   A classmap containing all discovered test classes.
+   */
+  protected function findTestClasses() {
+    $classmap = array();
+    foreach ($this->registerTestNamespaces() as $namespace => $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.
+   */
+  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;
+
+    $iterator = new \RecursiveDirectoryIterator($path, $flags);
+    $filter = new \RecursiveCallbackFilterIterator($iterator, function ($current, $key, $iterator) {
+      if ($iterator->hasChildren()) {
+        return TRUE;
+      }
+      return $current->isFile() && $current->getExtension() === 'php';
+    });
+    $files = new \RecursiveIteratorIterator($filter);
+    $classes = array();
+    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 \ReflectionClass $class
+   *   The reflected test class.
+   *
+   * @return array
+   *   An associative array containing:
+   *   - name: The test class name.
+   *   - description: The test (phpDoc) summary.
+   *   - group: A human-readable group name parsed from a @group class phpDoc
+   *     annotation. (Only one at this point.)
+   *   - requires: An associative array containing requirements:
+   *     - module: A list of Drupal module extension names that the test depends
+   *       on.
+   */
+  public static function getTestInfo(\ReflectionClass $class) {
+    $classname = $class->getName();
+    $info = array(
+      'name' => $classname,
+      'description' => static::parseTestClassSummary($class),
+    );
+    // For now, only @group and @requires are supported.
+    $annotations = static::parseTestClassAnnotations($class);
+    $info += array_intersect_key($annotations, array('group' => 1, 'requires' => 1));
+
+    // Automatically convert @coversDefaultClass into description.
+    if (isset($annotations['coversDefaultClass'][0])) {
+      $info['description'] = 'Tests ' . $annotations['coversDefaultClass'][0] . '.';
+    }
+
+    // @todo Remove support for legacy getInfo() methods.
+    if ($class->hasMethod('getInfo')) {
+      $legacy_info = $classname::getInfo();
+      if (isset($legacy_info['group'])) {
+        $info['group'][] = $legacy_info['group'];
+      }
+      if (isset($legacy_info['dependencies'])) {
+        $info += array('requires' => array());
+        $info['requires'] += array('module' => array());
+        $info['requires']['module'] = array_merge($info['requires']['module'], $legacy_info['dependencies']);
+      }
+    }
+
+    // For now, all PHPUnit tests are forced into a single PHPUnit group.
+    if ($class->isSubclassOf('PHPUnit_Framework_TestCase')) {
+      $info['group'] = 'PHPUnit';
+    }
+    // For now, only one @group is supported.
+    // @todo Support multiple @groups + change UI to expose a group select
+    //   dropdown to filter tests by group instead of collapsible table groups.
+    elseif (empty($info['group'])) {
+      throw new \LogicException("Missing @group for $classname.");
+    }
+    else {
+      $info['group'] = reset($info['group']);
+    }
+
+    return $info;
+  }
+
+  /**
+   * Parses the phpDoc summary line of a test class.
+   *
+   * @param \ReflectionClass $class
+   *   The reflected test class.
+   *
+   * @return string
+   *   The parsed phpDoc summary line.
+   *
+   * @todo Rethink this implementation; a single preg_match_all() may be faster.
+   */
+  public static function parseTestClassSummary(\ReflectionClass $class) {
+    $phpDoc = $class->getDocComment();
+    // Normalize line endings.
+    $phpDoc = preg_replace('/\r\n|\r/', '\n', $phpDoc);
+    // Strip leading and trailing doc block lines.
+    //$phpDoc = trim($phpDoc, "* /\n");
+    $phpDoc = substr($phpDoc, 4, -4);
+
+    // Extract actual phpDoc content.
+    $phpDoc = explode("\n", $phpDoc);
+    array_walk($phpDoc, function (&$value) {
+      $value = trim($value, "* /\n");
+    });
+
+    // Extract summary; allowed to it wrap and continue on next line.
+    list($summary) = explode("\n\n", implode("\n", $phpDoc));
+    if ($summary === '') {
+      throw new \LogicException(sprintf('Missing phpDoc on %s.', $class->getName()));
+    }
+    return $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());
+
+    // @todo Enhance PHPUnit upstream to allow for custom @requires identifiers.
+    // @see PHPUnit_Util_Test::getRequirements()
+    // @todo Add support for 'PHP', 'OS', 'function', 'extension'.
+    if (isset($annotations['class']['requires'])) {
+      foreach ($annotations['class']['requires'] as $i => $value) {
+        list($type, $value) = explode(' ', $value, 2);
+        if ($type == 'module') {
+          $annotations['class']['requires']['module'][$value] = $value;
+        }
+        unset($annotations['class']['requires'][$i]);
+      }
+    }
+    return $annotations['class'];
+  }
+
+  /**
+   * Returns all available extensions.
+   *
+   * @return \Drupal\Core\Extension\Extension[]
+   *   An array of Extension objects, keyed by extension name.
+   */
+  protected function getExtensions() {
+    $listing = new ExtensionDiscovery();
+    // Ensure that tests in all profiles are discovered.
+    $listing->setProfileDirectories(array());
+    $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/InstallationProfileModuleTestsTest.php b/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
index 994fb36..5a4dfcf 100644
--- a/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
+++ b/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
@@ -55,7 +55,7 @@ function setUp() {
    */
   function testInstallationProfileTests() {
     $this->drupalGet('admin/config/development/testing');
-    $this->assertText('Installation profile module tests helper');
+    $this->assertText('Drupal\drupal_system_listing_compatible_test\Tests\SystemListingCompatibleTest');
     $edit = array(
       'tests[Drupal\drupal_system_listing_compatible_test\Tests\SystemListingCompatibleTest]' => TRUE,
     );
diff --git a/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php b/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
index aa848e1..76bf168 100644
--- a/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
+++ b/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
@@ -63,8 +63,7 @@ function testOtherInstallationProfile() {
 
     // Assert the existence of a test for a module in a different installation
     // profile than the current.
-    $this->drupalGet('admin/config/development/testing');
-    $this->assertText('Installation profile module tests helper');
+    $this->assertText('Drupal\drupal_system_listing_compatible_test\Tests\SystemListingCompatibleTest');
   }
 
 }
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 8eff2a2..b987ac7 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -58,13 +58,13 @@
   foreach ($groups as $group => $tests) {
     echo $group . "\n";
     foreach ($tests as $class => $info) {
-      echo " - " . $info['name'] . ' (' . $class . ')' . "\n";
+      echo " - $class\n";
     }
   }
   exit;
 }
 
-$test_list = simpletest_script_get_test_list();
+$test_list = array_fill(0, 20, 'Drupal\simpletest\Tests\BrokenSetUpTest');
 
 // Try to allocate unlimited time to run the tests.
 drupal_set_time_limit(0);
@@ -561,7 +561,6 @@ function simpletest_script_setup_database($new = FALSE) {
  */
 function simpletest_script_get_all_tests($module = NULL) {
   $tests = simpletest_test_get_all($module);
-  $tests['PHPUnit'] = simpletest_phpunit_get_available_tests($module);
   return $tests;
 }
 
