diff --git a/core/lib/Drupal/Core/RunTests/Config.php b/core/lib/Drupal/Core/RunTests/Config.php
new file mode 100644
index 0000000..a0e7c31
--- /dev/null
+++ b/core/lib/Drupal/Core/RunTests/Config.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RunTests\Config.
+ */
+
+namespace Drupal\Core\RunTests;
+
+use Symfony\Component\Console\Input\InputInterface;
+
+/**
+ * Encapsulates all the configuration derived from CLI input.
+ */
+class Config extends \ArrayObject {
+
+  static $defaults = [
+      'script' => '',
+      'help' => FALSE,
+      'list' => FALSE,
+      'clean' => FALSE,
+      'url' => '',
+      'sqlite' => NULL,
+      'dburl' => NULL,
+      'php' => '',
+      'concurrency' => 1,
+      'all' => FALSE,
+      'module' => NULL,
+      'class' => FALSE,
+      'file' => FALSE,
+      'directory' => NULL,
+      'color' => FALSE,
+      'verbose' => FALSE,
+      'keep-results' => FALSE,
+      'test_names' => [],
+      'repeat' => 1,
+      'die-on-fail' => FALSE,
+      'browser' => FALSE,
+      // Used internally.
+      'test-id' => 0,
+      'execute-test' => '',
+      'xml' => '',
+      'non-html' => FALSE,
+    ];
+
+  /**
+   * Factory to create a config object based on input options.
+   *
+   * @param InputInterface $input
+   *   Console input object.
+   *
+   * @return static
+   *   Newly-created object.
+   */
+  public static function createFromInput(InputInterface $input) {
+    // Create a new array object with default values.
+    $config = new static(static::$defaults);
+    // Add in our input options.
+    $options = array_keys((array) $config);
+    foreach ($options as $option) {
+      if ($input->hasOption($option)) {
+        $config[$option] = $input->getOption($option);
+      }
+    }
+    if ($input->hasArgument('test_names')) {
+      $config['test_names'] = $input->getArgument('test_names');
+    }
+    // Gather our test_names.
+    return $config->parseTestNames();
+  }
+
+  /**
+   * Factory to create a Config object from an array.
+   *
+   * @param array $array
+   * @return static
+   */
+  public static function createFromArray($array) {
+    $config = new static(array_merge(static::$defaults, $array));
+    return $config->parseTestNames();
+  }
+
+  /**
+   * Parse the test_names argument.
+   *
+   * The test_names argument arrives in comma-delimited form, and we want to
+   * turn that into an array.
+   *
+   * @return self
+   */
+  protected function parseTestNames() {
+    if (!empty($this['test_names'])) {
+      $this['test_names'] = explode(',', $this['test_names']);
+    }
+    else {
+      $this['test_names'] = [];
+    }
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/RunTests/DrupalRunTestsException.php b/core/lib/Drupal/Core/RunTests/DrupalRunTestsException.php
new file mode 100644
index 0000000..70ff287
--- /dev/null
+++ b/core/lib/Drupal/Core/RunTests/DrupalRunTestsException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RunTests\DrupalRunTestsException.
+ */
+
+namespace Drupal\Core\RunTests;
+
+/**
+ * Exception class to use with DrupalRunTest.
+ */
+class DrupalRunTestsException extends \RuntimeException {
+}
diff --git a/core/lib/Drupal/Core/RunTests/RunTestsApplication.php b/core/lib/Drupal/Core/RunTests/RunTestsApplication.php
new file mode 100644
index 0000000..09e8d6b
--- /dev/null
+++ b/core/lib/Drupal/Core/RunTests/RunTestsApplication.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RunTests\RunTestsApplication.
+ */
+
+namespace Drupal\Core\RunTests;
+
+use Composer\Autoload\ClassLoader;
+use Drupal\Core\RunTests\RunTestsCommand;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Input\InputInterface;
+
+/**
+ * A single-command application class for run-tests.
+ *
+ * This helps us adapt the run-tests.sh behavior into the Console idiom.
+ */
+class RunTestsApplication extends Application {
+
+  protected $autoloader;
+  protected $scriptPath;
+
+  /**
+   * Constructs a new object.
+   *
+   * @param \Composer\Autoload\ClassLoader $auto_loader
+   *   The app's class loader.
+   * @param string $script_path
+   *   The script's full pathname, so that we can spawn child processes.
+   */
+  public function __construct(ClassLoader $auto_loader, $script_path) {
+    $this->autoloader = $auto_loader;
+    $this->scriptPath = $script_path;
+    parent::__construct('Drupal RunTests:TNG', '8.0.x');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getCommandName(InputInterface $input) {
+    return 'run-tests';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultCommands() {
+    // Keep the core default commands to have the HelpCommand
+    // which is used when using the --help option.
+    $default_commands = parent::getDefaultCommands();
+    $default_commands[] = new RunTestsCommand($this->autoloader, $this->scriptPath);
+    return $default_commands;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinition() {
+    $input_definition = parent::getDefinition();
+    // Clear out the normal first argument, which is the command name.
+    $input_definition->setArguments();
+    return $input_definition;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/RunTests/RunTestsCommand.php b/core/lib/Drupal/Core/RunTests/RunTestsCommand.php
new file mode 100644
index 0000000..4a2d129
--- /dev/null
+++ b/core/lib/Drupal/Core/RunTests/RunTestsCommand.php
@@ -0,0 +1,1522 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\RunTests\RunTestsCommand.
+ */
+
+namespace Drupal\Core\RunTests;
+
+use Composer\Autoload\ClassLoader;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Timer;
+use Drupal\Component\Uuid\Php;
+use Drupal\Core\Database\Database;
+use Drupal\Core\RunTests\Config;
+use Drupal\Core\RunTests\DrupalRunTestsException;
+use Drupal\Core\StreamWrapper\PublicStream;
+use Drupal\Core\Test\TestRunnerKernel;
+use Drupal\simpletest\Form\SimpletestResultsForm;
+use Drupal\simpletest\TestBase;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Formatter\OutputFormatterStyle;
+use Symfony\Component\HttpFoundation\Request;
+
+// Restricting the chunk of queries prevents memory exhaustion.
+const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
+// Exit status codes.
+const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
+const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
+const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
+
+/**
+ * Test runner console command.
+ *
+ * This command discovers available tests, filters them based on input options,
+ * sets up the running environment for all the tests, and then runs each test in
+ * a separate process.
+ *
+ * @todo This class has methods with simpletest_underscore_names. Some of these
+ *   are the names of the functions in the original run-tests.sh, and exist to
+ *   aid in reference during development. We must change these at some point for
+ *   coding standards. Other of these methods are named this way because they
+ *   shim functions provided by the simpletest module, and enable testing.
+ */
+class RunTestsCommand extends Command {
+
+  /**
+   * A configuration object, representing the user input.
+   *
+   * @var \Drupal\Core\RunTests\Config
+   */
+  protected $config;
+
+  /**
+   * The path to the script using this command.
+   *
+   * We need this so that we can spawn child processes.
+   *
+   * @var string
+   */
+  protected $scriptPath;
+
+  /**
+   * The application class loader.
+   *
+   * @var \Composer\Autoload\ClassLoader
+   */
+  protected $autoloader;
+
+  /**
+   * An output object, so we can say things to the user.
+   *
+   * @var \Symfony\Component\Console\Output\OutputInterface
+   */
+  protected $output;
+
+  /**
+   * The test-runner kernel's container.
+   *
+   * Some methods of this class need services, so we provide them here.
+   *
+   * @todo Properly inject services during execute().
+   *
+   * @var \Symfony\Component\DependencyInjection\ContainerInterface
+   */
+  protected $container;
+
+  /**
+   * A list of test IDs we're working with.
+   *
+   * @var int[]
+   */
+  protected $test_ids = [];
+
+  /**
+   * A list of the names of all the test classes we'll be running.
+   *
+   * @var string[]
+   */
+  protected $test_list;
+
+  /**
+   * Map the results string to be more human-readable.
+   *
+   * @var array
+   */
+  protected $results_map = [
+    'pass' => 'Pass',
+    'fail' => 'Fail',
+    'exception' => 'Exception'
+  ];
+
+  /**
+   * Contructs a new object.
+   *
+   * @param \Composer\Autoload\ClassLoader $auto_loader
+   *   The class loader.
+   * @param string|null $name
+   *   (optional) The name of the command.
+   */
+  public function __construct(ClassLoader $auto_loader, $script_path, $name = NULL) {
+    $this->autoloader = $auto_loader;
+    $this->scriptPath = $script_path;
+    parent::__construct($name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function configure() {
+    $this->setName('run-tests')
+      ->setDescription('Run tests.')
+      ->addArgument(
+        'test_names', InputArgument::OPTIONAL, 'Comma-delimited list of tests to run. Can be test classes, files, or groups based on other options.'
+      )
+      ->addOption('list', NULL, InputOption::VALUE_NONE, 'Display all available test groups.')
+      ->addOption('clean', NULL, InputOption::VALUE_NONE, 'Cleans up database tables or directories from previous, failed, tests and then exits (no tests are run).')
+      ->addOption('url', NULL, InputOption::VALUE_OPTIONAL, 'The base URL of the root directory of this Drupal checkout; e.g.: http://drupal.test/ Required unless the Drupal root directory maps exactly to: http://localhost:80/ Use a https:// URL to force all tests to be run under SSL.')
+      ->addOption('all', NULL, InputOption::VALUE_NONE, 'Run all available tests.')
+      ->addOption('module', NULL, InputOption::VALUE_OPTIONAL, "Run all tests belonging to the specified module name. (e.g., 'node')")
+      ->addOption('class', NULL, InputOption::VALUE_NONE, "Run tests identified by specific class names, instead of group names. A specific test method can be added, for example, 'Drupal\book\Tests\BookTest::testBookExport'.")
+      ->addOption('file', NULL, InputOption::VALUE_NONE, "Run tests identified by specific file names, instead of group names. Specify the path and the extension (i.e. 'core/modules/user/user.test').")
+      ->addOption('dburl', NULL, InputOption::VALUE_REQUIRED, ' A URI denoting the database driver, credentials, server hostname, and database name to use in tests. Required when running tests without a Drupal installation that contains default database connection info in settings.php. Examples: mysql://username:password@localhost/databasename#table_prefix sqlite://localhost/relative/path/db.sqlite sqlite://localhost//absolute/path/db.sqlite', NULL)
+      ->addOption('sqlite', NULL, InputOption::VALUE_OPTIONAL, "A pathname to use for the SQLite database of the test runner. Required unless this script is executed with a working Drupalinstallation that has Simpletest module installed. A relative pathname is interpreted relative to the Drupal root directory. Note that ':memory:' cannot be used, because this script spawns sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'", '')
+      ->addOption('browser', NULL, InputOption::VALUE_NONE, 'Opens the results in the browser. This enforces --keep-results and if you want to also view any pages rendered in the simpletest browser you need to add --verbose to the command line.')
+      ->addOption('keep-results', NULL, InputOption::VALUE_NONE, 'Keeps detailed assertion results (in the database) after tests have completed. By default, assertion results are cleared.')
+      ->addOption('concurrency', NULL, InputOption::VALUE_REQUIRED, 'Run tests in parallel, up to [num] tests at a time.', 1)
+      ->addOption('repeat', NULL, InputOption::VALUE_REQUIRED, 'Number of times to repeat the test.', 1)
+      ->addOption('color', NULL, InputOption::VALUE_NONE, 'Output text format results with color highlighting.')
+      ->addOption('die-on-fail', NULL, InputOption::VALUE_REQUIRED, 'Exit test execution immediately upon any failed assertion. This allows to access the test site by changing settings.php to use the test database and configuration directories. Use in combination with --repeat for debugging random test failures.', FALSE)
+      ->addOption('xml', NULL, InputOption::VALUE_REQUIRED, 'If provided, test results will be written as xml files to this path. Specify a directory.', '')
+      ->addOption('non-html', NULL, InputOption::VALUE_REQUIRED, 'Removes escaping from output. Useful for reading results on the CLI.', FALSE)
+      ->addOption('execute-test', NULL, InputOption::VALUE_OPTIONAL, 'Execute a test. Used internally to add a new process.', FALSE)
+      ->addOption('test-id', NULL, InputOption::VALUE_OPTIONAL, 'Execute a test. Used internally to add a new process.')
+      ->addOption('php', NULL, InputOption::VALUE_REQUIRED, 'The absolute path to the PHP executable. Usually not needed.', '')
+      ;
+  }
+
+  /**
+   * Set the styles we can output.
+   *
+   * Styles map to status strings. The currently available strings are: pass,
+   * fail, error, and exception. You can use these styles as follows:
+   * @code
+   * <pass>Your test passed!</>
+   * @endcode
+   *
+   * @param OutputInterface $output
+   *   An output interface object. The styles will be set for this output.
+   */
+  protected function setStyles(OutputInterface $output) {
+    $formatter = $output->getFormatter();
+    $formatter->setStyle('fail', new OutputFormatterStyle('red'));
+    $formatter->setStyle('pass', new OutputFormatterStyle('green'));
+    $formatter->setStyle('error', new OutputFormatterStyle('yellow'));
+    $formatter->setStyle('exception', new OutputFormatterStyle('cyan'));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(InputInterface $input, OutputInterface $output) {
+    $this->setStyles($output);
+    $this->output = $output;
+    try {
+      // Gather our configuration from the CLI inputs.
+      $this->config = Config::createFromInput($input);
+      $this->config['script'] = $this->scriptPath;
+
+      $this->validateConfig();
+      $this->simpletest_script_init();
+
+      try {
+        $request = Request::createFromGlobals();
+        $kernel = TestRunnerKernel::createFromRequest($request, $this->autoloader);
+        $kernel->prepareLegacyRequest($request);
+        $this->container = $kernel->getContainer();
+      }
+      catch (\Exception $e) {
+        $this->simpletest_script_print((string) $e, 'exception');
+        return SIMPLETEST_SCRIPT_EXIT_EXCEPTION;
+      }
+
+      // If we got --execute-test, this is probably a child process running an
+      // individual test.
+      if (!empty($this->config['execute-test'])) {
+        $test_id = $this->config['test-id'];
+
+        $this->simpletest_script_setup_database();
+        return $this->simpletest_script_run_one_test($test_id, $this->config['execute-test']);
+      }
+
+      if ($this->config['list']) {
+        return $this->listTests($this->config['module']);
+      }
+
+      $this->simpletest_script_setup_database($input, TRUE);
+
+      if ($this->config['clean']) {
+        return $this->clean();
+      }
+
+      $this->test_list = $this->simpletest_script_get_test_list();
+
+      // Try to allocate unlimited time to run the tests.
+      \drupal_set_time_limit(0);
+      $this->simpletest_script_reporter_init();
+
+      $tests_to_run = array();
+      $input_repeat = $this->config['repeat'];
+      for ($i = 0; $i < $input_repeat; $i++) {
+        $tests_to_run = array_merge($tests_to_run, $this->test_list);
+      }
+
+      // Execute tests.
+      $status = $this->simpletest_script_execute_batch($tests_to_run);
+
+      // Stop the timer.
+      $this->simpletest_script_reporter_timer_stop();
+
+      // Display results before database is cleared.
+      if ($this->config['browser']) {
+        $this->simpletest_script_open_browser();
+      }
+      else {
+        $this->simpletest_script_reporter_display_results();
+      }
+
+      if ($this->config['xml']) {
+        $this->simpletest_script_reporter_write_xml_results();
+      }
+
+      // Clean up all test results.
+      if (!$this->config['keep-results']) {
+        try {
+          \simpletest_clean_results_table();
+        }
+        catch (\Exception $e) {
+          $this->output->writeln((string) $e);
+          return SIMPLETEST_SCRIPT_EXIT_EXCEPTION;
+        }
+      }
+    }
+    catch (DrupalRunTestsException $e) {
+      $this->simpletest_script_print($e->getMessage(), 'exception');
+      $status = $e->getCode();
+    }
+    // Test complete, exit.
+    $output->writeln('status code: ' . $status);
+    return $status;
+  }
+
+  /**
+   * Validate the configuration.
+   *
+   * @throws DrupalRunTestsException
+   *   Thrown if any input doesn't validate properly.
+   */
+  protected function validateConfig() {
+    // Validate the --concurrency argument.
+    $concurrency = $this->config['concurrency'];
+    if (!is_numeric($concurrency) || $concurrency <= 0) {
+      throw new DrupalRunTestsException('--concurrency must be a strictly positive integer.', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+    // Force --keep-results if --browser is set.
+    if ($this->config['browser']) {
+      $this->config['keep-results'] = TRUE;
+    }
+  }
+
+  /**
+   * Supports the --list option.
+   *
+   * @param string $module
+   *   (optional) The module name, or NULL for all. Defaults to NULL.
+   *
+   * @return int
+   *   Script exit status code.
+   */
+  protected function listTests($module = NULL) {
+    // Display all available tests.
+    $this->output->write(
+      [
+        'Available test groups & classes',
+        '-------------------------------',
+        '',
+      ],
+      TRUE
+    );
+    try {
+      $groups = $this->simpletest_test_get_all();
+    }
+    catch (\Exception $e) {
+    }
+    foreach ($groups as $group => $tests) {
+      $this->output->writeln($group);
+      foreach ($tests as $class => $info) {
+        $this->output->writeln(" - $class");
+      }
+    }
+    return SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+  }
+
+  /**
+   * Support the --clean option.
+   *
+   * @return int
+   *   Exit code.
+   */
+  protected function clean() {
+    // Clean up left-over tables and directories.
+    try {
+      \simpletest_clean_environment();
+    }
+    catch (Exception $e) {
+      $this->output->writeln((string) $e);
+      return SIMPLETEST_SCRIPT_EXIT_EXCEPTION;
+    }
+    $this->output->writeln('');
+    $this->output->writeln('Environment cleaned.');
+
+    // Get the status messages and print them.
+    $messages = drupal_get_messages('status');
+    foreach ($messages['status'] as $text) {
+      $this->output->writeln(" - " . $text);
+    }
+    return SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+  }
+
+  /**
+   * Sets up database connection info for running tests.
+   *
+   * If this script is executed from within a real Drupal installation, then
+   * this function essentially performs nothing (unless the --sqlite or --dburl
+   * parameters were passed).
+   *
+   * Otherwise, there are three database connections of concern:
+   * - --sqlite: The test runner connection, providing access to Simpletest
+   *   database tables for recording test IDs and assertion results.
+   * - --dburl: A database connection that is used as base connection info for
+   *   all tests; i.e., every test will spawn from this connection. In case this
+   *   connection uses e.g. SQLite, then all tests will run against SQLite. This
+   *   is exposed as $databases['default']['default'] to Drupal.
+   * - The actual database connection used within a test. This is the same as
+   *   --dburl, but uses an additional database table prefix. This is
+   *   $databases['default']['default'] within a test environment. The original
+   *   connection is retained in
+   *   $databases['simpletest_original_default']['default'] and restored after
+   *   each test.
+   *
+   * @param bool $new
+   *   TRUE if this process is a run-tests.sh master process, FALSE otherwise.
+   *   If TRUE, the SQLite database file specified by --sqlite (if any) is set
+   *   up. Otherwise, database connections are prepared only.
+   */
+  protected function simpletest_script_setup_database($new = FALSE) {
+
+    // If there is an existing Drupal installation that contains a database
+    // connection info in settings.php, then $databases['default']['default']
+    // will hold the default database connection already. This connection is
+    // assumed to be valid, and this connection will be used in tests, so that
+    // they run against e.g. MySQL instead of SQLite.
+    // However, in case no Drupal installation exists, this default database
+    // connection can be set and/or overridden with the --dburl parameter.
+    if (!empty($this->config['dburl'])) {
+      // Remove a possibly existing default connection (from settings.php).
+      Database::removeConnection('default');
+      try {
+        $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($this->config['dburl'], DRUPAL_ROOT);
+      }
+      catch (\InvalidArgumentException $e) {
+        throw new DrupalRunTestsException('Invalid --dburl. Reason: ' . $e->getMessage(), SIMPLETEST_SCRIPT_EXIT_FAILURE);
+      }
+    }
+    // Otherwise, try to use the default database connection from settings.php.
+    else {
+      $databases['default'] = Database::getConnectionInfo('default');
+    }
+
+    // If there is no default database connection for tests, we cannot continue.
+    if (!isset($databases['default']['default'])) {
+      throw new DrupalRunTestsException('Missing default database connection for tests. Use --dburl to specify one.', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+    Database::addConnectionInfo('default', 'default', $databases['default']['default']);
+    // If no --sqlite parameter has been passed, then Simpletest module is
+    // assumed to be installed, so the test runner database connection is the
+    // default database connection.
+    if (empty($this->config['sqlite'])) {
+      $sqlite = FALSE;
+      $databases['test-runner']['default'] = $databases['default']['default'];
+    }
+    // Otherwise, set up a SQLite connection for the test runner.
+    else {
+      if ($this->config['sqlite'][0] === '/') {
+        $sqlite = $this->config['sqlite'];
+      }
+      else {
+        $sqlite = DRUPAL_ROOT . '/' . $this->config['sqlite'];
+      }
+      $databases['test-runner']['default'] = array(
+        'driver' => 'sqlite',
+        'database' => $sqlite,
+        'prefix' => array(
+          'default' => '',
+        ),
+      );
+      // Create the test runner SQLite database, unless it exists already.
+      if ($new && !file_exists($sqlite)) {
+        if (!is_dir(dirname($sqlite))) {
+          mkdir(dirname($sqlite));
+        }
+        touch($sqlite);
+      }
+    }
+
+    // Add the test runner database connection.
+    Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
+
+    // Create the Simpletest schema.
+    try {
+      $schema = Database::getConnection('default', 'test-runner')->schema();
+    }
+    catch (\PDOException $e) {
+      $this->simpletest_script_print_error('There was an error connecting to the databse. Try the --dburl and/or --sqlite options.');
+      throw new DrupalRunTestsException($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage(), SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+    if ($new && $sqlite) {
+      require_once DRUPAL_ROOT . '/' . \drupal_get_path('module', 'simpletest') . '/simpletest.install';
+      $simpletest_schema = \simpletest_schema();
+      foreach ($simpletest_schema as $name => $table_spec) {
+        try {
+          if ($schema->tableExists($name)) {
+            $schema->dropTable($name);
+          }
+          $schema->createTable($name, $table_spec);
+        }
+        catch (Exception $e) {
+          throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+        }
+      }
+    }
+    // Verify that the Simpletest database schema exists by checking one table.
+    try {
+      if (!$schema->tableExists('simpletest')) {
+        throw new DrupalRunTestsException('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+      }
+    }
+    catch (Exception $e) {
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+  }
+
+  /**
+   * Print error messages in appropriately alarming colors.
+   *
+   * Print error message prefixed with "  ERROR: " and displayed in fail color
+   * if color output is enabled.
+   *
+   * @param string $message
+   *   The message to print.
+   */
+  protected function simpletest_script_print_error($message) {
+    $this->simpletest_script_print("  ERROR: $message", 'error');
+  }
+
+  /**
+   * Print a message to the console.
+   *
+   * If color is enabled then the specified color code will be used.
+   *
+   * @param string $message
+   *   The message to print.
+   * @param string $status_code
+   *   The color code to use for coloring.
+   */
+  protected function simpletest_script_print($message, $status_code = NULL) {
+    if ($status_code !== NULL && $this->config['color']) {
+      $message = "<$status_code>$message</>";
+    }
+    $this->output->writeln($message);
+  }
+
+  /**
+   * Run a single test against a bootstrapped Drupal.
+   */
+  protected function simpletest_script_run_one_test($test_id, $test_class) {
+    try {
+      if (strpos($test_class, '::') > 0) {
+        list($class_name, $method) = explode('::', $test_class, 2);
+        $methods = [$method];
+      }
+      else {
+        $class_name = $test_class;
+        // Use empty array to run all the test methods.
+        $methods = array();
+      }
+      $test = new $class_name($test_id);
+      if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
+        $status = $this->simpletest_script_run_phpunit($test_id, $test_class);
+      }
+      else {
+        $test->dieOnFail = (bool) $this->config['die-on-fail'];
+        $test->verbose = (bool) $this->config['verbose'];
+        $test->run($methods);
+        $this->simpletest_script_reporter_display_summary($test_class, $test->results);
+
+        $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+        // Finished, kill this runner.
+        if ($test->results['#fail'] || $test->results['#exception']) {
+          $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
+        }
+      }
+      return $status;
+    }
+    // DrupalTestCase::run() catches exceptions already, so this is only reached
+    // when an exception is thrown in the wrapping test runner environment.
+    catch (\Exception $e) {
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+  }
+
+  /**
+   * Gather all the tests for a list of class names.
+   *
+   * Supports the --class option.
+   */
+  protected function getTestsForClassNames(array $test_names) {
+    $test_list = [];
+    foreach ($test_names as $test_class) {
+      list($class_name,) = explode('::', $test_class, 2);
+      if (class_exists($class_name)) {
+        $test_list[] = $test_class;
+      }
+      else {
+        // Tell the user that their class doesn't exist.
+        $this->simpletest_script_print_error('Test class not found: ' . $class_name);
+        $this->simpletest_script_print_alternatives($class_name, $this->getAllTestClasses(), 6);
+        throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+      }
+    }
+    return $test_list;
+  }
+
+  /**
+   * Gather all the test classes Simpletest can find.
+   *
+   * @return string[]
+   *   An array of test class names.
+   *
+   * @see \simpletest_test_get_all()
+   */
+  protected function getAllTestClasses() {
+    $groups = [];
+    $all_classes = [];
+    try {
+      $groups = $this->simpletest_test_get_all();
+    }
+    catch (\Exception $e) {
+    }
+    foreach ($groups as $group) {
+      $all_classes = array_merge($all_classes, array_keys($group));
+    }
+    return $all_classes;
+  }
+
+  /**
+   * Gather a list of all the tests found within a list of files.
+   *
+   * @param string[] $test_names
+   *   An array of file paths.
+   *
+   * @return array
+   *   All the test files found within each file specified.
+   *
+   * @throws DrupalRunTestsException
+   *   Thrown when any of the files does not exist.
+   */
+  protected function getTestsForFiles($test_names) {
+    // Extract test case class names from specified files.
+    $test_list = [];
+    foreach ($test_names as $file) {
+      if (!file_exists($file)) {
+        $this->simpletest_script_print_error('File not found: ' . $file);
+        throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+      }
+      $contents = file_get_contents($file);
+      $test_list = array_merge($test_list, $this->getTestClassesForFileContents($contents));
+    }
+    return $test_list;
+  }
+
+  /**
+   * Gathers tests within a specified file directory.
+   *
+   * @param string $directory
+   *   Path to the directory.
+   *
+   * @return string[]
+   *   Array of test class names.
+   */
+  protected function getTestsForDirectory($directory) {
+    $test_list = [];
+    // Extract test case class names from specified directory.
+    // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
+    // Since we do not want to hard-code too many structural file/directory
+    // assumptions about PSR-0/4 files and directories, we check for the
+    // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
+    // its path.
+    // Ignore anything from third party vendors.
+    $ignore = array('.', '..', 'vendor');
+    $files = [];
+    // Add DRUPAL_ROOT if beginning slash is missing.
+    if ($directory[0] !== '/') {
+      $directory = DRUPAL_ROOT . "/" . $input_directory;
+    }
+    foreach (file_scan_directory($directory, '/\.php$/', $ignore) as $file) {
+      // '/Tests/' can be contained anywhere in the file's path (there can
+      // be sub-directories below /Tests), but must be contained literally.
+      // Case-insensitive to match all Simpletest and PHPUnit tests:
+      //   ./lib/Drupal/foo/Tests/Bar/Baz.php
+      //   ./foo/src/Tests/Bar/Baz.php
+      //   ./foo/tests/Drupal/foo/Tests/FooTest.php
+      //   ./foo/tests/src/FooTest.php
+      // $file->filename doesn't give us a directory, so we use $file->uri
+      // Strip the drupal root directory and trailing slash off the URI.
+      $filename = substr($file->uri, strlen(DRUPAL_ROOT) + 1);
+      if (stripos($filename, '/Tests/')) {
+        $files[$filename] = $filename;
+      }
+    }
+    foreach ($files as $file) {
+      $contents = file_get_contents($file);
+      $test_list = array_merge($test_list, $this->getTestClassesForFileContents($contents));
+    }
+    return $test_list;
+  }
+
+  /**
+   * Extract class names from file contents.
+   *
+   * @param string $contents
+   *   The contents of a PHP file.
+   *
+   * @return string[]
+   *   A list of class names found in the file contents.
+   */
+  protected function getClassesForFileContents($contents) {
+    $test_list = [];
+    // Extract a potential namespace.
+    $namespace = FALSE;
+    if (preg_match('@^namespace ([^ ;]+)@m', $contents, $matches)) {
+      $namespace = $matches[1];
+    }
+    // Extract all class names.
+    // Abstract classes are excluded on purpose.
+    preg_match_all('@^class ([^ ]+)@m', $contents, $matches);
+    if (!$namespace) {
+      $test_list = array_merge($test_list, $matches[1]);
+    }
+    else {
+      foreach ($matches[1] as $class_name) {
+        $test_list[] = $namespace . '\\' . $class_name;
+      }
+    }
+    return $test_list;
+  }
+
+  /**
+   * Gather class names for tests in the given file contents.
+   *
+   * The file is assumed to have been previously autoloaded.
+   *
+   * @param string $contents
+   *   String contents of a PHP file.
+   *
+   * @return string
+   *   Array of class fully-qualified class names which subclass either
+   *   \Drupal\simpletest\TestBase or \PHPUnit_Framework_TestCase.
+   */
+  protected function getTestClassesForFileContents($contents) {
+    $test_list = [];
+    $all_classes = $this->getClassesForFileContents($contents);
+    foreach ($all_classes as $test_class) {
+      if (is_subclass_of($test_class, '\Drupal\simpletest\TestBase') || is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
+        $test_list[] = $test_class;
+      }
+    }
+    return $test_list;
+  }
+
+  /**
+   * Get the list of tests to run based on input options.
+   *
+   * If --all is specified then return all available tests, otherwise read the
+   * list of tests.
+   *
+   * Unless otherwise specified, the list of tests is assumed to be test groups.
+   *
+   * @return string[]
+   *   List of tests.
+   *
+   * @throws DrupalRunTestsException
+   *   Thrown if no tests could be found, or if no test group was specified, or
+   *   if there was an error getting tests from Simpletest.
+   */
+  protected function simpletest_script_get_test_list() {
+    $this->test_list = array();
+    // Handle --all and --module.
+    if ($this->config['all'] || !empty($this->config['module'])) {
+      try {
+        $groups = $this->simpletest_test_get_all($this->config['module']);
+      }
+      catch (Exception $e) {
+        throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+      }
+      $all_tests = array();
+      foreach ($groups as $group => $tests) {
+        $all_tests = array_merge($all_tests, array_keys($tests));
+      }
+      $this->test_list = $all_tests;
+    }
+    // Handle --class, --file and --directory.
+    else {
+      if (!empty($this->config['class'])) {
+        $this->test_list = $this->getTestsForClassNames($this->config['test_names']);
+      }
+      elseif (!empty($this->config['file'])) {
+        $this->test_list = $this->getTestsForFiles($this->config['test_names']);
+      }
+      elseif (!empty($this->config['directory'])) {
+        $this->test_list = $this->getTestsForDirectory($this->config['directory']);
+      }
+      // Assume test_names contains group names.
+      else {
+        if (!empty($this->config['test_names'])) {
+          try {
+            $groups = $this->simpletest_test_get_all();
+          }
+          catch (\Exception $e) {
+          }
+          foreach ($this->config['test_names'] as $group_name) {
+            if (isset($groups[$group_name])) {
+              $this->test_list = array_merge($this->test_list, array_keys($groups[$group_name]));
+            }
+            else {
+              $this->simpletest_script_print_error('Test group not found: ' . $group_name);
+              $this->simpletest_script_print_alternatives($group_name, array_keys($groups));
+              throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+            }
+          }
+        }
+      }
+    }
+
+    if (empty($this->test_list)) {
+      $this->simpletest_script_print_error('No valid tests were specified.');
+      throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+    return $this->test_list;
+  }
+
+  /**
+   * Initialize the reporter.
+   */
+  protected function simpletest_script_reporter_init() {
+    $this->output->writeln('');
+    $this->output->writeln("Drupal test run");
+    $this->output->writeln("---------------");
+    $this->output->writeln('');
+
+    // Tell the user about what tests are to be run.
+    if ($this->config['all']) {
+      $this->output->writeln("All tests will run.");
+      $this->output->writeln('');
+    }
+    else {
+      $this->output->writeln("Tests to be run:");
+      foreach ($this->test_list as $class_name) {
+        $this->output->writeln("  - $class_name");
+      }
+      $this->output->writeln('');
+    }
+
+    $this->output->writeln("Test run started:");
+    // @todo: $_SERVER? Really? :-)
+    $this->output->writeln("  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']));
+    Timer::start('run-tests');
+    $this->output->writeln('');
+
+    $this->output->writeln("Test summary");
+    $this->output->writeln("------------");
+    $this->output->writeln('');
+  }
+
+  /**
+   * Execute a batch of tests.
+   */
+  protected function simpletest_script_execute_batch($test_classes) {
+    $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
+
+    // Multi-process execution.
+    $children = array();
+    while (!empty($test_classes) || !empty($children)) {
+      while (count($children) < $this->config['concurrency']) {
+        if (empty($test_classes)) {
+          break;
+        }
+
+        try {
+          $test_id = Database::getConnection('default', 'test-runner')
+            ->insert('simpletest_test_id')
+            ->useDefaults(array('test_id'))
+            ->execute();
+        }
+        catch (\Exception $e) {
+          throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+        }
+        $this->test_ids[] = $test_id;
+
+        $test_class = array_shift($test_classes);
+        // Fork a child process.
+        $command = $this->simpletest_script_command($test_id, $test_class);
+        $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
+
+        if (!is_resource($process)) {
+          // @todo: Should really be an error code.
+          throw new DrupalRunTestsException('Unable to fork test process. Aborting.', SIMPLETEST_SCRIPT_EXIT_SUCCESS);
+        }
+
+        // Register our new child.
+        $children[] = array(
+          'process' => $process,
+          'test_id' => $test_id,
+          'class' => $test_class,
+          'pipes' => $pipes,
+        );
+      }
+
+      // Wait for children every 200ms.
+      usleep(200000);
+
+      // Check if some children finished.
+      foreach ($children as $cid => $child) {
+        $status = proc_get_status($child['process']);
+        if (empty($status['running'])) {
+          // The child exited, unregister it.
+          proc_close($child['process']);
+          // If the test failed, only add the status code.
+          if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
+            $total_status = max($status['exitcode'], $total_status);
+          }
+          // If the test errored out, add the status code and log it.
+          elseif ($status['exitcode']) {
+            $total_status = max($status['exitcode'], $total_status);
+            $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
+            $this->simpletest_script_print($message, 'error');
+            // Insert a fail for xml results.
+            TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
+            // Ensure that an error line is displayed for the class.
+            $this->simpletest_script_reporter_display_summary($child['class'], [
+              '#pass' => 0,
+              '#fail' => 1,
+              '#exception' => 0,
+              '#debug' => 0,
+            ]);
+            if ($this->config['die-on-fail']) {
+              list($db_prefix,) = \simpletest_last_test_get($child['test_id']);
+              $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
+              $this->output->writeln('Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n");
+              $this->config['keep-results'] = TRUE;
+              // Exit repeat loop immediately.
+              $this->config['repeat'] = -1;
+            }
+          }
+          // Free-up space by removing any potentially created resources.
+          if (!$this->config['keep-results']) {
+            $this->simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
+          }
+
+          // Remove this child.
+          unset($children[$cid]);
+        }
+      }
+    }
+    return $total_status;
+  }
+
+  /**
+   * Stop the test timer.
+   */
+  protected function simpletest_script_reporter_timer_stop() {
+    $this->output->writeln('');
+    $end = Timer::stop('run-tests');
+    $this->output->writeln("Test run duration: " . $this->container->get('date.formatter')->formatInterval($end['time'] / 1000));
+    $this->output->writeln('');
+  }
+
+  /**
+   * Display test results.
+   */
+  protected function simpletest_script_reporter_display_results() {
+    if ($this->config['verbose']) {
+      // Report results.
+      $this->output->write([
+        'Detailed test results',
+        '---------------------',
+      ], TRUE);
+
+      $results = $this->simpletest_script_load_messages_by_test_id($this->test_ids);
+      $test_class = '';
+      foreach ($results as $result) {
+        if (isset($this->results_map[$result->status])) {
+          if ($result->test_class != $test_class) {
+            // Display test class every time results are for new test class.
+            $this->output->write([
+              '',
+              '',
+              "---- $result->test_class ----",
+              '',
+              '',
+            ], TRUE);
+            $test_class = $result->test_class;
+
+            // Print table header.
+            $this->output->write([
+              "Status    Group      Filename          Line Function                            ",
+              "--------------------------------------------------------------------------------",
+            ], TRUE);
+          }
+
+          $this->simpletest_script_format_result($result);
+        }
+      }
+    }
+  }
+
+  /**
+   * Return a command used to run a test in a separate process.
+   *
+   * @param int $test_id
+   *   The current test ID.
+   * @param string $test_class
+   *   The name of the test class to run.
+   */
+  protected function simpletest_script_command($test_id, $test_class) {
+    $escaped_php = escapeshellarg($this->gleanPhpPath());
+    $command = $escaped_php . ' ' . escapeshellarg('./core/scripts/' . $this->config['script']);
+    $command .= ' --url ' . escapeshellarg($this->config['url']);
+    if (!empty($this->config['sqlite'])) {
+      $command .= ' --sqlite ' . escapeshellarg($this->config['sqlite']);
+    }
+    if (!empty($this->config['dburl'])) {
+      $command .= ' --dburl ' . escapeshellarg($this->config['dburl']);
+    }
+    $command .= ' --php ' . $escaped_php;
+    $command .= " --test-id $test_id";
+    foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $option) {
+      if ($this->config[$option]) {
+        $command .= ' --' . $option;
+      }
+    }
+    // --execute-test and class name needs to come last.
+    $command .= ' --execute-test ' . escapeshellarg($test_class);
+    return $command;
+  }
+
+  /**
+   * Displays the assertion result summary for a single test class.
+   *
+   * @param string $class
+   *   The test class name that was run.
+   * @param string[] $results
+   *   The assertion results using #pass, #fail, #exception, #debug array keys.
+   */
+  protected function simpletest_script_reporter_display_summary($class, array $results) {
+    // Output all test results vertically aligned.
+    // Cut off the class name after 60 chars, and pad each group with 3 digits
+    // by default (more than 999 assertions are rare).
+    $output = vsprintf('%-60.60s %10s %9s %14s %12s', array(
+      $class,
+      $results['#pass'] . ' passes',
+      !$results['#fail'] ? '' : $results['#fail'] . ' fails',
+      !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
+      !$results['#debug'] ? '' : $results['#debug'] . ' messages',
+    ));
+
+    $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
+    $this->simpletest_script_print($output, $status);
+  }
+
+  /**
+   * Removes all remnants of a test runner.
+   *
+   * In case a (e.g., fatal) error occurs after the test site has been fully
+   * setup and the error happens in many tests, the environment that executes
+   * the tests can easily run out of memory or disk space. This function ensures
+   * that all created resources are properly cleaned up after every executed
+   * test.
+   *
+   * This clean-up only exists in this script, since SimpleTest module itself
+   * does not use isolated sub-processes for each test being run, so a fatal
+   * error halts not only the test, but also the test runner (i.e., the parent
+   * site).
+   *
+   * @param int $test_id
+   *   The test ID of the test run.
+   * @param string $test_class
+   *   The class name of the test run.
+   * @param int $exitcode
+   *   The exit code of the test runner.
+   *
+   * @see simpletest_script_run_one_test()
+   */
+  protected function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
+    if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
+      // PHPUnit test, move on.
+      return;
+    }
+    // Retrieve the last database prefix used for testing.
+    try {
+      list($db_prefix,) = \simpletest_last_test_get($test_id);
+    }
+    catch (Exception $e) {
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    // If no database prefix was found, then the test was not set up correctly.
+    if (empty($db_prefix)) {
+      throw new DrupalRunTestsException("FATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)", SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    // Do not output verbose cleanup messages in case of a positive exitcode.
+    $output = !empty($exitcode);
+    $messages = array();
+
+    $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
+
+    // Read the log file in case any fatal errors caused the test to crash.
+    try {
+      \simpletest_log_read($test_id, $db_prefix, $test_class);
+    }
+    catch (Exception $e) {
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    // Check whether a test site directory was setup already.
+    // @see \Drupal\simpletest\TestBase::prepareEnvironment()
+    $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
+    if (is_dir($test_directory)) {
+      // Output the error_log.
+      if (is_file($test_directory . '/error.log')) {
+        if ($errors = file_get_contents($test_directory . '/error.log')) {
+          $output = TRUE;
+          $messages[] = $errors;
+        }
+      }
+      // Delete the test site directory.
+      // simpletest_clean_temporary_directories() cannot be used here, since it
+      // would also delete file directories of other tests that are potentially
+      // running concurrently.
+      file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
+      $messages[] = "- Removed test site directory.";
+    }
+
+    // Clear out all database tables from the test.
+    try {
+      $schema = Database::getConnection('default', 'default')->schema();
+      $count = 0;
+      foreach ($schema->findTables($db_prefix . '%') as $table) {
+        $schema->dropTable($table);
+        $count++;
+      }
+    }
+    catch (Exception $e) {
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    if ($count) {
+      $messages[] = "- Removed $count leftover tables.";
+    }
+
+    if ($output) {
+      $this->output->write($messages, TRUE);
+      $this->output->writeln('');
+    }
+  }
+
+  /**
+   * Format the result to fit the default 80 character terminal size.
+   *
+   * @param object $result
+   *   The result object to format.
+   */
+  protected function simpletest_script_format_result($result) {
+
+    $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n", $this->results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
+
+    $this->simpletest_script_print($summary, $result->status);
+
+    $message = trim(strip_tags($result->message));
+    if ($this->config['non-html']) {
+      $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8');
+    }
+    $lines = explode("\n", wordwrap($message), 76);
+    foreach ($lines as $line) {
+      $this->output->writeln("    $line");
+    }
+  }
+
+  /**
+   * Parse URLs and set up the $_SERVER environment.
+   */
+  protected function simpletest_script_init() {
+    $host = 'localhost';
+    $path = '';
+    $port = '80';
+
+    // Get URL from arguments.
+    $input_url = $this->config['url'];
+    if (!empty($input_url)) {
+      $parsed_url = parse_url($input_url);
+      $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
+      $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
+      $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
+      if ($path == '/') {
+        $path = '';
+      }
+      // If the passed URL schema is 'https' then setup the $_SERVER variables
+      // properly so that testing will run under HTTPS.
+      if ($parsed_url['scheme'] == 'https') {
+        $_SERVER['HTTPS'] = 'on';
+      }
+    }
+
+    if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
+      $base_url = 'https://';
+    }
+    else {
+      $base_url = 'http://';
+    }
+    $base_url .= $host;
+    if ($path !== '') {
+      $base_url .= $path;
+    }
+    putenv('SIMPLETEST_BASE_URL=' . $base_url);
+    $_SERVER['HTTP_HOST'] = $host;
+    $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+    $_SERVER['SERVER_ADDR'] = '127.0.0.1';
+    $_SERVER['SERVER_PORT'] = $port;
+    $_SERVER['SERVER_SOFTWARE'] = NULL;
+    $_SERVER['SERVER_NAME'] = 'localhost';
+    $_SERVER['REQUEST_URI'] = $path . '/';
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    $_SERVER['SCRIPT_NAME'] = $path . '/index.php';
+    $_SERVER['SCRIPT_FILENAME'] = $path . '/index.php';
+    $_SERVER['PHP_SELF'] = $path . '/index.php';
+    $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
+
+    if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
+      // Ensure that any and all environment variables are changed to https://.
+      foreach ($_SERVER as $key => $value) {
+        $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
+      }
+    }
+  }
+
+  /**
+   * Prints alternative test names.
+   *
+   * Searches the provided array of string values for close matches based on the
+   * Levenshtein algorithm.
+   *
+   * @param string $string
+   *   A string to test.
+   * @param string[] $array
+   *   A list of strings to search.
+   * @param int $degree
+   *   The matching strictness. Higher values return fewer matches. A value of
+   *   4 means that the function will return strings from $array if the
+   *   candidate string in $array would be identical to $string by changing 1/4
+   *   or fewer of its characters.
+   *
+   * @see http://php.net/manual/en/function.levenshtein.php
+   */
+  protected function simpletest_script_print_alternatives($string, array $array, $degree = 4) {
+    $alternatives = array();
+    foreach ($array as $item) {
+      $lev = levenshtein($string, $item);
+      if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
+        $alternatives[] = $item;
+      }
+    }
+    if (!empty($alternatives)) {
+      $this->simpletest_script_print("  Did you mean?", 'error');
+      foreach ($alternatives as $alternative) {
+        $this->simpletest_script_print("  - $alternative", 'error');
+      }
+    }
+  }
+
+  /**
+   * Loads the simpletest messages from the database.
+   *
+   * Messages are ordered by test class and message id.
+   *
+   * @param array $test_ids
+   *   Array of test IDs of the messages to be loaded.
+   *
+   * @return array
+   *   Array of simpletest messages from the database.
+   */
+  protected function simpletest_script_load_messages_by_test_id($test_ids) {
+    $results = array();
+
+    // Sqlite has a maximum number of variables per query. If required, the
+    // database query is split into chunks.
+    if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($this->config['sqlite'])) {
+      $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
+    }
+    else {
+      $test_id_chunks = array($test_ids);
+    }
+
+    foreach ($test_id_chunks as $test_id_chunk) {
+      try {
+        $result_chunk = Database::getConnection('default', 'test-runner')
+            ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
+              ':test_ids[]' => $test_id_chunk,
+            ))->fetchAll();
+      }
+      catch (Exception $e) {
+        $this->output->writeln((string) $e);
+        throw new DrupalRunTestsException(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+      }
+      if ($result_chunk) {
+        $results = array_merge($results, $result_chunk);
+      }
+    }
+
+    return $results;
+  }
+
+  /**
+   * Display test results.
+   */
+  protected function simpletest_script_open_browser() {
+    try {
+      $connection = Database::getConnection('default', 'test-runner');
+      $results = $connection->select('simpletest')
+        ->fields('simpletest')
+        ->condition('test_id', $this->test_ids, 'IN')
+        ->orderBy('test_class')
+        ->orderBy('message_id')
+        ->execute()
+        ->fetchAll();
+    }
+    catch (Exception $e) {
+      $this->output->writeln((string) $e);
+      throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    // Get the results form.
+    $form = array();
+    SimpletestResultsForm::addResultForm($form, $results);
+
+    // Get the assets to make the details element collapsible and theme the
+    // result form.
+    $assets = new \Drupal\Core\Asset\AttachedAssets();
+    $assets->setLibraries([
+      'core/drupal.collapse',
+      'system/admin',
+      'simpletest/drupal.simpletest',
+    ]);
+    $resolver = $this->container->get('asset.resolver');
+    list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
+    $js_collection_renderer = $this->container->get('asset.js.collection_renderer');
+    $js_assets_header = $js_collection_renderer->render($js_assets_header);
+    $js_assets_footer = $js_collection_renderer->render($js_assets_footer);
+    $css_assets = $this->container->get('asset.css.collection_renderer')
+      ->render($resolver->getCssAssets($assets, FALSE));
+
+    // Make the html page to write to disk.
+    $render_service = $this->container->get('renderer');
+    $html = '<head>' . $render_service->renderPlain($js_assets_header) . $render_service->renderPlain($css_assets) . '</head><body>' . $render_service->renderPlain($form) . $render_service->renderPlain($js_assets_footer) . '</body>';
+
+    // Ensure we have assets verbose directory - tests with no verbose output
+    // will not have created one.
+    $directory = PublicStream::basePath() . '/simpletest/verbose';
+    file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+    $uuid = new Php();
+    $filename = $directory . '/results-' . $uuid->generate() . '.html';
+    file_put_contents($filename, $html);
+
+    // See if we can find an OS helper to open URLs in default browser.
+    $browser = FALSE;
+    if (shell_exec('which xdg-open')) {
+      $browser = 'xdg-open';
+    }
+    elseif (shell_exec('which open')) {
+      $browser = 'open';
+    }
+    elseif (substr(PHP_OS, 0, 3) == 'WIN') {
+      $browser = 'start';
+    }
+
+    if ($browser) {
+      shell_exec($browser . ' ' . escapeshellarg($filename));
+    }
+    else {
+      // Can't find assets valid browser.
+      print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.';
+    }
+  }
+
+  /**
+   * Display jUnit XML test results.
+   */
+  protected function simpletest_script_reporter_write_xml_results() {
+    try {
+      $results = $this->simpletest_script_load_messages_by_test_id($this->test_ids);
+    }
+    catch (Exception $e) {
+      echo (string) $e;
+      throw new DrupalRunTestsException((string) $e, SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
+    }
+
+    $test_class = '';
+    $xml_files = array();
+
+    foreach ($results as $result) {
+      if (isset($this->results_map[$result->status])) {
+        if ($result->test_class != $test_class) {
+          // We've moved onto a new class, so write the last classes results to
+          // a file:
+          if (isset($xml_files[$test_class])) {
+            file_put_contents($this->config['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
+            unset($xml_files[$test_class]);
+          }
+          $test_class = $result->test_class;
+          if (!isset($xml_files[$test_class])) {
+            $doc = new DomDocument('1.0');
+            $root = $doc->createElement('testsuite');
+            $root = $doc->appendChild($root);
+            $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
+          }
+        }
+
+        // For convenience:
+        $dom_document = &$xml_files[$test_class]['doc'];
+
+        // Create the XML element for this test case:
+        $case = $dom_document->createElement('testcase');
+        $case->setAttribute('classname', $test_class);
+        if (strpos($result->function, '->') !== FALSE) {
+          list($class, $name) = explode('->', $result->function, 2);
+        }
+        else {
+          $name = $result->function;
+        }
+        $case->setAttribute('name', $name);
+
+        // Passes get no further attention, but failures and exceptions get to
+        // add more detail:
+        if ($result->status == 'fail') {
+          $fail = $dom_document->createElement('failure');
+          $fail->setAttribute('type', 'failure');
+          $fail->setAttribute('message', $result->message_group);
+          $text = $dom_document->createTextNode($result->message);
+          $fail->appendChild($text);
+          $case->appendChild($fail);
+        }
+        elseif ($result->status == 'exception') {
+          // In the case of an exception the $result->function may not be a
+          // class method so we record the full function name:
+          $case->setAttribute('name', $result->function);
+
+          $fail = $dom_document->createElement('error');
+          $fail->setAttribute('type', 'exception');
+          $fail->setAttribute('message', $result->message_group);
+          $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
+          $text = $dom_document->createTextNode($full_message);
+          $fail->appendChild($text);
+          $case->appendChild($fail);
+        }
+        // Append the test case XML to the test suite:
+        $xml_files[$test_class]['suite']->appendChild($case);
+      }
+    }
+    // The last test case hasn't been saved to a file yet, so do that now:
+    if (isset($xml_files[$test_class])) {
+      file_put_contents($this->config['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
+      unset($xml_files[$test_class]);
+    }
+  }
+
+  /**
+   * Run a group of phpunit tests.
+   */
+  protected function simpletest_script_run_phpunit($test_id, $class) {
+    $reflection = new \ReflectionClass($class);
+    if ($reflection->hasProperty('runLimit')) {
+      set_time_limit($reflection->getStaticPropertyValue('runLimit'));
+    }
+
+    $results = \simpletest_run_phpunit_tests($test_id, array($class), $status);
+    \simpletest_process_phpunit_results($results);
+
+    // Map phpunit results to a data structure we can pass to
+    // _simpletest_format_summary_line.
+    $summaries = array();
+    foreach ($results as $result) {
+      if (!isset($summaries[$result['test_class']])) {
+        $summaries[$result['test_class']] = array(
+          '#pass' => 0,
+          '#fail' => 0,
+          '#exception' => 0,
+          '#debug' => 0,
+        );
+      }
+
+      switch ($result['status']) {
+        case 'pass':
+          $summaries[$result['test_class']]['#pass']++;
+          break;
+
+        case 'fail':
+          $summaries[$result['test_class']]['#fail']++;
+          break;
+
+        case 'exception':
+          $summaries[$result['test_class']]['#exception']++;
+          break;
+
+        case 'debug':
+          $summaries[$result['test_class']]['#debug']++;
+          break;
+      }
+    }
+
+    foreach ($summaries as $class => $summary) {
+      $this->simpletest_script_reporter_display_summary($class, $summary);
+    }
+    return $status;
+  }
+
+  /**
+   * Find a usable PHP executable path based on input or environment.
+   *
+   * Determine location of php command automatically, unless a command line
+   * argument is supplied.
+   *
+   * @throws DrupalRunTestsException
+   *   Thrown when the method is unable to determine a PHP executable.
+   */
+  protected function gleanPhpPath() {
+    $php = NULL;
+    // Prefer the user-supplied PHP path.
+    if (!empty($this->config['php'])) {
+      $php = $this->config['php'];
+    }
+    // Fall back to the current binary. PHP_BINARY constant available as of PHP
+    // 5.4.
+    elseif ($php_binary = PHP_BINARY) {
+      $php = $php_binary;
+    }
+    // If that doesn't work, try the SUDO_COMMAND.
+    elseif ($sudo = getenv('SUDO_COMMAND')) {
+      // 'SUDO_COMMAND' is an environment variable set by the sudo program.
+      // Extract only the PHP interpreter, not the rest of the command.
+      list($php,) = explode(' ', $sudo, 2);
+    }
+    else {
+      $this->simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
+      $this->simpletest_script_help();
+      throw new DrupalRunTestsException('', SIMPLETEST_SCRIPT_EXIT_FAILURE);
+    }
+    return $php;
+  }
+
+  /**
+   * Shim for \simpletest_test_get_all().
+   *
+   * Enables testing.
+   *
+   * @param string|null $module
+   *   (optional) Name of a module. If set then only tests belonging to this
+   *   module are returned. Otherwise all tests are returned. Defaults to NULL.
+   *
+   * @return array[]
+   *   An array of tests keyed with the groups, and then keyed by test classes.
+   *
+   * @see \simpletest_test_get_all()
+   */
+  protected function simpletest_test_get_all($module = NULL) {
+    return \simpletest_test_get_all($module);
+  }
+
+}
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
old mode 100755
new mode 100644
index bf46a40..47126e9
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -2,1432 +2,15 @@
 
 /**
  * @file
- * This script runs Drupal tests from command line.
+ * A test runner script for Drupal.
  */
 
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\Timer;
-use Drupal\Component\Uuid\Php;
-use Drupal\Core\Database\Database;
-use Drupal\Core\StreamWrapper\PublicStream;
-use Drupal\Core\Test\TestRunnerKernel;
-use Drupal\simpletest\Form\SimpletestResultsForm;
-use Drupal\simpletest\TestBase;
-use Symfony\Component\HttpFoundation\Request;
+namespace Drupal\RunTests;
 
-$autoloader = require_once __DIR__ . '/../../autoload.php';
-
-const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
-const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
-const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
-
-// Restricting the chunk of queries prevents memory exhaustion.
-const SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT = 350;
-
-const SIMPLETEST_SCRIPT_EXIT_SUCCESS = 0;
-const SIMPLETEST_SCRIPT_EXIT_FAILURE = 1;
-const SIMPLETEST_SCRIPT_EXIT_EXCEPTION = 2;
-
-// Set defaults and get overrides.
-list($args, $count) = simpletest_script_parse_args();
-
-if ($args['help'] || $count == 0) {
-  simpletest_script_help();
-  exit(($count == 0) ? SIMPLETEST_SCRIPT_EXIT_FAILURE : SIMPLETEST_SCRIPT_EXIT_SUCCESS);
-}
-
-simpletest_script_init();
-
-try {
-  $request = Request::createFromGlobals();
-  $kernel = TestRunnerKernel::createFromRequest($request, $autoloader);
-  $kernel->prepareLegacyRequest($request);
-}
-catch (Exception $e) {
-  echo (string) $e;
-  exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-}
-
-if ($args['execute-test']) {
-  simpletest_script_setup_database();
-  simpletest_script_run_one_test($args['test-id'], $args['execute-test']);
-  // Sub-process exited already; this is just for clarity.
-  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
-}
-
-if ($args['list']) {
-  // Display all available tests.
-  echo "\nAvailable test groups & classes\n";
-  echo   "-------------------------------\n\n";
-  try {
-    $groups = simpletest_test_get_all($args['module']);
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-  foreach ($groups as $group => $tests) {
-    echo $group . "\n";
-    foreach ($tests as $class => $info) {
-      echo " - $class\n";
-    }
-  }
-  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
-}
-
-simpletest_script_setup_database(TRUE);
-
-if ($args['clean']) {
-  // Clean up left-over tables and directories.
-  try {
-    simpletest_clean_environment();
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-  echo "\nEnvironment cleaned.\n";
-
-  // Get the status messages and print them.
-  $messages = drupal_get_messages('status');
-  foreach ($messages['status'] as $text) {
-    echo " - " . $text . "\n";
-  }
-  exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
-}
-
-$test_list = simpletest_script_get_test_list();
-
-// Try to allocate unlimited time to run the tests.
-drupal_set_time_limit(0);
-simpletest_script_reporter_init();
-
-$tests_to_run = array();
-for ($i = 0; $i < $args['repeat']; $i++) {
-  $tests_to_run = array_merge($tests_to_run, $test_list);
-}
-
-// Execute tests.
-$status = simpletest_script_execute_batch($tests_to_run);
-
-// Stop the timer.
-simpletest_script_reporter_timer_stop();
-
-// Display results before database is cleared.
-if ($args['browser']) {
-  simpletest_script_open_browser();
-}
-else {
-  simpletest_script_reporter_display_results();
-}
-
-if ($args['xml']) {
-  simpletest_script_reporter_write_xml_results();
-}
-
-// Clean up all test results.
-if (!$args['keep-results']) {
-  try {
-    simpletest_clean_results_table();
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-}
-
-// Test complete, exit.
-exit($status);
-
-/**
- * Print help text.
- */
-function simpletest_script_help() {
-  global $args;
-
-  echo <<<EOF
-
-Run Drupal tests from the shell.
-
-Usage:        {$args['script']} [OPTIONS] <tests>
-Example:      {$args['script']} Profile
-
-All arguments are long options.
-
-  --help      Print this page.
-
-  --list      Display all available test groups.
-
-  --clean     Cleans up database tables or directories from previous, failed,
-              tests and then exits (no tests are run).
-
-  --url       The base URL of the root directory of this Drupal checkout; e.g.:
-                http://drupal.test/
-              Required unless the Drupal root directory maps exactly to:
-                http://localhost:80/
-              Use a https:// URL to force all tests to be run under SSL.
-
-  --sqlite    A pathname to use for the SQLite database of the test runner.
-              Required unless this script is executed with a working Drupal
-              installation that has Simpletest module installed.
-              A relative pathname is interpreted relative to the Drupal root
-              directory.
-              Note that ':memory:' cannot be used, because this script spawns
-              sub-processes. However, you may use e.g. '/tmpfs/test.sqlite'
-
-  --dburl     A URI denoting the database driver, credentials, server hostname,
-              and database name to use in tests.
-              Required when running tests without a Drupal installation that
-              contains default database connection info in settings.php.
-              Examples:
-                mysql://username:password@localhost/databasename#table_prefix
-                sqlite://localhost/relative/path/db.sqlite
-                sqlite://localhost//absolute/path/db.sqlite
-
-  --php       The absolute path to the PHP executable. Usually not needed.
-
-  --concurrency [num]
-
-              Run tests in parallel, up to [num] tests at a time.
-
-  --all       Run all available tests.
-
-  --module    Run all tests belonging to the specified module name.
-              (e.g., 'node')
-
-  --class     Run tests identified by specific class names, instead of group names.
-              A specific test method can be added, for example,
-              'Drupal\book\Tests\BookTest::testBookExport'.
-
-  --file      Run tests identified by specific file names, instead of group names.
-              Specify the path and the extension
-              (i.e. 'core/modules/user/user.test').
-
-  --directory Run all tests found within the specified file directory.
-
-  --xml       <path>
-
-              If provided, test results will be written as xml files to this path.
-
-  --color     Output text format results with color highlighting.
-
-  --verbose   Output detailed assertion messages in addition to summary.
-
-  --keep-results
-
-              Keeps detailed assertion results (in the database) after tests
-              have completed. By default, assertion results are cleared.
-
-  --repeat    Number of times to repeat the test.
-
-  --die-on-fail
-
-              Exit test execution immediately upon any failed assertion. This
-              allows to access the test site by changing settings.php to use the
-              test database and configuration directories. Use in combination
-              with --repeat for debugging random test failures.
-
-  --browser   Opens the results in the browser. This enforces --keep-results and
-              if you want to also view any pages rendered in the simpletest
-              browser you need to add --verbose to the command line.
-
-  --non-html  Removes escaping from output. Useful for reading results on the
-              CLI.
-
-  <test1>[,<test2>[,<test3> ...]]
-
-              One or more tests to be run. By default, these are interpreted
-              as the names of test groups as shown at
-              admin/config/development/testing.
-              These group names typically correspond to module names like "User"
-              or "Profile" or "System", but there is also a group "Database".
-              If --class is specified then these are interpreted as the names of
-              specific test classes whose test methods will be run. Tests must
-              be separated by commas. Ignored if --all is specified.
-
-To run this script you will normally invoke it from the root directory of your
-Drupal installation as the webserver user (differs per configuration), or root:
-
-sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
-  --url http://example.com/ --all
-sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
-  --url http://example.com/ --class "Drupal\block\Tests\BlockTest"
-
-Without a preinstalled Drupal site and enabled Simpletest module, specify a
-SQLite database pathname to create and the default database connection info to
-use in tests:
-
-sudo -u [wwwrun|www-data|etc] php ./core/scripts/{$args['script']}
-  --sqlite /tmpfs/drupal/test.sqlite
-  --dburl mysql://username:password@localhost/database
-  --url http://example.com/ --all
-
-EOF;
-}
-
-/**
- * Parse execution argument and ensure that all are valid.
- *
- * @return The list of arguments.
- */
-function simpletest_script_parse_args() {
-  // Set default values.
-  $args = array(
-    'script' => '',
-    'help' => FALSE,
-    'list' => FALSE,
-    'clean' => FALSE,
-    'url' => '',
-    'sqlite' => NULL,
-    'dburl' => NULL,
-    'php' => '',
-    'concurrency' => 1,
-    'all' => FALSE,
-    'module' => NULL,
-    'class' => FALSE,
-    'file' => FALSE,
-    'directory' => NULL,
-    'color' => FALSE,
-    'verbose' => FALSE,
-    'keep-results' => FALSE,
-    'test_names' => array(),
-    'repeat' => 1,
-    'die-on-fail' => FALSE,
-    'browser' => FALSE,
-    // Used internally.
-    'test-id' => 0,
-    'execute-test' => '',
-    'xml' => '',
-    'non-html' => FALSE,
-  );
-
-  // Override with set values.
-  $args['script'] = basename(array_shift($_SERVER['argv']));
-
-  $count = 0;
-  while ($arg = array_shift($_SERVER['argv'])) {
-    if (preg_match('/--(\S+)/', $arg, $matches)) {
-      // Argument found.
-      if (array_key_exists($matches[1], $args)) {
-        // Argument found in list.
-        $previous_arg = $matches[1];
-        if (is_bool($args[$previous_arg])) {
-          $args[$matches[1]] = TRUE;
-        }
-        else {
-          $args[$matches[1]] = array_shift($_SERVER['argv']);
-        }
-        // Clear extraneous values.
-        $args['test_names'] = array();
-        $count++;
-      }
-      else {
-        // Argument not found in list.
-        simpletest_script_print_error("Unknown argument '$arg'.");
-        exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-      }
-    }
-    else {
-      // Values found without an argument should be test names.
-      $args['test_names'] += explode(',', $arg);
-      $count++;
-    }
-  }
-
-  // Validate the concurrency argument
-  if (!is_numeric($args['concurrency']) || $args['concurrency'] <= 0) {
-    simpletest_script_print_error("--concurrency must be a strictly positive integer.");
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-  }
-
-  if ($args['browser']) {
-    $args['keep-results'] = TRUE;
-  }
-  return array($args, $count);
-}
-
-/**
- * Initialize script variables and perform general setup requirements.
- */
-function simpletest_script_init() {
-  global $args, $php;
-
-  $host = 'localhost';
-  $path = '';
-  $port = '80';
-
-  // Determine location of php command automatically, unless a command line argument is supplied.
-  if (!empty($args['php'])) {
-    $php = $args['php'];
-  }
-  elseif ($php_env = getenv('_')) {
-    // '_' is an environment variable set by the shell. It contains the command that was executed.
-    $php = $php_env;
-  }
-  elseif ($sudo = getenv('SUDO_COMMAND')) {
-    // 'SUDO_COMMAND' is an environment variable set by the sudo program.
-    // Extract only the PHP interpreter, not the rest of the command.
-    list($php, ) = explode(' ', $sudo, 2);
-  }
-  else {
-    simpletest_script_print_error('Unable to automatically determine the path to the PHP interpreter. Supply the --php command line argument.');
-    simpletest_script_help();
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-  }
-
-  // Get URL from arguments.
-  if (!empty($args['url'])) {
-    $parsed_url = parse_url($args['url']);
-    $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '');
-    $path = isset($parsed_url['path']) ? rtrim(rtrim($parsed_url['path']), '/') : '';
-    $port = (isset($parsed_url['port']) ? $parsed_url['port'] : $port);
-    if ($path == '/') {
-      $path = '';
-    }
-    // If the passed URL schema is 'https' then setup the $_SERVER variables
-    // properly so that testing will run under HTTPS.
-    if ($parsed_url['scheme'] == 'https') {
-      $_SERVER['HTTPS'] = 'on';
-    }
-  }
-
-  if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
-    $base_url = 'https://';
-  }
-  else {
-    $base_url = 'http://';
-  }
-  $base_url .= $host;
-  if ($path !== '') {
-    $base_url .= $path;
-  }
-  putenv('SIMPLETEST_BASE_URL=' . $base_url);
-  $_SERVER['HTTP_HOST'] = $host;
-  $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
-  $_SERVER['SERVER_ADDR'] = '127.0.0.1';
-  $_SERVER['SERVER_PORT'] = $port;
-  $_SERVER['SERVER_SOFTWARE'] = NULL;
-  $_SERVER['SERVER_NAME'] = 'localhost';
-  $_SERVER['REQUEST_URI'] = $path .'/';
-  $_SERVER['REQUEST_METHOD'] = 'GET';
-  $_SERVER['SCRIPT_NAME'] = $path .'/index.php';
-  $_SERVER['SCRIPT_FILENAME'] = $path .'/index.php';
-  $_SERVER['PHP_SELF'] = $path .'/index.php';
-  $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
-
-  if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
-    // Ensure that any and all environment variables are changed to https://.
-    foreach ($_SERVER as $key => $value) {
-      $_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
-    }
-  }
-
-  chdir(realpath(__DIR__ . '/../..'));
-}
-
-/**
- * Sets up database connection info for running tests.
- *
- * If this script is executed from within a real Drupal installation, then this
- * function essentially performs nothing (unless the --sqlite or --dburl
- * parameters were passed).
- *
- * Otherwise, there are three database connections of concern:
- * - --sqlite: The test runner connection, providing access to Simpletest
- *   database tables for recording test IDs and assertion results.
- * - --dburl: A database connection that is used as base connection info for all
- *   tests; i.e., every test will spawn from this connection. In case this
- *   connection uses e.g. SQLite, then all tests will run against SQLite. This
- *   is exposed as $databases['default']['default'] to Drupal.
- * - The actual database connection used within a test. This is the same as
- *   --dburl, but uses an additional database table prefix. This is
- *   $databases['default']['default'] within a test environment. The original
- *   connection is retained in
- *   $databases['simpletest_original_default']['default'] and restored after
- *   each test.
- *
- * @param bool $new
- *   Whether this process is a run-tests.sh master process. If TRUE, the SQLite
- *   database file specified by --sqlite (if any) is set up. Otherwise, database
- *   connections are prepared only.
- */
-function simpletest_script_setup_database($new = FALSE) {
-  global $args;
-
-  // If there is an existing Drupal installation that contains a database
-  // connection info in settings.php, then $databases['default']['default'] will
-  // hold the default database connection already. This connection is assumed to
-  // be valid, and this connection will be used in tests, so that they run
-  // against e.g. MySQL instead of SQLite.
-
-  // However, in case no Drupal installation exists, this default database
-  // connection can be set and/or overridden with the --dburl parameter.
-  if (!empty($args['dburl'])) {
-    // Remove a possibly existing default connection (from settings.php).
-    Database::removeConnection('default');
-    try {
-      $databases['default']['default'] = Database::convertDbUrlToConnectionInfo($args['dburl'], DRUPAL_ROOT);
-    }
-    catch (\InvalidArgumentException $e) {
-      simpletest_script_print_error('Invalid --dburl. Reason: ' . $e->getMessage());
-      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-    }
-  }
-  // Otherwise, use the default database connection from settings.php.
-  else {
-    $databases['default'] = Database::getConnectionInfo('default');
-  }
-
-  // If there is no default database connection for tests, we cannot continue.
-  if (!isset($databases['default']['default'])) {
-    simpletest_script_print_error('Missing default database connection for tests. Use --dburl to specify one.');
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-  }
-  Database::addConnectionInfo('default', 'default', $databases['default']['default']);
-
-  // If no --sqlite parameter has been passed, then Simpletest module is assumed
-  // to be installed, so the test runner database connection is the default
-  // database connection.
-  if (empty($args['sqlite'])) {
-    $sqlite = FALSE;
-    $databases['test-runner']['default'] = $databases['default']['default'];
-  }
-  // Otherwise, set up a SQLite connection for the test runner.
-  else {
-    if ($args['sqlite'][0] === '/') {
-      $sqlite = $args['sqlite'];
-    }
-    else {
-      $sqlite = DRUPAL_ROOT . '/' . $args['sqlite'];
-    }
-    $databases['test-runner']['default'] = array(
-      'driver' => 'sqlite',
-      'database' => $sqlite,
-      'prefix' => array(
-        'default' => '',
-      ),
-    );
-    // Create the test runner SQLite database, unless it exists already.
-    if ($new && !file_exists($sqlite)) {
-      if (!is_dir(dirname($sqlite))) {
-        mkdir(dirname($sqlite));
-      }
-      touch($sqlite);
-    }
-  }
-
-  // Add the test runner database connection.
-  Database::addConnectionInfo('test-runner', 'default', $databases['test-runner']['default']);
-
-  // Create the Simpletest schema.
-  try {
-    $schema = Database::getConnection('default', 'test-runner')->schema();
-  }
-  catch (\PDOException $e) {
-    simpletest_script_print_error($databases['test-runner']['default']['driver'] . ': ' . $e->getMessage());
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-  }
-  if ($new && $sqlite) {
-    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'simpletest') . '/simpletest.install';
-    foreach (simpletest_schema() as $name => $table_spec) {
-      try {
-        if ($schema->tableExists($name)) {
-          $schema->dropTable($name);
-        }
-        $schema->createTable($name, $table_spec);
-      }
-      catch (Exception $e) {
-        echo (string) $e;
-        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-      }
-    }
-  }
-  // Verify that the Simpletest database schema exists by checking one table.
-  try {
-    if (!$schema->tableExists('simpletest')) {
-      simpletest_script_print_error('Missing Simpletest database schema. Either install Simpletest module or use the --sqlite parameter.');
-      exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-    }
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-}
-
-/**
- * Execute a batch of tests.
- */
-function simpletest_script_execute_batch($test_classes) {
-  global $args, $test_ids;
-
-  $total_status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
-
-  // Multi-process execution.
-  $children = array();
-  while (!empty($test_classes) || !empty($children)) {
-    while (count($children) < $args['concurrency']) {
-      if (empty($test_classes)) {
-        break;
-      }
-
-      try {
-        $test_id = Database::getConnection('default', 'test-runner')
-          ->insert('simpletest_test_id')
-          ->useDefaults(array('test_id'))
-          ->execute();
-      }
-      catch (Exception $e) {
-        echo (string) $e;
-        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-      }
-      $test_ids[] = $test_id;
-
-      $test_class = array_shift($test_classes);
-      // Fork a child process.
-      $command = simpletest_script_command($test_id, $test_class);
-      $process = proc_open($command, array(), $pipes, NULL, NULL, array('bypass_shell' => TRUE));
-
-      if (!is_resource($process)) {
-        echo "Unable to fork test process. Aborting.\n";
-        exit(SIMPLETEST_SCRIPT_EXIT_SUCCESS);
-      }
-
-      // Register our new child.
-      $children[] = array(
-        'process' => $process,
-        'test_id' => $test_id,
-        'class' => $test_class,
-        'pipes' => $pipes,
-      );
-    }
-
-    // Wait for children every 200ms.
-    usleep(200000);
-
-    // Check if some children finished.
-    foreach ($children as $cid => $child) {
-      $status = proc_get_status($child['process']);
-      if (empty($status['running'])) {
-        // The child exited, unregister it.
-        proc_close($child['process']);
-        if ($status['exitcode'] === SIMPLETEST_SCRIPT_EXIT_FAILURE) {
-          $total_status = max($status['exitcode'], $total_status);
-        }
-        elseif ($status['exitcode']) {
-          $message = 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').';
-          echo $message . "\n";
-          // Insert a fail for xml results.
-          TestBase::insertAssert($child['test_id'], $child['class'], FALSE, $message, 'run-tests.sh check');
-          /// Ensure that an error line is displayed for the class.
-          simpletest_script_reporter_display_summary($child['class'], ['#pass' => 0, '#fail' => 1, '#exception' => 0, '#debug' => 0]);
-          if ($args['die-on-fail']) {
-            list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
-            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
-            echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix '. $db_prefix . ' and config directories in '. $test_directory . "\n";
-            $args['keep-results'] = TRUE;
-            // Exit repeat loop immediately.
-            $args['repeat'] = -1;
-          }
-        }
-        // Free-up space by removing any potentially created resources.
-        if (!$args['keep-results']) {
-          simpletest_script_cleanup($child['test_id'], $child['class'], $status['exitcode']);
-        }
-
-        // Remove this child.
-        unset($children[$cid]);
-      }
-    }
-  }
-  return $total_status;
-}
+use Drupal\Core\RunTests\RunTestsApplication;
 
-/**
- * Run a group of phpunit tests.
- */
-function simpletest_script_run_phpunit($test_id, $class) {
-  $reflection = new \ReflectionClass($class);
-  if ($reflection->hasProperty('runLimit')) {
-    set_time_limit($reflection->getStaticPropertyValue('runLimit'));
-  }
-
-  $results = simpletest_run_phpunit_tests($test_id, array($class), $status);
-  simpletest_process_phpunit_results($results);
-
-  // Map phpunit results to a data structure we can pass to
-  // _simpletest_format_summary_line.
-  $summaries = array();
-  foreach ($results as $result) {
-    if (!isset($summaries[$result['test_class']])) {
-      $summaries[$result['test_class']] = array(
-        '#pass' => 0,
-        '#fail' => 0,
-        '#exception' => 0,
-        '#debug' => 0,
-      );
-    }
-
-    switch ($result['status']) {
-      case 'pass':
-        $summaries[$result['test_class']]['#pass']++;
-        break;
-      case 'fail':
-        $summaries[$result['test_class']]['#fail']++;
-        break;
-      case 'exception':
-        $summaries[$result['test_class']]['#exception']++;
-        break;
-      case 'debug':
-        $summaries[$result['test_class']]['#debug']++;
-        break;
-    }
-  }
-
-  foreach ($summaries as $class => $summary) {
-    simpletest_script_reporter_display_summary($class, $summary);
-  }
-  return $status;
-}
-
-/**
- * Bootstrap Drupal and run a single test.
- */
-function simpletest_script_run_one_test($test_id, $test_class) {
-  global $args;
-
-  try {
-    if (strpos($test_class, '::') > 0) {
-      list($class_name, $method) = explode('::', $test_class, 2);
-      $methods = [$method];
-    }
-    else {
-      $class_name = $test_class;
-      // Use empty array to run all the test methods.
-      $methods = array();
-    }
-    $test = new $class_name($test_id);
-    if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
-      $status = simpletest_script_run_phpunit($test_id, $test_class);
-    }
-    else {
-      $test->dieOnFail = (bool) $args['die-on-fail'];
-      $test->verbose = (bool) $args['verbose'];
-      $test->run($methods);
-      simpletest_script_reporter_display_summary($test_class, $test->results);
-
-      $status = SIMPLETEST_SCRIPT_EXIT_SUCCESS;
-      // Finished, kill this runner.
-      if ($test->results['#fail'] || $test->results['#exception']) {
-        $status = SIMPLETEST_SCRIPT_EXIT_FAILURE;
-      }
-    }
-
-    exit($status);
-  }
-  // DrupalTestCase::run() catches exceptions already, so this is only reached
-  // when an exception is thrown in the wrapping test runner environment.
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-}
-
-/**
- * Return a command used to run a test in a separate process.
- *
- * @param $test_id
- *  The current test ID.
- * @param $test_class
- *  The name of the test class to run.
- */
-function simpletest_script_command($test_id, $test_class) {
-  global $args, $php;
-
-  $command = escapeshellarg($php) . ' ' . escapeshellarg('./core/scripts/' . $args['script']);
-  $command .= ' --url ' . escapeshellarg($args['url']);
-  if (!empty($args['sqlite'])) {
-    $command .= ' --sqlite ' . escapeshellarg($args['sqlite']);
-  }
-  if (!empty($args['dburl'])) {
-    $command .= ' --dburl ' . escapeshellarg($args['dburl']);
-  }
-  $command .= ' --php ' . escapeshellarg($php);
-  $command .= " --test-id $test_id";
-  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
-    if ($args[$arg]) {
-      $command .= ' --' . $arg;
-    }
-  }
-  // --execute-test and class name needs to come last.
-  $command .= ' --execute-test ' . escapeshellarg($test_class);
-  return $command;
-}
-
-/**
- * Removes all remnants of a test runner.
- *
- * In case a (e.g., fatal) error occurs after the test site has been fully setup
- * and the error happens in many tests, the environment that executes the tests
- * can easily run out of memory or disk space. This function ensures that all
- * created resources are properly cleaned up after every executed test.
- *
- * This clean-up only exists in this script, since SimpleTest module itself does
- * not use isolated sub-processes for each test being run, so a fatal error
- * halts not only the test, but also the test runner (i.e., the parent site).
- *
- * @param int $test_id
- *   The test ID of the test run.
- * @param string $test_class
- *   The class name of the test run.
- * @param int $exitcode
- *   The exit code of the test runner.
- *
- * @see simpletest_script_run_one_test()
- */
-function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
-  if (is_subclass_of($test_class, '\PHPUnit_Framework_TestCase')) {
-    // PHPUnit test, move on.
-    return;
-  }
-  // Retrieve the last database prefix used for testing.
-  try {
-    list($db_prefix,) = simpletest_last_test_get($test_id);
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-
-  // If no database prefix was found, then the test was not set up correctly.
-  if (empty($db_prefix)) {
-    echo "\nFATAL $test_class: Found no database prefix for test ID $test_id. (Check whether setUp() is invoked correctly.)";
-    return;
-  }
-
-  // Do not output verbose cleanup messages in case of a positive exitcode.
-  $output = !empty($exitcode);
-  $messages = array();
-
-  $messages[] = "- Found database prefix '$db_prefix' for test ID $test_id.";
-
-  // Read the log file in case any fatal errors caused the test to crash.
-  try {
-    simpletest_log_read($test_id, $db_prefix, $test_class);
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-
-  // Check whether a test site directory was setup already.
-  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
-  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
-  if (is_dir($test_directory)) {
-    // Output the error_log.
-    if (is_file($test_directory . '/error.log')) {
-      if ($errors = file_get_contents($test_directory . '/error.log')) {
-        $output = TRUE;
-        $messages[] = $errors;
-      }
-    }
-    // Delete the test site directory.
-    // simpletest_clean_temporary_directories() cannot be used here, since it
-    // would also delete file directories of other tests that are potentially
-    // running concurrently.
-    file_unmanaged_delete_recursive($test_directory, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
-    $messages[] = "- Removed test site directory.";
-  }
-
-  // Clear out all database tables from the test.
-  try {
-    $schema = Database::getConnection('default', 'default')->schema();
-    $count = 0;
-    foreach ($schema->findTables($db_prefix . '%') as $table) {
-      $schema->dropTable($table);
-      $count++;
-    }
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-
-  if ($count) {
-    $messages[] = "- Removed $count leftover tables.";
-  }
-
-  if ($output) {
-    echo implode("\n", $messages);
-    echo "\n";
-  }
-}
-
-/**
- * Get list of tests based on arguments. If --all specified then
- * returns all available tests, otherwise reads list of tests.
- *
- * Will print error and exit if no valid tests were found.
- *
- * @return List of tests.
- */
-function simpletest_script_get_test_list() {
-  global $args;
-
-  $test_list = array();
-  if ($args['all'] || $args['module']) {
-    try {
-      $groups = simpletest_test_get_all($args['module']);
-    }
-    catch (Exception $e) {
-      echo (string) $e;
-      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-    }
-    $all_tests = array();
-    foreach ($groups as $group => $tests) {
-      $all_tests = array_merge($all_tests, array_keys($tests));
-    }
-    $test_list = $all_tests;
-  }
-  else {
-    if ($args['class']) {
-      $test_list = array();
-      foreach ($args['test_names'] as $test_class) {
-        list($class_name, ) = explode('::', $test_class, 2);
-        if (class_exists($class_name)) {
-          $test_list[] = $test_class;
-        }
-        else {
-          try {
-            $groups = simpletest_test_get_all();
-          }
-          catch (Exception $e) {
-            echo (string) $e;
-            exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-          }
-          $all_classes = array();
-          foreach ($groups as $group) {
-            $all_classes = array_merge($all_classes, array_keys($group));
-          }
-          simpletest_script_print_error('Test class not found: ' . $class_name);
-          simpletest_script_print_alternatives($class_name, $all_classes, 6);
-          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-        }
-      }
-    }
-    elseif ($args['file']) {
-      // Extract test case class names from specified files.
-      foreach ($args['test_names'] as $file) {
-        if (!file_exists($file)) {
-          simpletest_script_print_error('File not found: ' . $file);
-          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-        }
-        $content = file_get_contents($file);
-        // Extract a potential namespace.
-        $namespace = FALSE;
-        if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
-          $namespace = $matches[1];
-        }
-        // Extract all class names.
-        // Abstract classes are excluded on purpose.
-        preg_match_all('@^class ([^ ]+)@m', $content, $matches);
-        if (!$namespace) {
-          $test_list = array_merge($test_list, $matches[1]);
-        }
-        else {
-          foreach ($matches[1] as $class_name) {
-            $namespace_class = $namespace . '\\' . $class_name;
-            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
-              $test_list[] = $namespace_class;
-            }
-          }
-        }
-      }
-    }
-    elseif ($args['directory']) {
-      // Extract test case class names from specified directory.
-      // Find all tests in the PSR-X structure; Drupal\$extension\Tests\*.php
-      // Since we do not want to hard-code too many structural file/directory
-      // assumptions about PSR-0/4 files and directories, we check for the
-      // minimal conditions only; i.e., a '*.php' file that has '/Tests/' in
-      // its path.
-      // Ignore anything from third party vendors.
-      $ignore = array('.', '..', 'vendor');
-      $files = [];
-      if ($args['directory'][0] === '/') {
-        $directory = $args['directory'];
-      }
-      else {
-        $directory = DRUPAL_ROOT . "/" . $args['directory'];
-      }
-      foreach (file_scan_directory($directory, '/\.php$/', $ignore) as $file) {
-        // '/Tests/' can be contained anywhere in the file's path (there can be
-        // sub-directories below /Tests), but must be contained literally.
-        // Case-insensitive to match all Simpletest and PHPUnit tests:
-        //   ./lib/Drupal/foo/Tests/Bar/Baz.php
-        //   ./foo/src/Tests/Bar/Baz.php
-        //   ./foo/tests/Drupal/foo/Tests/FooTest.php
-        //   ./foo/tests/src/FooTest.php
-        // $file->filename doesn't give us a directory, so we use $file->uri
-        // Strip the drupal root directory and trailing slash off the URI
-        $filename = substr($file->uri, strlen(DRUPAL_ROOT)+1);
-        if (stripos($filename, '/Tests/')) {
-          $files[$filename] = $filename;
-        }
-      }
-      foreach ($files as $file) {
-        $content = file_get_contents($file);
-        // Extract a potential namespace.
-        $namespace = FALSE;
-        if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
-          $namespace = $matches[1];
-        }
-        // Extract all class names.
-        // Abstract classes are excluded on purpose.
-        preg_match_all('@^class ([^ ]+)@m', $content, $matches);
-        if (!$namespace) {
-          $test_list = array_merge($test_list, $matches[1]);
-        }
-        else {
-          foreach ($matches[1] as $class_name) {
-            $namespace_class = $namespace . '\\' . $class_name;
-            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, '\PHPUnit_Framework_TestCase')) {
-              $test_list[] = $namespace_class;
-            }
-          }
-        }
-      }
-    }
-    else {
-      try {
-        $groups = simpletest_test_get_all();
-      }
-      catch (Exception $e) {
-        echo (string) $e;
-        exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-      }
-      foreach ($args['test_names'] as $group_name) {
-        if (isset($groups[$group_name])) {
-          $test_list = array_merge($test_list, array_keys($groups[$group_name]));
-        }
-        else {
-          simpletest_script_print_error('Test group not found: ' . $group_name);
-          simpletest_script_print_alternatives($group_name, array_keys($groups));
-          exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-        }
-      }
-    }
-  }
-
-  if (empty($test_list)) {
-    simpletest_script_print_error('No valid tests were specified.');
-    exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
-  }
-  return $test_list;
-}
-
-/**
- * Initialize the reporter.
- */
-function simpletest_script_reporter_init() {
-  global $args, $test_list, $results_map;
-
-  $results_map = array(
-    'pass' => 'Pass',
-    'fail' => 'Fail',
-    'exception' => 'Exception'
-  );
-
-  echo "\n";
-  echo "Drupal test run\n";
-  echo "---------------\n";
-  echo "\n";
-
-  // Tell the user about what tests are to be run.
-  if ($args['all']) {
-    echo "All tests will run.\n\n";
-  }
-  else {
-    echo "Tests to be run:\n";
-    foreach ($test_list as $class_name) {
-      echo "  - $class_name\n";
-    }
-    echo "\n";
-  }
-
-  echo "Test run started:\n";
-  echo "  " . date('l, F j, Y - H:i', $_SERVER['REQUEST_TIME']) . "\n";
-  Timer::start('run-tests');
-  echo "\n";
-
-  echo "Test summary\n";
-  echo "------------\n";
-  echo "\n";
-}
-
-/**
- * Displays the assertion result summary for a single test class.
- *
- * @param string $class
- *   The test class name that was run.
- * @param array $results
- *   The assertion results using #pass, #fail, #exception, #debug array keys.
- */
-function simpletest_script_reporter_display_summary($class, $results) {
-  // Output all test results vertically aligned.
-  // Cut off the class name after 60 chars, and pad each group with 3 digits
-  // by default (more than 999 assertions are rare).
-  $output = vsprintf('%-60.60s %10s %9s %14s %12s', array(
-    $class,
-                                   $results['#pass']      . ' passes',
-    !$results['#fail']      ? '' : $results['#fail']      . ' fails',
-    !$results['#exception'] ? '' : $results['#exception'] . ' exceptions',
-    !$results['#debug']     ? '' : $results['#debug']     . ' messages',
-  ));
-
-  $status = ($results['#fail'] || $results['#exception'] ? 'fail' : 'pass');
-  simpletest_script_print($output . "\n", simpletest_script_color_code($status));
-}
-
-/**
- * Display jUnit XML test results.
- */
-function simpletest_script_reporter_write_xml_results() {
-  global $args, $test_ids, $results_map;
-
-  try {
-    $results = simpletest_script_load_messages_by_test_id($test_ids);
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-
-  $test_class = '';
-  $xml_files = array();
-
-  foreach ($results as $result) {
-    if (isset($results_map[$result->status])) {
-      if ($result->test_class != $test_class) {
-        // We've moved onto a new class, so write the last classes results to a file:
-        if (isset($xml_files[$test_class])) {
-          file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
-          unset($xml_files[$test_class]);
-        }
-        $test_class = $result->test_class;
-        if (!isset($xml_files[$test_class])) {
-          $doc = new DomDocument('1.0');
-          $root = $doc->createElement('testsuite');
-          $root = $doc->appendChild($root);
-          $xml_files[$test_class] = array('doc' => $doc, 'suite' => $root);
-        }
-      }
-
-      // For convenience:
-      $dom_document = &$xml_files[$test_class]['doc'];
-
-      // Create the XML element for this test case:
-      $case = $dom_document->createElement('testcase');
-      $case->setAttribute('classname', $test_class);
-      if (strpos($result->function, '->') !== FALSE) {
-        list($class, $name) = explode('->', $result->function, 2);
-      }
-      else {
-        $name = $result->function;
-      }
-      $case->setAttribute('name', $name);
-
-      // Passes get no further attention, but failures and exceptions get to add more detail:
-      if ($result->status == 'fail') {
-        $fail = $dom_document->createElement('failure');
-        $fail->setAttribute('type', 'failure');
-        $fail->setAttribute('message', $result->message_group);
-        $text = $dom_document->createTextNode($result->message);
-        $fail->appendChild($text);
-        $case->appendChild($fail);
-      }
-      elseif ($result->status == 'exception') {
-        // In the case of an exception the $result->function may not be a class
-        // method so we record the full function name:
-        $case->setAttribute('name', $result->function);
-
-        $fail = $dom_document->createElement('error');
-        $fail->setAttribute('type', 'exception');
-        $fail->setAttribute('message', $result->message_group);
-        $full_message = $result->message . "\n\nline: " . $result->line . "\nfile: " . $result->file;
-        $text = $dom_document->createTextNode($full_message);
-        $fail->appendChild($text);
-        $case->appendChild($fail);
-      }
-      // Append the test case XML to the test suite:
-      $xml_files[$test_class]['suite']->appendChild($case);
-    }
-  }
-  // The last test case hasn't been saved to a file yet, so do that now:
-  if (isset($xml_files[$test_class])) {
-    file_put_contents($args['xml'] . '/' . str_replace('\\', '_', $test_class) . '.xml', $xml_files[$test_class]['doc']->saveXML());
-    unset($xml_files[$test_class]);
-  }
-}
-
-/**
- * Stop the test timer.
- */
-function simpletest_script_reporter_timer_stop() {
-  echo "\n";
-  $end = Timer::stop('run-tests');
-  echo "Test run duration: " . \Drupal::service('date.formatter')->formatInterval($end['time'] / 1000);
-  echo "\n\n";
-}
-
-/**
- * Display test results.
- */
-function simpletest_script_reporter_display_results() {
-  global $args, $test_ids, $results_map;
-
-  if ($args['verbose']) {
-    // Report results.
-    echo "Detailed test results\n";
-    echo "---------------------\n";
-
-    try {
-      $results = simpletest_script_load_messages_by_test_id($test_ids);
-    }
-    catch (Exception $e) {
-      echo (string) $e;
-      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-    }
-    $test_class = '';
-    foreach ($results as $result) {
-      if (isset($results_map[$result->status])) {
-        if ($result->test_class != $test_class) {
-          // Display test class every time results are for new test class.
-          echo "\n\n---- $result->test_class ----\n\n\n";
-          $test_class = $result->test_class;
-
-          // Print table header.
-          echo "Status    Group      Filename          Line Function                            \n";
-          echo "--------------------------------------------------------------------------------\n";
-        }
-
-        simpletest_script_format_result($result);
-      }
-    }
-  }
-}
-
-/**
- * Format the result so that it fits within the default 80 character
- * terminal size.
- *
- * @param $result The result object to format.
- */
-function simpletest_script_format_result($result) {
-  global $args, $results_map, $color;
-
-  $summary = sprintf("%-9.9s %-10.10s %-17.17s %4.4s %-35.35s\n",
-    $results_map[$result->status], $result->message_group, basename($result->file), $result->line, $result->function);
-
-  simpletest_script_print($summary, simpletest_script_color_code($result->status));
-
-  $message = trim(strip_tags($result->message));
-  if ($args['non-html']) {
-    $message = Html::decodeEntities($message, ENT_QUOTES, 'UTF-8');
-  }
-  $lines = explode("\n", wordwrap($message), 76);
-  foreach ($lines as $line) {
-    echo "    $line\n";
-  }
-}
-
-/**
- * Print error message prefixed with "  ERROR: " and displayed in fail color
- * if color output is enabled.
- *
- * @param $message The message to print.
- */
-function simpletest_script_print_error($message) {
-  simpletest_script_print("  ERROR: $message\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
-}
-
-/**
- * Print a message to the console, if color is enabled then the specified
- * color code will be used.
- *
- * @param $message The message to print.
- * @param $color_code The color code to use for coloring.
- */
-function simpletest_script_print($message, $color_code) {
-  global $args;
-  if ($args['color']) {
-    echo "\033[" . $color_code . "m" . $message . "\033[0m";
-  }
-  else {
-    echo $message;
-  }
-}
-
-/**
- * Get the color code associated with the specified status.
- *
- * @param $status The status string to get code for.
- * @return Color code.
- */
-function simpletest_script_color_code($status) {
-  switch ($status) {
-    case 'pass':
-      return SIMPLETEST_SCRIPT_COLOR_PASS;
-    case 'fail':
-      return SIMPLETEST_SCRIPT_COLOR_FAIL;
-    case 'exception':
-      return SIMPLETEST_SCRIPT_COLOR_EXCEPTION;
-  }
-  return 0; // Default formatting.
-}
-
-/**
- * Prints alternative test names.
- *
- * Searches the provided array of string values for close matches based on the
- * Levenshtein algorithm.
- *
- * @see http://php.net/manual/en/function.levenshtein.php
- *
- * @param string $string
- *   A string to test.
- * @param array $array
- *   A list of strings to search.
- * @param int $degree
- *   The matching strictness. Higher values return fewer matches. A value of
- *   4 means that the function will return strings from $array if the candidate
- *   string in $array would be identical to $string by changing 1/4 or fewer of
- *   its characters.
- */
-function simpletest_script_print_alternatives($string, $array, $degree = 4) {
-  $alternatives = array();
-  foreach ($array as $item) {
-    $lev = levenshtein($string, $item);
-    if ($lev <= strlen($item) / $degree || FALSE !== strpos($string, $item)) {
-      $alternatives[] = $item;
-    }
-  }
-  if (!empty($alternatives)) {
-    simpletest_script_print("  Did you mean?\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
-    foreach ($alternatives as $alternative) {
-      simpletest_script_print("  - $alternative\n", SIMPLETEST_SCRIPT_COLOR_FAIL);
-    }
-  }
-}
-
-/**
- * Loads the simpletest messages from the database.
- *
- * Messages are ordered by test class and message id.
- *
- * @param array $test_ids
- *   Array of test IDs of the messages to be loaded.
- *
- * @return array
- *   Array of simpletest messages from the database.
- */
-function simpletest_script_load_messages_by_test_id($test_ids) {
-  global $args;
-  $results = array();
-
-  // Sqlite has a maximum number of variables per query. If required, the
-  // database query is split into chunks.
-  if (count($test_ids) > SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT && !empty($args['sqlite'])) {
-    $test_id_chunks = array_chunk($test_ids, SIMPLETEST_SCRIPT_SQLITE_VARIABLE_LIMIT);
-  }
-  else {
-    $test_id_chunks = array($test_ids);
-  }
-
-  foreach ($test_id_chunks as $test_id_chunk) {
-    try {
-      $result_chunk = Database::getConnection('default', 'test-runner')
-        ->query("SELECT * FROM {simpletest} WHERE test_id IN ( :test_ids[] ) ORDER BY test_class, message_id", array(
-          ':test_ids[]' => $test_id_chunk,
-        ))->fetchAll();
-    }
-    catch (Exception $e) {
-      echo (string) $e;
-      exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-    }
-    if ($result_chunk) {
-      $results = array_merge($results, $result_chunk);
-    }
-  }
-
-  return $results;
-}
-
-/**
- * Display test results.
- */
-function simpletest_script_open_browser() {
-  global $test_ids;
-
-  try {
-    $connection = Database::getConnection('default', 'test-runner');
-    $results = $connection->select('simpletest')
-      ->fields('simpletest')
-      ->condition('test_id', $test_ids, 'IN')
-      ->orderBy('test_class')
-      ->orderBy('message_id')
-      ->execute()
-      ->fetchAll();
-  }
-  catch (Exception $e) {
-    echo (string) $e;
-    exit(SIMPLETEST_SCRIPT_EXIT_EXCEPTION);
-  }
-
-  // Get the results form.
-  $form = array();
-  SimpletestResultsForm::addResultForm($form, $results);
-
-  // Get the assets to make the details element collapsible and theme the result
-  // form.
-  $assets = new \Drupal\Core\Asset\AttachedAssets();
-  $assets->setLibraries(['core/drupal.collapse', 'system/admin', 'simpletest/drupal.simpletest']);
-  $resolver = \Drupal::service('asset.resolver');
-  list($js_assets_header, $js_assets_footer) = $resolver->getJsAssets($assets, FALSE);
-  $js_collection_renderer = \Drupal::service('asset.js.collection_renderer');
-  $js_assets_header = $js_collection_renderer->render($js_assets_header);
-  $js_assets_footer = $js_collection_renderer->render($js_assets_footer);
-  $css_assets = \Drupal::service('asset.css.collection_renderer')->render($resolver->getCssAssets($assets, FALSE));
-
-  // Make the html page to write to disk.
-  $render_service = \Drupal::service('renderer');
-  $html = '<head>' . $render_service->renderPlain($js_assets_header) . $render_service->renderPlain($css_assets) . '</head><body>' . $render_service->renderPlain($form) . $render_service->renderPlain($js_assets_footer) .'</body>';
-
-  // Ensure we have assets verbose directory - tests with no verbose output will not
-  // have created one.
-  $directory = PublicStream::basePath() . '/simpletest/verbose';
-  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-  $uuid = new Php();
-  $filename = $directory .'/results-'. $uuid->generate() .'.html';
-  file_put_contents($filename, $html);
-
-  // See if we can find an OS helper to open URLs in default browser.
-  $browser = FALSE;
-  if (shell_exec('which xdg-open')) {
-    $browser = 'xdg-open';
-  }
-  elseif (shell_exec('which open')) {
-    $browser = 'open';
-  }
-  elseif (substr(PHP_OS, 0, 3) == 'WIN') {
-    $browser = 'start';
-  }
+$autoloader = require_once __DIR__ . '/../../autoload.php';
+require_once __DIR__ . '/../modules/simpletest/simpletest.module';
 
-  if ($browser) {
-    shell_exec($browser . ' ' . escapeshellarg($filename));
-  }
-  else {
-    // Can't find assets valid browser.
-    print 'Open file://' . realpath($filename) . ' in your browser to see the verbose output.';
-  }
-}
+$app = new RunTestsApplication($autoloader, basename(__FILE__));
+$app->run();
diff --git a/core/tests/Drupal/Tests/Core/RunTests/RunTestsCommandTest.php b/core/tests/Drupal/Tests/Core/RunTests/RunTestsCommandTest.php
new file mode 100644
index 0000000..056559c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/RunTests/RunTestsCommandTest.php
@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Session\AnonymousUserSessionTest.
+ */
+
+namespace Drupal\Tests\Core\RunTests;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\Core\RunTests\RunTestsCommand;
+use Drupal\Core\RunTests\DrupalRunTestsException;
+use Drupal\Core\RunTests\Config;
+
+/**
+ * @coversDefaultClass \Drupal\Core\RunTests\RunTestsCommand
+ * @group RunTests
+ */
+class RunTestsCommandTest extends UnitTestCase {
+
+  /**
+   * @return array
+   *   - Array of classes and their methods expected to be found, or array of
+   *     NULL if an exception is expected.
+   *   - Array of class names to look for.
+   */
+  public function provideGetTestsForClassNames() {
+    return [
+      [[RunTestsCommandTest::class . '::foo'], [RunTestsCommandTest::class . '::foo']],
+      [[NULL], [RunTestsCommandTest::class . '::foo', '\I\do\not\exist::imaginary']],
+      [[NULL], ['\this\class\does\not\exist::atAll']],
+    ];
+  }
+
+
+  /**
+   * @covers ::getTestsForClassNames
+   * @dataProvider provideGetTestsForClassNames
+   */
+  public function testGetTestsForClassNames($expected, $class_list) {
+    // If $expected is NULL, then we expect an exception.
+    if ($expected === [NULL]) {
+      $this->setExpectedException(DrupalRunTestsException::class);
+    }
+
+    $mock_command = $this->getMockBuilder(RunTestsCommand::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['simpletest_script_print_error', 'simpletest_script_print_alternatives', 'getAllTestClasses'])
+      ->getMock();
+    // These methods are only called when there's an exception.
+    $method_calls = ($expected === [NULL]) ? 1 : 0;
+    $mock_command->expects($this->exactly($method_calls))
+      ->method('simpletest_script_print_error');
+    $mock_command->expects($this->exactly($method_calls))
+      ->method('simpletest_script_print_alternatives');
+    $mock_command->expects($this->exactly($method_calls))
+      ->method('getAllTestClasses')
+      ->willReturn([]);
+
+    $getTestsForClassNames = new \ReflectionMethod($mock_command, 'getTestsForClassNames');
+    $getTestsForClassNames->setAccessible(TRUE);
+
+    $this->assertArrayEquals(
+      $expected,
+      $getTestsForClassNames->invokeArgs($mock_command, [$class_list])
+    );
+  }
+
+  /**
+   * Helper method to get the file name for a class using reflection.
+   *
+   * @param type $class_name
+   *   Name of the class.
+   * @return string
+   *   Path to file containing class.
+   */
+  protected function getFileForClass($class_name) {
+    $ref = new \ReflectionClass($class_name);
+    return $ref->getFileName();
+  }
+
+  /**
+   * @return array
+   *   - Expected array of class names.
+   *   - File contents to parse for class names.
+   */
+  public function provideGetClassesForFileContents() {
+    return [
+      [[], ''],
+      [[], 'no php here.'],
+      [[], '<?php'],
+      // Try this class.
+      [
+        [RunTestsCommandTest::class],
+        file_get_contents(__FILE__)
+      ],
+      // Contents of WebTestBase. It's abstract so the result is empty array.
+      [
+        [],
+        file_get_contents($this->getFileForClass(\Drupal\simpletest\WebTestBase::class))
+      ],
+      // Contents of \Drupal\simpletest\Tests\KernelTestBaseTest.
+      [
+        [\Drupal\simpletest\Tests\KernelTestBaseTest::class],
+        file_get_contents($this->getFileForClass(\Drupal\simpletest\Tests\KernelTestBaseTest::class)),
+      ],
+      // Minimum for the regex.
+      [
+        ['Foo\Bar'],
+        "namespace Foo;\nclass Bar",
+      ],
+      // Class only.
+      [
+        ['Bar'],
+        'class Bar',
+      ],
+      [
+        [],
+        'abstract class Bar',
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::getClassesForFileContents
+   * @dataProvider provideGetClassesForFileContents
+   */
+  public function testGetClassesForFileContents($expected, $contents) {
+    $mock_command = $this->getMockBuilder(RunTestsCommand::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $getClassesForFileContents = new \ReflectionMethod($mock_command, 'getClassesForFileContents');
+    $getClassesForFileContents->setAccessible(TRUE);
+
+    $this->assertArrayEquals(
+      $expected,
+      $getClassesForFileContents->invokeArgs($mock_command, [$contents])
+    );
+  }
+
+  /**
+   * @return array
+   *   - Expected command line string.
+   *   - Config array.
+   *   - Test ID as a string.
+   *   - Test class name string.
+   */
+  public function provideSimpletestScriptCommand() {
+    $php_binary = PHP_BINARY;
+    return [
+      [
+        "'$php_binary' './core/scripts/' --url '' --php '$php_binary' --test-id 23 --execute-test 'test\class'",
+        [],
+        '23',
+        'test\class',
+      ],
+      [
+        "'$php_binary' './core/scripts/test_script' --url 'http:://awesome' --sqlite 'test.sqlite' --dburl 'yoursql://admin@password:foo' --php '$php_binary' --test-id 24 --execute-test 'test\class'",
+        [
+          'script' => 'test_script',
+          'url' => 'http:://awesome',
+          'sqlite' => 'test.sqlite',
+          'dburl' => 'yoursql://admin@password:foo',
+        ],
+        '24',
+        'test\class',
+      ],
+      [
+        "'test/php/path' './core/scripts/' --url '' --php 'test/php/path' --test-id 25 --execute-test 'test\class'",
+        [
+          'php' => 'test/php/path',
+        ],
+        '25',
+        'test\class',
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::simpletest_script_command
+   * @dataProvider provideSimpletestScriptCommand
+   */
+  public function testSimpletestScriptCommand($expected, $config_array, $test_id, $test_class) {
+    $mock_command = $this->getMockBuilder(RunTestsCommand::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $simpletestScriptCommand = new \ReflectionMethod($mock_command, 'simpletest_script_command');
+    $simpletestScriptCommand->setAccessible(TRUE);
+
+    $ref_config = new \ReflectionProperty($mock_command, 'config');
+    $ref_config->setAccessible(TRUE);
+    $ref_config->setValue($mock_command, Config::createFromArray($config_array));
+
+    $this->assertEquals(
+      $expected,
+      $simpletestScriptCommand->invokeArgs($mock_command, [$test_id, $test_class])
+    );
+  }
+
+  /**
+   * @return array
+   *   - Expected array of test classes.
+   *   - Deep array of available test classes, keyed by group, to mock output
+   *     of simpletest_test_get_all().
+   */
+  public function provideGetAllTestClasses() {
+    return [
+      [
+        ['test\class'],
+        [
+          'group' => [
+            'test\class' => [
+            ],
+          ],
+        ],
+      ],
+      [
+        [],
+        [],
+      ],
+    ];
+  }
+
+
+  /**
+   * @covers ::getAllTestClasses
+   * @dataProvider provideGetAllTestClasses
+   */
+  public function testGetAllTestClasses($expected, $test_get_all) {
+    $mock_command = $this->getMockBuilder(RunTestsCommand::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['simpletest_test_get_all'])
+      ->getMock();
+    $mock_command->expects($this->once())
+      ->method('simpletest_test_get_all')
+      ->willReturn($test_get_all);
+
+    $method = new \ReflectionMethod($mock_command, 'getAllTestClasses');
+    $method->setAccessible(TRUE);
+
+    $this->assertArrayEquals($expected, $method->invoke($mock_command));
+  }
+
+  public function provideGetAllTestClassesException() {
+    return [
+      [new \ReflectionException()],
+      [new \Exception()],
+    ];
+  }
+
+  /**
+   * @covers ::getAllTestClasses
+   * @dataProvider provideGetAllTestClassesException
+   */
+  public function testGetAllTestClassesException($exception) {
+    // getAllTestClasses() should return an empty array when any exception is
+    // thrown internally. simpletest_test_get_all() can throw
+    // \ReflectionException, and ::getAllTestClasses() should be able to handle
+    // generic \Exception.
+    $mock_command = $this->getMockBuilder(RunTestsCommand::class)
+      ->disableOriginalConstructor()
+      ->setMethods(['simpletest_test_get_all'])
+      ->getMock();
+    $mock_command->expects($this->once())
+      ->method('simpletest_test_get_all')
+      ->will($this->throwException($exception));
+
+    $method = new \ReflectionMethod($mock_command, 'getAllTestClasses');
+    $method->setAccessible(TRUE);
+
+    $this->assertArrayEquals([], $method->invoke($mock_command));
+  }
+
+}
