diff --git a/composer.json b/composer.json
index 8a78ac5..bfc9c34 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,8 @@
     "easyrdf/easyrdf": "0.8.*",
     "phpunit/phpunit": "4.1.*",
     "phpunit/phpunit-mock-objects": "dev-master#e60bb929c50ae4237aaf680a4f6773f4ee17f0a2",
-    "zendframework/zend-feed": "2.2.*"
+    "zendframework/zend-feed": "2.2.*",
+    "sun/staticreflection": "dev-master"
   },
   "autoload": {
     "psr-4": {
diff --git a/composer.lock b/composer.lock
index e99a1b9..485e3bd 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "d89a37ea785ca09523298ff00ade2eca",
+    "hash": "27fae8d4b8021fda1efcde568835e30f",
     "packages": [
         {
             "name": "doctrine/annotations",
@@ -1376,6 +1376,54 @@
             "time": "2014-03-07 15:35:33"
         },
         {
+            "name": "sun/staticreflection",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sun/staticreflection.git",
+                "reference": "de13aeefe84d9363ba95babd656d4f467e261b4e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sun/staticreflection/zipball/de13aeefe84d9363ba95babd656d4f467e261b4e",
+                "reference": "de13aeefe84d9363ba95babd656d4f467e261b4e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-reflection": "*",
+                "ext-tokenizer": "*",
+                "php": ">=5.4.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Sun\\StaticReflection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Daniel F. Kudwien (sun)",
+                    "email": "sun@unleashedmind.com"
+                }
+            ],
+            "description": "Static PHP class code reflection for post-discovery scenarios.",
+            "keywords": [
+                "annotations",
+                "autoload",
+                "discovery",
+                "docblock",
+                "parser",
+                "phpdoc",
+                "reflection",
+                "token"
+            ],
+            "time": "2014-06-24 14:18:07"
+        },
+        {
             "name": "symfony-cmf/routing",
             "version": "1.1.0",
             "target-dir": "Symfony/Cmf/Component/Routing",
@@ -2393,12 +2441,8 @@
             "time": "2013-06-12 19:46:58"
         }
     ],
-    "packages-dev": [
-
-    ],
-    "aliases": [
-
-    ],
+    "packages-dev": [],
+    "aliases": [],
     "minimum-stability": "stable",
     "stability-flags": {
         "symfony/yaml": 20,
@@ -2406,12 +2450,11 @@
         "doctrine/annotations": 20,
         "kriswallsmith/assetic": 15,
         "symfony-cmf/routing": 15,
-        "phpunit/phpunit-mock-objects": 20
+        "phpunit/phpunit-mock-objects": 20,
+        "sun/staticreflection": 20
     },
     "platform": {
         "php": ">=5.4.2"
     },
-    "platform-dev": [
-
-    ]
+    "platform-dev": []
 }
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/locale/src/Tests/LocaleLocaleLookupTest.php b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php
index 908ff19..df32c02 100644
--- a/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php
+++ b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php
@@ -10,6 +10,9 @@
 use Drupal\Core\Language\Language;
 use Drupal\simpletest\WebTestBase;
 
+/**
+ * Tests that LocaleLookup does not cause circular references.
+ */
 class LocaleLocaleLookupTest extends WebTestBase {
 
   /**
diff --git a/core/modules/migrate/tests/src/process/CallbackTest.php b/core/modules/migrate/tests/src/process/CallbackTest.php
index c4ace45..1a29ab9 100644
--- a/core/modules/migrate/tests/src/process/CallbackTest.php
+++ b/core/modules/migrate/tests/src/process/CallbackTest.php
@@ -10,6 +10,8 @@
 use Drupal\migrate\Plugin\migrate\process\Callback;
 
 /**
+ * Tests the callback process plugin.
+ *
  * @group migrate
  * @group Drupal
  */
diff --git a/core/modules/migrate/tests/src/process/ConcatTest.php b/core/modules/migrate/tests/src/process/ConcatTest.php
index 22be55e..a189fe1 100644
--- a/core/modules/migrate/tests/src/process/ConcatTest.php
+++ b/core/modules/migrate/tests/src/process/ConcatTest.php
@@ -10,6 +10,8 @@
 use Drupal\migrate\Plugin\migrate\process\Concat;
 
 /**
+ * Tests the concat process plugin.
+ *
  * @group migrate
  * @group Drupal
  */
diff --git a/core/modules/migrate/tests/src/process/GetTest.php b/core/modules/migrate/tests/src/process/GetTest.php
index a268e13..daedc08 100644
--- a/core/modules/migrate/tests/src/process/GetTest.php
+++ b/core/modules/migrate/tests/src/process/GetTest.php
@@ -10,6 +10,8 @@
 use Drupal\migrate\Row;
 
 /**
+ * Tests the get process plugin.
+ *
  * @group migrate
  * @group Drupal
  */
diff --git a/core/modules/migrate/tests/src/process/StaticMapTest.php b/core/modules/migrate/tests/src/process/StaticMapTest.php
index 774afba..9044526 100644
--- a/core/modules/migrate/tests/src/process/StaticMapTest.php
+++ b/core/modules/migrate/tests/src/process/StaticMapTest.php
@@ -9,6 +9,8 @@
 use Drupal\migrate\Plugin\migrate\process\StaticMap;
 
 /**
+ * Tests the static map process plugin.
+ *
  * @group migrate
  * @group Drupal
  */
diff --git a/core/modules/migrate_drupal/tests/src/source/VariableTest.php b/core/modules/migrate_drupal/tests/src/source/VariableTest.php
index b5da66b..9b1f0b7 100644
--- a/core/modules/migrate_drupal/tests/src/source/VariableTest.php
+++ b/core/modules/migrate_drupal/tests/src/source/VariableTest.php
@@ -10,6 +10,8 @@
 use Drupal\migrate\Tests\MigrateSqlSourceTestCase;
 
 /**
+ * Tests the variable source plugin.
+ *
  * @group migrate_drupal
  * @group Drupal
  */
diff --git a/core/modules/migrate_drupal/tests/src/source/d6/Drupal6SqlBaseTest.php b/core/modules/migrate_drupal/tests/src/source/d6/Drupal6SqlBaseTest.php
index b8c7c96..5cb192e 100644
--- a/core/modules/migrate_drupal/tests/src/source/d6/Drupal6SqlBaseTest.php
+++ b/core/modules/migrate_drupal/tests/src/source/d6/Drupal6SqlBaseTest.php
@@ -10,6 +10,8 @@
 use Drupal\migrate\Tests\MigrateTestCase;
 
 /**
+ * Tests the D6 SQL base class.
+ *
  * @group migrate_drupal
  * @group Drupal
  */
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 96ab21b..9911234 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -5,6 +5,8 @@
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Render\Element;
 use Drupal\simpletest\TestBase;
+use Drupal\simpletest\TestDiscovery;
+use Sun\StaticReflection\ReflectionClass;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Process\PhpExecutableFinder;
 
@@ -88,40 +90,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 +291,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 +418,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 +431,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,60 +599,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();
-    /** @var $test_suite \PHPUnit_Framework_TestSuite[] */
-    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 = $test->getName();
-      if (!array_key_exists($name, $test_classes) && (!$module || substr($name, 0, $n) == $prefix)) {
-        $test_classes[$name] = $name::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 0987d3d..ad3886e 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -207,27 +207,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..6377cd3
--- /dev/null
+++ b/core/modules/simpletest/src/TestDiscovery.php
@@ -0,0 +1,342 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\simpletest\TestDiscovery.
+ */
+
+namespace Drupal\simpletest;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Sun\StaticReflection\ReflectionClass;
+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();
+
+    // Add PHPUnit test namespace of Drupal core.
+    $this->testNamespaces['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])) {
+        $loader->addPsr4($base_namespace, "$base_path/src");
+      }
+      // Add Simpletest test namespace.
+      $this->testNamespaces[$base_namespace . 'Tests\\'][] = "$base_path/src/Tests";
+
+      // Add PHPUnit test namespace.
+      // @todo Move PHPUnit namespace of extensions into Drupal\Tests\$name.
+      // @see https://drupal.org/node/2260121
+      $this->testNamespaces[$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 ($this->testNamespaces as $prefix => $paths) {
+      $loader->addPsr4($prefix, $paths);
+    }
+
+    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) {
+      $class = new ReflectionClass($classname, $pathname);
+
+      // Skip interfaces, abstract classes, and traits.
+      if (!$class->isInstantiable()) {
+        continue;
+      }
+      // Skip non-test classes.
+      // For performance reasons, test the most common base classes first.
+      $is_test = $is_phpunit = $class->isSubclassOfAny(array(
+        'PHPUnit_Framework_TestCase',
+        'Drupal\Tests\UnitTestCase',
+      ));
+      $is_test = $is_test || $class->isSubclassOfAny(array(
+        'Drupal\simpletest\TestBase',
+        'Drupal\simpletest\UnitTestBase',
+        'Drupal\simpletest\KernelTestBase',
+        'Drupal\simpletest\DrupalUnitTestBase',
+        'Drupal\simpletest\WebTestBase',
+        'Drupal\simpletest\InstallerTestBase',
+      ));
+      // Searching for these triggers autoloading of all dependencies.
+      $is_test = $is_test || $class->isSubclassOf('Drupal\simpletest\TestBase');
+      $is_test = $is_test || $is_phpunit = $class->isSubclassOf('PHPUnit_Framework_TestCase');
+      if (!$is_test) {
+        continue;
+      }
+      $info = static::getTestInfo($class, $is_phpunit);
+
+      // Skip this test class if it requires unavailable modules.
+      // @todo PHPUnit skips tests with unmet requirements when executing a test
+      //   (instead of excluding them upfront). Refactor test runner to follow
+      //   that approach.
+      if (!empty($info['requires']['module'])) {
+        if (array_diff($info['requires']['module'], $this->availableExtensions['module'])) {
+          continue;
+        }
+      }
+
+      $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 => $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.
+   *
+   * @todo Limit to filenames with 'Test' suffix.
+   */
+  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 \Sun\StaticReflection\ReflectionClass $class
+   *   The reflected test class.
+   * @param bool $is_phpunit
+   *   (optional) Whether the test class is a PHPUnit test.
+   *
+   * @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, $is_phpunit = FALSE) {
+    $classname = $class->getName();
+    $info = array(
+      'name' => $classname,
+    );
+    $annotations = $class->parseDocComment();
+
+    // Automatically convert @coversDefaultClass into summary.
+    if (isset($annotations['coversDefaultClass'][0])) {
+      $info['description'] = 'Tests ' . $annotations['coversDefaultClass'][0] . '.';
+    }
+    else {
+      if (empty($annotations['summary'])) {
+        throw new \LogicException(sprintf('Missing PHPDoc summary line on %s in %s.', $classname, $class->getFileName()));
+      }
+      $info['description'] = $annotations['summary'];
+    }
+
+    // Reduce to @group and @requires.
+    $info += array_intersect_key($annotations, array('group' => 1, 'requires' => 1));
+
+    // @todo Add support for 'PHP', 'OS', 'function', 'extension'.
+    if (isset($info['requires'])) {
+      foreach ($info['requires'] as $i => $value) {
+        list($type, $value) = explode(' ', $value, 2);
+        if ($type === 'module') {
+          $info['requires']['module'][$value] = $value;
+          unset($info['requires'][$i]);
+        }
+      }
+    }
+
+    // @todo Remove legacy getInfo() methods.
+    if (method_exists($classname, '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']);
+      }
+    }
+
+    // Process @group information.
+    // @todo Support multiple @groups + change UI to expose a group select
+    //   dropdown to filter tests by group instead of collapsible table rows.
+    // @todo Replace single enforced PHPUnit group with base class groups.
+    if ($is_phpunit) {
+      $info['group'] = 'PHPUnit';
+    }
+    else {
+      if (empty($info['group'])) {
+        throw new \LogicException("Missing @group for $classname.");
+      }
+      $info['group'] = reset($info['group']);
+    }
+
+    return $info;
+  }
+
+  /**
+   * 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..b99ef44 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -58,7 +58,7 @@
   foreach ($groups as $group => $tests) {
     echo $group . "\n";
     foreach ($tests as $class => $info) {
-      echo " - " . $info['name'] . ' (' . $class . ')' . "\n";
+      echo " - $class\n";
     }
   }
   exit;
@@ -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;
 }
 
diff --git a/core/tests/Drupal/Tests/Core/Path/PathMatcherTest.php b/core/tests/Drupal/Tests/Core/Path/PathMatcherTest.php
index 6c43478..6627c29 100644
--- a/core/tests/Drupal/Tests/Core/Path/PathMatcherTest.php
+++ b/core/tests/Drupal/Tests/Core/Path/PathMatcherTest.php
@@ -12,6 +12,8 @@
 use Drupal\Tests\UnitTestCase;
 
 /**
+ * Tests that path matching is working properly.
+ *
  * @group Drupal
  * @see \Drupal\Core\Path\PathMatcher
  */
diff --git a/core/vendor/composer/autoload_psr4.php b/core/vendor/composer/autoload_psr4.php
index f832317..f96fb66 100644
--- a/core/vendor/composer/autoload_psr4.php
+++ b/core/vendor/composer/autoload_psr4.php
@@ -6,6 +6,7 @@
 $baseDir = dirname(dirname($vendorDir));
 
 return array(
+    'Sun\\StaticReflection\\' => array($vendorDir . '/sun/staticreflection/src'),
     'GuzzleHttp\\Stream\\' => array($vendorDir . '/guzzlehttp/streams/src'),
     'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
     'Drupal\\Driver\\' => array($baseDir . '/drivers/lib/Drupal/Driver'),
diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json
index c49881a..b318b4a 100644
--- a/core/vendor/composer/installed.json
+++ b/core/vendor/composer/installed.json
@@ -2470,5 +2470,55 @@
             "mock",
             "xunit"
         ]
+    },
+    {
+        "name": "sun/staticreflection",
+        "version": "dev-master",
+        "version_normalized": "9999999-dev",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/sun/staticreflection.git",
+            "reference": "de13aeefe84d9363ba95babd656d4f467e261b4e"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/sun/staticreflection/zipball/de13aeefe84d9363ba95babd656d4f467e261b4e",
+            "reference": "de13aeefe84d9363ba95babd656d4f467e261b4e",
+            "shasum": ""
+        },
+        "require": {
+            "ext-reflection": "*",
+            "ext-tokenizer": "*",
+            "php": ">=5.4.2"
+        },
+        "time": "2014-06-24 14:18:07",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Sun\\StaticReflection\\": "src/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "MIT"
+        ],
+        "authors": [
+            {
+                "name": "Daniel F. Kudwien (sun)",
+                "email": "sun@unleashedmind.com"
+            }
+        ],
+        "description": "Static PHP class code reflection for post-discovery scenarios.",
+        "keywords": [
+            "annotations",
+            "autoload",
+            "discovery",
+            "docblock",
+            "parser",
+            "phpdoc",
+            "reflection",
+            "token"
+        ]
     }
 ]
diff --git a/core/vendor/sun/staticreflection/LICENSE b/core/vendor/sun/staticreflection/LICENSE
new file mode 100644
index 0000000..96d2915
--- /dev/null
+++ b/core/vendor/sun/staticreflection/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Daniel F. Kudwien (sun) <sun@unleashedmind.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/core/vendor/sun/staticreflection/README.md b/core/vendor/sun/staticreflection/README.md
new file mode 100644
index 0000000..f458dbd
--- /dev/null
+++ b/core/vendor/sun/staticreflection/README.md
@@ -0,0 +1,255 @@
+# StaticReflection [![Build Status](https://travis-ci.org/sun/staticreflection.svg)](https://travis-ci.org/sun/staticreflection) [![Code Coverage](https://scrutinizer-ci.com/g/sun/staticreflection/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/sun/staticreflection/?branch=master) [![Code Quality](https://scrutinizer-ci.com/g/sun/staticreflection/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/sun/staticreflection/?branch=master)
+_Static PHP class code reflection for post-discovery scenarios._
+
+This utility library for PHP frameworks/applications allows to reflect PHP class
+files (headers) without loading them into memory, if the filesystem locations of
+each class is known already _(cf. discovery/classmap)_.
+
+Static reflection is useful in case you want to check a _large list_ of
+previously discovered class files for common aspects, like interfaces or base
+classes, in order to filter out inapplicable candidates.
+
+In a decoupled, modern code base, native [PHP Reflection] can grow out of
+control very easily, as it loads every reflected class and interface, as well as
+every dependency of every class and interface. — Static reflection avoids to
+(auto-)load all dependencies and ancestor classes of each reflected class into
+memory.
+
+High memory consumption can be a problem, because PHP's default memory limit is
+very small.  Even if it was increased, just loading hundreds or even thousands
+of classes may easily exceed a custom limit, too.
+
+When reflecting 1,000+ classes (e.g., tests), then, on average, 3x times more
+PHP files are loaded into memory, which can result in a peak memory consumption
+of 120+ MB by the total loaded code only — even though you only care for the
+filtered 1k.
+
+In the worst/ideal case, you're just trying to generate a _list_ of
+available classes, without using them immediately (e.g., for a UI/CLI selection
+or swappable/configurable plugin implementations).
+
+`ReflectionClass` provides the same API as the native `\ReflectionClass`.
+
+Example [xhprof](http://php.net/manual/en/book.xhprof.php) diff results:
+
+1,538 candidate classes, of which 180 are filtered out (since abstract, traits,
+interfaces, or other helper classes):
+
+|       | \ReflectionClass | ReflectionClass | Diff | Diff% |
+| ----- | ----------------:| ---------------:| ----:| -----:|
+| Number of Function Calls | 64,747 | 202,783 | 138,036 | 213.2%
+| Incl. Wall Time (microsec) | 2,514,801 | 3,272,539 |757,738 | 30.1%
+| Incl. CPU (microsecs) | 2,480,415 | 3,120,020 | 639,605 | 25.8%
+| Incl. MemUse (bytes) | 108,805,120 | 10,226,160 | -98,578,960 | -90.6%
+| Incl. PeakMemUse (bytes) | 108,927,216 | 10,347,608 | -98,579,608 | **-90.5%**
+
+
+## Usage Example
+
+1. Some (arbitrary) discovery, producing a classmap:  
+    _(…you may skip this.)_
+
+    ```php
+    use Sun\StaticReflection\ReflectionClass;
+
+    $loader = require __DIR__ . '/vendor/autoload.php';
+
+    // This working example does not have a known classmap upfront, so:
+    // Generate one, utilizing the ClassLoader.
+    $prefixes = $loader->getPrefixesPsr4();
+
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+
+    $classmap = array();
+    foreach ($prefixes as $namespace_prefix => $paths) {
+      foreach ($paths as $path) {
+        $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);
+        foreach ($files as $fileinfo) {
+          $class = $namespace_prefix;
+          if ('' !== $subpath = $fileinfo->getSubPath()) {
+            $class .= strtr($subpath, '/', '\\') . '\\';
+          }
+          $class .= $fileinfo->getBasename('.php');
+          $classmap[$class] = $fileinfo->getPathname();
+        }
+      }
+    }
+    echo json_encode($classmap, JSON_PRETTY_PRINT);
+    ```
+
+    ```json
+    {
+      "Sun\StaticReflection\ReflectionClass": "src/ReflectionClass.php",
+      "Sun\Tests\StaticReflection\ReflectionClassTest": "tests/src/ReflectionClassTest.php",
+      "Sun\Tests\StaticReflection\Fixtures\Example": "tests/fixtures/Example.php",
+      "Sun\Tests\StaticReflection\Fixtures\Base\ImportedInterface": "tests/fixtures/Base/ImportedInterface.php"
+      ...
+    }
+    ```
+    → You have a `classname => pathname` map.
+
+1. _The Real Meat:_ **Filter all discovered class files.**
+
+    ```php
+    $list = array();
+    foreach ($classmap as $classname => $pathname) {
+      // Note: This IS a \ReflectionClass, but does NOT construct one.
+      /** @var \Sun\StaticReflection\ReflectionClass */
+      $class = new ReflectionClass($classname, $pathname);
+
+      // Only include tests.
+      if (!$class->isSubclassOf('PHPUnit_Framework_TestCase')) {
+        continue;
+      }
+
+      // ...and (optionally) prepare them for a listing/later selection:
+      $doc = $class->parseDocComment();
+      $list[$classname] = array(
+        'summary' => $doc['summary'],
+        'covers' => $doc['coversDefaultClass'][0],
+      );
+    }
+    echo json_encode($list, JSON_PRETTY_PRINT);
+    ```
+
+    ```json
+    {
+      "Sun\Tests\StaticReflection\ReflectionClassTest": {
+        "summary": "Tests ReflectionClass.",
+        "covers": "\Sun\StaticReflection\ReflectionClass"
+      }
+    }
+    ```
+    → `ReflectionClass` achieved everything you wanted to achieve.
+
+    …but without using `\ReflectionClass` + loading everything into memory.
+
+1. Why this matters:
+
+    ```php
+    array_walk($classmap, function (&$pathname, $classname) {
+      $pathname = class_exists($classname, FALSE) || interface_exists($classname, FALSE);
+    });
+    echo json_encode($classmap, JSON_PRETTY_PRINT);
+    ```
+
+    ```json
+    {
+      "Sun\Tests\StaticReflection\ReflectionClassTest": false,
+      "Sun\Tests\StaticReflection\Fixtures\Example": false,
+      "Sun\Tests\StaticReflection\Fixtures\Example1Interface": true,
+      "Sun\Tests\StaticReflection\Fixtures\Base\Example": true,
+      ...
+    }
+    ```
+    → The reflected classes themselves did not get loaded.
+
+    However, you asked each class whether it is a subclass of _X_.  Due to
+    class/interface inheritance, the condition of _X_ may not necessarily be
+    within the "visible" scope of the statically reflected class (i.e., the
+    class file itself).  So what happened?
+
+    Instead of reflecting each class file itself, _only_ the **ancestors** of
+    each class/interface were introspected.
+
+    As an example, the following inheritance tree maps to the above
+    `Fixtures\Example` class:
+
+    ```
+    Sun\Tests\StaticReflection\Fixtures\Example
+    ∟ extends
+      ∟ Sun\Tests\StaticReflection\Fixtures\Base\Example
+        ∟ Sun\Tests\StaticReflection\Fixtures\Base\Root
+    ∟ implements
+      ∟ Sun\Tests\StaticReflection\Fixtures\Base\Example2Interface
+      ∟ Sun\Tests\StaticReflection\Fixtures\Base\ImportedInterface
+      ∟ Sun\Tests\StaticReflection\Fixtures\Example1Interface
+        ∟ Sun\Tests\StaticReflection\Fixtures\Base\InvisibleInterface
+    ```
+    → Only the parent class and interfaces _(ancestors)_ were autoloaded.
+
+    That is, because the full stack of their dependencies was not directly
+    _"visible"_ in the statically reflected code.
+
+
+_Pro-Tip:_ To filter for a unique parent/root class/interface, use
+`ReflectionClass::isSubclassOfAny()` to check for the most common/expected
+parent classes and only compare against the statically reflected information
+first.  Only proceed to `isSubclassOf()` in case you want/need to check
+further; e.g.:
+
+```php
+// Static reflection.
+if (!$class->isSubclassOfAny(['Condition\CommonFlavor', 'Condition\AltFlavor'])) {
+  continue;
+}
+// Native reflection of ancestors (if the reflected class has any).
+if (!$class->isSubclassOf('Condition\Absolute\Root')) {
+  continue;
+}
+```
+
+
+## Requirements
+
+* PHP >=5.4.2
+
+
+## Limitations
+
+1. Only one class/interface/trait per file (PSR-2, PSR-0/PSR-4).
+
+1. `implementsInterface($interface)` returns TRUE even if `$interface` is a
+    class.
+
+1. `\ReflectionClass::IS_IMPLICIT_ABSTRACT` is not supported, since methods are
+    not analyzed. (only the file header is analyzed)
+
+1. `\ReflectionClass::$name` is read-only and thus not available. Use
+    `getName()` instead.
+
+1. Calling any other `\ReflectionClass` methods that are not implemented (yet)
+    causes a **fatal error**.
+
+    The parent `\ReflectionClass` class might be dynamically instantiated
+    on-demand in the future.  `ReflectionClass` does implement all methods that
+    can be technically supported already.
+
+
+## Notes
+
+* Technically, StaticReflection is able to work around bytecode caches that are
+    stripping off comments.  
+    _(…just in case that even counts as an issue today)_
+
+
+## Inspirations
+
+Static/Reflection:
+
+* Doctrine's (Static) [Reflection](https://github.com/doctrine/common/tree/master/lib/Doctrine/Common/Reflection)
+* Zend Framework's [Reflection](https://github.com/zendframework/zf2/tree/master/library/Zend/Server/Reflection)
+
+PHPDoc tags/annotations parsing:
+
+* PHPUnit's [Util\Test](https://github.com/sebastianbergmann/phpunit/blob/master/src/Util/Test.php)
+* Doctrine's [Annotations](https://github.com/doctrine/annotations/tree/master/lib/Doctrine/Common/Annotations)
+* phpDocumentor's [Descriptor](https://github.com/phpDocumentor/phpDocumentor2/tree/develop/src/phpDocumentor/Descriptor)
+* Shira's [DocComment](https://github.com/ShiraNai7/php-doc-comment)
+* Philip Graham's [Annotations](https://github.com/pgraham/php-annotations)
+
+
+## License
+
+[MIT](LICENSE) — Copyright (c) 2014 Daniel F. Kudwien (sun)
+
+
+[PHP Reflection]: http://php.net/manual/en/book.reflection.php
diff --git a/core/vendor/sun/staticreflection/composer.json b/core/vendor/sun/staticreflection/composer.json
new file mode 100644
index 0000000..491e3bc
--- /dev/null
+++ b/core/vendor/sun/staticreflection/composer.json
@@ -0,0 +1,27 @@
+{
+    "name": "sun/staticreflection",
+    "description": "Static PHP class code reflection for post-discovery scenarios.",
+    "license": "MIT",
+    "keywords": [ "reflection", "token", "parser", "discovery", "autoload", "docblock", "phpdoc", "annotations" ],
+    "authors": [
+        { "name": "Daniel F. Kudwien (sun)", "email": "sun@unleashedmind.com" }
+    ],
+    "require": {
+        "php": ">=5.4.2",
+        "ext-tokenizer": "*",
+        "ext-Reflection": "*"
+    },
+    "require-dev": {
+    },
+    "autoload": {
+        "psr-4": {
+            "Sun\\StaticReflection\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Sun\\Tests\\StaticReflection\\": "tests/src/",
+            "Sun\\Tests\\StaticReflection\\Fixtures\\": "tests/fixtures/"
+        }
+    }
+}
diff --git a/core/vendor/sun/staticreflection/src/ReflectionClass.php b/core/vendor/sun/staticreflection/src/ReflectionClass.php
new file mode 100644
index 0000000..df306d6
--- /dev/null
+++ b/core/vendor/sun/staticreflection/src/ReflectionClass.php
@@ -0,0 +1,593 @@
+<?php
+
+/**
+ * @file
+ * Contains \Sun\StaticReflection\ReflectionClass.
+ */
+
+namespace Sun\StaticReflection;
+
+/**
+ * Statically reflects a PHP class.
+ *
+ * Use this class when operating on many PHP class files that have been
+ * discovered upfront and which need to be minimally validated at the
+ * class-level (e.g., testing for base classes/interfaces).
+ *
+ * Native PHP facilities like \ReflectionClass and is_subclass_of() would:
+ * 1. trigger the classloader to autoload each file
+ * 2. trigger the classloader to recursively autoload all parent classes and
+ *    interfaces
+ * 3. exceed reasonable CPU and memory consumption very quickly.
+ *
+ * Usage is identical to \ReflectionClass. For that reason (and type-hint
+ * compatibility), this class wraps \ReflectionClass.
+ *
+ * Note: The read-only public property \ReflectionClass::$name does not get
+ * populated by this implementation. Use ReflectionClass::getName() instead.
+ *
+ * Optionally, the doc comment block of the statically reflected class can be
+ * parsed for its PHPDoc summary line as well as (simple) tags/annotations.
+ *
+ * @author Daniel F. Kudwien (sun)
+ *
+ * @todo Dynamically instantiate the wrapped \ReflectionClass in case a parent
+ *   method requiring native/non-static reflection is called.
+ */
+class ReflectionClass extends \ReflectionClass {
+
+  private $classname;
+  private $pathname;
+  private $info;
+  private static $ancestorCache = array();
+
+  /**
+   * Constructs a new ReflectionClass.
+   *
+   * @param string $classname
+   *   The fully-qualified class name (FQCN) to reflect.
+   * @param string $pathname
+   *   The pathname of the file containing $classname. If omitted, a native
+   *   \ReflectionClass will be instantiated.
+   *
+   * @throws \ReflectionException
+   *   If the given $classname is not located in the given $pathname.
+   */
+  public function __construct($classname, $pathname = NULL) {
+    // If the pathname is unknown, it must be retrieved from \ReflectionClass.
+    // If a class instance was passed then there's no point in omitting
+    // \ReflectionClass, since code has been loaded already.
+    if (!isset($pathname) || !is_string($classname)) {
+      parent::__construct($classname);
+      $this->classname = is_object($classname) ? get_class($classname) : $classname;
+      $this->pathname = parent::getFileName();
+    }
+    else {
+      $this->classname = $classname;
+      $this->pathname = $pathname;
+    }
+
+    // Resemble \ReflectionClass instantiation.
+    $this->info = $this->reflect();
+  }
+
+  /**
+   * Statically reflects the PHP class file.
+   */
+  protected function reflect() {
+    $content = $this->readFileHeader();
+    $info = self::tokenize($content);
+
+    if ($info['fqcn'] !== $this->classname) {
+      throw new \ReflectionException(vsprintf('Expected %s but found %s in %s.', array(
+        $this->classname,
+        $info['fqcn'],
+        $this->pathname,
+      )));
+    }
+    return $info;
+  }
+
+  /**
+   * Reads the PHP class file header.
+   *
+   * @todo Throw \ReflectionException on 404.
+   */
+  private function readFileHeader() {
+    $content = '';
+
+    // \SplFileObject is very resource-intensive when operating on thousands of
+    // files. Use legacy functions until PHP core improves.
+    $file = fopen($this->pathname, 'r');
+    while (FALSE !== $line = fgets($file)) {
+      $content .= $line;
+      if (preg_match('@^\s*(?:(?:abstract|final)\s+)?(?:interface|class|trait)\s+\w+@', $line)) {
+        break;
+      }
+    }
+    fclose($file);
+    unset($file);
+
+    // Strip '{' and (most importantly trailing) whitespace from definition.
+    $content = trim($content, " \t\r\n{");
+    return $content;
+  }
+
+  /**
+   * Tokenizes the file (header) content of a PHP class file.
+   *
+   * @param string $content
+   *   The PHP file (header) content to tokenize.
+   *
+   * @return array
+   *   An associative array containing the parsed results, keyed by PHP
+   *   Tokenizer tokens:
+   *   - fqcn: The FQCN of the parsed element.
+   *   - T_NAMESPACE: The namespace (if any).
+   *   - One of T_CLASS, T_INTERFACE, T_TRAIT: The FQCN of the parsed element
+   *     (the other two will be FALSE).
+   *   - T_EXTENDS, T_IMPLEMENTS: FQCNs of ancestors.
+   *   - T_USE: Imported namespaces (if any), keyed by local alias.
+   *   - T_ABSTRACT, T_FINAL: Respective Boolean flags.
+   *   - T_DOC_COMMENT: The doc comment block of the class.
+   *
+   * This is a vastly simplified re-implementation of Doctrine's TokenParser.
+   * @see \Doctrine\Common\Annotations\TokenParser
+   *
+   * @todo Add public static utility method returning translated values.
+   */
+  private static function tokenize($content) {
+    $tokens = token_get_all($content);
+
+    $result = array(
+      'fqcn' => '',
+      T_DOC_COMMENT => array(),
+      T_NAMESPACE => '',
+      T_USE => array(),
+      T_AS => '',
+      T_ABSTRACT => FALSE,
+      T_FINAL => FALSE,
+      T_CLASS => '',
+      T_INTERFACE => '',
+      T_TRAIT => '',
+      T_EXTENDS => array(),
+      T_IMPLEMENTS => array(),
+    );
+    /** @var mixed Reference to the last discovered result context. */
+    $context = NULL;
+    /** @var int   The ID of the last discovered result context token. */
+    $context_id = NULL;
+
+    foreach ($tokens as $token) {
+      if (is_array($token)) {
+        if (isset($result[$id = $token[0]])) {
+          // Enter a new context.
+          // For simple string contexts (e.g., T_NAMESPACE, T_CLASS) all code
+          // subsequent code is appended until either a PHP statement delimiter
+          // (e.g., ';') is encountered or new context is entered.
+          $context = &$result[$id];
+          $context_id = $id;
+
+          // All doc comments are recorded; the last one wins. (see below)
+          if ($id === T_DOC_COMMENT) {
+            $context[] = $token[1];
+            unset($context, $context_id);
+          }
+          // Create a new sub-element for contexts supporting multiple values.
+          elseif ($id === T_USE || $id === T_IMPLEMENTS || $id === T_EXTENDS) {
+            $context = &$context[];
+            $context = '';
+          }
+          // Boolean flags.
+          elseif ($id === T_ABSTRACT || $id === T_FINAL) {
+            $context = TRUE;
+            unset($context, $context_id);
+          }
+        }
+        // Not a result context; append content to last result context.
+        elseif (isset($context_id) && $id !== T_WHITESPACE && $id !== T_COMMENT) {
+          $context .= $token[1];
+        }
+      }
+      // Append simple strings to last result context.
+      elseif (isset($context_id)) {
+        // Create a new sub-element for T_IMPLEMENTS, T_EXTENDS, T_USE.
+        if ($token === ',') {
+          $context = &$result[$context_id][];
+          $context = '';
+        }
+        // Terminate last result context upon PHP statement delimiters.
+        elseif ($token === ';' || $token === '{') {
+          // When terminating 'use' or 'as', inject the local alias as key.
+          if ($context_id === T_AS) {
+            $import = array_pop($result[T_USE]);
+            $result[T_USE][$context] = $import;
+            $context = '';
+          }
+          elseif ($context_id === T_USE) {
+            $import = array_pop($result[T_USE]);
+            $result[T_USE][self::basename($import)] = $import;
+          }
+          unset($context, $context_id);
+        }
+      }
+    }
+    unset($result[T_AS]);
+
+    // The last doc comment belongs to the class.
+    $result[T_DOC_COMMENT] = end($result[T_DOC_COMMENT]) ?: '';
+
+    // Resolve class, parent class, interface, and ancestor names.
+    foreach (array(T_CLASS, T_INTERFACE, T_TRAIT) as $id) {
+      if ($result[$id] !== '') {
+        $result[$id] = self::resolveName($result[T_NAMESPACE], $result[$id]);
+        $result['fqcn'] = $result[$id];
+      }
+      else {
+        $result[$id] = FALSE;
+      }
+    }
+    foreach (array(T_EXTENDS, T_IMPLEMENTS) as $id) {
+      foreach ($result[$id] as &$ancestor) {
+        $ancestor = self::resolveName($result[T_NAMESPACE], $ancestor, $result[T_USE]);
+      }
+    }
+
+    return $result;
+  }
+
+  /**
+   * Resolves the name of an ancestor class.
+   *
+   * @param string $namespace
+   *   The namespace context of the parsed class.
+   * @param string $name
+   *   The name to resolve against $namespace.
+   * @param array $imports
+   *   An associative array of imported namespaces, keyed by local alias, as
+   *   parsed by ReflectionClass::tokenize().
+   *
+   * @return string
+   *   $name resolved against $namespace and $imports.
+   */
+  private static function resolveName($namespace, $name, array $imports = array()) {
+    // Strip namespace prefix, if any.
+    if ($name[0] === '\\') {
+      return substr($name, 1);
+    }
+    if ($imports) {
+      // If $name maps directly to an imported namespace alias, use its FQCN.
+      if (isset($imports[$name])) {
+        return $imports[$name];
+      }
+      // Otherwise, check whether $name up until the first namespace separator
+      // maps to an alias. If so, prefix $name with its FQCN.
+      $space = strtok($name, '\\');
+      if (isset($imports[$space])) {
+        return $imports[$space] . substr($name, strlen($space));
+      }
+    }
+    if ($namespace === '') {
+      return $name;
+    }
+    return $namespace . '\\' . $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDocComment() {
+    return $this->info[T_DOC_COMMENT];
+  }
+
+  /**
+   * Parses the doc block comment of the class.
+   *
+   * @return array
+   *   An associative array whose key 'summary' contains the PHPDoc summary line
+   *   of the class. Other keys contain the PHPDoc tags/annotations (if any).
+   *
+   * @todo Replace with separate getDocCommentSummary() + getAnnotations() methods.
+   */
+  public function parseDocComment() {
+    $docblock = $this->getDocComment();
+    $result = array();
+    $result['summary'] = self::parseSummary($docblock);
+    $result += self::parseAnnotations($docblock);
+    return $result;
+  }
+
+  /**
+   * Parses the summary line from the class doc comment block.
+   *
+   * @param string $docblock
+   *   The doc comment block to parse.
+   *
+   * @return string
+   *   The parsed PHPDoc summary line.
+   *
+   * @todo Split docblock cleaning/stripping into separate method.
+   */
+  private static function parseSummary($docblock) {
+    $content = preg_replace([
+      // Strip trailing '*/', leading '/**', and '*' prefixes.
+      '@^[ \t]*\*+/$|^[ \t]*/?\*+[ \t]*@m',
+      // Normalize line endings.
+      '@\r?\n@',
+      // Strip everything starting with the first PHPDoc tag/annotation.
+      '/^@.+/ms',
+    ], ['', "\n", ''], $docblock);
+
+    preg_match('@\n?(.+?)(?=\n\n)@s', $content, $matches);
+    if (isset($matches[1])) {
+      $summary = $matches[1];
+    }
+    else {
+      $summary = substr($content, 1);
+    }
+    return trim(strtr($summary, "\n", ' '));
+  }
+
+  /**
+   * Parses PHPDoc tags/annotations from the class doc comment block.
+   *
+   * @param string $docblock
+   *   The doc comment block to parse.
+   *
+   * @return array
+   *   The parsed annotations. Each value is an array of values.
+   *
+   * @see \PHPUnit_Util_Test::parseAnnotations()
+   * @author Sebastian Bergmann <sebastian@phpunit.de>
+   * @copyright 2001-2014 Sebastian Bergmann <sebastian@phpunit.de>
+   */
+  private static function parseAnnotations($docblock) {
+    $annotations = array();
+    // Strip away the docblock header and footer to ease parsing of one line
+    // annotations.
+    $docblock = substr($docblock, 3, -2);
+
+    if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docblock, $matches)) {
+      $numMatches = count($matches[0]);
+      for ($i = 0; $i < $numMatches; ++$i) {
+        $annotations[$matches['name'][$i]][] = $matches['value'][$i];
+      }
+    }
+    return $annotations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFileName() {
+    return $this->pathname;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Note that only interfaces implemented directly on the statically reflected
+   * class are returned.
+   */
+  public function getInterfaceNames() {
+    return $this->info[T_IMPLEMENTS];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getModifiers() {
+    $flags = 0;
+    if ($this->isAbstract()) {
+      $flags |= \ReflectionClass::IS_EXPLICIT_ABSTRACT;
+    }
+    if ($this->isFinal()) {
+      $flags |= \ReflectionClass::IS_FINAL;
+    }
+    return $flags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->classname;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNamespaceName() {
+    return $this->info[T_NAMESPACE];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getShortName() {
+    return self::basename($this->classname);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Note: This method returns TRUE even if $class is not an interface; i.e.,
+   * if the input is bogus. Static reflection should not be used if that level
+   * of accuracy is required.
+   */
+  public function implementsInterface($class) {
+    // Traits cannot implement interfaces.
+    if ($this->info[T_TRAIT]) {
+      return FALSE;
+    }
+    // An interface implements itself.
+    if ($class === $this->info['fqcn']) {
+      return TRUE;
+    }
+    return $this->isSubclassOf($class);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function inNamespace() {
+    return !empty($this->info[T_NAMESPACE]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAbstract() {
+    return $this->info[T_ABSTRACT];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isFinal() {
+    return $this->info[T_FINAL];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isInstantiable() {
+    return $this->info[T_CLASS] && !$this->info[T_ABSTRACT];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isInterface() {
+    return !empty($this->info[T_INTERFACE]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isInternal() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isIterateable() {
+    return array_intersect($this->info[T_IMPLEMENTS], array('Iterator', 'IteratorAggregate', 'Traversable')) || preg_grep('/Iterator$/', $this->info[T_IMPLEMENTS]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isSubclassOf($class) {
+    if (!$this->info[T_EXTENDS] && !$this->info[T_IMPLEMENTS]) {
+      return FALSE;
+    }
+    if ($class === $this->info['fqcn']) {
+      return FALSE;
+    }
+    // Check for a direct match first.
+    if ($this->isSubclassOfAny(array($class))) {
+      return TRUE;
+    }
+    // Otherwise, inspect all ancestors. This causes all interfaces and parent
+    // classes, and all of their dependencies to get autoloaded.
+    if ($this->isSubclassOfAnyAncestors($this->info[T_EXTENDS], $class)) {
+      return TRUE;
+    }
+    return $this->isSubclassOfAnyAncestors($this->info[T_IMPLEMENTS], $class);
+  }
+
+  /**
+   * Returns whether the statically reflected class is a subclass of one of the
+   * given classes.
+   *
+   * Same as isSubclassOf(), but allows to check multiple classes at once for a
+   * direct match (as an OR condition), so as to avoid autoloading of all parent
+   * classes and interfaces.
+   *
+   * Only use this as a separate precondition prior to calling
+   * ReflectionClass::isSubclassOf() in order to improve performance when
+   * testing many classes that commonly extend from certain base classes or
+   * implement certain interfaces.
+   *
+   * @param string[] $classes
+   *   A list of FQCNs to test against the statically reflected class.
+   *
+   * @return bool
+   *   TRUE if the statically reflected class is a subclass of any class in
+   *   $classes, FALSE otherwise. Note that indirect ancestor classes are NOT
+   *   resolved; only direct matches in the statically reflected class may be
+   *   found.
+   *
+   * @see ReflectionClass::isSubclassOf()
+   */
+  public function isSubclassOfAny(array $classes) {
+    if ($this->info[T_EXTENDS] && array_intersect($this->info[T_EXTENDS], $classes)) {
+      return TRUE;
+    }
+    return $this->info[T_IMPLEMENTS] && array_intersect($this->info[T_IMPLEMENTS], $classes);
+  }
+
+  /**
+   * Returns whether a class is a subclass of a given class.
+   *
+   * To avoid loading the statically reflected class itself, this
+   * re-implementation of is_subclass_of() is used to test against the ancestor
+   * classes only (which will be reflected by PHP core); i.e., only the parent
+   * class and interfaces.
+   *
+   * It uses an internal cache, because it is assumed that many classes inherit
+   * from the same ancestors.
+   *
+   * @see is_subclass_of()
+   */
+  protected function isSubclassOfAnyAncestors(array $ancestors, $class) {
+    foreach ($ancestors as $ancestor) {
+      if (!isset(self::$ancestorCache[$ancestor])) {
+        self::$ancestorCache[$ancestor] = array();
+        self::$ancestorCache[$ancestor] += class_parents($ancestor) ?: array();
+        self::$ancestorCache[$ancestor] += class_implements($ancestor) ?: array();
+      }
+      if (isset(self::$ancestorCache[$ancestor][$class])) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTrait() {
+    return !empty($this->info[T_TRAIT]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isUserDefined() {
+    return TRUE;
+  }
+
+  /**
+   * Returns the basename (short name) of a class name.
+   *
+   * @param string $fqcn
+   *   The fully-qualified class name for which to return the basename.
+   *
+   * @return string
+   *
+   * This function is named basename(), because basename() natively supports the
+   * operation, but only on Windows. PHP core does not provide a native function.
+   * This algorithm requires two lines of code, but has been measured to be the
+   * most performant user space implementation.
+   *
+   * @see basename()
+   */
+  public static function basename($fqcn) {
+    $parts = explode('\\', $fqcn);
+    return end($parts);
+  }
+
+}
