python,runfiles: runfiles library in @bazel_tools

Also update the Python stub script template to set
$RUNFILES_MANIFEST_FILE or $RUNFILES_DIR so the
runfiles library only needs to look for those.

See https://github.com/bazelbuild/bazel/issues/4460

RELNOTES[NEW]: python,runfiles: You can now depend on `@bazel_tools//tools/runfiles:py-runfiles` to get a platform-independent runfiles library for Python. See DocString of https://github.com/bazelbuild/bazel/blob/master/src/tools/runfiles/runfiles.py for usage information.

Change-Id: I4f68a11cb59f2782e5203e39fe60cc66b46023a2
PiperOrigin-RevId: 184515490
diff --git a/src/create_embedded_tools.py b/src/create_embedded_tools.py
index 352069c..0bcbdef 100644
--- a/src/create_embedded_tools.py
+++ b/src/create_embedded_tools.py
@@ -44,6 +44,7 @@
      lambda x: 'tools/jdk/ExperimentalTestRunner_deploy.jar'),
     ('*Runner_deploy.jar', lambda x: 'tools/jdk/TestRunner_deploy.jar'),
     ('*singlejar', lambda x: 'tools/jdk/singlejar/singlejar'),
+    ('src/tools/runfiles/runfiles.py', lambda x: 'tools/runfiles/runfiles.py'),
     ('*launcher.exe', lambda x: 'tools/launcher/launcher.exe'),
     ('*def_parser.exe', lambda x: 'tools/def_parser/def_parser.exe'),
     ('*ijar.exe', lambda x: 'tools/jdk/ijar/ijar.exe'),
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
index 84dd571..d2cdccc 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
@@ -100,6 +100,34 @@
     return [d for d in repo_dirs if os.path.isdir(d)]
   return [os.path.join(module_space, "%workspace_name%")]
 
+# Finds the runfiles manifest or the runfiles directory.
+def RunfilesEnvvar(module_space):
+  # If this binary is the data-dependency of another one, the other sets
+  # RUNFILES_MANIFEST_FILE or RUNFILES_DIR for our sake.
+  runfiles = os.environ.get('RUNFILES_MANIFEST_FILE', None)
+  if runfiles:
+    return ('RUNFILES_MANIFEST_FILE', runfiles)
+
+  runfiles = os.environ.get('RUNFILES_DIR', None)
+  if runfiles:
+    return ('RUNFILES_DIR', runfiles)
+
+  # If running from a zip, there's no manifest file.
+  if IsRunningFromZip():
+    return ('RUNFILES_DIR', module_space)
+
+  # Look for the runfiles "output" manifest, argv[0] + ".runfiles_manifest"
+  runfiles = module_space + '_manifest'
+  if os.path.exists(runfiles):
+    return ('RUNFILES_MANIFEST_FILE', runfiles)
+
+  # Look for the runfiles "input" manifest, argv[0] + ".runfiles/MANIFEST"
+  runfiles = os.path.join(module_space, 'MANIFEST')
+  if os.path.exists(runfiles):
+    return ('RUNFILES_DIR', runfiles)
+
+  return (None, None)
+
 def Main():
   args = sys.argv[1:]
 
@@ -125,6 +153,9 @@
     python_path = python_path.replace("/", os.sep)
 
   new_env['PYTHONPATH'] = python_path
+  runfiles_envkey, runfiles_envvalue = RunfilesEnvvar(module_space)
+  if runfiles_envkey:
+    new_env[runfiles_envkey] = runfiles_envvalue
 
   # Now look for my main python source file.
   # The magic string percent-main-percent is replaced with the filename of the
diff --git a/src/test/py/bazel/BUILD b/src/test/py/bazel/BUILD
index cfaf121..87f3eef 100644
--- a/src/test/py/bazel/BUILD
+++ b/src/test/py/bazel/BUILD
@@ -95,7 +95,11 @@
 py_test(
     name = "runfiles_test",
     srcs = ["runfiles_test.py"],
-    deps = [":test_base"],
+    data = glob(["testdata/runfiles_test/**"]),
+    deps = [
+        ":test_base",
+        "//third_party/py/six",
+    ],
 )
 
 py_test(
diff --git a/src/test/py/bazel/runfiles_test.py b/src/test/py/bazel/runfiles_test.py
index da6a143..c965f38 100644
--- a/src/test/py/bazel/runfiles_test.py
+++ b/src/test/py/bazel/runfiles_test.py
@@ -15,6 +15,7 @@
 
 import os
 import unittest
+import six
 from src.test.py.bazel import test_base
 
 
@@ -70,13 +71,50 @@
     exit_code, stdout, stderr = self.RunProgram([bin_path])
     self.AssertExitCode(exit_code, 0, stderr)
     if len(stdout) != 2:
-      self.fail("stdout: " + stdout)
+      self.fail("stdout: %s" % stdout)
     self.assertEqual(stdout[0], "Hello Foo!")
-    self.assertRegexpMatches(stdout[1], "^rloc=.*/foo/bar/hello.txt")
+    six.assertRegex(self, stdout[1], "^rloc=.*/foo/bar/hello.txt")
     with open(stdout[1].split("=", 1)[1], "r") as f:
       lines = [l.strip() for l in f.readlines()]
     if len(lines) != 1:
-      self.fail("lines: " + lines)
+      self.fail("lines: %s" % lines)
+    self.assertEqual(lines[0], "world")
+
+  def testPythonRunfilesLibraryInBazelToolsRepo(self):
+    for s, t in [
+        ("WORKSPACE.mock", "WORKSPACE"),
+        ("foo/BUILD.mock", "foo/BUILD"),
+        ("foo/runfiles.py", "foo/runfiles.py"),
+        ("foo/datadep/hello.txt", "foo/datadep/hello.txt"),
+    ]:
+      self.CopyFile(
+          self.Rlocation(
+              "io_bazel/src/test/py/bazel/testdata/runfiles_test/" + s), t)
+
+    exit_code, stdout, stderr = self.RunBazel(["info", "bazel-bin"])
+    self.AssertExitCode(exit_code, 0, stderr)
+    bazel_bin = stdout[0]
+
+    exit_code, _, stderr = self.RunBazel(["build", "//foo:runfiles-py"])
+    self.AssertExitCode(exit_code, 0, stderr)
+
+    if test_base.TestBase.IsWindows():
+      bin_path = os.path.join(bazel_bin, "foo/runfiles-py.exe")
+    else:
+      bin_path = os.path.join(bazel_bin, "foo/runfiles-py")
+
+    self.assertTrue(os.path.exists(bin_path))
+
+    exit_code, stdout, stderr = self.RunProgram([bin_path])
+    self.AssertExitCode(exit_code, 0, stderr)
+    if len(stdout) != 2:
+      self.fail("stdout: %s" % stdout)
+    self.assertEqual(stdout[0], "Hello Foo!")
+    six.assertRegex(self, stdout[1], "^rloc=.*/foo/datadep/hello.txt")
+    with open(stdout[1].split("=", 1)[1], "r") as f:
+      lines = [l.strip() for l in f.readlines()]
+    if len(lines) != 1:
+      self.fail("lines: %s" % lines)
     self.assertEqual(lines[0], "world")
 
 
diff --git a/src/test/py/bazel/test_base.py b/src/test/py/bazel/test_base.py
index be8a0c4..27d1898 100644
--- a/src/test/py/bazel/test_base.py
+++ b/src/test/py/bazel/test_base.py
@@ -174,6 +174,33 @@
       os.chmod(abspath, stat.S_IRWXU)
     return abspath
 
+  def CopyFile(self, src_path, dst_path, executable=False):
+    """Copy a file to a path under the test's scratch directory.
+
+    Args:
+      src_path: string; a path, the file to copy
+      dst_path: string; a path, relative to the test's scratch directory, the
+        destination to copy the file to, e.g. "foo/bar/BUILD"
+      executable: bool; whether to make the destination file executable
+    Returns:
+      The absolute path of the destination file.
+    Raises:
+      ArgumentError: if `dst_path` is absolute or contains uplevel references
+      IOError: if an I/O error occurs
+    """
+    if not src_path or not dst_path:
+      return
+    abspath = self.Path(dst_path)
+    if os.path.exists(abspath) and not os.path.isfile(abspath):
+      raise IOError('"%s" (%s) exists and is not a file' % (dst_path, abspath))
+    self.ScratchDir(os.path.dirname(dst_path))
+    with open(src_path, 'r') as s:
+      with open(abspath, 'w') as d:
+        d.write(s.read())
+    if executable:
+      os.chmod(abspath, stat.S_IRWXU)
+    return abspath
+
   def RunBazel(self, args, env_remove=None, env_add=None):
     """Runs "bazel <args>", waits for it to exit.
 
diff --git a/src/test/py/bazel/testdata/runfiles_test/WORKSPACE.mock b/src/test/py/bazel/testdata/runfiles_test/WORKSPACE.mock
new file mode 100644
index 0000000..e7c8745
--- /dev/null
+++ b/src/test/py/bazel/testdata/runfiles_test/WORKSPACE.mock
@@ -0,0 +1 @@
+workspace(name = "foo_ws")
diff --git a/src/test/py/bazel/testdata/runfiles_test/foo/BUILD.mock b/src/test/py/bazel/testdata/runfiles_test/foo/BUILD.mock
new file mode 100644
index 0000000..db28159
--- /dev/null
+++ b/src/test/py/bazel/testdata/runfiles_test/foo/BUILD.mock
@@ -0,0 +1,7 @@
+py_binary(
+    name = "runfiles-py",
+    srcs = ["runfiles.py"],
+    data = ["datadep/hello.txt"],
+    main = "runfiles.py",
+    deps = ["@bazel_tools//tools/runfiles:py-runfiles"],
+)
diff --git a/src/test/py/bazel/testdata/runfiles_test/foo/datadep/hello.txt b/src/test/py/bazel/testdata/runfiles_test/foo/datadep/hello.txt
new file mode 100644
index 0000000..cc628cc
--- /dev/null
+++ b/src/test/py/bazel/testdata/runfiles_test/foo/datadep/hello.txt
@@ -0,0 +1 @@
+world
diff --git a/src/test/py/bazel/testdata/runfiles_test/foo/runfiles.py b/src/test/py/bazel/testdata/runfiles_test/foo/runfiles.py
new file mode 100644
index 0000000..c4f3def
--- /dev/null
+++ b/src/test/py/bazel/testdata/runfiles_test/foo/runfiles.py
@@ -0,0 +1,21 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Mock Python binary, only used in tests."""
+
+from __future__ import print_function
+from bazel_tools.tools.runfiles import runfiles
+
+print('Hello Foo!')
+r = runfiles.Create()
+print('rloc=%s' % r.Rlocation('foo_ws/foo/datadep/hello.txt'))
diff --git a/src/tools/runfiles/BUILD b/src/tools/runfiles/BUILD
index e0ba09f..5ac490d 100644
--- a/src/tools/runfiles/BUILD
+++ b/src/tools/runfiles/BUILD
@@ -20,11 +20,24 @@
     name = "embedded_tools",
     srcs = [
         "BUILD.tools",
+        "runfiles.py",
         "//src/tools/runfiles/java/com/google/devtools/build/runfiles:embedded_tools",
     ],
     visibility = ["//src:__pkg__"],
 )
 
+py_library(
+    name = "py-runfiles",
+    srcs = ["runfiles.py"],
+)
+
+py_test(
+    name = "py-runfiles-test",
+    srcs = ["runfiles_test.py"],
+    main = "runfiles_test.py",
+    deps = [":py-runfiles"],
+)
+
 sh_library(
     name = "runfiles_sh_lib",
     srcs = ["runfiles.sh"],
diff --git a/src/tools/runfiles/runfiles.py b/src/tools/runfiles/runfiles.py
new file mode 100644
index 0000000..0a9149b
--- /dev/null
+++ b/src/tools/runfiles/runfiles.py
@@ -0,0 +1,203 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Runfiles lookup library for Bazel-built Python binaries and tests.
+
+Usage:
+
+from bazel_tools.tools.runfiles import runfiles
+
+r = runfiles.Create()
+with open(r.Rlocation("io_bazel/foo/bar.txt"), "r") as f:
+  contents = f.readlines()
+
+The code above creates a manifest- or directory-based implementations based on
+the environment variables in os.environ. See `Create()` for more info.
+
+If you want to explicitly create a manifest- or directory-based
+implementations, you can do so as follows:
+
+  r1 = runfiles.CreateManifestBased("path/to/foo.runfiles_manifest")
+
+  r2 = runfiles.CreateDirectoryBased("path/to/foo.runfiles/")
+
+If you want to start subprocesses that also need runfiles, you need to set the
+right environment variables for them:
+
+  import subprocess
+  from bazel_tools.tools.runfiles import runfiles
+
+  r = runfiles.Create()
+  env = {}
+  ...
+  env.update(r.EnvVar())
+  p = subprocess.Popen([r.Rlocation("path/to/binary")], env, ...)
+"""
+
+import os
+import posixpath
+
+
+def CreateManifestBased(manifest_path):
+  return _Runfiles(_ManifestBased(manifest_path))
+
+
+def CreateDirectoryBased(runfiles_dir_path):
+  return _Runfiles(_DirectoryBased(runfiles_dir_path))
+
+
+def Create(env=None):
+  """Returns a new `Runfiles` instance.
+
+  The returned object is either:
+  - manifest-based, meaning it looks up runfile paths from a manifest file, or
+  - directory-based, meaning it looks up runfile paths under a given directory
+    path
+
+  If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this method
+  returns a manifest-based implementation. The object eagerly reads and caches
+  the whole manifest file upon instantiation; this may be relevant for
+  performance consideration.
+
+  Otherwise, if `env` contains "RUNFILES_DIR" or "TEST_SRCDIR" with non-empty
+  value (checked in this priority order), this method returns a directory-based
+  implementation.
+
+  If neither cases apply, this method returns null.
+
+  Args:
+    env: {string: string}; optional; the map of environment variables. If None,
+        this function uses the environment variable map of this process.
+  Raises:
+    IOError: if some IO error occurs.
+  """
+  env_map = os.environ if env is None else env
+  manifest = env_map.get("RUNFILES_MANIFEST_FILE")
+  if manifest:
+    return CreateManifestBased(manifest)
+
+  directory = env_map.get("RUNFILES_DIR")
+  if not directory:
+    directory = env_map.get("TEST_SRCDIR")
+  if directory:
+    return CreateDirectoryBased(directory)
+
+  return None
+
+
+class _Runfiles(object):
+  """Returns the runtime location of runfiles.
+
+  Runfiles are data-dependencies of Bazel-built binaries and tests.
+  """
+
+  def __init__(self, strategy):
+    self._strategy = strategy
+
+  def Rlocation(self, path):
+    """Returns the runtime path of a runfile.
+
+    Runfiles are data-dependencies of Bazel-built binaries and tests.
+
+    The returned path may not be valid. The caller should check the path's
+    validity and that the path exists.
+
+    The function may return None. In that case the caller can be sure that the
+    rule does not know about this data-dependency.
+
+    Args:
+      path: string; runfiles-root-relative path of the runfile
+    Returns:
+      the path to the runfile, which the caller should check for existence, or
+      None if the method doesn't know about this runfile
+    Raises:
+      TypeError: if `path` is not a string
+      ValueError: if `path` is None or empty, or it's absolute or contains
+        uplevel references
+    """
+    if not path:
+      raise ValueError()
+    if not isinstance(path, str):
+      raise TypeError()
+    if ".." in path:
+      raise ValueError("path contains uplevel references: \"%s\"" % path)
+    if os.path.isabs(path) or path[0] == os.sep:
+      raise ValueError("path is absolute: \"%s\"" % path)
+    return self._strategy.RlocationChecked(path)
+
+  def EnvVar(self):
+    """Returns an environment variable for subprocesses.
+
+    The caller should set the returned key-value pair in the environment of
+    subprocesses in case those subprocesses are also Bazel-built binaries that
+    need to use runfiles.
+
+    Returns:
+      {string: string}; a single-entry dict; key is an environment variable
+      name (either "RUNFILES_MANIFEST_FILE" or "RUNFILES_DIR"), value is the
+      value for this environment variable
+    """
+    return self._strategy.EnvVar()
+
+
+class _ManifestBased(object):
+  """`Runfiles` strategy that parses a runfiles-manifest to look up runfiles."""
+
+  def __init__(self, path):
+    if not path:
+      raise ValueError()
+    if not isinstance(path, str):
+      raise TypeError()
+    self._path = path
+    self._runfiles = _ManifestBased._LoadRunfiles(path)
+
+  def RlocationChecked(self, path):
+    return self._runfiles.get(path)
+
+  @staticmethod
+  def _LoadRunfiles(path):
+    """Loads the runfiles manifest."""
+    result = {}
+    with open(path, "r") as f:
+      for line in f:
+        line = line.strip()
+        if line:
+          tokens = line.split(" ", 1)
+          if len(tokens) == 1:
+            result[line] = line
+          else:
+            result[tokens[0]] = tokens[1]
+    return result
+
+  def EnvVar(self):
+    return {"RUNFILES_MANIFEST_FILE": self._path}
+
+
+class _DirectoryBased(object):
+  """`Runfiles` strategy that appends runfiles paths to the runfiles root."""
+
+  def __init__(self, path):
+    if not path:
+      raise ValueError()
+    if not isinstance(path, str):
+      raise TypeError()
+    self._runfiles_root = path
+
+  def RlocationChecked(self, path):
+    # Use posixpath instead of os.path, because Bazel only creates a runfiles
+    # tree on Unix platforms, so `Create()` will only create a directory-based
+    # runfiles strategy on those platforms.
+    return posixpath.join(self._runfiles_root, path)
+
+  def EnvVar(self):
+    return {"RUNFILES_DIR": self._runfiles_root}
diff --git a/src/tools/runfiles/runfiles_test.py b/src/tools/runfiles/runfiles_test.py
new file mode 100644
index 0000000..10c1b8e
--- /dev/null
+++ b/src/tools/runfiles/runfiles_test.py
@@ -0,0 +1,135 @@
+# pylint: disable=g-bad-file-header
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import tempfile
+import unittest
+
+from src.tools.runfiles import runfiles
+
+
+class RunfilesTest(unittest.TestCase):
+  # """Unit tests for `runfiles.Runfiles`."""
+
+  def testRlocationArgumentValidation(self):
+    r = runfiles.Create({"RUNFILES_DIR": "whatever"})
+    self.assertRaises(ValueError, lambda: r.Rlocation(None))
+    self.assertRaises(ValueError, lambda: r.Rlocation(""))
+    self.assertRaises(TypeError, lambda: r.Rlocation(1))
+    self.assertRaisesRegexp(ValueError, "contains uplevel",
+                            lambda: r.Rlocation("foo/.."))
+    if RunfilesTest.IsWindows():
+      self.assertRaisesRegexp(ValueError, "is absolute",
+                              lambda: r.Rlocation("\\foo"))
+      self.assertRaisesRegexp(ValueError, "is absolute",
+                              lambda: r.Rlocation("c:/foo"))
+      self.assertRaisesRegexp(ValueError, "is absolute",
+                              lambda: r.Rlocation("c:\\foo"))
+    else:
+      self.assertRaisesRegexp(ValueError, "is absolute",
+                              lambda: r.Rlocation("/foo"))
+
+  def testCreatesManifestBasedRunfiles(self):
+    with _MockFile(["a/b c/d"]) as mf:
+      r = runfiles.Create({
+          "RUNFILES_MANIFEST_FILE": mf.Path(),
+          "RUNFILES_DIR": "ignored when RUNFILES_MANIFEST_FILE has a value",
+          "TEST_SRCDIR": "ignored when RUNFILES_MANIFEST_FILE has a value"
+      })
+      self.assertEqual(r.Rlocation("a/b"), "c/d")
+      self.assertIsNone(r.Rlocation("foo"))
+      self.assertDictEqual(r.EnvVar(), {"RUNFILES_MANIFEST_FILE": mf.Path()})
+
+  def testCreatesDirectoryBasedRunfiles(self):
+    r = runfiles.Create({
+        "RUNFILES_DIR": "runfiles/dir",
+        "TEST_SRCDIR": "ignored when RUNFILES_DIR is set"
+    })
+    self.assertEqual(r.Rlocation("a/b"), "runfiles/dir/a/b")
+    self.assertEqual(r.Rlocation("foo"), "runfiles/dir/foo")
+    self.assertDictEqual(r.EnvVar(), {"RUNFILES_DIR": "runfiles/dir"})
+
+    r = runfiles.Create({"TEST_SRCDIR": "test/srcdir"})
+    self.assertEqual(r.Rlocation("a/b"), "test/srcdir/a/b")
+    self.assertEqual(r.Rlocation("foo"), "test/srcdir/foo")
+    self.assertDictEqual(r.EnvVar(), {"RUNFILES_DIR": "test/srcdir"})
+
+  def testFailsToCreateManifestBasedBecauseManifestDoesNotExist(self):
+
+    def _Run():
+      runfiles.Create({"RUNFILES_MANIFEST_FILE": "non-existing path"})
+
+    self.assertRaisesRegexp(IOError, "non-existing path", _Run)
+
+  def testFailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined(self):
+    with _MockFile(["a b"]) as mf:
+      runfiles.Create({
+          "RUNFILES_MANIFEST_FILE": mf.Path(),
+          "RUNFILES_DIR": "whatever",
+          "TEST_SRCDIR": "whatever"
+      })
+    runfiles.Create({"RUNFILES_DIR": "whatever", "TEST_SRCDIR": "whatever"})
+    runfiles.Create({"TEST_SRCDIR": "whatever"})
+    self.assertIsNone(runfiles.Create({"FOO": "bar"}))
+
+  def testManifestBasedRlocation(self):
+    with _MockFile([
+        "Foo/runfile1", "Foo/runfile2 C:/Actual Path\\runfile2",
+        "Foo/Bar/runfile3 D:\\the path\\run file 3.txt"
+    ]) as mf:
+      r = runfiles.CreateManifestBased(mf.Path())
+      self.assertEqual(r.Rlocation("Foo/runfile1"), "Foo/runfile1")
+      self.assertEqual(r.Rlocation("Foo/runfile2"), "C:/Actual Path\\runfile2")
+      self.assertEqual(
+          r.Rlocation("Foo/Bar/runfile3"), "D:\\the path\\run file 3.txt")
+      self.assertIsNone(r.Rlocation("unknown"))
+      self.assertDictEqual(r.EnvVar(), {"RUNFILES_MANIFEST_FILE": mf.Path()})
+
+  def testDirectoryBasedRlocation(self):
+    # The _DirectoryBased strategy simply joins the runfiles directory and the
+    # runfile's path on a "/". This strategy does not perform any normalization,
+    # nor does it check that the path exists.
+    r = runfiles.CreateDirectoryBased("foo/bar baz//qux/")
+    self.assertEqual(r.Rlocation("arg"), "foo/bar baz//qux/arg")
+    self.assertDictEqual(r.EnvVar(), {"RUNFILES_DIR": "foo/bar baz//qux/"})
+
+  @staticmethod
+  def IsWindows():
+    return os.name == "nt"
+
+
+class _MockFile(object):
+
+  def __init__(self, contents):
+    self._contents = contents
+    self._path = None
+
+  def __enter__(self):
+    tmpdir = os.environ.get("TEST_TMPDIR")
+    self._path = os.path.join(tempfile.mkdtemp(dir=tmpdir), "x")
+    with open(self._path, "wt") as f:
+      f.writelines(l + "\n" for l in self._contents)
+    return self
+
+  def __exit__(self, exc_type, exc_value, traceback):
+    os.remove(self._path)
+    os.rmdir(os.path.dirname(self._path))
+
+  def Path(self):
+    return self._path
+
+
+if __name__ == "__main__":
+  unittest.main()