diff --git a/src/__tests__/PhutilLibraryTestCase.php b/src/__tests__/PhutilLibraryTestCase.php
index 26fb89d..0e5c626 100644
--- a/src/__tests__/PhutilLibraryTestCase.php
+++ b/src/__tests__/PhutilLibraryTestCase.php
@@ -1,146 +1,191 @@
 <?php
 
 /**
  * @concrete-extensible
  */
 class PhutilLibraryTestCase extends PhutilTestCase {
 
   /**
    * This is more of an acceptance test case instead of a unit test. It verifies
    * that all symbols can be loaded correctly. It can catch problems like
    * missing methods in descendants of abstract base classes.
    */
   public function testEverythingImplemented() {
     id(new PhutilSymbolLoader())
       ->setLibrary($this->getLibraryName())
       ->selectAndLoadSymbols();
     $this->assertTrue(true);
   }
 
   /**
    * This is more of an acceptance test case instead of a unit test. It verifies
    * that all the library map is up-to-date.
    */
   public function testLibraryMap() {
     $root = $this->getLibraryRoot();
     $library = phutil_get_library_name_for_root($root);
 
     $new_library_map = id(new PhutilLibraryMapBuilder($root))
       ->buildMap();
 
     $bootloader = PhutilBootloader::getInstance();
     $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library);
     unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]);
 
-    $this->assertEqual(
-      $new_library_map,
-      $old_library_map,
+    $identical = ($new_library_map === $old_library_map);
+    if (!$identical) {
+      $differences = $this->getMapDifferences(
+        $old_library_map,
+        $new_library_map);
+      sort($differences);
+    } else {
+      $differences = array();
+    }
+
+    $this->assertTrue(
+      $identical,
       pht(
-        'The library map does not appear to be up-to-date. Try '.
-        'rebuilding the map with `%s`.',
-        'arc liberate'));
+        "The library map is out of date. Rebuild it with `%s`.\n".
+        "These entries differ: %s.",
+        'arc liberate',
+        implode(', ', $differences)));
+  }
+
+
+  private function getMapDifferences($old, $new) {
+    $changed = array();
+
+    $all = $old + $new;
+    foreach ($all as $key => $value) {
+      $old_exists = array_key_exists($key, $old);
+      $new_exists = array_key_exists($key, $new);
+
+      // One map has it and the other does not, so mark it as changed.
+      if ($old_exists != $new_exists) {
+        $changed[] = $key;
+        continue;
+      }
+
+      $oldv = idx($old, $key);
+      $newv = idx($new, $key);
+      if ($oldv === $newv) {
+        continue;
+      }
+
+      if (is_array($oldv) && is_array($newv)) {
+        $child_changed = $this->getMapDifferences($oldv, $newv);
+        foreach ($child_changed as $child) {
+          $changed[] = $key.'.'.$child;
+        }
+      } else {
+        $changed[] = $key;
+      }
+    }
+
+    return $changed;
   }
 
+
   /**
    * This is more of an acceptance test case instead of a unit test. It verifies
    * that methods in subclasses have the same visibility as the method in the
    * parent class.
    */
   public function testMethodVisibility() {
     $symbols = id(new PhutilSymbolLoader())
       ->setLibrary($this->getLibraryName())
       ->selectSymbolsWithoutLoading();
 
     $classes = array();
     foreach ($symbols as $symbol) {
       if ($symbol['type'] == 'class') {
         $classes[$symbol['name']] = new ReflectionClass($symbol['name']);
       }
     }
 
     $failures = array();
 
     foreach ($classes as $class_name => $class) {
       $parents = array();
       $parent = $class;
       while ($parent = $parent->getParentClass()) {
         $parents[] = $parent;
       }
 
       $interfaces = $class->getInterfaces();
 
       foreach ($class->getMethods() as $method) {
         $method_name = $method->getName();
 
         foreach (array_merge($parents, $interfaces) as $extends) {
           if ($extends->hasMethod($method_name)) {
             $xmethod = $extends->getMethod($method_name);
 
             if (!$this->compareVisibility($xmethod, $method)) {
               $failures[] = pht(
                 'Class "%s" implements method "%s" with the wrong visibility. '.
                 'The method has visibility "%s", but it is defined in parent '.
                 '"%s" with visibility "%s". In Phabricator, a method which '.
                 'overrides another must always have the same visibility.',
                 $class_name,
                 $method_name,
                 $this->getVisibility($method),
                 $extends->getName(),
                 $this->getVisibility($xmethod));
             }
 
             // We found a declaration somewhere, so stop looking.
             break;
           }
         }
       }
     }
 
     $this->assertTrue(
       empty($failures),
       "\n\n".implode("\n\n", $failures));
   }
 
   /**
    * Get the name of the library currently being tested.
    */
   protected function getLibraryName() {
     return phutil_get_library_name_for_root($this->getLibraryRoot());
   }
 
   /**
    * Get the root directory for the library currently being tested.
    */
   protected function getLibraryRoot() {
     $caller = id(new ReflectionClass($this))->getFileName();
     return phutil_get_library_root_for_path($caller);
   }
 
   private function compareVisibility(
     ReflectionMethod $parent_method,
     ReflectionMethod $method) {
 
     static $bitmask;
 
     if ($bitmask === null) {
       $bitmask  = ReflectionMethod::IS_PUBLIC;
       $bitmask += ReflectionMethod::IS_PROTECTED;
       $bitmask += ReflectionMethod::IS_PRIVATE;
     }
 
     $parent_modifiers = $parent_method->getModifiers();
     $modifiers = $method->getModifiers();
     return !(($parent_modifiers ^ $modifiers) & $bitmask);
   }
 
   private function getVisibility(ReflectionMethod $method) {
     if ($method->isPrivate()) {
       return 'private';
     } else if ($method->isProtected()) {
       return 'protected';
     } else {
       return 'public';
     }
   }
 
 }