From 057af7ac1455983e80e757f12b64ac85adcd29bb Mon Sep 17 00:00:00 2001 From: Marko Budiselic <marko.budiselic@memgraph.io> Date: Sun, 8 Jan 2017 01:03:12 +0100 Subject: [PATCH] Much more serious implementation of FSWatcher Summary: Much more serious implementation of FSWatcher Test Plan: ctest -R fswatcher Reviewers: dtomicevic, sale Subscribers: buda Differential Revision: https://phabricator.memgraph.io/D33 --- CMakeLists.txt | 16 +- include/logging/logger.hpp | 4 +- include/utils/algorithm.hpp | 27 +++ include/utils/assert.hpp | 57 +++-- include/utils/fswatcher.hpp | 448 ++++++++++++++++++++++++++++-------- include/utils/linux.hpp | 54 +++++ tests/unit/fswatcher.cpp | 125 +++++++++- 7 files changed, 604 insertions(+), 127 deletions(-) create mode 100644 include/utils/algorithm.hpp create mode 100644 include/utils/linux.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f6c9eb5c..f794e975b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,17 +188,23 @@ if (SYNC_LOGGER) endif() # ----------------------------------------------------------------------------- -# assert +# custom assert control parameters + +# Runtime assert, if value is OFF runtime asserts will be inactive during +# runtime. Default value is ON. option(RUNTIME_ASSERT "Enable runtime assertions" ON) message(STATUS "RUNTIME_ASSERT: ${RUNTIME_ASSERT}") if(RUNTIME_ASSERT) add_definitions(-DRUNTIME_ASSERT_ON) endif() -option(THROW_EXCEPTION_ON_ERROR "Throw exception on error" ON) -message(STATUS "THROW_EXCEPTION_ON_ERROR: ${THROW_EXCEPTION_ON_ERROR}") -if(THROW_EXCEPTION_ON_ERROR) - add_definitions(-DTHROW_EXCEPTION_ON_ERROR) +# by default on custom assert only the message, filename and line number will be +# printed on stderr, if STACKTRACE_ASSERT is ON the whole stacktrace is going to +# be printed on stderr +option(STACKTRACE_ASSERT "Dump stacktrace on custom assert" OFF) +message(STATUS "STACKTRACE_ASSERT: ${STACKTRACE_ASSERT}") +if(STACKTRACE_ASSERT) + add_definitions(-DSTACKTRACE_ASSERT_ON) endif() # ----------------------------------------------------------------------------- diff --git a/include/logging/logger.hpp b/include/logging/logger.hpp index 90f0a66c9..ff2b0f9fd 100644 --- a/include/logging/logger.hpp +++ b/include/logging/logger.hpp @@ -1,10 +1,10 @@ #pragma once -#include <cassert> #include <fmt/format.h> #include "logging/log.hpp" #include "logging/levels.hpp" +#include "utils/assert.hpp" class Logger { @@ -54,7 +54,7 @@ public: template <class Level, class... Args> void emit(Args&&... args) { - assert(log != nullptr); + runtime_assert(log != nullptr, "Log object has to be defined."); auto message = std::make_unique<Message<Level>>( Timestamp::now(), name, fmt::format(std::forward<Args>(args)...) diff --git a/include/utils/algorithm.hpp b/include/utils/algorithm.hpp new file mode 100644 index 000000000..c1a265ec9 --- /dev/null +++ b/include/utils/algorithm.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <algorithm> + +#include "logging/default.hpp" + +/** + * Goes from first to last item in a container, if an element satisfying the + * predicate then the action is going to be executed and the element is going + * to be shifted to the end of the container. + * + * @tparam ForwardIt type of forward iterator + * @tparam UnaryPredicate type of predicate + * @tparam Action type of action + * + * @return a past-the-end iterator for the new end of the range + */ +template <class ForwardIt, class UnaryPredicate, class Action> +ForwardIt action_remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p, + Action a) +{ + auto it = std::remove_if(first, last, p); + if (it == last) + return it; + std::for_each(it, last, a); + return it; +} diff --git a/include/utils/assert.hpp b/include/utils/assert.hpp index b5fec6070..ac87544ad 100644 --- a/include/utils/assert.hpp +++ b/include/utils/assert.hpp @@ -1,40 +1,53 @@ +/** + * Permanent Assert -> always active + * Runtime Assert -> active only if RUNTIME_ASSERT_ON is present + */ #pragma once #include <iostream> #include <sstream> -#include "utils/exceptions/basic_exception.hpp" +#include "utils/stacktrace/stacktrace.hpp" -// #define THROW_EXCEPTION_ON_ERROR -// #define RUNTIME_ASSERT_ON +/** + * if STACKTRACE_ASSERT_ON is defined the full stacktrace will be printed on + * stderr otherwise just the basic information will be printed out (message, + * file, line) + */ +#ifdef STACKTRACE_ASSERT_ON +#define __handle_assert_message(message) \ + Stacktrace stacktrace; \ + std::cerr << "ASSERT: " << message << std::endl; \ + std::cerr << stacktrace.dump(); +#else +#define __handle_assert_message(message) \ + std::cerr << "ASSERT: " << message << " In file " << __FILE__ << " #" \ + << __LINE__ << std::endl; +#endif -// // handle assertion error -// void assert_error_handler_(const char *file_name, unsigned line_number, -// const char *message) -// { -// // this is a good place to put your debug breakpoint -// // and add some other destination for error message -// #ifdef THROW_EXCEPTION_ON_ERROR -// throw BasicException(message); -// #else -// std::cerr << message << " in file " << file_name << " #" << line_number -// << std::endl; -// exit(1); -// #endif -// } - -// parmanant exception will always be executed +/** + * parmanant assertion will always be active + * when condition is satisfied program has to exit + * + * a good use-case for this type of assert is during unit testing because + * assert has to stay active all the time + */ #define permanent_assert(condition, message) \ if (!(condition)) \ { \ std::ostringstream s; \ s << message; \ - std::cout << s.str() << std::endl; \ + __handle_assert_message(s.str()); \ std::exit(EXIT_FAILURE); \ } -// assert_error_handler_(__FILE__, __LINE__, s.str().c_str()); -// runtime exception +/** + * runtime assertion is more like standart C assert but with custom + * define which controls when the assertion will be active + * + * could be used wherever the standard C assert is used but + * the user should not forget about RUNTIME_ASSERT_ON define + */ #ifdef RUNTIME_ASSERT_ON #define runtime_assert(condition, message) permanent_assert(condition, message) #else diff --git a/include/utils/fswatcher.hpp b/include/utils/fswatcher.hpp index fe22869ed..d4c29ecaa 100644 --- a/include/utils/fswatcher.hpp +++ b/include/utils/fswatcher.hpp @@ -1,36 +1,121 @@ +/** + * Memgraph Ltd + * + * File System Watcher + * + * @author Marko Budiselic + */ #pragma once -#include <vector> #include <atomic> -#include <sys/inotify.h> #include <chrono> +#include <errno.h> +#include <limits.h> +#include <mutex> +#include <sys/inotify.h> #include <thread> -// TODO: remove experimental from here once that becomes possible +#include <unistd.h> +#include <vector> + #include <experimental/filesystem> - -#include "utils/exceptions/basic_exception.hpp" -#include "utils/exceptions/not_yet_implemented.hpp" - -// TODO: remove experimental from here once it becomes possible namespace fs = std::experimental::filesystem; +#include "logging/loggable.hpp" +#include "utils/algorithm.hpp" +#include "utils/assert.hpp" +#include "utils/exceptions/basic_exception.hpp" +#include "utils/linux.hpp" +#include "utils/underlying_cast.hpp" + namespace utils { using ms = std::chrono::milliseconds; -/* - * File System Event Types +/** + * watch descriptor type, on linux it is int */ -enum class FSEvent : int +using os_wd_t = int; +/** + * machine event mask type, on linux it is inotify mask type which is int + */ +using os_mask_t = int; +// NOTE: for now the types are linux dependent, once when the code base will be +// compiled for another OS figure out the most appropriate solution + +/** + * inotify buffer length (10 events) + */ +using in_event_t = struct inotify_event; +constexpr uint64_t IN_HEADER_SIZE = sizeof(struct inotify_event); +constexpr uint64_t IN_BUFF_LEN = 10 * (IN_HEADER_SIZE + NAME_MAX + 1); + +/** + * File System Event Type - abstraction for underlying event types + */ +enum class FSEventType : os_mask_t { - Created = 0x1, - Modified = 0x2, - Deleted = 0x4 + Created = IN_CREATE, + Modified = IN_MODIFY, + Deleted = IN_DELETE, + All = Created | Modified | Deleted }; -/* - * Custom exception +/** + * @struct FSEventBase + * + * Base DTO object. + * + * In derived classes the path can represent eather directory or path to a file. + * In derived classes the type can represent a set of event types or specific + * event. + */ +struct FSEventBase +{ + FSEventBase(const fs::path &path, const FSEventType type) + : path(path), type(type) + { + } + + fs::path path; + FSEventType type; + + operator os_mask_t() { return underlying_cast(type); } +}; + +/** + * @struct WatchDescriptor + * + * The purpose of this struct is to register new watchers for specific + * directory and event type. + */ +struct WatchDescriptor : public FSEventBase +{ + WatchDescriptor(const fs::path &directory, const FSEventType type) + : FSEventBase(directory, type) + { + runtime_assert(fs::is_directory(path), + "The path parameter should be directory"); + } +}; + +/** + * @struct FSEvent + * + * The purpose of this struct is to carry information about a new fs event. + * In this case path is a path to the affected file and type is the event type. + */ +struct FSEvent : public FSEventBase +{ + FSEvent(const fs::path &directory, const fs::path &filename, + const FSEventType type) + : FSEventBase(directory / filename, type) + { + } +}; + +/** + * Custom FSWatcher Exception */ class FSWatcherException : public BasicException { @@ -38,129 +123,304 @@ public: using BasicException::BasicException; }; -/* +/** * File System Watcher * - * The idea is to create wrapper of inotify or any other file system - * notificatino system. + * The idea is to create wrapper for inotify or any other file system + * notification system. + * + * The object is not thread safe. * * parameters: - * * interval - time between two check for the new file system events + * * interval - time between two checks for the new file system events */ -class FSWatcher +class FSWatcher : public Loggable { - // watch descriptor type - using wd_t = int; - // callback type (the code that will be notified will be notified - // through callback of this type - using callback_t = std::function<void(fs::path &path, FSEvent event)>; - - // storage type for all subscribers - // <path, watch descriptor, callback> - using entry_t = std::tuple<fs::path, wd_t, callback_t>; + /** + * callback type (the code that will be notified will be notified + * through callback of this type + */ + using callback_t = std::function<void(FSEvent event)>; public: - FSWatcher(ms interval = ms(100)) : interval_(interval) { init(); } - ~FSWatcher() = default; + /** + * Initialize underlying notification system. + */ + FSWatcher(ms check_interval = ms(100)) + : Loggable("FSWatcher"), check_interval_(check_interval) + { + inotify_fd_ = inotify_init(); + if (inotify_fd_ == -1) + throw FSWatcherException("Unable to initialize inotify\n"); + os_linux::set_non_blocking(inotify_fd_); + } - // copy and move constructors and assignemnt operators are deleted because - // std::atomic can't be copied or moved + ~FSWatcher() + { + logger.debug("destructor call"); + unwatchAll(); + } + + /* + * copy and move constructors and assignemnt operators are deleted because + * std::atomic can't be copied or moved + */ FSWatcher(const FSWatcher &other) = delete; FSWatcher(FSWatcher &&other) = delete; FSWatcher &operator=(const FSWatcher &) = delete; FSWatcher &operator=(FSWatcher &&) = delete; - /* - * Initialize file system notification system. + /** + * Add Watcher */ - void init() + void watch(WatchDescriptor descriptor, callback_t callback) { - inotify_fd_ = inotify_init(); - if (inotify_fd_ == -1) - throw FSWatcherException("Unable to initialize inotify"); - } - - /* - * Add subscriber - * - * parameters: - * * path: path to a file which will be monitored - * * type: type of events - * * callback: callback that will be called on specified event type - */ - void watch(const fs::path &path, FSEvent, callback_t callback) - { - // TODO: instead IN_ALL_EVENTS pass FSEvent - int wd = inotify_add_watch(inotify_fd_, path.c_str(), IN_ALL_EVENTS); + stop(); + os_wd_t wd = + inotify_add_watch(inotify_fd_, descriptor.path.c_str(), descriptor); if (wd == -1) - throw FSWatcherException("Unable to add watch"); - entries_.emplace_back(std::make_tuple(path, wd, callback)); - } - - /* - * Remove subscriber on specified path and type - */ - void unwatch(const fs::path &path, FSEvent) - { - // iterate through all entries and remove specified watch descriptor - for (auto &entry : entries_) { - // if paths are not equal pass - if (std::get<fs::path>(entry) != path) - continue; - - // get watch descriptor and remove it from the watching - auto status = inotify_rm_watch(inotify_fd_, std::get<wd_t>(entry)); - if (status == -1) - throw FSWatcherException("Unable to remove watch"); + switch (errno) + { + case EACCES: + throw FSWatcherException("Unable to add watcher. Read access " + "to the given file is not permitted."); + case EBADF: + throw FSWatcherException("Unable to add watcher. The given " + "file descriptor is not valid."); + case EFAULT: + throw FSWatcherException("Unable to add watcher. pathname " + "points outside of the process's " + "accessible address space"); + case EINVAL: + throw FSWatcherException( + "Unable to add watcher. The given event mask contains no " + "valid events; or fd is not an inotify file descriptor."); + case ENAMETOOLONG: + throw FSWatcherException( + "Unable to add watcher. pathname is too long."); + case ENOENT: + throw FSWatcherException("Unable to add watcher. A directory " + "component in pathname does not exist " + "or is a dangling symbolic link."); + case ENOMEM: + throw FSWatcherException("Unable to add watcher. Insufficient " + "kernel memory was available."); + case ENOSPC: + throw FSWatcherException( + "Unable to add watcher. The user limit on the total number " + "of inotify watches was reached or the kernel failed to " + "allocate a needed resource."); + default: + throw FSWatcherException( + "Unable to add watcher. Unknown Linux API error."); + } } + + // update existing + auto it = + std::find_if(entries_.begin(), entries_.end(), + [wd](Entry &entry) { return wd == entry.os_wd; }); + if (it != entries_.end()) + { + it->descriptor = descriptor; + it->callback = callback; + } + else + { + entries_.emplace_back(Entry(wd, descriptor, callback)); + } + + logger.debug("REGISTERED: wd({}) for path({}) and mask ({})", wd, + descriptor.path.c_str(), (os_mask_t)(descriptor)); + start(); } - /* - * Remove all subscribers. + /** + * Remove subscriber on specified path and type. + * + * Time complexity: O(n) (where n is number of entries) + */ + void unwatch(WatchDescriptor descriptor) + { + stop(); + + auto it = action_remove_if( + entries_.begin(), entries_.end(), + [&descriptor](Entry entry) { + auto stored_descriptor = entry.descriptor; + if (stored_descriptor.path != descriptor.path) return false; + if (stored_descriptor.type != descriptor.type) return false; + return true; + }, + [this](Entry entry) { remove_underlaying_watcher(entry); }); + + if (it != entries_.end()) entries_.erase(it); + + if (entries_.size() > 0) start(); + } + + /** + * Removes all subscribers and stops the watching process. */ void unwatchAll() { - // iterate through all entries and remove all watch descriptors - for (auto &entry : entries_) - { - auto status = inotify_rm_watch(inotify_fd_, std::get<wd_t>(entry)); - if (status == -1) - throw FSWatcherException("Unable to remove watch"); - } + stop(); + + if (entries_.size() <= 0) return; + + entries_.erase(action_remove_if( + entries_.begin(), entries_.end(), [](Entry) { return true; }, + [this](Entry &entry) { remove_underlaying_watcher(entry); })); } - /* - * Start watching + /** + * Start the watching process. */ void start() { - throw NotYetImplemented("FSWatch::start"); - is_running_.store(true); + // run separate thread dispatch_thread_ = std::thread([this]() { - while (is_running_.load()) { - std::this_thread::sleep_for(interval_); - // TODO implement file system event processing + + logger.debug("dispatch thread - start"); + + while (is_running_.load()) + { + std::this_thread::sleep_for(check_interval_); + + // read file descriptor and process new events + // the read call should be non blocking otherwise + // this thread can easily be blocked forever + auto n = ::read(inotify_fd_, buffer_, IN_BUFF_LEN); + if (n == 0) throw FSWatcherException("read() -> 0."); + if (n == -1) continue; + + logger.info("Read {} bytes from inotify fd", (long)n); + + // process all of the events in buffer returned by read() + for (auto p = buffer_; p < buffer_ + n;) + { + // get in_event + auto in_event = reinterpret_cast<in_event_t *>(p); + auto in_event_length = IN_HEADER_SIZE + in_event->len; + + // skip if in_event is undefined OR is equal to IN_IGNORED + if ((in_event->len == 0 && in_event->mask == 0) || + in_event->mask == IN_IGNORED) + { + p += in_event_length; + continue; + } + + logger.info("LEN: {}, MASK: {}, NAME: {}", in_event_length, + in_event->mask, in_event->name); + + // find our watch descriptor + auto entry = find_if(entries_.begin(), entries_.end(), + [in_event](Entry &entry) { + return entry.os_wd == in_event->wd; + }); + auto &descriptor = entry->descriptor; + + // call user's callback + entry->callback( + FSEvent(descriptor.path, fs::path(in_event->name), + static_cast<FSEventType>(in_event->mask))); + + // move on + p += in_event_length; + } } + + logger.debug("dispatch thread - finish"); }); } - /* - * Stop watching + /** + * Stop the watching process. */ void stop() { - is_running_.store(false); + if (is_running_.load()) + { + is_running_.store(false); + dispatch_thread_.join(); + } } + /** + * @return check interval - time between two underlaying check calls + */ + ms check_interval() const { return check_interval_; } + + /** + * @return number of entries + */ + size_t size() const { return entries_.size(); } + private: + /** + * Internal storage for all subscribers. + * + * <os watch descriptor, API watch descriptor, callback> + */ + struct Entry + { + Entry(os_wd_t os_wd, WatchDescriptor descriptor, callback_t callback) + : os_wd(os_wd), descriptor(descriptor), callback(callback) + { + } + + os_wd_t os_wd; + WatchDescriptor descriptor; + callback_t callback; + }; + + /** + * Removes the os specific watch descriptor. + */ + void remove_underlaying_watcher(Entry entry) + { + auto status = inotify_rm_watch(inotify_fd_, entry.os_wd); + if (status == -1) + throw FSWatcherException("Unable to remove underlaying watch."); + else + logger.info("UNREGISTER: fd({}), wd({}), status({})", inotify_fd_, + entry.os_wd, status); + } + + /** + * inotify file descriptor + */ int inotify_fd_; + + /** + * running flag, has to be atomic because two threads can update and read + * the value + */ std::atomic<bool> is_running_; - ms interval_; - std::vector<entry_t> entries_; + + /** + * interval between the end of events processing and next start of + * processing + */ + ms check_interval_; + + /** + */ + std::vector<Entry> entries_; + + /** + * thread for events processing + */ std::thread dispatch_thread_; + + /** + * buffer for underlying events (inotify dependent) + */ + char *buffer_[IN_BUFF_LEN]; }; } diff --git a/include/utils/linux.hpp b/include/utils/linux.hpp new file mode 100644 index 000000000..6806c3081 --- /dev/null +++ b/include/utils/linux.hpp @@ -0,0 +1,54 @@ +#pragma once + +// ** Books ** +// http://instructor.sdu.edu.kz/~konst/sysprog2015fall/readings/linux%20system%20programming/The%20Linux%20Programming%20Interface-Michael%20Kerrisk.pdf + +// ** Documentation ** +// http://man7.org/linux/man-pages/man2/read.2.html +// http://man7.org/linux/man-pages/man2/select.2.html +// http://man7.org/linux/man-pages/man2/fcntl.2.html + +// ** Community ** +// http://stackoverflow.com/questions/5616092/non-blocking-call-for-reading-descriptor +// http://stackoverflow.com/questions/2917881/how-to-implement-a-timeout-in-read-function-call + +#include <unistd.h> +#include <fcntl.h> + +#include "utils/exceptions/basic_exception.hpp" +#include "utils/exceptions/not_yet_implemented.hpp" +#include "utils/likely.hpp" + +namespace os_linux +{ + class LinuxException : public BasicException + { + using BasicException::BasicException; + }; + + /** + * Sets non blocking flag to a file descriptor. + */ + void set_non_blocking(int fd) + { + auto flags = fcntl(fd, F_GETFL, 0); + + if(UNLIKELY(flags == -1)) + throw LinuxException("Cannot read flags from file descriptor."); + + flags |= O_NONBLOCK; + + auto status = fcntl(fd, F_SETFL, flags); + + if(UNLIKELY(status == -1)) + throw LinuxException("Can't set NON_BLOCK flag to file descriptor"); + } + + /** + * Reads a file descriptor with timeout. + */ + void tread() + { + throw NotYetImplemented(); + } +} diff --git a/tests/unit/fswatcher.cpp b/tests/unit/fswatcher.cpp index 148e78d5b..1e05293f6 100644 --- a/tests/unit/fswatcher.cpp +++ b/tests/unit/fswatcher.cpp @@ -1,12 +1,129 @@ -#define CATCH_CONFIG_MAIN -#include "catch.hpp" +#include <fstream> +#include <thread> +#include "gtest/gtest.h" +#include "logging/default.cpp" #include "utils/fswatcher.hpp" +#include "utils/signals/handler.hpp" +#include "utils/stacktrace/log.hpp" +#include "utils/terminate_handler.hpp" +using namespace std::chrono_literals; using namespace utils; -TEST_CASE("FSWatcher init") +fs::path working_dir = "../data"; +fs::path filename = "test.txt"; +fs::path test_path = working_dir / filename; + +void create_delete_loop(int iterations, ms action_delta) +{ + for (int i = 0; i < iterations; ++i) + { + // create test file + std::ofstream outfile(test_path); + outfile.close(); + std::this_thread::sleep_for(action_delta); + + // remove test file + fs::remove(test_path); + std::this_thread::sleep_for(action_delta); + } +} + +void modify_loop(int iterations, ms action_delta) +{ + // create test file + std::ofstream outfile(test_path); + outfile.close(); + std::this_thread::sleep_for(action_delta); + + // append TEST multiple times + for (int i = 0; i < iterations; ++i) + { + outfile.open(test_path, std::ios_base::app); + outfile << "TEST" << i; + outfile.close(); + std::this_thread::sleep_for(action_delta); + } + + // remove test file + fs::remove(test_path); + std::this_thread::sleep_for(action_delta); +} + +TEST(FSWatcherTest, CreateDeleteLoop) { FSWatcher watcher; - watcher.init(); + + // parameters + int iterations = 10; + int created_no = 0; + int deleted_no = 0; + + // time distance between two actions should be big enough otherwise + // one event will hide another one + ms action_delta = watcher.check_interval() * 2; + + // watchers + watcher.watch(WatchDescriptor(working_dir, FSEventType::Created), + [&](FSEvent) {}); + watcher.watch(WatchDescriptor(working_dir, FSEventType::Deleted), + [&](FSEvent) {}); + // above watchers should be ignored + watcher.watch(WatchDescriptor(working_dir, FSEventType::All), + [&](FSEvent event) { + if (event.type == FSEventType::Created) created_no++; + if (event.type == FSEventType::Deleted) deleted_no++; + }); + + ASSERT_EQ(watcher.size(), 1); + + create_delete_loop(iterations, action_delta); + ASSERT_EQ(created_no, iterations); + ASSERT_EQ(deleted_no, iterations); + + watcher.unwatchAll(); + ASSERT_EQ(watcher.size(), 0); + + watcher.unwatchAll(); + ASSERT_EQ(watcher.size(), 0); + + create_delete_loop(iterations, action_delta); + ASSERT_EQ(created_no, iterations); + ASSERT_EQ(deleted_no, iterations); +} + +TEST(FSWatcherTest, ModifiyLoop) +{ + FSWatcher watcher; + + // parameters + int iterations = 10; + int modified_no = 0; + ms action_delta = watcher.check_interval() * 2; + + watcher.watch(WatchDescriptor(working_dir, FSEventType::Modified), + [&](FSEvent) { modified_no++; }); + ASSERT_EQ(watcher.size(), 1); + + modify_loop(iterations, action_delta); + ASSERT_EQ(modified_no, iterations); + + watcher.unwatch(WatchDescriptor(working_dir, FSEventType::Modified)); + ASSERT_EQ(watcher.size(), 0); + + watcher.unwatch(WatchDescriptor(working_dir, FSEventType::Modified)); + ASSERT_EQ(watcher.size(), 0); + + modify_loop(iterations, action_delta); + ASSERT_EQ(modified_no, iterations); +} + +int main(int argc, char **argv) +{ + logging::init_sync(); + logging::log->pipe(std::make_unique<Stdout>()); + + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); }