diff --git a/core/core.services.yml b/core/core.services.yml
index a960712..6023afb 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1242,6 +1242,18 @@ services:
     class: Drupal\Core\StreamWrapper\TemporaryStream
     tags:
       - { name: stream_wrapper, scheme: temporary }
+  stream_wrapper.module:
+    class: Drupal\Core\StreamWrapper\ModuleStream
+    tags:
+      - { name: stream_wrapper, scheme: module }
+  stream_wrapper.theme:
+    class: Drupal\Core\StreamWrapper\ThemeStream
+    tags:
+      - { name: stream_wrapper, scheme: theme }
+  stream_wrapper.profile:
+    class: Drupal\Core\StreamWrapper\ProfileStream
+    tags:
+      - { name: stream_wrapper, scheme: profile }
   kernel_destruct_subscriber:
     class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber
     tags:
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 86e12c7..92510ab 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -240,14 +240,18 @@ function drupal_get_filename($type, $name, $filename = NULL) {
 /**
  * Returns the path to a system item (module, theme, etc.).
  *
- * @param $type
+ * This function should only be used when including a file containing PHP code;
+ * the 'module://', 'profile://' and 'theme://' stream wrappers should be used
+ * for other use cases.
+ *
+ * @param string $type
  *   The type of the item; one of 'core', 'profile', 'module', 'theme', or
  *   'theme_engine'.
  * @param $name
  *   The name of the item for which the path is requested. Ignored for
  *   $type 'core'.
  *
- * @return
+ * @return string
  *   The path to the requested item or an empty string if the item is not found.
  */
 function drupal_get_path($type, $name) {
diff --git a/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php
new file mode 100644
index 0000000..13cdf21
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ExtensionStreamBase.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\StreamWrapper\ExtensionStreamBase.
+ */
+
+namespace Drupal\Core\StreamWrapper;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Defines a base stream wrapper implementation.
+ *
+ * ExtensionStreamBase is a read-only Drupal stream wrapper base class for
+ * system files located in extensions: modules, themes and installed profile.
+ */
+abstract class ExtensionStreamBase extends LocalReadOnlyStream {
+
+  // @todo Move this in \Drupal\Core\StreamWrapper\LocalStream in Drupal 9.0.x.
+  use StringTranslationTrait;
+
+  /**
+   * The current request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getType() {
+    return StreamWrapperInterface::LOCAL | StreamWrapperInterface::READ;
+  }
+
+  /**
+   * Gets the module, theme, or profile name of the current URI.
+   *
+   * This will return the name of the module, theme or profile e.g.
+   * @code SystemStream::getOwnerName('module://foo') @endcode and @code
+   * SystemStream::getOwnerName('module://foo/')@endcode will both return @code
+   * 'foo'@endcode
+   *
+   * @return string
+   *   The extension name.
+   *
+   * @throws \InvalidArgumentException
+   *   In case of a malformed uri.
+   */
+  protected function getOwnerName() {
+    $uri_parts = explode('://', $this->uri, 2);
+    if (count($uri_parts) === 1) {
+      // The delimiter ('://') was not found in $uri, malformed $uri passed.
+      throw new \InvalidArgumentException("Malformed uri parameter passed: {$this->uri}");
+    }
+
+    // Remove the trailing filename from the path.
+    $length = strpos($uri_parts[1], '/');
+    return ($length === FALSE) ? $uri_parts[1] : substr($uri_parts[1], 0, $length);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTarget($uri = NULL) {
+    if ($target = strstr(parent::getTarget($uri), '/')) {
+      return trim($target, '/');
+    }
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExternalUrl() {
+    $dir = $this->getDirectoryPath();
+    if (empty($dir)) {
+      throw new \InvalidArgumentException("Extension directory for {$this->uri} does not exist.");
+    }
+    $path = rtrim(base_path() . $dir . '/' . $this->getTarget(), '/');
+    return $this->getRequest()->getUriForPath($path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dirname($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+
+    list($scheme) = explode('://', $uri, 2);
+    $dirname = dirname($this->getTarget($uri));
+    $dirname = $dirname !== '.' ? rtrim("/$dirname", '/') : '';
+
+    return "$scheme://{$this->getOwnerName()}{$dirname}";
+  }
+
+  /**
+   * Returns the current request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request
+   *   The current request object.
+   */
+  protected function getRequest() {
+    if (!isset($this->request)) {
+      $this->request = \Drupal::service('request_stack')->getCurrentRequest();
+    }
+    return $this->request;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php
index ad4636c..f37aa25 100644
--- a/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php
+++ b/core/lib/Drupal/Core/StreamWrapper/LocalReadOnlyStream.php
@@ -47,15 +47,7 @@ public function stream_open($uri, $mode, $options, &$opened_path) {
       }
       return FALSE;
     }
-
-    $this->uri = $uri;
-    $path = $this->getLocalPath();
-    $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
-    if ($this->handle !== FALSE && ($options & STREAM_USE_PATH)) {
-      $opened_path = $path;
-    }
-
-    return (bool) $this->handle;
+    return parent::stream_open($uri, $mode, $options, $opened_path);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php
index 82f4427..87c2ce2 100644
--- a/core/lib/Drupal/Core/StreamWrapper/LocalStream.php
+++ b/core/lib/Drupal/Core/StreamWrapper/LocalStream.php
@@ -15,10 +15,13 @@
  * "sites/default/files/example.txt" and then PHP filesystem functions are
  * invoked.
  *
- * Drupal\Core\StreamWrapper\LocalStream implementations need to implement at least the
- * getDirectoryPath() and getExternalUrl() methods.
+ * \Drupal\Core\StreamWrapper\LocalStream implementations need to implement at
+ * least the getDirectoryPath() and getExternalUrl() methods.
  */
 abstract class LocalStream implements StreamWrapperInterface {
+
+  use LocalStreamTrait;
+
   /**
    * Stream context resource.
    *
@@ -52,12 +55,10 @@ public static function getType() {
   /**
    * Gets the path that the wrapper is responsible for.
    *
-   * @todo Review this method name in D8 per https://www.drupal.org/node/701358.
-   *
    * @return string
    *   String specifying the path.
    */
-  abstract function getDirectoryPath();
+  protected abstract function getDirectoryPath();
 
   /**
    * {@inheritdoc}
@@ -74,36 +75,9 @@ function getUri() {
   }
 
   /**
-   * Returns the local writable target of the resource within the stream.
-   *
-   * This function should be used in place of calls to realpath() or similar
-   * functions when attempting to determine the location of a file. While
-   * functions like realpath() may return the location of a read-only file, this
-   * method may return a URI or path suitable for writing that is completely
-   * separate from the URI used for reading.
-   *
-   * @param string $uri
-   *   Optional URI.
-   *
-   * @return string|bool
-   *   Returns a string representing a location suitable for writing of a file,
-   *   or FALSE if unable to write to the file such as with read-only streams.
-   */
-  protected function getTarget($uri = NULL) {
-    if (!isset($uri)) {
-      $uri = $this->uri;
-    }
-
-    list(, $target) = explode('://', $uri, 2);
-
-    // Remove erroneous leading or trailing, forward-slashes and backslashes.
-    return trim($target, '\/');
-  }
-
-  /**
    * {@inheritdoc}
    */
-  function realpath() {
+  public function realpath() {
     return $this->getLocalPath();
   }
 
@@ -142,6 +116,7 @@ protected function getLocalPath($uri = NULL) {
       $realpath = realpath(dirname($path)) . '/' . drupal_basename($path);
     }
     $directory = realpath($this->getDirectoryPath());
+
     if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {
       return FALSE;
     }
@@ -170,7 +145,7 @@ public function stream_open($uri, $mode, $options, &$opened_path) {
     $path = $this->getLocalPath();
     $this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
 
-    if ((bool) $this->handle && $options & STREAM_USE_PATH) {
+    if ((bool) $this->handle && ($options & STREAM_USE_PATH)) {
       $opened_path = $path;
     }
 
@@ -400,33 +375,6 @@ public function rename($from_uri, $to_uri) {
   }
 
   /**
-   * Gets the name of the directory from a given path.
-   *
-   * This method is usually accessed through drupal_dirname(), which wraps
-   * around the PHP dirname() function because it does not support stream
-   * wrappers.
-   *
-   * @param string $uri
-   *   A URI or path.
-   *
-   * @return string
-   *   A string containing the directory name.
-   *
-   * @see drupal_dirname()
-   */
-  public function dirname($uri = NULL) {
-    list($scheme) = explode('://', $uri, 2);
-    $target = $this->getTarget($uri);
-    $dirname = dirname($target);
-
-    if ($dirname == '.') {
-      $dirname = '';
-    }
-
-    return $scheme . '://' . $dirname;
-  }
-
-  /**
    * Support for mkdir().
    *
    * @param string $uri
diff --git a/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php b/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php
new file mode 100644
index 0000000..0fcc87e
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/LocalStreamTrait.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\StreamWrapper\LocalStreamTrait.
+ */
+
+namespace Drupal\Core\StreamWrapper;
+
+trait LocalStreamTrait {
+
+  /**
+   * Gets the name of the directory from a given path.
+   *
+   * This method is usually accessed through drupal_dirname(), which wraps
+   * around the PHP dirname() function because it does not support stream
+   * wrappers.
+   *
+   * @param string $uri
+   *   A URI or path.
+   *
+   * @return string
+   *   A string containing the directory name.
+   *
+   * @see drupal_dirname()
+   */
+  public function dirname($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+
+    list($scheme) = explode('://', $uri, 2);
+    $dirname = dirname($this->getTarget($uri));
+
+    return $dirname !== '.' ? "$scheme://$dirname" : "$scheme://";
+  }
+
+  /**
+   * Returns the local writable target of the resource within the stream.
+   *
+   * This function should be used in place of calls to realpath() or similar
+   * functions when attempting to determine the location of a file. While
+   * functions like realpath() may return the location of a read-only file, this
+   * method may return a URI or path suitable for writing that is completely
+   * separate from the URI used for reading.
+   *
+   * @param string $uri
+   *   Optional URI.
+   *
+   * @return string
+   *   Returns a string representing a location suitable for writing of a file.
+   *
+   * @throws \InvalidArgumentException
+   *   If a malformed $uri parameter is passed in.
+   */
+  protected function getTarget($uri = NULL) {
+    if (!isset($uri)) {
+      $uri = $this->uri;
+    }
+
+    $uri_parts = explode('://', $uri, 2);
+    if (count($uri_parts) === 1) {
+      // The delimiter ('://') was not found in $uri, malformed $uri passed.
+      throw new \InvalidArgumentException("Malformed uri parameter passed: $uri");
+    }
+
+    // Remove erroneous leading or trailing forward-slashes and backslashes.
+    return trim($uri_parts[1], '\/');
+  }
+
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php
new file mode 100644
index 0000000..157b876
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ModuleStream.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\StreamWrapper\ModuleStream.
+ */
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only module:// stream wrapper for module files.
+ */
+class ModuleStream extends ExtensionStreamBase {
+
+  /**
+   * The module handler service.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getOwnerName() {
+    $name = parent::getOwnerName();
+    if (!$this->getModuleHandler()->moduleExists($name)) {
+      // The module does not exist or is not installed.
+      throw new \InvalidArgumentException("Module $name does not exist or is not installed");
+    }
+    return $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDirectoryPath() {
+    return $this->getModuleHandler()->getModule($this->getOwnerName())->getPath();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Module files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('Local files stored under module directory.');
+  }
+
+  /**
+   * Returns the module handler service.
+   *
+   * @return \Drupal\Core\Extension\ModuleHandlerInterface
+   *   The module handler service.
+   */
+  protected function getModuleHandler() {
+    if (!isset($this->moduleHandler)) {
+      $this->moduleHandler = \Drupal::moduleHandler();
+    }
+    return $this->moduleHandler;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php
new file mode 100644
index 0000000..e016a42
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ProfileStream.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\StreamWrapper\ProfileStream.
+ */
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only profile:// stream wrapper for installed profile files.
+ */
+class ProfileStream extends ModuleStream {
+
+  use LocalStreamTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getOwnerName() {
+    return drupal_get_profile();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Installed profile files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('Local files stored under installed profile directory.');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php
index f13cfb8..2bf1502 100644
--- a/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php
+++ b/core/lib/Drupal/Core/StreamWrapper/StreamWrapperInterface.php
@@ -182,7 +182,7 @@ public function realpath();
    *   An optional URI.
    *
    * @return string
-   *   A string containing the directory name, or FALSE if not applicable.
+   *   A string containing the directory name.
    *
    * @see drupal_dirname()
    */
diff --git a/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php
new file mode 100644
index 0000000..bf100ac
--- /dev/null
+++ b/core/lib/Drupal/Core/StreamWrapper/ThemeStream.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\StreamWrapper\ThemeStream.
+ */
+
+namespace Drupal\Core\StreamWrapper;
+
+/**
+ * Defines the read-only theme:// stream wrapper for theme files.
+ */
+class ThemeStream extends ExtensionStreamBase {
+
+  /**
+   * The theme handler service.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getOwnerName() {
+    $name = parent::getOwnerName();
+    if (!$this->getThemeHandler()->themeExists($name)) {
+      // The theme does not exist or is not installed.
+      throw new \InvalidArgumentException("Theme $name does not exist or is not installed");
+    }
+    return $name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDirectoryPath() {
+    return $this->getThemeHandler()->getTheme($this->getOwnerName())->getPath();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getName() {
+    return $this->t('Theme files');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->t('Local files stored under theme directory.');
+  }
+
+  /**
+   * Returns the theme handler service.
+   *
+   * @return \Drupal\Core\Extension\ThemeHandlerInterface
+   *   The theme handler service.
+   */
+  protected function getThemeHandler() {
+    if (!isset($this->themeHandler)) {
+      $this->themeHandler = \Drupal::service('theme_handler');
+    }
+    return $this->themeHandler;
+  }
+
+}
diff --git a/core/modules/file/tests/file_test/file_test.dummy.inc b/core/modules/file/tests/file_test/file_test.dummy.inc
new file mode 100644
index 0000000..a09fd2e
--- /dev/null
+++ b/core/modules/file/tests/file_test/file_test.dummy.inc
@@ -0,0 +1 @@
+Dummy file used by SystemStreamTest.php.
diff --git a/core/modules/search/src/Tests/SearchSimplifyTest.php b/core/modules/search/src/Tests/SearchSimplifyTest.php
index 82363fd..6780ba0 100644
--- a/core/modules/search/src/Tests/SearchSimplifyTest.php
+++ b/core/modules/search/src/Tests/SearchSimplifyTest.php
@@ -25,7 +25,7 @@ function testSearchSimplifyUnicode() {
     // their own lines).  So the even-numbered lines should simplify to nothing,
     // and the odd-numbered lines we need to split into shorter chunks and
     // verify that simplification doesn't lose any characters.
-    $input = file_get_contents(\Drupal::root() . '/core/modules/search/tests/UnicodeTest.txt');
+    $input = file_get_contents('module://search/tests/UnicodeTest.txt');
     $basestrings = explode(chr(10), $input);
     $strings = array();
     foreach ($basestrings as $key => $string) {
diff --git a/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php b/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php
new file mode 100644
index 0000000..565c6ef
--- /dev/null
+++ b/core/modules/system/tests/src/Kernel/File/ExtensionStreamTest.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\system\Kernel\File\ExtensionStreamTest.
+ */
+
+namespace Drupal\Tests\system\Kernel\File;
+
+use Drupal\Core\Site\Settings;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests system stream wrapper functions.
+ *
+ * @group system
+ */
+class ExtensionStreamTest extends KernelTestBase {
+
+  /**
+   * A list of extension stream wrappers keyed by scheme.
+   *
+   * @var \Drupal\Core\StreamWrapper\StreamWrapperInterface[]
+   */
+  protected $streamWrappers = [];
+
+  /**
+   * The base url for the current request.
+   *
+   * @var string
+   */
+  protected $baseUrl;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Find the base url to be used later in tests.
+    $this->baseUrl = $this->container->get('request_stack')->getCurrentRequest()->getUriForPath(base_path());
+
+    /** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
+    $stream_wrapper_manager = $this->container->get('stream_wrapper_manager');
+    // Get stream wrapper instances.
+    foreach (['module', 'theme', 'profile'] as $scheme) {
+      $this->streamWrappers[$scheme] = $stream_wrapper_manager->getViaScheme($scheme);
+    }
+
+    /** @var \Drupal\Core\State\StateInterface $state */
+    $state = $this->container->get('state');
+
+    // Set 'minimal' as installed profile for the purposes of this test.
+    $system_module_files = $state->get('system.module.files', []);
+    $system_module_files += ['minimal' => 'core/profiles/minimal/minimal.info.yml'];
+    $state->set('system.module.files', $system_module_files);
+    // Add default profile for the purposes of this test.
+    new Settings(Settings::getAll() +  ['install_profile' => 'minimal']);
+    $this->config('core.extension')->set('module.minimal', 0)->save();
+    $this->container->get('module_handler')->addProfile('minimal', 'core/profiles/minimal');
+
+    /** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
+    $theme_installer = $this->container->get('theme_installer');
+    // Install Bartik and Seven themes.
+    $theme_installer->install(['bartik', 'seven']);
+  }
+
+  /**
+   * Tests invalid stream uris.
+   *
+   * @param string $uri
+   *   The URI being tested.
+   *
+   * @dataProvider providerInvalidUris
+   */
+  public function testInvalidStreamUri($uri) {
+    $message = "\\InvalidArgumentException thrown on invalid uri $uri.";
+    try {
+      $this->streamWrappers['module']->dirname($uri);
+      $this->fail($message);
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->assertSame($e->getMessage(), "Malformed uri parameter passed: $uri", $message);
+    }
+  }
+
+  /**
+   * Provides test cases for testInvalidStreamUri()
+   *
+   * @return array[]
+   *   A list of urls to test.
+   */
+  public function providerInvalidUris() {
+    return [
+      ['invalid/uri'],
+      ['invalid_uri'],
+      ['module/invalid/uri'],
+      ['module/invalid_uri'],
+      ['module:invalid_uri'],
+      ['module::/invalid/uri'],
+      ['module::/invalid_uri'],
+      ['module//:invalid/uri'],
+      ['module//invalid_uri'],
+      ['module//invalid/uri'],
+    ];
+  }
+
+  /**
+   * Test the extension stream wrapper methods.
+   *
+   * @param string $uri
+   *   The uri to be tested,
+   * @param string|\InvalidArgumentException $dirname
+   *   The expectation for dirname() method.
+   * @param string|\InvalidArgumentException $realpath
+   *   The expectation for realpath() method.
+   * @param string|\InvalidArgumentException $getExternalUrl
+   *   The expectation for getExternalUrl() method.
+   *
+   * @dataProvider providerStreamWrapperMethods
+   */
+  public function testStreamWrapperMethods($uri, $dirname, $realpath, $getExternalUrl) {
+    // Prefix realpath() expected value with Drupal root directory.
+    $realpath = is_string($realpath) ? DRUPAL_ROOT . $realpath : $realpath;
+    // Prefix getExternalUrl() expected value with base url.
+    $getExternalUrl = is_string($getExternalUrl) ? "{$this->baseUrl}$getExternalUrl" : $getExternalUrl;
+    $case = compact($dirname, $realpath, $getExternalUrl);
+
+    foreach ($case as $method => $expected) {
+      list($scheme, ) = explode('://', $uri);
+      $this->streamWrappers[$scheme]->setUri($uri);
+      if ($expected instanceof \InvalidArgumentException) {
+        /** @var \InvalidArgumentException $expected */
+        $message = sprintf('Exception thrown: \InvalidArgumentException("%s").', $expected->getMessage());
+        try {
+          $this->streamWrappers[$scheme]->$method();
+          $this->fail($message);
+        }
+        catch (\InvalidArgumentException $e) {
+          $this->assertSame($expected->getMessage(), $e->getMessage(), $message);
+        }
+      }
+      elseif (is_string($expected)) {
+        $this->assertSame($expected,  $this->streamWrappers[$scheme]->$method());
+      }
+    }
+  }
+
+  /**
+   * Provides test cases for testStreamWrapperMethods().
+   *
+   * @return array[]
+   *   A list of test cases. Each case consists of the following items:
+   *   - The uri to be tested.
+   *   - The result or the exception when running dirname() method.
+   *   - The result or the exception when running realpath() method. The value
+   *     is prefixed later, in the test method, with the Drupal root directory.
+   *   - The result or the exception when running getExternalUrl() method. The
+   *     value is prefixed later, in the test method, with the base url.
+   */
+  public function providerStreamWrapperMethods() {
+    return [
+      // Cases for module:// stream wrapper.
+      [
+        'module://system',
+        'module://system',
+        '/core/modules/system',
+        'core/modules/system',
+      ],
+      [
+        'module://system/css/system.admin.css',
+        'module://system/css',
+        '/core/modules/system/css/system.admin.css',
+        'core/modules/system/css/system.admin.css',
+      ],
+      [
+        'module://file_test/file_test.dummy.inc',
+        'module://file_test',
+        '/core/modules/file/tests/file_test/file_test.dummy.inc',
+        'core/modules/file/tests/file_test/file_test.dummy.inc',
+      ],
+      [
+        'module://file_test/src/file_test.dummy.inc',
+        'module://file_test/src',
+        '/core/modules/file/tests/file_test/src/file_test.dummy.inc',
+        'core/modules/file/tests/file_test/src/file_test.dummy.inc',
+      ],
+      [
+        'module://ckeditor/ckeditor.info.yml',
+        new \InvalidArgumentException('Module ckeditor does not exist or is not installed'),
+        new \InvalidArgumentException('Module ckeditor does not exist or is not installed'),
+        new \InvalidArgumentException('Module ckeditor does not exist or is not installed'),
+      ],
+      [
+        'module://foo_bar/foo.bar.js',
+        new \InvalidArgumentException('Module foo_bar does not exist or is not installed'),
+        new \InvalidArgumentException('Module foo_bar does not exist or is not installed'),
+        new \InvalidArgumentException('Module foo_bar does not exist or is not installed'),
+      ],
+      // Cases for theme:// stream wrapper.
+      [
+        'theme://seven',
+        'theme://seven',
+        '/core/themes/seven',
+        'core/themes/seven',
+      ],
+      [
+        'theme://seven/style.css',
+        'theme://seven',
+        '/core/themes/seven/style.css',
+        'core/themes/seven/style.css',
+      ],
+      [
+        'theme://bartik/color/preview.js',
+        'theme://bartik/color',
+        '/core/themes/bartik/color/preview.js',
+        'core/themes/bartik/color/preview.js',
+      ],
+      [
+        'theme://fifteen/screenshot.png',
+        new \InvalidArgumentException('Theme fifteen does not exist or is not installed'),
+        new \InvalidArgumentException('Theme fifteen does not exist or is not installed'),
+        new \InvalidArgumentException('Theme fifteen does not exist or is not installed'),
+      ],
+      [
+        'theme://stark/stark.info.yml',
+        new \InvalidArgumentException('Theme stark does not exist or is not installed'),
+        new \InvalidArgumentException('Theme stark does not exist or is not installed'),
+        new \InvalidArgumentException('Theme stark does not exist or is not installed'),
+      ],
+      // Cases for profile:// stream wrapper.
+      [
+        'profile://',
+        'profile://',
+        '/core/profiles/minimal',
+        'core/profiles/minimal',
+      ],
+      [
+        'profile://config/install/block.block.stark_login.yml',
+        'profile://config/install',
+        '/core/profiles/minimal/config/install/block.block.stark_login.yml',
+        'core/profiles/minimal/config/install/block.block.stark_login.yml',
+      ],
+      [
+        'profile://config/install/node.type.article.yml',
+        'profile://config/install',
+        '/core/profiles/minimal/config/install/node.type.article.yml',
+        'core/profiles/minimal/config/install/node.type.article.yml',
+      ],
+      [
+        'profile://minimal.info.yml',
+        'profile://',
+        '/core/profiles/minimal/minimal.info.yml',
+        'core/profiles/minimal/minimal.info.yml',
+      ],
+    ];
+  }
+
+}
