diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index d92e27c6b1bb1a0cfc8c9c3e12bfb3a9b15315c4..b2924892b02b9b6aca8b52d262cdedc61a4b0f87 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -240,15 +240,15 @@
  * @see default.settings.php
  */
 function conf_path($require_settings = TRUE, $reset = FALSE) {
-  static $conf_path;
+  $conf_path = &drupal_static(__FUNCTION__, '');
 
-  if (isset($conf_path) && !$reset) {
+  if ($conf_path && !$reset) {
     return $conf_path;
   }
 
   // Check for a simpletest override.
-  if ($test_prefix = drupal_valid_test_ua()) {
-    $conf_path = 'sites/simpletest/' . substr($test_prefix, 10);
+  if ($simpletest_conf_path = _drupal_simpletest_conf_path()) {
+    $conf_path = $simpletest_conf_path;
     return $conf_path;
   }
 
@@ -262,6 +262,50 @@ function conf_path($require_settings = TRUE, $reset = FALSE) {
   return $conf_path;
 }
 
+/**
+ * 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.
  *
@@ -490,32 +534,20 @@ 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 $base_url, $databases, $cookie_domain, $drupal_hash_salt, $config_directories, $config;
+  global $databases, $cookie_domain, $db_prefix, $drupal_hash_salt, $base_secure_url, $base_insecure_url, $config_directories, $config;
   $settings = array();
   $config = array();
 
   // Make conf_path() available as local variable in settings.php.
   $conf_path = conf_path();
   if (is_readable(DRUPAL_ROOT . '/' . $conf_path . '/settings.php')) {
-    require DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
+    include_once DRUPAL_ROOT . '/' . $conf_path . '/settings.php';
   }
   // Initialize Settings.
   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';
 
@@ -1721,27 +1753,11 @@ 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();
-  _drupal_request_initialize();
+
+  // Make sure we are using the test database prefix in child Drupal sites.
+  _drupal_initialize_db_test_prefix();
 
   // Activate the class loader.
   drupal_classloader();
@@ -1829,6 +1845,39 @@ 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,
+      );
+    }
+  }
+}
+
 /**
  * Returns the current bootstrap phase for this Drupal process.
  *
@@ -1943,9 +1992,10 @@ 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.
+ *   Internal use only. A new prefix to be stored. Passed in by tests that use
+ *   the test runner from within a test.
  *
- * @return string|FALSE
+ * @return
  *   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.
@@ -1959,68 +2009,82 @@ 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;
-    // 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 = $private_key . filectime(__FILE__) . fileinode(__FILE__);
+    // We use the salt from settings.php to make the HMAC key, since
+    // the database is not yet initialized and we can't access the configuration
+    // system. The file properties add more entropy not easily accessible to
+    // others.
+    $key = drupal_get_hash_salt() . filectime(__FILE__) . fileinode(__FILE__);
     $time_diff = REQUEST_TIME - $time;
-    $test_hmac = Crypt::hmacBase64($check_string, $key);
+    // 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('+' => '-', '/' => '_', '=' => ''));
     // 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 $config_directories, the return value of conf_path(),
+ * settings(), and $config overrides.
+ *
+ * @param string $test_prefix
+ *   The simpletest prefix.
+ */
+function _drupal_load_test_overrides($test_prefix) {
+  global $config_directories, $config;
+
+  // 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 $config, $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, $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'.");
-      }
-      // If the file is not readable, a PHP warning is expected in this case.
-      $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);
-    }
-    // The file properties add more entropy not easily accessible to others.
-    $key = $private_key . filectime(__FILE__) . fileinode(__FILE__);
+  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 the configuration
+    // system. The file properties add more entropy not easily accessible to
+    // others.
+    $key = drupal_get_hash_salt() . 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 170e9411601220f8e0ed7f4fe0e5b45251adb7f4..a566bc527abe6ccaf969405ac1d6b7fa971d7ca9 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3071,6 +3071,14 @@ 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 208a6791af9bb3d566c139fc5d20e06765f39119..586effad13d35184bec17e6499eddd10cec3c7a3 100644
--- a/core/includes/errors.inc
+++ b/core/includes/errors.inc
@@ -141,7 +141,8 @@ 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.
-  if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+  $test_info = &$GLOBALS['drupal_test_info'];
+  if (!empty($test_info['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/install.core.inc b/core/includes/install.core.inc
index 9b3d7a0ba1d7d9d4db0e42229733a6f04590f7c2..0b25e590a1e198abde87110680d0966a2ad96fa0 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -276,18 +276,19 @@ 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.
-  // 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()) {
+  // 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')) {
     header($request->server->get('SERVER_PROTOCOL') . ' 403 Forbidden');
     exit;
   }
 
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION);
-
   // Ensure that procedural dependencies are loaded as early as possible,
   // since the error/exception handlers depend on them.
   require_once __DIR__ . '/../modules/system/system.install';
@@ -1079,6 +1080,7 @@ 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)) {
@@ -1101,6 +1103,7 @@ 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();
@@ -1162,6 +1165,12 @@ 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'];
+  }
   $drivers = drupal_get_database_types();
   $reflection = new \ReflectionClass($drivers[$driver]);
   $install_namespace = $reflection->getNamespaceName();
@@ -1228,14 +1237,33 @@ function install_settings_form_submit($form, &$form_state) {
   // Update global settings array and save.
   $settings = array();
   $database = $form_state['storage']['database'];
-  $settings['databases']['default']['default'] = (object) array(
-    'value'    => $database,
-    'required' => TRUE,
-  );
-  $settings['drupal_hash_salt'] = (object) array(
-    'value'    => Crypt::randomStringHashed(55),
-    'required' => TRUE,
-  );
+  // 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,
+    );
+  }
 
   // Remember the profile which was used.
   $settings['settings'] = array(
@@ -2353,7 +2381,7 @@ function install_check_requirements($install_state) {
   if (!$install_state['settings_verified']) {
     $readable = FALSE;
     $writable = FALSE;
-    $conf_path = './' . conf_path(FALSE);
+    $conf_path = './' . conf_path(FALSE, TRUE);
     $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 c8da4bc38fd243aa7e65deb51b312967718ad02f..e33a00a8cd4009eaa72037efc064dbc71566771d 100644
--- a/core/lib/Drupal/Core/Controller/ExceptionController.php
+++ b/core/lib/Drupal/Core/Controller/ExceptionController.php
@@ -292,7 +292,8 @@ 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.
-    if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+    $test_info = &$GLOBALS['drupal_test_info'];
+    if (!empty($test_info['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 97eab5441ac82358c60bb271b7f544086c2ee636..633edc6ca9ea5dbfe433730d98a993bbd8c33c5c 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) . '/files/.ht.sqlite';
+    $default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
     $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
     return $form;
   }
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index f9107febea68b59dddf20fed157c4d681ba03524..85fa7f817ee594907e4cc5b2f1f55766e19e1646 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -335,13 +335,20 @@ public function updateModules(array $module_list, array $module_filenames = arra
   }
 
   /**
-   * Returns the classname based on environment.
+   * Returns the classname based on environment and testing prefix.
    *
    * @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 a8de4a89d209c471fd888eb0e14a262cf2e295ca..6f5bfd17696bb37459af3e5a304332b04b88b993 100644
--- a/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
+++ b/core/lib/Drupal/Core/Http/Plugin/SimpletestHttpRequestSubscriber.php
@@ -34,8 +34,9 @@ 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.
-    if ($test_prefix = drupal_valid_test_ua()) {
-      $event['request']->setHeader('User-Agent', drupal_generate_test_ua($test_prefix));
+    $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']));
     }
   }
 }
diff --git a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
index 760a1f82e91f4a836d01e695808787d3c89b8fd8..90f17b33049b3ba6a1e12c320bae8057e250b3b3 100644
--- a/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
+++ b/core/lib/Drupal/Core/StreamWrapper/PublicStream.php
@@ -38,6 +38,13 @@ 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/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
index 2461165102fa9bd4f48258beba9e96975a406a65..ff1c5846f290f5a6eac69f3da938eeaa5f8d27d9 100644
--- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
+++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php
@@ -95,12 +95,11 @@ function testImportCreate() {
 
     // Add the new files to the staging directory.
     $src_dir = drupal_get_path('module', 'field_test_config') . '/staging';
-    $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"));
+    $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"));
 
     // 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 d230bc5afa2ab33ed74449936e4d8772fdaeb8bf..b427d3f33a7b27dbb62b5f6aa4ed86af3d6a7efd 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 conf_path() . '/files';
+    return 'sites/default/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 cbea40f039a53b317e6cd137608ffdfa87c3899e..4836f0955e04005e67e56728bdc1507b6fd60cdf 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 conf_path() . '/files';
+    return 'sites/default/files';
   }
 
   /**
diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
index 70d297c08f8af2b44a6f82d8774a1e0498da9dcf..58cbd722adabf2a9b90162ad939792658616fb96 100644
--- a/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
+++ b/core/modules/language/lib/Drupal/language/Tests/LanguageNegotiationInfoTest.php
@@ -23,6 +23,13 @@ class LanguageNegotiationInfoTest extends WebTestBase {
    */
   public static $modules = array('language');
 
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\language\ConfigurableLanguageManagerInterface
+   */
+  protected $languageManager;
+
   /**
    * {@inheritdoc}
    */
@@ -39,60 +46,31 @@ 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();
-    // Refresh/rewrite language negotiation configuration, in order to pick up
-    // the manipulations performed by language_test module's info alter hooks.
-    $this->container->get('language_negotiator')->purgeConfiguration();
-  }
-
   /**
    * Tests alterations to language types/negotiation info.
    */
   function testInfoAlterations() {
-    $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();
+    // 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();
 
     // 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
@@ -109,34 +87,30 @@ function testInfoAlterations() {
     );
     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
 
-    // Alter language negotiation info to remove interface language negotiation
-    // method.
-    $this->stateSet(array(
-      'language_test.language_negotiation_info_alter' => TRUE,
-    ));
-
-    $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled');
+    // 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 = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array();
     $this->assertFalse(isset($negotiation[$interface_method_id]), 'Interface language negotiation method removed from the stored settings.');
-
-    $this->drupalGet('admin/config/regional/language/detection');
-    $this->assertNoFieldByName($form_field, NULL, 'Interface language negotiation method unavailable.');
+    $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, 'Interface language negotiation method unavailable.');
 
     // 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->assertFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
+        $this->assertFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method available for %type.', array('%type' => $type)));
       }
       else {
-        $this->assertNoFieldByName($form_field, NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
+        $this->assertNoFieldByXPath("//input[@name=\"$form_field\"]", NULL, format_string('Type-specific test language negotiation method unavailable for %type.', array('%type' => $type)));
       }
     }
 
     // Check language negotiation results.
     $this->drupalGet('');
-    $last = $this->container->get('state')->get('language_test.language_negotiation_last');
-    foreach ($this->languageManager()->getDefinedLanguageTypes() as $type) {
+    $last = \Drupal::state()->get('language_test.language_negotiation_last');
+    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)));
@@ -144,11 +118,10 @@ function testInfoAlterations() {
 
     // Uninstall language_test and check that everything is set back to the
     // original status.
-    $this->container->get('module_handler')->uninstall(array('language_test'));
-    $this->rebuildContainer();
+    $this->languageNegotiationUpdate('uninstall');
 
     // 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)));
     }
 
@@ -158,7 +131,7 @@ function testInfoAlterations() {
 
     // Check that unavailable language negotiation methods are not present in
     // the negotiation settings.
-    $negotiation = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled');
+    $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array();
     $this->assertFalse(isset($negotiation[$test_method_id]), 'The disabled test language negotiation method is not part of the content language negotiation settings.');
 
     // Check that configuration page presents the correct options and settings.
@@ -166,14 +139,41 @@ function testInfoAlterations() {
     $this->assertNoRaw(t('This is a test language negotiation method'), 'No test language negotiation method available.');
   }
 
+  /**
+   * 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 = $this->container->get('config.factory')->get('language.types')->get('negotiation.' . $type . '.enabled');
+        $negotiation = \Drupal::config('language.types')->get('negotiation.' . $type . '.enabled') ?: array();
         $equal = count($info['fixed']) == count($negotiation);
         while ($equal && list($id) = each($negotiation)) {
           list(, $info_id) = each($info['fixed']);
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 1d59ca13d599aff1c3db2aa3ea2309cdbfe1f877..c6d4b7a0afdb6914e067f8249d8b25935a35c3e3 100644
--- a/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/Config/NodeImportCreateTest.php
@@ -71,8 +71,7 @@ public function testImportCreate() {
     $this->copyConfig($active, $staging);
     // Manually add new node type.
     $src_dir = drupal_get_path('module', 'node_test_config') . '/staging';
-    $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"));
+    $this->assertTrue(file_unmanaged_copy("$src_dir/$node_type_config_name.yml", "public://config_staging/$node_type_config_name.yml"));
 
     // Import the content of the staging directory.
     $this->configImporter()->import();
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
index 6ee194ce7f858896ce78ef6797b55a284df91953..92b161fe44a15582f2d9c62d157dbdc33661f25f 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php
@@ -57,13 +57,6 @@ abstract class DrupalUnitTestBase extends UnitTestBase {
   private $themeFiles;
   private $themeData;
 
-  /**
-   * The configuration directories for this test run.
-   *
-   * @var array
-   */
-  protected $configDirectories = array();
-
   /**
    * A KeyValueMemoryFactory instance to use when building the container.
    *
@@ -100,27 +93,6 @@ protected function beforePrepareEnvironment() {
     }
   }
 
-  /**
-   * 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.
    */
@@ -129,9 +101,6 @@ protected function setUp() {
 
     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.
@@ -188,9 +157,7 @@ protected function setUp() {
   }
 
   protected function tearDown() {
-    if ($this->kernel instanceof DrupalKernel) {
-      $this->kernel->shutdown();
-    }
+    $this->kernel->shutdown();
     // Before tearing down the test environment, ensure that no stream wrapper
     // of this test leaks into the parent environment. Unlike all other global
     // state variables in Drupal, stream wrappers are a global state construct
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 47ba5111a94ef56e6dea0c783e044ac505640ccb..b6df34700810310e73e26c74c1d2c94ba90283c1 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -36,13 +36,6 @@ abstract class TestBase {
    */
   protected $testId;
 
-  /**
-   * The site directory of this test run.
-   *
-   * @var string
-   */
-  protected $siteDirectory = NULL;
-
   /**
    * The database prefix of this test run.
    *
@@ -158,13 +151,6 @@ abstract class TestBase {
    */
   public $dieOnFail = FALSE;
 
-  /**
-   * The DrupalKernel instance used in the test.
-   *
-   * @var \Drupal\Core\DrupalKernel
-   */
-  protected $kernel;
-
   /**
    * The dependency injection container used in the test.
    *
@@ -622,20 +608,7 @@ 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.
@@ -884,9 +857,7 @@ public function run(array $methods = array()) {
    * @see drupal_valid_test_ua()
    */
   private function prepareDatabasePrefix() {
-    $suffix = mt_rand(1000, 1000000);
-    $this->siteDirectory = 'sites/simpletest/' . $suffix;
-    $this->databasePrefix = 'simpletest' . $suffix;
+    $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000);
 
     // 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
@@ -914,9 +885,19 @@ private function changeDatabasePrefix() {
     $connection_info = Database::getConnectionInfo('default');
     Database::renameConnection('default', 'simpletest_original_default');
     foreach ($connection_info as $target => $value) {
-      $connection_info[$target]['prefix'] = $value['prefix']['default'] . $this->databasePrefix;
+      $connection_info[$target]['prefix'] = array(
+        'default' => $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'];
   }
 
   /**
@@ -957,9 +938,10 @@ private function prepareEnvironment() {
     $language_interface = language(Language::TYPE_INTERFACE);
 
     // When running the test runner within a test, back up the original database
-    // prefix.
-    if (DRUPAL_TEST_IN_CHILD_SITE) {
+    // prefix and re-set the new/nested prefix in drupal_valid_test_ua().
+    if (drupal_valid_test_ua()) {
       $this->originalPrefix = drupal_valid_test_ua();
+      drupal_valid_test_ua($this->databasePrefix);
     }
 
     // Backup current in-memory configuration.
@@ -999,16 +981,22 @@ private function prepareEnvironment() {
 
     // Create test directory ahead of installation so fatal errors and debug
     // information can be logged during installation process.
-    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';
-
+    // 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;
 
+    // Create and set new configuration directories.
+    $this->prepareConfigDirectories();
+
     // Unregister all custom stream wrappers of the parent site.
     // Availability of Drupal stream wrappers varies by test base class:
     // - UnitTestBase operates in a completely empty environment.
@@ -1052,31 +1040,53 @@ private 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', DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
+    ini_set('error_log', $this->public_files_directory . '/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();
 
     // Remove all configuration overrides.
     $GLOBALS['config'] = array();
 
-    // 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);
   }
 
+  /**
+   * 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;
+    }
+  }
+
   /**
    * Rebuild \Drupal::getContainer().
    *
@@ -1093,11 +1103,11 @@ private function prepareEnvironment() {
    *   tests can invoke this workaround when requiring services from newly
    *   enabled modules to be immediately available in the same request.
    */
-  protected function rebuildContainer($environment = 'testing') {
+  protected function rebuildContainer() {
     // Preserve the request object after the container rebuild.
     $request = \Drupal::request();
 
-    $this->kernel = new DrupalKernel($environment, drupal_classloader(), FALSE);
+    $this->kernel = new DrupalKernel('testing', 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.
@@ -1159,12 +1169,8 @@ private function restoreEnvironment() {
       }
     }
 
-    // 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));
-
-    // Delete test site directory.
-    file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback'));
+    // Delete temporary files directory.
+    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10), array($this, 'filePreDeleteCallback'));
 
     // Restore original database connection.
     Database::removeConnection('default');
@@ -1193,13 +1199,19 @@ private function restoreEnvironment() {
     \Drupal::setContainer($this->originalContainer);
     $GLOBALS['config_directories'] = $this->originalConfigDirectories;
 
+    // Re-initialize original stream wrappers of the parent site.
+    // This must happen after static variables have been reset and the original
+    // container and $config_directories are restored, as simpletest_log_read()
+    // uses the public stream wrapper to locate the error.log.
+    file_get_stream_wrappers();
+
+    // 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);
+
     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/BrokenSetUpTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php
index 8981519172308d725901a8b1362cb6fc84e8c5a1..42b786ac3fa584e908fbc4ccc188cd24daaa7320 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/Tests/BrokenSetUpTest.php
@@ -28,13 +28,6 @@ class BrokenSetUpTest extends WebTestBase {
    */
   public static $modules = array('simpletest');
 
-  /**
-   * The path to the shared trigger file.
-   *
-   * @var string
-   */
-  protected $sharedTriggerFile;
-
   public static function getInfo() {
     return array(
       'name' => 'Broken SimpleTest method',
@@ -45,20 +38,15 @@ public static function getInfo() {
 
   function setUp() {
     // If the test is being run from the main site, set up normally.
-    if (!$this->isInChildSite()) {
+    if (!drupal_valid_test_ua()) {
       parent::setUp();
-
-      $this->sharedTriggerFile = $this->public_files_directory . '/trigger';
-
       // Create and log in user.
       $admin_user = $this->drupalCreateUser(array('administer unit tests'));
       $this->drupalLogin($admin_user);
     }
     // If the test is being run from within simpletest, set up the broken test.
     else {
-      $this->sharedTriggerFile = $this->originalFileDirectory . '/trigger';
-
-      if (file_get_contents($this->sharedTriggerFile) === 'setup') {
+      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'setup') {
         throw new \Exception('Broken setup');
       }
       $this->pass('The setUp() method has run.');
@@ -67,13 +55,13 @@ function setUp() {
 
   function tearDown() {
     // If the test is being run from the main site, tear down normally.
-    if (!$this->isInChildSite()) {
-      unlink($this->sharedTriggerFile);
+    if (!drupal_valid_test_ua()) {
+      unlink($this->originalFileDirectory . '/simpletest/trigger');
       parent::tearDown();
     }
     // If the test is being run from within simpletest, output a message.
     else {
-      if (file_get_contents($this->sharedTriggerFile) === 'teardown') {
+      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'teardown') {
         throw new \Exception('Broken teardown');
       }
       $this->pass('The tearDown() method has run.');
@@ -86,9 +74,9 @@ function tearDown() {
   function testMethod() {
     // If the test is being run from the main site, run it again from the web
     // interface within the simpletest child site.
-    if (!$this->isInChildSite()) {
+    if (!drupal_valid_test_ua()) {
       // Verify that a broken setUp() method is caught.
-      file_put_contents($this->sharedTriggerFile, 'setup');
+      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'setup');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertRaw('Broken setup');
@@ -99,7 +87,7 @@ function testMethod() {
       $this->assertNoRaw('The tearDown() method has run.');
 
       // Verify that a broken tearDown() method is caught.
-      file_put_contents($this->sharedTriggerFile, 'teardown');
+      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'teardown');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertNoRaw('Broken setup');
@@ -110,7 +98,7 @@ function testMethod() {
       $this->assertNoRaw('The tearDown() method has run.');
 
       // Verify that a broken test method is caught.
-      file_put_contents($this->sharedTriggerFile, 'test');
+      file_put_contents($this->originalFileDirectory . '/simpletest/trigger', 'test');
       $edit['Drupal\simpletest\Tests\BrokenSetUpTest'] = TRUE;
       $this->drupalPostForm('admin/config/development/testing', $edit, t('Run tests'));
       $this->assertNoRaw('Broken setup');
@@ -122,7 +110,7 @@ function testMethod() {
     }
     // If the test is being run from within simpletest, output a message.
     else {
-      if (file_get_contents($this->sharedTriggerFile) === 'test') {
+      if (file_get_contents($this->originalFileDirectory . '/simpletest/trigger') === 'test') {
         throw new \Exception('Broken test');
       }
       $this->pass('The test method has run.');
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php b/core/modules/simpletest/lib/Drupal/simpletest/Tests/MissingCheckedRequirementsTest.php
index 24b43483954d7171610773ae049cccddb8d4d7ce..61d6401e53654f8ac128401c69197eb377a375d3 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 ($this->isInChildSite()) {
+    if (drupal_valid_test_ua()) {
       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 (!$this->isInChildSite()) {
+    if (!drupal_valid_test_ua()) {
       // 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 627ae0baf601de6983796cedeb9df0fbbaa512aa..2a19a5c13c4356c579750835ceba9a53937fcda4 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->isInChildSite()) {
+    if (!$this->inCURL()) {
       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->isInChildSite()) {
+    if (!$this->inCURL()) {
       // Retrieve the test page and check its title and headers.
       $this->drupalGet('test-page');
       $this->assertTrue($this->drupalGetHeader('Date'), 'An HTTP header was received.');
@@ -91,12 +91,10 @@ function testInternalBrowser() {
       $headers = $this->drupalGetHeaders(TRUE);
       $this->assertEqual(count($headers), 2, 'Simpletest stopped following redirects after the first one.');
 
-      // Remove the Simpletest private key file so we can test the protection
+      // 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.
-      // @see drupal_valid_test_ua()
-      // Not using File API; a potential error must trigger a PHP warning.
-      unlink($this->siteDirectory . '/.htkey');
+      drupal_unlink($this->public_files_directory . '/settings.php');
       global $base_url;
       $this->drupalGet(url($base_url . '/core/install.php', array('external' => TRUE, 'absolute' => TRUE)));
       $this->assertResponse(403, 'Cannot access install.php.');
@@ -107,7 +105,7 @@ function testInternalBrowser() {
    * Test validation of the User-Agent header we use to perform test requests.
    */
   function testUserAgentValidation() {
-    if (!$this->isInChildSite()) {
+    if (!$this->inCURL()) {
       global $base_url;
       $system_path = $base_url . '/' . drupal_get_path('module', 'system');
       $HTTP_path = $system_path .'/tests/http.php?q=node';
@@ -149,7 +147,7 @@ function testWebTestRunner() {
     $this->valid_permission = 'access content';
     $this->invalid_permission = 'invalid permission';
 
-    if ($this->isInChildSite()) {
+    if ($this->inCURL()) {
       // Only run following code if this test is running itself through a CURL
       // request.
       $this->stubTest();
@@ -337,4 +335,10 @@ 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 643f6529d8094af430f9be06764f05c526b248e2..86a725435cc5fef938f84b3a73af11963f7fcab3 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
@@ -19,6 +19,11 @@
  */
 abstract class UnitTestBase extends TestBase {
 
+  /**
+   * @var array
+   */
+ protected $configDirectories;
+
   /**
    * Constructor for UnitTestBase.
    */
@@ -36,7 +41,6 @@ function __construct($test_id = NULL) {
    * setUp() method.
    */
   protected function setUp() {
-    file_prepare_directory($this->public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
     $this->settingsSet('file_public_path', $this->public_files_directory);
   }
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index f5b8cb9b87f01d597d6c83847f9e278ec72c98af..3917ffae8b22bbc1ab33d7284db0b64e7f6d26c1 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -767,78 +767,46 @@ protected function setUp() {
     $batch = &batch_get();
     $batch = array();
 
-    // 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.
-    // Not using File API; a potential error must trigger a PHP warning.
-    copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
-
-    // All file system paths are created 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,
-    );
-    // 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();
+    $this->settingsSet('file_public_path', $this->public_files_directory);
+    $GLOBALS['config']['system.file']['path']['private'] = $this->private_files_directory;
+    $GLOBALS['config']['system.file']['path']['temporary'] = $this->temp_files_directory;
+    $GLOBALS['config']['locale.settings']['translation']['path'] = $this->translation_files_directory;
 
     // 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);
 
-    // 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.
-    // TestBase::restoreEnvironment() will delete the entire site directory.
-    // Not using File API; a potential error must trigger a PHP warning.
-    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
+    // 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);
 
+    $this->settingsSet('cache', array());
     $this->rebuildContainer();
 
-    // Manually create and configure private and temporary files directories.
-    // While these could be preset/enforced in settings.php like the public
-    // files directory above, some tests expect them to be configurable in the
-    // UI. If declared in settings.php, they would no longer be configurable.
-    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 configure the test mail collector implementation to prevent
-    // tests from sending out e-mails and collect them in state instead.
-    // While this should be enforced via settings.php prior to installation,
-    // some tests expect to be able to test mail system implementations.
-    \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();
@@ -855,18 +823,22 @@ protected function setUp() {
       $this->rebuildContainer();
     }
 
-    // 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.
+    // Reset/rebuild all data structures after enabling the modules.
     $this->resetAll();
 
+    // Now make sure that the file path configurations are saved. This is done
+    // after we install the modules to override default values.
+    \Drupal::config('system.file')
+      ->set('path.private', $this->private_files_directory)
+      ->set('path.temporary', $this->temp_files_directory)
+      ->save();
+    \Drupal::config('locale.settings')
+      ->set('translation.path', $this->translation_files_directory)
+      ->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();
+
     // 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();
@@ -923,29 +895,40 @@ protected function installParameters() {
   }
 
   /**
-   * Rewrites the settings.php file of the test site.
+   * Writes a test-specific settings.php file for the child site.
    *
-   * @param array $settings
-   *   An array of settings to write out, in the format expected by
-   *   drupal_rewrite_settings().
+   * The child site loads this after the parent site's settings.php, so settings
+   * here override those.
    *
+   * @param $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(array $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;
+    }
+
     include_once DRUPAL_ROOT . '/core/includes/install.inc';
-    $filename = $this->siteDirectory . '/settings.php';
-    // system_requirements() removes write permissions from settings.php
-    // whenever it is invoked.
-    // Not using File API; a potential error must trigger a PHP warning.
-    chmod($filename, 0666);
+    $filename = $this->public_files_directory . '/settings.php';
+    file_put_contents($filename, "<?php\n");
     drupal_rewrite_settings($settings, $filename);
+
+    // Restore the original globals.
+    foreach ($original_globals as $variable_name => $value) {
+      $GLOBALS[$variable_name] = $value;
+    }
   }
 
   /**
-   * Queues custom translations to be written to settings.php.
+   * Sets custom translations to the settings object and queues them to writing.
    *
-   * Use WebTestBase::writeCustomTranslations() to apply and write the queued
-   * translations.
+   * In order for those custom translations to persist (being written in test
+   * site's settings.php) make sure to also call self::writeCustomTranslations()
    *
    * @param string $langcode
    *   The langcode to add translations for.
@@ -958,56 +941,32 @@ protected function writeSettings(array $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) {
-    // 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;
-        }
+    $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,
+        );
       }
     }
   }
 
   /**
-   * Writes custom translations to the test site's settings.php.
-   *
-   * Use TestBase::addCustomTranslations() to queue custom translations before
-   * calling this method.
+   * Writes custom translations to test site's settings.php.
    */
   protected function writeCustomTranslations() {
-    $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);
-    }
+    $this->writeSettings(array('settings' => $this->customTranslations));
+    $this->customTranslations = array();
   }
 
   /**
    * Overrides \Drupal\simpletest\TestBase::rebuildContainer().
    */
-  protected function rebuildContainer($environment = 'prod') {
-    parent::rebuildContainer($environment);
+  protected function rebuildContainer() {
+    parent::rebuildContainer();
     // Make sure the url generator has a request object, otherwise calls to
     // $this->drupalGet() will fail.
     $this->prepareRequestForGenerator();
@@ -1047,8 +1006,8 @@ protected function refreshVariables() {
     // Clear the tag cache.
     drupal_static_reset('Drupal\Core\Cache\CacheBackendInterface::tagCache');
 
-    $this->container->get('config.factory')->reset();
-    $this->container->get('state')->resetCache();
+    \Drupal::service('config.factory')->reset();
+    \Drupal::state()->resetCache();
   }
 
   /**
@@ -1311,21 +1270,6 @@ 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.
    *
@@ -1369,18 +1313,10 @@ 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();
@@ -1900,28 +1836,6 @@ protected function serializePostValues($post = array()) {
     return implode('&', $post);
   }
 
-  /**
-   * 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.
    */
diff --git a/core/modules/simpletest/simpletest.api.php b/core/modules/simpletest/simpletest.api.php
index dad9a397806d00e7c2df0a1c0691a3081deabd96..e11c651f65f9601a23c0ac7216a216f6b49c8d39 100644
--- a/core/modules/simpletest/simpletest.api.php
+++ b/core/modules/simpletest/simpletest.api.php
@@ -5,6 +5,18 @@
  * Hooks provided by the SimpleTest module.
  */
 
+/**
+ * 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 917a938cda8df9509389b0a516e36c6d9a427af4..3421dd0a745241bfb0787b1585f49e8c28db7c79 100644
--- a/core/modules/simpletest/simpletest.install
+++ b/core/modules/simpletest/simpletest.install
@@ -58,28 +58,6 @@ 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'));
   }
 
-  $site_directory = 'sites/simpletest';
-  if (!drupal_verify_install_file(DRUPAL_ROOT . '/' . $site_directory, FILE_EXIST|FILE_READABLE|FILE_WRITABLE|FILE_EXECUTABLE, '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 !sites-simpletest directory to exist and be writable in order to run tests.', array(
-        '!sites-simpletest' => '<code>./' . check_plain($site_directory) . '</code>',
-      )),
-    );
-  }
-  elseif (!file_save_htaccess(DRUPAL_ROOT . '/' . $site_directory, FALSE)) {
-    $requirements['simpletest_site_directory'] = array(
-      'title' => t('Simpletest site directory'),
-      'value' => t('Not protected'),
-      'severity' => REQUIREMENT_ERROR,
-      'description' => t('The file !file does not exist and could not be created automatically, which poses a security risk. Ensure that the directory is writable.', array(
-        '!file' => '<code>./' . check_plain($site_directory) . '/.htaccess</code>',
-      )),
-    );
-  }
-
   return $requirements;
 }
 
@@ -179,12 +157,8 @@ function simpletest_schema() {
  * Implements hook_uninstall().
  */
 function simpletest_uninstall() {
-  // Do not clean the environment in case the Simpletest module is uninstalled
-  // in a (recursive) test for itself, since simpletest_clean_environment()
-  // would also delete the test site of the parent test process.
-  if (!DRUPAL_TEST_IN_CHILD_SITE) {
-    simpletest_clean_environment();
-  }
-  // Delete verbose test output and any other testing framework files.
+  simpletest_clean_database();
+
+  // Remove generated files.
   file_unmanaged_delete_recursive('public://simpletest');
 }
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index 82db60ee7dfc7ad591b2e0338dc4e67633a547a8..c6b53bf0c4055c406c8c22750aa5b9f4d1417a0a 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -383,16 +383,18 @@ function simpletest_last_test_get($test_id) {
  *
  * @param $test_id
  *   The test ID to which the log relates.
- * @param $database_prefix
+ * @param $prefix
  *   The database prefix to which the log relates.
  * @param $test_class
  *   The test class to which the log relates.
- *
+ * @param $during_test
+ *   Indicates that the current file directory path is a temporary file
+ *   file directory used during testing.
  * @return
  *   Found any entries in log.
  */
-function simpletest_log_read($test_id, $database_prefix, $test_class) {
-  $log = DRUPAL_ROOT . '/sites/simpletest/' . substr($database_prefix, 10) . '/error.log';
+function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALSE) {
+  $log = 'public://' . ($during_test ? '' : '/simpletest/' . substr($prefix, 10)) . '/error.log';
   $found = FALSE;
   if (file_exists($log)) {
     foreach (file($log) as $line) {
@@ -646,11 +648,11 @@ function simpletest_clean_database() {
  */
 function simpletest_clean_temporary_directories() {
   $count = 0;
-  if (is_dir(DRUPAL_ROOT . '/sites/simpletest')) {
-    $files = scandir(DRUPAL_ROOT . '/sites/simpletest');
+  if (is_dir('public://simpletest')) {
+    $files = scandir('public://simpletest');
     foreach ($files as $file) {
-      if ($file[0] != '.') {
-        $path = DRUPAL_ROOT . '/sites/simpletest/' . $file;
+      $path = 'public://simpletest/' . $file;
+      if (is_dir($path) && (is_numeric($file) || strpos($file, 'config_simpletest') !== FALSE)) {
         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 6fb3ab18deab622749525dce273b5cfe16312d75..d75db82f6709bbf814b345d79077bc9470c64532 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Ajax/FormValuesTest.php
@@ -53,8 +53,6 @@ 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(
@@ -63,8 +61,5 @@ function testSimpleAjaxFormValue() {
       $commands = $this->drupalPostAjaxForm('ajax_forms_test_get_form', $edit, $element_name);
       $this->assertResponse(500);
     }
-    // The exceptions are expected. Do not interpret them as a test failure.
-    // Not using File API; a potential error must trigger a PHP warning.
-    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 76d0fde611d762b658e81290e69e0b580fe4d0dc..ae61ae5706effc89b41de58858c249c8411f24fe 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\DrupalUnitTestBase;
+use Drupal\simpletest\UnitTestBase;
 
 /**
  * Tests compilation of the DIC.
  */
-class DrupalKernelTest extends DrupalUnitTestBase {
+class DrupalKernelTest extends UnitTestBase {
 
   public static function getInfo() {
     return array(
@@ -26,13 +26,7 @@ public static function getInfo() {
   }
 
   function setUp() {
-    // 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();
-
+    parent::setUp();
     $this->settingsSet('php_storage', array('service_container' => array(
       'bin' => 'service_container',
       'class' => 'Drupal\Component\PhpStorage\MTimeProtectedFileStorage',
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 2726a2266a4a7c0adbd22d8bb66102f3e367c257..399cbc5255fefdc00a64d85c6438539f4422b063 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Installer/InstallerTranslationTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Installer;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\system\Tests\InstallerTest;
 
 /**
@@ -15,11 +16,11 @@
 class InstallerTranslationTest extends InstallerTest {
 
   /**
-   * Overrides the language code in which to install Drupal.
+   * Whether the installer has completed.
    *
-   * @var string
+   * @var bool
    */
-  protected $langcode = 'de';
+  protected $isInstalled = FALSE;
 
   public static function getInfo() {
     return array(
@@ -29,34 +30,104 @@ public static function getInfo() {
     );
   }
 
-  /**
-   * 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;
-  }
+  protected function setUp() {
+    $this->isInstalled = FALSE;
 
-  /**
-   * 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();
+
+    $settings['conf_path'] = (object) array(
+      'value' => $this->public_files_directory,
+      'required' => TRUE,
+    );
+    $settings['config_directories'] = (object) array(
+      'value' => array(),
+      'required' => TRUE,
+    );
+    $settings['config']['system.file'] = (object) array(
+      'value' => array(
+        'path' => array(
+          'private' => $this->private_files_directory,
+          'temporary' => $this->temp_files_directory,
+        ),
+      ),
+      'required' => TRUE,
+    );
+    // Add the translations directory so we can retrieve German translations.
+    $settings['config']['locale.settings'] = (object) array(
+      'value' => array(
+        'translation' => array(
+          'path' => drupal_get_path('module', 'simpletest') . '/files/translations',
+        ),
+      ),
+      'required' => TRUE,
+    );
+    $this->writeSettings($settings);
+
+    // Submit the installer with German language.
+    $edit = array(
+      'langcode' => 'de',
+    );
+    $this->drupalPostForm($GLOBALS['base_url'] . '/core/install.php', $edit, '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'));
+    $this->assertNotEqual($submit_value, 'Save and continue');
+
+    // Submit the Standard profile installation.
+    $edit = array(
+      'profile' => 'standard',
+    );
+    $this->drupalPostForm(NULL, $edit, $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();
+
+    \Drupal::config('system.file')
+      ->set('path.private', $this->private_files_directory)
+      ->set('path.temporary', $this->temp_files_directory)
+      ->save();
+    \Drupal::config('locale.settings')
+      ->set('translation.path', $this->translation_files_directory)
+      ->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();
+
+    // 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->isInstalled = TRUE;
   }
 
 }
diff --git a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
index 34a82ac4d6f97fe5a1bc6363eab36956f151e902..e788a6fc402235472926efa22c5f4847bbce4c5d 100644
--- a/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/InstallerTest.php
@@ -8,60 +8,13 @@
 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.
    *
@@ -77,74 +30,57 @@ public static function getInfo() {
     );
   }
 
-  /**
-   * Overrides WebTestBase::setUp().
-   */
   protected function setUp() {
     $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)) {
-      // Not using File API; a potential error must trigger a PHP warning.
-      copy(DRUPAL_ROOT . '/sites/default/default.settings.php', DRUPAL_ROOT . '/' . $this->siteDirectory . '/settings.php');
-      $this->writeSettings($settings);
-    }
-
-    // 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();
-
-    // Configure 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();
-
-    // Import new settings.php written by the installer.
-    drupal_settings_initialize();
-    foreach ($GLOBALS['config_directories'] as $type => $path) {
-      $this->configDirectories[$type] = $path;
+    $settings['conf_path'] = (object) array(
+      'value' => $this->public_files_directory,
+      'required' => TRUE,
+    );
+    $settings['config_directories'] = (object) array(
+      'value' => array(),
+      'required' => TRUE,
+    );
+    $settings['config']['system.file'] = (object) array(
+      'value' => array(
+        'path' => array(
+          'private' => $this->private_files_directory,
+          'temporary' => $this->temp_files_directory,
+        ),
+      ),
+      'required' => TRUE,
+    );
+    $settings['config']['locale.settings'] = (object) array(
+      'value' => array(
+        'translation' => array(
+          'path' => $this->translation_files_directory,
+        ),
+      ),
+      '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;
     }
-
-    // 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.
-    // WebTestBase::tearDown() will delete the entire test site directory.
-    // Not using File API; a potential error must trigger a PHP warning.
-    chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
-
     $this->rebuildContainer();
 
-    // Manually configure the test mail collector implementation to prevent
-    // tests from sending out e-mails and collect them in state instead.
-    \Drupal::config('system.mail')
-      ->set('interface.default', 'Drupal\Core\Mail\TestMailCollector')
+    \Drupal::config('system.file')
+      ->set('path.private', $this->private_files_directory)
+      ->set('path.temporary', $this->temp_files_directory)
       ->save();
+    \Drupal::config('locale.settings')
+      ->set('translation.path', $this->translation_files_directory)
+      ->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();
 
     // When running from run-tests.sh we don't get an empty current path which
     // would indicate we're on the home page.
@@ -156,49 +92,6 @@ protected function setUp() {
     $this->isInstalled = TRUE;
   }
 
-  /**
-   * Installer step: Select language.
-   */
-  protected function setUpLanguage() {
-    $edit = array(
-      'langcode' => $this->langcode,
-    );
-    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
-  }
-
-  /**
-   * Installer step: Select installation profile.
-   */
-  protected function setUpProfile() {
-    $edit = array(
-      'profile' => $this->profile,
-    );
-    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
-  }
-
-  /**
-   * Installer step: Configure settings.
-   */
-  protected function setUpSettings() {
-    $edit = $this->translatePostValues($this->parameters['forms']['install_settings_form']);
-    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
-  }
-
-  /**
-   * Installer step: Configure site.
-   */
-  protected function setUpSite() {
-    $edit = $this->translatePostValues($this->parameters['forms']['install_configure_form']);
-    $this->drupalPostForm(NULL, $edit, $this->translations['Save and continue']);
-  }
-
-  /**
-   * Installer step: Confirm installation.
-   */
-  protected function setUpConfirm() {
-    $this->clickLink($this->translations['Visit your new site']);
-  }
-
   /**
    * {@inheritdoc}
    *
@@ -211,14 +104,35 @@ protected function refreshVariables() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * This override is necessary because the parent drupalGet() calls t(), which
+   * is not available early during installation.
+   */
+  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;
+    }
+    $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->assertUrl('user/1');
+    $this->drupalGet('user');
     $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 2356a849cfcf9eda40bd0a0ca5b92691e8a111b3..7d20be9bc8ca123776b90dd8a6ddcf53bafe7f2a 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ErrorHandlerTest.php
@@ -94,9 +94,6 @@ function testErrorHandler() {
    * Test the exception handler.
    */
   function testExceptionHandler() {
-    // Ensure the test error log is empty before these tests.
-    $this->assertNoErrorsLogged();
-
     $error_exception = array(
       '%type' => 'Exception',
       '!message' => 'Drupal is awesome',
@@ -124,10 +121,6 @@ function testExceptionHandler() {
     $this->assertText($error_pdo_exception['!message'], format_string('Found !message in error page.', $error_pdo_exception));
     $error_details = format_string('in %function (line ', $error_pdo_exception);
     $this->assertRaw($error_details, format_string("Found '!message' in error page.", array('!message' => $error_details)));
-
-    // The exceptions are expected. Do not interpret them as a test failure.
-    // Not using File API; a potential error must trigger a PHP warning.
-    unlink(DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log');
   }
 
   /**
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 be463ab5d0950fc4c54ad943cf68773c7c8c7aa7..2c42bd00dbc3a2859ceb18df5fe973e6aa9b4f6e 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ShutdownFunctionsTest.php
@@ -29,14 +29,6 @@ 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.
-    // Not using File API; a potential error must trigger a PHP warning.
-    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 5e32a6b060efc1eb0db38caa691ad147cd91cdcb..4f90a5768d54ee82b3e5042c7a0770f80d3f0335 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -326,8 +326,9 @@ function system_requirements($phase) {
     }
     else {
       // If we are installing Drupal, the settings.php file might not exist yet
-      // in the intended site directory, so don't require it.
-      $directories[] = conf_path(FALSE) . '/files';
+      // 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';
     }
     if (!empty($GLOBALS['config']['system.file']['path']['private'])) {
       $directories[] = $GLOBALS['config']['system.file']['path']['private'];
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index 5594dd532504aaab69621f7e06266a1c1217aa68..9b1a7bb1105e9eb84213d49240cca9a40a3b3335 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -7,6 +7,7 @@
 require_once __DIR__ . '/../vendor/autoload.php';
 
 use Drupal\Component\Utility\Timer;
+use Drupal\Core\StreamWrapper\PublicStream;
 
 const SIMPLETEST_SCRIPT_COLOR_PASS = 32; // Green.
 const SIMPLETEST_SCRIPT_COLOR_FAIL = 31; // Red.
@@ -415,7 +416,8 @@ 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']);
-            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
+            $public_files = PublicStream::basePath();
+            $test_directory = $public_files . '/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.
@@ -581,9 +583,10 @@ 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 site directory was setup already.
-  // @see \Drupal\simpletest\TestBase::prepareEnvironment()
-  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
+  // Check whether a test file directory was setup already.
+  // @see prepareEnvironment()
+  $public_files = PublicStream::basePath();
+  $test_directory = $public_files . '/simpletest/' . substr($db_prefix, 10);
   if (is_dir($test_directory)) {
     // Output the error_log.
     if (is_file($test_directory . '/error.log')) {
@@ -592,7 +595,8 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
         $messages[] = $errors;
       }
     }
-    // Delete the test site directory.
+
+    // Delete the test files directory.
     // simpletest_clean_temporary_directories() cannot be used here, since it
     // would also delete file directories of other tests that are potentially
     // running concurrently.