#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
#if BUILDFLAG(IS_POSIX)
#include "base/files/file_descriptor_watcher_posix.h"
#endif
#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) || \
BUILDFLAG(IS_WIN)
constexpr size_t kExpectedEventsForNewFileWrite = …;
#else
constexpr size_t kExpectedEventsForNewFileWrite = 1;
#endif
enum class ExpectedEventsSinceLastWait { … };
struct Event { … };
EventListMatcher;
Event ToEvent(const FilePathWatcher::ChangeInfo& change_info,
const FilePath& path,
bool error) { … }
std::ostream& operator<<(std::ostream& os,
const FilePathWatcher::ChangeType& change_type) { … }
std::ostream& operator<<(std::ostream& os,
const FilePathWatcher::FilePathType& file_path_type) { … }
std::ostream& operator<<(std::ostream& os,
const FilePathWatcher::ChangeInfo& change_info) { … }
std::ostream& operator<<(std::ostream& os, const Event& event) { … }
void SpinEventLoopForABit() { … }
template <typename MatcherType, typename Value>
std::string Explain(const MatcherType& matcher, const Value& value) { … }
inline constexpr auto HasPath = …;
inline constexpr auto HasErrored = …;
inline constexpr auto HasCookie = …;
inline constexpr auto IsType = …;
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
inline constexpr auto IsFile = …;
inline constexpr auto IsDirectory = …;
#else
inline constexpr auto IsUnknownPathType = []() {
return testing::Field(
&Event::change_info,
testing::Field(&FilePathWatcher::ChangeInfo::file_path_type,
FilePathWatcher::FilePathType::kUnknown));
};
#endif
class AccumulatingEventExpecter { … };
class TestDelegateBase { … };
class TestDelegate final : public TestDelegateBase { … };
}
#if BUILDFLAG(IS_FUCHSIA)
#define FilePathWatcherTest …
#endif
class FilePathWatcherTest : public testing::Test { … };
bool FilePathWatcherTest::SetupWatch(const FilePath& target,
FilePathWatcher* watcher,
TestDelegateBase* delegate,
FilePathWatcher::Type watch_type) { … }
bool FilePathWatcherTest::SetupWatchWithOptions(
const FilePath& target,
FilePathWatcher* watcher,
TestDelegateBase* delegate,
FilePathWatcher::WatchOptions watch_options) { … }
bool FilePathWatcherTest::SetupWatchWithChangeInfo(
const FilePath& target,
FilePathWatcher* watcher,
TestDelegateBase* delegate,
FilePathWatcher::WatchOptions watch_options) { … }
TEST_F(FilePathWatcherTest, NewFile) { … }
TEST_F(FilePathWatcherTest, NewDirectory) { … }
TEST_F(FilePathWatcherTest, NewDirectoryRecursiveWatch) { … }
TEST_F(FilePathWatcherTest, ModifiedFile) { … }
TEST_F(FilePathWatcherTest, CreateParentDirectory) { … }
TEST_F(FilePathWatcherTest, CreateSiblingFile) { … }
TEST_F(FilePathWatcherTest, CreateParentSiblingFile) { … }
TEST_F(FilePathWatcherTest, MovedToFile) { … }
TEST_F(FilePathWatcherTest, MovedFromFile) { … }
TEST_F(FilePathWatcherTest, DeletedFile) { … }
#if BUILDFLAG(IS_WIN)
TEST_F(FilePathWatcherTest, WindowsBufferOverflow) {
FilePathWatcher watcher;
TestDelegate delegate;
AccumulatingEventExpecter event_expecter;
ASSERT_TRUE(SetupWatch(test_file(), &watcher, &delegate,
FilePathWatcher::Type::kNonRecursive));
{
AutoLock auto_lock(watcher.GetWatchThreadLockForTest());
ASSERT_TRUE(WriteFile(test_file(), "content"));
const size_t kWriteFileEventSize =
(sizeof(FILE_NOTIFY_INFORMATION) + test_file().AsUTF8Unsafe().size()) *
2;
const size_t kMaxBufferSize = 64 * 1024;
for (size_t bytes_in_buffer = 0; bytes_in_buffer < kMaxBufferSize;
bytes_in_buffer += kWriteFileEventSize) {
WriteFile(test_file(), "content");
}
}
event_expecter.AddExpectedEventForPath(test_file());
event_expecter.AddExpectedEventForPath(test_file());
delegate.RunUntilEventsMatch(event_expecter);
}
#endif
namespace {
class Deleter final : public TestDelegateBase { … };
}
TEST_F(FilePathWatcherTest, DeleteDuringNotify) { … }
TEST_F(FilePathWatcherTest, DestroyWithPendingNotification) { … }
TEST_F(FilePathWatcherTest, MultipleWatchersSingleFile) { … }
TEST_F(FilePathWatcherTest, NonExistentDirectory) { … }
TEST_F(FilePathWatcherTest, DirectoryChain) { … }
TEST_F(FilePathWatcherTest, DisappearingDirectory) { … }
TEST_F(FilePathWatcherTest, DeleteAndRecreate) { … }
TEST_F(FilePathWatcherTest, WatchDirectory) { … }
TEST_F(FilePathWatcherTest, MoveParent) { … }
TEST_F(FilePathWatcherTest, RecursiveWatch) { … }
#if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_ANDROID)
TEST_F(FilePathWatcherTest, RecursiveWithSymLink) { … }
#endif
TEST_F(FilePathWatcherTest, MoveChild) { … }
#if BUILDFLAG(IS_ANDROID)
#define FileAttributesChanged …
#endif
TEST_F(FilePathWatcherTest, FileAttributesChanged) { … }
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
TEST_F(FilePathWatcherTest, CreateLink) { … }
TEST_F(FilePathWatcherTest, DeleteLink) { … }
TEST_F(FilePathWatcherTest, ModifiedLinkedFile) { … }
TEST_F(FilePathWatcherTest, CreateTargetLinkedFile) { … }
TEST_F(FilePathWatcherTest, DeleteTargetLinkedFile) { … }
TEST_F(FilePathWatcherTest, LinkedDirectoryPart1) { … }
TEST_F(FilePathWatcherTest, LinkedDirectoryPart2) { … }
TEST_F(FilePathWatcherTest, LinkedDirectoryPart3) { … }
TEST_F(FilePathWatcherTest, RacyRecursiveWatch) { … }
TEST_F(FilePathWatcherTest, InotifyLimitInWatch) { … }
TEST_F(FilePathWatcherTest, InotifyLimitInUpdate) { … }
TEST_F(FilePathWatcherTest, InotifyLimitInUpdateRecursive) { … }
#endif
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
TEST_F(FilePathWatcherTest, ReturnFullPath_RecursiveInRootFolder) { … }
TEST_F(FilePathWatcherTest, ReturnFullPath_RecursiveInNestedFolder) { … }
TEST_F(FilePathWatcherTest, ReturnFullPath_NonRecursiveInRootFolder) { … }
TEST_F(FilePathWatcherTest, ReturnFullPath_NonRecursiveRemoveEnclosingFolder) { … }
TEST_F(FilePathWatcherTest, ReturnWatchedPath_RecursiveInRootFolder) { … }
TEST_F(FilePathWatcherTest, ReturnWatchedPath_NonRecursiveInRootFolder) { … }
#endif
namespace {
enum Permission { … };
#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
}
#if BUILDFLAG(IS_APPLE)
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"));
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));
ASSERT_TRUE(ChangeFilePermissions(test_dir1, Read, false));
ASSERT_TRUE(ChangeFilePermissions(test_dir1, Read, true));
ASSERT_TRUE(ChangeFilePermissions(test_dir1, Execute, false));
event_expecter.AddExpectedEventForPath(test_file);
delegate.RunUntilEventsMatch(event_expecter);
ASSERT_TRUE(ChangeFilePermissions(test_dir1, Execute, true));
}
#endif
#if BUILDFLAG(IS_APPLE)
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));
}
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));
}
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);
}
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));
ASSERT_TRUE(Move(sub_dir1, tmp_dir.Append(FILE_PATH_LITERAL("over_here"))));
delegate.RunUntilEventsMatch(event_expecter);
}
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, true);
delegate.RunUntilEventsMatch(event_expecter);
}
#endif
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
class FilePathWatcherWithChangeInfoTest
: public FilePathWatcherTest,
public testing::WithParamInterface<
std::tuple<FilePathWatcher::Type, bool>> { … };
TEST_P(FilePathWatcherWithChangeInfoTest, NewFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, NewDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, MovedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, MatchCookies) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeletedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeletedDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, MultipleWatchersSingleFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, NonExistentDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DirectoryChain) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DisappearingDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeleteAndRecreate) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, WatchDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, MoveParent) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, MoveChild) { … }
#if !BUILDFLAG(IS_ANDROID)
TEST_P(FilePathWatcherWithChangeInfoTest, FileAttributesChanged) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, CreateLink) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeleteLink) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedLinkedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, CreateTargetLinkedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeleteTargetLinkedFile) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart1) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart2) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, LinkedDirectoryPart3) { … }
#endif
TEST_P(FilePathWatcherWithChangeInfoTest, CreatedFileInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, ModifiedFileInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeletedFileInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, FileInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DirectoryInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, NestedDirectoryInDirectory) { … }
TEST_P(FilePathWatcherWithChangeInfoTest, DeleteDirectoryRecursively) { … }
INSTANTIATE_TEST_SUITE_P(…);
#else
TEST_F(FilePathWatcherTest, UseDummyChangeInfoIfNotSupported) {
const auto matcher = testing::ElementsAre(testing::AllOf(
HasPath(test_file()), testing::Not(HasErrored()), IsUnknownPathType(),
IsType(FilePathWatcher::ChangeType::kUnknown),
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
}