blob: efaad49c2c17b5b5b80dfaa8f4a5851e260f28a3 [file] [log] [blame]
// 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 <list>
#include <memory>
#include <string>
#include <vector>
#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 <windows.h>
#include <aclapi.h>
#elif BUILDFLAG(IS_POSIX)
#include <sys/stat.h>
#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 {
FilePath path;
bool error;
// TODO(https://crbug.com/1425601): Support change types.
bool operator==(const Event& other) const {
return error == other.error && path == other.path;
}
};
std::ostream& operator<<(std::ostream& os, const Event& event) {
return os << "Event{ error: " << (event.error ? "true" : "false")
<< ", path: " << event.path << " }";
}
using EventListMatcher = testing::Matcher<std::list<Event>>;
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 <typename MatcherType, typename Value>
std::string Explain(const MatcherType& matcher, const Value& value) {
testing::StringMatchResultListener listener;
testing::ExplainMatchResult(matcher, value, &listener);
return listener.str();
}
// 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 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;
}
// TODO(https://crbug.com/1425601): Add a version of this method which
// includes the type of change.
void AddExpectedEventForPath(const FilePath& path, bool error = false) {
expected_events_.emplace_back(path, error);
expected_events_since_last_wait_ = ExpectedEventsSinceLastWait::kSome;
}
private:
std::list<Event> expected_events_;
ExpectedEventsSinceLastWait expected_events_since_last_wait_ =
ExpectedEventsSinceLastWait::kNone;
};
class TestDelegateBase : public SupportsWeakPtr<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;
};
// 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 : 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_);
received_events_.emplace_back(path, error);
}
// 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(AccumulatingEventExpecter& event_expecter,
const Location& location = FROM_HERE) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return RunUntilEventsMatch(
event_expecter.GetMatcher(),
event_expecter.GetAndResetExpectedEventsSinceLastWait(), location);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
// Uniquely generated ID used to tie events to this delegate.
const size_t id_;
std::list<Event> received_events_ GUARDED_BY_CONTEXT(sequence_checker_);
};
} // 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);
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,
BindPostTaskToCurrentDefault(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,
BindPostTaskToCurrentDefault(BindRepeating(
&TestDelegateBase::OnFileChanged, 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);
}
// 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 moving the file into place is caught.
TEST_F(FilePathWatcherTest, MovedFile) {
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);
}
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 : public TestDelegateBase {
public:
explicit Deleter(OnceClosure done_closure)
: watcher_(std::make_unique<FilePathWatcher>()),
done_closure_(std::move(done_closure)) {}
Deleter(const Deleter&) = delete;
Deleter& operator=(const Deleter&) = delete;
~Deleter() override = default;
void OnFileChanged(const FilePath&, bool) override {
watcher_.reset();
std::move(done_closure_).Run();
}
FilePathWatcher* watcher() const { return watcher_.get(); }
private:
std::unique_ptr<FilePathWatcher> watcher_;
OnceClosure done_closure_;
};
} // 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<std::string> 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 (std::vector<std::string>::const_iterator d(dir_names.begin());
d != dir_names.end(); ++d) {
sub_path = sub_path.AppendASCII(*d);
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));
// TODO(https://crbug.com/1432064): Add a test asserting that creation of the
// parent directory does not trigger an event.
// 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;
// AccumulatingEventExpecter event_expecter;
// 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<FilePath> 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<FilePathWatcher>();
// 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<FilePathWatcher>();
// "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<FilePathWatcher>();
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<FilePathWatcher>();
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)
} // namespace base