// Copyright (c) 2008 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/directory_watcher.h" #include <limits> #include "base/basictypes.h" #include "base/file_path.h" #include "base/file_util.h" #include "base/message_loop.h" #include "base/path_service.h" #include "base/platform_thread.h" #include "base/string_util.h" #include "base/thread.h" #if defined(OS_WIN) #include "base/win_util.h" #endif // defined(OS_WIN) #include "testing/gtest/include/gtest/gtest.h" namespace { // For tests where we wait a bit to verify nothing happened const int kWaitForEventTime = 500; class DirectoryWatcherTest : public testing::Test { public: // Implementation of DirectoryWatcher on Mac requires UI loop. DirectoryWatcherTest() : loop_(MessageLoop::TYPE_UI), notified_delegates_(0), expected_notified_delegates_(0) { } void OnTestDelegateFirstNotification(const FilePath& path) { notified_delegates_++; if (notified_delegates_ >= expected_notified_delegates_) MessageLoop::current()->Quit(); } protected: virtual void SetUp() { // Name a subdirectory of the temp directory. FilePath path; ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &path)); test_dir_ = path.Append(FILE_PATH_LITERAL("DirectoryWatcherTest")); // Create a fresh, empty copy of this directory. file_util::Delete(test_dir_, true); file_util::CreateDirectory(test_dir_); } virtual void TearDown() { // Make sure there are no tasks in the loop. loop_.RunAllPending(); // Clean up test directory. ASSERT_TRUE(file_util::Delete(test_dir_, true)); ASSERT_FALSE(file_util::PathExists(test_dir_)); } // Write |content| to the |filename|. Returns true on success. bool WriteTestFile(const FilePath& filename, const std::string& content) { return (file_util::WriteFile(filename, content.c_str(), content.length()) == static_cast<int>(content.length())); } // Create directory |name| under test_dir_. If |sync| is true, runs // SyncIfPOSIX. Returns path to the created directory, including test_dir_. FilePath CreateTestDirDirectoryASCII(const std::string& name, bool sync) { FilePath path(test_dir_.AppendASCII(name)); EXPECT_TRUE(file_util::CreateDirectory(path)); if (sync) SyncIfPOSIX(); return path; } void SetExpectedNumberOfNotifiedDelegates(int n) { notified_delegates_ = 0; expected_notified_delegates_ = n; } void VerifyExpectedNumberOfNotifiedDelegates() { // Check that we get at least the expected number of notified delegates. if (expected_notified_delegates_ - notified_delegates_ > 0) loop_.Run(); EXPECT_EQ(expected_notified_delegates_, notified_delegates_); } void VerifyNoExtraNotifications() { // Check that we get no more than the expected number of notified delegates. loop_.PostDelayedTask(FROM_HERE, new MessageLoop::QuitTask, kWaitForEventTime); loop_.Run(); EXPECT_EQ(expected_notified_delegates_, notified_delegates_); } // We need this function for reliable tests on Mac OS X. FSEvents API // has a latency interval and can merge multiple events into one, // and we need a clear distinction between events triggered by test setup code // and test code. void SyncIfPOSIX() { #if defined(OS_POSIX) sync(); #endif // defined(OS_POSIX) } MessageLoop loop_; // The path to a temporary directory used for testing. FilePath test_dir_; // The number of test delegates which received their notification. int notified_delegates_; // The number of notified test delegates after which we quit the message loop. int expected_notified_delegates_; }; class TestDelegate : public DirectoryWatcher::Delegate { public: explicit TestDelegate(DirectoryWatcherTest* test) : test_(test), got_notification_(false), original_thread_id_(PlatformThread::CurrentId()) { } bool got_notification() const { return got_notification_; } void reset() { got_notification_ = false; } virtual void OnDirectoryChanged(const FilePath& path) { EXPECT_EQ(original_thread_id_, PlatformThread::CurrentId()); if (!got_notification_) test_->OnTestDelegateFirstNotification(path); got_notification_ = true; } private: // Hold a pointer to current test fixture to inform it on first notification. DirectoryWatcherTest* test_; // Set to true after first notification. bool got_notification_; // Keep track of original thread id to verify that callbacks are called // on the same thread. PlatformThreadId original_thread_id_; }; // Basic test: add a file and verify we notice it. TEST_F(DirectoryWatcherTest, NewFile) { DirectoryWatcher watcher; TestDelegate delegate(this); ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, false)); SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); VerifyExpectedNumberOfNotifiedDelegates(); } // Verify that modifying a file is caught. TEST_F(DirectoryWatcherTest, ModifiedFile) { // Write a file to the test dir. ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); SyncIfPOSIX(); DirectoryWatcher watcher; TestDelegate delegate(this); ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, false)); // Now make sure we get notified if the file is modified. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "new content")); VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, DeletedFile) { // Write a file to the test dir. ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); SyncIfPOSIX(); DirectoryWatcher watcher; TestDelegate delegate(this); ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, false)); // Now make sure we get notified if the file is deleted. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(file_util::Delete(test_dir_.AppendASCII("test_file"), false)); VerifyExpectedNumberOfNotifiedDelegates(); } // Verify that letting the watcher go out of scope stops notifications. TEST_F(DirectoryWatcherTest, Unregister) { TestDelegate delegate(this); { DirectoryWatcher watcher; ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, false)); // And then let it fall out of scope, clearing its watch. } // Write a file to the test dir. SetExpectedNumberOfNotifiedDelegates(0); ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); VerifyExpectedNumberOfNotifiedDelegates(); VerifyNoExtraNotifications(); } TEST_F(DirectoryWatcherTest, SubDirRecursive) { FilePath subdir(CreateTestDirDirectoryASCII("SubDir", true)); // Verify that modifications to a subdirectory are noticed by recursive watch. TestDelegate delegate(this); DirectoryWatcher watcher; ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, true)); // Write a file to the subdir. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdir.AppendASCII("test_file"), "some content")); VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, SubDirNonRecursive) { #if defined(OS_WIN) // Disable this test for earlier version of Windows. It turned out to be // very difficult to create a reliable test for them. if (win_util::GetWinVersion() < win_util::WINVERSION_VISTA) return; #endif // defined(OS_WIN) FilePath subdir(CreateTestDirDirectoryASCII("SubDir", false)); // Create a test file before the test. On Windows we get a notification // when creating a file in a subdir even with a non-recursive watch. ASSERT_TRUE(WriteTestFile(subdir.AppendASCII("test_file"), "some content")); SyncIfPOSIX(); // Verify that modifications to a subdirectory are not noticed // by a not-recursive watch. DirectoryWatcher watcher; TestDelegate delegate(this); ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, false)); // Modify the test file. There should be no notifications. SetExpectedNumberOfNotifiedDelegates(0); ASSERT_TRUE(WriteTestFile(subdir.AppendASCII("test_file"), "other content")); VerifyExpectedNumberOfNotifiedDelegates(); VerifyNoExtraNotifications(); } namespace { // Used by the DeleteDuringNotify test below. // Deletes the DirectoryWatcher when it's notified. class Deleter : public DirectoryWatcher::Delegate { public: Deleter(DirectoryWatcher* watcher, MessageLoop* loop) : watcher_(watcher), loop_(loop) { } virtual void OnDirectoryChanged(const FilePath& path) { watcher_.reset(NULL); loop_->PostTask(FROM_HERE, new MessageLoop::QuitTask()); } scoped_ptr<DirectoryWatcher> watcher_; MessageLoop* loop_; }; } // anonymous namespace // Verify that deleting a watcher during the callback TEST_F(DirectoryWatcherTest, DeleteDuringNotify) { DirectoryWatcher* watcher = new DirectoryWatcher; Deleter deleter(watcher, &loop_); // Takes ownership of watcher. ASSERT_TRUE(watcher->Watch(test_dir_, &deleter, NULL, false)); ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); loop_.Run(); // We win if we haven't crashed yet. // Might as well double-check it got deleted, too. ASSERT_TRUE(deleter.watcher_.get() == NULL); } TEST_F(DirectoryWatcherTest, BackendLoop) { base::Thread thread("test"); ASSERT_TRUE(thread.Start()); DirectoryWatcher watcher; TestDelegate delegate(this); ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, thread.message_loop(), true)); } TEST_F(DirectoryWatcherTest, MultipleWatchersSingleFile) { DirectoryWatcher watcher1, watcher2; TestDelegate delegate1(this), delegate2(this); ASSERT_TRUE(watcher1.Watch(test_dir_, &delegate1, NULL, false)); ASSERT_TRUE(watcher2.Watch(test_dir_, &delegate2, NULL, false)); SetExpectedNumberOfNotifiedDelegates(2); ASSERT_TRUE(WriteTestFile(test_dir_.AppendASCII("test_file"), "content")); VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, MultipleWatchersDifferentFiles) { const int kNumberOfWatchers = 3; DirectoryWatcher watchers[kNumberOfWatchers]; TestDelegate delegates[kNumberOfWatchers] = { TestDelegate(this), TestDelegate(this), TestDelegate(this), }; FilePath subdirs[kNumberOfWatchers]; for (int i = 0; i < kNumberOfWatchers; i++) { subdirs[i] = CreateTestDirDirectoryASCII("Dir" + IntToString(i), false); ASSERT_TRUE(watchers[i].Watch(subdirs[i], &delegates[i], NULL, ((i % 2) == 0))); } for (int i = 0; i < kNumberOfWatchers; i++) { // Verify that we only get modifications from one watcher (each watcher has // different directory). for (int j = 0; j < kNumberOfWatchers; j++) delegates[j].reset(); // Write a file to the subdir. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdirs[i].AppendASCII("test_file"), "content")); VerifyExpectedNumberOfNotifiedDelegates(); VerifyNoExtraNotifications(); loop_.RunAllPending(); } } #if defined(OS_WIN) || defined(OS_MACOSX) // TODO(phajdan.jr): Enable when support for Linux recursive watches is added. TEST_F(DirectoryWatcherTest, WatchCreatedDirectory) { TestDelegate delegate(this); DirectoryWatcher watcher; ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, true)); SetExpectedNumberOfNotifiedDelegates(1); FilePath subdir(CreateTestDirDirectoryASCII("SubDir", true)); VerifyExpectedNumberOfNotifiedDelegates(); delegate.reset(); // Verify that changes inside the subdir are noticed. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdir.AppendASCII("test_file"), "some content")); VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, RecursiveWatchDeletedSubdirectory) { FilePath subdir(CreateTestDirDirectoryASCII("SubDir", true)); TestDelegate delegate(this); DirectoryWatcher watcher; ASSERT_TRUE(watcher.Watch(test_dir_, &delegate, NULL, true)); // Write a file to the subdir. SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdir.AppendASCII("test_file"), "some content")); VerifyExpectedNumberOfNotifiedDelegates(); delegate.reset(); SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(file_util::Delete(subdir, true)); VerifyExpectedNumberOfNotifiedDelegates(); } TEST_F(DirectoryWatcherTest, MoveFileAcrossWatches) { FilePath subdir1(CreateTestDirDirectoryASCII("SubDir1", true)); FilePath subdir2(CreateTestDirDirectoryASCII("SubDir2", true)); TestDelegate delegate1(this), delegate2(this); DirectoryWatcher watcher1, watcher2; ASSERT_TRUE(watcher1.Watch(subdir1, &delegate1, NULL, true)); ASSERT_TRUE(watcher2.Watch(subdir2, &delegate2, NULL, true)); SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdir1.AppendASCII("file"), "some content")); SyncIfPOSIX(); VerifyExpectedNumberOfNotifiedDelegates(); VerifyNoExtraNotifications(); delegate1.reset(); delegate2.reset(); SetExpectedNumberOfNotifiedDelegates(2); ASSERT_TRUE(file_util::Move(subdir1.AppendASCII("file"), subdir2.AppendASCII("file"))); VerifyExpectedNumberOfNotifiedDelegates(); delegate1.reset(); delegate2.reset(); SetExpectedNumberOfNotifiedDelegates(1); ASSERT_TRUE(WriteTestFile(subdir2.AppendASCII("file"), "other content")); VerifyExpectedNumberOfNotifiedDelegates(); VerifyNoExtraNotifications(); } #endif // defined(OS_WIN) || defined(OS_MACOSX) // Verify that watching a directory that doesn't exist fails, but doesn't // asssert. // Basic test: add a file and verify we notice it. TEST_F(DirectoryWatcherTest, NonExistentDirectory) { DirectoryWatcher watcher; ASSERT_FALSE(watcher.Watch(test_dir_.AppendASCII("does-not-exist"), NULL, NULL, false)); } } // namespace