diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -48,8 +48,9 @@
     'FileFinder' => 'filesystem/FileFinder.php',
     'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
     'FileList' => 'filesystem/FileList.php',
+    'FileListTestCase' => 'filesystem/__tests__/FileListTestCase.php',
     'Filesystem' => 'filesystem/Filesystem.php',
-    'FilesystemException' => 'filesystem/FilesystemException.php',
+    'FilesystemException' => 'filesystem/exception/FilesystemException.php',
     'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
     'Future' => 'future/Future.php',
     'FutureIterator' => 'future/FutureIterator.php',
@@ -146,6 +147,7 @@
     'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
     'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
     'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
+    'PhutilDirectoryFixtureTestCase' => 'filesystem/__tests__/PhutilDirectoryFixtureTestCase.php',
     'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
     'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php',
     'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
@@ -177,6 +179,7 @@
     'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
     'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
     'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
+    'PhutilFileTreeTestCase' => 'filesystem/__tests__/PhutilFileTreeTestCase.php',
     'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
     'PhutilGitURI' => 'parser/PhutilGitURI.php',
     'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
@@ -221,7 +224,7 @@
     'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
     'PhutilLocale' => 'internationalization/PhutilLocale.php',
     'PhutilLock' => 'filesystem/PhutilLock.php',
-    'PhutilLockException' => 'filesystem/PhutilLockException.php',
+    'PhutilLockException' => 'filesystem/exception/PhutilLockException.php',
     'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
     'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
     'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
@@ -357,6 +360,7 @@
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
     'QueryFuture' => 'future/query/QueryFuture.php',
     'TempFile' => 'filesystem/TempFile.php',
+    'TempFileTestCase' => 'filesystem/__tests__/TempFileTestCase.php',
     'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
     'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
     'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
@@ -517,6 +521,7 @@
     'ExecFutureTestCase' => 'PhutilTestCase',
     'ExecPassthruTestCase' => 'PhutilTestCase',
     'FileFinderTestCase' => 'PhutilTestCase',
+    'FileListTestCase' => 'PhutilTestCase',
     'FilesystemException' => 'Exception',
     'FilesystemTestCase' => 'PhutilTestCase',
     'FutureIterator' => 'Iterator',
@@ -595,6 +600,7 @@
     'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
     'PhutilDeferredLogTestCase' => 'PhutilTestCase',
     'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
+    'PhutilDirectoryFixtureTestCase' => 'PhutilTestCase',
     'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
     'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilDocblockParserTestCase' => 'PhutilTestCase',
@@ -614,6 +620,7 @@
     'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
     'PhutilFileLock' => 'PhutilLock',
     'PhutilFileLockTestCase' => 'PhutilTestCase',
+    'PhutilFileTreeTestCase' => 'PhutilTestCase',
     'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
     'PhutilGitURITestCase' => 'PhutilTestCase',
     'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
@@ -753,6 +760,7 @@
     'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
     'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
     'QueryFuture' => 'Future',
+    'TempFileTestCase' => 'PhutilTestCase',
     'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
     'XHPASTNode' => 'AASTNode',
     'XHPASTNodeTestCase' => 'PhutilTestCase',
diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php
--- a/src/filesystem/Filesystem.php
+++ b/src/filesystem/Filesystem.php
@@ -731,38 +731,42 @@
 
 
   /**
-   * Return all directories between a path and "/". Iterating over them walks
-   * from the path to the root.
+   * Return all directories between a path and the specified root directory
+   * (defaulting to "/"). Iterating over them walks from the path to the root.
    *
-   * @param  string Path, absolute or relative to PWD.
-   * @return list   List of parent paths, including the provided path.
+   * @param  string        Path, absolute or relative to PWD.
+   * @param  string        The root directory.
+   * @return list<string>  List of parent paths, including the provided path.
    * @task   directory
    */
-  public static function walkToRoot($path) {
+  public static function walkToRoot($path, $root = '/') {
     $path = self::resolvePath($path);
+    $root = self::resolvePath($root);
 
     if (is_link($path)) {
       $path = realpath($path);
     }
 
-    $walk = array();
-    $parts = explode(DIRECTORY_SEPARATOR, $path);
-    foreach ($parts as $k => $part) {
-      if (!strlen($part)) {
-        unset($parts[$k]);
-      }
+    if (is_link($root)) {
+      $root = realpath($root);
     }
-    do {
+
+    $walk = array();
+    $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path));
+
+    while ($parts && implode(DIRECTORY_SEPARATOR, $parts) != $root) {
       if (phutil_is_windows()) {
         $walk[] = implode(DIRECTORY_SEPARATOR, $parts);
       } else {
         $walk[] = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts);
       }
-      if (empty($parts)) {
-        break;
-      }
+
       array_pop($parts);
-    } while (true);
+    }
+
+    if ($root == '/') {
+      $walk[] = $root;
+    }
 
     return $walk;
   }
@@ -851,7 +855,6 @@
    * @task   path
    */
   public static function isDescendant($path, $root) {
-
     try {
       self::assertExists($path);
       self::assertExists($root);
diff --git a/src/filesystem/__tests__/FileListTestCase.php b/src/filesystem/__tests__/FileListTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/filesystem/__tests__/FileListTestCase.php
@@ -0,0 +1,3 @@
+<?php
+
+final class FileListTestCase extends PhutilTestCase {}
diff --git a/src/filesystem/__tests__/FilesystemTestCase.php b/src/filesystem/__tests__/FilesystemTestCase.php
--- a/src/filesystem/__tests__/FilesystemTestCase.php
+++ b/src/filesystem/__tests__/FilesystemTestCase.php
@@ -2,6 +2,168 @@
 
 final class FilesystemTestCase extends PhutilTestCase {
 
+  public function testReadFile() {
+    $this->assertEqual(
+      'Test file.',
+      Filesystem::readFile(dirname(__FILE__).'/data/test'));
+
+    $caught = null;
+    try {
+      Filesystem::readFile(dirname(__FILE__).'/data/nofile');
+    } catch (Exception $ex) {
+      $caught = $ex;
+    }
+    $this->assertTrue($caught instanceof FilesystemException);
+  }
+
+  public function testWriteFile() {}
+
+  public function testWriteFileIfChanged() {}
+
+  public function testWriteUniqueFile() {
+    $tmp = new TempFile();
+    $dir = dirname($tmp);
+
+    // Writing an empty file should work.
+    $f = Filesystem::writeUniqueFile($dir, '');
+    $this->assertEqual('', Filesystem::readFile($f));
+
+    // File name should be unique.
+    $g = Filesystem::writeUniqueFile($dir, 'quack');
+    $this->assertTrue($f != $g);
+  }
+
+  public function testAppendFile() {}
+
+  public function testRemove() {}
+
+  public function testRename() {}
+
+  public function testChangePermissions() {}
+
+  public function testGetModifiedTime() {}
+
+  public function testReadRandomBytes() {
+    $number_of_bytes = 1024;
+    $data = Filesystem::readRandomBytes($number_of_bytes);
+    $this->assertTrue(strlen($data) == $number_of_bytes);
+
+    $data1 = Filesystem::readRandomBytes(128);
+    $data2 = Filesystem::readRandomBytes(128);
+    $this->assertFalse($data1 == $data2);
+
+    $caught = null;
+    try {
+      Filesystem::readRandomBytes(0);
+    } catch (Exception $ex) {
+      $caught = $ex;
+    }
+    $this->assertTrue($caught instanceof Exception);
+  }
+
+  public function readRandomCharacters() {}
+
+  public function testGetMimeType() {}
+
+  public function testCreateDirectory() {}
+
+  public function testCreateTemporaryDirectory() {}
+
+  public function testListDirectory() {}
+
+  public function testWalkToRoot() {
+    $test_cases = array(
+      array(
+        '/foo/bar/baz',
+        '/',
+        array(
+          '/foo/bar/baz',
+          '/foo/bar',
+          '/foo',
+          '/',
+        ),
+      ),
+      array(
+        '/foo/bar/baz/',
+        '/',
+        array(
+          '/foo/bar/baz',
+          '/foo/bar',
+          '/foo',
+          '/',
+        ),
+      ),
+
+      array(
+        '/foo/bar/baz',
+        '/foo',
+        array(
+          '/foo/bar/baz',
+          '/foo/bar',
+          '/foo',
+        ),
+      ),
+    );
+
+    foreach ($test_cases as $test_case) {
+      list($path, $root, $expected) = $test_case;
+
+      $this->assertEqual(
+        $expected,
+        Filesystem::walkToRoot($path, $root));
+    }
+  }
+
+  public function testIsAbsolutePath() {}
+
+  public function testResolvePath() {}
+
+  public function testisDescendant() {
+    $test_cases = array(
+      array(
+        __FILE__,
+        dirname(__FILE__),
+        true,
+      ),
+      array(
+        dirname(__FILE__),
+        dirname(dirname(__FILE__)),
+        true,
+      ),
+      array(
+        dirname(__FILE__),
+        phutil_get_library_root_for_path(__FILE__),
+        true,
+      ),
+      array(
+        dirname(dirname(__FILE__)),
+        dirname(__FILE__),
+        false,
+      ),
+      array(
+        dirname(__FILE__).'/quack',
+        dirname(__FILE__),
+        false,
+      ),
+    );
+
+    foreach ($test_cases as $test_case) {
+      list($path, $root, $expected) = $test_case;
+
+      $this->assertEqual(
+        $expected,
+        Filesystem::isDescendant($path, $root),
+        sprintf(
+          'Filesystem::isDescendant(%s, %s)',
+          phutil_var_export($path),
+          phutil_var_export($root)));
+    }
+  }
+
+  public function testReadablePath() {}
+
+  public function testPathExists() {}
+
   public function testBinaryExists() {
     // Test for the `which` binary on Linux, and the `where` binary on Windows,
     // because `which which` is cute.
@@ -42,35 +204,18 @@
       Filesystem::resolveBinary('halting-problem-decider'));
   }
 
-  public function testWriteUniqueFile() {
-    $tmp = new TempFile();
-    $dir = dirname($tmp);
+  public function testPathsAreEquivalent() {}
 
-    // Writing an empty file should work.
-    $f = Filesystem::writeUniqueFile($dir, '');
-    $this->assertEqual('', Filesystem::readFile($f));
+  public function testAssertExists() {}
 
-    // File name should be unique.
-    $g = Filesystem::writeUniqueFile($dir, 'quack');
-    $this->assertTrue($f != $g);
-  }
+  public function testAssertNotExists() {}
 
-  public function testReadRandomBytes() {
-    $number_of_bytes = 1024;
-    $data = Filesystem::readRandomBytes($number_of_bytes);
-    $this->assertTrue(strlen($data) == $number_of_bytes);
+  public function testAssertIsFile() {}
 
-    $data1 = Filesystem::readRandomBytes(128);
-    $data2 = Filesystem::readRandomBytes(128);
-    $this->assertFalse($data1 == $data2);
+  public function testAssertIsDirectory() {}
 
-    $caught = null;
-    try {
-      Filesystem::readRandomBytes(0);
-    } catch (Exception $ex) {
-      $caught = $ex;
-    }
-    $this->assertTrue($caught instanceof Exception);
-  }
+  public function testAssertWritable() {}
+
+  public function testAssertReadable() {}
 
 }
diff --git a/src/filesystem/__tests__/PhutilDirectoryFixtureTestCase.php b/src/filesystem/__tests__/PhutilDirectoryFixtureTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/filesystem/__tests__/PhutilDirectoryFixtureTestCase.php
@@ -0,0 +1,3 @@
+<?php
+
+final class PhutilDirectoryFixtureTestCase extends PhutilTestCase {}
diff --git a/src/filesystem/__tests__/PhutilFileTreeTestCase.php b/src/filesystem/__tests__/PhutilFileTreeTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/filesystem/__tests__/PhutilFileTreeTestCase.php
@@ -0,0 +1,3 @@
+<?php
+
+final class PhutilFileTreeTestCase extends PhutilTestCase {}
diff --git a/src/filesystem/__tests__/TempFileTestCase.php b/src/filesystem/__tests__/TempFileTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/filesystem/__tests__/TempFileTestCase.php
@@ -0,0 +1,3 @@
+<?php
+
+final class TempFileTestCase extends PhutilTestCase {}
diff --git a/src/filesystem/FilesystemException.php b/src/filesystem/exception/FilesystemException.php
rename from src/filesystem/FilesystemException.php
rename to src/filesystem/exception/FilesystemException.php
diff --git a/src/filesystem/PhutilLockException.php b/src/filesystem/exception/PhutilLockException.php
rename from src/filesystem/PhutilLockException.php
rename to src/filesystem/exception/PhutilLockException.php