diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index 1484e48151d6f6623d95ac941feb782484531883..53e6bc75ef23054e187dad0cbf9f9b6503109b41 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -22,7 +22,6 @@ maintainer. Current component maintainers for Drupal 8:
 
 Ajax system
 - Alex Bronstein 'effulgentsia' <http://drupal.org/user/78040>
-- Randy Fay 'rfay' <http://drupal.org/user/30906>
 - Earl Miles 'merlinofchaos' <http://drupal.org/user/26979>
 
 Base system
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 6e0e2c66df6f8e0704ee4e740e23e33d63f1f2d5..c94bc62c98f349423d225b6d2b6f77c7863c0e7c 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -1108,7 +1108,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
  *   A keyed array containing the current state of the form. The current
  *   user-submitted data is stored in $form_state['values'], though
  *   form validation functions are passed an explicit copy of the
- *   values for the sake of simplicity. Validation handlers can also
+ *   values for the sake of simplicity. Validation handlers can also use
  *   $form_state to pass information on to submit handlers. For example:
  *     $form_state['data_for_submission'] = $data;
  *   This technique is useful when validation requires file parsing,
diff --git a/core/includes/language.inc b/core/includes/language.inc
index 8e5f1ac6912143c6855a7180ae820dc9fd5e153b..0892cc4b882c5ca439d8503ae10aabc8d2ebeb2e 100644
--- a/core/includes/language.inc
+++ b/core/includes/language.inc
@@ -2,7 +2,9 @@
 
 /**
  * @file
- * Multiple language handling functionality.
+ * Language Negotiation API.
+ *
+ * @see http://drupal.org/node/1497272
  */
 
 /**
@@ -11,10 +13,99 @@
 const LANGUAGE_NEGOTIATION_DEFAULT = 'language-default';
 
 /**
- * Chooses a language for the given type based on language negotiation settings.
+ * @defgroup language_negotiation Language Negotiation API functionality
+ * @{
+ * Functions to customize the language types and the negotiation process.
+ *
+ * The language negotiation API is based on two major concepts:
+ * - Language types: types of translatable data (the types of data that a user
+ *   can view or request).
+ * - Language negotiation methods: functions for determining which language to
+ *   use to present a particular piece of data to the user.
+ * Both language types and language negotiation methods are customizable.
+ *
+ * Drupal defines three built-in language types:
+ * - Interface language: The page's main language, used to present translated
+ *   user interface elements such as titles, labels, help text, and messages.
+ * - Content language: The language used to present content that is available
+ *   in more than one language (see
+ *   @link field_language Field Language API @endlink for details).
+ * - URL language: The language associated with URLs. When generating a URL,
+ *   this value will be used by url() as a default if no explicit preference is
+ *   provided.
+ * Modules can define additional language types through
+ * hook_language_types_info(), and alter existing language type definitions
+ * through hook_language_types_info_alter().
+ *
+ * Language types may be configurable or fixed. The language negotiation
+ * methods associated with a configurable language type can be explicitly
+ * set through the user interface. A fixed language type has predetermined
+ * (module-defined) language negotiation settings and, thus, does not appear in
+ * the configuration page. Here is a code snippet that makes the content
+ * language (which by default inherits the interface language's values)
+ * configurable:
+ * @code
+ * function mymodule_language_types_info_alter(&$language_types) {
+ *   unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']);
+ * }
+ * @endcode
+ *
+ * Every language type can have a different set of language negotiation methods
+ * assigned to it. Different language types often share the same language
+ * negotiation settings, but they can have independent settings if needed. If
+ * two language types are configured the same way, their language switcher
+ * configuration will be functionally identical and the same settings will act
+ * on both language types.
+ *
+ * Drupal defines the following built-in language negotiation methods:
+ * - URL: Determine the language from the URL (path prefix or domain).
+ * - Session: Determine the language from a request/session parameter.
+ * - User: Follow the user's language preference.
+ * - Browser: Determine the language from the browser's language settings.
+ * - Default language: Use the default site language.
+ * Language negotiation methods are simple callback functions that implement a
+ * particular logic to return a language code. For instance, the URL method
+ * searches for a valid path prefix or domain name in the current request URL.
+ * If a language negotiation method does not return a valid language code, the
+ * next method associated to the language type (based on method weight) is
+ * invoked.
+ *
+ * Modules can define additional language negotiation methods through
+ * hook_language_negotiation_info(), and alter existing methods through
+ * hook_language_negotiation_info_alter(). Here is an example snippet that lets
+ * path prefixes be ignored for administrative paths:
+ * @code
+ * function mymodule_language_negotiation_info_alter(&$negotiation_info) {
+ *   // Replace the core function with our own function.
+ *   module_load_include('language', 'inc', 'language.negotiation');
+ *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['callbacks']['negotiation'] = 'mymodule_from_url';
+ *   $negotiation_info[LANGUAGE_NEGOTIATION_URL]['file'] = drupal_get_path('module', 'mymodule') . '/mymodule.module';
+ * }
+ *
+ * function mymodule_from_url($languages) {
+ *   // Use the core URL language negotiation method to get a valid language
+ *   // code.
+ *   module_load_include('language', 'inc', 'language.negotiation');
+ *   $langcode = language_from_url($languages);
+ *
+ *   // If we are on an administrative path, override with the default language.
+ *   if (isset($_GET['q']) && strtok($_GET['q'], '/') == 'admin') {
+ *     return language_default()->langcode;
+ *   }
+ *   return $langcode;
+ * }
+ * ?>
+ * @endcode
+ *
+ * For more information, see
+ * @link http://drupal.org/node/1497272 Language Negotiation API @endlink
+ */
+
+/**
+ * Chooses a language based on language negotiation method settings.
  *
  * @param $type
- *   The language type key.
+ *   The language type key to find the language for.
  *
  * @return
  *   The negotiated language object.
@@ -68,8 +159,8 @@ function language_types_info() {
  * Returns only the configurable language types.
  *
  * A language type maybe configurable or fixed. A fixed language type is a type
- * whose negotiation values are unchangeable and defined while defining the
- * language type itself.
+ * whose language negotiation methods are module-defined and not altered through
+ * the user interface.
  *
  * @param $stored
  *   (optional) By default, retrieves values from the 'language_types' variable
@@ -162,6 +253,10 @@ function language_types_set() {
  *
  * @param $type
  *   The language type.
+ *
+ * @return
+ *   The identifier of the first language negotiation method for the given
+ *   language type, or the default method if none exists.
  */
 function language_negotiation_method_get_first($type) {
   $negotiation = variable_get("language_negotiation_$type", array());
@@ -169,7 +264,7 @@ function language_negotiation_method_get_first($type) {
 }
 
 /**
- * Checks if a language negotiation method is enabled for a language type.
+ * Checks whether a language negotiation method is enabled for a language type.
  *
  * @param $method_id
  *   The language negotiation method ID.
@@ -259,7 +354,7 @@ function language_negotiation_purge() {
  * @param $type
  *   The language type.
  * @param $method_weights
- *   An array of language negotiation method weights keyed by method id.
+ *   An array of language negotiation method weights keyed by method ID.
  */
 function language_negotiation_set($type, $method_weights) {
   // Save only the necessary fields.
@@ -278,8 +373,7 @@ function language_negotiation_set($type, $method_weights) {
       // If the language negotiation method does not express any preference
       // about types, make it available for any configurable type.
       $types = array_flip(isset($method['types']) ? $method['types'] : $default_types);
-      // Check if the language negotiation method is defined and has the right
-      // type.
+      // Check whether the method is defined and has the right type.
       if (isset($types[$type])) {
         $method_data = array();
         foreach ($method_fields as $field) {
@@ -328,13 +422,14 @@ function language_negotiation_info() {
  * Invokes a language negotiation method and caches the results.
  *
  * @param $method_id
- *   The language negotiation method ID.
+ *   The language negotiation method's identifier.
  * @param $method
- *   (optional) The language negotiation method to be invoked. If not passed it
- *   will be explicitly loaded through language_negotiation_info().
+ *   (optional) An associative array of information about the method to be
+ *   invoked (see hook_language_negotiation_info() for details). If not passed
+ *   in, it will be loaded through language_negotiation_info().
  *
  * @return
- *   The language negotiation method's return value.
+ *   A language object representing the language chosen by the method.
  */
 function language_negotiation_method_invoke($method_id, $method = NULL) {
   $results = &drupal_static(__FUNCTION__);
@@ -361,10 +456,10 @@ function language_negotiation_method_invoke($method_id, $method = NULL) {
     $results[$method_id] = isset($languages[$langcode]) ? $languages[$langcode] : FALSE;
   }
 
-  // Since objects are resources we need to return a clone to prevent the
-  // language negotiation method cache to be unintentionally altered. The same
-  // language negotiation methods might be used with different language types
-  // based on configuration.
+  // Since objects are resources, we need to return a clone to prevent the
+  // language negotiation method cache from being unintentionally altered. The
+  // same methods might be used with different language types based on
+  // configuration.
   return !empty($results[$method_id]) ? clone($results[$method_id]) : $results[$method_id];
 }
 
@@ -381,8 +476,8 @@ function language_from_default() {
 /**
  * Split the given path into prefix and actual path.
  *
- * Parse the given path and return the language object identified by the
- * prefix and the actual path.
+ * Parse the given path and return the language object identified by the prefix
+ * and the actual path.
  *
  * @param $path
  *   The path to split.
@@ -434,3 +529,7 @@ function language_fallback_get_candidates($type = LANGUAGE_TYPE_CONTENT) {
 
   return $fallback_candidates;
 }
+
+/**
+ * @} End of "language_negotiation"
+ */
diff --git a/core/includes/module.inc b/core/includes/module.inc
index 6b4604a7b21d40f9efb396cb59ef81240d56b90b..928abc93642e9a67ab8dc3e9cad9fa01f31c3a3a 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -79,16 +79,19 @@ function module_list($refresh = FALSE, $bootstrap_refresh = FALSE, $sort = FALSE
   // Use the advanced drupal_static() pattern, since this is called very often.
   static $drupal_static_fast;
   if (!isset($drupal_static_fast)) {
-    $drupal_static_fast['list'] = &drupal_static(__FUNCTION__ . ':list', array());
+    $drupal_static_fast['list'] = &drupal_static(__FUNCTION__ . ':list');
     $drupal_static_fast['sorted_list'] = &drupal_static(__FUNCTION__ . ':sorted_list');
   }
   $list = &$drupal_static_fast['list'];
   $sorted_list = &$drupal_static_fast['sorted_list'];
 
-  if (empty($list) || $refresh || $fixed_list) {
+  if (!isset($list) || $refresh || isset($fixed_list)) {
     $list = array();
     $sorted_list = NULL;
-    if ($fixed_list) {
+    // The fixed list may be a completely empty array, thus check for isset().
+    // Calling code may use this to empty out the module list entirely. For
+    // example, unit tests need to ensure that no modules are invoked.
+    if (isset($fixed_list)) {
       foreach ($fixed_list as $name => $module) {
         drupal_get_filename('module', $name, $module['filename']);
         $list[$name] = $name;
diff --git a/core/lib/Drupal/Core/Cache/NullBackend.php b/core/lib/Drupal/Core/Cache/NullBackend.php
index c3da5d70a125e17141a1520e8cbdcee2efbc60a2..0408fc3efad2d0bda575216c2632e6bf4b91abf7 100644
--- a/core/lib/Drupal/Core/Cache/NullBackend.php
+++ b/core/lib/Drupal/Core/Cache/NullBackend.php
@@ -42,7 +42,7 @@ function getMultiple(&$cids) {
   /**
    * Implements Drupal\Core\Cache\CacheBackendInterface::set().
    */
-  function set($cid, $data, $expire = CACHE_PERMANENT) {}
+  function set($cid, $data, $expire = CACHE_PERMANENT, array $tags = array()) {}
 
   /**
    * Implements Drupal\Core\Cache\CacheBackendInterface::delete().
@@ -74,6 +74,11 @@ function expire() {}
    */
   function garbageCollection() {}
 
+  /**
+   * Implements Drupal\Core\Cache\CacheBackendInterface::invalidateTags().
+   */
+  public function invalidateTags(array $tags) {}
+
   /**
    * Implements Drupal\Core\Cache\CacheBackendInterface::isEmpty().
    */
diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index dbf7c368fc2bedef6fd1cafe490bb0c38fc98528..94a23fb05fcf25cd58ca82fac9dda4f2986e8bc1 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -819,6 +819,9 @@ public function transactionDepth() {
    * @param $name
    *   Optional name of the savepoint.
    *
+   * @return Drupal\Core\Database\Transaction
+   *   A DatabaseTransaction object.
+   *
    * @see Drupal\Core\Database\Transaction
    */
   public function startTransaction($name = '') {
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 4859aba086f8981217f736ba4570ad33db4f4a83..d2d4bad5149eb8627f10ab8bddea6f4400b94db3 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -676,7 +676,7 @@ function node_type_update_nodes($old_type, $type) {
  *  TRUE to rebuild node types. Equivalent to calling node_types_rebuild().
  *
  * @return
- *   Associative array with two components:
+ *   An object with two properties:
  *   - names: Associative array of the names of node types, keyed by the type.
  *   - types: Associative array of node type objects, keyed by the type.
  *   Both of these arrays will include new types that have been defined by
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
index 552b4e35c4cb82c7b33af8ca0eaed2f34fd77cb9..aa5f233790a0ce45f996b487a7baab14c295dec3 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php
@@ -539,6 +539,181 @@ public function run(array $methods = array()) {
     restore_error_handler();
   }
 
+  /**
+   * Generates a database prefix for running tests.
+   *
+   * The database prefix is used by prepareEnvironment() to setup a public files
+   * directory for the test to be run, which also contains the PHP error log,
+   * which is written to in case of a fatal error. Since that directory is based
+   * on the database prefix, all tests (even unit tests) need to have one, in
+   * order to access and read the error log.
+   *
+   * @see TestBase::prepareEnvironment()
+   *
+   * The generated database table prefix is used for the Drupal installation
+   * being performed for the test. It is also used as user agent HTTP header
+   * value by the cURL-based browser of DrupalWebTestCase, which is sent to the
+   * Drupal installation of the test. During early Drupal bootstrap, the user
+   * agent HTTP header is parsed, and if it matches, all database queries use
+   * the database table prefix that has been generated here.
+   *
+   * @see WebTestBase::curlInitialize()
+   * @see drupal_valid_test_ua()
+   * @see WebTestBase::setUp()
+   */
+  protected function prepareDatabasePrefix() {
+    $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
+    // with the testId, so the database prefix has to be associated with it.
+    db_update('simpletest_test_id')
+      ->fields(array('last_prefix' => $this->databasePrefix))
+      ->condition('test_id', $this->testId)
+      ->execute();
+  }
+
+  /**
+   * Changes the database connection to the prefixed one.
+   *
+   * @see WebTestBase::setUp()
+   */
+  protected function changeDatabasePrefix() {
+    if (empty($this->databasePrefix)) {
+      $this->prepareDatabasePrefix();
+    }
+
+    // Clone the current connection and replace the current prefix.
+    $connection_info = Database::getConnectionInfo('default');
+    Database::renameConnection('default', 'simpletest_original_default');
+    foreach ($connection_info as $target => $value) {
+      $connection_info[$target]['prefix'] = array(
+        'default' => $value['prefix']['default'] . $this->databasePrefix,
+      );
+    }
+    Database::addConnectionInfo('default', 'default', $connection_info['default']);
+  }
+
+  /**
+   * Prepares the current environment for running the test.
+   *
+   * Backups various current environment variables and resets them, so they do
+   * not interfere with the Drupal site installation in which tests are executed
+   * and can be restored in TestBase::tearDown().
+   *
+   * Also sets up new resources for the testing environment, such as the public
+   * filesystem and configuration directories.
+   *
+   * @see TestBase::tearDown()
+   */
+  protected function prepareEnvironment() {
+    global $user, $language_interface, $conf;
+
+    // Backup current in-memory configuration.
+    $this->originalConf = $conf;
+
+    // Backup statics and globals.
+    $this->originalContainer = clone drupal_container();
+    $this->originalLanguage = $language_interface;
+    $this->originalConfigDirectory = $GLOBALS['config_directory_name'];
+
+    // Save further contextual information.
+    $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files');
+    $this->originalProfile = drupal_get_profile();
+    $this->originalUser = $user;
+
+    // Save and clean the shutdown callbacks array because it is static cached
+    // and will be changed by the test run. Otherwise it will contain callbacks
+    // from both environments and the testing environment will try to call the
+    // handlers defined by the original one.
+    $callbacks = &drupal_register_shutdown_function();
+    $this->originalShutdownCallbacks = $callbacks;
+    $callbacks = array();
+
+    // Create test directory ahead of installation so fatal errors and debug
+    // information can be logged during installation process.
+    // Use temporary files directory with the same prefix as the database.
+    $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10);
+    $this->private_files_directory = $this->public_files_directory . '/private';
+    $this->temp_files_directory = $this->private_files_directory . '/temp';
+
+    // 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);
+    $this->generatedTestFiles = FALSE;
+
+    // Create and set a new configuration directory and signature key.
+    // The child site automatically adjusts the global $config_directory_name to
+    // a test-prefix-specific directory within the public files directory.
+    // @see config_get_config_directory()
+    $GLOBALS['config_directory_name'] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config';
+    $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name'];
+    file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+
+    // Log fatal errors.
+    ini_set('log_errors', 1);
+    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;
+  }
+
+  /**
+   * Deletes created files, database tables, and reverts all environment changes.
+   *
+   * This method needs to be invoked for both unit and integration tests.
+   *
+   * @see TestBase::prepareDatabasePrefix()
+   * @see TestBase::changeDatabasePrefix()
+   * @see TestBase::prepareEnvironment()
+   */
+  protected function tearDown() {
+    global $user, $language_interface, $conf;
+
+    // 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);
+
+    $emailCount = count(variable_get('drupal_test_email_collector', array()));
+    if ($emailCount) {
+      $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.');
+      $this->pass($message, t('E-mail'));
+    }
+
+    // Delete temporary files directory.
+    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10));
+
+    // Restore original database connection.
+    Database::removeConnection('default');
+    Database::renameConnection('simpletest_original_default', 'default');
+
+    // Reset all static variables.
+    drupal_static_reset();
+
+    // Restore has_run state.
+    $has_run = &drupal_static('module_load_all');
+    $has_run = TRUE;
+
+    // Restore original in-memory configuration.
+    $conf = $this->originalConf;
+
+    // Restore original statics and globals.
+    drupal_container($this->originalContainer);
+    $language_interface = $this->originalLanguage;
+    $GLOBALS['config_directory_name'] = $this->originalConfigDirectory;
+
+    // Restore original shutdown callbacks.
+    $callbacks = &drupal_register_shutdown_function();
+    $callbacks = $this->originalShutdownCallbacks;
+
+    // Restore original user session.
+    $user = $this->originalUser;
+    drupal_save_session(TRUE);
+  }
+
   /**
    * Handle errors during test runs.
    *
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
index 5b5850be2282c3324f80a053f2d5722877db802e..b5314cecb6d753eb867567f5b08050d9255d3447 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/UnitTestBase.php
@@ -38,55 +38,40 @@ function __construct($test_id = NULL) {
   protected function setUp() {
     global $conf;
 
-    // Store necessary current values before switching to the test environment.
-    $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files');
+    // Create the database prefix for this test.
+    $this->prepareDatabasePrefix();
 
-    // Reset all statics so that test is performed with a clean environment.
+    // Prepare the environment for running tests.
+    $this->prepareEnvironment();
+    $this->originalThemeRegistry = theme_get_registry(FALSE);
+
+    // Reset all statics and variables to perform tests in a clean environment.
+    $conf = array();
     drupal_static_reset();
 
-    // Generate temporary prefixed database to ensure that tests have a clean starting point.
-    $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');
+    // Empty out module list.
+    module_list(TRUE, FALSE, FALSE, array());
+    // Prevent module_load_all() from attempting to refresh it.
+    $has_run = &drupal_static('module_load_all');
+    $has_run = TRUE;
+
+    // Re-implant theme registry.
+    // Required for l() and other functions to work correctly and not trigger
+    // database lookups.
+    $theme_get_registry = &drupal_static('theme_get_registry');
+    $theme_get_registry[FALSE] = $this->originalThemeRegistry;
 
-    // Create test directory.
-    $public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10);
-    file_prepare_directory($public_files_directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-    $conf['file_public_path'] = $public_files_directory;
+    $conf['file_public_path'] = $this->public_files_directory;
 
-    // Clone the current connection and replace the current prefix.
-    $connection_info = Database::getConnectionInfo('default');
-    Database::renameConnection('default', 'simpletest_original_default');
-    foreach ($connection_info as $target => $value) {
-      $connection_info[$target]['prefix'] = array(
-        'default' => $value['prefix']['default'] . $this->databasePrefix,
-      );
-    }
-    Database::addConnectionInfo('default', 'default', $connection_info['default']);
+    // 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();
 
-    // Set user agent to be consistent with web test case.
+    // Set user agent to be consistent with WebTestBase.
     $_SERVER['HTTP_USER_AGENT'] = $this->databasePrefix;
 
-    // If locale is enabled then t() will try to access the database and
-    // subsequently will fail as the database is not accessible.
-    $module_list = module_list();
-    if (isset($module_list['locale'])) {
-      $this->originalModuleList = $module_list;
-      unset($module_list['locale']);
-      module_list(TRUE, FALSE, FALSE, $module_list);
-    }
     $this->setup = TRUE;
   }
-
-  protected function tearDown() {
-    global $conf;
-
-    // Get back to the original connection.
-    Database::removeConnection('default');
-    Database::renameConnection('simpletest_original_default', 'default');
-
-    $conf['file_public_path'] = $this->originalFileDirectory;
-    // Restore modules if necessary.
-    if (isset($this->originalModuleList)) {
-      module_list(TRUE, FALSE, FALSE, $this->originalModuleList);
-    }
-  }
 }
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index 14e218af8c6acb5c95ce9aa627f03ba040e7540e..3bb1e0ce77e200e50d423f70ecec1f707b1d86fb 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -534,117 +534,6 @@ protected function drupalLogout() {
     }
   }
 
-  /**
-   * Generates a database prefix for running tests.
-   *
-   * The generated database table prefix is used for the Drupal installation
-   * being performed for the test. It is also used as user agent HTTP header
-   * value by the cURL-based browser of Drupal\simpletest\WebTestBase, which is sent
-   * to the Drupal installation of the test. During early Drupal bootstrap, the
-   * user agent HTTP header is parsed, and if it matches, all database queries
-   * use the database table prefix that has been generated here.
-   *
-   * @see Drupal\simpletest\WebTestBase::curlInitialize()
-   * @see drupal_valid_test_ua()
-   * @see Drupal\simpletest\WebTestBase::setUp()
-   */
-  protected function prepareDatabasePrefix() {
-    $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
-    // with the testId, so the database prefix has to be associated with it.
-    db_update('simpletest_test_id')
-      ->fields(array('last_prefix' => $this->databasePrefix))
-      ->condition('test_id', $this->testId)
-      ->execute();
-  }
-
-  /**
-   * Changes the database connection to the prefixed one.
-   *
-   * @see Drupal\simpletest\WebTestBase::setUp()
-   */
-  protected function changeDatabasePrefix() {
-    if (empty($this->databasePrefix)) {
-      $this->prepareDatabasePrefix();
-    }
-
-    // Clone the current connection and replace the current prefix.
-    $connection_info = Database::getConnectionInfo('default');
-    Database::renameConnection('default', 'simpletest_original_default');
-    foreach ($connection_info as $target => $value) {
-      $connection_info[$target]['prefix'] = array(
-        'default' => $value['prefix']['default'] . $this->databasePrefix,
-      );
-    }
-    Database::addConnectionInfo('default', 'default', $connection_info['default']);
-  }
-
-  /**
-   * Prepares the current environment for running the test.
-   *
-   * Backups various current environment variables and resets them, so they do
-   * not interfere with the Drupal site installation in which tests are executed
-   * and can be restored in tearDown().
-   *
-   * Also sets up new resources for the testing environment, such as the public
-   * filesystem and configuration directories.
-   *
-   * @see Drupal\simpletest\WebTestBase::setUp()
-   * @see Drupal\simpletest\WebTestBase::tearDown()
-   */
-  protected function prepareEnvironment() {
-    global $user, $language_interface, $conf;
-
-    // Store necessary current values before switching to prefixed database.
-    $this->originalContainer = clone drupal_container();
-    $this->originalLanguage = $language_interface;
-    $this->originalLanguageDefault = variable_get('language_default');
-    $this->originalConfigDirectory = $GLOBALS['config_directory_name'];
-    $this->originalFileDirectory = variable_get('file_public_path', conf_path() . '/files');
-    $this->originalProfile = drupal_get_profile();
-    $this->originalUser = $user;
-
-    // Save and clean the shutdown callbacks array because it is static cached
-    // and will be changed by the test run. Otherwise it will contain callbacks
-    // from both environments and the testing environment will try to call the
-    // handlers defined by the original one.
-    $callbacks = &drupal_register_shutdown_function();
-    $this->originalShutdownCallbacks = $callbacks;
-    $callbacks = array();
-
-    // Create test directory ahead of installation so fatal errors and debug
-    // information can be logged during installation process.
-    // Use temporary files directory with the same prefix as the database.
-    $this->public_files_directory = $this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10);
-    $this->private_files_directory = $this->public_files_directory . '/private';
-    $this->temp_files_directory = $this->private_files_directory . '/temp';
-
-    // 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);
-    $this->generatedTestFiles = FALSE;
-
-    // Create and set a new configuration directory and signature key.
-    // The child site automatically adjusts the global $config_directory_name to
-    // a test-prefix-specific directory within the public files directory.
-    // @see config_get_config_directory()
-    $GLOBALS['config_directory_name'] = 'simpletest/' . substr($this->databasePrefix, 10) . '/config';
-    $this->configFileDirectory = $this->originalFileDirectory . '/' . $GLOBALS['config_directory_name'];
-    file_prepare_directory($this->configFileDirectory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-
-    // Log fatal errors.
-    ini_set('log_errors', 1);
-    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;
-  }
-
   /**
    * Sets up a Drupal site for running functional and integration tests.
    *
@@ -841,21 +730,6 @@ protected function refreshVariables() {
    * and reset the database prefix.
    */
   protected function tearDown() {
-    global $user, $language_interface;
-
-    // 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);
-
-    $emailCount = count(variable_get('drupal_test_email_collector', array()));
-    if ($emailCount) {
-      $message = format_plural($emailCount, '1 e-mail was sent during this test.', '@count e-mails were sent during this test.');
-      $this->pass($message, t('E-mail'));
-    }
-
-    // Delete temporary files directory.
-    file_unmanaged_delete_recursive($this->originalFileDirectory . '/simpletest/' . substr($this->databasePrefix, 10));
-
     // Remove all prefixed tables.
     $connection_info = Database::getConnectionInfo('default');
     $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
@@ -872,21 +746,7 @@ protected function tearDown() {
       $this->fail('Failed to drop all prefixed tables.');
     }
 
-    // Get back to the original connection.
-    Database::removeConnection('default');
-    Database::renameConnection('simpletest_original_default', 'default');
-
-    // Restore the original dependency injection container.
-    drupal_container($this->originalContainer);
-
-    // Restore original shutdown callbacks array to prevent original
-    // environment of calling handlers from test run.
-    $callbacks = &drupal_register_shutdown_function();
-    $callbacks = $this->originalShutdownCallbacks;
-
-    // Return the user to the original one.
-    $user = $this->originalUser;
-    drupal_save_session(TRUE);
+    parent::tearDown();
 
     // Ensure that internal logged in variable and cURL options are reset.
     $this->loggedInUser = FALSE;
@@ -903,18 +763,6 @@ protected function tearDown() {
     // Rebuild caches.
     $this->refreshVariables();
 
-    // Reset public files directory.
-    $GLOBALS['conf']['file_public_path'] = $this->originalFileDirectory;
-
-    // Reset configuration globals.
-    $GLOBALS['config_directory_name'] = $this->originalConfigDirectory;
-
-    // Reset language.
-    $language_interface = $this->originalLanguage;
-    if ($this->originalLanguageDefault) {
-      $GLOBALS['conf']['language_default'] = $this->originalLanguageDefault;
-    }
-
     // Close the CURL handler.
     $this->curlClose();
   }
diff --git a/core/modules/system/language.api.php b/core/modules/system/language.api.php
index 9f28619a2d389eb769db7f07b46eed705c1b15c7..f1cd519d08c676be859bfef16ed2505a718ceca0 100644
--- a/core/modules/system/language.api.php
+++ b/core/modules/system/language.api.php
@@ -62,22 +62,22 @@ function hook_language_switch_links_alter(array &$links, $type, $path) {
 }
 
 /**
- * Allow modules to define their own language types.
+ * Define language types.
  *
  * @return
- *   An associative array of language type definitions.
+ *   An associative array of language type definitions. The keys are the
+ *   identifiers, which are also used as names for global variables representing
+ *   the types in the bootstrap phase. The values are associative arrays that
+ *   may contain the following elements:
+ *   - name: The human-readable language type identifier.
+ *   - description: A description of the language type.
+ *   - fixed: A fixed array of language negotiation method identifiers to use to
+ *     initialize this language. Defining this key makes the language type
+ *     non-configurable, so it will always use the specified methods in the
+ *     given priority order. Omit to make the language type configurable.
  *
- *   Each language type has an identifier key which is used as the name for the
- *   global variable corresponding to the language type in the bootstrap phase.
- *
- *   The language type definition is an associative array that may contain the
- *   following key-value pairs:
- *   - "name": The human-readable language type identifier.
- *   - "description": A description of the language type.
- *   - "fixed": A fixed array of language negotiation method identifiers to use
- *     to initialize this language. Defining this key makes the language type
- *     non-configurable and will always use the specified methods in the given
- *     priority order.
+ * @see hook_language_types_info_alter()
+ * @ingroup language_negotiation
  */
 function hook_language_types_info() {
   return array(
@@ -94,10 +94,11 @@ function hook_language_types_info() {
 /**
  * Perform alterations on language types.
  *
- * @see hook_language_types_info().
- *
  * @param $language_types
  *   Array of language type definitions.
+ *
+ * @see hook_language_types_info()
+ * @ingroup language_negotiation
  */
 function hook_language_types_info_alter(array &$language_types) {
   if (isset($language_types['custom_language_type'])) {
@@ -106,31 +107,35 @@ function hook_language_types_info_alter(array &$language_types) {
 }
 
 /**
- * Allow modules to define their own language negotiation methods.
+ * Define language negotiation methods.
  *
  * @return
- *   An array of language negotiation method definitions. Each method has an
- *   identifier key. The language negotiation method definition is an indexed
- *   array that may contain the following key-value pairs:
- *   - "types": An array of allowed language types. If a language negotiation
+ *   An associative array of language negotiation method definitions. The keys
+ *   are method identifiers, and the values are associative arrays definining
+ *   each method, with the following elements:
+ *   - types: An array of allowed language types. If a language negotiation
  *     method does not specify which language types it should be used with, it
  *     will be available for all the configurable language types.
- *   - "callbacks": An array of functions that will be called to perform various
- *     tasks. Possible key-value pairs are:
- *     - "negotiation": Required. The callback that will determine the language
- *       value.
- *     - "language_switch": The callback that will determine the language
- *       switch links associated to the current language method.
- *     - "url_rewrite": The callback that will provide URL rewriting.
- *   - "file": A file that will be included before the callback is invoked; this
- *     allows callback functions to be in separate files.
- *   - "weight": The default weight the language negotiation method has.
- *   - "name": A human-readable identifier.
- *   - "description": A description of the language negotiation method.
- *   - "config": An internal path pointing to the language negotiation method
- *     configuration page.
- *   - "cache": The value Drupal's page cache should be set to for the current
- *     language negotiation method to be invoked.
+ *   - callbacks: An associative array of functions that will be called to
+ *     perform various tasks. Possible elements are:
+ *     - negotiation: (required) Name of the callback function that determines
+ *       the language value.
+ *     - language_switch: (optional) Name of the callback function that
+ *       determines links for a language switcher block associated with this
+ *       method. See language_switcher_url() for an example.
+ *     - url_rewrite: (optional) Name of the callback function that provides URL
+ *       rewriting, if needed by this method.
+ *   - file: The file where callback functions are defined (this file will be
+ *     included before the callbacks are invoked).
+ *   - weight: The default weight of the method.
+ *   - name: The translated human-readable name for the method.
+ *   - description: A translated longer description of the method.
+ *   - config: An internal path pointing to the method's configuration page.
+ *   - cache: The value Drupal's page cache should be set to for the current
+ *     method to be invoked.
+ *
+ * @see hook_language_negotiation_info_alter()
+ * @ingroup language_negotiation
  */
 function hook_language_negotiation_info() {
   return array(
@@ -155,6 +160,9 @@ function hook_language_negotiation_info() {
  *
  * @param $negotiation_info
  *   Array of language negotiation method definitions.
+ *
+ * @see hook_language_negotiation_info()
+ * @ingroup language_negotiation
  */
 function hook_language_negotiation_info_alter(array &$negotiation_info) {
   if (isset($negotiation_info['custom_language_method'])) {
diff --git a/core/modules/system/tests/actions.test b/core/modules/system/lib/Drupal/system/Tests/Actions/ConfigurationTest.php
similarity index 56%
rename from core/modules/system/tests/actions.test
rename to core/modules/system/lib/Drupal/system/Tests/Actions/ConfigurationTest.php
index 469b16e13d51f5bc321cfe7bfc03907b06ecbc8f..1a31fb4ff1cd26df5fd9317385f2f8aa8da9ba53 100644
--- a/core/modules/system/tests/actions.test
+++ b/core/modules/system/lib/Drupal/system/Tests/Actions/ConfigurationTest.php
@@ -1,8 +1,18 @@
 <?php
 
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Actions\ConfigurationTest.
+ */
+
+namespace Drupal\system\Tests\Actions;
+
 use Drupal\simpletest\WebTestBase;
 
-class ActionsConfigurationTestCase extends WebTestBase {
+/**
+ * Actions configuration.
+ */
+class ConfigurationTest extends WebTestBase {
   public static function getInfo() {
     return array(
       'name' => 'Actions configuration',
@@ -64,66 +74,3 @@ function testActionConfiguration() {
     $this->assertFalse($exists, t('Make sure the action is gone from the database after being deleted.'));
   }
 }
-
-/**
- * Test actions executing in a potential loop, and make sure they abort properly.
- */
-class ActionLoopTestCase extends WebTestBase {
-  protected $aid;
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Actions executing in a potentially infinite loop',
-      'description' => 'Tests actions executing in a loop, and makes sure they abort properly.',
-      'group' => 'Actions',
-    );
-  }
-
-  function setUp() {
-    parent::setUp('dblog', 'actions_loop_test');
-  }
-
-  /**
-   * Set up a loop with 3 - 12 recursions, and see if it aborts properly.
-   */
-  function testActionLoop() {
-    $user = $this->drupalCreateUser(array('administer actions'));
-    $this->drupalLogin($user);
-
-    $info = actions_loop_test_action_info();
-    $this->aid = actions_save('actions_loop_test_log', $info['actions_loop_test_log']['type'], array(), $info['actions_loop_test_log']['label']);
-
-    // Delete any existing watchdog messages to clear the plethora of
-    // "Action added" messages from when Drupal was installed.
-    db_delete('watchdog')->execute();
-    // To prevent this test from failing when xdebug is enabled, the maximum
-    // recursion level should be kept low enough to prevent the xdebug
-    // infinite recursion protection mechanism from aborting the request.
-    // See http://drupal.org/node/587634.
-    variable_set('actions_max_stack', mt_rand(3, 12));
-    $this->triggerActions();
-  }
-
-  /**
-   * Create an infinite loop by causing a watchdog message to be set,
-   * which causes the actions to be triggered again, up to actions_max_stack
-   * times.
-   */
-  protected function triggerActions() {
-    $this->drupalGet('<front>', array('query' => array('trigger_actions_on_watchdog' => $this->aid)));
-    $expected = array();
-    $expected[] = 'Triggering action loop';
-    for ($i = 1; $i <= variable_get('actions_max_stack', 35); $i++) {
-      $expected[] = "Test log #$i";
-    }
-    $expected[] = 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.';
-
-    $result = db_query("SELECT message FROM {watchdog} WHERE type = 'actions_loop_test' OR type = 'actions' ORDER BY wid");
-    $loop_started = FALSE;
-    foreach ($result as $row) {
-      $expected_message = array_shift($expected);
-      $this->assertEqual($row->message, $expected_message, t('Expected message %expected, got %message.', array('%expected' => $expected_message, '%message' => $row->message)));
-    }
-    $this->assertTrue(empty($expected), t('All expected messages found.'));
-  }
-}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Actions/LoopTest.php b/core/modules/system/lib/Drupal/system/Tests/Actions/LoopTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..97667c0cea5db861f8c8d75d254973d1b0424e36
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Actions/LoopTest.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Actions\LoopTest.
+ */
+
+namespace Drupal\system\Tests\Actions;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Test actions executing in a potential loop, and make sure they abort properly.
+ */
+class LoopTest extends WebTestBase {
+  protected $aid;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Actions executing in a potentially infinite loop',
+      'description' => 'Tests actions executing in a loop, and makes sure they abort properly.',
+      'group' => 'Actions',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('dblog', 'actions_loop_test');
+  }
+
+  /**
+   * Set up a loop with 3 - 12 recursions, and see if it aborts properly.
+   */
+  function testActionLoop() {
+    $user = $this->drupalCreateUser(array('administer actions'));
+    $this->drupalLogin($user);
+
+    $info = actions_loop_test_action_info();
+    $this->aid = actions_save('actions_loop_test_log', $info['actions_loop_test_log']['type'], array(), $info['actions_loop_test_log']['label']);
+
+    // Delete any existing watchdog messages to clear the plethora of
+    // "Action added" messages from when Drupal was installed.
+    db_delete('watchdog')->execute();
+    // To prevent this test from failing when xdebug is enabled, the maximum
+    // recursion level should be kept low enough to prevent the xdebug
+    // infinite recursion protection mechanism from aborting the request.
+    // See http://drupal.org/node/587634.
+    variable_set('actions_max_stack', mt_rand(3, 12));
+    $this->triggerActions();
+  }
+
+  /**
+   * Create an infinite loop by causing a watchdog message to be set,
+   * which causes the actions to be triggered again, up to actions_max_stack
+   * times.
+   */
+  protected function triggerActions() {
+    $this->drupalGet('<front>', array('query' => array('trigger_actions_on_watchdog' => $this->aid)));
+    $expected = array();
+    $expected[] = 'Triggering action loop';
+    for ($i = 1; $i <= variable_get('actions_max_stack', 35); $i++) {
+      $expected[] = "Test log #$i";
+    }
+    $expected[] = 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.';
+
+    $result = db_query("SELECT message FROM {watchdog} WHERE type = 'actions_loop_test' OR type = 'actions' ORDER BY wid");
+    $loop_started = FALSE;
+    foreach ($result as $row) {
+      $expected_message = array_shift($expected);
+      $this->assertEqual($row->message, $expected_message, t('Expected message %expected, got %message.', array('%expected' => $expected_message, '%message' => $row->message)));
+    }
+    $this->assertTrue(empty($expected), t('All expected messages found.'));
+  }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Cache/NullBackendTest.php b/core/modules/system/lib/Drupal/system/Tests/Cache/NullBackendTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5a42e5defb622d8869f37e2c96769e4155c83a35
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Cache/NullBackendTest.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Cache\NullBackendTest.
+ */
+
+namespace Drupal\system\Tests\Cache;
+
+use Drupal\Core\Cache\NullBackend;
+use Drupal\simpletest\UnitTestBase;
+
+/**
+ * Tests the cache NullBackend.
+ */
+class NullBackendTest extends UnitTestBase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Cache NullBackend test',
+      'description' => 'Tests the cache NullBackend.',
+      'group' => 'Cache',
+    );
+  }
+
+  /**
+   * Tests that the NullBackend does not actually store variables.
+   */
+  function testNullBackend() {
+    $null_cache = new NullBackend('test');
+
+    $key = $this->randomName();
+    $value = $this->randomName();
+
+    $null_cache->set($key, $value);
+    $this->assertTrue($null_cache->isEmpty());
+    $this->assertFalse($null_cache->get($key));
+  }
+}
diff --git a/core/modules/system/tests/mail.test b/core/modules/system/lib/Drupal/system/Tests/Common/HtmlToTextTest.php
similarity index 86%
rename from core/modules/system/tests/mail.test
rename to core/modules/system/lib/Drupal/system/Tests/Common/HtmlToTextTest.php
index 38c6dc8c0af2d94d654a8daff89f80ba2505d661..a30c3c106dcbb429b3d11c30389aa006b3d8f147 100644
--- a/core/modules/system/tests/mail.test
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/HtmlToTextTest.php
@@ -2,97 +2,17 @@
 
 /**
  * @file
- * Test the Drupal mailing system.
+ * Definition of Drupal\system\Tests\Common\HtmlToTextTest.
  */
 
-use Drupal\Core\Mail\MailInterface;
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Defines a mail class used for testing.
- */
-class MailTestCase extends WebTestBase implements MailInterface {
-  /**
-   * The most recent message that was sent through the test case.
-   *
-   * We take advantage here of the fact that static variables are shared among
-   * all instance of the same class.
-   */
-  private static $sent_message;
-
-  public static function getInfo() {
-    return array(
-      'name' => 'Mail system',
-      'description' => 'Performs tests on the pluggable mailing framework.',
-      'group' => 'Mail',
-    );
-  }
-
-  function setUp() {
-    parent::setUp(array('simpletest'));
-
-    // Set MailTestCase (i.e. this class) as the SMTP library
-    variable_set('mail_system', array('default-system' => 'MailTestCase'));
-  }
-
-  /**
-   * Assert that the pluggable mail system is functional.
-   */
-  public function testPluggableFramework() {
-    global $language_interface;
-
-    // Use MailTestCase for sending a message.
-    $message = drupal_mail('simpletest', 'mail_test', 'testing@example.com', $language_interface);
+namespace Drupal\system\Tests\Common;
 
-    // Assert whether the message was sent through the send function.
-    $this->assertEqual(self::$sent_message['to'], 'testing@example.com', t('Pluggable mail system is extendable.'));
-  }
-
-  /**
-   * Test that message sending may be canceled.
-   *
-   * @see simpletest_mail_alter()
-   */
-  public function testCancelMessage() {
-    global $language;
-
-    // Reset the class variable holding a copy of the last sent message.
-    self::$sent_message = NULL;
-
-    // Send a test message that simpletest_mail_alter should cancel.
-    $message = drupal_mail('simpletest', 'cancel_test', 'cancel@example.com', $language);
-
-    // Assert that the message was not actually sent.
-    $this->assertNull(self::$sent_message, 'Message was canceled.');
-  }
-
-  /**
-   * Concatenate and wrap the e-mail body for plain-text mails.
-   *
-   * @see Drupal\Core\Mail\PhpMail
-   */
-  public function format(array $message) {
-    // Join the body array into one string.
-    $message['body'] = implode("\n\n", $message['body']);
-    // Convert any HTML to plain-text.
-    $message['body'] = drupal_html_to_text($message['body']);
-    // Wrap the mail body for sending.
-    $message['body'] = drupal_wrap_mail($message['body']);
-    return $message;
-  }
-
-  /**
-   * Send function that is called through the mail system.
-   */
-  public function mail(array $message) {
-    self::$sent_message = $message;
-  }
-}
+use Drupal\simpletest\WebTestBase;
 
 /**
- * Unit tests for drupal_html_to_text().
+ * Tests for drupal_html_to_text().
  */
-class DrupalHtmlToTextTestCase extends WebTestBase {
+class HtmlToTextTest extends WebTestBase {
   public static function getInfo() {
     return array(
       'name'  => 'HTML to text conversion',
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/MailTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/MailTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf33f487e778df734ea452cc3994e4cd53baca08
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/MailTest.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\system\Tests\Common\MailTest.
+ */
+
+namespace Drupal\system\Tests\Common;
+
+use Drupal\Core\Mail\MailInterface;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Defines a mail class used for testing.
+ */
+class MailTest extends WebTestBase implements MailInterface {
+  /**
+   * The most recent message that was sent through the test case.
+   *
+   * We take advantage here of the fact that static variables are shared among
+   * all instance of the same class.
+   */
+  private static $sent_message;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Mail system',
+      'description' => 'Performs tests on the pluggable mailing framework.',
+      'group' => 'Mail',
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('simpletest'));
+
+    // Set MailTestCase (i.e. this class) as the SMTP library
+    variable_set('mail_system', array('default-system' => 'Drupal\system\Tests\Common\MailTest'));
+  }
+
+  /**
+   * Assert that the pluggable mail system is functional.
+   */
+  public function testPluggableFramework() {
+    global $language_interface;
+
+    // Use MailTestCase for sending a message.
+    $message = drupal_mail('simpletest', 'mail_test', 'testing@example.com', $language_interface);
+
+    // Assert whether the message was sent through the send function.
+    $this->assertEqual(self::$sent_message['to'], 'testing@example.com', t('Pluggable mail system is extendable.'));
+  }
+
+  /**
+   * Test that message sending may be canceled.
+   *
+   * @see simpletest_mail_alter()
+   */
+  public function testCancelMessage() {
+    global $language;
+
+    // Reset the class variable holding a copy of the last sent message.
+    self::$sent_message = NULL;
+
+    // Send a test message that simpletest_mail_alter should cancel.
+    $message = drupal_mail('simpletest', 'cancel_test', 'cancel@example.com', $language);
+
+    // Assert that the message was not actually sent.
+    $this->assertNull(self::$sent_message, 'Message was canceled.');
+  }
+
+  /**
+   * Concatenate and wrap the e-mail body for plain-text mails.
+   *
+   * @see Drupal\Core\Mail\PhpMail
+   */
+  public function format(array $message) {
+    // Join the body array into one string.
+    $message['body'] = implode("\n\n", $message['body']);
+    // Convert any HTML to plain-text.
+    $message['body'] = drupal_html_to_text($message['body']);
+    // Wrap the mail body for sending.
+    $message['body'] = drupal_wrap_mail($message['body']);
+    return $message;
+  }
+
+  /**
+   * Send function that is called through the mail system.
+   */
+  public function mail(array $message) {
+    self::$sent_message = $message;
+  }
+}
diff --git a/core/modules/system/system.info b/core/modules/system/system.info
index 8a11985572ec0cdbe7ddbca75588501b0b010bd6..1b87b854f238a2c7a22c74895d68c91be95d443c 100644
--- a/core/modules/system/system.info
+++ b/core/modules/system/system.info
@@ -8,7 +8,6 @@ required = TRUE
 configure = admin/config/system
 
 ; Tests in tests directory.
-files[] = tests/actions.test
 files[] = tests/ajax.test
 files[] = tests/batch.test
 files[] = tests/bootstrap.test
@@ -22,7 +21,6 @@ files[] = tests/form.test
 files[] = tests/image.test
 files[] = tests/installer.test
 files[] = tests/lock.test
-files[] = tests/mail.test
 files[] = tests/menu.test
 files[] = tests/module.test
 files[] = tests/pager.test