From 115f89472777e434c3ad71da027c63ab1ee05cf6 Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 5 Jul 2026 19:42:19 +0800 Subject: [PATCH 1/2] refactor(async): remove fs_event module Remove the fs_event filesystem watcher entirely: the platform backends were hard to use and unreliable, and clice #473 already dropped file watching. Recommended replacement is periodic polling with kota::timer plus fs::stat (compare mtime/size). - Delete include/kota/async/io/fs_event.h (71 lines) and src/async/io/fs_event.cpp (965 lines, all three platform backends) - Delete tests: fs_event_tests.cpp, fs_event_dir_tests.cpp, and the shared fs_event_fixture.h (~2900 lines) - Drop the fs_event include from kota/async/async.h - Drop the fs_event source entry and the Apple-only "-framework CoreServices" link (fs_event was its only user) from src/async/CMakeLists.txt; drop the matching CoreServices frameworks entry from xmake.lua - README: remove the fs_event mention and add a migration note pointing at the timer + fs::stat polling pattern uv::poll_* wrappers are untouched (src/http/manager.cpp uses them), and the fs.h/fs.cpp API surface is fully preserved. Net: 8 insertions, 3936 deletions. Tests: 1367 passed, 2 skipped. --- README.md | 9 +- include/kota/async/async.h | 1 - include/kota/async/io/fs_event.h | 71 - src/async/CMakeLists.txt | 7 - src/async/io/fs_event.cpp | 965 -------- tests/unit/async/fs_event_fixture.h | 41 - tests/unit/async/io/fs_event_dir_tests.cpp | 2322 -------------------- tests/unit/async/io/fs_event_tests.cpp | 525 ----- xmake.lua | 3 - 9 files changed, 8 insertions(+), 3936 deletions(-) delete mode 100644 include/kota/async/io/fs_event.h delete mode 100644 src/async/io/fs_event.cpp delete mode 100644 tests/unit/async/fs_event_fixture.h delete mode 100644 tests/unit/async/io/fs_event_dir_tests.cpp delete mode 100644 tests/unit/async/io/fs_event_tests.cpp diff --git a/README.md b/README.md index 8048ec17..79271340 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,15 @@ All public APIs live under the `kota::` namespace, public headers under `include - pipes, TCP sockets, TCP acceptors, console / TTY streams - UDP sockets with multicast and per-packet send/recv - Child process API (`process::spawn`) with stdio piping, async wait/kill, and resource-usage reporting. -- Async filesystem API covering the full libuv fs surface (stat / mkdir / scandir / chmod / link / rename / sendfile / utime / mkstemp / …) and filesystem-change notifications via `fs_event`. +- Async filesystem API covering the full libuv fs surface (stat / mkdir / scandir / chmod / link / rename / sendfile / utime / mkstemp / …). - Libuv watcher wrappers: timer, idle, prepare, check, signal, plus a `sleep` helper. + +> **Filesystem-change notifications** are no longer provided. The former `fs_event` +> watcher has been removed — the underlying platform backends proved hard to use and +> unreliable across OSes. For change detection, build a periodic poll on top of the +> existing primitives: a `kota::timer` firing at your desired interval combined with +> `fs::stat` (comparing mtime / size) gives predictable, portable "did this path +> change?" semantics without the platform-specific pitfalls. - Blocking-work offload via `queue(fn, loop)` onto the libuv thread pool. - Coroutine-friendly sync primitives: mutex, semaphore, event (with interrupt), and condition variable. - Error vocabulary: `error` (libuv status wrapper with named codes), `result`, and the general `outcome`. diff --git a/include/kota/async/async.h b/include/kota/async/async.h index 6114c49c..806bb142 100644 --- a/include/kota/async/async.h +++ b/include/kota/async/async.h @@ -2,7 +2,6 @@ #include "kota/support/config.h" #include "kota/async/io/fs.h" -#include "kota/async/io/fs_event.h" #include "kota/async/io/loop.h" #include "kota/async/io/process.h" #include "kota/async/io/request.h" diff --git a/include/kota/async/io/fs_event.h b/include/kota/async/io/fs_event.h deleted file mode 100644 index 12e9049e..00000000 --- a/include/kota/async/io/fs_event.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "kota/async/io/loop.h" -#include "kota/async/runtime/task.h" -#include "kota/async/vocab/error.h" - -namespace kota { - -class fs_event { -public: - fs_event() noexcept; - - fs_event(const fs_event&) = delete; - fs_event& operator=(const fs_event&) = delete; - - fs_event(fs_event&&) noexcept; - fs_event& operator=(fs_event&&) noexcept; - - ~fs_event(); - - enum class effect : std::uint8_t { - create, - modify, - destroy, - rename, - overflow, - // Default for change{}; no platform backend produces this value. - other, - }; - - struct change { - std::string path; - effect type = effect::other; - std::string old_path; - }; - - constexpr static std::chrono::milliseconds default_debounce{200}; - - struct options { - std::chrono::milliseconds debounce = default_debounce; - bool recursive = true; - }; - - static result create(std::string_view path, - options opts, - event_loop& loop = event_loop::current()); - - static result create(std::string_view path, event_loop& loop = event_loop::current()); - - task, error> next(); - - void stop(); - -private: - struct Self; - - explicit fs_event(std::shared_ptr self) noexcept; - - // shared_ptr (not unique_handle): macOS/Windows callbacks run on background - // threads and must prevent Self destruction via shared_from_this(). - std::shared_ptr self; -}; - -} // namespace kota diff --git a/src/async/CMakeLists.txt b/src/async/CMakeLists.txt index 56841490..f190b796 100644 --- a/src/async/CMakeLists.txt +++ b/src/async/CMakeLists.txt @@ -51,7 +51,6 @@ target_sources(kota_async PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/io/system.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/io/udp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/io/watcher.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/io/fs_event.cpp" ) target_include_directories(kota_async PUBLIC @@ -62,10 +61,4 @@ target_link_libraries(kota_async PUBLIC libuv::libuv ) -if(APPLE) - target_link_libraries(kota_async PRIVATE - "-framework CoreServices" - ) -endif() - kota_apply_project_options(kota_async) diff --git a/src/async/io/fs_event.cpp b/src/async/io/fs_event.cpp deleted file mode 100644 index 8013a832..00000000 --- a/src/async/io/fs_event.cpp +++ /dev/null @@ -1,965 +0,0 @@ -#include "kota/async/io/fs_event.h" - -#include -#include -#include -#include -#include - -#include "kota/async/io/loop.h" -#include "kota/async/io/watcher.h" -#include "kota/async/runtime/sync.h" - -#if defined(__linux__) -#include -#include -#include -#include -#elif defined(__APPLE__) -#include -#include -#include -#include -#include -#include -#elif defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include -#include -#endif - -namespace kota { - -// Common state shared by all three platform Self structs. -// -// Thread safety: `closed` is atomic and may be read/written from any thread. -// All other fields are only accessed from the event-loop thread except during -// platform callbacks (macOS dispatch queue, Windows worker thread), which -// read `root_path`, `recursive` (immutable after create) and call -// post_change()/post_changes() → notifier.send() (thread-safe) to marshal -// events to the loop thread. stop_platform() must fully synchronize with -// all platform callbacks before returning, so that stop() can safely -// destroy the notifier afterwards. -struct fs_event_base { - event_loop* loop; - relay notifier; - timer debounce_timer; - event has_events{false}; - // Accessed only on the event-loop thread; guarded by debounce_timer. - std::vector buffer; - std::chrono::milliseconds debounce_ms; - std::atomic closed{false}; - // Immutable after create(). - bool recursive = false; - std::string root_path; - std::string file_filter; - - void push_event(fs_event::change c) { - if(closed.load(std::memory_order_acquire)) - return; - buffer.push_back(std::move(c)); - has_events.set(); - } - - void post_change(std::weak_ptr weak, fs_event::change c) { - notifier.send([weak = std::move(weak), c = std::move(c)]() mutable { - if(auto s = weak.lock()) { - s->push_event(std::move(c)); - } - }); - } - - void post_changes(std::weak_ptr weak, std::vector changes) { - notifier.send([weak = std::move(weak), changes = std::move(changes)]() mutable { - if(auto s = weak.lock()) { - for(auto& c: changes) { - s->push_event(std::move(c)); - } - } - }); - } -}; - -#if defined(__linux__) - -namespace { - -constexpr uint32_t inotify_mask = IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | - IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO | IN_DONT_FOLLOW | - IN_ONLYDIR | IN_EXCL_UNLINK; - -} // namespace - -struct fs_event::Self : fs_event_base, std::enable_shared_from_this { - int inotify_fd = -1; - uv_poll_t poll_handle{}; - bool poll_initialized = false; - - std::unordered_map wd_to_path; - std::unordered_map path_to_wd; - - // Safety net: stop_platform() handles the full teardown; this only - // closes the fd in case Self outlives the fs_event wrapper. - ~Self() { - if(inotify_fd >= 0) { - ::close(inotify_fd); - inotify_fd = -1; - } - wd_to_path.clear(); - path_to_wd.clear(); - } - - void stop_platform() { - if(poll_initialized) { - uv_poll_stop(&poll_handle); - auto prevent_destroy = new std::shared_ptr(shared_from_this()); - poll_handle.data = prevent_destroy; - uv_close(reinterpret_cast(&poll_handle), [](uv_handle_t* h) { - delete static_cast*>(h->data); - }); - poll_initialized = false; - } - if(inotify_fd >= 0) { - ::close(inotify_fd); - inotify_fd = -1; - } - wd_to_path.clear(); - path_to_wd.clear(); - } - - bool add_watch(const std::string& path) { - int wd = inotify_add_watch(inotify_fd, path.c_str(), inotify_mask); - if(wd < 0) { - return false; - } - wd_to_path[wd] = path; - path_to_wd[path] = wd; - return true; - } - - constexpr static int max_scan_depth = 128; - - void scan_directory(const std::string& path, int depth = 0) { - if(!add_watch(path)) { - return; - } - if(!recursive || depth >= max_scan_depth) { - return; - } - std::error_code ec; - for(auto& entry: std::filesystem::directory_iterator(path, ec)) { - if(ec) - break; - if(entry.is_directory(ec) && !ec) { - scan_directory(entry.path().string(), depth + 1); - } - } - } - - std::string build_path(int wd, const char* name, uint32_t len) { - auto it = wd_to_path.find(wd); - if(it == wd_to_path.end()) - return {}; - if(name && len > 0) { - return it->second + "/" + name; - } - return it->second; - } - - void update_renamed_paths(const std::string& old_path, const std::string& new_path) { - std::string old_prefix = old_path + "/"; - std::vector> updates; - for(auto& [wd, p]: wd_to_path) { - if(p == old_path) { - updates.push_back({wd, new_path}); - } else if(p.size() > old_prefix.size() && - p.compare(0, old_prefix.size(), old_prefix) == 0) { - updates.push_back({wd, new_path + p.substr(old_path.size())}); - } - } - for(auto& [wd, new_p]: updates) { - path_to_wd.erase(wd_to_path[wd]); - wd_to_path[wd] = new_p; - path_to_wd[new_p] = wd; - } - } - - void process_inotify_events() { - struct raw_event { - std::string path; - uint32_t mask; - uint32_t cookie; - bool is_dir; - int wd; - bool consumed = false; - }; - - std::vector events; - std::unordered_map move_from_cookies; - - // Drain all available inotify events before processing so that - // cookie-paired IN_MOVED_FROM/IN_MOVED_TO are matched even if - // the kernel delivers them across separate read() buffers. - constexpr size_t inotify_read_buf_size = 8192; - alignas(struct inotify_event) char buf[inotify_read_buf_size]; - for(;;) { - ssize_t n = ::read(inotify_fd, buf, sizeof(buf)); - if(n < 0) { - if(errno == EINTR) - continue; - break; - } - if(n == 0) - break; - - for(char* p = buf; p < buf + n;) { - auto* ev = reinterpret_cast(p); - p += sizeof(struct inotify_event) + ev->len; - - if(ev->mask & IN_Q_OVERFLOW) { - push_event(change{{}, effect::overflow, {}}); - continue; - } - - std::string path = build_path(ev->wd, ev->name, ev->len); - if(path.empty()) - continue; - - bool is_dir = (ev->mask & IN_ISDIR) != 0; - events.push_back({std::move(path), ev->mask, ev->cookie, is_dir, ev->wd}); - - if((ev->mask & IN_MOVED_FROM) && ev->cookie != 0) { - move_from_cookies[ev->cookie] = events.size() - 1; - } - } - } - - for(size_t i = 0; i < events.size(); i++) { - auto& e = events[i]; - if(e.consumed) - continue; - - if(e.mask & IN_MOVED_TO) { - auto it = move_from_cookies.find(e.cookie); - if(it != move_from_cookies.end()) { - auto& from = events[it->second]; - from.consumed = true; - push_event(change{e.path, effect::rename, from.path}); - if(e.is_dir && recursive) { - update_renamed_paths(from.path, e.path); - scan_directory(e.path); - } - } else { - push_event(change{e.path, effect::create, {}}); - if(e.is_dir && recursive) { - scan_directory(e.path); - } - } - } else if(e.mask & IN_CREATE) { - push_event(change{e.path, effect::create, {}}); - if(e.is_dir && recursive) { - scan_directory(e.path); - } - } else if(e.mask & (IN_MODIFY | IN_ATTRIB)) { - push_event(change{e.path, effect::modify, {}}); - } else if(e.mask & IN_MOVED_FROM) { - push_event(change{e.path, effect::destroy, {}}); - if(e.is_dir) { - auto pit = path_to_wd.find(e.path); - if(pit != path_to_wd.end()) { - wd_to_path.erase(pit->second); - path_to_wd.erase(pit); - } - } - } else if(e.mask & (IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF)) { - bool is_self = (e.mask & (IN_DELETE_SELF | IN_MOVE_SELF)) != 0; - if(!is_self || e.path == root_path) { - push_event(change{e.path, effect::destroy, {}}); - } - if(!is_self && e.is_dir) { - auto pit = path_to_wd.find(e.path); - if(pit != path_to_wd.end()) { - wd_to_path.erase(pit->second); - path_to_wd.erase(pit); - } - } - } - - if(e.mask & IN_IGNORED) { - auto wit = wd_to_path.find(e.wd); - if(wit != wd_to_path.end()) { - path_to_wd.erase(wit->second); - wd_to_path.erase(wit); - } - } - } - } - - static void on_poll(uv_poll_t* handle, int status, int events) { - auto* self = static_cast(handle->data); - if(status < 0 || self->closed.load(std::memory_order_acquire)) - return; - if(events & UV_READABLE) { - self->process_inotify_events(); - } - } - - error init_platform(std::shared_ptr& s) { - inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); - if(inotify_fd < 0) { - return error::unknown_error; - } - - scan_directory(root_path); - if(wd_to_path.empty()) { - ::close(inotify_fd); - inotify_fd = -1; - return error::permission_denied; - } - - uv_loop_t& uv = *loop; - int rc = uv_poll_init(&uv, &poll_handle, inotify_fd); - if(rc < 0) { - ::close(inotify_fd); - inotify_fd = -1; - return error(rc); - } - - poll_handle.data = s.get(); - poll_initialized = true; - - rc = uv_poll_start(&poll_handle, UV_READABLE, Self::on_poll); - if(rc < 0) { - stop_platform(); - return error(rc); - } - - // The notifier relay keeps the loop alive, not this poll handle. - uv_unref(reinterpret_cast(&poll_handle)); - return error{}; - } -}; - -#elif defined(__APPLE__) - -namespace { - -constexpr FSEventStreamEventFlags IGNORED_FLAGS = - kFSEventStreamEventFlagItemIsHardlink | kFSEventStreamEventFlagItemIsLastHardlink | - kFSEventStreamEventFlagItemIsSymlink | kFSEventStreamEventFlagItemIsDir | - kFSEventStreamEventFlagItemIsFile -#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101300 - | kFSEventStreamEventFlagItemCloned -#endif - ; - -} // namespace - -struct fs_event::Self : fs_event_base, std::enable_shared_from_this { - FSEventStreamRef stream = nullptr; - dispatch_queue_t dispatch_queue = nullptr; - std::string pending_old_name; - - ~Self() { - stop_platform(); - } - - void stop_platform() { - if(stream) { - FSEventStreamStop(stream); - FSEventStreamInvalidate(stream); - FSEventStreamRelease(stream); - stream = nullptr; - } - - if(dispatch_queue) { - // After invalidation, an already-dispatched callback may still be - // running on the queue. Synchronously drain it so that stop() can - // safely destroy the notifier without racing with send(). - dispatch_sync_f(dispatch_queue, nullptr, +[](void*) {}); - dispatch_release(dispatch_queue); - dispatch_queue = nullptr; - } - } - - static void fsevents_callback(ConstFSEventStreamRef, - void* info, - size_t num_events, - void* event_paths, - const FSEventStreamEventFlags flags[], - const FSEventStreamEventId[]) { - auto* raw = static_cast(info); - auto shared = raw->shared_from_this(); - - if(shared->closed.load(std::memory_order_acquire)) - return; - - auto** paths = static_cast(event_paths); - - std::vector changes; - changes.reserve(num_events); - - for(size_t i = 0; i < num_events; ++i) { - if(flags[i] & kFSEventStreamEventFlagMustScanSubDirs) { - changes.push_back(change{{}, effect::overflow, {}}); - continue; - } - - if(flags[i] & kFSEventStreamEventFlagHistoryDone) - continue; - if(flags[i] & (kFSEventStreamEventFlagMount | kFSEventStreamEventFlagUnmount)) - continue; - if(flags[i] & kFSEventStreamEventFlagRootChanged) { - struct stat st; - if(stat(shared->root_path.c_str(), &st) != 0) { - changes.push_back(change{shared->root_path, effect::destroy, {}}); - } - continue; - } - - if((flags[i] & ~IGNORED_FLAGS) == 0) { - continue; - } - - std::string path = paths[i]; - if(!path.empty() && path.back() == '/') { - path.pop_back(); - } - - if(!shared->recursive) { - size_t prefix_len = shared->root_path.size(); - if(shared->root_path.back() != '/') - prefix_len += 1; - if(path.size() <= prefix_len) - continue; - auto relative = std::string_view(path).substr(prefix_len); - if(relative.find('/') != std::string_view::npos) { - continue; - } - } - - if(shared->closed.load(std::memory_order_acquire)) - continue; - - bool is_created = (flags[i] & kFSEventStreamEventFlagItemCreated) != 0; - bool is_renamed = (flags[i] & kFSEventStreamEventFlagItemRenamed) != 0; - bool is_modified = (flags[i] & (kFSEventStreamEventFlagItemModified | - kFSEventStreamEventFlagItemInodeMetaMod | - kFSEventStreamEventFlagItemFinderInfoMod | - kFSEventStreamEventFlagItemChangeOwner | - kFSEventStreamEventFlagItemXattrMod)) != 0; - - if(is_renamed) { - struct stat st; - if(stat(path.c_str(), &st) != 0) { - if(!shared->pending_old_name.empty()) { - changes.push_back( - change{std::move(shared->pending_old_name), effect::destroy, {}}); - } - shared->pending_old_name = std::move(path); - } else { - if(!shared->pending_old_name.empty()) { - changes.push_back(change{std::move(path), - effect::rename, - std::move(shared->pending_old_name)}); - shared->pending_old_name.clear(); - } else { - changes.push_back(change{std::move(path), effect::create, {}}); - } - } - continue; - } - - struct stat st; - bool exists = (stat(path.c_str(), &st) == 0); - - if(!exists) { - changes.push_back(change{std::move(path), effect::destroy, {}}); - } else if(is_created) { - // FSEvents may set Created for both genuine creates and for - // modifications of existing files. Use birthtime vs mtime to - // disambiguate: a freshly created file has birthtime ≈ mtime. - long long diff_ms = - (st.st_mtimespec.tv_sec - st.st_birthtimespec.tv_sec) * 1000LL + - (st.st_mtimespec.tv_nsec - st.st_birthtimespec.tv_nsec) / 1000000LL; - constexpr long long create_detect_threshold_ms = 200; - if(diff_ms < create_detect_threshold_ms) { - changes.push_back(change{std::move(path), effect::create, {}}); - } else { - changes.push_back(change{std::move(path), effect::modify, {}}); - } - } else if(is_modified) { - changes.push_back(change{std::move(path), effect::modify, {}}); - } - } - - if(!shared->pending_old_name.empty()) { - changes.push_back(change{std::move(shared->pending_old_name), effect::destroy, {}}); - shared->pending_old_name.clear(); - } - - if(changes.empty()) - return; - - if(shared->closed.load(std::memory_order_acquire)) - return; - - shared->post_changes(shared, std::move(changes)); - } - - error init_platform(std::shared_ptr& s) { - CFStringRef cf_path = - CFStringCreateWithCString(nullptr, root_path.c_str(), kCFStringEncodingUTF8); - if(!cf_path) { - return error::unknown_error; - } - - CFArrayRef paths_to_watch = CFArrayCreate(nullptr, - reinterpret_cast(&cf_path), - 1, - &kCFTypeArrayCallBacks); - CFRelease(cf_path); - - if(!paths_to_watch) { - return error::unknown_error; - } - - FSEventStreamContext ctx{}; - ctx.info = s.get(); - - constexpr double latency_sec = 0.001; - FSEventStreamCreateFlags fs_flags = kFSEventStreamCreateFlagFileEvents; - - stream = FSEventStreamCreate(nullptr, - &Self::fsevents_callback, - &ctx, - paths_to_watch, - kFSEventStreamEventIdSinceNow, - latency_sec, - fs_flags); - - CFRelease(paths_to_watch); - - if(!stream) { - return error::unknown_error; - } - - dispatch_queue = dispatch_queue_create("kota.fs_event", DISPATCH_QUEUE_SERIAL); - if(!dispatch_queue) { - FSEventStreamRelease(stream); - stream = nullptr; - return error::unknown_error; - } - - FSEventStreamSetDispatchQueue(stream, dispatch_queue); - FSEventStreamStart(stream); - return error{}; - } -}; - -#elif defined(_WIN32) - -struct fs_event::Self : fs_event_base, std::enable_shared_from_this { - std::wstring root_wpath; - - HANDLE dir_handle = INVALID_HANDLE_VALUE; - std::thread worker_thread; - std::atomic running{false}; - - constexpr static DWORD DEFAULT_BUF_SIZE = 1024 * 1024; - constexpr static DWORD NETWORK_BUF_SIZE = 64 * 1024; - - std::vector read_buffer; - std::vector write_buffer; - OVERLAPPED overlapped{}; - - ~Self() { - stop_platform(); - } - - void stop_platform() { - if(!worker_thread.joinable()) - return; - - DWORD apc_ok = QueueUserAPC( - [](ULONG_PTR param) { - auto* s = reinterpret_cast(param); - s->running.store(false, std::memory_order_release); - if(s->dir_handle != INVALID_HANDLE_VALUE) { - CancelIoEx(s->dir_handle, &s->overlapped); - } - }, - worker_thread.native_handle(), - reinterpret_cast(this)); - if(!apc_ok) { - running.store(false, std::memory_order_release); - if(dir_handle != INVALID_HANDLE_VALUE) { - CancelIoEx(dir_handle, &overlapped); - } - } - worker_thread.join(); - } - - static std::string wide_to_utf8(const wchar_t* str, size_t len) { - if(len == 0) - return {}; - int size = WideCharToMultiByte(CP_UTF8, - 0, - str, - static_cast(len), - nullptr, - 0, - nullptr, - nullptr); - if(size <= 0) - return {}; - std::string result(size, '\0'); - int written = WideCharToMultiByte(CP_UTF8, - 0, - str, - static_cast(len), - result.data(), - size, - nullptr, - nullptr); - if(written <= 0) - return {}; - result.resize(written); - return result; - } - - void poll() { - if(!running.load(std::memory_order_acquire)) - return; - - BOOL ok = ReadDirectoryChangesW(dir_handle, - write_buffer.data(), - static_cast(write_buffer.size()), - recursive ? TRUE : FALSE, - FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | - FILE_NOTIFY_CHANGE_ATTRIBUTES | - FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE, - nullptr, - &overlapped, - [](DWORD error_code, DWORD num_bytes, LPOVERLAPPED ov) { - auto* self = reinterpret_cast(ov->hEvent); - self->on_completion(error_code, num_bytes); - }); - - if(!ok) { - running.store(false, std::memory_order_release); - post_change(shared_from_this(), change{root_path, effect::destroy, {}}); - } - } - - void on_completion(DWORD error_code, DWORD num_bytes) { - if(!running.load(std::memory_order_acquire)) - return; - - switch(error_code) { - case ERROR_OPERATION_ABORTED: return; - case ERROR_INVALID_PARAMETER: - read_buffer.resize(NETWORK_BUF_SIZE); - write_buffer.resize(NETWORK_BUF_SIZE); - poll(); - return; - case ERROR_NOTIFY_ENUM_DIR: - post_change(shared_from_this(), change{{}, effect::overflow, {}}); - poll(); - return; - case ERROR_ACCESS_DENIED: { - DWORD attrs = GetFileAttributesW(root_wpath.c_str()); - bool is_dir = - attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY); - if(!is_dir) { - post_change(shared_from_this(), change{root_path, effect::destroy, {}}); - running.store(false, std::memory_order_release); - return; - } - poll(); - return; - } - default: - if(error_code != ERROR_SUCCESS) { - running.store(false, std::memory_order_release); - post_change(shared_from_this(), change{root_path, effect::destroy, {}}); - return; - } - } - - std::swap(read_buffer, write_buffer); - poll(); - - if(num_bytes == 0) - return; - - std::vector changes; - std::string pending_old_name; - BYTE* ptr = read_buffer.data(); - for(;;) { - auto* info = reinterpret_cast(ptr); - size_t name_chars = info->FileNameLength / sizeof(wchar_t); - std::string name = wide_to_utf8(info->FileName, name_chars); - std::replace(name.begin(), name.end(), '\\', '/'); - std::string full = root_path + "/" + name; - - switch(info->Action) { - case FILE_ACTION_ADDED: - changes.push_back(change{std::move(full), effect::create, {}}); - break; - case FILE_ACTION_REMOVED: - changes.push_back(change{std::move(full), effect::destroy, {}}); - break; - case FILE_ACTION_MODIFIED: { - std::wstring wfull = - root_wpath + L"/" + std::wstring(info->FileName, name_chars); - DWORD attrs = GetFileAttributesW(wfull.c_str()); - if(attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY)) - break; - changes.push_back(change{std::move(full), effect::modify, {}}); - break; - } - case FILE_ACTION_RENAMED_OLD_NAME: - if(!pending_old_name.empty()) { - changes.push_back(change{std::move(pending_old_name), effect::destroy, {}}); - } - pending_old_name = std::move(full); - break; - case FILE_ACTION_RENAMED_NEW_NAME: - if(!pending_old_name.empty()) { - changes.push_back( - change{std::move(full), effect::rename, std::move(pending_old_name)}); - pending_old_name.clear(); - } else { - changes.push_back(change{std::move(full), effect::create, {}}); - } - break; - } - - if(info->NextEntryOffset == 0) - break; - ptr += info->NextEntryOffset; - } - - if(!pending_old_name.empty()) { - changes.push_back(change{std::move(pending_old_name), effect::destroy, {}}); - } - - if(!changes.empty()) { - post_changes(shared_from_this(), std::move(changes)); - } - } - - void worker_entry() { - poll(); - while(running.load(std::memory_order_acquire)) { - SleepEx(INFINITE, TRUE); - } - while(SleepEx(0, TRUE) == WAIT_IO_COMPLETION) {} - if(dir_handle != INVALID_HANDLE_VALUE) { - CloseHandle(dir_handle); - dir_handle = INVALID_HANDLE_VALUE; - } - } - - error init_platform(std::shared_ptr& s) { - std::wstring wpath; - { - int wlen = MultiByteToWideChar(CP_UTF8, 0, root_path.c_str(), -1, nullptr, 0); - if(wlen <= 0) - return error::invalid_argument; - wpath.resize(wlen - 1); - MultiByteToWideChar(CP_UTF8, 0, root_path.c_str(), -1, wpath.data(), wlen); - } - - dir_handle = CreateFileW(wpath.c_str(), - FILE_LIST_DIRECTORY, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, - nullptr); - - if(dir_handle == INVALID_HANDLE_VALUE) { - return error::no_such_file_or_directory; - } - - BY_HANDLE_FILE_INFORMATION file_info; - if(!GetFileInformationByHandle(dir_handle, &file_info) || - !(file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { - CloseHandle(dir_handle); - dir_handle = INVALID_HANDLE_VALUE; - return error::invalid_argument; - } - - root_wpath = std::move(wpath); - read_buffer.resize(DEFAULT_BUF_SIZE); - write_buffer.resize(DEFAULT_BUF_SIZE); - ZeroMemory(&overlapped, sizeof(OVERLAPPED)); - // APC-only: hEvent is unused by the kernel when a completion routine - // is supplied, so we repurpose it to pass Self* to the callback. - overlapped.hEvent = reinterpret_cast(s.get()); - running.store(true, std::memory_order_release); - - auto shared_for_thread = s; - worker_thread = std::thread([shared_for_thread]() { shared_for_thread->worker_entry(); }); - return error{}; - } -}; - -#endif - -fs_event::fs_event() noexcept = default; - -fs_event::fs_event(std::shared_ptr self) noexcept : self(std::move(self)) {} - -fs_event::~fs_event() { - stop(); -} - -fs_event::fs_event(fs_event&&) noexcept = default; - -fs_event& fs_event::operator=(fs_event&& other) noexcept { - if(this != &other) { - stop(); - self = std::move(other.self); - } - return *this; -} - -result fs_event::create(std::string_view path, options opts, event_loop& loop) { - std::error_code ec; - std::filesystem::path fs_path(path); - auto status = std::filesystem::status(fs_path, ec); - - auto s = std::make_shared(); - s->loop = &loop; - s->debounce_ms = opts.debounce; - - if(!ec && std::filesystem::is_directory(status)) { - s->recursive = opts.recursive; - auto canonical = std::filesystem::canonical(fs_path, ec); - if(ec) { - return outcome_error(error::no_such_file_or_directory); - } - s->root_path = canonical.string(); - } else { - auto parent = fs_path.parent_path(); - auto filename = fs_path.filename().string(); - if(filename.empty()) { - return outcome_error(error::invalid_argument); - } - auto parent_status = std::filesystem::status(parent, ec); - if(ec || !std::filesystem::is_directory(parent_status)) { - return outcome_error(error::no_such_file_or_directory); - } - s->recursive = false; - auto canonical = std::filesystem::canonical(parent, ec); - if(ec) { - return outcome_error(error::no_such_file_or_directory); - } - s->root_path = canonical.string(); - s->file_filter = std::move(filename); - } - -#if defined(_WIN32) - std::replace(s->root_path.begin(), s->root_path.end(), '\\', '/'); -#endif - - s->notifier = loop.create_relay(); - s->debounce_timer = timer::create(loop); - - auto init_err = s->init_platform(s); - if(init_err) { - return outcome_error(init_err); - } - - return fs_event(std::move(s)); -} - -result fs_event::create(std::string_view path, event_loop& loop) { - return create(path, options{}, loop); -} - -// Flow: wait for events → debounce → drain buffer → apply file_filter. -// The outer loop retries when file_filter discards everything in a batch. -// stop() wakes both has_events.wait() and debounce_timer.wait() via the -// closed flag, guaranteeing this coroutine never deadlocks. -task, error> fs_event::next() { - if(!self || self->closed.load(std::memory_order_acquire)) { - co_await fail(error::invalid_argument); - } - - while(true) { - while(self->buffer.empty()) { - if(self->closed.load(std::memory_order_acquire)) { - co_await fail(error::operation_aborted); - } - self->has_events.reset(); - co_await self->has_events.wait(); - - if(self->closed.load(std::memory_order_acquire)) { - co_await fail(error::operation_aborted); - } - } - - self->debounce_timer.start(self->debounce_ms); - co_await self->debounce_timer.wait(); - - if(self->closed.load(std::memory_order_acquire)) { - co_await fail(error::operation_aborted); - } - - auto batch = std::exchange(self->buffer, {}); - - if(self->file_filter.empty()) { - co_return batch; - } - - std::vector filtered; - for(auto& c: batch) { - auto slash = c.path.rfind('/'); - std::string_view name = (slash != std::string::npos) - ? std::string_view(c.path).substr(slash + 1) - : std::string_view(c.path); - if(name != self->file_filter) - continue; - - filtered.push_back(std::move(c)); - } - - if(filtered.empty()) { - if(self->closed.load(std::memory_order_acquire)) { - co_await fail(error::operation_aborted); - } - continue; - } - - co_return filtered; - } -} - -// Must be called from the event-loop thread. has_events and debounce_timer -// are not thread-safe; calling stop() from another thread races with next(). -void fs_event::stop() { - if(!self) { - return; - } - - if(self->closed.exchange(true, std::memory_order_acq_rel)) { - return; - } - - self->has_events.set(); - // Fire immediately instead of stopping: stop() only cancels the timer - // but does not wake a coroutine suspended on debounce_timer.wait(). - self->debounce_timer.start(std::chrono::milliseconds{0}); - - self->stop_platform(); - self->notifier = relay{}; -} - -} // namespace kota diff --git a/tests/unit/async/fs_event_fixture.h b/tests/unit/async/fs_event_fixture.h deleted file mode 100644 index 76793a3b..00000000 --- a/tests/unit/async/fs_event_fixture.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "kota/async/async.h" -#include "kota/async/io/fs_event.h" - -namespace kota { - -// Platform watchers (inotify, FSEvents, ReadDirectoryChangesW) need time to -// register with the kernel after create() returns. This helper encapsulates -// the stabilisation delay so tests don't scatter magic numbers. -inline task wait_for_watcher_ready(event_loop& loop) { - co_await sleep(500, loop); -} - -inline task, error> next_or_timeout(fs_event& w, - event_loop& loop, - int timeout_ms = 10000) { - auto do_timeout = [&]() -> task, error> { - co_await sleep(timeout_ms, loop); - co_await fail(error::connection_timed_out); - }; - - auto result = co_await when_any(w.next(), do_timeout()); - if(result.has_error()) { - co_await fail(result.error()); - } - - co_return std::visit([](auto&& v) -> std::vector { return std::move(v); }, - std::move(*result)); -} - -inline bool has_effect(const std::vector& changes, fs_event::effect eff) { - return std::ranges::any_of(changes, [eff](const auto& c) { return c.type == eff; }); -} - -} // namespace kota diff --git a/tests/unit/async/io/fs_event_dir_tests.cpp b/tests/unit/async/io/fs_event_dir_tests.cpp deleted file mode 100644 index d4a754e8..00000000 --- a/tests/unit/async/io/fs_event_dir_tests.cpp +++ /dev/null @@ -1,2322 +0,0 @@ -#include -#include -#include -#include - -#include "../fs_event_fixture.h" -#include "../loop_fixture.h" -#include "kota/zest/zest.h" - -namespace kota { - -namespace { - -task watch_file_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "test.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::create); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_file_modify(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "modify.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - constexpr std::string_view payload = "hello"; - co_await fs::write(fd, std::span(payload.data(), payload.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::modify); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_file_delete(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "delete.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::unlink(file, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::destroy); - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_file_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string src = (std::filesystem::path(dir) / "before.txt").string(); - std::string dst = (std::filesystem::path(dir) / "after.txt").string(); - - int fd = co_await fs::open(src, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::rename(src, dst, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_relevant = has_effect(changes, fs_event::effect::rename) || - has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::destroy); - - watcher->stop(); - co_await fs::unlink(dst, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found_relevant ? 1 : 0; -} - -task watch_nonexistent_path(event_loop& loop) { - auto result = fs_event::create("/nonexistent/dir/that/does/not/exist", {}, loop); - if(result.has_value()) { - co_return 0; - } - - co_return result.error() == error::no_such_file_or_directory ? 1 : 0; -} - -task watch_close_then_next(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - watcher->stop(); - - auto result = co_await watcher->next(); - - co_await fs::rmdir(dir, loop).or_fail(); - - if(!result.has_error()) { - co_return 0; - } - - co_return result.error() == error::invalid_argument ? 1 : 0; -} - -task watch_stop_during_next(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - // Schedule stop() after a short delay. The key invariant is that - // next() must return (not deadlock). It may return buffered events - // (if the platform delivered some before stop) or operation_aborted. - auto stopper = [&]() -> task { - co_await sleep(100, loop); - watcher->stop(); - }; - auto stop_task = stopper(); - loop.schedule(stop_task); - - auto result = co_await next_or_timeout(*watcher, loop, 5000); - - co_await fs::rmdir(dir, loop).or_fail(); - - if(result.has_error()) { - co_return result.error() == error::operation_aborted ? 1 : 0; - } - co_return 1; -} - -task watch_stop_during_debounce(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{2000}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "debounce_stop.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - // Wait a bit for events to arrive and next() to enter debounce phase, - // then stop while debounce_timer.wait() is suspended. - auto stopper = [&]() -> task { - co_await sleep(200, loop); - watcher->stop(); - }; - auto stop_task = stopper(); - loop.schedule(stop_task); - - auto result = co_await next_or_timeout(*watcher, loop, 5000); - - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - if(result.has_error()) { - co_return result.error() == error::operation_aborted ? 1 : 0; - } - co_return 1; -} - -task watch_multiple_next_calls(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file1 = (std::filesystem::path(dir) / "a.txt").string(); - int fd = co_await fs::open(file1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto batch1 = co_await next_or_timeout(*watcher, loop).or_fail(); - if(batch1.empty()) { - co_return 0; - } - - std::string file2 = (std::filesystem::path(dir) / "b.txt").string(); - fd = co_await fs::open(file2, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto batch2 = co_await next_or_timeout(*watcher, loop).or_fail(); - if(batch2.empty()) { - co_return 0; - } - - watcher->stop(); - co_await fs::unlink(file1, loop).or_fail(); - co_await fs::unlink(file2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return 1; -} - -task watch_debounce_batching(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file1 = (std::filesystem::path(dir) / "x.txt").string(); - std::string file2 = (std::filesystem::path(dir) / "y.txt").string(); - std::string file3 = (std::filesystem::path(dir) / "z.txt").string(); - - int fd = co_await fs::open(file1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - fd = co_await fs::open(file2, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - fd = co_await fs::open(file3, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file1, loop).or_fail(); - co_await fs::unlink(file2, loop).or_fail(); - co_await fs::unlink(file3, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return changes.size() >= 3 ? 1 : 0; -} - -task watch_subdirectory_changes(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string subdir = (std::filesystem::path(dir) / "sub").string(); - co_await fs::mkdir(subdir, 0755, loop).or_fail(); - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(subdir) / "nested.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_subdir_event = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("sub") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(subdir, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found_subdir_event ? 1 : 0; -} - -task watch_non_recursive(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string subdir = (std::filesystem::path(dir) / "child").string(); - co_await fs::mkdir(subdir, 0755, loop).or_fail(); - - auto watcher = - fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}, false}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - // Create file in subdir first (should be excluded) - std::string deep_file = (std::filesystem::path(subdir) / "deep.txt").string(); - int fd = co_await fs::open(deep_file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - co_await sleep(200, loop); - - // Create file at top level (should be visible) - std::string file = (std::filesystem::path(dir) / "top.txt").string(); - fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_top = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("top.txt") != std::string::npos; - }); - bool found_deep = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("deep.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(deep_file, loop).or_fail(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(subdir, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_top && !found_deep) ? 1 : 0; -} - -task watch_move_assignment(event_loop& loop) { - auto dir_template1 = (std::filesystem::temp_directory_path() / "kotatsu-dw1-XXXXXX").string(); - auto dir_template2 = (std::filesystem::temp_directory_path() / "kotatsu-dw2-XXXXXX").string(); - std::string dir1 = co_await fs::mkdtemp(dir_template1, loop).or_fail(); - std::string dir2 = co_await fs::mkdtemp(dir_template2, loop).or_fail(); - - auto watcher_a = fs_event::create(dir1, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher_a.has_value()) { - co_await fail(watcher_a.error()); - } - - auto watcher_b = fs_event::create(dir2, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher_b.has_value()) { - co_await fail(watcher_b.error()); - } - - *watcher_a = std::move(*watcher_b); - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir2) / "moved.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher_a, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::create); - - watcher_a->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir1, loop).or_fail(); - co_await fs::rmdir(dir2, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_destructor_cleanup(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - { - auto watcher = - fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - } - - co_await fs::rmdir(dir, loop).or_fail(); - - co_return 1; -} - -task watch_double_close(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - watcher->stop(); - watcher->stop(); - - co_await fs::rmdir(dir, loop).or_fail(); - - co_return 1; -} - -task watch_default_options(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, {}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return 1; -} - -task watch_rename_populates_old_path(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string src = (std::filesystem::path(dir) / "old_name.txt").string(); - std::string dst = (std::filesystem::path(dir) / "new_name.txt").string(); - - int fd = co_await fs::open(src, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::rename(src, dst, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_rename = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::rename && - c.path.find("new_name.txt") != std::string::npos; - }); - bool found_old_path = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::rename && - c.path.find("new_name.txt") != std::string::npos && - c.old_path.find("old_name.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(dst, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_rename && found_old_path) ? 1 : 0; -} - -task watch_rename_existing_file(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string src = (std::filesystem::path(dir) / "existing.txt").string(); - int fd = co_await fs::open(src, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - co_await sleep(100, loop); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string dst = (std::filesystem::path(dir) / "renamed.txt").string(); - co_await fs::rename(src, dst, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::rename) || - has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::destroy); - - watcher->stop(); - co_await fs::unlink(dst, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_directory_creation(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub = (std::filesystem::path(dir) / "newdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("newdir") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_directory_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub1 = (std::filesystem::path(dir) / "before_dir").string(); - co_await fs::mkdir(sub1, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub2 = (std::filesystem::path(dir) / "after_dir").string(); - co_await fs::rename(sub1, sub2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_relevant = has_effect(changes, fs_event::effect::rename) || - has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::destroy); - - watcher->stop(); - co_await fs::rmdir(sub2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found_relevant ? 1 : 0; -} - -task watch_directory_deletion(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "todelete").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::rmdir(sub, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("todelete") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_subfile_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub = (std::filesystem::path(dir) / "subdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - // Consume the mkdir event batch - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await sleep(100, loop); - - std::string file = (std::filesystem::path(sub) / "child.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("child.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_subfile_modify(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "subdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - std::string file = (std::filesystem::path(sub) / "edit.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - constexpr std::string_view payload = "updated"; - co_await fs::write(fd, std::span(payload.data(), payload.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::modify && c.path.find("edit.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_subfile_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "subdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - std::string src = (std::filesystem::path(sub) / "a.txt").string(); - int fd = co_await fs::open(src, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string dst = (std::filesystem::path(sub) / "b.txt").string(); - co_await fs::rename(src, dst, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return (c.type == fs_event::effect::rename || c.type == fs_event::effect::create || - c.type == fs_event::effect::destroy) && - c.path.find("subdir") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(dst, loop).or_fail(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_subfile_delete(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "subdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - std::string file = (std::filesystem::path(sub) / "gone.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::unlink(file, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("gone.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_nested_subdir_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub1 = (std::filesystem::path(dir) / "level1").string(); - co_await fs::mkdir(sub1, 0755, loop).or_fail(); - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await sleep(100, loop); - - std::string sub2 = (std::filesystem::path(sub1) / "level2").string(); - co_await fs::mkdir(sub2, 0755, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("level2") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(sub2, loop).or_fail(); - co_await fs::rmdir(sub1, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_deep_nested_file(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string d1 = (std::filesystem::path(dir) / "a").string(); - co_await fs::mkdir(d1, 0755, loop).or_fail(); - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await sleep(100, loop); - - std::string d2 = (std::filesystem::path(d1) / "b").string(); - co_await fs::mkdir(d2, 0755, loop).or_fail(); - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await sleep(100, loop); - - std::string file = (std::filesystem::path(d2) / "deep.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("deep.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(d2, loop).or_fail(); - co_await fs::rmdir(d1, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_subdir_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub1 = (std::filesystem::path(dir) / "orig").string(); - co_await fs::mkdir(sub1, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub2 = (std::filesystem::path(dir) / "moved").string(); - co_await fs::rename(sub1, sub2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_relevant = has_effect(changes, fs_event::effect::rename) || - has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::destroy); - - watcher->stop(); - co_await fs::rmdir(sub2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found_relevant ? 1 : 0; -} - -task watch_renamed_dir_still_tracked(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "trackme").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub2 = (std::filesystem::path(dir) / "tracked").string(); - co_await fs::rename(sub, sub2, loop).or_fail(); - - // Consume rename events - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(sub2) / "after_rename.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("after_rename.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(sub2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_error_on_bad_parent(event_loop& loop) { - auto result = fs_event::create("/nonexistent/parent/file.txt", {}, loop); - co_return result.has_error() ? 1 : 0; -} - -task watch_multiple_watchers_same_dir(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto w1 = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - auto w2 = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - - if(!w1.has_value() || !w2.has_value()) { - co_await fail(error::unknown_error); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "shared.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes1 = co_await next_or_timeout(*w1, loop).or_fail(); - auto changes2 = co_await next_or_timeout(*w2, loop).or_fail(); - - bool w1_saw = has_effect(changes1, fs_event::effect::create); - bool w2_saw = has_effect(changes2, fs_event::effect::create); - - w1->stop(); - w2->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (w1_saw && w2_saw) ? 1 : 0; -} - -task watch_multiple_watchers_different_dirs(event_loop& loop) { - auto t1 = (std::filesystem::temp_directory_path() / "kotatsu-dw1-XXXXXX").string(); - auto t2 = (std::filesystem::temp_directory_path() / "kotatsu-dw2-XXXXXX").string(); - std::string dir1 = co_await fs::mkdtemp(t1, loop).or_fail(); - std::string dir2 = co_await fs::mkdtemp(t2, loop).or_fail(); - - auto w1 = fs_event::create(dir1, fs_event::options{std::chrono::milliseconds{50}}, loop); - auto w2 = fs_event::create(dir2, fs_event::options{std::chrono::milliseconds{50}}, loop); - - if(!w1.has_value() || !w2.has_value()) { - co_await fail(error::unknown_error); - } - - co_await wait_for_watcher_ready(loop); - - std::string f1 = (std::filesystem::path(dir1) / "one.txt").string(); - std::string f2 = (std::filesystem::path(dir2) / "two.txt").string(); - int fd = co_await fs::open(f1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - fd = co_await fs::open(f2, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto c1 = co_await next_or_timeout(*w1, loop).or_fail(); - auto c2 = co_await next_or_timeout(*w2, loop).or_fail(); - - bool w1_saw = std::ranges::any_of(c1, [](const auto& c) { - return c.path.find("one.txt") != std::string::npos; - }); - bool w2_saw = std::ranges::any_of(c2, [](const auto& c) { - return c.path.find("two.txt") != std::string::npos; - }); - - w1->stop(); - w2->stop(); - co_await fs::unlink(f1, loop).or_fail(); - co_await fs::unlink(f2, loop).or_fail(); - co_await fs::rmdir(dir1, loop).or_fail(); - co_await fs::rmdir(dir2, loop).or_fail(); - - co_return (w1_saw && w2_saw) ? 1 : 0; -} - -task watch_rapid_create_delete(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{100}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string f1 = (std::filesystem::path(dir) / "keep.txt").string(); - std::string f2 = (std::filesystem::path(dir) / "ephemeral.txt").string(); - - int fd = co_await fs::open(f1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - fd = co_await fs::open(f2, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::unlink(f2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_keep_create = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("keep.txt") != std::string::npos && c.type == fs_event::effect::create; - }); - - watcher->stop(); - co_await fs::unlink(f1, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_keep_create ? 1 : 0; -} - -task watch_rapid_multiple_writes(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "multi.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{100}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - for(int i = 0; i < 5; ++i) { - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - std::string payload = "write" + std::to_string(i); - co_await fs::write(fd, std::span(payload.data(), payload.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - } - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_modify = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::modify && c.path.find("multi.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_modify ? 1 : 0; -} - -task watch_attribute_change(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "attrs.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::chmod(file, 0444, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("attrs.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::chmod(file, 0644, loop).or_fail(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_atomic_replace(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string target = (std::filesystem::path(dir) / "target.txt").string(); - int fd = co_await fs::open(target, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view original = "original"; - co_await fs::write(fd, std::span(original.data(), original.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string tmp = (std::filesystem::path(dir) / "target.txt.tmp").string(); - fd = co_await fs::open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view updated = "updated"; - co_await fs::write(fd, std::span(updated.data(), updated.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::rename(tmp, target, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("target.txt") != std::string::npos && - (c.type == fs_event::effect::create || c.type == fs_event::effect::modify); - }); - - watcher->stop(); - co_await fs::unlink(target, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_toctou_dir_and_file(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{100}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string sub = (std::filesystem::path(dir) / "racedir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - std::string file = (std::filesystem::path(sub) / "quick.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - bool found = false; - for(int attempt = 0; attempt < 3 && !found; ++attempt) { - auto result = co_await next_or_timeout(*watcher, loop, 3000); - if(result.has_error()) - break; - found = std::ranges::any_of(*result, [](const auto& c) { - return c.path.find("quick.txt") != std::string::npos; - }); - } - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_unicode_filename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "\xe6\x96\x87\xe4\xbb\xb6.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && - c.path.find("\xe6\x96\x87\xe4\xbb\xb6") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -// Windows symlink creation requires SeCreateSymbolicLinkPrivilege, which -// is unavailable in unprivileged CI runners. -#if !defined(_WIN32) -task watch_symlink_create_delete(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string real = (std::filesystem::path(dir) / "real.txt").string(); - int fd = co_await fs::open(real, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string link = (std::filesystem::path(dir) / "link.txt").string(); - co_await fs::symlink(real, link, 0, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_create = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("link.txt") != std::string::npos; - }); - - co_await fs::unlink(link, loop).or_fail(); - - auto changes2 = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_delete = std::ranges::any_of(changes2, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("link.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(real, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_create && found_delete) ? 1 : 0; -} -#endif - -task watch_large_burst(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - constexpr int count = 50; - std::vector files; - files.reserve(count); - for(int i = 0; i < count; ++i) { - std::string file = - (std::filesystem::path(dir) / ("burst_" + std::to_string(i) + ".txt")).string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - files.push_back(std::move(file)); - } - - int total_creates = 0; - for(int attempt = 0; attempt < 5; ++attempt) { - auto result = co_await next_or_timeout(*watcher, loop, 3000); - if(result.has_error()) - break; - total_creates += static_cast(std::ranges::count_if(*result, [](const auto& c) { - return c.type == fs_event::effect::create; - })); - } - - watcher->stop(); - for(auto& f: files) { - co_await fs::unlink(f, loop).or_fail(); - } - co_await fs::rmdir(dir, loop).or_fail(); - - co_return total_creates >= (count * 48 / 50) ? 1 : 0; -} - -task watch_debounce_coalesces(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "coalesce.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - for(int i = 0; i < 10; ++i) { - std::string f = - (std::filesystem::path(dir) / ("coalesce_" + std::to_string(i) + ".txt")).string(); - fd = co_await fs::open(f, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - } - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - auto create_count = static_cast(std::ranges::count_if(changes, [](const auto& c) { - return c.type == fs_event::effect::create; - })); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - for(int i = 0; i < 10; ++i) { - std::string f = - (std::filesystem::path(dir) / ("coalesce_" + std::to_string(i) + ".txt")).string(); - co_await fs::unlink(f, loop).or_fail(); - } - co_await fs::rmdir(dir, loop).or_fail(); - - co_return create_count >= 9 ? 1 : 0; -} - -task watch_root_dir_deleted(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - auto canonical_dir = std::filesystem::canonical(dir).string(); -#if defined(_WIN32) - std::replace(canonical_dir.begin(), canonical_dir.end(), '\\', '/'); -#endif - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::rmdir(dir, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [&](const auto& c) { - return c.type == fs_event::effect::destroy && c.path == canonical_dir; - }); - - watcher->stop(); - co_return found ? 1 : 0; -} - -task watch_subdir_delete_with_files(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "mydir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(sub) / "child.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - co_await next_or_timeout(*watcher, loop).or_fail(); - co_await sleep(200, loop); - - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(sub, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_file_delete = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("child.txt") != std::string::npos; - }); - bool found_dir_delete = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("mydir") != std::string::npos && - c.path.find("child.txt") == std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_file_delete && found_dir_delete) ? 1 : 0; -} - -// See watch_symlink_create_delete for why Windows is skipped. -#if !defined(_WIN32) -task watch_symlink_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string real = (std::filesystem::path(dir) / "real.txt").string(); - int fd = co_await fs::open(real, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - std::string link1 = (std::filesystem::path(dir) / "link1.txt").string(); - co_await fs::symlink(real, link1, 0, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string link2 = (std::filesystem::path(dir) / "link2.txt").string(); - co_await fs::rename(link1, link2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_rename = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::rename && c.path.find("link2.txt") != std::string::npos; - }); - bool found_destroy = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("link1.txt") != std::string::npos; - }); - bool found_create = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("link2.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(link2, loop).or_fail(); - co_await fs::unlink(real, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_rename || (found_destroy && found_create)) ? 1 : 0; -} - -task watch_symlink_update(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string real = (std::filesystem::path(dir) / "real.txt").string(); - int fd = co_await fs::open(real, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view initial = "hello"; - co_await fs::write(fd, std::span(initial.data(), initial.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - std::string link = (std::filesystem::path(dir) / "link.txt").string(); - co_await fs::symlink(real, link, 0, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(link, O_WRONLY, 0, loop).or_fail(); - constexpr std::string_view updated = "world"; - co_await fs::write(fd, std::span(updated.data(), updated.size()), 0, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_modify = has_effect(changes, fs_event::effect::modify); - - watcher->stop(); - co_await fs::unlink(link, loop).or_fail(); - co_await fs::unlink(real, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found_modify ? 1 : 0; -} - -task watch_folder_symlink(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "realdir").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string link = (std::filesystem::path(dir) / "linkdir").string(); - co_await fs::symlink(sub, link, 0, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_create = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("linkdir") != std::string::npos; - }); - - co_await fs::unlink(link, loop).or_fail(); - - auto changes2 = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found_delete = std::ranges::any_of(changes2, [](const auto& c) { - return c.type == fs_event::effect::destroy && c.path.find("linkdir") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(sub, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (found_create && found_delete) ? 1 : 0; -} -#endif - -task watch_rapid_create_update(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "rapid.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view v1 = "hello"; - co_await fs::write(fd, std::span(v1.data(), v1.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - constexpr std::string_view v2 = "updated"; - co_await fs::write(fd, std::span(v2.data(), v2.size()), 0, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_create = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("rapid.txt") != std::string::npos && c.type == fs_event::effect::create; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_create ? 1 : 0; -} - -task watch_rapid_update_delete(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "doomed.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view initial = "hello"; - co_await fs::write(fd, std::span(initial.data(), initial.size()), -1, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - constexpr std::string_view updated = "updated"; - co_await fs::write(fd, std::span(updated.data(), updated.size()), 0, loop) - .or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::unlink(file, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_destroy = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("doomed.txt") != std::string::npos && - c.type == fs_event::effect::destroy; - }); - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_destroy ? 1 : 0; -} - -task watch_rapid_delete_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "phoenix.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view v1 = "v1"; - co_await fs::write(fd, std::span(v1.data(), v1.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) { - co_await fail(watcher.error()); - } - - co_await wait_for_watcher_ready(loop); - - co_await fs::unlink(file, loop).or_fail(); - fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view v2 = "v2"; - co_await fs::write(fd, std::span(v2.data(), v2.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_relevant = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("phoenix.txt") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_relevant ? 1 : 0; -} - -task watch_case_only_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string lower = (std::filesystem::path(dir) / "hello.txt").string(); - std::string upper = (std::filesystem::path(dir) / "HELLO.TXT").string(); - - int fd = co_await fs::open(lower, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view data = "hello"; - co_await fs::write(fd, std::span(data.data(), data.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - co_await fs::rename(lower, upper, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_event = has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::destroy) || - has_effect(changes, fs_event::effect::rename); - - watcher->stop(); - co_await fs::unlink(upper, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_event ? 1 : 0; -} - -task watch_nested_dir_rename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string sub = (std::filesystem::path(dir) / "parent").string(); - std::string subsub = (std::filesystem::path(dir) / "parent" / "child").string(); - co_await fs::mkdir(sub, 0755, loop).or_fail(); - co_await fs::mkdir(subsub, 0755, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string sub2 = (std::filesystem::path(dir) / "parent2").string(); - std::string subsub2 = (std::filesystem::path(dir) / "parent2" / "child2").string(); - co_await fs::rename(sub, sub2, loop).or_fail(); - co_await fs::rename(sub2 + "/child", subsub2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_parent = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("parent2") != std::string::npos && - c.path.find("child") == std::string::npos; - }); - bool saw_child = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("child2") != std::string::npos; - }); - - watcher->stop(); - co_await fs::rmdir(subsub2, loop).or_fail(); - co_await fs::rmdir(sub2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (saw_parent && saw_child) ? 1 : 0; -} - -task watch_create_rename_coalesce(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string f1 = (std::filesystem::path(dir) / "temp.txt").string(); - std::string f2 = (std::filesystem::path(dir) / "final.txt").string(); - - int fd = co_await fs::open(f1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view data = "hello"; - co_await fs::write(fd, std::span(data.data(), data.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::rename(f1, f2, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_final = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("final.txt") != std::string::npos && - (c.type == fs_event::effect::create || c.type == fs_event::effect::rename); - }); - - watcher->stop(); - co_await fs::unlink(f2, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_final ? 1 : 0; -} - -task watch_chain_rename_coalesce(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{200}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string f1 = (std::filesystem::path(dir) / "step1.txt").string(); - std::string f2 = (std::filesystem::path(dir) / "step2.txt").string(); - std::string f3 = (std::filesystem::path(dir) / "step3.txt").string(); - std::string f4 = (std::filesystem::path(dir) / "step4.txt").string(); - - int fd = co_await fs::open(f1, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - constexpr std::string_view data = "chain"; - co_await fs::write(fd, std::span(data.data(), data.size()), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::rename(f1, f2, loop).or_fail(); - co_await fs::rename(f2, f3, loop).or_fail(); - co_await fs::rename(f3, f4, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool saw_final = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("step4.txt") != std::string::npos && - (c.type == fs_event::effect::create || c.type == fs_event::effect::rename); - }); - - watcher->stop(); - co_await fs::unlink(f4, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return saw_final ? 1 : 0; -} - -task watch_special_char_filename(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "file with spaces & (parens).txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = std::ranges::any_of(changes, [](const auto& c) { - return c.type == fs_event::effect::create && c.path.find("spaces") != std::string::npos; - }); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -task watch_rapid_stop_create_cycle(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-dw-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - for(int i = 0; i < 5; ++i) { - auto watcher = - fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - watcher->stop(); - } - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "final.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - bool found = has_effect(changes, fs_event::effect::create); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return found ? 1 : 0; -} - -} // namespace - -TEST_SUITE(fs_event_dir_io, loop_fixture) { - -TEST_CASE(detect_file_creation) { - auto worker = watch_file_create(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(detect_file_modification) { - auto worker = watch_file_modify(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(detect_file_deletion) { - auto worker = watch_file_delete(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(detect_file_rename) { - auto worker = watch_file_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(error_on_nonexistent_path) { - auto worker = watch_nonexistent_path(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(close_then_next_returns_error) { - auto worker = watch_close_then_next(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(stop_during_next_returns_aborted) { - auto worker = watch_stop_during_next(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(stop_during_debounce_returns) { - auto worker = watch_stop_during_debounce(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(multiple_next_calls) { - auto worker = watch_multiple_next_calls(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(debounce_batches_events) { - auto worker = watch_debounce_batching(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(recursive_subdirectory_events) { - auto worker = watch_subdirectory_changes(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(non_recursive_watches_top_level) { - auto worker = watch_non_recursive(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(move_assignment_closes_old_watcher) { - auto worker = watch_move_assignment(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(destructor_cleans_up_without_close) { - auto worker = watch_destructor_cleanup(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(double_close_is_safe) { - auto worker = watch_double_close(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(create_with_default_options) { - auto worker = watch_default_options(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rename_populates_old_path) { - auto worker = watch_rename_populates_old_path(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rename_existing_file) { - auto worker = watch_rename_existing_file(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(directory_creation) { - auto worker = watch_directory_creation(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(directory_rename) { - auto worker = watch_directory_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(directory_deletion) { - auto worker = watch_directory_deletion(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subfile_create) { - auto worker = watch_subfile_create(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subfile_modify) { - auto worker = watch_subfile_modify(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subfile_rename) { - auto worker = watch_subfile_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subfile_delete) { - auto worker = watch_subfile_delete(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(nested_subdir_create) { - auto worker = watch_nested_subdir_create(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(deep_nested_file) { - auto worker = watch_deep_nested_file(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subdir_rename) { - auto worker = watch_subdir_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(renamed_dir_still_tracked) { - auto worker = watch_renamed_dir_still_tracked(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(error_on_bad_parent) { - auto worker = watch_error_on_bad_parent(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(multiple_watchers_same_dir) { - auto worker = watch_multiple_watchers_same_dir(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(multiple_watchers_different_dirs) { - auto worker = watch_multiple_watchers_different_dirs(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rapid_create_delete) { - auto worker = watch_rapid_create_delete(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rapid_multiple_writes) { - auto worker = watch_rapid_multiple_writes(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(attribute_change) { - auto worker = watch_attribute_change(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(atomic_replace) { - auto worker = watch_atomic_replace(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(toctou_dir_and_file) { - auto worker = watch_toctou_dir_and_file(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(unicode_filename) { - auto worker = watch_unicode_filename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(symlink_create_delete) { -#if defined(_WIN32) - kota::zest::skip(); -#else - auto worker = watch_symlink_create_delete(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -#endif -} - -TEST_CASE(large_burst) { - auto worker = watch_large_burst(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(debounce_coalesces) { - auto worker = watch_debounce_coalesces(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(root_dir_deleted) { - auto worker = watch_root_dir_deleted(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(subdir_delete_with_files) { - auto worker = watch_subdir_delete_with_files(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(symlink_rename) { -#if defined(_WIN32) - kota::zest::skip(); -#else - auto worker = watch_symlink_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -#endif -} - -TEST_CASE(symlink_update) { -#if defined(_WIN32) - kota::zest::skip(); -#else - auto worker = watch_symlink_update(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -#endif -} - -TEST_CASE(folder_symlink) { -#if defined(_WIN32) - kota::zest::skip(); -#else - auto worker = watch_folder_symlink(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -#endif -} - -TEST_CASE(rapid_create_update) { - auto worker = watch_rapid_create_update(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rapid_update_delete) { - auto worker = watch_rapid_update_delete(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rapid_delete_create) { - auto worker = watch_rapid_delete_create(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(case_only_rename) { - auto worker = watch_case_only_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(nested_dir_rename) { - auto worker = watch_nested_dir_rename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(create_rename_coalesce) { - auto worker = watch_create_rename_coalesce(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(chain_rename_coalesce) { - auto worker = watch_chain_rename_coalesce(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(special_char_filename) { - auto worker = watch_special_char_filename(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(rapid_stop_create_cycle) { - auto worker = watch_rapid_stop_create_cycle(loop); - schedule_all(worker); - - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -}; // TEST_SUITE(fs_event_dir_io) - -} // namespace kota diff --git a/tests/unit/async/io/fs_event_tests.cpp b/tests/unit/async/io/fs_event_tests.cpp deleted file mode 100644 index eb1505bb..00000000 --- a/tests/unit/async/io/fs_event_tests.cpp +++ /dev/null @@ -1,525 +0,0 @@ -#include -#include -#include -#include - -#include "../fs_event_fixture.h" -#include "../loop_fixture.h" -#include "kota/zest/zest.h" - -namespace kota { - -namespace { - -task fse_detect_modify(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "target.json").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("initial", 7), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - co_await fs::write(fd, std::span("updated", 7), 0, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return has_effect(changes, fs_event::effect::modify) ? 1 : 0; -} - -task fse_detect_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "new_file.json").string(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("hello", 5), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return has_effect(changes, fs_event::effect::create) ? 1 : 0; -} - -task fse_detect_destroy(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "doomed.json").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - co_await fs::unlink(file, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return has_effect(changes, fs_event::effect::destroy) ? 1 : 0; -} - -task fse_ignores_sibling(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string target = (std::filesystem::path(dir) / "target.json").string(); - std::string sibling = (std::filesystem::path(dir) / "sibling.txt").string(); - - int fd = co_await fs::open(target, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(target, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(sibling, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("noise", 5), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - co_await sleep(200, loop); - - fd = co_await fs::open(target, O_WRONLY, 0, loop).or_fail(); - co_await fs::write(fd, std::span("signal", 6), 0, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(target, loop).or_fail(); - co_await fs::unlink(sibling, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - if(!has_effect(changes, fs_event::effect::modify)) - co_return 0; - - if(std::ranges::any_of(changes, [](const auto& c) { - return c.path.find("sibling") != std::string::npos; - })) { - co_return 0; - } - - co_return 1; -} - -task fse_atomic_replace(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "config.json").string(); - std::string tmp = (std::filesystem::path(dir) / "config.json.tmp").string(); - - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("v1", 2), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("v2", 2), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - co_await fs::rename(tmp, file, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return (has_effect(changes, fs_event::effect::create) || - has_effect(changes, fs_event::effect::rename)) - ? 1 - : 0; -} - -task fse_error_bad_parent(event_loop& loop) { - auto result = fs_event::create("/nonexistent/path/file.txt", {}, loop); - co_return result.has_error() ? 1 : 0; -} - -task fse_stop_then_next(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "test.json").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - watcher->stop(); - auto result = co_await watcher->next(); - - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - if(!result.has_error()) - co_return 0; - co_return result.error() == error::invalid_argument ? 1 : 0; -} - -task fse_error_empty_path(event_loop& loop) { - auto result = fs_event::create("", {}, loop); - co_return result.has_error() ? 1 : 0; -} - -task fse_default_constructed([[maybe_unused]] event_loop& loop) { - fs_event watcher; - watcher.stop(); - auto result = co_await watcher.next(); - co_return result.has_error() && result.error() == error::invalid_argument ? 1 : 0; -} - -task fse_two_arg_create(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "test.json").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(file, O_WRONLY, 0, loop).or_fail(); - co_await fs::write(fd, std::span("changed", 7), 0, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return has_effect(changes, fs_event::effect::modify) ? 1 : 0; -} - -task fse_stop_during_next(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string file = (std::filesystem::path(dir) / "test.json").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(file, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - auto stopper = [&]() -> task { - co_await sleep(100, loop); - watcher->stop(); - }; - auto stop_task = stopper(); - loop.schedule(stop_task); - - auto result = co_await next_or_timeout(*watcher, loop, 5000); - - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - if(result.has_error()) { - co_return result.error() == error::operation_aborted ? 1 : 0; - } - co_return 1; -} - -task fse_file_filter_rejects_similar_name(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string target = (std::filesystem::path(dir) / "config.json").string(); - std::string similar = (std::filesystem::path(dir) / "config.json.bak").string(); - - int fd = co_await fs::open(target, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(target, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(similar, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("backup", 6), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - co_await sleep(200, loop); - - fd = co_await fs::open(target, O_WRONLY, 0, loop).or_fail(); - co_await fs::write(fd, std::span("signal", 6), 0, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(target, loop).or_fail(); - co_await fs::unlink(similar, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - if(!has_effect(changes, fs_event::effect::modify)) - co_return 0; - - bool leaked_similar = std::ranges::any_of(changes, [](const auto& c) { - return c.path.find(".bak") != std::string::npos; - }); - co_return leaked_similar ? 0 : 1; -} - -#if !defined(__APPLE__) -task fse_file_filter_only_sibling_no_event(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - std::string target = (std::filesystem::path(dir) / "watched.json").string(); - std::string sibling = (std::filesystem::path(dir) / "other.txt").string(); - - int fd = co_await fs::open(target, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto watcher = fs_event::create(target, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - fd = co_await fs::open(sibling, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::write(fd, std::span("noise", 5), -1, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto stopper = [&]() -> task { - co_await sleep(1000, loop); - watcher->stop(); - }; - auto stop_task = stopper(); - loop.schedule(stop_task); - - auto result = co_await watcher->next(); - - co_await fs::unlink(target, loop).or_fail(); - co_await fs::unlink(sibling, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return result.has_error() && result.error() == error::operation_aborted ? 1 : 0; -} -#endif - -task fse_file_filter_dir_mode_no_filter(event_loop& loop) { - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - std::string dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - - auto watcher = fs_event::create(dir, fs_event::options{std::chrono::milliseconds{50}}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - co_await wait_for_watcher_ready(loop); - - std::string file = (std::filesystem::path(dir) / "any_file.txt").string(); - int fd = co_await fs::open(file, O_CREAT | O_WRONLY | O_TRUNC, 0644, loop).or_fail(); - co_await fs::close(fd, loop).or_fail(); - - auto changes = co_await next_or_timeout(*watcher, loop).or_fail(); - - watcher->stop(); - co_await fs::unlink(file, loop).or_fail(); - co_await fs::rmdir(dir, loop).or_fail(); - - co_return has_effect(changes, fs_event::effect::create) ? 1 : 0; -} - -} // namespace - -TEST_SUITE(fs_event_io, loop_fixture) { - -TEST_CASE(detect_modify) { - auto worker = fse_detect_modify(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(detect_create) { - auto worker = fse_detect_create(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(detect_destroy) { - auto worker = fse_detect_destroy(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(ignores_sibling) { - auto worker = fse_ignores_sibling(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(atomic_replace) { - auto worker = fse_atomic_replace(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(error_bad_parent) { - auto worker = fse_error_bad_parent(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(stop_then_next) { - auto worker = fse_stop_then_next(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(error_empty_path) { - auto worker = fse_error_empty_path(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(default_constructed) { - auto worker = fse_default_constructed(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(two_arg_create) { - auto worker = fse_two_arg_create(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(stop_during_next) { - auto worker = fse_stop_during_next(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(file_filter_rejects_similar_name) { - auto worker = fse_file_filter_rejects_similar_name(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(file_filter_only_sibling_no_event) { -#if defined(__APPLE__) - kota::zest::skip(); -#else - auto worker = fse_file_filter_only_sibling_no_event(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -#endif -} - -TEST_CASE(file_filter_dir_mode_no_filter) { - auto worker = fse_file_filter_dir_mode_no_filter(loop); - schedule_all(worker); - auto result = worker.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -TEST_CASE(watcher_keeps_loop_alive) { - // A live watcher should keep the loop alive via its relay. - // After stop(), the loop hold is released and run() returns. - auto dir_template = (std::filesystem::temp_directory_path() / "kotatsu-fe-XXXXXX").string(); - - auto t = [&]() -> task { - auto dir = co_await fs::mkdtemp(dir_template, loop).or_fail(); - auto watcher = fs_event::create(dir, fs_event::options{}, loop); - if(!watcher.has_value()) - co_await fail(watcher.error()); - - // Schedule a delayed stop; loop.run() must NOT exit before it fires. - auto stopper = [&]() -> task { - co_await sleep(200, loop); - watcher->stop(); - }; - auto stop_task = stopper(); - loop.schedule(stop_task); - - co_await sleep(300, loop); - co_await fs::rmdir(dir, loop).or_fail(); - co_return 1; - }; - - auto task = t(); - schedule_all(task); - auto result = task.result(); - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(*result, 1); -} - -}; // TEST_SUITE(fs_event_io) - -} // namespace kota diff --git a/xmake.lua b/xmake.lua index 5bc2b35a..48d0235a 100644 --- a/xmake.lua +++ b/xmake.lua @@ -208,9 +208,6 @@ if has_config("async") then add_headerfiles("include/(kota/async/**)") add_deps("support") add_packages("libuv", { public = true }) - if is_plat("macosx") then - add_frameworks("CoreServices") - end end) end From 48ab56575d422962ed578c2ec3dfebb2b46731ac Mon Sep 17 00:00:00 2001 From: ykiko Date: Sun, 5 Jul 2026 20:31:24 +0800 Subject: [PATCH 2/2] style: format README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 79271340..0e1e5c58 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ All public APIs live under the `kota::` namespace, public headers under `include > existing primitives: a `kota::timer` firing at your desired interval combined with > `fs::stat` (comparing mtime / size) gives predictable, portable "did this path > change?" semantics without the platform-specific pitfalls. + - Blocking-work offload via `queue(fn, loop)` onto the libuv thread pool. - Coroutine-friendly sync primitives: mutex, semaphore, event (with interrupt), and condition variable. - Error vocabulary: `error` (libuv status wrapper with named codes), `result`, and the general `outcome`.