Add symlink tests

Track how symlinks work in the build graph with "stat" today. These will
be updated in the next change that switches to lstat.

Test: run ninja_tests (the build-tools build does this)
Change-Id: Ic1674bef202c198b6c707fc8a36edc2d89c27ee7
diff --git a/src/graph_test.cc b/src/graph_test.cc
index 78364f3..5dbe3ea 100644
--- a/src/graph_test.cc
+++ b/src/graph_test.cc
@@ -335,6 +335,90 @@
   ASSERT_EQ("dependency cycle: a -> a [-w phonycycle=err]", err);
 }
 
+TEST_F(GraphTest, OutputSymlinkSourceUpdate) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build other: cat\n"
+"build sym: cat\n"
+"build out: cat | sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "other");
+  fs_.Tick();
+  fs_.Create("other", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, OutputSymlinkDangling) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build sym: cat\n"
+"build out: cat | sym\n"));
+
+  fs_.CreateSymlink("sym", "dangling");
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlink) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "in");
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlinkUpdate) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.CreateSymlink("sym", "in");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  ASSERT_EQ("", err);
+
+  // This can be incorrect if the destination of the symlink changed to
+  // a file with an equal or older timestamp.
+  EXPECT_FALSE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlinkDangling) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "in");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+
+// TODO: tests around directory timestamps
+
 TEST_F(GraphTest, DependencyCycle) {
   AssertParse(&state_,
 "build out: cat mid\n"
diff --git a/src/test.cc b/src/test.cc
index 5b0ab30..415b598 100644
--- a/src/test.cc
+++ b/src/test.cc
@@ -175,9 +175,20 @@
   files_created_.insert(path);
 }
 
+void VirtualFileSystem::CreateSymlink(const string& path,
+                                      const string& dest) {
+  files_[path].mtime = now_;
+  files_[path].contents = dest;
+  files_[path].is_symlink = true;
+  files_created_.insert(path);
+}
+
 TimeStamp VirtualFileSystem::Stat(const string& path, string* err) const {
   FileMap::const_iterator i = files_.find(path);
   if (i != files_.end()) {
+    if (i->second.is_symlink) {
+      return Stat(i->second.contents, err);
+    }
     *err = i->second.stat_error;
     return i->second.mtime;
   }
@@ -216,6 +227,9 @@
   files_read_.push_back(path);
   FileMap::iterator i = files_.find(path);
   if (i != files_.end()) {
+    if (i->second.is_symlink) {
+      return LoadFile(i->second.contents, result, err);
+    }
     std::string& contents = i->second.contents;
     *result = std::unique_ptr<HeapLoadedFile>(new HeapLoadedFile(path,
                                                                  contents));
diff --git a/src/test.h b/src/test.h
index affab62..e97f4c4 100644
--- a/src/test.h
+++ b/src/test.h
@@ -137,6 +137,9 @@
   /// "Create" a file with contents.
   void Create(const string& path, const string& contents);
 
+  /// "Create" a symlink pointing to another path.
+  void CreateSymlink(const string& path, const string& dest);
+
   /// Tick "time" forwards; subsequent file operations will be newer than
   /// previous ones.
   int Tick() {
@@ -159,6 +162,7 @@
     int mtime;
     string stat_error;  // If mtime is -1.
     string contents;
+    bool is_symlink = false;
   };
 
   vector<string> directories_made_;