diff --git a/composer.json b/composer.json
index d94ede2..f58b5af 100644
--- a/composer.json
+++ b/composer.json
@@ -26,10 +26,10 @@
     "zendframework/zend-feed": "2.2.*"
   },
   "autoload": {
-    "psr-0": {
-      "Drupal\\Core": "core/lib/",
-      "Drupal\\Component": "core/lib/",
-      "Drupal\\Driver": "drivers/lib/"
+    "psr-4": {
+      "Drupal\\Core\\": "core/lib/Drupal/Core",
+      "Drupal\\Component\\": "core/lib/Drupal/Component",
+      "Drupal\\Driver\\": "drivers/lib/Drupal/Driver"
     },
     "files": [
       "core/lib/Drupal.php"
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index d6e6aee..83aa307 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -2124,7 +2124,10 @@ function drupal_classloader($class_loader = NULL) {
  */
 function drupal_classloader_register($name, $path) {
   $loader = drupal_classloader();
-  $loader->add('Drupal\\' . $name, DRUPAL_ROOT . '/' . $path . '/lib');
+  $loader->addPsr4('Drupal\\' . $name . '\\', array(
+    DRUPAL_ROOT . '/' . $path . '/lib/Drupal/' . $name,
+    DRUPAL_ROOT . '/' . $path . '/src',
+  ));
 }
 
 /**
diff --git a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
index b04e39d..b1c0dbf 100644
--- a/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
+++ b/core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotatedClassDiscovery.php
@@ -99,7 +99,6 @@ public function getDefinitions() {
     // Search for classes within all PSR-0 namespace locations.
     foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
       foreach ($dirs as $dir) {
-        $dir .= DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $namespace);
         if (file_exists($dir)) {
           $iterator = new \RecursiveIteratorIterator(
             new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 8812cab..e6f9385 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -218,7 +218,7 @@ public function discoverServiceProviders() {
       $this->moduleList = isset($extensions['module']) ? $extensions['module'] : array();
     }
     $module_filenames = $this->getModuleFileNames();
-    $this->registerNamespaces($this->getModuleNamespaces($module_filenames));
+    $this->registerNamespacesPsr4($this->getModuleNamespacesPsr4($module_filenames));
 
     // Load each module's serviceProvider class.
     foreach ($this->moduleList as $module => $weight) {
@@ -249,7 +249,6 @@ public function discoverServiceProviders() {
     return $serviceProviders;
   }
 
-
   /**
    * {@inheritdoc}
    */
@@ -409,8 +408,8 @@ protected function initializeContainer() {
       // All namespaces must be registered before we attempt to use any service
       // from the container.
       $container_modules = $this->container->getParameter('container.modules');
-      $namespaces_before = $this->classLoader->getPrefixes();
-      $this->registerNamespaces($this->container->getParameter('container.namespaces'));
+      $namespaces_before = $this->classLoader->getPrefixesPsr4();
+      $this->registerNamespacesPsr4($this->container->getParameter('container.namespaces'));
 
       // If 'container.modules' is wrong, the container must be rebuilt.
       if (!isset($this->moduleList)) {
@@ -423,9 +422,9 @@ protected function initializeContainer() {
         // registerNamespaces() performs a merge rather than replace, so to
         // effectively remove erroneous registrations, we must replace them with
         // empty arrays.
-        $namespaces_after = $this->classLoader->getPrefixes();
+        $namespaces_after = $this->classLoader->getPrefixesPsr4();
         $namespaces_before += array_fill_keys(array_diff(array_keys($namespaces_after), array_keys($namespaces_before)), array());
-        $this->registerNamespaces($namespaces_before);
+        $this->registerNamespacesPsr4($namespaces_before);
       }
     }
 
@@ -501,14 +500,15 @@ protected function buildContainer() {
     $container->setParameter('container.modules', $this->getModulesParameter());
 
     // Get a list of namespaces and put it onto the container.
-    $namespaces = $this->getModuleNamespaces($this->getModuleFileNames());
+    $namespaces = $this->getModuleNamespacesPsr4($this->getModuleFileNames());
     // Add all components in \Drupal\Core and \Drupal\Component that have a
     // Plugin directory.
     foreach (array('Core', 'Component') as $parent_directory) {
       $path = DRUPAL_ROOT . '/core/lib/Drupal/' . $parent_directory;
+      $parent_namespace = 'Drupal\\' . $parent_directory;
       foreach (new \DirectoryIterator($path) as $component) {
         if (!$component->isDot() && $component->isDir() && is_dir($component->getPathname() . '/Plugin')) {
-          $namespaces['Drupal\\' . $parent_directory . '\\' . $component->getFilename()] = DRUPAL_ROOT . '/core/lib';
+          $namespaces[$parent_namespace . '\\' . $component->getFilename()] = $path . '/' . $component->getFilename();
         }
       }
     }
@@ -681,7 +681,11 @@ protected function getModulesParameter() {
   }
 
   /**
-   * Returns the file name for each enabled module.
+   * Gets the file name for each enabled module.
+   *
+   * @return array
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
    */
   protected function getModuleFileNames() {
     $filenames = array();
@@ -694,18 +698,67 @@ protected function getModuleFileNames() {
   }
 
   /**
-   * Gets the namespaces of each enabled module.
+   * Gets the PSR-4 base directories for module namespaces.
+   *
+   * @param array $module_file_names
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
+   *
+   * @return array
+   *   Array where each key is a module namespace like 'Drupal\system', and each
+   *   value is an array of PSR-4 base directories associated with the module
+   *   namespace.
    */
-  protected function getModuleNamespaces($moduleFileNames) {
+  protected function getModuleNamespacesPsr4($module_file_names) {
     $namespaces = array();
-    foreach ($moduleFileNames as $module => $filename) {
+    foreach ($module_file_names as $module => $filename) {
+      // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+      $namespaces["Drupal\\$module"][] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib/Drupal/' . $module;
+      $namespaces["Drupal\\$module"][] = DRUPAL_ROOT . '/' . dirname($filename) . '/src';
+    }
+    return $namespaces;
+  }
+
+  /**
+   * Gets the PSR-0 base directories for module namespaces.
+   *
+   * @param array $module_file_names
+   *   Array where each key is a module name, and each value is a path to the
+   *   respective *.module or *.profile file.
+   *
+   * @return array
+   *   Array where each key is a module namespace like 'Drupal\system', and each
+   *   value is a PSR-0 base directory associated with the module namespace.
+   */
+  protected function getModuleNamespaces($module_file_names) {
+    $namespaces = array();
+    foreach ($module_file_names as $module => $filename) {
       $namespaces["Drupal\\$module"] = DRUPAL_ROOT . '/' . dirname($filename) . '/lib';
     }
     return $namespaces;
   }
 
   /**
-   * Registers a list of namespaces.
+   * Registers a list of namespaces with PSR-4 directories for class loading.
+   *
+   * @param array $namespaces
+   *   Array where each key is a namespace like 'Drupal\system', and each value
+   *   is either a PSR-4 base directory, or an array of PSR-4 base directories
+   *   associated with this namespace.
+   */
+  protected function registerNamespacesPsr4(array $namespaces = array()) {
+    foreach ($namespaces as $prefix => $paths) {
+      $this->classLoader->addPsr4($prefix . '\\', $paths);
+    }
+  }
+
+  /**
+   * Registers a list of namespaces with PSR-0 directories for class loading.
+   *
+   * @param array $namespaces
+   *   Array where each key is a namespace like 'Drupal\system', and each value
+   *   is either a PSR-0 base directory, or an array of PSR-0 base directories
+   *   associated with this namespace.
    */
   protected function registerNamespaces(array $namespaces = array()) {
     foreach ($namespaces as $prefix => $path) {
diff --git a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
index 02e96d1..f9eb453 100644
--- a/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
+++ b/core/lib/Drupal/Core/Plugin/Discovery/AnnotatedClassDiscovery.php
@@ -16,17 +16,23 @@
 class AnnotatedClassDiscovery extends ComponentAnnotatedClassDiscovery {
 
   /**
-   * The subdirectory within a namespace to look for plugins.
+   * A suffix to append to each PSR-4 directory associated with a base
+   * namespace, to form the directories where plugins are found.
    *
-   * If the plugins are in the top level of the namespace and not within a
-   * subdirectory, set this to an empty string.
+   * @var string
+   */
+  protected $directorySuffix = '';
+
+  /**
+   * A suffix to append to each base namespace, to obtain the namespaces where
+   * plugins are found.
    *
    * @var string
    */
-  protected $subdir = '';
+  protected $namespaceSuffix = '';
 
   /**
-   * An object containing the namespaces to look for plugin implementations.
+   * A list of base namespaces with their PSR-4 directories.
    *
    * @var \Traversable
    */
@@ -48,7 +54,13 @@ class AnnotatedClassDiscovery extends ComponentAnnotatedClassDiscovery {
    */
   function __construct($subdir, \Traversable $root_namespaces, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin') {
     if ($subdir) {
-      $this->subdir = str_replace('/', '\\', $subdir);
+      // Prepend a directory separator to $subdir,
+      // if it does not already have one.
+      if ('/' !== $subdir[0]) {
+        $subdir = '/' . $subdir;
+      }
+      $this->directorySuffix = $subdir;
+      $this->namespaceSuffix = str_replace('/', '\\', $subdir);
     }
     $this->rootNamespacesIterator = $root_namespaces;
     $plugin_namespaces = array();
@@ -104,11 +116,28 @@ protected function getProviderFromNamespace($namespace) {
    */
   protected function getPluginNamespaces() {
     $plugin_namespaces = array();
-    foreach ($this->rootNamespacesIterator as $namespace => $dir) {
-      if ($this->subdir) {
-        $namespace .= "\\{$this->subdir}";
+    if ($this->namespaceSuffix) {
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        // Append the namespace suffix to the base namespace, to obtain the
+        // plugin namespace. E.g. 'Drupal\Views' may become
+        // 'Drupal\Views\Plugin\Block'.
+        $namespace .= $this->namespaceSuffix;
+        foreach ((array) $dirs as $dir) {
+          // Append the directory suffix to the PSR-4 base directory, to obtain
+          // the directory where plugins are found.
+          // E.g. DRUPAL_ROOT . '/core/modules/views/src' may become
+          // DRUPAL_ROOT . '/core/modules/views/src/Plugin/Block'.
+          $plugin_namespaces[$namespace][] = $dir . $this->directorySuffix;
+        }
+      }
+    }
+    else {
+      // Both the namespace suffix and the directory suffix are empty,
+      // so the plugin namespaces and directories are the same as the base
+      // directories.
+      foreach ($this->rootNamespacesIterator as $namespace => $dirs) {
+        $plugin_namespaces[$namespace] = (array) $dirs;
       }
-      $plugin_namespaces[$namespace] = array($dir);
     }
 
     return $plugin_namespaces;
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
index 0d15565..7cb9862 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateActionConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_action_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ActionSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ActionSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
index 4a13937..42b8633 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateAggregatorConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_aggregator_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6AggregatorSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6AggregatorSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
index 228b537..5a5fbd4 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateBookConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_book_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6BookSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6BookSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
index 3609266..21fc8b6 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateContactConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_contact_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ContactSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ContactSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
index 7bfda84..333f815 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateDblogConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_dblog_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6DblogSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6DblogSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
index d5647e3..9f02830 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFieldConfigsTest.php
@@ -33,7 +33,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_field_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FieldSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6FieldSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
index e35fff9..a09f17f 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateFileConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_file_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FileSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6FileSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
index 9c564f9..9ebecf1 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateForumConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_forum_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6ForumSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6ForumSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
index 4c633e1..c84aee2 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateLocaleConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_locale_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6LocaleSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6LocaleSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
index 7c26613..b14cfc5 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateMenuConfigsTest.php
@@ -40,7 +40,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_menu_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6MenuSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6MenuSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
index a20ba71..39edf2e 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateNodeConfigsTest.php
@@ -41,7 +41,7 @@ public function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_node_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6NodeSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6NodeSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
index f1b86bd..f34891d 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSearchConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_search_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SearchSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SearchSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
index 24115e3..00b3512 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSimpletestConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_simpletest_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SimpletestSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SimpletestSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
index 36c47d2..0be390e 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateStatisticsConfigsTest.php
@@ -41,7 +41,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_statistics_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6StatisticsSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6StatisticsSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, new MigrateMessage());
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
index 971cf87..86b3892 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateSyslogConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_syslog_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6SyslogSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6SyslogSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
index 4a6b7d6..07b60cc 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTaxonomyConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_taxonomy_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6TaxonomySettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6TaxonomySettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
index d33015e..8fc8489 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateTextConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_text_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6TextSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6TextSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
index c685edf..2296598 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUpdateConfigsTest.php
@@ -40,7 +40,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_update_settings');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UpdateSettings.php',
+      dirname(__DIR__) . '/Dump/Drupal6UpdateSettings.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
index 37211ea..6c41f5d 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserConfigsTest.php
@@ -33,7 +33,7 @@ protected function setUp() {
     parent::setUp();
     $migration = entity_load('migration', 'd6_user_mail');
     $dumps = array(
-      drupal_get_path('module', 'migrate_drupal') . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UserMail.php',
+      dirname(__DIR__) . '/Dump/Drupal6UserMail.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
index a70a207..46147f7 100644
--- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
+++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php
@@ -49,10 +49,9 @@ public function setUp() {
 
     /** @var \Drupal\migrate\entity\Migration $migration */
     $migration = entity_load('migration', 'd6_user_role');
-    $path = drupal_get_path('module', 'migrate_drupal');
     $dumps = array(
-      $path . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6UserRole.php',
-      $path . '/lib/Drupal/migrate_drupal/Tests/Dump/Drupal6FilterFormat.php',
+      dirname(__DIR__) . '/Dump/Drupal6UserRole.php',
+      dirname(__DIR__) . '/Dump/Drupal6FilterFormat.php',
     );
     $this->prepare($migration, $dumps);
     $executable = new MigrateExecutable($migration, $this);
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index adc5bda..dbf1dff 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -466,21 +466,25 @@ function simpletest_test_get_all($module = NULL) {
       }
       $classes = array();
       foreach ($all_data as $name => $data) {
-        // Build directory in which the test files would reside.
-        $tests_dir = DRUPAL_ROOT . '/' . $data->getPath() . '/lib/Drupal/' . $name . '/Tests';
+        $extension_dir = DRUPAL_ROOT . '/' . $data->getPath();
+
+        // Build directories in which the test files would reside.
+        $tests_dirs = array(
+          $extension_dir . '/lib/Drupal/' . $name . '/Tests',
+          $extension_dir . '/src/Tests',
+        );
+
+        $namespace = 'Drupal\\' . $name . '\Tests\\';
         // Scan it for test files if it exists.
-        if (is_dir($tests_dir)) {
-          $files = file_scan_directory($tests_dir, '/\.php$/');
-          if (!empty($files)) {
-            $basedir = DRUPAL_ROOT . '/' . $data->getPath() . '/lib/';
-            foreach ($files as $file) {
-              // Convert the file name into the namespaced class name.
-              $replacements = array(
-                '/' => '\\',
-                $basedir => '',
-                '.php' => '',
-              );
-              $classes[] = strtr($file->uri, $replacements);
+        foreach ($tests_dirs as $tests_dir) {
+          if (is_dir($tests_dir)) {
+            $files = file_scan_directory($tests_dir, '/\.php$/');
+            if (!empty($files)) {
+              $strlen = strlen($tests_dir) + 1;
+              // Convert the file names into the namespaced class names.
+              foreach ($files as $file) {
+                $classes[] = $namespace . str_replace('/', '\\', substr($file->uri, $strlen, -4));
+              }
             }
           }
         }
@@ -569,7 +573,10 @@ function simpletest_classloader_register() {
   foreach ($types as $type) {
     foreach ($extensions[$type] as $name => $uri) {
       drupal_classloader_register($name, dirname($uri));
-      $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($uri) . '/tests');
+      $classloader->addPsr4('Drupal\\' . $name . '\\Tests\\', array(
+        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/Drupal/' . $name . '/Tests',
+        DRUPAL_ROOT . '/' . dirname($uri) . '/tests/src',
+      ));
       // While being there, prime drupal_get_filename().
       drupal_get_filename($type, $name, $uri);
     }
diff --git a/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php b/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
index 49fcb02..afe2b52 100644
--- a/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
+++ b/core/modules/simpletest/tests/Drupal/simpletest/Tests/PhpUnitErrorTest.php
@@ -22,7 +22,14 @@ public static function getInfo() {
    * Test errors reported.
    */
   public function testPhpUnitXmlParsing() {
-    require_once __DIR__ . '/../../../../simpletest.module';
+    // This test class could be either in tests/Drupal/simpletest/Tests/, or in
+    // tests/src/, after the PSR-4 transition.
+    if (file_exists(__DIR__ . '/../../simpletest.module')) {
+      require_once __DIR__ . '/../../simpletest.module';
+    }
+    else {
+      require_once __DIR__ . '/../../../../simpletest.module';
+    }
     $phpunit_error_xml = __DIR__ . '/phpunit_error.xml';
     $res = simpletest_phpunit_xml_to_rows(1, $phpunit_error_xml);
     $this->assertEquals(count($res), 4, 'All testcases got extracted');
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
index d2fba9b..a2fa299 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/AnnotatedClassDiscoveryTest.php
@@ -57,7 +57,13 @@ public function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    $namespaces = new \ArrayObject(array(
+      'Drupal\plugin_test' => array(
+        // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test',
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/src',
+      ),
+    ));
     $this->discovery = new AnnotatedClassDiscovery('Plugin/plugin_test/fruit', $namespaces);
     $this->emptyDiscovery = new AnnotatedClassDiscovery('Plugin/non_existing_module/non_existing_plugin_type', $namespaces);
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
index d61c013..09358cc 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomAnnotationClassDiscoveryTest.php
@@ -41,7 +41,13 @@ protected function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $root_namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    $root_namespaces = new \ArrayObject(array(
+      'Drupal\plugin_test' => array(
+        // @todo Remove lib/Drupal/$module, once the switch to PSR-4 is complete.
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test',
+        DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/src',
+      ),
+    ));
 
     $this->discovery = new AnnotatedClassDiscovery('Plugin/plugin_test/custom_annotation', $root_namespaces, 'Drupal\plugin_test\Plugin\Annotation\PluginExample');
     $this->emptyDiscovery = new AnnotatedClassDiscovery('Plugin/non_existing_module/non_existing_plugin_type', $root_namespaces, 'Drupal\plugin_test\Plugin\Annotation\PluginExample');
diff --git a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
index 52d4fea..1071e77 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Plugin/Discovery/CustomDirectoryAnnotatedClassDiscoveryTest.php
@@ -70,7 +70,21 @@ protected function setUp() {
         'provider' => 'plugin_test',
       ),
     );
-    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test/lib'));
+    // Due to the transition from PSR-0 to PSR-4, plugin classes can be in
+    // either one of
+    // - core/modules/system/tests/modules/plugin_test/lib/Drupal/plugin_test/
+    // - core/modules/system/tests/modules/plugin_test/src/
+    // To avoid false positives with "Drupal\plugin_test\Drupal\plugin_test\..",
+    // only one of them can be registered.
+    // Note: This precaution is only needed if the plugin namespace is identical
+    // with the module namespace. Usually this is not the case, because every
+    // plugin namespace is like "Drupal\$module\Plugin\..".
+    // @todo Clean this up, once the transition to PSR-4 is complete.
+    $extension_dir = DRUPAL_ROOT . '/core/modules/system/tests/modules/plugin_test';
+    $base_directory = is_dir($extension_dir . '/lib/Drupal/plugin_test')
+      ? $extension_dir . '/lib/Drupal/plugin_test'
+      : $extension_dir . '/src';
+    $namespaces = new \ArrayObject(array('Drupal\plugin_test' => $base_directory));
     $this->discovery = new AnnotatedClassDiscovery('', $namespaces);
     $empty_namespaces = new \ArrayObject();
     $this->emptyDiscovery = new AnnotatedClassDiscovery('', $empty_namespaces);
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
index 3069aab..b6314b9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ScriptTest.php
@@ -30,10 +30,7 @@ public static function getInfo() {
    */
   public function setUp() {
     parent::setUp();
-    $path_parts = explode(DIRECTORY_SEPARATOR, __DIR__);
-    // This file is 8 levels below the Drupal root.
-    $root = implode(DIRECTORY_SEPARATOR, array_slice($path_parts, 0, -8));
-    chdir($root);
+    chdir(DRUPAL_ROOT);
   }
 
   /**
diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist
index aacbc2f..32b3384 100644
--- a/core/phpunit.xml.dist
+++ b/core/phpunit.xml.dist
@@ -18,7 +18,9 @@
       <!-- Exclude Drush tests. -->
       <exclude>./drush/tests</exclude>
       <!-- Exclude special-case files from config's test modules. -->
+      <!-- @todo Remove /lib/Drupal/config_test after the transition to PSR-4. -->
       <exclude>./modules/config/tests/config_test/lib/Drupal/config_test</exclude>
+      <exclude>./modules/config/tests/config_test/src</exclude>
     </testsuite>
   </testsuites>
   <!-- Filter for coverage reports. -->
diff --git a/core/scripts/switch-psr4.sh b/core/scripts/switch-psr4.sh
new file mode 100644
index 0000000..0eebbe5
--- /dev/null
+++ b/core/scripts/switch-psr4.sh
@@ -0,0 +1,319 @@
+#!/bin/php
+<?php
+
+namespace Drupal\Core\SwitchPsr4;
+
+/**
+ * @file
+ * Moves module-provided class files to their PSR-4 location.
+ *
+ * E.g.:
+ * core/modules/action/{lib/Drupal/action → src}/ActionAccessController.php
+ * core/modules/action/{lib/Drupal/action → src}/ActionAddFormController.php
+ */
+
+// Determine DRUPAL_ROOT.
+$dir = dirname(__FILE__);
+while (!defined('DRUPAL_ROOT')) {
+  if (is_dir($dir . '/core')) {
+    define('DRUPAL_ROOT', $dir);
+  }
+  $dir = dirname($dir);
+}
+
+// Run the script.
+run();
+
+/**
+ * Runs the script.
+ */
+function run() {
+  $cmd_arguments = $_SERVER['argv'];
+  // The first argument is the script name.
+  $scriptname = array_shift($cmd_arguments);
+  if (in_array('--help', $cmd_arguments)) {
+    print get_help_text($scriptname);
+  }
+  elseif (!empty($cmd_arguments)) {
+    // If one or more arguments are given, treat those arguments as directories,
+    // and process all modules found within these directories.
+    $directories = array();
+    foreach ($cmd_arguments as $arg) {
+      if ('-' === $arg{0}) {
+        // The only valid option is '--help'.
+        print "Invalid option '$arg'";
+        return;
+      }
+      if (!is_dir($arg)) {
+        print "The argument '$arg' is not a directory.";
+        continue;
+      }
+      $directories[] = $arg;
+    }
+    // Process all directories that were found in the argument list.
+    foreach ($directories as $dir) {
+      process_candidate_dir($dir);
+    }
+  }
+  else {
+    // If no arguments are given, process all modules and profiles in the core
+    // directories instead.
+    process_candidate_dir(DRUPAL_ROOT . '/core/modules');
+    process_candidate_dir(DRUPAL_ROOT . '/core/profiles');
+  }
+}
+
+/**
+ * @param string $scriptname
+ *
+ * @return string
+ *   Help text in case the "--help" option is present.
+ */
+function get_help_text($scriptname) {
+  $script = basename($scriptname);
+  return <<<EOF
+
+Move module class files from PSR-0 to PSR-4.
+
+E.g. the following files would be moved.
+  - core/modules/action/{lib/Drupal/action → src}/ActionListController.php
+  - core/modules/action/tests/{Drupal/action/Tests → src}/Menu/ActionLocalTasksTest.php
+
+Class files which are already in the PSR-4 path remain where they are.
+
+Warning: Classes with an underscore in the class name (after the last namespace
+separator) will end up in an incorrect location, and need to be fixed manually.
+Such class names are not allowed in Drupal coding standards, but they may still
+occur in some custom and contrib modules.
+
+The script takes any number of arguments which, if present, specify the
+directories to scan for modules and module class files.
+
+If no arguments are given, the following directories will be processed:
+  - core/modules/
+  - core/profiles/
+
+See:
+  - https://drupal.org/node/2083547 Drupal issue
+  - https://drupal.org/node/2156625 Documentation of PSR-4 in Drupal
+
+Usage:        {$script} [OPTIONS] [DIRECTORIES]
+Examples:     {$script}
+              {$script} core/modules/views
+              {$script} modules/contrib modules/custom
+              {$script} modules/contrib/devel
+
+Options:
+  --help      Display this help page and exit.
+
+
+EOF;
+}
+
+/**
+ * Scans all subdirectories of a given directory for Drupal extensions, and runs
+ * process_extension() for each one that it finds.
+ *
+ * @param string $dir
+ *   A directory whose subdirectories could contain Drupal extensions.
+ */
+function process_extensions_base_dir($dir) {
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      // do nothing
+    }
+    elseif ($fileinfo->isDir()) {
+      process_candidate_dir($fileinfo->getPathname());
+    }
+  }
+}
+
+/**
+ * Recursively scans a directory for Drupal extensions, and runs
+ * process_extension() for each one that it finds.
+ *
+ * @param string $dir
+ *   A directory that could be a Drupal extension directory.
+ */
+function process_candidate_dir($dir) {
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      // Ignore "." and "..".
+    }
+    elseif ($fileinfo->isDir()) {
+      // It's a directory.
+      switch ($fileinfo->getFilename()) {
+        case 'lib':
+        case 'src':
+          // Ignore these directory names.
+          continue;
+        default:
+          // Look for more extensions in subdirectories.
+          process_candidate_dir($fileinfo->getPathname());
+      }
+    }
+    else {
+      // It's a file.
+      if (preg_match('/^(.+).info.yml$/', $fileinfo->getFilename(), $m)) {
+        // It's a *.info.yml file, so we found an extension directory.
+        $extension_name = $m[1];
+      }
+    }
+  }
+  if (isset($extension_name)) {
+    process_extension($extension_name, $dir);
+    process_extension_phpunit($extension_name, $dir);
+  }
+}
+
+/**
+ * Process a Drupal extension (module, theme) in a directory.
+ *
+ * This will move all class files in this extension from
+ * lib/Drupal/$extension_name/$path to src/$path.
+ *
+ * @param string $name
+ *   Name of the extension.
+ * @param string $dir
+ *   Directory of the extension.
+ * @throws \Exception
+ */
+function process_extension($name, $dir) {
+
+  if (!is_dir($source = "$dir/lib/Drupal/$name")) {
+    // Nothing to do in this module.
+    return;
+  }
+
+  if (!is_dir($destination = "$dir/src")) {
+    mkdir($destination);
+  }
+
+  // Move class files two levels up.
+  move_directory_contents($source, $destination);
+
+  // Clean up.
+  require_dir_empty("$dir/lib/Drupal");
+  rmdir("$dir/lib/Drupal");
+}
+
+/**
+ * Process a Drupal extension (module, theme) in a directory.
+ *
+ * This will move all PHPUnit class files in this extension from
+ * tests/Drupal/$name/Tests/ to tests/src/.
+ *
+ * @param string $name
+ *   Name of the extension.
+ * @param string $dir
+ *   Directory of the extension.
+ */
+function process_extension_phpunit($name, $dir) {
+
+  if (!is_dir($source = "$dir/tests/Drupal/$name/Tests")) {
+    // Nothing to do in this module.
+    return;
+  }
+
+  if (!is_dir($dest = "$dir/tests/src")) {
+    mkdir($dest);
+  }
+
+  // Move class files two levels up.
+  move_directory_contents($source, $dest);
+
+  // Clean up.
+  require_dir_empty("$dir/tests/Drupal/$name");
+  rmdir("$dir/tests/Drupal/$name");
+  require_dir_empty("$dir/tests/Drupal");
+  rmdir("$dir/tests/Drupal");
+}
+
+/**
+ * Move directory contents from an existing source directory to an existing
+ * destination directory.
+ *
+ * @param string $source
+ *   An existing source directory.
+ * @param string $destination
+ *   An existing destination directory.
+ *
+ * @throws \Exception
+ */
+function move_directory_contents($source, $destination) {
+
+  if (!is_dir($source)) {
+    throw new \Exception("The source '$source' is not a directory.");
+  }
+
+  if (!is_dir($destination)) {
+    throw new \Exception("The destination '$destination' is not a directory.");
+  }
+
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($source) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      continue;
+    }
+    $dest_path = $destination . '/' . $fileinfo->getFilename();
+    if (!file_exists($dest_path)) {
+      rename($fileinfo->getPathname(), $dest_path);
+    }
+    elseif ($fileinfo->isFile()) {
+      throw new \Exception("Destination '$dest_path' already exists, cannot overwrite.");
+    }
+    elseif ($fileinfo->isDir()) {
+      if (!is_dir($dest_path)) {
+        throw new \Exception("Destination '$dest_path' is not a directory.");
+      }
+      move_directory_contents($fileinfo->getPathname(), $dest_path);
+    }
+  }
+
+  require_dir_empty($source);
+
+  rmdir($source);
+}
+
+/**
+ * Throws an exception if a directory is not empty.
+ *
+ * @param string $dir
+ *   Directory to check.
+ *
+ * @throws \Exception
+ */
+function require_dir_empty($dir) {
+  if (is_file($dir)) {
+    throw new \Exception("The path '$dir' is a file, when it should be a directory.");
+  }
+  if (!is_dir($dir)) {
+    throw new \Exception("The directory '$dir' does not exist.");
+  }
+  if (!is_readable($dir)) {
+    throw new \Exception("The directory '$dir' is not readable.");
+  }
+  /**
+   * @var \DirectoryIterator $fileinfo
+   */
+  foreach (new \DirectoryIterator($dir) as $fileinfo) {
+    if ($fileinfo->isDot()) {
+      continue;
+    }
+    $path = $fileinfo->getPathname();
+    if ($fileinfo->isFile()) {
+      throw new \Exception("File '$path' found in a directory that should be empty.");
+    }
+    elseif ($fileinfo->isDir()) {
+      throw new \Exception("Subdirectory '$path' found in a directory that should be empty.");
+    }
+  }
+}
diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php
index c69a52f..ccfe5f4 100644
--- a/core/tests/bootstrap.php
+++ b/core/tests/bootstrap.php
@@ -56,6 +56,8 @@ function drupal_phpunit_contrib_extension_directory_roots() {
  */
 function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $loader, $dirs) {
   foreach ($dirs as $extension => $dir) {
+    // Register PSR-0 test directories.
+    // @todo Remove this, when the transition to PSR-4 is complete.
     $lib_path = $dir . '/lib';
     if (is_dir($lib_path)) {
       $loader->add('Drupal\\' . $extension, $lib_path);
@@ -64,6 +66,13 @@ function drupal_phpunit_register_extension_dirs(Composer\Autoload\ClassLoader $l
     if (is_dir($tests_path)) {
       $loader->add('Drupal\\' . $extension, $tests_path);
     }
+    // Register PSR-4 test directories.
+    if (is_dir($dir . '/src')) {
+      $loader->addPsr4('Drupal\\' . $extension . '\\', $dir . '/src');
+    }
+    if (is_dir($dir . '/tests/src')) {
+      $loader->addPsr4('Drupal\\' . $extension . '\Tests\\', $dir . '/tests/src');
+    }
   }
 }
 
diff --git a/core/vendor/composer/autoload_namespaces.php b/core/vendor/composer/autoload_namespaces.php
index 80f7f7a..b6e4ab8 100644
--- a/core/vendor/composer/autoload_namespaces.php
+++ b/core/vendor/composer/autoload_namespaces.php
@@ -27,9 +27,6 @@
     'Psr\\Log\\' => array($vendorDir . '/psr/log'),
     'Gliph' => array($vendorDir . '/sdboyer/gliph/src'),
     'EasyRdf_' => array($vendorDir . '/easyrdf/easyrdf/lib'),
-    'Drupal\\Driver' => array($baseDir . '/drivers/lib'),
-    'Drupal\\Core' => array($baseDir . '/core/lib'),
-    'Drupal\\Component' => array($baseDir . '/core/lib'),
     'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib'),
     'Doctrine\\Common\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib'),
     'Doctrine\\Common\\Collections\\' => array($vendorDir . '/doctrine/collections/lib'),
diff --git a/core/vendor/composer/autoload_psr4.php b/core/vendor/composer/autoload_psr4.php
index e53db41..f832317 100644
--- a/core/vendor/composer/autoload_psr4.php
+++ b/core/vendor/composer/autoload_psr4.php
@@ -8,4 +8,7 @@
 return array(
     'GuzzleHttp\\Stream\\' => array($vendorDir . '/guzzlehttp/streams/src'),
     'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+    'Drupal\\Driver\\' => array($baseDir . '/drivers/lib/Drupal/Driver'),
+    'Drupal\\Core\\' => array($baseDir . '/core/lib/Drupal/Core'),
+    'Drupal\\Component\\' => array($baseDir . '/core/lib/Drupal/Component'),
 );
