// Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "base/files/file_path_watcher.h" #include #include #include #include #include "base/atomic_sequence_num.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/files/scoped_temp_dir.h" #include "base/functional/bind.h" #include "base/location.h" #include "base/logging.h" #include "base/run_loop.h" #include "base/sequence_checker.h" #include "base/strings/stringprintf.h" #include "base/task/bind_post_task.h" #include "base/task/sequenced_task_runner.h" #include "base/task/single_thread_task_runner.h" #include "base/test/bind.h" #include "base/test/run_until.h" #include "base/test/task_environment.h" #include "base/test/test_file_util.h" #include "base/test/test_timeouts.h" #include "base/thread_annotations.h" #include "base/threading/thread.h" #include "build/build_config.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #if BUILDFLAG(IS_WIN) #include #include #elif BUILDFLAG(IS_POSIX) #include #endif #if BUILDFLAG(IS_ANDROID) #include "base/android/path_utils.h" #endif // BUILDFLAG(IS_ANDROID) #if BUILDFLAG(IS_POSIX) #include "base/files/file_descriptor_watcher_posix.h" #endif // BUILDFLAG(IS_POSIX) #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) #include "base/files/file_path_watcher_inotify.h" #include "base/format_macros.h" #endif namespace base { namespace { AtomicSequenceNumber g_next_delegate_id; #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) // inotify fires two events - one for each file creation + modification. constexpr size_t kExpectedEventsForNewFileWrite = 2; #else constexpr size_t kExpectedEventsForNewFileWrite = 1; #endif enum class ExpectedEventsSinceLastWait { kNone, kSome }; struct Event { bool error; FilePath path; FilePathWatcher::ChangeInfo change_info; bool operator==(const Event& other) const { return error == other.error && path == other.path && change_info.file_path_type == other.change_info.file_path_type && change_info.change_type == other.change_info.change_type && // Don't compare the values of the cookies. change_info.cookie.has_value() == other.change_info.cookie.has_value(); } }; using EventListMatcher = testing::Matcher>; Event ToEvent(const FilePathWatcher::ChangeInfo& change_info, const FilePath& path, bool error) { return Event{.error = error, .path = path, .change_info = change_info}; } std::ostream& operator<<(std::ostream& os, const FilePathWatcher::ChangeType& change_type) { switch (change_type) { case FilePathWatcher::ChangeType::kUnsupported: return os << "unsupported"; case FilePathWatcher::ChangeType::kCreated: return os << "created"; case FilePathWatcher::ChangeType::kDeleted: return os << "deleted"; case FilePathWatcher::ChangeType::kModified: return os << "modified"; case FilePathWatcher::ChangeType::kMoved: return os << "moved"; } } std::ostream& operator<<(std::ostream& os, const FilePathWatcher::FilePathType& file_path_type) { switch (file_path_type) { case FilePathWatcher::FilePathType::kUnknown: return os << "Unknown"; case FilePathWatcher::FilePathType::kFile: return os << "File"; case FilePathWatcher::FilePathType::kDirectory: return os << "Directory"; } } std::ostream& operator<<(std::ostream& os, const FilePathWatcher::ChangeInfo& change_info) { return os << "ChangeInfo{ file_path_type: " << change_info.file_path_type << ", change_type: " << change_info.change_type << ", cookie: " << change_info.cookie.has_value() << " }"; } std::ostream& operator<<(std::ostream& os, const Event& event) { if (event.error) { return os << "Event{ ERROR }"; } return os << "Event{ path: " << event.path << ", change_info: " << event.change_info << " }"; } void SpinEventLoopForABit() { base::RunLoop loop; SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( FROM_HERE, loop.QuitClosure(), TestTimeouts::tiny_timeout()); loop.Run(); } // Returns the reason why `value` matches, or doesn't match, `matcher`. template std::string Explain(const MatcherType& matcher, const Value& value) { testing::StringMatchResultListener listener; testing::ExplainMatchResult(matcher, value, &listener); return listener.str(); } inline constexpr auto HasPath = [](const FilePath& path) { return testing::Field(&Event::path, path); }; inline constexpr auto HasErrored = []() { return testing::Field(&Event::error, testing::IsTrue()); }; inline constexpr auto HasCookie = []() { return testing::Field( &Event::change_info, testing::Field(&FilePathWatcher::ChangeInfo::cookie, testing::IsTrue())); }; inline constexpr auto IsType = [](const FilePathWatcher::ChangeType& change_type) { return testing::Field( &Event::change_info, testing::Field(&FilePathWatcher::ChangeInfo::change_type, change_type)); }; #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) inline constexpr auto IsFile = []() { return testing::Field( &Event::change_info, testing::Field(&FilePathWatcher::ChangeInfo::file_path_type, FilePathWatcher::FilePathType::kFile)); }; inline constexpr auto IsDirectory = []() { return testing::Field( &Event::change_info, testing::Field(&FilePathWatcher::ChangeInfo::file_path_type, FilePathWatcher::FilePathType::kDirectory)); }; #else inline constexpr auto IsUnknownPathType = []() { return testing::Field( &Event::change_info, testing::Field(&FilePathWatcher::ChangeInfo::file_path_type, FilePathWatcher::FilePathType::kUnknown)); }; #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || // BUILDFLAG(IS_ANDROID) // Enables an accumulative, add-as-you-go pattern for expecting events: // - Do something that should fire `event1` on `delegate` // - Add `event1` to an `accumulated_event_expecter` // - Wait until `delegate` matches { `event1` } // - Do something that should fire `event2` on `delegate` // - Add `event2` to an `accumulated_event_expecter` // - Wait until `delegate` matches { `event1`, `event2` } // - ... // // These tests use an accumulative pattern due to the potential for // false-positives, given that all we know is the number of changes at a given // path (which is often fixed) and whether or not an error occurred (which is // rare). // // TODO(https://crbug.com/1425601): This is not a common pattern. Generally, // expectations are specified all-in-one at the start of a test, like so: // - Expect events { `event1`, `event2` } // - Do something that should fire `event1` on `delegate` // - Do something that should fire `event2` on `delegate` // - Wait until `delegate` matches { `event1`, `event2` } // // The potential for false-positives is much less if event types are known. We // should consider moving towards the latter pattern // (see `FilePathWatcherWithChangeInfoTest`) once that is supported. class AccumulatingEventExpecter { public: EventListMatcher GetMatcher() { return testing::ContainerEq(expected_events_); } ExpectedEventsSinceLastWait GetAndResetExpectedEventsSinceLastWait() { auto temp = expected_events_since_last_wait_; expected_events_since_last_wait_ = ExpectedEventsSinceLastWait::kNone; return temp; } void AddExpectedEventForPath(const FilePath& path, bool error = false) { expected_events_.emplace_back(ToEvent({}, path, error)); expected_events_since_last_wait_ = ExpectedEventsSinceLastWait::kSome; } private: std::list expected_events_; ExpectedEventsSinceLastWait expected_events_since_last_wait_ = ExpectedEventsSinceLastWait::kNone; }; class TestDelegateBase { public: TestDelegateBase() = default; TestDelegateBase(const TestDelegateBase&) = delete; TestDelegateBase& operator=(const TestDelegateBase&) = delete; virtual ~TestDelegateBase() = default; virtual void OnFileChanged(const FilePath& path, bool error) = 0; virtual void OnFileChangedWithInfo( const FilePathWatcher::ChangeInfo& change_info, const FilePath& path, bool error) = 0; virtual base::WeakPtr AsWeakPtr() = 0; }; // Receives and accumulates notifications from a specific `FilePathWatcher`. // This class is not thread safe. All methods must be called from the sequence // the instance is constructed on. class TestDelegate final : public TestDelegateBase { public: TestDelegate() : id_(g_next_delegate_id.GetNext()) {} TestDelegate(const TestDelegate&) = delete; TestDelegate& operator=(const TestDelegate&) = delete; ~TestDelegate() override = default; // TestDelegateBase: void OnFileChanged(const FilePath& path, bool error) override { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); Event event = ToEvent({}, path, error); received_events_.emplace_back(std::move(event)); } void OnFileChangedWithInfo(const FilePathWatcher::ChangeInfo& change_info, const FilePath& path, bool error) override { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); Event event = ToEvent(change_info, path, error); received_events_.emplace_back(std::move(event)); } base::WeakPtr AsWeakPtr() override { return weak_ptr_factory_.GetWeakPtr(); } // Gives all in-flight events a chance to arrive, then forgets all events that // have been received by this delegate. This method may be a useful reset // after performing a file system operation that may result in a variable // sequence of events. void SpinAndDiscardAllReceivedEvents() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); SpinEventLoopForABit(); received_events_.clear(); } // Spin the event loop until `received_events_` match `matcher`, or we time // out. void RunUntilEventsMatch( const EventListMatcher& matcher, ExpectedEventsSinceLastWait expected_events_since_last_wait, const Location& location = FROM_HERE) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (expected_events_since_last_wait == ExpectedEventsSinceLastWait::kNone) { // Give unexpected events a chance to arrive. SpinEventLoopForABit(); } EXPECT_TRUE(test::RunUntil([&]() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return testing::Matches(matcher)(received_events_); })) << "Timed out attemping to match events at " << location.file_name() << ":" << location.line_number() << std::endl << Explain(matcher, received_events_); } // Convenience method for above. void RunUntilEventsMatch(const EventListMatcher& matcher, const Location& location = FROM_HERE) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return RunUntilEventsMatch(matcher, ExpectedEventsSinceLastWait::kSome, location); } // Convenience method for above. void RunUntilEventsMatch(AccumulatingEventExpecter& event_expecter, const Location& location = FROM_HERE) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return RunUntilEventsMatch( event_expecter.GetMatcher(), event_expecter.GetAndResetExpectedEventsSinceLastWait(), location); } // Convenience method for above when no events are expected. void SpinAndExpectNoEvents(const Location& location = FROM_HERE) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return RunUntilEventsMatch(testing::IsEmpty(), ExpectedEventsSinceLastWait::kNone, location); } const std::list& events() const { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); return received_events_; } private: SEQUENCE_CHECKER(sequence_checker_); // Uniquely generated ID used to tie events to this delegate. const size_t id_; std::list received_events_ GUARDED_BY_CONTEXT(sequence_checker_); base::WeakPtrFactory weak_ptr_factory_{this}; }; } // namespace #if BUILDFLAG(IS_FUCHSIA) // FilePatchWatcherImpl is not implemented (see crbug.com/851641). // Disable all tests. #define FilePathWatcherTest DISABLED_FilePathWatcherTest #endif class FilePathWatcherTest : public testing::Test { public: FilePathWatcherTest() #if BUILDFLAG(IS_POSIX) : task_environment_(test::TaskEnvironment::MainThreadType::IO) #endif { } FilePathWatcherTest(const FilePathWatcherTest&) = delete; FilePathWatcherTest& operator=(const FilePathWatcherTest&) = delete; ~FilePathWatcherTest() override = default; protected: void SetUp() override { #if BUILDFLAG(IS_ANDROID) // Watching files is only permitted when all parent directories are // accessible, which is not the case for the default temp directory // on Android which is under /data/data. Use /sdcard instead. // TODO(pauljensen): Remove this when crbug.com/475568 is fixed. FilePath parent_dir; ASSERT_TRUE(android::GetExternalStorageDirectory(&parent_dir)); ASSERT_TRUE(temp_dir_.CreateUniqueTempDirUnderPath(parent_dir)); #else // BUILDFLAG(IS_ANDROID) ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); #endif // BUILDFLAG(IS_ANDROID) } void TearDown() override { RunLoop().RunUntilIdle(); } FilePath test_file() { return temp_dir_.GetPath().AppendASCII("FilePathWatcherTest"); } FilePath test_link() { return temp_dir_.GetPath().AppendASCII("FilePathWatcherTest.lnk"); } bool SetupWatch(const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::Type watch_type); bool SetupWatchWithOptions(const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::WatchOptions watch_options); bool SetupWatchWithChangeInfo(const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::WatchOptions watch_options); test::TaskEnvironment task_environment_; ScopedTempDir temp_dir_; }; bool FilePathWatcherTest::SetupWatch(const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::Type watch_type) { return watcher->Watch( target, watch_type, BindRepeating(&TestDelegateBase::OnFileChanged, delegate->AsWeakPtr())); } bool FilePathWatcherTest::SetupWatchWithOptions( const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::WatchOptions watch_options) { return watcher->WatchWithOptions( target, watch_options, BindRepeating(&TestDelegateBase::OnFileChanged, delegate->AsWeakPtr())); } bool FilePathWatcherTest::SetupWatchWithChangeInfo( const FilePath& target, FilePathWatcher* watcher, TestDelegateBase* delegate, FilePathWatcher::WatchOptions watch_options) { return watcher->WatchWithChangeInfo( target, watch_options, BindPostTaskToCurrentDefault(BindRepeating( &TestDelegateBase::OnFileChangedWithInfo, delegate->AsWeakPtr()))); } // Basic test: Create the file and verify that we notice. TEST_F(FilePathWatcherTest, NewFile) { FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(WriteFile(test_file(), "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(test_file()); } delegate.RunUntilEventsMatch(event_expecter); } // Basic test: Create the directory and verify that we notice. TEST_F(FilePathWatcherTest, NewDirectory) { FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(CreateDirectory(test_file())); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } // Basic test: Create the directory and verify that we notice. TEST_F(FilePathWatcherTest, NewDirectoryRecursiveWatch) { if (!FilePathWatcher::RecursiveWatchAvailable()) { return; } FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kRecursive)); ASSERT_TRUE(CreateDirectory(test_file())); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that modifying the file is caught. TEST_F(FilePathWatcherTest, ModifiedFile) { ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is modified. ASSERT_TRUE(WriteFile(test_file(), "new content")); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that creating the parent directory of the watched file is not caught. TEST_F(FilePathWatcherTest, CreateParentDirectory) { FilePathWatcher watcher; TestDelegate delegate; FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); ASSERT_TRUE(SetupWatch(child, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we do not get notified when the parent is created. ASSERT_TRUE(CreateDirectory(parent)); delegate.SpinAndExpectNoEvents(); } // Verify that changes to the sibling of the watched file are not caught. TEST_F(FilePathWatcherTest, CreateSiblingFile) { FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we do not get notified if a sibling of the watched file is // created or modified. ASSERT_TRUE(WriteFile(test_file().AddExtensionASCII(".swap"), "content")); ASSERT_TRUE(WriteFile(test_file().AddExtensionASCII(".swap"), "new content")); delegate.SpinAndExpectNoEvents(); } // Verify that changes to the sibling of the parent of the watched file are not // caught. TEST_F(FilePathWatcherTest, CreateParentSiblingFile) { FilePathWatcher watcher; TestDelegate delegate; FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath parent_sibling(temp_dir_.GetPath().AppendASCII("parent_sibling")); FilePath child(parent.AppendASCII("child")); ASSERT_TRUE(SetupWatch(child, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Don't notice changes to a sibling directory of `parent` while `parent` does // not exist. ASSERT_TRUE(CreateDirectory(parent_sibling)); ASSERT_TRUE(DeletePathRecursively(parent_sibling)); // Don't notice changes to a sibling file of `parent` while `parent` does // not exist. ASSERT_TRUE(WriteFile(parent_sibling, "do not notice this")); ASSERT_TRUE(DeleteFile(parent_sibling)); // Don't notice the creation of `parent`. ASSERT_TRUE(CreateDirectory(parent)); // Don't notice changes to a sibling directory of `parent` while `parent` // exists. ASSERT_TRUE(CreateDirectory(parent_sibling)); ASSERT_TRUE(DeletePathRecursively(parent_sibling)); // Don't notice changes to a sibling file of `parent` while `parent` exists. ASSERT_TRUE(WriteFile(parent_sibling, "do not notice this")); ASSERT_TRUE(DeleteFile(parent_sibling)); delegate.SpinAndExpectNoEvents(); } // Verify that moving an unwatched file to a watched path is caught. TEST_F(FilePathWatcherTest, MovedToFile) { FilePath source_file(temp_dir_.GetPath().AppendASCII("source")); ASSERT_TRUE(WriteFile(source_file, "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is moved. ASSERT_TRUE(Move(source_file, test_file())); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that moving the watched file to an unwatched path is caught. TEST_F(FilePathWatcherTest, MovedFromFile) { ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is modified. ASSERT_TRUE(Move(test_file(), temp_dir_.GetPath().AppendASCII("dest"))); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, DeletedFile) { ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is deleted. DeleteFile(test_file()); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } namespace { // Used by the DeleteDuringNotify test below. // Deletes the FilePathWatcher when it's notified. class Deleter final : public TestDelegateBase { public: explicit Deleter(OnceClosure done_closure) : watcher_(std::make_unique()), done_closure_(std::move(done_closure)) {} Deleter(const Deleter&) = delete; Deleter& operator=(const Deleter&) = delete; ~Deleter() override = default; void OnFileChanged(const FilePath& /*path*/, bool /*error*/) override { watcher_.reset(); std::move(done_closure_).Run(); } void OnFileChangedWithInfo(const FilePathWatcher::ChangeInfo& /*change_info*/, const FilePath& /*path*/, bool /*error*/) override { watcher_.reset(); std::move(done_closure_).Run(); } base::WeakPtr AsWeakPtr() override { return weak_ptr_factory_.GetWeakPtr(); } FilePathWatcher* watcher() const { return watcher_.get(); } private: std::unique_ptr watcher_; OnceClosure done_closure_; base::WeakPtrFactory weak_ptr_factory_{this}; }; } // namespace // Verify that deleting a watcher during the callback doesn't crash. TEST_F(FilePathWatcherTest, DeleteDuringNotify) { RunLoop run_loop; Deleter deleter(run_loop.QuitClosure()); ASSERT_TRUE(SetupWatch(test_file(), deleter.watcher(), &deleter, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(WriteFile(test_file(), "content")); run_loop.Run(); // We win if we haven't crashed yet. // Might as well double-check it got deleted, too. ASSERT_TRUE(deleter.watcher() == nullptr); } // Verify that deleting the watcher works even if there is a pending // notification. TEST_F(FilePathWatcherTest, DestroyWithPendingNotification) { TestDelegate delegate; FilePathWatcher watcher; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(WriteFile(test_file(), "content")); } TEST_F(FilePathWatcherTest, MultipleWatchersSingleFile) { FilePathWatcher watcher1, watcher2; TestDelegate delegate1, delegate2; AccumulatingEventExpecter event_expecter1, event_expecter2; ASSERT_TRUE(SetupWatch(test_file(), &watcher1, &delegate1, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(SetupWatch(test_file(), &watcher2, &delegate2, FilePathWatcher::Type::kNonRecursive)); // Expect to be notified for writing to a new file for each delegate. ASSERT_TRUE(WriteFile(test_file(), "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter1.AddExpectedEventForPath(test_file()); event_expecter2.AddExpectedEventForPath(test_file()); } delegate1.RunUntilEventsMatch(event_expecter1); delegate2.RunUntilEventsMatch(event_expecter2); } // Verify that watching a file whose parent directory doesn't exist yet works if // the directory and file are created eventually. TEST_F(FilePathWatcherTest, NonExistentDirectory) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file(dir.AppendASCII("file")); TestDelegate delegate; ASSERT_TRUE(SetupWatch(file, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // The delegate is only watching the file. Parent directory creation should // not trigger an event. ASSERT_TRUE(CreateDirectory(dir)); // TODO(https://crbug.com/1432064): Expect that no events are fired. // It may take some time for `watcher` to re-construct its watch list, so it's // possible an event is missed. _At least_ one event should be fired, though. ASSERT_TRUE(WriteFile(file, "content")); VLOG(1) << "Waiting for file creation"; delegate.RunUntilEventsMatch(testing::Not(testing::IsEmpty()), ExpectedEventsSinceLastWait::kSome); delegate.SpinAndDiscardAllReceivedEvents(); AccumulatingEventExpecter event_expecter; ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file change"; event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(DeleteFile(file)); VLOG(1) << "Waiting for file deletion"; event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); } // Exercises watch reconfiguration for the case that directories on the path // are rapidly created. TEST_F(FilePathWatcherTest, DirectoryChain) { FilePath path(temp_dir_.GetPath()); std::vector dir_names; for (int i = 0; i < 20; i++) { std::string dir(StringPrintf("d%d", i)); dir_names.push_back(dir); path = path.AppendASCII(dir); } FilePathWatcher watcher; FilePath file(path.AppendASCII("file")); TestDelegate delegate; ASSERT_TRUE(SetupWatch(file, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); FilePath sub_path(temp_dir_.GetPath()); for (const auto& dir_name : dir_names) { sub_path = sub_path.AppendASCII(dir_name); ASSERT_TRUE(CreateDirectory(sub_path)); // TODO(https://crbug.com/1432064): Expect that no events are fired. } // It may take some time for `watcher` to re-construct its watch list, so it's // possible an event is missed. _At least_ one event should be fired, though. VLOG(1) << "Create File"; ASSERT_TRUE(WriteFile(file, "content")); VLOG(1) << "Waiting for file creation + modification"; delegate.RunUntilEventsMatch(testing::Not(testing::IsEmpty()), ExpectedEventsSinceLastWait::kSome); delegate.SpinAndDiscardAllReceivedEvents(); AccumulatingEventExpecter event_expecter; ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file modification"; event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, DisappearingDirectory) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file(dir.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(WriteFile(file, "content")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(file, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(DeletePathRecursively(dir)); event_expecter.AddExpectedEventForPath(file); #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1432044): Figure out why this may fire two events on // inotify. Only the file is being watched, so presumably there should only be // one deletion event. event_expecter.AddExpectedEventForPath(file); #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || // BUILDFLAG(IS_ANDROID) delegate.RunUntilEventsMatch(event_expecter); } // Tests that a file that is deleted and reappears is tracked correctly. TEST_F(FilePathWatcherTest, DeleteAndRecreate) { ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(DeleteFile(test_file())); VLOG(1) << "Waiting for file deletion"; event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(test_file(), "content")); VLOG(1) << "Waiting for file creation + modification"; for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(test_file()); } delegate.RunUntilEventsMatch(event_expecter); } // TODO(https://crbug.com/1432064): Split into smaller tests. TEST_F(FilePathWatcherTest, WatchDirectory) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file1(dir.AppendASCII("file1")); FilePath file2(dir.AppendASCII("file2")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(dir, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(CreateDirectory(dir)); VLOG(1) << "Waiting for directory creation"; event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(file1, "content")); VLOG(1) << "Waiting for file1 creation + modification"; for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(dir); } delegate.RunUntilEventsMatch(event_expecter); #if !BUILDFLAG(IS_APPLE) ASSERT_TRUE(WriteFile(file1, "content v2")); // Mac implementation does not detect files modified in a directory. // TODO(https://crbug.com/1432064): Expect that no events are fired on Mac. // TODO(https://crbug.com/1019297): Consider using FSEvents to support // watching a directory and its immediate children, as Type::kNonRecursive // does on other platforms. VLOG(1) << "Waiting for file1 modification"; event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); #endif // !BUILDFLAG(IS_APPLE) ASSERT_TRUE(DeleteFile(file1)); VLOG(1) << "Waiting for file1 deletion"; event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(file2, "content")); VLOG(1) << "Waiting for file2 creation + modification"; for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(dir); } delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, MoveParent) { FilePathWatcher file_watcher, subdir_watcher; TestDelegate file_delegate, subdir_delegate; AccumulatingEventExpecter file_event_expecter, subdir_event_expecter; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath dest(temp_dir_.GetPath().AppendASCII("dest")); FilePath subdir(dir.AppendASCII("subdir")); FilePath file(subdir.AppendASCII("file")); ASSERT_TRUE(SetupWatch(file, &file_watcher, &file_delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(SetupWatch(subdir, &subdir_watcher, &subdir_delegate, FilePathWatcher::Type::kNonRecursive)); // Setup a directory hierarchy. // We should only get notified on `subdir_delegate` of its creation. ASSERT_TRUE(CreateDirectory(subdir)); subdir_event_expecter.AddExpectedEventForPath(subdir); // TODO(https://crbug.com/1432064): Expect that no events are fired on the // file delegate. subdir_delegate.RunUntilEventsMatch(subdir_event_expecter); ASSERT_TRUE(WriteFile(file, "content")); VLOG(1) << "Waiting for file creation + modification"; for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { file_event_expecter.AddExpectedEventForPath(file); subdir_event_expecter.AddExpectedEventForPath(subdir); } file_delegate.RunUntilEventsMatch(file_event_expecter); subdir_delegate.RunUntilEventsMatch(subdir_event_expecter); Move(dir, dest); VLOG(1) << "Waiting for directory move"; file_event_expecter.AddExpectedEventForPath(file); subdir_event_expecter.AddExpectedEventForPath(subdir); file_delegate.RunUntilEventsMatch(file_event_expecter); subdir_delegate.RunUntilEventsMatch(subdir_event_expecter); } TEST_F(FilePathWatcherTest, RecursiveWatch) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; bool setup_result = SetupWatch(dir, &watcher, &delegate, FilePathWatcher::Type::kRecursive); if (!FilePathWatcher::RecursiveWatchAvailable()) { ASSERT_FALSE(setup_result); return; } ASSERT_TRUE(setup_result); // TODO(https://crbug.com/1432064): Create a version of this test which also // verifies that the events occur on the correct file path if the watcher is // set up to record the target of the event. // Main directory("dir") creation. ASSERT_TRUE(CreateDirectory(dir)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Create "$dir/file1". FilePath file1(dir.AppendASCII("file1")); ASSERT_TRUE(WriteFile(file1, "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(dir); } delegate.RunUntilEventsMatch(event_expecter); // Create "$dir/subdir". FilePath subdir(dir.AppendASCII("subdir")); ASSERT_TRUE(CreateDirectory(subdir)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Create "$dir/subdir/subdir2". FilePath subdir2(subdir.AppendASCII("subdir2")); ASSERT_TRUE(CreateDirectory(subdir2)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Rename "$dir/subdir/subdir2" to "$dir/subdir/subdir2b". FilePath subdir2b(subdir.AppendASCII("subdir2b")); Move(subdir2, subdir2b); event_expecter.AddExpectedEventForPath(dir); #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) // inotify generates separate IN_MOVED_TO and IN_MOVED_FROM events for a // rename. Since both the source and destination are within the scope of this // watch, both events should be received. event_expecter.AddExpectedEventForPath(dir); #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || // BUILDFLAG(IS_ANDROID) delegate.RunUntilEventsMatch(event_expecter); // Mac and Win don't generate events for Touch. // TODO(https://crbug.com/1432064): Add explicit expectations for Mac and Win. // Android TouchFile returns false. #if !(BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)) // Touch "$dir". Time access_time; ASSERT_TRUE(Time::FromString("Wed, 16 Nov 1994, 00:00:00", &access_time)); ASSERT_TRUE(TouchFile(dir, access_time, access_time)); // TODO(https://crbug.com/1432044): Investigate why we're getting two events // here from inotify. event_expecter.AddExpectedEventForPath(dir); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // TODO(https://crbug.com/1432064): Add a test touching `subdir`. #endif // !(BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)) // Create "$dir/subdir/subdir_file1". FilePath subdir_file1(subdir.AppendASCII("subdir_file1")); ASSERT_TRUE(WriteFile(subdir_file1, "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(dir); } delegate.RunUntilEventsMatch(event_expecter); // Create "$dir/subdir/subdir_child_dir". FilePath subdir_child_dir(subdir.AppendASCII("subdir_child_dir")); ASSERT_TRUE(CreateDirectory(subdir_child_dir)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Create "$dir/subdir/subdir_child_dir/child_dir_file1". FilePath child_dir_file1(subdir_child_dir.AppendASCII("child_dir_file1")); ASSERT_TRUE(WriteFile(child_dir_file1, "content v2")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(dir); } delegate.RunUntilEventsMatch(event_expecter); // Write into "$dir/subdir/subdir_child_dir/child_dir_file1". ASSERT_TRUE(WriteFile(child_dir_file1, "content")); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Apps cannot change file attributes on Android in /sdcard as /sdcard uses the // "fuse" file system, while /data uses "ext4". Running these tests in /data // would be preferable and allow testing file attributes and symlinks. // TODO(pauljensen): Re-enable when crbug.com/475568 is fixed and SetUp() places // the |temp_dir_| in /data. #if !BUILDFLAG(IS_ANDROID) // Modify "$dir/subdir/subdir_child_dir/child_dir_file1" attributes. ASSERT_TRUE(MakeFileUnreadable(child_dir_file1)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); #endif // !BUILDFLAG(IS_ANDROID)) // Delete "$dir/subdir/subdir_file1". ASSERT_TRUE(DeleteFile(subdir_file1)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); // Delete "$dir/subdir/subdir_child_dir/child_dir_file1". ASSERT_TRUE(DeleteFile(child_dir_file1)); event_expecter.AddExpectedEventForPath(dir); delegate.RunUntilEventsMatch(event_expecter); } #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID) // Apps cannot create symlinks on Android in /sdcard as /sdcard uses the // "fuse" file system, while /data uses "ext4". Running these tests in /data // would be preferable and allow testing file attributes and symlinks. // TODO(pauljensen): Re-enable when crbug.com/475568 is fixed and SetUp() places // the |temp_dir_| in /data. // // This test is disabled on Fuchsia since it doesn't support symlinking. TEST_F(FilePathWatcherTest, RecursiveWithSymLink) { if (!FilePathWatcher::RecursiveWatchAvailable()) { return; } FilePathWatcher watcher; FilePath test_dir(temp_dir_.GetPath().AppendASCII("test_dir")); ASSERT_TRUE(CreateDirectory(test_dir)); FilePath symlink(test_dir.AppendASCII("symlink")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(symlink, &watcher, &delegate, FilePathWatcher::Type::kRecursive)); // TODO(https://crbug.com/1432064): Figure out what the intended behavior here // is. Many symlink operations don't seem to be supported on Mac, while in // other cases Mac fires more events than expected. // Link creation. FilePath target1(temp_dir_.GetPath().AppendASCII("target1")); ASSERT_TRUE(CreateSymbolicLink(target1, symlink)); event_expecter.AddExpectedEventForPath(symlink); delegate.RunUntilEventsMatch(event_expecter); // Target1 creation. ASSERT_TRUE(CreateDirectory(target1)); event_expecter.AddExpectedEventForPath(symlink); delegate.RunUntilEventsMatch(event_expecter); // Create a file in target1. FilePath target1_file(target1.AppendASCII("file")); ASSERT_TRUE(WriteFile(target1_file, "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(symlink); } delegate.RunUntilEventsMatch(event_expecter); // Link change. FilePath target2(temp_dir_.GetPath().AppendASCII("target2")); ASSERT_TRUE(CreateDirectory(target2)); // TODO(https://crbug.com/1432064): Expect that no events are fired. ASSERT_TRUE(DeleteFile(symlink)); event_expecter.AddExpectedEventForPath(symlink); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(CreateSymbolicLink(target2, symlink)); event_expecter.AddExpectedEventForPath(symlink); delegate.RunUntilEventsMatch(event_expecter); // Create a file in target2. FilePath target2_file(target2.AppendASCII("file")); ASSERT_TRUE(WriteFile(target2_file, "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(symlink); } delegate.RunUntilEventsMatch(event_expecter); } #endif // BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID) TEST_F(FilePathWatcherTest, MoveChild) { FilePathWatcher file_watcher, subdir_watcher; TestDelegate file_delegate, subdir_delegate; AccumulatingEventExpecter file_event_expecter, subdir_event_expecter; FilePath source_dir(temp_dir_.GetPath().AppendASCII("source")); FilePath source_subdir(source_dir.AppendASCII("subdir")); FilePath source_file(source_subdir.AppendASCII("file")); FilePath dest_dir(temp_dir_.GetPath().AppendASCII("dest")); FilePath dest_subdir(dest_dir.AppendASCII("subdir")); FilePath dest_file(dest_subdir.AppendASCII("file")); // Setup a directory hierarchy. ASSERT_TRUE(CreateDirectory(source_subdir)); ASSERT_TRUE(WriteFile(source_file, "content")); ASSERT_TRUE(SetupWatch(dest_file, &file_watcher, &file_delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(SetupWatch(dest_subdir, &subdir_watcher, &subdir_delegate, FilePathWatcher::Type::kNonRecursive)); // Move the directory into place, s.t. the watched file appears. ASSERT_TRUE(Move(source_dir, dest_dir)); file_event_expecter.AddExpectedEventForPath(dest_file); subdir_event_expecter.AddExpectedEventForPath(dest_subdir); file_delegate.RunUntilEventsMatch(file_event_expecter); subdir_delegate.RunUntilEventsMatch(subdir_event_expecter); } // Verify that changing attributes on a file is caught #if BUILDFLAG(IS_ANDROID) // Apps cannot change file attributes on Android in /sdcard as /sdcard uses the // "fuse" file system, while /data uses "ext4". Running these tests in /data // would be preferable and allow testing file attributes and symlinks. // TODO(pauljensen): Re-enable when crbug.com/475568 is fixed and SetUp() places // the |temp_dir_| in /data. #define FileAttributesChanged DISABLED_FileAttributesChanged #endif // BUILDFLAG(IS_ANDROID) TEST_F(FilePathWatcherTest, FileAttributesChanged) { ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is modified. ASSERT_TRUE(MakeFileUnreadable(test_file())); event_expecter.AddExpectedEventForPath(test_file()); delegate.RunUntilEventsMatch(event_expecter); } #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) // Verify that creating a symlink is caught. TEST_F(FilePathWatcherTest, CreateLink) { FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; // Note that we are watching the symlink. ASSERT_TRUE(SetupWatch(test_link(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the link is created. // Note that test_file() doesn't have to exist. ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); event_expecter.AddExpectedEventForPath(test_link()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that deleting a symlink is caught. TEST_F(FilePathWatcherTest, DeleteLink) { // Unfortunately this test case only works if the link target exists. // TODO(craig) fix this as part of crbug.com/91561. ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_link(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the link is deleted. ASSERT_TRUE(DeleteFile(test_link())); event_expecter.AddExpectedEventForPath(test_link()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that modifying a target file that a link is pointing to // when we are watching the link is caught. TEST_F(FilePathWatcherTest, ModifiedLinkedFile) { ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; // Note that we are watching the symlink. ASSERT_TRUE(SetupWatch(test_link(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the file is modified. ASSERT_TRUE(WriteFile(test_file(), "new content")); event_expecter.AddExpectedEventForPath(test_link()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that creating a target file that a link is pointing to // when we are watching the link is caught. TEST_F(FilePathWatcherTest, CreateTargetLinkedFile) { ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; // Note that we are watching the symlink. ASSERT_TRUE(SetupWatch(test_link(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the target file is created. ASSERT_TRUE(WriteFile(test_file(), "content")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(test_link()); } delegate.RunUntilEventsMatch(event_expecter); } // Verify that deleting a target file that a link is pointing to // when we are watching the link is caught. TEST_F(FilePathWatcherTest, DeleteTargetLinkedFile) { ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; // Note that we are watching the symlink. ASSERT_TRUE(SetupWatch(test_link(), &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // Now make sure we get notified if the target file is deleted. ASSERT_TRUE(DeleteFile(test_file())); event_expecter.AddExpectedEventForPath(test_link()); delegate.RunUntilEventsMatch(event_expecter); } // Verify that watching a file whose parent directory is a link that // doesn't exist yet works if the symlink is created eventually. TEST_F(FilePathWatcherTest, LinkedDirectoryPart1) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; // dir/file should exist. ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(WriteFile(file, "content")); // Note that we are watching dir.lnk/file which doesn't exist yet. ASSERT_TRUE(SetupWatch(linkfile, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); VLOG(1) << "Waiting for link creation"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file creation + modification"; // TODO(https://crbug.com/1432064): Should this fire two events on inotify? event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file change"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(DeleteFile(file)); VLOG(1) << "Waiting for file deletion"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); } // Verify that watching a file whose parent directory is a // dangling symlink works if the directory is created eventually. // TODO(https://crbug.com/1432064): Add test coverage for symlinked file // creation independent of a corresponding write. TEST_F(FilePathWatcherTest, LinkedDirectoryPart2) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); TestDelegate delegate; // Now create the link from dir.lnk pointing to dir but // neither dir nor dir/file exist yet. ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); // Note that we are watching dir.lnk/file. ASSERT_TRUE(SetupWatch(linkfile, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(CreateDirectory(dir)); // TODO(https://crbug.com/1432064): Expect that no events are fired. // It may take some time for `watcher` to re-construct its watch list, so it's // possible an event is missed. _At least_ one event should be fired, though. ASSERT_TRUE(WriteFile(file, "content")); VLOG(1) << "Waiting for file creation"; delegate.RunUntilEventsMatch(testing::Not(testing::IsEmpty()), ExpectedEventsSinceLastWait::kSome); delegate.SpinAndDiscardAllReceivedEvents(); AccumulatingEventExpecter event_expecter; ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file change"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(DeleteFile(file)); VLOG(1) << "Waiting for file deletion"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); } // Verify that watching a file with a symlink on the path // to the file works. TEST_F(FilePathWatcherTest, LinkedDirectoryPart3) { FilePathWatcher watcher; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); // Note that we are watching dir.lnk/file but the file doesn't exist yet. ASSERT_TRUE(SetupWatch(linkfile, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); ASSERT_TRUE(WriteFile(file, "content")); VLOG(1) << "Waiting for file creation"; for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(linkfile); } delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(WriteFile(file, "content v2")); VLOG(1) << "Waiting for file change"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(DeleteFile(file)); VLOG(1) << "Waiting for file deletion"; event_expecter.AddExpectedEventForPath(linkfile); delegate.RunUntilEventsMatch(event_expecter); } // Regression tests that FilePathWatcherImpl does not leave its reference in // `g_inotify_reader` due to a race in recursive watch. // See https://crbug.com/990004. TEST_F(FilePathWatcherTest, RacyRecursiveWatch) { if (!FilePathWatcher::RecursiveWatchAvailable()) { GTEST_SKIP(); } FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); // Create and delete many subdirs. 20 is an arbitrary number big enough // to have more chances to make FilePathWatcherImpl leak watchers. std::vector subdirs; for (int i = 0; i < 20; ++i) { subdirs.emplace_back(dir.AppendASCII(StringPrintf("subdir_%d", i))); } Thread subdir_updater("SubDir Updater"); ASSERT_TRUE(subdir_updater.Start()); auto subdir_update_task = BindLambdaForTesting([&]() { for (const auto& subdir : subdirs) { // First update event to trigger watch callback. ASSERT_TRUE(CreateDirectory(subdir)); // Second update event. The notification sent for this event will race // with the upcoming deletion of the directory below. This test is about // verifying that the impl handles this. FilePath subdir_file(subdir.AppendASCII("subdir_file")); ASSERT_TRUE(WriteFile(subdir_file, "content")); // Racy subdir delete to trigger watcher leak. ASSERT_TRUE(DeletePathRecursively(subdir)); } }); // Try the racy subdir update 100 times. for (int i = 0; i < 100; ++i) { RunLoop run_loop; auto watcher = std::make_unique(); // Keep watch callback in `watcher_callback` so that "watcher.reset()" // inside does not release the callback and the lambda capture with it. // Otherwise, accessing `run_loop` as part of the lamda capture would be // use-after-free under asan. auto watcher_callback = BindLambdaForTesting([&](const FilePath& path, bool error) { // Release watchers in callback so that the leaked watchers of // the subdir stays. Otherwise, when the subdir is deleted, // its delete event would clean up leaked watchers in // `g_inotify_reader`. watcher.reset(); run_loop.Quit(); }); bool setup_result = watcher->Watch(dir, FilePathWatcher::Type::kRecursive, watcher_callback); ASSERT_TRUE(setup_result); subdir_updater.task_runner()->PostTask(FROM_HERE, subdir_update_task); // Wait for the watch callback. run_loop.Run(); // `watcher` should have been released. ASSERT_FALSE(watcher); // There should be no outstanding watchers. ASSERT_FALSE(FilePathWatcher::HasWatchesForTest()); } } // Verify that "Watch()" returns false and callback is not invoked when limit is // hit during setup. TEST_F(FilePathWatcherTest, InotifyLimitInWatch) { auto watcher = std::make_unique(); // "test_file()" is like "/tmp/__unique_path__/FilePathWatcherTest" and has 4 // dir components ("/" + 3 named parts). "Watch()" creates inotify watches // for each dir component of the given dir. It would fail with limit set to 1. ScopedMaxNumberOfInotifyWatchesOverrideForTest max_inotify_watches(1); ASSERT_FALSE( watcher->Watch(test_file(), FilePathWatcher::Type::kNonRecursive, BindLambdaForTesting([&](const FilePath& path, bool error) { ADD_FAILURE(); }))); // Triggers update but callback should not be invoked. ASSERT_TRUE(WriteFile(test_file(), "content")); // Ensures that the callback did not happen. RunLoop().RunUntilIdle(); } // Verify that "error=true" callback happens when limit is hit during update. TEST_F(FilePathWatcherTest, InotifyLimitInUpdate) { enum kTestType { // Destroy watcher in "error=true" callback. // No crash/deadlock when releasing watcher in the callback. kDestroyWatcher, // Do not destroy watcher in "error=true" callback. kDoNothing, }; for (auto callback_type : {kDestroyWatcher, kDoNothing}) { SCOPED_TRACE(testing::Message() << "type=" << callback_type); RunLoop run_loop; auto watcher = std::make_unique(); bool error_callback_called = false; auto watcher_callback = BindLambdaForTesting([&](const FilePath& path, bool error) { // No callback should happen after "error=true" one. ASSERT_FALSE(error_callback_called); if (!error) { return; } error_callback_called = true; if (callback_type == kDestroyWatcher) { watcher.reset(); } run_loop.Quit(); }); ASSERT_TRUE(watcher->Watch( test_file(), FilePathWatcher::Type::kNonRecursive, watcher_callback)); ScopedMaxNumberOfInotifyWatchesOverrideForTest max_inotify_watches(1); // Triggers update and over limit. ASSERT_TRUE(WriteFile(test_file(), "content")); run_loop.Run(); // More update but no more callback should happen. ASSERT_TRUE(DeleteFile(test_file())); RunLoop().RunUntilIdle(); } } // Similar to InotifyLimitInUpdate but test a recursive watcher. TEST_F(FilePathWatcherTest, InotifyLimitInUpdateRecursive) { enum kTestType { // Destroy watcher in "error=true" callback. // No crash/deadlock when releasing watcher in the callback. kDestroyWatcher, // Do not destroy watcher in "error=true" callback. kDoNothing, }; FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); for (auto callback_type : {kDestroyWatcher, kDoNothing}) { SCOPED_TRACE(testing::Message() << "type=" << callback_type); RunLoop run_loop; auto watcher = std::make_unique(); bool error_callback_called = false; auto watcher_callback = BindLambdaForTesting([&](const FilePath& path, bool error) { // No callback should happen after "error=true" one. ASSERT_FALSE(error_callback_called); if (!error) { return; } error_callback_called = true; if (callback_type == kDestroyWatcher) { watcher.reset(); } run_loop.Quit(); }); ASSERT_TRUE(watcher->Watch(dir, FilePathWatcher::Type::kRecursive, watcher_callback)); constexpr size_t kMaxLimit = 10u; ScopedMaxNumberOfInotifyWatchesOverrideForTest max_inotify_watches( kMaxLimit); // Triggers updates and over limit. for (size_t i = 0; i < kMaxLimit; ++i) { FilePath subdir = dir.AppendASCII(StringPrintf("subdir_%" PRIuS, i)); ASSERT_TRUE(CreateDirectory(subdir)); } run_loop.Run(); // More update but no more callback should happen. for (size_t i = 0; i < kMaxLimit; ++i) { FilePath subdir = dir.AppendASCII(StringPrintf("subdir_%" PRIuS, i)); ASSERT_TRUE(DeleteFile(subdir)); } RunLoop().RunUntilIdle(); } } #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) // TODO(fxbug.dev/60109): enable BUILDFLAG(IS_FUCHSIA) when implemented. #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) TEST_F(FilePathWatcherTest, ReturnFullPath_RecursiveInRootFolder) { FilePathWatcher directory_watcher; FilePath watched_folder(temp_dir_.GetPath().AppendASCII("watched_folder")); FilePath file(watched_folder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kRecursive, .report_modified_path = true})); // Triggers two events: // create on /watched_folder/file. // modify on /watched_folder/file. ASSERT_TRUE(WriteFile(file, "test")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(file); } delegate.RunUntilEventsMatch(event_expecter); // Expects modify on /watched_folder/file. ASSERT_TRUE(WriteFile(file, "test123")); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder/file. ASSERT_TRUE(DeleteFile(file)); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, ReturnFullPath_RecursiveInNestedFolder) { FilePathWatcher directory_watcher; FilePath watched_folder(temp_dir_.GetPath().AppendASCII("watched_folder")); FilePath subfolder(watched_folder.AppendASCII("subfolder")); FilePath file(subfolder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kRecursive, .report_modified_path = true})); // Expects create on /watched_folder/subfolder. ASSERT_TRUE(CreateDirectory(subfolder)); event_expecter.AddExpectedEventForPath(subfolder); delegate.RunUntilEventsMatch(event_expecter); // Triggers two events: // create on /watched_folder/subfolder/file. // modify on /watched_folder/subfolder/file. ASSERT_TRUE(WriteFile(file, "test")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(file); } delegate.RunUntilEventsMatch(event_expecter); // Expects modify on /watched_folder/subfolder/file. ASSERT_TRUE(WriteFile(file, "test123")); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder/subfolder/file. ASSERT_TRUE(DeleteFile(file)); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder/subfolder. ASSERT_TRUE(DeleteFile(subfolder)); event_expecter.AddExpectedEventForPath(subfolder); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, ReturnFullPath_NonRecursiveInRootFolder) { FilePathWatcher directory_watcher; FilePath watched_folder(temp_dir_.GetPath().AppendASCII("watched_folder")); FilePath file(watched_folder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE( SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kNonRecursive, .report_modified_path = true})); // Triggers two events: // create on /watched_folder/file. // modify on /watched_folder/file. ASSERT_TRUE(WriteFile(file, "test")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(file); } delegate.RunUntilEventsMatch(event_expecter); // Expects modify on /watched_folder/file. ASSERT_TRUE(WriteFile(file, "test123")); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder/file. ASSERT_TRUE(DeleteFile(file)); event_expecter.AddExpectedEventForPath(file); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, ReturnFullPath_NonRecursiveRemoveEnclosingFolder) { FilePathWatcher directory_watcher; FilePath root_folder(temp_dir_.GetPath().AppendASCII("root_folder")); FilePath folder(root_folder.AppendASCII("folder")); FilePath watched_folder(folder.AppendASCII("watched_folder")); FilePath file(watched_folder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); ASSERT_TRUE(WriteFile(file, "test")); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE( SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kNonRecursive, .report_modified_path = true})); // Triggers three events: // delete on /watched_folder/file. // delete on /watched_folder twice. // TODO(https://crbug.com/1432044): Figure out why duplicate events are fired // on `watched_folder`. ASSERT_TRUE(DeletePathRecursively(folder)); event_expecter.AddExpectedEventForPath(file); event_expecter.AddExpectedEventForPath(watched_folder); event_expecter.AddExpectedEventForPath(watched_folder); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, ReturnWatchedPath_RecursiveInRootFolder) { FilePathWatcher directory_watcher; FilePath watched_folder(temp_dir_.GetPath().AppendASCII("watched_folder")); FilePath file(watched_folder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE( SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kRecursive})); // Triggers two events: // create on /watched_folder. // modify on /watched_folder. ASSERT_TRUE(WriteFile(file, "test")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(watched_folder); } delegate.RunUntilEventsMatch(event_expecter); // Expects modify on /watched_folder. ASSERT_TRUE(WriteFile(file, "test123")); event_expecter.AddExpectedEventForPath(watched_folder); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder. ASSERT_TRUE(DeleteFile(file)); event_expecter.AddExpectedEventForPath(watched_folder); delegate.RunUntilEventsMatch(event_expecter); } TEST_F(FilePathWatcherTest, ReturnWatchedPath_NonRecursiveInRootFolder) { FilePathWatcher directory_watcher; FilePath watched_folder(temp_dir_.GetPath().AppendASCII("watched_folder")); FilePath file(watched_folder.AppendASCII("file")); ASSERT_TRUE(CreateDirectory(watched_folder)); TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE( SetupWatchWithOptions(watched_folder, &directory_watcher, &delegate, {.type = FilePathWatcher::Type::kNonRecursive})); // Triggers two events: // Expects create /watched_folder. // Expects modify /watched_folder. ASSERT_TRUE(WriteFile(file, "test")); for (size_t i = 0; i < kExpectedEventsForNewFileWrite; ++i) { event_expecter.AddExpectedEventForPath(watched_folder); } delegate.RunUntilEventsMatch(event_expecter); // Expects modify on /watched_folder. ASSERT_TRUE(WriteFile(file, "test123")); event_expecter.AddExpectedEventForPath(watched_folder); delegate.RunUntilEventsMatch(event_expecter); // Expects delete on /watched_folder. ASSERT_TRUE(DeleteFile(file)); event_expecter.AddExpectedEventForPath(watched_folder); delegate.RunUntilEventsMatch(event_expecter); } #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || // BUILDFLAG(IS_ANDROID) namespace { enum Permission { Read, Write, Execute }; #if BUILDFLAG(IS_APPLE) bool ChangeFilePermissions(const FilePath& path, Permission perm, bool allow) { struct stat stat_buf; if (stat(path.value().c_str(), &stat_buf) != 0) { return false; } mode_t mode = 0; switch (perm) { case Read: mode = S_IRUSR | S_IRGRP | S_IROTH; break; case Write: mode = S_IWUSR | S_IWGRP | S_IWOTH; break; case Execute: mode = S_IXUSR | S_IXGRP | S_IXOTH; break; default: ADD_FAILURE() << "unknown perm " << perm; return false; } if (allow) { stat_buf.st_mode |= mode; } else { stat_buf.st_mode &= ~mode; } return chmod(path.value().c_str(), stat_buf.st_mode) == 0; } #endif // BUILDFLAG(IS_APPLE) } // namespace #if BUILDFLAG(IS_APPLE) // Linux implementation of FilePathWatcher doesn't catch attribute changes. // http://crbug.com/78043 // Windows implementation of FilePathWatcher catches attribute changes that // don't affect the path being watched. // http://crbug.com/78045 // Verify that changing attributes on a directory works. TEST_F(FilePathWatcherTest, DirAttributesChanged) { FilePath test_dir1( temp_dir_.GetPath().AppendASCII("DirAttributesChangedDir1")); FilePath test_dir2(test_dir1.AppendASCII("DirAttributesChangedDir2")); FilePath test_file(test_dir2.AppendASCII("DirAttributesChangedFile")); // Setup a directory hierarchy. ASSERT_TRUE(CreateDirectory(test_dir1)); ASSERT_TRUE(CreateDirectory(test_dir2)); ASSERT_TRUE(WriteFile(test_file, "content")); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(test_file, &watcher, &delegate, FilePathWatcher::Type::kNonRecursive)); // We should not get notified in this case as it hasn't affected our ability // to access the file. ASSERT_TRUE(ChangeFilePermissions(test_dir1, Read, false)); ASSERT_TRUE(ChangeFilePermissions(test_dir1, Read, true)); // TODO(https://crbug.com/1432064): Expect that no events are fired. // We should get notified in this case because filepathwatcher can no // longer access the file. ASSERT_TRUE(ChangeFilePermissions(test_dir1, Execute, false)); event_expecter.AddExpectedEventForPath(test_file); delegate.RunUntilEventsMatch(event_expecter); ASSERT_TRUE(ChangeFilePermissions(test_dir1, Execute, true)); // TODO(https://crbug.com/1432064): Expect that no events are fired. } #endif // BUILDFLAG(IS_APPLE) #if BUILDFLAG(IS_APPLE) // Fail fast if trying to trivially watch a non-existent item. TEST_F(FilePathWatcherTest, TrivialNoDir) { const FilePath tmp_dir = temp_dir_.GetPath(); const FilePath non_existent = tmp_dir.Append(FILE_PATH_LITERAL("nope")); FilePathWatcher watcher; TestDelegate delegate; ASSERT_FALSE(SetupWatch(non_existent, &watcher, &delegate, FilePathWatcher::Type::kTrivial)); } // Succeed starting a watch on a directory. TEST_F(FilePathWatcherTest, TrivialDirStart) { const FilePath tmp_dir = temp_dir_.GetPath(); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatch(tmp_dir, &watcher, &delegate, FilePathWatcher::Type::kTrivial)); } // Observe a change on a directory TEST_F(FilePathWatcherTest, TrivialDirChange) { const FilePath tmp_dir = temp_dir_.GetPath(); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(tmp_dir, &watcher, &delegate, FilePathWatcher::Type::kTrivial)); ASSERT_TRUE(TouchFile(tmp_dir, Time::Now(), Time::Now())); event_expecter.AddExpectedEventForPath(tmp_dir); delegate.RunUntilEventsMatch(event_expecter); } // Observe no change when a parent is modified. TEST_F(FilePathWatcherTest, TrivialParentDirChange) { const FilePath tmp_dir = temp_dir_.GetPath(); const FilePath sub_dir1 = tmp_dir.Append(FILE_PATH_LITERAL("subdir")); const FilePath sub_dir2 = sub_dir1.Append(FILE_PATH_LITERAL("subdir_redux")); ASSERT_TRUE(CreateDirectory(sub_dir1)); ASSERT_TRUE(CreateDirectory(sub_dir2)); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(sub_dir2, &watcher, &delegate, FilePathWatcher::Type::kTrivial)); // There should be no notification for a change to |sub_dir2|'s parent. ASSERT_TRUE(Move(sub_dir1, tmp_dir.Append(FILE_PATH_LITERAL("over_here")))); delegate.RunUntilEventsMatch(event_expecter); } // Do not crash when a directory is moved; https://crbug.com/1156603. TEST_F(FilePathWatcherTest, TrivialDirMove) { const FilePath tmp_dir = temp_dir_.GetPath(); const FilePath sub_dir = tmp_dir.Append(FILE_PATH_LITERAL("subdir")); ASSERT_TRUE(CreateDirectory(sub_dir)); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatch(sub_dir, &watcher, &delegate, FilePathWatcher::Type::kTrivial)); ASSERT_TRUE(Move(sub_dir, tmp_dir.Append(FILE_PATH_LITERAL("over_here")))); event_expecter.AddExpectedEventForPath(sub_dir, /**error=*/true); delegate.RunUntilEventsMatch(event_expecter); } #endif // BUILDFLAG(IS_APPLE) #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1432064): Ideally most all of the tests above would be // parameterized in this way. // TODO(https://crbug.com/1425601): ChangeInfo is currently only supported by // the inotify based implementation. class FilePathWatcherWithChangeInfoTest : public FilePathWatcherTest, public testing::WithParamInterface< std::tuple> { public: void SetUp() override { FilePathWatcherTest::SetUp(); } protected: FilePathWatcher::Type type() const { return std::get<0>(GetParam()); } bool report_modified_path() const { return std::get<1>(GetParam()); } FilePathWatcher::WatchOptions GetWatchOptions() const { return FilePathWatcher::WatchOptions{ .type = type(), .report_modified_path = report_modified_path()}; } }; TEST_P(FilePathWatcherWithChangeInfoTest, NewFile) { // Each change should have these attributes. const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); // Match the expected change types, in this order. // TODO(https://crbug.com/1425601): Update this when change types are // supported on more platforms. static_assert(kExpectedEventsForNewFileWrite == 2); const auto sequence_matcher = testing::ElementsAre(IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified)); // Put it all together. const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(test_file(), "content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, NewDirectory) { const auto matcher = testing::ElementsAre(testing::AllOf( HasPath(test_file()), testing::Not(HasErrored()), IsDirectory(), IsType(FilePathWatcher::ChangeType::kCreated), testing::Not(HasCookie()))); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(CreateDirectory(test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedFile) { // TODO(https://crbug.com/1425601): Some platforms will not support // `ChangeType::kContentsModified`. Update this matcher once support for those // platforms is added. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kModified), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(test_file(), "new content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, MovedFile) { // TODO(https://crbug.com/1425601): Some platforms will not provide separate // events for "moved from" and "moved to". Update this matcher once support // for those platforms is added. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kMoved), HasCookie())); FilePath source_file(temp_dir_.GetPath().AppendASCII("source")); ASSERT_TRUE(WriteFile(source_file, "content")); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(Move(source_file, test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, MatchCookies) { FilePath source_file(test_file().AppendASCII("source")); FilePath dest_file(test_file().AppendASCII("dest")); const auto each_event_matcher = testing::Each( testing::AllOf(testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kMoved), HasCookie())); // TODO(https://crbug.com/1425601): Some platforms will not provide separate // events for "moved from" and "moved to". Update this matcher once support // for those platforms is added. const auto sequence_matcher = testing::UnorderedElementsAre( testing::AllOf( HasPath(report_modified_path() ? source_file : test_file()), IsType(FilePathWatcher::ChangeType::kMoved)), testing::AllOf(HasPath(report_modified_path() ? dest_file : test_file()), IsType(FilePathWatcher::ChangeType::kMoved))); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(test_file())); ASSERT_TRUE(WriteFile(source_file, "content")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(Move(source_file, dest_file)); delegate.RunUntilEventsMatch(matcher); const auto& events = delegate.events(); ASSERT_THAT(events, testing::SizeIs(2)); EXPECT_TRUE(events.front().change_info.cookie.has_value()); EXPECT_EQ(events.front().change_info.cookie, events.back().change_info.cookie); } TEST_P(FilePathWatcherWithChangeInfoTest, DeletedFile) { const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeleteFile(test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DeletedDirectory) { const auto matcher = testing::ElementsAre(testing::AllOf( HasPath(test_file()), testing::Not(HasErrored()), IsDirectory(), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); ASSERT_TRUE(CreateDirectory(test_file())); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeletePathRecursively(test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, MultipleWatchersSingleFile) { const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); // TODO(https://crbug.com/1425601): Update this when change types are // supported on more platforms. static_assert(kExpectedEventsForNewFileWrite == 2); const auto sequence_matcher = testing::ElementsAre(IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified)); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); FilePathWatcher watcher1, watcher2; TestDelegate delegate1, delegate2; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher1, &delegate1, GetWatchOptions())); ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher2, &delegate2, GetWatchOptions())); // Expect each delegate to get notified of all changes. ASSERT_TRUE(WriteFile(test_file(), "content")); delegate1.RunUntilEventsMatch(matcher); delegate2.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, NonExistentDirectory) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file(dir.AppendASCII("file")); const auto each_event_matcher = testing::Each(testing::AllOf(HasPath(file), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified), IsType(FilePathWatcher::ChangeType::kDeleted)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(file, &watcher, &delegate, GetWatchOptions())); // The delegate is only watching the file. Parent directory creation should // not trigger an event. ASSERT_TRUE(CreateDirectory(dir)); // It may take some time for `watcher` to re-construct its watch list, so spin // for a bit while we ensure that creating the parent directory does not // trigger an event. delegate.RunUntilEventsMatch(testing::IsEmpty(), ExpectedEventsSinceLastWait::kNone); ASSERT_TRUE(WriteFile(file, "content")); ASSERT_TRUE(WriteFile(file, "content v2")); ASSERT_TRUE(DeleteFile(file)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DirectoryChain) { FilePath path(temp_dir_.GetPath()); std::vector dir_names; for (int i = 0; i < 20; i++) { std::string dir(StringPrintf("d%d", i)); dir_names.push_back(dir); path = path.AppendASCII(dir); } FilePath file(path.AppendASCII("file")); const auto each_event_matcher = testing::Each(testing::AllOf(HasPath(file), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(file, &watcher, &delegate, GetWatchOptions())); FilePath sub_path(temp_dir_.GetPath()); for (const auto& dir_name : dir_names) { sub_path = sub_path.AppendASCII(dir_name); ASSERT_TRUE(CreateDirectory(sub_path)); } // Allow the watcher to reconstruct its watch list. SpinEventLoopForABit(); ASSERT_TRUE(WriteFile(file, "content")); ASSERT_TRUE(WriteFile(file, "content v2")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DisappearingDirectory) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file(dir.AppendASCII("file")); const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(file), testing::Not(HasErrored()), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); // TODO(https://crbug.com/1432044): inotify incorrectly reports an additional // deletion event for the parent directory (though while confusingly reporting // the path as `file`). Once fixed, update this matcher to assert that only // one event is received. const auto sequence_matcher = testing::Contains(IsFile()); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(WriteFile(file, "content")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(file, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeletePathRecursively(dir)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DeleteAndRecreate) { const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); // TODO(https://crbug.com/1425601): Update this when change types are // supported on on more platforms. static_assert(kExpectedEventsForNewFileWrite == 2); const auto sequence_matcher = testing::ElementsAre(IsType(FilePathWatcher::ChangeType::kDeleted), IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified)); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(WriteFile(test_file(), "content")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeleteFile(test_file())); ASSERT_TRUE(WriteFile(test_file(), "content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, WatchDirectory) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath file1(dir.AppendASCII("file1")); FilePath file2(dir.AppendASCII("file2")); const auto each_event_matcher = testing::Each( testing::AllOf(testing::Not(HasErrored()), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(report_modified_path() ? file1 : dir), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(report_modified_path() ? file1 : dir), IsFile(), IsType(FilePathWatcher::ChangeType::kModified)), testing::AllOf(HasPath(report_modified_path() ? file1 : dir), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted)), testing::AllOf(HasPath(report_modified_path() ? file2 : dir), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated))}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(dir)); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(dir, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(file1, "content")); ASSERT_TRUE(WriteFile(file1, "content v2")); ASSERT_TRUE(DeleteFile(file1)); ASSERT_TRUE(WriteFile(file2, "content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, MoveParent) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath dest(temp_dir_.GetPath().AppendASCII("dest")); FilePath subdir(dir.AppendASCII("subdir")); FilePath file(subdir.AppendASCII("file")); const auto each_event_matcher = testing::Each(testing::Not(HasErrored())); // TODO(https://crbug.com/1432044): inotify incorrectly sometimes reports // the first event as a directory creation... why? const auto file_delegate_sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(file), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(file), IsDirectory(), IsType(FilePathWatcher::ChangeType::kMoved))}); const auto subdir_delegate_sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(subdir), IsDirectory(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(report_modified_path() ? file : subdir), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(subdir), IsDirectory(), IsType(FilePathWatcher::ChangeType::kMoved))}); const auto file_delegate_matcher = testing::AllOf(each_event_matcher, file_delegate_sequence_matcher); const auto subdir_delegate_matcher = testing::AllOf(each_event_matcher, subdir_delegate_sequence_matcher); FilePathWatcher file_watcher, subdir_watcher; TestDelegate file_delegate, subdir_delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(file, &file_watcher, &file_delegate, GetWatchOptions())); ASSERT_TRUE(SetupWatchWithChangeInfo(subdir, &subdir_watcher, &subdir_delegate, GetWatchOptions())); // Setup a directory hierarchy. // We should only get notified on `subdir_delegate` of its creation. ASSERT_TRUE(CreateDirectory(subdir)); // Allow the watchers to reconstruct their watch lists. SpinEventLoopForABit(); ASSERT_TRUE(WriteFile(file, "content")); // Allow the file watcher to reconstruct its watch list. SpinEventLoopForABit(); Move(dir, dest); file_delegate.RunUntilEventsMatch(file_delegate_matcher); subdir_delegate.RunUntilEventsMatch(subdir_delegate_matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, MoveChild) { FilePath source_dir(temp_dir_.GetPath().AppendASCII("source")); FilePath source_subdir(source_dir.AppendASCII("subdir")); FilePath source_file(source_subdir.AppendASCII("file")); FilePath dest_dir(temp_dir_.GetPath().AppendASCII("dest")); FilePath dest_subdir(dest_dir.AppendASCII("subdir")); FilePath dest_file(dest_subdir.AppendASCII("file")); const auto each_event_matcher = testing::Each( testing::AllOf(testing::Not(HasErrored()), IsDirectory(), IsType(FilePathWatcher::ChangeType::kMoved), HasCookie())); const auto file_delegate_sequence_matcher = testing::ElementsAre(HasPath(dest_file)); const auto subdir_delegate_sequence_matcher = testing::ElementsAre(HasPath(dest_subdir)); const auto file_delegate_matcher = testing::AllOf(each_event_matcher, file_delegate_sequence_matcher); const auto subdir_delegate_matcher = testing::AllOf(each_event_matcher, subdir_delegate_sequence_matcher); // Setup a directory hierarchy. ASSERT_TRUE(CreateDirectory(source_subdir)); ASSERT_TRUE(WriteFile(source_file, "content")); FilePathWatcher file_watcher, subdir_watcher; TestDelegate file_delegate, subdir_delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(dest_file, &file_watcher, &file_delegate, GetWatchOptions())); ASSERT_TRUE(SetupWatchWithChangeInfo(dest_subdir, &subdir_watcher, &subdir_delegate, GetWatchOptions())); // Move the directory into place, s.t. the watched file appears. ASSERT_TRUE(Move(source_dir, dest_dir)); file_delegate.RunUntilEventsMatch(file_delegate_matcher); subdir_delegate.RunUntilEventsMatch(subdir_delegate_matcher); } // TODO(pauljensen): Re-enable when crbug.com/475568 is fixed and SetUp() places // the |temp_dir_| in /data. #if !BUILDFLAG(IS_ANDROID) TEST_P(FilePathWatcherWithChangeInfoTest, FileAttributesChanged) { const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_file()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kModified), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the file is modified. ASSERT_TRUE(MakeFileUnreadable(test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, CreateLink) { // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_link()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated), testing::Not(HasCookie()))); FilePathWatcher watcher; TestDelegate delegate; AccumulatingEventExpecter event_expecter; ASSERT_TRUE(SetupWatchWithChangeInfo(test_link(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the link is created. // Note that test_file() doesn't have to exist. ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); delegate.RunUntilEventsMatch(matcher); } // Unfortunately this test case only works if the link target exists. // TODO(craig) fix this as part of crbug.com/91561. TEST_P(FilePathWatcherWithChangeInfoTest, DeleteLink) { // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_link()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_link(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the link is deleted. ASSERT_TRUE(DeleteFile(test_link())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedLinkedFile) { // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_link()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kModified), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_link(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the file is modified. ASSERT_TRUE(WriteFile(test_file(), "new content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, CreateTargetLinkedFile) { // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(test_link()), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); // TODO(https://crbug.com/1425601): Update this when change types are // supported on on more platforms. static_assert(kExpectedEventsForNewFileWrite == 2); const auto sequence_matcher = testing::ElementsAre(IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified)); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_link(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the target file is created. ASSERT_TRUE(WriteFile(test_file(), "content")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DeleteTargetLinkedFile) { // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(test_link()), testing::Not(HasErrored()), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); ASSERT_TRUE(WriteFile(test_file(), "content")); ASSERT_TRUE(CreateSymbolicLink(test_file(), test_link())); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE(SetupWatchWithChangeInfo(test_link(), &watcher, &delegate, GetWatchOptions())); // Now make sure we get notified if the target file is deleted. ASSERT_TRUE(DeleteFile(test_file())); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart1) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(linkfile), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified), IsType(FilePathWatcher::ChangeType::kDeleted)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); // dir/file should exist. ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(WriteFile(file, "content")); FilePathWatcher watcher; TestDelegate delegate; // Note that we are watching dir.lnk/file which doesn't exist yet. ASSERT_TRUE(SetupWatchWithChangeInfo(linkfile, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); // Allow the watcher to reconstruct its watch list. SpinEventLoopForABit(); ASSERT_TRUE(WriteFile(file, "content v2")); ASSERT_TRUE(WriteFile(file, "content v2")); ASSERT_TRUE(DeleteFile(file)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart2) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(linkfile), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified), IsType(FilePathWatcher::ChangeType::kDeleted)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); // Now create the link from dir.lnk pointing to dir but // neither dir nor dir/file exist yet. ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); FilePathWatcher watcher; TestDelegate delegate; // Note that we are watching dir.lnk/file. ASSERT_TRUE(SetupWatchWithChangeInfo(linkfile, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(CreateDirectory(dir)); // Allow the watcher to reconstruct its watch list. SpinEventLoopForABit(); ASSERT_TRUE(WriteFile(file, "content")); ASSERT_TRUE(WriteFile(file, "content v2")); ASSERT_TRUE(DeleteFile(file)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart3) { FilePath dir(temp_dir_.GetPath().AppendASCII("dir")); FilePath link_dir(temp_dir_.GetPath().AppendASCII("dir.lnk")); FilePath file(dir.AppendASCII("file")); FilePath linkfile(link_dir.AppendASCII("file")); // TODO(https://crbug.com/1425601): Check for symlink-ness on platforms which // support it. const auto each_event_matcher = testing::Each( testing::AllOf(HasPath(linkfile), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified), IsType(FilePathWatcher::ChangeType::kDeleted)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(dir)); ASSERT_TRUE(CreateSymbolicLink(dir, link_dir)); FilePathWatcher watcher; TestDelegate delegate; // Note that we are watching dir.lnk/file but the file doesn't exist yet. ASSERT_TRUE(SetupWatchWithChangeInfo(linkfile, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(file, "content")); ASSERT_TRUE(WriteFile(file, "content v2")); ASSERT_TRUE(DeleteFile(file)); delegate.RunUntilEventsMatch(matcher); } #endif // !BUILDFLAG(IS_ANDROID) TEST_P(FilePathWatcherWithChangeInfoTest, CreatedFileInDirectory) { // Expect the change to be reported as a file creation, not as a // directory modification. FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); const auto matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(report_modified_path() ? child : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated), testing::Not(HasErrored()), testing::Not(HasCookie()))}); ASSERT_TRUE(CreateDirectory(parent)); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(child, "contents")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedFileInDirectory) { // Expect the change to be reported as a file modification, not as a // directory modification. FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(report_modified_path() ? child : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kModified), testing::Not(HasErrored()), testing::Not(HasCookie()))); ASSERT_TRUE(CreateDirectory(parent)); ASSERT_TRUE(WriteFile(child, "contents")); #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. #if BUILDFLAG(IS_ANDROID) // TODO(https://crbug.com/1496350): There appears to be a race condition // between setting up the inotify watch and the processing of the file system // notifications created while setting up the file system for this test. Spin // the event loop to ensure that the events have been processed by the time // the inotify watch has been set up. SpinEventLoopForABit(); #endif // BUILDFLAG(IS_ANDROID) #endif // BUILDFLAG(IS_ANDROID) FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(child, "contents v2")); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DeletedFileInDirectory) { // Expect the change to be reported as a file deletion, not as a // directory modification. FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); const auto matcher = testing::ElementsAre( testing::AllOf(HasPath(report_modified_path() ? child : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasErrored()), testing::Not(HasCookie()))); ASSERT_TRUE(CreateDirectory(parent)); ASSERT_TRUE(WriteFile(child, "contents")); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeleteFile(child)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, FileInDirectory) { // Expect the changes to be reported as events on the file, not as // modifications to the directory. FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); const auto each_event_matcher = testing::Each(testing::AllOf( HasPath(report_modified_path() ? child : parent), testing::Not(HasErrored()), IsFile(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::IsSupersetOf({IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kModified), IsType(FilePathWatcher::ChangeType::kDeleted)}); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(parent)); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(WriteFile(child, "contents")); ASSERT_TRUE(WriteFile(child, "contents v2")); ASSERT_TRUE(DeleteFile(child)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DirectoryInDirectory) { // Expect the changes to be reported as events on the child directory, not as // modifications to the parent directory. FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); const auto each_event_matcher = testing::Each(testing::AllOf( HasPath(report_modified_path() ? child : parent), testing::Not(HasErrored()), IsDirectory(), testing::Not(HasCookie()))); const auto sequence_matcher = testing::ElementsAre(IsType(FilePathWatcher::ChangeType::kCreated), IsType(FilePathWatcher::ChangeType::kDeleted)); const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(parent)); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(CreateDirectory(child)); ASSERT_TRUE(DeletePathRecursively(child)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, NestedDirectoryInDirectory) { FilePath parent(temp_dir_.GetPath().AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); FilePath grandchild(child.AppendASCII("grandchild")); const auto each_event_matcher = testing::Each( testing::AllOf(testing::Not(HasErrored()), testing::Not(HasCookie()))); EventListMatcher sequence_matcher; if (type() == FilePathWatcher::Type::kRecursive) { sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(report_modified_path() ? grandchild : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(report_modified_path() ? grandchild : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kModified)), testing::AllOf(HasPath(report_modified_path() ? grandchild : parent), IsFile(), IsType(FilePathWatcher::ChangeType::kDeleted)), testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory(), IsType(FilePathWatcher::ChangeType::kDeleted))}); } else { // Do not expect changes to `grandchild` when watching `parent` // non-recursively. sequence_matcher = testing::ElementsAre( testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory(), IsType(FilePathWatcher::ChangeType::kCreated)), testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory(), IsType(FilePathWatcher::ChangeType::kDeleted))); } const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(parent)); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(CreateDirectory(child)); // Allow the watcher to reconstruct its watch list. SpinEventLoopForABit(); ASSERT_TRUE(WriteFile(grandchild, "contents")); ASSERT_TRUE(WriteFile(grandchild, "contents v2")); ASSERT_TRUE(DeleteFile(grandchild)); ASSERT_TRUE(DeletePathRecursively(child)); delegate.RunUntilEventsMatch(matcher); } TEST_P(FilePathWatcherWithChangeInfoTest, DeleteDirectoryRecursively) { FilePath grandparent(temp_dir_.GetPath()); FilePath parent(grandparent.AppendASCII("parent")); FilePath child(parent.AppendASCII("child")); FilePath grandchild(child.AppendASCII("grandchild")); const auto each_event_matcher = testing::Each(testing::AllOf( testing::Not(HasErrored()), IsType(FilePathWatcher::ChangeType::kDeleted), testing::Not(HasCookie()))); // TODO(https://crbug.com/1432044): inotify incorrectly reports an additional // deletion event. Once fixed, update this matcher to assert that only one // event per removed file/dir is received. EventListMatcher sequence_matcher; if (type() == FilePathWatcher::Type::kRecursive) { sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(parent), IsDirectory()), testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory()), // TODO(https://crbug.com/1432044): inotify incorrectly reports this // deletion on the path of just "grandchild" rather than on // "/absolute/path/blah/blah/parent/child/grantchild". testing::AllOf( HasPath(report_modified_path() ? grandchild.BaseName() : parent), IsFile())}); } else { // Do not expect changes to `grandchild` when watching `parent` // non-recursively. sequence_matcher = testing::IsSupersetOf( {testing::AllOf(HasPath(parent), IsDirectory()), testing::AllOf(HasPath(report_modified_path() ? child : parent), IsDirectory())}); } const auto matcher = testing::AllOf(each_event_matcher, sequence_matcher); ASSERT_TRUE(CreateDirectory(parent)); ASSERT_TRUE(CreateDirectory(child)); ASSERT_TRUE(WriteFile(grandchild, "contents")); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(parent, &watcher, &delegate, GetWatchOptions())); ASSERT_TRUE(DeletePathRecursively(grandparent)); delegate.RunUntilEventsMatch(matcher); } INSTANTIATE_TEST_SUITE_P( /* no prefix */, FilePathWatcherWithChangeInfoTest, ::testing::Combine(::testing::Values(FilePathWatcher::Type::kNonRecursive, FilePathWatcher::Type::kRecursive), // Is WatchOptions.report_modified_path enabled? ::testing::Bool())); #else TEST_F(FilePathWatcherTest, UseDummyChangeInfoIfNotSupported) { const auto matcher = testing::ElementsAre(testing::AllOf( HasPath(test_file()), testing::Not(HasErrored()), IsUnknownPathType(), IsType(FilePathWatcher::ChangeType::kUnsupported), testing::Not(HasCookie()))); FilePathWatcher watcher; TestDelegate delegate; ASSERT_TRUE( SetupWatchWithChangeInfo(test_file(), &watcher, &delegate, {.type = FilePathWatcher::Type::kNonRecursive})); ASSERT_TRUE(CreateDirectory(test_file())); delegate.RunUntilEventsMatch(matcher); } #endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || // BUILDFLAG(IS_ANDROID) } // namespace base