diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index c4b0cb4..1e51356 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -269,15 +269,15 @@ function timer_stop($name) {
  * @see default.settings.php
  */
 function conf_path($require_settings = TRUE, $reset = FALSE) {
-  $conf_path = &drupal_static(__FUNCTION__, '');
+  static $conf_path;
 
-  if ($conf_path && !$reset) {
+  if (isset($conf_path) && !$reset) {
     return $conf_path;
   }
 
   // Check for a simpletest override.
-  if ($simpletest_conf_path = _drupal_simpletest_conf_path()) {
-    $conf_path = $simpletest_conf_path;
+  if ($test_prefix = drupal_valid_test_ua()) {
+    $conf_path = 'sites/simpletest/' . substr($test_prefix, 10);
     return $conf_path;
   }
 
@@ -292,50 +292,6 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
 }
 
 /**
- * Determines whether to use an overridden value for conf_path().
- *
- * Simpletest may provide a secondary, test-specific settings.php file to load
- * after the primary one used by the parent site and override its variables.
- * - If the child settings.php does not override $conf_path, then this function
- * returns FALSE and conf_path() returns the directory of the primary
- * settings.php.
- * - If the child settings.php does override $conf_path, then
- * _drupal_load_test_overrides() sets the 'simpletest_conf_path' setting, and
- * this function returns that to conf_path(), causing installations and
- * upgrades to act on that one.
- *
- * @return string|false
- *   The overridden $conf_path, or FALSE if the $conf_path should not currently
- *   be overridden.
- *
- * @see conf_path()
- * @see _drupal_load_test_overrides()
- */
-function _drupal_simpletest_conf_path() {
-  // Ensure that the settings object is available. conf_path() is called once
-  // before the Settings class is included, and at that point it should still
-  // load the primary $conf_path. See drupal_settings_initialize().
-  if (!class_exists('Drupal\Component\Utility\Settings', FALSE)) {
-    return FALSE;
-  }
-
-  // If no $simpletest_conf_path is set, use the normal $conf_path.
-  if (!($simpletest_conf_path = settings()->get('simpletest_conf_path'))) {
-    return FALSE;
-  }
-
-  // Ensure that this is actually a simpletest request. We can't check this
-  // before settings.php is loaded.
-  if (!drupal_valid_test_ua()) {
-    return FALSE;
-  }
-
-  // When the $simpletest_conf_path is set in a valid test request,
-  // return that path.
-  return $simpletest_conf_path;
-}
-
-/**
  * Finds the appropriate configuration directory for a given host and path.
  *
  * Finds a matching configuration directory file by stripping the website's
@@ -563,20 +519,33 @@ function drupal_valid_http_host($host) {
  * Sets the base URL, cookie domain, and session name from configuration.
  */
 function drupal_settings_initialize() {
-  global $base_url, $base_path, $base_root, $script_path;
-
   // Export these settings.php variables to the global namespace.
-  global $databases, $cookie_domain, $conf, $db_prefix, $drupal_hash_salt, $base_secure_url, $base_insecure_url, $config_directories;
+  global $base_url, $databases, $cookie_domain, $conf, $drupal_hash_salt, $config_directories;
   $conf = array();
+  $settings = array();
 
   // Make conf_path() available as local variable in settings.php.
   $conf_path = conf_path();
   if (is_readable(DRUPAL_ROOT . '/' . $conf_path . '/settings.php')) {
-    include_once DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
+    require DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
   }
-  require_once __DIR__ . '../../lib/Drupal/Component/Utility/Settings.php';
 
-  new Settings(isset($settings) ? $settings : array());
+  new Settings($settings);
+}
+
+/**
+ * Initializes global request variables.
+ *
+ * @todo D8: Eliminate this entirely in favor of Request object.
+ */
+function _drupal_request_initialize() {
+  // Provided by settings.php.
+  // @see drupal_settings_initialize()
+  global $base_url, $cookie_domain;
+  // Set and derived from $base_url by this function.
+  global $base_path, $base_root, $script_path;
+  global $base_secure_url, $base_insecure_url;
+
   $is_https = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on';
 
   if (isset($base_url)) {
@@ -1952,11 +1921,27 @@ function _drupal_exception_handler($exception) {
  */
 function _drupal_bootstrap_configuration() {
   drupal_environment_initialize();
+
+  // Indicate that code is operating in a test child site.
+  if ($test_prefix = drupal_valid_test_ua()) {
+    // Only code that interfaces directly with tests should rely on this
+    // constant; e.g., the error/exception handler conditionally adds further
+    // error information into HTTP response headers that are consumed by
+    // Simpletest's internal browser.
+    define('DRUPAL_TEST_IN_CHILD_SITE', TRUE);
+
+    // Log fatal errors to the test site directory.
+    ini_set('log_errors', 1);
+    ini_set('error_log', DRUPAL_ROOT . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log');
+  }
+  else {
+    // Ensure that no other code defines this.
+    define('DRUPAL_TEST_IN_CHILD_SITE', FALSE);
+  }
+
   // Initialize the configuration, including variables from settings.php.
   drupal_settings_initialize();
-
-  // Make sure we are using the test database prefix in child Drupal sites.
-  _drupal_initialize_db_test_prefix();
+  _drupal_request_initialize();
 
   // Activate the class loader.
   drupal_classloader();
@@ -2046,39 +2031,6 @@ function _drupal_bootstrap_page_cache() {
 }
 
 /**
- * In a test environment, get the test db prefix and set it in $databases.
- */
-function _drupal_initialize_db_test_prefix() {
-  // The user agent header is used to pass a database prefix in the request when
-  // running tests. However, for security reasons, it is imperative that we
-  // validate we ourselves made the request.
-  if ($test_prefix = drupal_valid_test_ua()) {
-    // Set the test run id for use in other parts of Drupal.
-    $test_info = &$GLOBALS['drupal_test_info'];
-    $test_info['test_run_id'] = $test_prefix;
-    $test_info['in_child_site'] = TRUE;
-
-    foreach ($GLOBALS['databases']['default'] as &$value) {
-      // Extract the current default database prefix.
-      if (!isset($value['prefix'])) {
-        $current_prefix = '';
-      }
-      elseif (is_array($value['prefix'])) {
-        $current_prefix = $value['prefix']['default'];
-      }
-      else {
-        $current_prefix = $value['prefix'];
-      }
-
-      // Remove the current database prefix and replace it by our own.
-      $value['prefix'] = array(
-        'default' => $current_prefix . $test_prefix,
-      );
-    }
-  }
-}
-
-/**
  * Loads system variables and all enabled bootstrap modules.
  */
 function _drupal_bootstrap_variables() {
@@ -2202,10 +2154,9 @@ function module_hook($module, $hook) {
  * Returns the test prefix if this is an internal request from SimpleTest.
  *
  * @param string $new_prefix
- *   Internal use only. A new prefix to be stored. Passed in by tests that use
- *   the test runner from within a test.
+ *   Internal use only. A new prefix to be stored.
  *
- * @return
+ * @return string|FALSE
  *   Either the simpletest prefix (the string "simpletest" followed by any
  *   number of digits) or FALSE if the user agent does not contain a valid
  *   HMAC and timestamp.
@@ -2219,80 +2170,81 @@ function drupal_valid_test_ua($new_prefix = NULL) {
   if (isset($test_prefix)) {
     return $test_prefix;
   }
+  // Unless the below User-Agent and HMAC validation succeeds, we are not in
+  // a test environment.
+  $test_prefix = FALSE;
 
+  // Perform a basic check on the User-Agent HTTP request header first. Any
+  // inbound request that uses the simpletest UA header needs to be validated.
   if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^(simpletest\d+);(.+);(.+);(.+)$/", $_SERVER['HTTP_USER_AGENT'], $matches)) {
     list(, $prefix, $time, $salt, $hmac) = $matches;
     $check_string =  $prefix . ';' . $time . ';' . $salt;
-    // We use the salt from settings.php to make the HMAC key, since
-    // the database is not yet initialized and we can't access any Drupal variables.
+    // Read the hash salt prepared by drupal_generate_test_ua().
+    // This function is called before settings.php is read and Drupal's error
+    // handlers are set up. While Drupal's error handling may be properly
+    // configured on production sites, the server's PHP error_reporting may not.
+    // Ensure that no information leaks on production sites.
+    $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
+    if (!is_readable($key_file)) {
+      header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
+      exit;
+    }
+    $private_key = file_get_contents($key_file);
     // The file properties add more entropy not easily accessible to others.
-    $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
+    $key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
     $time_diff = REQUEST_TIME - $time;
-    // We can't use Crypt::hmacBase64() yet because this can be called in very
-    // early bootstrap when autoloader has not been initialized yet.
-    $test_hmac = base64_encode(hash_hmac('sha256', $check_string, $key, TRUE));
-    $test_hmac = strtr($test_hmac, array('+' => '-', '/' => '_', '=' => ''));
+    $test_hmac = Crypt::hmacBase64($check_string, $key);
     // Since we are making a local request a 5 second time window is allowed,
     // and the HMAC must match.
-    if ($time_diff >= 0 && $time_diff <= 5 && $hmac == $test_hmac) {
+    if ($time_diff >= 0 && $time_diff <= 5 && $hmac === $test_hmac) {
       $test_prefix = $prefix;
-      _drupal_load_test_overrides($test_prefix);
-      return $test_prefix;
     }
   }
-
-  $test_prefix = FALSE;
   return $test_prefix;
 }
 
 /**
- * Overrides low-level and environment-specific configuration.
- *
- * Very strictly for internal use only.
- *
- * Loads settings.php from the simpletest public files directory. These files
- * can change the global $conf, the global $config_directories, the return
- * value of conf_path(), and settings().
- *
- * @param string $test_prefix
- *   The simpletest prefix.
- */
-function _drupal_load_test_overrides($test_prefix) {
-  global $conf, $config_directories;
-
-  // Do not use the parent site's config directories. Use only the child site's.
-  // @see \Drupal\simpletest\TestBase::prepareConfigDirectories()
-  $path_prefix = 'simpletest/' . substr($test_prefix, 10);
-  $config_directories = array();
-  foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) {
-    $config_directories[$type] = conf_path() . '/files/' . $path_prefix . '/config_' . $type;
-  }
-
-  // Check for and load a settings.php file in the simpletest files directory.
-  $filename = conf_path() . '/files/' . $path_prefix . '/settings.php';
-  if (file_exists($filename)) {
-    $settings = settings()->getAll();
-    $conf_path = &drupal_static('conf_path');
-    // This can override $conf, $conf_path, $settings, and $config_directories.
-    include $filename;
-    // Keep the overriden $conf_path alive across drupal_static_reset() calls.
-    // @see conf_path()
-    $settings['simpletest_conf_path'] = $conf_path;
-    new Settings($settings);
-  }
-}
-
-/**
  * Generates a user agent string with a HMAC and timestamp for simpletest.
  */
 function drupal_generate_test_ua($prefix) {
-  static $key;
-
-  if (!isset($key)) {
-    // We use the salt from settings.php to make the HMAC key, since
-    // the database is not yet initialized and we can't access any Drupal variables.
+  static $key, $last_prefix;
+
+  if (!isset($key) || $last_prefix != $prefix) {
+    $last_prefix = $prefix;
+    $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
+    // When issuing an outbound HTTP client request from within an inbound test
+    // request, then the outbound request has to use the same User-Agent header
+    // as the inbound request. A newly generated private key for the same test
+    // prefix would invalidate all subsequent inbound requests.
+    // @see \Drupal\Core\Http\Plugin\SimpletestHttpRequestSubscriber
+    if (DRUPAL_TEST_IN_CHILD_SITE && $parent_prefix = drupal_valid_test_ua()) {
+      if ($parent_prefix != $prefix) {
+        throw new \RuntimeException("Malformed User-Agent: Expected '$parent_prefix' but got '$prefix'.");
+      }
+      $private_key = file_get_contents($key_file);
+    }
+    else {
+      // Generate and save a new hash salt for a test run.
+      // Consumed by drupal_valid_test_ua() before settings.php is loaded.
+      $private_key = Crypt::randomStringHashed(55);
+      file_put_contents($key_file, $private_key);
+
+      // Write a custom .htaccess file to limit web access to the entire test
+      // site directory to the specific Simpletest User-Agent header for which
+      // we just created a private key. (Apache only; requires mod_setenvif)
+      $htaccess = "
+<IfModule mod_setenvif.c>
+  BrowserMatch ^$prefix ua_is_simpletest
+  Order Allow,Deny
+  Allow from ua_is_simpletest
+  Deny from All
+</IfModule>
+";
+      # @todo Temporarily disabled to focus on primary test failures first.
+      # file_put_contents(DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htaccess', $htaccess);
+    }
     // The file properties add more entropy not easily accessible to others.
-    $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
+    $key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
   }
   // Generate a moderately secure HMAC based on the database credentials.
   $salt = uniqid('', TRUE);
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 84e5edb..322d68d 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3060,14 +3060,6 @@ function _drupal_bootstrap_code() {
   // Make sure all stream wrappers are registered.
   file_get_stream_wrappers();
 
-  // Now that stream wrappers are registered, log fatal errors from a simpletest
-  // child site to a test specific file directory.
-  $test_info = &$GLOBALS['drupal_test_info'];
-  if (!empty($test_info['in_child_site'])) {
-    ini_set('log_errors', 1);
-    ini_set('error_log', 'public://error.log');
-  }
-
   // Set the allowed protocols once we have the config available.
   $allowed_protocols = \Drupal::config('system.filter')->get('protocols');
   if (!isset($allowed_protocols)) {
diff --git a/core/includes/errors.inc b/core/includes/errors.inc
index 4f5c923..1dc59a3 100644
--- a/core/includes/errors.inc
+++ b/core/includes/errors.inc
@@ -132,8 +132,7 @@ function _drupal_log_error($error, $fatal = FALSE) {
 
   // When running inside the testing framework, we relay the errors
   // to the tested site by the way of HTTP headers.
-  $test_info = &$GLOBALS['drupal_test_info'];
-  if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+  if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
     // $number does not use drupal_static as it should not be reset
     // as it uniquely identifies each PHP error.
     static $number = 0;
diff --git a/core/includes/file.inc b/core/includes/file.inc
index 545fca3..bd3d15d 100644
--- a/core/includes/file.inc
+++ b/core/includes/file.inc
@@ -575,12 +575,12 @@ function file_ensure_htaccess() {
  */
 function file_save_htaccess($directory, $private = TRUE) {
   if (file_uri_scheme($directory)) {
-    $directory = file_stream_wrapper_uri_normalize($directory);
+    $htaccess_path = file_stream_wrapper_uri_normalize($directory . '/.htaccess');
   }
   else {
     $directory = rtrim($directory, '/\\');
+    $htaccess_path = $directory . '/.htaccess';
   }
-  $htaccess_path =  $directory . '/.htaccess';
 
   if (file_exists($htaccess_path)) {
     // Short circuit if the .htaccess file already exists.
@@ -1495,8 +1495,10 @@ function drupal_mkdir($uri, $mode = NULL, $recursive = FALSE, $context = NULL) {
   if (!isset($mode)) {
     // Configuration system stores default mode as strings.
     $mode = FALSE;
-    // During early update there's no container.
-    if (is_object(\Drupal::getContainer())) {
+    // During early update there is no container. When called from
+    // UnitTestBase::setUp(), the container has no config.factory service.
+    $container = \Drupal::getContainer();
+    if (is_object($container) && $container->has('config.factory')) {
       $mode = octdec(\Drupal::config('system.file')->get('chmod.directory'));
     }
     if (!$mode) {
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 5a46c8c..a9e520b 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -288,19 +288,18 @@ function install_begin_request(&$install_state) {
   // which will be used for installing Drupal.
   conf_path(FALSE);
 
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
-
   // If the hash salt leaks, it becomes possible to forge a valid testing user
-  // agent, install a new copy of Drupal, and take over the original site. To
-  // avoid this yet allow for automated testing of the installer, make sure
-  // there is also a special test-specific settings.php overriding conf_path().
-  // _drupal_load_test_overrides() sets the simpletest_conf_path in-memory
-  // setting in this case.
-  if ($install_state['interactive'] && drupal_valid_test_ua() && !settings()->get('simpletest_conf_path')) {
+  // agent, install a new copy of Drupal, and take over the original site.
+  // The user agent header is used to pass a database prefix in the request when
+  // running tests. However, for security reasons, it is imperative that no
+  // installation be permitted using such a prefix.
+  if ($install_state['interactive'] && strpos($request->server->get('HTTP_USER_AGENT'), 'simpletest') !== FALSE && !drupal_valid_test_ua()) {
     header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
     exit;
   }
 
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
+
   // Register the 'language_manager' service.
   $container->register('language_manager', 'Drupal\Core\Language\LanguageManager');
 
@@ -1068,7 +1067,6 @@ function install_verify_database_settings() {
   global $databases;
   if (!empty($databases)) {
     $database = $databases['default']['default'];
-    drupal_static_reset('conf_path');
     $settings_file = './' . conf_path(FALSE) . '/settings.php';
     $errors = install_database_errors($database, $settings_file);
     if (empty($errors)) {
@@ -1091,7 +1089,6 @@ function install_verify_database_settings() {
 function install_settings_form($form, &$form_state, &$install_state) {
   global $databases;
 
-  drupal_static_reset('conf_path');
   $conf_path = './' . conf_path(FALSE);
   $settings_file = $conf_path . '/settings.php';
   $database = isset($databases['default']['default']) ? $databases['default']['default'] : array();
@@ -1152,13 +1149,13 @@ function install_settings_form($form, &$form_state, &$install_state) {
  */
 function install_settings_form_validate($form, &$form_state) {
   $driver = $form_state['values']['driver'];
-  $database = $form_state['values'][$driver];
-  // When testing the interactive installer, copy the database password and
-  // the test prefix.
-  if ($test_prefix = drupal_valid_test_ua()) {
-    $database['prefix'] = $test_prefix;
-    $database['password'] = $GLOBALS['databases']['default']['default']['password'];
+
+  // @todo Remove when PIFR submits 'prefix' instead of 'db_prefix'.
+  if (!empty($form_state['input'][$driver]['db_prefix'])) {
+    $form_state['values'][$driver]['prefix'] = $form_state['input'][$driver]['db_prefix'];
   }
+  $database = $form_state['values'][$driver];
+
   $drivers = drupal_get_database_types();
   $reflection = new \ReflectionClass($drivers[$driver]);
   $install_namespace = $reflection->getNamespaceName();
@@ -1166,13 +1163,6 @@ function install_settings_form_validate($form, &$form_state) {
   $database['namespace'] = substr($install_namespace, 0, strrpos($install_namespace, '\\'));
   $database['driver'] = $driver;
 
-  // @todo PIFR uses 'db_prefix' instead of 'prefix'. Remove this when it gets
-  //   fixed.
-  if (!$test_prefix) {
-    $database['prefix'] = $database['db_prefix'];
-  }
-  unset($database['db_prefix']);
-
   $form_state['storage']['database'] = $database;
   $errors = install_database_errors($database, $form_state['values']['settings_file']);
   foreach ($errors as $name => $message) {
@@ -1230,35 +1220,15 @@ function install_settings_form_submit($form, &$form_state) {
   global $install_state, $conf;
 
   // Update global settings array and save.
-  $settings = array();
   $database = $form_state['storage']['database'];
-  // Ideally, there is no difference between the code executed by the
-  // automated test browser and an ordinary browser. However, the database
-  // settings need a different format and also need to skip the password
-  // when testing. The hash salt also needs to be skipped because the original
-  // salt is used to verify the validity of the automated test browser.
-  // Because of these, there's a little difference in the code following but
-  // it is small and self-contained.
-  if ($test_prefix = drupal_valid_test_ua()) {
-    foreach ($form_state['storage']['database'] as $k => $v) {
-      if ($k != 'password') {
-        $settings['databases']['default']['default'][$k] = (object) array(
-          'value'    => $v,
-          'required' => TRUE,
-        );
-      }
-    }
-  }
-  else {
-    $settings['databases']['default']['default'] = (object) array(
-      'value'    => $database,
-      'required' => TRUE,
-    );
-    $settings['drupal_hash_salt'] = (object) array(
-      'value'    => Crypt::randomStringHashed(55),
-      'required' => TRUE,
-    );
-  }
+  $settings['databases']['default']['default'] = (object) array(
+    'value'    => $database,
+    'required' => TRUE,
+  );
+  $settings['drupal_hash_salt'] = (object) array(
+    'value'    => Crypt::randomStringHashed(55),
+    'required' => TRUE,
+  );
 
   // Remember the profile which was used.
   $settings['settings'] = array(
@@ -2376,7 +2346,7 @@ function install_check_requirements($install_state) {
   if (!$install_state['settings_verified']) {
     $readable = FALSE;
     $writable = FALSE;
-    $conf_path = './' . conf_path(FALSE, TRUE);
+    $conf_path = './' . conf_path(FALSE);
     $settings_file = $conf_path . '/settings.php';
     $default_settings_file = './sites/default/default.settings.php';
     $file = $conf_path;
diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php
index 06a89a8..9455b90 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -270,8 +270,7 @@ public function on500Html(FlattenException $exception, Request $request) {
 
     // When running inside the testing framework, we relay the errors
     // to the tested site by the way of HTTP headers.
-    $test_info = &$GLOBALS['drupal_test_info'];
-    if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+    if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
       // $number does not use drupal_static as it should not be reset
       // as it uniquely identifies each PHP error.
       static $number = 0;
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
index 633edc6..97eab54 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
@@ -49,7 +49,7 @@ public function getFormOptions(array $database) {
     // Make the text more accurate for SQLite.
     $form['database']['#title'] = t('Database file');
     $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
-    $default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
+    $default_database = conf_path(FALSE) . '/files/.ht.sqlite';
     $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
     return $form;
   }
diff --git a/core/lib/Drupal/Core/Database/Install/Tasks.php b/core/lib/Drupal/Core/Database/Install/Tasks.php
index e80dd86..f950647 100644
--- a/core/lib/Drupal/Core/Database/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Install/Tasks.php
@@ -251,14 +251,14 @@ public function getFormOptions(array $database) {
       '#weight' => 10,
     );
 
-    $profile = drupal_get_profile();
-    $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
-    $form['advanced_options']['db_prefix'] = array(
+    $form['advanced_options']['prefix'] = array(
       '#type' => 'textfield',
       '#title' => t('Table name prefix'),
       '#default_value' => '',
       '#size' => 45,
-      '#description' => t('If more than one application will be sharing this database, a unique table name prefix–such as %prefix–will prevent collisions.', array('%prefix' => $db_prefix)),
+      '#description' => t('If more than one application will be sharing this database, a unique table name prefix–such as %prefix–will prevent collisions.', array(
+        '%prefix' => preg_replace('@[^a-z0-9]@', '_', strtolower(drupal_install_profile_distribution_name())) . '_',
+      )),
       '#weight' => 10,
     );
 
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 0160119..e7a18b9 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -334,20 +334,13 @@ public function updateModules(array $module_list, array $module_filenames = arra
   }
 
   /**
-   * Returns the classname based on environment and testing prefix.
+   * Returns the classname based on environment.
    *
    * @return string
    *   The class name.
    */
   protected function getClassName() {
     $parts = array('service_container', $this->environment);
-    // Make sure to use a testing-specific container even in the parent site.
-    if (!empty($GLOBALS['drupal_test_info']['test_run_id'])) {
-      $parts[] = $GLOBALS['drupal_test_info']['test_run_id'];
-    }
-    elseif ($prefix = drupal_valid_test_ua()) {
-      $parts[] = $prefix;
-    }
     return implode('_', $parts);
   }
 
diff --git a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
index 6f5bfd1..a8de4a8 100644
--- a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
+++ b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
@@ -34,9 +34,8 @@ public function onBeforeSendRequest(Event $event) {
     // user-agent is used to ensure that multiple testing sessions running at the
     // same time won't interfere with each other as they would if the database
     // prefix were stored statically in a file or database variable.
-    $test_info = &$GLOBALS['drupal_test_info'];
-    if (!empty($test_info['test_run_id'])) {
-      $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_info['test_run_id']));
+    if ($test_prefix = drupal_valid_test_ua()) {
+      $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_prefix));
     }
   }
 }
diff --git a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
index 90f17b3..760a1f8 100644
--- a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
+++ b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
@@ -38,13 +38,6 @@ public function getExternalUrl() {
    */
   public static function basePath() {
     $base_path = settings()->get('file_public_path', conf_path() . '/files');
-    if ($test_prefix = drupal_valid_test_ua()) {
-      // Append the testing suffix unless already given.
-      // @see \Drupal\simpletest\WebTestBase::setUp()
-      if (strpos($base_path, '/simpletest/' . substr($test_prefix, 10)) === FALSE) {
-        return $base_path . '/simpletest/' . substr($test_prefix, 10);
-      }
-    }
     return $base_path;
   }
 
diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php
index d302699..3575adc 100644
--- a/core/lib/Drupal/Core/SystemListingInfo.php
+++ b/core/lib/Drupal/Core/SystemListingInfo.php
@@ -28,6 +28,7 @@ protected function profiles($directory) {
     // For SimpleTest to be able to test modules packaged together with a
     // distribution we need to include the profile of the parent site (in
     // which test runs are triggered).
+    // @todo !drupal_installation_attempted() defeats the whole purpose?
     if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
       $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
       if ($testing_profile && $testing_profile != $profile) {
diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
index ff1c584..2461165 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
@@ -95,11 +95,12 @@ function testImportCreate() {
 
     // Add the new files to the staging directory.
     $src_dir = drupal_get_path('module', 'field_test_config') . '/staging';
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "public://config_staging/$field_config_name.yml"));
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "public://config_staging/$instance_config_name.yml"));
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "public://config_staging/$field_config_name_2.yml"));
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "public://config_staging/$instance_config_name_2a.yml"));
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "public://config_staging/$instance_config_name_2b.yml"));
+    $target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY];
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name.yml", "$target_dir/$field_config_name.yml"));
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name.yml", "$target_dir/$instance_config_name.yml"));
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$field_config_name_2.yml", "$target_dir/$field_config_name_2.yml"));
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2a.yml", "$target_dir/$instance_config_name_2a.yml"));
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$instance_config_name_2b.yml", "$target_dir/$instance_config_name_2b.yml"));
 
     // Import the content of the staging directory.
     $this->configImporter()->import();
diff --git a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php
index b427d3f..d230bc5 100644
--- a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php
+++ b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyReadOnlyStreamWrapper.php
@@ -16,7 +16,7 @@
  */
 class DummyReadOnlyStreamWrapper extends LocalReadOnlyStream {
   function getDirectoryPath() {
-    return 'sites/default/files';
+    return conf_path() . '/files';
   }
 
   /**
diff --git a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php
index 4836f09..cbea40f 100644
--- a/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php
+++ b/core/modules/file/tests/file_test/lib/Drupal/file_test/DummyStreamWrapper.php
@@ -16,7 +16,7 @@
  */
 class DummyStreamWrapper extends LocalStream {
   function getDirectoryPath() {
-    return 'sites/default/files';
+    return conf_path() . '/files';
   }
 
   /**
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
index 471dab2..f5d77e9 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
@@ -24,13 +24,6 @@ class LanguageNegotiationInfoTest extends WebTestBase {
   public static $modules = array('language');
 
   /**
-   * The language manager.
-   *
-   * @var \Drupal\language\ConfigurableLanguageManagerInterface
-   */
-  protected $languageManager;
-
-  /**
    * {@inheritdoc}
    */
   public static function getInfo() {
@@ -46,31 +39,62 @@ public static function getInfo() {
    */
   function setUp() {
     parent::setUp();
-    $this->languageManager = $this->container->get('language_manager');
     $admin_user = $this->drupalCreateUser(array('administer languages', 'access administration pages', 'view the administration theme'));
     $this->drupalLogin($admin_user);
     $this->drupalPostForm('admin/config/regional/language/add', array('predefined_langcode' => 'it'), t('Add language'));
   }
 
   /**
+   * Returns the configurable language manager.
+   *
+   * @return \Drupal\language\ConfigurableLanguageManager
+   */
+  protected function languageManager() {
+    return $this->container->get('language_manager');
+  }
+
+  /**
+   * Sets state flags for language_test module.
+   *
+   * Ensures to correctly update data both in the child site and the test runner
+   * environment.
+   *
+   * @param array $values
+   *   The key/value pairs to set in state.
+   */
+  protected function stateSet(array $values) {
+    // Set the new state values.
+    $this->container->get('state')->setMultiple($values);
+    // Refresh in-memory static state/config caches and static variables.
+    $this->refreshVariables();
+    // Clear language negotiation plugin definitions, in order to pick up
+    // conditional manipulations in alter hooks controlled by state flags.
+    $this->container->get('plugin.manager.language_negotiation_method')->clearCachedDefinitions();
+    // Reset state of LanguageManager (and implicitly LanguageNegotiator).
+    $this->languageManager()->reset();
+  }
+
+  /**
    * Tests alterations to language types/negotiation info.
    */
   function testInfoAlterations() {
-    // Enable language type/negotiation info alterations.
-    \Drupal::state()->set('language_test.language_types', TRUE);
-    \Drupal::state()->set('language_test.language_negotiation_info', TRUE);
-    $this->languageNegotiationUpdate();
+    $this->stateSet(array(
+      // Enable language_test type info.
+      'language_test.language_types' => TRUE,
+      // Enable language_test negotiation info (not altered yet).
+      'language_test.language_negotiation_info' => TRUE,
+      // Alter Language::TYPE_CONTENT to be configurable.
+      'language_test.content_language_type' => TRUE,
+    ));
+    $this->container->get('module_handler')->install(array('language_test'));
+    $this->rebuildContainer();
 
     // Check that fixed language types are properly configured without the need
     // of saving the language negotiation settings.
     $this->checkFixedLanguageTypes();
 
-    // Make the content language type configurable by updating the language
-    // negotiation settings with the proper flag enabled.
-    \Drupal::state()->set('language_test.content_language_type', TRUE);
-    $this->languageNegotiationUpdate();
     $type = Language::TYPE_CONTENT;
-    $language_types = $this->languageManager->getLanguageTypes();
+    $language_types = $this->languageManager()->getLanguageTypes();
     $this->assertTrue(in_array($type, $language_types), 'Content language type is configurable.');
 
     // Enable some core and custom language negotiation methods. The test
@@ -87,30 +111,40 @@ function testInfoAlterations() {
     );
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
-    // Remove the interface language negotiation method by updating the language
-    // negotiation settings with the proper flag enabled.
-    \Drupal::state()->set('language_test.language_negotiation_info_alter', TRUE);
-    $this->languageNegotiationUpdate();
-    $negotiation = variable_get("language_negotiation_$type", array());
-    $this->assertFalse(isset($negotiation[$interface_method_id]), 'Interface language negotiation method removed from the stored settings.');
-    $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language negotiation method unavailable.');
+    // Alter language negotiation info to remove interface language negotiation
+    // method.
+    $this->stateSet(array(
+      'language_test.language_negotiation_info_alter' => TRUE,
+    ));
+    $this->drupalGet('admin/config/regional/language/detection');
+    $this->assertNoFieldByName($form_field, NULL, 'Interface language negotiation method unavailable.');
+
+    // @todo Given the derived global $conf variables state tracking and
+    //   automated rewriting of variable values upon
+    //   LanguageNegotiator::purgeConfiguration(), it is impossible to
+    //   synchronize the global $conf state correctly between the child site and
+    //   the test runner (a refresh of global $conf variables would have to
+    //   happen right within the functional code). Re-enable this assertion when
+    //   converting language negotiation variables into configuration/state.
+    //$negotiation = variable_get("language_negotiation_$type", array());
+    //$this->assertFalse(isset($negotiation[$interface_method_id]), 'Interface language negotiation method removed from the stored settings.');
 
     // Check that type-specific language negotiation methods can be assigned
     // only to the corresponding language types.
-    foreach ($this->languageManager->getLanguageTypes() as $type) {
+    foreach ($this->languageManager()->getLanguageTypes() as $type) {
       $form_field = $type . '[enabled][test_language_negotiation_method_ts]';
       if ($type == $test_type) {
-        $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
+        $this->assertFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
       }
       else {
-        $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
+        $this->assertNoFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
       }
     }
 
     // Check language negotiation results.
     $this->drupalGet('');
     $last = \Drupal::state()->get('language_test.language_negotiation_last');
-    foreach ($this->languageManager->getDefinedLanguageTypes() as $type) {
+    foreach ($this->languageManager()->getDefinedLanguageTypes() as $type) {
       $langcode = $last[$type];
       $value = $type == Language::TYPE_CONTENT || strpos($type, 'test') !== FALSE ? 'it' : 'en';
       $this->assertEqual($langcode, $value, format_string('The negotiated language for %type is %language', array('%type' => $type, '%language' => $value)));
@@ -118,10 +152,11 @@ function testInfoAlterations() {
 
     // Uninstall language_test and check that everything is set back to the
     // original status.
-    $this->languageNegotiationUpdate('uninstall');
+    $this->container->get('module_handler')->uninstall(array('language_test'));
+    $this->rebuildContainer();
 
     // Check that only the core language types are available.
-    foreach ($this->languageManager->getDefinedLanguageTypes() as $type) {
+    foreach ($this->languageManager()->getDefinedLanguageTypes() as $type) {
       $this->assertTrue(strpos($type, 'test') === FALSE, format_string('The %type language is still available', array('%type' => $type)));
     }
 
@@ -140,38 +175,11 @@ function testInfoAlterations() {
   }
 
   /**
-   * Update language types/negotiation information.
-   *
-   * Manually invoke language_modules_installed()/language_modules_uninstalled()
-   * since they would not be invoked after installing/uninstalling language_test
-   * the first time.
-   */
-  protected function languageNegotiationUpdate($op = 'install') {
-    static $last_op = NULL;
-    $modules = array('language_test');
-
-    // Install/uninstall language_test only if we did not already before.
-    if ($last_op != $op) {
-      call_user_func(array($this->container->get('module_handler'), $op), $modules);
-      $last_op = $op;
-    }
-    else {
-      $function = "language_modules_{$op}ed";
-      if (function_exists($function)) {
-        $function($modules);
-      }
-    }
-
-    $this->languageManager->reset();
-    $this->drupalGet('admin/config/regional/language/detection');
-  }
-
-  /**
    * Check that language negotiation for fixed types matches the stored one.
    */
   protected function checkFixedLanguageTypes() {
-    $configurable = $this->languageManager->getLanguageTypes();
-    foreach ($this->languageManager->getDefinedLanguageTypesInfo() as $type => $info) {
+    $configurable = $this->languageManager()->getLanguageTypes();
+    foreach ($this->languageManager()->getDefinedLanguageTypesInfo() as $type => $info) {
       if (!in_array($type, $configurable) && isset($info['fixed'])) {
         $negotiation = variable_get("language_negotiation_$type", array());
         $equal = count($info['fixed']) == count($negotiation);
diff --git a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
index c6d4b7a..1d59ca1 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
@@ -71,7 +71,8 @@ public function testImportCreate() {
     $this->copyConfig($active, $staging);
     // Manually add new node type.
     $src_dir = drupal_get_path('module', 'node_test_config') . '/staging';
-    $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "public://config_staging/$node_type_config_name.yml"));
+    $target_dir = $this->configDirectories[CONFIG_STAGING_DIRECTORY];
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "$target_dir/$node_type_config_name.yml"));
 
     // Import the content of the staging directory.
     $this->configImporter()->import();
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index 021e1f9..3580996 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -139,13 +139,15 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
     }
 
     $response = $this->curlExec($curl_options);
-    $headers = $this->drupalGetHeaders();
-    $headers = implode("\n", $headers);
 
-    $this->verbose($method . ' request to: ' . $url .
-      '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
-      '<hr />Response headers: ' . $headers .
-      '<hr />Response body: ' . $response);
+    $verbose = $method . ' request to: ' . $url;
+    $verbose .= '<hr />Response code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
+    for ($i = 0; $i < $this->requestId; $i++) {
+      $verbose .= '<hr />Request headers: <pre>' . check_plain($this->requestHeaders[$i]) . '</pre>';
+      $verbose .= '<hr />Response headers: <pre>' . check_plain($this->responseHeaders[$i]) . '</pre>';
+    }
+    $verbose .= "\n<hr />\n" . $response;
+    $this->verbose($verbose);
 
     return $response;
   }
diff --git a/core/modules/simpletest/config/schema/simpletest.schema.yml b/core/modules/simpletest/config/schema/simpletest.schema.yml
index 5423adc..21fd498 100644
--- a/core/modules/simpletest/config/schema/simpletest.schema.yml
+++ b/core/modules/simpletest/config/schema/simpletest.schema.yml
@@ -4,6 +4,13 @@ simpletest.settings:
   type: mapping
   label: 'Testing'
   mapping:
+    clear:
+      type: mapping
+      label: 'Clean-up options'
+      mapping:
+        artifacts:
+          type: boolean
+          label: 'Clear test artifacts'
     clear_results:
       type: boolean
       label: 'Clear results after each complete test suite run'
diff --git a/core/modules/simpletest/config/simpletest.settings.yml b/core/modules/simpletest/config/simpletest.settings.yml
index 52b2d3d..b1ebceb 100644
--- a/core/modules/simpletest/config/simpletest.settings.yml
+++ b/core/modules/simpletest/config/simpletest.settings.yml
@@ -1,3 +1,6 @@
+clear:
+  artifacts: true
+# @todo Move into 'clear'.
 clear_results: '1'
 httpauth:
   method: '1'
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
index b7848de..a970bf6 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
@@ -57,6 +57,13 @@
   private $themeData;
 
   /**
+   * The configuration directories for this test run.
+   *
+   * @var array
+   */
+  protected $configDirectories = array();
+
+  /**
    * A KeyValueMemoryFactory instance to use when building the container.
    *
    * @var \Drupal\Core\KeyValueStore\KeyValueMemoryFactory.
@@ -72,22 +79,49 @@ function __construct($test_id = NULL) {
   }
 
   /**
-   * Sets up Drupal unit test environment.
-   *
-   * @see \DrupalUnitTestBase::$modules
-   * @see \DrupalUnitTestBase
+   * Overrides TestBase::beforePrepareEnvironment().
    */
-  protected function setUp() {
+  protected function beforePrepareEnvironment() {
     // Copy/prime extension file lists once to avoid filesystem scans.
     if (!isset($this->moduleFiles)) {
       $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: array();
       $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: array();
       $this->themeData = \Drupal::state()->get('system.theme.data') ?: array();
     }
+  }
 
+  /**
+   * Create and set new configuration directories.
+   *
+   * @see config_get_config_directory()
+   */
+  protected function prepareConfigDirectories() {
+    $this->configDirectories = array();
+    include_once DRUPAL_ROOT . '/core/includes/install.inc';
+    foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) {
+      // Assign the relative path to the global variable.
+      $path = $this->siteDirectory . '/config_' . $type;
+      $GLOBALS['config_directories'][$type] = $path;
+      // Ensure the directory can be created and is writeable.
+      if (!install_ensure_config_directory($type)) {
+        throw new \RuntimeException("Failed to create '$type' config directory $path");
+      }
+      // Provide the already resolved path for tests.
+      $this->configDirectories[$type] = $path;
+    }
+  }
+
+  /**
+   * Sets up Drupal unit test environment.
+   */
+  protected function setUp() {
     $this->keyValueFactory = new KeyValueMemoryFactory();
 
     parent::setUp();
+
+    // Create and set new configuration directories.
+    $this->prepareConfigDirectories();
+
     // Build a minimal, partially mocked environment for unit tests.
     $this->containerBuild(\Drupal::getContainer());
     // Make sure it survives kernel rebuilds.
@@ -133,7 +167,9 @@ protected function setUp() {
   }
 
   protected function tearDown() {
-    $this->kernel->shutdown();
+    if ($this->kernel instanceof DrupalKernel) {
+      $this->kernel->shutdown();
+    }
     parent::tearDown();
   }
 
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestSettingsForm.php b/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestSettingsForm.php
index 24bdb85..94ed19d 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestSettingsForm.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Form/SimpletestSettingsForm.php
@@ -30,18 +30,24 @@ public function buildForm(array $form, array &$form_state) {
       '#type' => 'details',
       '#title' => $this->t('General'),
     );
-    $form['general']['simpletest_clear_results'] = array(
-      '#type' => 'checkbox',
-      '#title' => $this->t('Clear results after each complete test suite run'),
-      '#description' => $this->t('By default SimpleTest will clear the results after they have been viewed on the results page, but in some cases it may be useful to leave the results in the database. The results can then be viewed at <em>admin/config/development/testing/results/[test_id]</em>. The test ID can be found in the database, simpletest table, or kept track of when viewing the results the first time. Additionally, some modules may provide more analysis or features that require this setting to be disabled.'),
-      '#default_value' => $config->get('clear_results'),
-    );
     $form['general']['simpletest_verbose'] = array(
       '#type' => 'checkbox',
       '#title' => $this->t('Provide verbose information when running tests'),
       '#description' => $this->t('The verbose data will be printed along with the standard assertions and is useful for debugging. The verbose data will be erased between each test suite run. The verbose data output is very detailed and should only be used when debugging.'),
       '#default_value' => $config->get('verbose'),
     );
+    $form['general']['simpletest_clear_artifacts'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Keep test artifacts'),
+      '#description' => $this->t('Retains the test site directory and database tables of all executed tests instead of deleting them to allow for advanced debugging.'),
+      '#default_value' => !$config->get('clear.artifacts'),
+    );
+    $form['general']['simpletest_clear_results'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Clear results after each complete test suite run'),
+      '#description' => $this->t('By default SimpleTest will clear the results after they have been viewed on the results page, but in some cases it may be useful to leave the results in the database. The results can then be viewed at <em>admin/config/development/testing/results/[test_id]</em>. The test ID can be found in the database, simpletest table, or kept track of when viewing the results the first time. Additionally, some modules may provide more analysis or features that require this setting to be disabled.'),
+      '#default_value' => $config->get('clear_results'),
+    );
 
     $form['httpauth'] = array(
       '#type' => 'details',
@@ -108,8 +114,9 @@ public function validateForm(array &$form, array &$form_state) {
    */
   public function submitForm(array &$form, array &$form_state) {
     $this->configFactory->get('simpletest.settings')
-      ->set('clear_results', $form_state['values']['simpletest_clear_results'])
       ->set('verbose', $form_state['values']['simpletest_verbose'])
+      ->set('clear.artifacts', !$form_state['values']['simpletest_clear_artifacts'])
+      ->set('clear_results', $form_state['values']['simpletest_clear_results'])
       ->set('httpauth.method', $form_state['values']['simpletest_httpauth_method'])
       ->set('httpauth.username', $form_state['values']['simpletest_httpauth_username'])
       ->set('httpauth.password', $form_state['values']['simpletest_httpauth_password'])
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 669c615..60a52bb 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -37,6 +37,13 @@
   protected $testId;
 
   /**
+   * The site directory of this test run.
+   *
+   * @var string
+   */
+  protected $siteDirectory = NULL;
+
+  /**
    * The database prefix of this test run.
    *
    * @var string
@@ -85,20 +92,11 @@
   protected $skipClasses = array(__CLASS__ => TRUE);
 
   /**
-   * Flag to indicate whether the test has been set up.
+   * Whether to delete all test artifacts after a test run.
    *
-   * The setUp() method isolates the test from the parent Drupal site by
-   * creating a random prefix for the database and setting up a clean file
-   * storage directory. The tearDown() method then cleans up this test
-   * environment. We must ensure that setUp() has been run. Otherwise,
-   * tearDown() will act on the parent Drupal site rather than the test
-   * environment, destroying live data.
+   * @var bool
    */
-  protected $setup = FALSE;
-
-  protected $setupDatabasePrefix = FALSE;
-
-  protected $setupEnvironment = FALSE;
+  public $clearArtifacts;
 
   /**
    * TRUE if verbose debugging is enabled.
@@ -168,6 +166,13 @@
   public $dieOnFail = FALSE;
 
   /**
+   * The DrupalKernel instance used in the test.
+   *
+   * @var \Drupal\Core\DrupalKernel
+   */
+  protected $kernel;
+
+  /**
    * The dependency injection container used in the test.
    *
    * @var \Symfony\Component\DependencyInjection\ContainerInterface
@@ -622,7 +627,20 @@ protected function assertIdenticalObject($object1, $object2, $message = '', $gro
     return $this->assertTrue($identical, $message, $group);
   }
 
-
+  /**
+   * Asserts that no errors have been logged to the PHP error.log thus far.
+   *
+   * @return bool
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   *
+   * @see TestBase::prepareEnvironment()
+   * @see _drupal_bootstrap_configuration()
+   */
+  protected function assertNoErrorsLogged() {
+    // Since PHP only creates the error.log file when an actual error is
+    // triggered, it is sufficient to check whether the file exists.
+    return $this->assertFalse(!file_exists(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'), 'PHP error.log is empty.');
+  }
 
   /**
    * Fire an assertion that is always positive.
@@ -738,6 +756,11 @@ public function run(array $methods = array()) {
     $simpletest_config = \Drupal::config('simpletest.settings');
 
     $class = get_class($this);
+
+    if (!isset($this->clearArtifacts)) {
+      $this->clearArtifacts = $simpletest_config->get('clear.artifacts');
+    }
+
     if ($simpletest_config->get('verbose')) {
       // Initialize verbose debugging.
       $this->verbose = TRUE;
@@ -790,19 +813,45 @@ public function run(array $methods = array()) {
             'function' => $class . '->' . $method . '()',
           );
           $completion_check_id = TestBase::insertAssert($this->testId, $class, FALSE, t('The test did not complete due to a fatal error.'), 'Completion check', $caller);
-          $this->setUp();
-          if ($this->setup) {
-            try {
-              $this->$method();
-              // Finish up.
-            }
-            catch (\Exception $e) {
-              $this->exceptionHandler($e);
-            }
+          try {
+            $this->prepareEnvironment();
+          }
+          catch (\Exception $e) {
+            $this->exceptionHandler($e);
+            // The prepareEnvironment() method isolates the test from the parent
+            // Drupal site by creating a random database prefix and test site
+            // directory. If this fails, a test would possibly operate in the
+            // parent site. Therefore, the entire test run for this test class
+            // has to be aborted.
+            break;
+          }
+          try {
+            $this->setUp();
+          }
+          catch (\Exception $e) {
+            $this->exceptionHandler($e);
+            // Abort if setUp() fails, since all test methods will fail.
+            // But ensure to clean up and restore the environment, since
+            // prepareEnvironment() succeeded.
+            $this->tearDown();
+            break;
+          }
+          try {
+            $this->$method();
+          }
+          catch (\Exception $e) {
+            $this->exceptionHandler($e);
+          }
+          try {
             $this->tearDown();
           }
-          else {
-            $this->fail(t("The test cannot be executed because it has not been set up properly."));
+          catch (\Exception $e) {
+            $this->exceptionHandler($e);
+            // If a test fails to tear down, abort the entire test class, since
+            // it is likely that all tests will fail in the same way and a
+            // failure here only results in additional test artifacts that have
+            // to be manually deleted.
+            break;
           }
           // Remove the completion check record.
           TestBase::deleteAssert($completion_check_id);
@@ -838,15 +887,20 @@ public function run(array $methods = array()) {
    * @see WebTestBase::setUp()
    */
   protected function prepareDatabasePrefix() {
-    $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000);
+    $suffix = mt_rand(1000, 1000000);
+    $this->siteDirectory = 'sites/simpletest/' . $suffix;
+    $this->databasePrefix = 'simpletest' . $suffix;
 
     // As soon as the database prefix is set, the test might start to execute.
     // All assertions as well as the SimpleTest batch operations are associated
     // with the testId, so the database prefix has to be associated with it.
-    db_update('simpletest_test_id')
+    $affected_rows = db_update('simpletest_test_id')
       ->fields(array('last_prefix' => $this->databasePrefix))
       ->condition('test_id', $this->testId)
       ->execute();
+    if (!$affected_rows) {
+      throw new \RuntimeException('Failed to set up database prefix.');
+    }
   }
 
   /**
@@ -857,34 +911,25 @@ protected function prepareDatabasePrefix() {
   protected function changeDatabasePrefix() {
     if (empty($this->databasePrefix)) {
       $this->prepareDatabasePrefix();
-      // If $this->prepareDatabasePrefix() failed to work, return without
-      // setting $this->setupDatabasePrefix to TRUE, so setUp() methods will
-      // know to bail out.
-      if (empty($this->databasePrefix)) {
-        return;
-      }
     }
 
     // Clone the current connection and replace the current prefix.
     $connection_info = Database::getConnectionInfo('default');
     Database::renameConnection('default', 'simpletest_original_default');
     foreach ($connection_info as $target => $value) {
-      $connection_info[$target]['prefix'] = array(
-        'default' => $value['prefix']['default'] . $this->databasePrefix,
-      );
+      $connection_info[$target]['prefix'] = $value['prefix']['default'] . $this->databasePrefix;
     }
     Database::addConnectionInfo('default', 'default', $connection_info['default']);
+  }
 
-    // Additionally override global $databases, since the installer does not use
-    // the Database connection info.
-    // @see install_verify_database_settings()
-    // @see install_database_errors()
-    // @todo Fix installer to use Database connection info.
-    global $databases;
-    $databases['default']['default'] = $connection_info['default'];
-
-    // Indicate the database prefix was set up correctly.
-    $this->setupDatabasePrefix = TRUE;
+  /**
+   * Act on global state information before the environment is altered for a test.
+   *
+   * Allows e.g. DrupalUnitTestBase to prime system/extension info from the
+   * parent site (and inject it into the test environment so as to improve
+   * performance).
+   */
+  protected function beforePrepareEnvironment() {
   }
 
   /**
@@ -898,16 +943,28 @@ protected function changeDatabasePrefix() {
    * filesystem and configuration directories.
    *
    * @see TestBase::tearDown()
+   *
+   * This method is private as it must only be called once by TestBase::run()
+   * (multiple invocations for the same test would have unpredictable
+   * consequences) and it must not be callable or overridable by test classes.
+   *
+   * @see TestBase::beforePrepareEnvironment()
    */
-  protected function prepareEnvironment() {
+  private function prepareEnvironment() {
     global $user, $conf;
+
+    // Allow (base) test classes to backup global state information.
+    $this->beforePrepareEnvironment();
+
+    // Create the database prefix for this test.
+    $this->prepareDatabasePrefix();
+
     $language_interface = language(Language::TYPE_INTERFACE);
 
     // When running the test runner within a test, back up the original database
-    // prefix and re-set the new/nested prefix in drupal_valid_test_ua().
-    if (drupal_valid_test_ua()) {
+    // prefix.
+    if (DRUPAL_TEST_IN_CHILD_SITE) {
       $this->originalPrefix = drupal_valid_test_ua();
-      drupal_valid_test_ua($this->databasePrefix);
     }
 
     // Backup current in-memory configuration.
@@ -947,21 +1004,15 @@ protected function prepareEnvironment() {
 
     // Create test directory ahead of installation so fatal errors and debug
     // information can be logged during installation process.
-    // Use temporary files directory with the same prefix as the database.
-    $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10);
-    $this->private_files_directory = $this->public_files_directory . '/private';
-    $this->temp_files_directory = $this->private_files_directory . '/temp';
-    $this->translation_files_directory = $this->public_files_directory . '/translations';
-
-    // Create the directories
-    file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-    file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY);
-    file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY);
-    file_prepare_directory($this->translation_files_directory, FILE_CREATE_DIRECTORY);
-    $this->generatedTestFiles = FALSE;
+    file_prepare_directory($this->siteDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+
+    // Prepare filesystem directory paths.
+    $this->public_files_directory = $this->siteDirectory . '/files';
+    $this->private_files_directory = $this->siteDirectory . '/private';
+    $this->temp_files_directory = $this->siteDirectory . '/temp';
+    $this->translation_files_directory = $this->siteDirectory . '/translations';
 
-    // Create and set new configuration directories.
-    $this->prepareConfigDirectories();
+    $this->generatedTestFiles = FALSE;
 
     // Reset statics before the old container is replaced so that objects with a
     // __destruct() method still have access to it.
@@ -986,46 +1037,29 @@ protected function prepareEnvironment() {
     \Drupal::setContainer($this->container);
 
     // Unset globals.
+    unset($GLOBALS['config_directories']);
     unset($GLOBALS['theme_key']);
     unset($GLOBALS['theme']);
 
     // Log fatal errors.
     ini_set('log_errors', 1);
-    ini_set('error_log', $this->public_files_directory . '/error.log');
+    ini_set('error_log', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
 
-    // Set the test information for use in other parts of Drupal.
-    $test_info = &$GLOBALS['drupal_test_info'];
-    $test_info['test_run_id'] = $this->databasePrefix;
-    $test_info['in_child_site'] = FALSE;
+    // Change the database prefix.
+    // All static variables need to be reset before the database prefix is
+    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
+    // write back to persistent caches when they are destructed.
+    $this->changeDatabasePrefix();
 
-    // Indicate the environment was set up correctly.
-    $this->setupEnvironment = TRUE;
-  }
+    // Reset all variables to perform tests in a clean environment.
+    $conf = array();
 
-  /**
-   * Create and set new configuration directories.
-   *
-   * The child site uses drupal_valid_test_ua() to adjust the config directory
-   * paths to a test-prefix-specific directory within the public files
-   * directory.
-   *
-   * @see config_get_config_directory()
-   */
-  protected function prepareConfigDirectories() {
-    $GLOBALS['config_directories'] = array();
-    $this->configDirectories = array();
-    include_once DRUPAL_ROOT . '/core/includes/install.inc';
-    foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $type) {
-      // Assign the relative path to the global variable.
-      $path = conf_path() . '/files/simpletest/' . substr($this->databasePrefix, 10) . '/config_' . $type;
-      $GLOBALS['config_directories'][$type] = $path;
-      // Ensure the directory can be created and is writeable.
-      if (!install_ensure_config_directory($type)) {
-        return FALSE;
-      }
-      // Provide the already resolved path for tests.
-      $this->configDirectories[$type] = $path;
-    }
+    // After preparing the environment and changing the database prefix, we are
+    // in a valid test environment.
+    drupal_valid_test_ua($this->databasePrefix);
+    conf_path(FALSE, TRUE);
+
+    drupal_set_time_limit($this->timeLimit);
   }
 
   /**
@@ -1044,11 +1078,11 @@ protected function prepareConfigDirectories() {
    *   tests can invoke this workaround when requiring services from newly
    *   enabled modules to be immediately available in the same request.
    */
-  protected function rebuildContainer() {
+  protected function rebuildContainer($environment = 'testing') {
     // Preserve the request object after the container rebuild.
     $request = \Drupal::request();
 
-    $this->kernel = new DrupalKernel('testing', drupal_classloader(), FALSE);
+    $this->kernel = new DrupalKernel($environment, drupal_classloader(), FALSE);
     $this->kernel->boot();
     // DrupalKernel replaces the container in \Drupal::getContainer() with a
     // different object, so we need to replace the instance on this test class.
@@ -1088,30 +1122,31 @@ protected function tearDown() {
       }
     }
 
-    // Ensure that TestBase::changeDatabasePrefix() has run and TestBase::$setup
-    // was not tricked into TRUE, since the following code would delete the
-    // entire parent site otherwise.
-    if ($this->setupDatabasePrefix) {
-      // Remove all prefixed tables.
-      $connection_info = Database::getConnectionInfo('default');
-      $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
-      $prefix_length = strlen($connection_info['default']['prefix']['default']);
-      foreach ($tables as $table) {
-        if (db_drop_table(substr($table, $prefix_length))) {
-          unset($tables[$table]);
-        }
-      }
-      if (!empty($tables)) {
-        $this->fail('Failed to drop all prefixed tables.');
-      }
-    }
-
     // In case a fatal error occurred that was not in the test process read the
     // log to pick up any fatal errors.
     simpletest_log_read($this->testId, $this->databasePrefix, get_class($this), TRUE);
 
-    // Delete temporary files directory.
-    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10), array($this, 'filePreDeleteCallback'));
+    // @todo Move into TestBase::run().
+    if ($this->clearArtifacts) {
+      // Remove all prefixed tables.
+      // @todo Connection prefix info is not normalized into an array.
+      $original_connection_info = Database::getConnectionInfo('simpletest_original_default');
+      $original_prefix = is_array($original_connection_info['default']['prefix']) ? $original_connection_info['default']['prefix']['default'] : $original_connection_info['default']['prefix'];
+      $test_connection_info = Database::getConnectionInfo('default');
+      $test_prefix = is_array($test_connection_info['default']['prefix']) ? $test_connection_info['default']['prefix']['default'] : $test_connection_info['default']['prefix'];
+      if ($original_prefix != $test_prefix) {
+        $tables = Database::getConnection()->schema()->findTables($test_prefix . '%');
+        $prefix_length = strlen($test_prefix);
+        foreach ($tables as $table) {
+          if (Database::getConnection()->schema()->dropTable(substr($table, $prefix_length))) {
+            unset($tables[$table]);
+          }
+        }
+      }
+
+      // Delete test site directory.
+      file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback'));
+    }
 
     // Restore original database connection.
     Database::removeConnection('default');
@@ -1139,9 +1174,14 @@ protected function tearDown() {
     // Restore original statics and globals.
     \Drupal::setContainer($this->originalContainer);
     $GLOBALS['config_directories'] = $this->originalConfigDirectories;
+
     if (isset($this->originalPrefix)) {
       drupal_valid_test_ua($this->originalPrefix);
     }
+    else {
+      drupal_valid_test_ua(FALSE);
+    }
+    conf_path(TRUE, TRUE);
 
     // Restore original shutdown callbacks.
     $callbacks = &drupal_register_shutdown_function();
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php
index 61d6401..24b4348 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php
@@ -39,7 +39,7 @@ function setUp() {
    * Overrides checkRequirements().
    */
   protected function checkRequirements() {
-    if (drupal_valid_test_ua()) {
+    if ($this->isInChildSite()) {
       return array(
         'Test is not allowed to run.'
       );
@@ -53,7 +53,7 @@ protected function checkRequirements() {
   protected function testCheckRequirements() {
     // If this is the main request, run the web test script and then assert
     // that the child tests did not run.
-    if (!drupal_valid_test_ua()) {
+    if (!$this->isInChildSite()) {
       // Run this test from web interface.
       $edit['Drupal\simpletest\Tests\MissingCheckedRequirementsTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
index 2a19a5c1..5f13902 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/SimpleTestTest.php
@@ -39,7 +39,7 @@ public static function getInfo() {
   }
 
   function setUp() {
-    if (!$this->inCURL()) {
+    if (!$this->isInChildSite()) {
       parent::setUp();
       // Create and log in an admin user.
       $this->drupalLogin($this->drupalCreateUser(array('administer unit tests')));
@@ -54,7 +54,7 @@ function setUp() {
    * Test the internal browsers functionality.
    */
   function testInternalBrowser() {
-    if (!$this->inCURL()) {
+    if (!$this->isInChildSite()) {
       // Retrieve the test page and check its title and headers.
       $this->drupalGet('test-page');
       $this->assertTrue($this->drupalGetHeader('Date'), 'An HTTP header was received.');
@@ -94,7 +94,7 @@ function testInternalBrowser() {
       // Remove the Simpletest settings.php so we can test the protection
       // against requests that forge a valid testing user agent to gain access
       // to the installer.
-      drupal_unlink($this->public_files_directory . '/settings.php');
+      drupal_unlink($this->siteDirectory . '/.htkey');
       global $base_url;
       $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE)));
       $this->assertResponse(403, 'Cannot access install.php.');
@@ -105,7 +105,7 @@ function testInternalBrowser() {
    * Test validation of the User-Agent header we use to perform test requests.
    */
   function testUserAgentValidation() {
-    if (!$this->inCURL()) {
+    if (!$this->isInChildSite()) {
       global $base_url;
       $system_path = $base_url . '/' . drupal_get_path('module', 'system');
       $HTTP_path = $system_path .'/tests/http.php?q=node';
@@ -147,7 +147,7 @@ function testWebTestRunner() {
     $this->valid_permission = 'access content';
     $this->invalid_permission = 'invalid permission';
 
-    if ($this->inCURL()) {
+    if ($this->isInChildSite()) {
       // Only run following code if this test is running itself through a CURL
       // request.
       $this->stubTest();
@@ -335,10 +335,4 @@ function asText(\SimpleXMLElement $element) {
     return trim(html_entity_decode(strip_tags($element->asXML())));
   }
 
-  /**
-   * Check if the test is being run from inside a CURL request.
-   */
-  function inCURL() {
-    return (bool) drupal_valid_test_ua();
-  }
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
index 5510aa4..643f652 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
@@ -20,11 +20,6 @@
 abstract class UnitTestBase extends TestBase {
 
   /**
-   * @var array
-   */
- protected $configDirectories;
-
-  /**
    * Constructor for UnitTestBase.
    */
   function __construct($test_id = NULL) {
@@ -41,32 +36,7 @@ function __construct($test_id = NULL) {
    * setUp() method.
    */
   protected function setUp() {
-    global $conf;
-
-    // Create the database prefix for this test.
-    $this->prepareDatabasePrefix();
-
-    // Prepare the environment for running tests.
-    $this->prepareEnvironment();
-    if (!$this->setupEnvironment) {
-      return FALSE;
-    }
-
-    // Reset all statics and variables to perform tests in a clean environment.
-    $conf = array();
-    drupal_static_reset();
-
+    file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
     $this->settingsSet('file_public_path', $this->public_files_directory);
-
-    // Change the database prefix.
-    // All static variables need to be reset before the database prefix is
-    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
-    // write back to persistent caches when they are destructed.
-    $this->changeDatabasePrefix();
-    if (!$this->setupDatabasePrefix) {
-      return FALSE;
-    }
-
-    $this->setup = TRUE;
   }
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 265012e..fbc0c60 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\Settings;
 use Drupal\Component\Utility\String;
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Database\Database;
@@ -47,7 +48,21 @@
   protected $curlHandle;
 
   /**
-   * The headers of the page currently loaded in the internal browser.
+   * The request headers of all requests performed.
+   *
+   * @var array
+   */
+  protected $requestHeaders;
+
+  /**
+   * The response headers of all requests performed.
+   *
+   * @var array
+   */
+  protected $responseHeaders;
+
+  /**
+   * The response headers of the page currently loaded in the internal browser.
    *
    * @var Array
    */
@@ -717,8 +732,6 @@ protected function drupalLogout() {
    * @see \Drupal\simpletest\WebTestBase::prepareEnvironment()
    */
   protected function setUp() {
-    global $conf;
-
     // When running tests through the Simpletest UI (vs. on the command line),
     // Simpletest's batch conflicts with the installer's batch. Batch API does
     // not support the concept of nested batches (in which the nested is not
@@ -726,33 +739,6 @@ protected function setUp() {
     // Backup the currently running Simpletest batch.
     $this->originalBatch = batch_get();
 
-    // Create the database prefix for this test.
-    $this->prepareDatabasePrefix();
-
-    // Prepare the environment for running tests.
-    $this->prepareEnvironment();
-    if (!$this->setupEnvironment) {
-      return FALSE;
-    }
-
-    // Reset all statics and variables to perform tests in a clean environment.
-    $conf = array();
-    drupal_static_reset();
-
-    // Change the database prefix.
-    // All static variables need to be reset before the database prefix is
-    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
-    // write back to persistent caches when they are destructed.
-    $this->changeDatabasePrefix();
-    if (!$this->setupDatabasePrefix) {
-      return FALSE;
-    }
-
-    // Set the 'simpletest_parent_profile' variable to add the parent profile's
-    // search path to the child site's search paths.
-    // @see drupal_system_listing()
-    $conf['simpletest_parent_profile'] = $this->originalProfile;
-
     // Define information about the user 1 account.
     $this->root_user = new UserSession(array(
       'uid' => 1,
@@ -764,56 +750,98 @@ protected function setUp() {
     // Reset the static batch to remove Simpletest's batch operations.
     $batch = &batch_get();
     $batch = array();
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' =>  $this->temp_files_directory,
-      ),
-      'locale.settings' =>  array(
-        'translation.path' => $this->translation_files_directory,
-      ),
+
+    // Get parameters for install_drupal() before removing global variables.
+    $parameters = $this->installParameters();
+
+    // Prepare installer settings that are not install_drupal() parameters.
+    // Copy and prepare an actual settings.php, so as to resemble a regular
+    // installation.
+    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
+
+    // All file system paths are set up by System module during installation.
+    // @see system_requirements()
+    // @see TestBase::prepareEnvironment()
+    $settings['settings']['file_public_path'] = (object) array(
+      'value' => $this->public_files_directory,
+      'required' => TRUE,
     );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
-    }
-    $this->settingsSet('file_public_path', $this->public_files_directory);
+    // @todo While private/temporary filesystem paths can be preset in the
+    //   installer via global configuration overrides, some tests expect the
+    //   values to be configurable through the UI. By setting them in
+    //   settings.php, they are no longer configurable... Why are these not
+    //   settings like the public filesystem path to begin with? (consistency)
+    /*
+    $settings['conf']['system.file']['path']['private'] = (object) array(
+      'value' => $this->private_files_directory,
+      'required' => TRUE,
+    );
+    $settings['conf']['system.file']['path']['temporary'] = (object) array(
+      'value' => $this->temp_files_directory,
+      'required' => TRUE,
+    );
+    */
+    // Use the test mail class instead of the default mail handler class.
+    // @todo Some mail system specific tests expect to be able to override the
+    //   mail implementation.
+    /*
+    $settings['conf']['system.mail']['interface']['default'] = (object) array(
+      'value' => 'Drupal\Core\Mail\TestMailCollector',
+      'required' => TRUE,
+    );
+    */
+    // Add the parent profile's search path to the child site's search paths.
+    // @see drupal_system_listing()
+    $settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
+      'value' => $this->originalProfile,
+      'required' => TRUE,
+    );
+    $this->writeSettings($settings);
+
+    // Since Drupal is bootstrapped already, install_begin_request() will not
+    // bootstrap into DRUPAL_BOOTSTRAP_CONFIGURATION (again). Hence, we have to
+    // reload the newly written custom settings.php manually.
+    drupal_settings_initialize();
+
     // Execute the non-interactive installer.
     require_once DRUPAL_ROOT . '/core/includes/install.core.inc';
-    $this->settingsSet('cache', array('default' => 'cache.backend.memory'));
-    $parameters = $this->installParameters();
     install_drupal($parameters);
 
-    // Set the install_profile so that web requests to the requests to the child
-    // site have the correct profile.
-    $settings = array(
-      'settings' => array(
-        'install_profile' => (object) array(
-          'value' => $this->profile,
-          'required' => TRUE,
-        ),
-      ),
-    );
-    $this->writeSettings($settings);
-    // Override install profile in Settings to so the correct profile is used by
-    // tests.
-    $this->settingsSet('install_profile', $this->profile);
+    // Import new settings.php written by the installer.
+    drupal_settings_initialize();
+    foreach ($GLOBALS['config_directories'] as $type => $path) {
+      $this->configDirectories[$type] = $path;
+    }
+
+    // After writing settings.php, the installer removes write permissions
+    // from the site directory. To allow drupal_generate_test_ua() to write
+    // a file containing the private key for drupal_valid_test_ua(), the site
+    // directory has to be writable.
+    // Use chmod() without a Drupal wrapper, so potential errors are visible.
+    // WebTestBase::tearDown() will delete the entire test site directory.
+    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
 
-    $this->settingsSet('cache', array());
     $this->rebuildContainer();
 
+    // Manually provide private and temporary files directories.
+    // (see @todo above)
+    file_prepare_directory($this->private_files_directory, FILE_CREATE_DIRECTORY);
+    file_prepare_directory($this->temp_files_directory, FILE_CREATE_DIRECTORY);
+    \Drupal::config('system.file')
+      ->set('path.private', $this->private_files_directory)
+      ->set('path.temporary', $this->temp_files_directory)
+      ->save();
+
+    // Manually override the default mail handler implementation.
+    // (see @todo above)
+    \Drupal::config('system.mail')
+      ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
+      ->save();
+
     // Restore the original Simpletest batch.
     $batch = &batch_get();
     $batch = $this->originalBatch;
 
-    // Set path variables.
-
-    // Set 'parent_profile' of simpletest to add the parent profile's
-    // search path to the child site's search paths.
-    // @see drupal_system_listing()
-    \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
-
     // Collect modules to install.
     $class = get_class($this);
     $modules = array();
@@ -830,30 +858,24 @@ protected function setUp() {
       $this->rebuildContainer();
     }
 
-    // Reset/rebuild all data structures after enabling the modules.
+    // Like DRUPAL_BOOTSTRAP_CONFIGURATION above, any further bootstrap phases
+    // are not re-executed by the installer, as Drupal is bootstrapped already.
+    // Reset/rebuild all data structures after enabling the modules, primarily
+    // to synchronize all data structures and caches between the test runner and
+    // the child site.
+    // Affects e.g. file_get_stream_wrappers().
+    // @see _drupal_bootstrap_code()
+    // @see _drupal_bootstrap_full()
+    // @todo Test-specific setUp() methods may set up further fixtures; find a
+    //   way to execute this after setUp() is done, or to eliminate it entirely.
     $this->resetAll();
 
-    // Now make sure that the file path configurations are saved. This is done
-    // after we install the modules to override default values.
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->save();
-    }
-
-    // Use the test mail class instead of the default mail handler class.
-    \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
-
-    drupal_set_time_limit($this->timeLimit);
     // Temporary fix so that when running from run-tests.sh we don't get an
     // empty current path which would indicate we're on the home page.
     $path = current_path();
     if (empty($path)) {
       _current_path('run-tests');
     }
-    $this->setup = TRUE;
   }
 
   /**
@@ -864,6 +886,11 @@ protected function setUp() {
    */
   protected function installParameters() {
     $connection_info = Database::getConnectionInfo();
+    $driver = $connection_info['default']['driver'];
+    unset($connection_info['default']['driver']);
+    unset($connection_info['default']['namespace']);
+    unset($connection_info['default']['pdo']);
+    unset($connection_info['default']['init_commands']);
     $parameters = array(
       'interactive' => FALSE,
       'parameters' => array(
@@ -871,7 +898,10 @@ protected function installParameters() {
         'langcode' => 'en',
       ),
       'forms' => array(
-        'install_settings_form' => $connection_info['default'],
+        'install_settings_form' => array(
+          'driver' => $driver,
+          $driver => $connection_info['default'],
+        ),
         'install_configure_form' => array(
           'site_name' => 'Drupal',
           'site_mail' => 'simpletest@example.com',
@@ -896,40 +926,28 @@ protected function installParameters() {
   }
 
   /**
-   * Writes a test-specific settings.php file for the child site.
-   *
-   * The child site loads this after the parent site's settings.php, so settings
-   * here override those.
+   * Rewrites the settings.php file of the test site.
    *
-   * @param $settings An array of settings to write out, in the format expected
-   *   by drupal_rewrite_settings().
+   * @param array $settings
+   *   An array of settings to write out, in the format expected by
+   *   drupal_rewrite_settings().
    *
-   * @see _drupal_load_test_overrides()
    * @see drupal_rewrite_settings()
    */
-  protected function writeSettings($settings) {
-    // drupal_rewrite_settings() sets the in-memory global variables in addition
-    // to writing the file. We'll want to restore the original globals.
-    foreach (array_keys($settings) as $variable_name) {
-      $original_globals[$variable_name] = isset($GLOBALS[$variable_name]) ? $GLOBALS[$variable_name] : NULL;
-    }
-
+  protected function writeSettings(array $settings) {
     include_once DRUPAL_ROOT . '/core/includes/install.inc';
-    $filename = $this->public_files_directory . '/settings.php';
-    file_put_contents($filename, "<?php\n");
+    $filename = $this->siteDirectory . '/settings.php';
+    // system_requirements() removes write permissions from settings.php
+    // whenever it is invoked.
+    chmod($filename, 0666);
     drupal_rewrite_settings($settings, $filename);
-
-    // Restore the original globals.
-    foreach ($original_globals as $variable_name => $value) {
-      $GLOBALS[$variable_name] = $value;
-    }
   }
 
   /**
-   * Sets custom translations to the settings object and queues them to writing.
+   * Queues custom translations to be written to settings.php.
    *
-   * In order for those custom translations to persist (being written in test
-   * site's settings.php) make sure to also call self::writeCustomTranslations()
+   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
+   * translations.
    *
    * @param string $langcode
    *   The langcode to add translations for.
@@ -942,32 +960,56 @@ protected function writeSettings($settings) {
    *     'Long month name' => array('March' => 'marzo'),
    *   );
    *   @endcode
+   *   Pass an empty array to remove all existing custom translations for the
+   *   given $langcode.
    */
   protected function addCustomTranslations($langcode, array $values) {
-    $this->settingsSet('locale_custom_strings_' . $langcode, $values);
-    foreach ($values as $key => $translations) {
-      foreach ($translations as $label => $value) {
-        $this->customTranslations['locale_custom_strings_' . $langcode][$key][$label] = (object) array(
-          'value' => $value,
-          'required' => TRUE,
-        );
+    // If $values is empty, then the test expects all custom translations to be
+    // cleared.
+    if (empty($values)) {
+      $this->customTranslations[$langcode] = array();
+    }
+    // Otherwise, $values are expected to be merged into previously passed
+    // values, while retaining keys that are not explicitly set.
+    else {
+      foreach ($values as $context => $translations) {
+        foreach ($translations as $original => $translation) {
+          $this->customTranslations[$langcode][$context][$original] = $translation;
+        }
       }
     }
   }
 
   /**
-   * Writes custom translations to test site's settings.php.
+   * Writes custom translations to the test site's settings.php.
+   *
+   * Use TestBase::addCustomTranslations() to queue custom translations before
+   * calling this method.
    */
   protected function writeCustomTranslations() {
-    $this->writeSettings(array('settings' => $this->customTranslations));
-    $this->customTranslations = array();
+    $settings = array();
+    foreach ($this->customTranslations as $langcode => $values) {
+      $settings_key = 'locale_custom_strings_' . $langcode;
+
+      // Update in-memory settings directly.
+      $this->settingsSet($settings_key, $values);
+
+      $settings['settings'][$settings_key] = (object) array(
+        'value' => $values,
+        'required' => TRUE,
+      );
+    }
+    // Only rewrite settings if there are any translation changes to write.
+    if (!empty($settings)) {
+      $this->writeSettings($settings);
+    }
   }
 
   /**
    * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
    */
-  protected function rebuildContainer() {
-    parent::rebuildContainer();
+  protected function rebuildContainer($environment = 'prod') {
+    parent::rebuildContainer($environment);
     // Make sure the url generator has a request object, otherwise calls to
     // $this->drupalGet() will fail.
     $this->prepareRequestForGenerator();
@@ -1005,9 +1047,13 @@ protected function resetAll() {
    * database to ensure that the most up-to-date set of variables is loaded.
    */
   protected function refreshVariables() {
-    global $conf;
     cache('bootstrap')->delete('variables');
-    $conf = variable_initialize();
+    // To retain in-memory global $conf from settings.php, we simply re-invoke
+    // DRUPAL_BOOTSTRAP_VARIABLES.
+    // @todo Should this reload $conf overrides from settings.php, so as to
+    //   eliminate any additional global $conf vars that may have been set?
+    _drupal_bootstrap_variables();
+
     // Clear the tag cache.
     drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
     \Drupal::service('config.factory')->reset();
@@ -1068,6 +1114,7 @@ protected function curlInitialize() {
         CURLOPT_SSL_VERIFYHOST => FALSE,
         CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'),
         CURLOPT_USERAGENT => $this->databasePrefix,
+        CURLINFO_HEADER_OUT => TRUE,
       );
       if (isset($this->httpauth_credentials)) {
         $curl_options[CURLOPT_HTTPAUTH] = $this->httpauth_method;
@@ -1180,12 +1227,25 @@ protected function curlExec($curl_options, $redirect = FALSE) {
       // Reset headers, the session ID and the redirect counter.
       $this->session_id = NULL;
       $this->headers = array();
+      $this->requestId = 0;
+      $this->requestHeaders = array();
+      $this->responseHeaders = array();
       $this->redirect_count = 0;
     }
+    $this->responseHeaders[$this->requestId] = array();
 
     $content = curl_exec($this->curlHandle);
     $status = curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE);
 
+    $this->requestHeaders[$this->requestId] = curl_getinfo($this->curlHandle, CURLINFO_HEADER_OUT);
+
+    // Sort response headers alphabetically, but keep the HTTP status first.
+    $response_headers = array_shift($this->responseHeaders[$this->requestId]);
+    sort($this->responseHeaders[$this->requestId]);
+    $this->responseHeaders[$this->requestId] = $response_headers . implode('', $this->responseHeaders[$this->requestId]);
+
+    $this->requestId++;
+
     // cURL incorrectly handles URLs with fragments, so instead of
     // letting cURL handle redirects we take of them ourselves to
     // to prevent fragments being sent to the web server as part
@@ -1230,9 +1290,11 @@ protected function curlHeaderCallback($curlHandler, $header) {
     if ($header[0] == ' ' || $header[0] == "\t") {
       // Normalize whitespace between chucks.
       $this->headers[] = array_pop($this->headers) . ' ' . trim($header);
+      $this->responseHeaders[$this->requestId][] = array_pop($this->responseHeaders[$this->requestId]) . ' ' . trim($header);
     }
     else {
       $this->headers[] = $header;
+      $this->responseHeaders[$this->requestId][] = $header;
     }
 
     // Errors are being sent via X-Drupal-Assertion-* headers,
@@ -1275,6 +1337,21 @@ protected function curlClose() {
   }
 
   /**
+   * Returns whether the test is being executed from within a test site.
+   *
+   * Mainly used by recursive tests (i.e. to test the testing framework).
+   *
+   * @return bool
+   *   TRUE if this test was instantiated in a request within the test site,
+   *   FALSE otherwise.
+   *
+   * @see _drupal_bootstrap_configuration()
+   */
+  protected function isInChildSite() {
+    return DRUPAL_TEST_IN_CHILD_SITE;
+  }
+
+  /**
    * Parse content returned from curlExec using DOM and SimpleXML.
    *
    * @return
@@ -1317,10 +1394,18 @@ protected function parse() {
   protected function drupalGet($path, array $options = array(), array $headers = array()) {
     $options['absolute'] = TRUE;
 
+    // The URL generator service is not necessarily available yet; e.g., in
+    // interactive installer tests.
+    if ($this->container->has('url_generator')) {
+      $url = $this->container->get('url_generator')->generateFromPath($path, $options);
+    }
+    else {
+      $url = $this->getAbsoluteUrl($path);
+    }
+
     // We re-using a CURL connection here. If that connection still has certain
     // options set, it might change the GET into a POST. Make sure we clear out
     // previous options.
-    $url = $this->container->get('url_generator')->generateFromPath($path, $options);
     $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $url, CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
     // Ensure that any changes to variables in the other thread are picked up.
     $this->refreshVariables();
@@ -1333,7 +1418,10 @@ protected function drupalGet($path, array $options = array(), array $headers = a
     $verbose = 'GET request to: ' . $path .
                '<hr />Ending URL: ' . $this->getUrl();
     if ($this->dumpHeaders) {
-      $verbose .= '<hr />Headers: <pre>' . check_plain(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>';
+      for ($i = 0; $i < $this->requestId; $i++) {
+        $verbose .= '<hr />Request headers: <pre>' . check_plain($this->requestHeaders[$i]) . '</pre>';
+        $verbose .= '<hr />Response headers: <pre>' . check_plain($this->responseHeaders[$i]) . '</pre>';
+      }
     }
     $verbose .= '<hr />' . $out;
 
@@ -1523,7 +1611,10 @@ protected function drupalPostForm($path, $edit, $submit, array $options = array(
           $verbose = 'POST request to: ' . $path;
           $verbose .= '<hr />Ending URL: ' . $this->getUrl();
           if ($this->dumpHeaders) {
-            $verbose .= '<hr />Headers: <pre>' . check_plain(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>';
+            for ($i = 0; $i < $this->requestId; $i++) {
+              $verbose .= '<hr />Request headers: <pre>' . check_plain($this->requestHeaders[$i]) . '</pre>';
+              $verbose .= '<hr />Response headers: <pre>' . check_plain($this->responseHeaders[$i]) . '</pre>';
+            }
           }
           $verbose .= '<hr />Fields: ' . highlight_string('<?php ' . var_export($post_array, TRUE), TRUE);
           $verbose .= '<hr />' . $out;
@@ -1840,6 +1931,28 @@ protected function serializePostValues($post = array()) {
   }
 
   /**
+   * Transforms a nested array into a flat array suitable for WebTestBase::drupalPostForm().
+   *
+   * @param array $values
+   *   A multi-dimensional form values array to convert.
+   *
+   * @return array
+   *   The flattened $edit array suitable for WebTestBase::drupalPostForm().
+   */
+  protected function translatePostValues(array $values) {
+    $edit = array();
+    // The easiest and most straightforward way to translate values suitable for
+    // WebTestBase::drupalPostForm() is to actually build the POST data string
+    // and convert the resulting key/value pairs back into a flat array.
+    $query = http_build_query($values);
+    foreach (explode('&', $query) as $item) {
+      list($key, $value) = explode('=', $item);
+      $edit[urldecode($key)] = urldecode($value);
+    }
+    return $edit;
+  }
+
+  /**
    * Runs cron in the Drupal installed by Simpletest.
    */
   protected function cronRun() {
@@ -1887,14 +2000,16 @@ protected function drupalHead($path, array $options = array(), array $headers =
     $options['absolute'] = TRUE;
     $url = $this->container->get('url_generator')->generateFromPath($path, $options);
     $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers));
-    // Ensure that any changes to variables in the other thread are picked up.
-    $this->refreshVariables();
 
+    $verbose = 'HEAD request to: ' . $path;
+    $verbose .= '<hr />Ending URL: ' . $this->getUrl();
     if ($this->dumpHeaders) {
-      $this->verbose('GET request to: ' . $path .
-                     '<hr />Ending URL: ' . $this->getUrl() .
-                     '<hr />Headers: <pre>' . check_plain(var_export(array_map('trim', $this->headers), TRUE)) . '</pre>');
+      for ($i = 0; $i < $this->requestId; $i++) {
+        $verbose .= '<hr />Request headers: <pre>' . check_plain($this->requestHeaders[$i]) . '</pre>';
+        $verbose .= '<hr />Response headers: <pre>' . check_plain($this->responseHeaders[$i]) . '</pre>';
+      }
     }
+    $this->verbose($verbose);
 
     return $out;
   }
diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php
index e11c651..dad9a39 100644
--- a/core/modules/simpletest/simpletest.api.php
+++ b/core/modules/simpletest/simpletest.api.php
@@ -6,18 +6,6 @@
  */
 
 /**
- * Global variable that holds information about the tests being run.
- *
- * An array, with the following keys:
- *  - 'test_run_id': the ID of the test being run, in the form 'simpletest_%"
- *  - 'in_child_site': TRUE if the current request is a cURL request from
- *     the parent site.
- *
- * @var array
- */
-global $drupal_test_info;
-
-/**
  * @addtogroup hooks
  * @{
  */
diff --git a/core/modules/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install
index 4b940ed..f9703d5 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -58,6 +58,20 @@ function simpletest_requirements($phase) {
     $requirements['php_memory_limit']['description'] = t('The testing framework requires the PHP memory limit to be at least %memory_minimum_limit. The current value is %memory_limit. <a href="@url">Follow these steps to continue</a>.', array('%memory_limit' => $memory_limit, '%memory_minimum_limit' => SIMPLETEST_MINIMUM_PHP_MEMORY_LIMIT, '@url' => 'http://drupal.org/node/207036'));
   }
 
+  if ($phase == 'runtime') {
+    $site_directory = 'sites/simpletest';
+    if (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $site_directory, FILE_EXIST|FILE_WRITABLE, 'dir')) {
+      $requirements['simpletest_site_directory'] = array(
+        'title' => t('Simpletest site directory'),
+        'value' => is_dir(DRUPAL_ROOT . '/' . $site_directory) ? t('Not writable') : t('Missing'),
+        'severity' => REQUIREMENT_ERROR,
+        'description' => t('The testing framework requires the !test-sites-directory to exist and be writable in order to run tests.', array(
+          '!test-sites-directory' => '<code>./' . $site_directory . '</code>',
+        )),
+      );
+    }
+  }
+
   return $requirements;
 }
 
@@ -154,6 +168,20 @@ function simpletest_schema() {
 }
 
 /**
+ * Implements hook_install().
+ */
+function simpletest_install() {
+  // Attempt to automatically create the sites directory for tests.
+  // @see simpletest_requirements()
+  $site_directory = 'sites/simpletest';
+  if (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $site_directory, FILE_EXIST|FILE_READABLE|FILE_WRITABLE|FILE_EXECUTABLE, 'dir')) {
+    drupal_set_message(t('The test sites directory !directory could not be created automatically. Create it manually and make it writable for the web server.', array(
+      '!directory' => '<code>./' . $site_directory . '</code>',
+    )), 'warning');
+  }
+}
+
+/**
  * Implements hook_uninstall().
  */
 function simpletest_uninstall() {
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index bd50676..f78c6b0 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -367,7 +367,7 @@ function simpletest_last_test_get($test_id) {
  *
  * @param $test_id
  *   The test ID to which the log relates.
- * @param $prefix
+ * @param $database_prefix
  *   The database prefix to which the log relates.
  * @param $test_class
  *   The test class to which the log relates.
@@ -377,8 +377,8 @@ function simpletest_last_test_get($test_id) {
  * @return
  *   Found any entries in log.
  */
-function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALSE) {
-  $log = 'public://' . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log';
+function simpletest_log_read($test_id, $database_prefix, $test_class, $during_test = FALSE) {
+  $log = DRUPAL_ROOT . '/sites/simpletest/' . substr($database_prefix, 10) . '/error.log';
   $found = FALSE;
   if (file_exists($log)) {
     foreach (file($log) as $line) {
@@ -632,11 +632,11 @@ function simpletest_clean_database() {
  */
 function simpletest_clean_temporary_directories() {
   $count = 0;
-  if (is_dir('public://simpletest')) {
-    $files = scandir('public://simpletest');
+  if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
+    $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
     foreach ($files as $file) {
-      $path = 'public://simpletest/' . $file;
-      if (is_dir($path) && (is_numeric($file) || strpos($file, 'config_simpletest') !== FALSE)) {
+      if ($file[0] != '.') {
+        $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
         file_unmanaged_delete_recursive($path, array('Drupal\simpletest\TestBase', 'filePreDeleteCallback'));
         $count++;
       }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
index d75db82..aef116d 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
@@ -53,6 +53,8 @@ function testSimpleAjaxFormValue() {
     }
 
     // Verify that AJAX elements with invalid callbacks return error code 500.
+    // Ensure the test error log is empty before these tests.
+    $this->assertNoErrorsLogged();
     foreach (array('null', 'empty', 'nonexistent') as $key) {
       $element_name = 'select_' . $key . '_callback';
       $edit = array(
@@ -61,5 +63,8 @@ function testSimpleAjaxFormValue() {
       $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
       $this->assertResponse(500);
     }
+    // Delete the test error log to prevent these expected exceptions from being
+    // interpreted as a test failure.
+    unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
   }
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php
index 2639f3b..ff97b9c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/DrupalKernel/DrupalKernelTest.php
@@ -10,12 +10,12 @@
 use Drupal\Core\DrupalKernel;
 use Drupal\Component\PhpStorage\MTimeProtectedFastFileStorage;
 use Drupal\Component\PhpStorage\FileReadOnlyStorage;
-use Drupal\simpletest\UnitTestBase;
+use Drupal\simpletest\DrupalUnitTestBase;
 
 /**
  * Tests compilation of the DIC.
  */
-class DrupalKernelTest extends UnitTestBase {
+class DrupalKernelTest extends DrupalUnitTestBase {
 
   public static function getInfo() {
     return array(
@@ -26,16 +26,12 @@ public static function getInfo() {
   }
 
   function setUp() {
-    parent::setUp();
-    global $conf;
-    $conf['php_storage']['service_container']= array(
-      'bin' => 'service_container',
-      'class' => 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage',
-      'directory' => DRUPAL_ROOT . '/' . $this->public_files_directory . '/php',
-      'secret' => drupal_get_hash_salt(),
-    );
-    // Use a non-persistent cache to avoid queries to non-existing tables.
-    $this->settingsSet('cache', array('default' => 'cache.backend.memory'));
+    // DrupalKernel relies on global $config_directories and requires those
+    // directories to exist. Therefore, create the directories, but do not
+    // invoke DrupalUnitTestBase::setUp(), since that would set up further
+    // environment aspects, which would distort this test, because it tests
+    // the DrupalKernel (re-)building itself.
+    $this->prepareConfigDirectories();
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/File/FileTestBase.php b/core/modules/system/lib/Drupal/system/Tests/File/FileTestBase.php
index 540c19a..70fa934 100644
--- a/core/modules/system/lib/Drupal/system/Tests/File/FileTestBase.php
+++ b/core/modules/system/lib/Drupal/system/Tests/File/FileTestBase.php
@@ -16,16 +16,6 @@
  */
 abstract class FileTestBase extends WebTestBase {
 
-  function setUp() {
-    parent::setUp();
-    // Make sure that custom stream wrappers are registered.
-    // @todo This has the potential to be a major bug deeply buried in File API;
-    //   file_unmanaged_*() API functions and test functions are invoking native
-    //   PHP functions directly, whereas Drupal's custom stream wrappers are not
-    //   registered yet.
-    file_get_stream_wrappers();
-  }
-
   /**
    * Check that two files have the same values for all fields other than the
    * timestamp.
diff --git a/core/modules/system/lib/Drupal/system/Tests/File/ReadOnlyStreamWrapperTest.php b/core/modules/system/lib/Drupal/system/Tests/File/ReadOnlyStreamWrapperTest.php
index ef84796..5f3ea59 100644
--- a/core/modules/system/lib/Drupal/system/Tests/File/ReadOnlyStreamWrapperTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/File/ReadOnlyStreamWrapperTest.php
@@ -30,14 +30,9 @@ public static function getInfo() {
     );
   }
 
-  function setUp() {
-    parent::setUp();
-    drupal_static_reset('file_get_stream_wrappers');
-  }
-
   function tearDown() {
-    parent::tearDown();
     stream_wrapper_unregister($this->scheme);
+    parent::tearDown();
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/File/StreamWrapperTest.php b/core/modules/system/lib/Drupal/system/Tests/File/StreamWrapperTest.php
index 16bc592..6ce6568 100644
--- a/core/modules/system/lib/Drupal/system/Tests/File/StreamWrapperTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/File/StreamWrapperTest.php
@@ -33,14 +33,9 @@ public static function getInfo() {
     );
   }
 
-  function setUp() {
-    parent::setUp();
-    drupal_static_reset('file_get_stream_wrappers');
-  }
-
   function tearDown() {
-    parent::tearDown();
     stream_wrapper_unregister($this->scheme);
+    parent::tearDown();
   }
 
   /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
index 69b3352..6c02193 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
@@ -7,7 +7,6 @@
 
 namespace Drupal\system\Tests\Installer;
 
-use Drupal\Component\Utility\NestedArray;
 use Drupal\system\Tests\InstallerTest;
 
 /**
@@ -15,6 +14,8 @@
  */
 class InstallerTranslationTest extends InstallerTest {
 
+  protected $langcode = 'de';
+
   public static function getInfo() {
     return array(
       'name' => 'Installer translation test',
@@ -23,125 +24,34 @@ public static function getInfo() {
     );
   }
 
-  protected function setUp() {
-    global $conf;
-
-    // When running tests through the SimpleTest UI (vs. on the command line),
-    // SimpleTest's batch conflicts with the installer's batch. Batch API does
-    // not support the concept of nested batches (in which the nested is not
-    // progressive), so we need to temporarily pretend there was no batch.
-    // Back up the currently running SimpleTest batch.
-    $this->originalBatch = batch_get();
-
-    // Add the translations directory so we can retrieve German translations.
-    $conf['locale.settings']['translation.path'] = drupal_get_path('module', 'simpletest') . '/files/translations';
-    $conf['language_default']['name'] = 'German';
-    $conf['language_default']['id'] = 'de';
-
-    // Create the database prefix for this test.
-    $this->prepareDatabasePrefix();
-
-    // Prepare the environment for running tests.
-    $this->prepareEnvironment();
-    if (!$this->setupEnvironment) {
-      return FALSE;
-    }
-
-    // Reset all statics and variables to perform tests in a clean environment.
-    $conf = array();
-    drupal_static_reset();
-
-    // Change the database prefix.
-    // All static variables need to be reset before the database prefix is
-    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
-    // write back to persistent caches when they are destructed.
-    $this->changeDatabasePrefix();
-    if (!$this->setupDatabasePrefix) {
-      return FALSE;
-    }
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' => $this->temp_files_directory,
-      ),
-      'locale.settings' => array(
-        'translation.path' => $this->translation_files_directory,
-      ),
-    );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
-    }
-    $settings['conf_path'] = (object) array(
-      'value' => $this->public_files_directory,
-      'required' => TRUE,
-    );
-    $settings['config_directories'] = (object) array(
-      'value' => array(),
-      'required' => TRUE,
-    );
-    $this->writeSettings($settings);
-
-    // Submit the installer with German language.
-    $this->drupalPostForm($GLOBALS['base_url'] . '/core/install.php', array('langcode' => 'de'), 'Save and continue');
-
-    // On the following page where installation profile is being selected the
-    // interface should be already translated, so there is no "Set up database"
-    // text anymore.
-    $this->assertNoText('Set up database', '"Set up database" string was not found.');
-
-    // After this assertion all we needed to test is tested, but the test
-    // expects the installation to succeed. If the test would finish here, an
-    // exception would occur. That is why the full installation has to be
-    // finished in the further steps.
-
-    // Get the "Save and continue" submit button translated value from the
-    // translated interface.
-    $submit_value = (string) current($this->xpath('//input[@type="submit"]/@value'));
-
-    // Submit the standard profile installation.
-    $this->drupalPostForm(NULL, array('profile' => 'standard'), $submit_value);
-
-    // Submit the next step.
-    $this->drupalPostForm(NULL, array(), $submit_value);
-
-    // Reload config directories.
-    include $this->public_files_directory . '/settings.php';
-    foreach ($config_directories as $type => $path) {
-      $GLOBALS['config_directories'][$type] = $path;
-    }
-    $this->rebuildContainer();
-
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->save();
-    }
-
-    // Submit site configuration form.
-    $this->drupalPostForm(NULL, array(
-      'site_mail' => 'admin@test.de',
-      'account[name]' => 'admin',
-      'account[mail]' => 'admin@test.de',
-      'account[pass][pass1]' => '123',
-      'account[pass][pass2]' => '123',
-      'site_default_country' => 'DE',
-    ), $submit_value);
-
-    // Use the test mail class instead of the default mail handler class.
-    \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
+  /**
+   * Overrides InstallerTest::setUpLanguage().
+   */
+  protected function setUpLanguage() {
+    parent::setUpLanguage();
+    // After selecting a different language than English, all following screens
+    // should be translated already.
+    // @todo Instead of actually downloading random translations that cannot be
+    //   asserted, write and supply a German translation file. Until then, take
+    //   over whichever string happens to be there, but ensure that the English
+    //   string no longer appears.
+    $elements = $this->xpath('//input[@type="submit"]/@value');
+    $string = (string) current($elements);
+    $this->assertNotEqual($string, 'Save and continue');
+    $this->translations['Save and continue'] = $string;
+  }
 
-    drupal_set_time_limit($this->timeLimit);
-    // When running from run-tests.sh we don't get an empty current path which
-    // would indicate we're on the home page.
-    $path = current_path();
-    if (empty($path)) {
-      _current_path('run-tests');
-    }
-    $this->setup = TRUE;
+  /**
+   * Overrides InstallerTest::setUpConfirm().
+   */
+  protected function setUpConfirm() {
+    // We don't know the translated link text of "Visit your new site", but
+    // luckily, there is only one link.
+    $elements = $this->xpath('//a');
+    $string = (string) current($elements);
+    $this->assertNotEqual($string, 'Visit your new site');
+    $this->translations['Visit your new site'] = $string;
+    parent::setUpConfirm();
   }
 
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
index 5bbaab8..9713438 100644
--- a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
@@ -8,13 +8,67 @@
 namespace Drupal\system\Tests;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Session\UserSession;
 use Drupal\simpletest\WebTestBase;
 
 /**
  * Allows testing of the interactive installer.
+ *
+ * @todo Move majority of code into new Drupal\simpletest\InstallerTestBase.
  */
 class InstallerTest extends WebTestBase {
 
+  /**
+   * Custom settings.php values to write for a test run.
+   *
+   * @var array
+   *   An array of settings to write out, in the format expected by
+   *   drupal_rewrite_settings().
+   */
+  protected $settings = array();
+
+  /**
+   * The language code in which to install Drupal.
+   *
+   * @var string
+   */
+  protected $langcode = 'en';
+
+  /**
+   * The installation profile to install.
+   *
+   * @var string
+   */
+  protected $profile = 'minimal';
+
+  /**
+   * Additional parameters to use for installer screens.
+   *
+   * @see WebTestBase::installParameters()
+   *
+   * @var array
+   */
+  protected $parameters = array();
+
+  /**
+   * A string translation map used for translated installer screens.
+   *
+   * Keys are English strings, values are translated strings.
+   *
+   * @var array
+   */
+  protected $translations = array(
+    'Save and continue' => 'Save and continue',
+    'Visit your new site' => 'Visit your new site',
+  );
+
+  /**
+   * Whether the installer has completed.
+   *
+   * @var bool
+   */
+  protected $isInstalled = FALSE;
+
   public static function getInfo() {
     return array(
       'name' => 'Installer tests',
@@ -24,132 +78,126 @@ public static function getInfo() {
   }
 
   protected function setUp() {
-    global $conf;
-
-    // When running tests through the SimpleTest UI (vs. on the command line),
-    // SimpleTest's batch conflicts with the installer's batch. Batch API does
-    // not support the concept of nested batches (in which the nested is not
-    // progressive), so we need to temporarily pretend there was no batch.
-    // Back up the currently running SimpleTest batch.
-    $this->originalBatch = batch_get();
-
-    // Create the database prefix for this test.
-    $this->prepareDatabasePrefix();
-
-    // Prepare the environment for running tests.
-    $this->prepareEnvironment();
-    if (!$this->setupEnvironment) {
-      return FALSE;
+    $this->isInstalled = FALSE;
+
+    // Define information about the user 1 account.
+    $this->root_user = new UserSession(array(
+      'uid' => 1,
+      'name' => 'admin',
+      'mail' => 'admin@example.com',
+      'pass_raw' => $this->randomName(),
+    ));
+
+    // If any $settings are defined for this test, copy and prepare an actual
+    // settings.php, so as to resemble a regular installation.
+    if (!empty($this->settings)) {
+      copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
+      $this->writeSettings($settings);
     }
 
-    // Reset all statics and variables to perform tests in a clean environment.
-    $conf = array();
-    drupal_static_reset();
-
-    // Change the database prefix.
-    // All static variables need to be reset before the database prefix is
-    // changed, since \Drupal\Core\Utility\CacheArray implementations attempt to
-    // write back to persistent caches when they are destructed.
-    $this->changeDatabasePrefix();
-    if (!$this->setupDatabasePrefix) {
-      return FALSE;
-    }
-    $variable_groups = array(
-      'system.file' => array(
-        'path.private' =>  $this->private_files_directory,
-        'path.temporary' =>  $this->temp_files_directory,
-      ),
-      'locale.settings' =>  array(
-        'translation.path' => $this->translation_files_directory,
-      ),
-    );
-    foreach ($variable_groups as $config_base => $variables) {
-      foreach ($variables as $name => $value) {
-        NestedArray::setValue($GLOBALS['conf'], array_merge(array($config_base), explode('.', $name)), $value);
-      }
-    }
-    $settings['conf_path'] = (object) array(
-      'value' => $this->public_files_directory,
-      'required' => TRUE,
-    );
-    $settings['config_directories'] = (object) array(
-      'value' => array(),
-      'required' => TRUE,
-    );
-    $this->writeSettings($settings);
-
-    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=en&profile=minimal');
-    $this->drupalPostForm(NULL, array(), 'Save and continue');
-    // Reload config directories.
-    include $this->public_files_directory . '/settings.php';
-    foreach ($config_directories as $type => $path) {
-      $GLOBALS['config_directories'][$type] = $path;
-    }
-    $this->rebuildContainer();
+    // Note that WebTestBase::installParameters() returns form input values
+    // suitable for a programmed drupal_form_submit().
+    // @see WebTestBase::translatePostValues()
+    $this->parameters = $this->installParameters();
+
+    $this->drupalGet($GLOBALS['base_url'] . '/core/install.php');
+
+    // Select language.
+    $this->setUpLanguage();
+
+    // Select profile.
+    $this->setUpProfile();
+
+    // Set up settings.
+    $this->setUpSettings();
+
+    // @todo Allow test classes based on this class to act on further installer
+    //   screens.
+
+    // Configure site.
+    $this->setUpSite();
+
+    // Confirm installation.
+    $this->setUpConfirm();
 
-    foreach ($variable_groups as $config_base => $variables) {
-      $config = \Drupal::config($config_base);
-      foreach ($variables as $name => $value) {
-        $config->set($name, $value);
-      }
-      $config->save();
+    // Import new settings.php written by the installer.
+    drupal_settings_initialize();
+    foreach ($GLOBALS['config_directories'] as $type => $path) {
+      $this->configDirectories[$type] = $path;
     }
 
-    // Use the test mail class instead of the default mail handler class.
-    \Drupal::config('system.mail')->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')->save();
+    // After writing settings.php, the installer removes write permissions
+    // from the site directory. To allow drupal_generate_test_ua() to write
+    // a file containing the private key for drupal_valid_test_ua(), the site
+    // directory has to be writable.
+    // Use chmod() without a Drupal wrapper, so potential errors are visible.
+    // WebTestBase::tearDown() will delete the entire test site directory.
+    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
+
+    $this->rebuildContainer();
+
+    \Drupal::config('system.mail')
+      ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
+      ->save();
 
-    drupal_set_time_limit($this->timeLimit);
     // When running from run-tests.sh we don't get an empty current path which
     // would indicate we're on the home page.
     $path = current_path();
     if (empty($path)) {
       _current_path('run-tests');
     }
-    $this->setup = TRUE;
+
+    $this->isInstalled = TRUE;
   }
 
-  /**
-   * {@inheritdoc}
-   *
-   * During setup(), drupalPost calls refreshVariables() which tries to read
-   * variables which are not yet there because the child Drupal is not yet
-   * installed.
-   */
-  protected function refreshVariables() {
-    if (!empty($this->setup)) {
-      parent::refreshVariables();
-    }
+  protected function setUpLanguage() {
+    $edit = array(
+      'langcode' => $this->langcode,
+    );
+    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
+  }
+
+  protected function setUpProfile() {
+    $edit = array(
+      'profile' => $this->profile,
+    );
+    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
+  }
+
+  protected function setUpSettings() {
+    $edit = $this->translatePostValues($this->parameters['forms']['install_settings_form']);
+    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
+  }
+
+  protected function setUpSite() {
+    $edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']);
+    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
+  }
+
+  protected function setUpConfirm() {
+    $this->clickLink($this->translations['Visit your new site']);
   }
 
   /**
    * {@inheritdoc}
    *
-   * This override is necessary because the parent drupalGet() calls t(), which
-   * is not available early during installation.
+   * WebTestBase::refreshVariables() tries to operate on persistent storage,
+   * which is only available after the installer completed.
    */
-  protected function drupalGet($path, array $options = array(), array $headers = array()) {
-    // We are re-using a CURL connection here. If that connection still has
-    // certain options set, it might change the GET into a POST. Make sure we
-    // clear out previous options.
-    $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $this->getAbsoluteUrl($path), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers));
-    $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up.
-
-    // Replace original page output with new output from redirected page(s).
-    if ($new = $this->checkForMetaRefresh()) {
-      $out = $new;
+  protected function refreshVariables() {
+    if ($this->isInstalled) {
+      parent::refreshVariables();
     }
-    $this->verbose('GET request to: ' . $path .
-                   '<hr />Ending URL: ' . $this->getUrl() .
-                   '<hr />' . $out);
-    return $out;
   }
 
   /**
    * Ensures that the user page is available after every test installation.
    */
   public function testInstaller() {
-    $this->drupalGet('user');
+    $this->assertUrl('user/1');
     $this->assertResponse(200);
+    // Confirm that we are logged-in after installation.
+    $this->assertText($this->root_user->getUsername());
   }
 
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
index 7d20be9..074ae3b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
@@ -29,6 +29,13 @@ public static function getInfo() {
     );
   }
 
+  protected function tearDown() {
+    // This test intentionally triggers errors and exceptions; prevent them from
+    // being interpreted as actual test failures.
+    unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
+    parent::tearDown();
+  }
+
   /**
    * Test the error handler.
    */
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
index 96b2f8d..930be2c 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
@@ -29,6 +29,13 @@ public static function getInfo() {
     );
   }
 
+  protected function tearDown() {
+    // This test intentionally throws an exception in a PHP shutdown function.
+    // Prevent it from being interpreted as an actual test failure.
+    unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
+    parent::tearDown();
+  }
+
   /**
    * Test shutdown functions.
    */
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 7d121af..8682edc 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -327,15 +327,14 @@ function system_requirements($phase) {
     }
     else {
       // If we are installing Drupal, the settings.php file might not exist yet
-      // in the intended conf_path() directory, so don't require it. The
-      // conf_path() cache must also be reset in this case.
-      $directories[] = conf_path(FALSE, TRUE) . '/files';
+      // in the intended site directory, so don't require it.
+      $directories[] = conf_path(FALSE) . '/files';
     }
-    if (!empty($conf['system.file']['path.private'])) {
-      $directories[] = $conf['system.file']['path.private'];
+    if (!empty($conf['system.file']['path']['private'])) {
+      $directories[] = $conf['system.file']['path']['private'];
     }
-    if (!empty($conf['system.file']['path.temporary'])) {
-      $directories[] = $conf['system.file']['path.temporary'];
+    if (!empty($conf['system.file']['path']['temporary'])) {
+      $directories[] = $conf['system.file']['path']['temporary'];
     }
     else {
       // If the temporary directory is not overridden use an appropriate
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php
index 408b243..f83a875 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayFeedTest.php
@@ -19,7 +19,7 @@ class DisplayFeedTest extends PluginTestBase {
    *
    * @var array
    */
-  public static $testViews = array('test_feed_display');
+  public static $testViews = array('test_display_feed');
 
   /**
    * Modules to enable.
@@ -52,7 +52,7 @@ public function testFeedUI() {
     $this->drupalGet('admin/structure/views');
 
     // Check the attach TO interface.
-    $this->drupalGet('admin/structure/views/nojs/display/test_feed_display/feed_1/displays');
+    $this->drupalGet('admin/structure/views/nojs/display/test_display_feed/feed_1/displays');
 
     // Load all the options of the checkbox.
     $result = $this->xpath('//div[@id="edit-displays"]/div');
@@ -68,13 +68,13 @@ public function testFeedUI() {
     $this->assertEqual($options, array('default', 'page'), 'Make sure all displays appears as expected.');
 
     // Post and save this and check the output.
-    $this->drupalPostForm('admin/structure/views/nojs/display/test_feed_display/feed_1/displays', array('displays[page]' => 'page'), t('Apply'));
-    $this->drupalGet('admin/structure/views/view/test_feed_display/edit/feed_1');
+    $this->drupalPostForm('admin/structure/views/nojs/display/test_display_feed/feed_1/displays', array('displays[page]' => 'page'), t('Apply'));
+    $this->drupalGet('admin/structure/views/view/test_display_feed/edit/feed_1');
     $this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', 'Page');
 
     // Add the default display, so there should now be multiple displays.
-    $this->drupalPostForm('admin/structure/views/nojs/display/test_feed_display/feed_1/displays', array('displays[default]' => 'default'), t('Apply'));
-    $this->drupalGet('admin/structure/views/view/test_feed_display/edit/feed_1');
+    $this->drupalPostForm('admin/structure/views/nojs/display/test_display_feed/feed_1/displays', array('displays[default]' => 'default'), t('Apply'));
+    $this->drupalGet('admin/structure/views/view/test_display_feed/edit/feed_1');
     $this->assertFieldByXpath('//*[@id="views-feed-1-displays"]', 'Multiple displays');
   }
 
@@ -92,14 +92,14 @@ public function testFeedOutput() {
     $result = $this->xpath('//title');
     $this->assertEqual($result[0], $site_name, 'The site title is used for the feed title.');
 
-    $view = $this->container->get('entity.manager')->getStorageController('view')->load('test_feed_display');
+    $view = $this->container->get('entity.manager')->getStorageController('view')->load('test_display_feed');
     $display = &$view->getDisplay('feed_1');
     $display['display_options']['sitename_title'] = 0;
     $view->save();
 
     $this->drupalGet('test-feed-display.xml');
     $result = $this->xpath('//title');
-    $this->assertEqual($result[0], 'test_feed_display', 'The display title is used for the feed title.');
+    $this->assertEqual($result[0], 'test_display_feed', 'The display title is used for the feed title.');
   }
 
 }
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_feed_display.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml
similarity index 89%
rename from core/modules/views/tests/modules/views_test_config/test_views/views.view.test_feed_display.yml
rename to core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml
index 1c82fa3..d4ece5d 100644
--- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_feed_display.yml
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml
@@ -1,7 +1,7 @@
 base_table: node
-core: 8.0-dev
+core: 8
 description: ''
-status: '1'
+status: true
 display:
   default:
     display_options:
@@ -44,7 +44,7 @@ display:
           provider: views
       pager:
         options:
-          items_per_page: '10'
+          items_per_page: 10
         type: full
       query:
         type: views_query
@@ -64,11 +64,11 @@ display:
           provider: views
       style:
         type: default
-      title: test_feed_display
+      title: test_display_feed
     display_plugin: default
     display_title: Master
     id: default
-    position: '0'
+    position: 0
   feed_1:
     display_options:
       displays: {  }
@@ -79,19 +79,19 @@ display:
         type: node_rss
       style:
         type: rss
-      sitename_title: '1'
+      sitename_title: true
     display_plugin: feed
     display_title: Feed
     id: feed_1
-    position: '0'
+    position: 0
   page:
     display_options:
       path: test-feed-display
     display_plugin: page
     display_title: Page
     id: page
-    position: '0'
-label: test_feed_display
+    position: 0
+label: test_display_feed
 module: views
-id: test_feed_display
+id: test_display_feed
 tag: default
diff --git a/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php b/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
index b45efe7..960dbc0 100644
--- a/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
+++ b/core/modules/views_ui/lib/Drupal/views_ui/ViewListController.php
@@ -258,6 +258,10 @@ protected function getDisplayPaths(EntityInterface $view) {
     $executable = $view->getExecutable();
     $executable->initDisplay();
     foreach ($executable->displayHandlers as $display) {
+      // DisplayBag may return NULL if a plugin was not found.
+      if (!isset($display)) {
+        continue;
+      }
       if ($display->hasPath()) {
         $path = $display->getPath();
         if ($view->status() && strpos($path, '%') === FALSE) {
diff --git a/core/rebuild.php b/core/rebuild.php
index 1b94601..b408c43 100644
--- a/core/rebuild.php
+++ b/core/rebuild.php
@@ -15,6 +15,7 @@
 // Change the directory to the Drupal root.
 chdir('..');
 
+require_once __DIR__ . '/vendor/autoload.php';
 require_once dirname(__DIR__) . '/core/includes/bootstrap.inc';
 require_once dirname(__DIR__) . '/core/includes/utility.inc';
 
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index e9f0ffe..904eff1 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -6,12 +6,14 @@
 
 require_once __DIR__ . '/../vendor/autoload.php';
 
-use Drupal\Core\StreamWrapper\PublicStream;
-
 const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
 const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
 const SIMPLETEST_SCRIPT_COLOR_EXCEPTION = 33; // Brown.
 
+// Set custom error handlers to cope with early environment errors.
+set_error_handler('simpletest_script_error_handler');
+set_exception_handler('simpletest_script_exception_handler');
+
 // Set defaults and get overrides.
 list($args, $count) = simpletest_script_parse_args();
 
@@ -34,6 +36,10 @@
 // Bootstrap to perform initial validation or other operations.
 drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
 
+// DRUPAL_BOOTSTRAP_CONFIGURATION sets new handlers, restore ours.
+restore_error_handler();
+restore_exception_handler();
+
 if (!\Drupal::moduleHandler()->moduleExists('simpletest')) {
   simpletest_script_print_error("The Testing (simpletest) module must be installed before this script can run.");
   exit;
@@ -50,9 +56,15 @@
   echo "\nEnvironment cleaned.\n";
 
   // Get the status messages and print them.
-  $messages = array_pop(drupal_get_messages('status'));
-  foreach ($messages as $text) {
-    echo " - " . $text . "\n";
+  $messages = drupal_get_messages();
+  if (isset($messages['error'])) {
+    foreach ($messages['error'] as $text) {
+      trigger_error($text, E_USER_ERROR);
+    }
+  }
+  foreach ($messages['status'] as $text) {
+    // Status messages are encoded for HTML already.
+    echo " - " . htmlspecialchars_decode($text, ENT_QUOTES) . "\n";
   }
   exit;
 }
@@ -153,6 +165,11 @@ function simpletest_script_help() {
 
   --verbose   Output detailed assertion messages in addition to summary.
 
+  --keep-artifacts
+
+              Retains the test site directory and database tables of all
+              executed tests. By default, all artifacts are deleted.
+
   --keep-results
 
               Keeps detailed assertion results (in the database) after tests
@@ -210,6 +227,7 @@ function simpletest_script_parse_args() {
     'file' => FALSE,
     'color' => FALSE,
     'verbose' => FALSE,
+    'keep-artifacts' => FALSE,
     'keep-results' => FALSE,
     'test_names' => array(),
     'repeat' => 1,
@@ -415,8 +433,7 @@ function simpletest_script_execute_batch($test_classes) {
           echo 'FATAL ' . $child['class'] . ': test runner returned a non-zero error code (' . $status['exitcode'] . ').' . "\n";
           if ($args['die-on-fail']) {
             list($db_prefix, ) = simpletest_last_test_get($child['test_id']);
-            $public_files = PublicStream::basePath();
-            $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
+            $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.
@@ -489,6 +506,11 @@ function simpletest_script_run_one_test($test_id, $test_class) {
   try {
     // Bootstrap Drupal.
     drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
+
+    // DRUPAL_BOOTSTRAP_CONFIGURATION sets new handlers, restore ours.
+    restore_error_handler();
+    restore_exception_handler();
+
     simpletest_classloader_register();
     // We have to add a Request.
     $request = \Symfony\Component\HttpFoundation\Request::createFromGlobals();
@@ -500,6 +522,7 @@ function simpletest_script_run_one_test($test_id, $test_class) {
     $conf['simpletest.settings']['clear_results'] = !$args['keep-results'];
 
     $test = new $test_class($test_id);
+    $test->clearArtifacts = !$args['keep-artifacts'];
     $test->dieOnFail = (bool) $args['die-on-fail'];
     $test->run();
     $info = $test->getInfo();
@@ -535,7 +558,7 @@ function simpletest_script_command($test_id, $test_class) {
   $command .= ' --url ' . escapeshellarg($args['url']);
   $command .= ' --php ' . escapeshellarg($php);
   $command .= " --test-id $test_id";
-  foreach (array('verbose', 'keep-results', 'color', 'die-on-fail') as $arg) {
+  foreach (array('verbose', 'keep-results', 'keep-artifacts', 'color', 'die-on-fail') as $arg) {
     if ($args[$arg]) {
       $command .= ' --' . $arg;
     }
@@ -585,10 +608,9 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
   // Read the log file in case any fatal errors caused the test to crash.
   simpletest_log_read($test_id, $db_prefix, $test_class);
 
-  // Check whether a test file directory was setup already.
-  // @see prepareEnvironment()
-  $public_files = PublicStream::basePath();
-  $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
+  // 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')) {
@@ -597,8 +619,7 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
         $messages[] = $errors;
       }
     }
-
-    // Delete the test files directory.
+    // 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.
@@ -864,6 +885,56 @@ function simpletest_script_format_result($result) {
 }
 
 /**
+ * Custom PHP error handler for early run-tests.sh execution.
+ *
+ * @param $error_level
+ *   The level of the error raised.
+ * @param $message
+ *   The error message.
+ * @param $filename
+ *   The filename that the error was raised in.
+ * @param $line
+ *   The line number the error was raised at.
+ * @param $context
+ *   An array that points to the active symbol table at the point the error
+ *   occurred.
+ */
+function simpletest_script_error_handler($error_level, $message, $filename, $line, $context) {
+  $map = array(
+    E_ERROR => 'Error',
+    E_WARNING => 'Warning',
+    E_PARSE => 'Parse error',
+    E_NOTICE => 'Notice',
+    E_CORE_ERROR => 'Core error',
+    E_CORE_WARNING => 'Core warning',
+    E_COMPILE_ERROR => 'Compile error',
+    E_COMPILE_WARNING => 'Compile warning',
+    E_USER_ERROR => 'User error',
+    E_USER_WARNING => 'User warning',
+    E_USER_NOTICE => 'User notice',
+    E_STRICT => 'Strict warning',
+    E_RECOVERABLE_ERROR => 'Recoverable fatal error',
+    E_DEPRECATED => 'Deprecated function',
+    E_USER_DEPRECATED => 'User deprecated function',
+  );
+  // Error messages are encoded for HTML already.
+  simpletest_script_print_error($map[$error_level] . ': ' . htmlspecialchars_decode($message, ENT_QUOTES));
+  echo "in $filename: $line\n";
+  debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+  echo "\n";
+}
+
+/**
+ * Custom PHP exception handler for early run-tests.sh execution.
+ *
+ * @param \Exception|int $exception
+ *   The exception object that was thrown.
+ */
+function simpletest_script_exception_handler($e) {
+  simpletest_script_print_error((string) $e);
+}
+
+/**
  * Print error message prefixed with "  ERROR: " and displayed in fail color
  * if color output is enabled.
  *
