use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct UvNotFound;
impl std::fmt::Display for UvNotFound {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "pyuv (libuv bindings) not available")
}
}
impl std::error::Error for UvNotFound {}
pub fn import_pyuv() -> Result<(), UvNotFound> {
Err(UvNotFound)
}
pub struct UvFileWatcher;
impl UvFileWatcher {
pub fn new() -> Result<Self, UvNotFound> {
import_pyuv()?;
Ok(Self)
}
}
pub struct UvTreeWatcher;
impl UvTreeWatcher {
pub fn new<P: AsRef<std::path::Path>>(_path: P) -> Result<Self, UvNotFound> {
import_pyuv()?;
Ok(Self)
}
}
pub fn _uv_thread() -> &'static Mutex<Option<bool>> {
static M: OnceLock<Mutex<Option<bool>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(None))
}
pub fn start_uv_thread() -> Result<(), UvNotFound> {
Err(UvNotFound)
}
pub fn normpath(path: &str) -> String {
std::fs::canonicalize(path)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| path.to_string())
}
pub struct UvWatcher {
pub watches: Mutex<HashSet<String>>,
}
impl Default for UvWatcher {
fn default() -> Self {
Self::new()
}
}
impl UvWatcher {
pub fn new() -> Self {
Self {
watches: Mutex::new(HashSet::new()),
}
}
pub fn _start_watch_1_x(&self, path: &str) -> bool {
let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
watches.insert(path.to_string())
}
pub fn _start_watch_0_x(&self, path: &str) -> bool {
let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
watches.insert(path.to_string())
}
pub fn watch(&self, path: &str) {
let normalized = normpath(path);
let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
watches.insert(normalized);
}
pub fn unwatch(&self, path: &str) {
let normalized = normpath(path);
let mut watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
watches.remove(&normalized);
}
pub fn is_watching(&self, path: &str) -> bool {
let normalized = normpath(path);
let watches = self.watches.lock().unwrap_or_else(|e| e.into_inner());
watches.contains(&normalized)
}
pub fn watch_count(&self) -> usize {
self.watches.lock().unwrap_or_else(|e| e.into_inner()).len()
}
}
pub struct UvFileWatcherEvents {
pub events: Mutex<HashMap<String, Vec<u32>>>,
}
impl Default for UvFileWatcherEvents {
fn default() -> Self {
Self::new()
}
}
impl UvFileWatcherEvents {
pub fn new() -> Self {
Self {
events: Mutex::new(HashMap::new()),
}
}
pub fn _stopped_watching(&self, path: &str) {
let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
events.remove(path);
}
pub fn _record_event(&self, path: &str, events_mask: u32) {
self.record_event(path, events_mask)
}
pub fn record_event(&self, path: &str, events_mask: u32) {
let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
events
.entry(path.to_string())
.or_default()
.push(events_mask);
}
pub fn check(&self, path: &str) -> bool {
let normalized = normpath(path);
let mut events = self.events.lock().unwrap_or_else(|e| e.into_inner());
let queued = events.remove(&normalized);
queued.map(|v| !v.is_empty()).unwrap_or(false)
}
pub fn event_count(&self, path: &str) -> usize {
let normalized = normpath(path);
let events = self.events.lock().unwrap_or_else(|e| e.into_inner());
events.get(&normalized).map(|v| v.len()).unwrap_or(0)
}
}
pub struct UvTreeWatcherEvents {
pub basedir: String,
pub modified: Mutex<bool>,
pub ignored_events: Mutex<Vec<(String, String)>>,
pub watcher: UvWatcher,
}
impl UvTreeWatcherEvents {
pub const IS_DUMMY: bool = false;
pub fn new(basedir: impl Into<String>) -> Self {
Self {
basedir: normpath(&basedir.into()),
modified: Mutex::new(true),
ignored_events: Mutex::new(Vec::new()),
watcher: UvWatcher::new(),
}
}
pub fn watch_directory<I>(&self, directories: I)
where
I: IntoIterator<Item = String>,
{
for dir in directories {
self.watch_one_directory(&dir);
}
}
pub fn watch_one_directory(&self, dirname: &str) {
self.watcher.watch(dirname);
}
pub fn _stopped_watching(&self, path: &str) {
let mut watches = self
.watcher
.watches
.lock()
.unwrap_or_else(|e| e.into_inner());
watches.remove(path);
}
pub fn _record_event(&self, path: &str, name: &str, events_mask: u32) -> bool {
self.record_event(path, name, events_mask)
}
pub fn record_event(&self, path: &str, name: &str, events_mask: u32) -> bool {
let ignored = self
.ignored_events
.lock()
.unwrap_or_else(|e| e.into_inner());
for (p, n) in ignored.iter() {
if p == path && n == name {
return *self.modified.lock().unwrap_or_else(|e| e.into_inner());
}
}
drop(ignored);
let _ = events_mask;
let mut m = self.modified.lock().unwrap_or_else(|e| e.into_inner());
*m = true;
true
}
pub fn check(&self) -> bool {
let mut m = self.modified.lock().unwrap_or_else(|e| e.into_inner());
let prev = *m;
*m = false;
prev
}
pub fn ignore(&self, path: impl Into<String>, name: impl Into<String>) {
let mut ignored = self
.ignored_events
.lock()
.unwrap_or_else(|e| e.into_inner());
ignored.push((path.into(), name.into()));
}
}
pub struct UvThread {
pub daemon: bool,
pub joined: Mutex<bool>,
}
impl Default for UvThread {
fn default() -> Self {
Self::new()
}
}
impl UvThread {
pub fn new() -> Self {
Self {
daemon: true,
joined: Mutex::new(false),
}
}
pub fn _async_cb(&self) {
let mut joined = self.joined.lock().unwrap_or_else(|e| e.into_inner());
*joined = true;
}
pub fn run(&self) {
}
pub fn join(&self) {
let mut j = self.joined.lock().unwrap_or_else(|e| e.into_inner());
*j = true;
}
pub fn is_joined(&self) -> bool {
*self.joined.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
mod tests_state {
use super::*;
#[test]
fn uv_watcher_new_starts_empty() {
let w = UvWatcher::new();
assert_eq!(w.watch_count(), 0);
}
#[test]
fn uv_watcher_watch_inserts_path() {
let w = UvWatcher::new();
w.watch("/tmp/foo");
assert!(w.is_watching("/tmp/foo"));
}
#[test]
fn uv_watcher_watch_idempotent() {
let w = UvWatcher::new();
w.watch("/tmp/foo");
w.watch("/tmp/foo");
assert_eq!(w.watch_count(), 1);
}
#[test]
fn uv_watcher_unwatch_removes_path() {
let w = UvWatcher::new();
w.watch("/tmp/foo");
w.unwatch("/tmp/foo");
assert!(!w.is_watching("/tmp/foo"));
assert_eq!(w.watch_count(), 0);
}
#[test]
fn uv_watcher_unwatch_unknown_no_op() {
let w = UvWatcher::new();
w.unwatch("/tmp/never");
assert_eq!(w.watch_count(), 0);
}
#[test]
fn uv_watcher_supports_multiple_paths() {
let w = UvWatcher::new();
w.watch("/tmp/a");
w.watch("/tmp/b");
w.watch("/tmp/c");
assert_eq!(w.watch_count(), 3);
w.unwatch("/tmp/b");
assert_eq!(w.watch_count(), 2);
assert!(w.is_watching("/tmp/a"));
assert!(!w.is_watching("/tmp/b"));
assert!(w.is_watching("/tmp/c"));
}
#[test]
fn start_uv_thread_returns_uv_not_found() {
let r = start_uv_thread();
assert!(r.is_err());
}
#[test]
fn normpath_returns_string_for_non_existing() {
let r = normpath("/__never_existing_path_12345");
assert_eq!(r, "/__never_existing_path_12345");
}
#[test]
fn uv_file_watcher_events_new_starts_empty() {
let e = UvFileWatcherEvents::new();
assert_eq!(e.event_count("/x"), 0);
}
#[test]
fn uv_file_watcher_events_record_appends() {
let e = UvFileWatcherEvents::new();
e.record_event("/x", 1);
e.record_event("/x", 2);
assert_eq!(e.event_count("/x"), 2);
}
#[test]
fn uv_file_watcher_events_check_returns_true_when_events_present() {
let e = UvFileWatcherEvents::new();
e.record_event("/__nonexistent_test_path_uv", 1);
assert!(e.check("/__nonexistent_test_path_uv"));
}
#[test]
fn uv_file_watcher_events_check_consumes_queue() {
let e = UvFileWatcherEvents::new();
e.record_event("/__nonexistent_test_path2_uv", 1);
e.check("/__nonexistent_test_path2_uv");
assert_eq!(e.event_count("/__nonexistent_test_path2_uv"), 0);
}
#[test]
fn uv_file_watcher_events_check_false_when_empty() {
let e = UvFileWatcherEvents::new();
assert!(!e.check("/never"));
}
#[test]
fn _uv_thread_starts_as_none() {
let m = _uv_thread().lock().unwrap_or_else(|e| e.into_inner());
let _ = *m;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uv_not_found_implements_error_traits() {
let e = UvNotFound;
assert!(e.to_string().contains("pyuv"));
let _: &dyn std::error::Error = &e;
}
#[test]
fn import_pyuv_returns_uv_not_found_in_stub() {
assert!(import_pyuv().is_err());
}
#[test]
fn uv_file_watcher_new_errors() {
assert!(UvFileWatcher::new().is_err());
}
#[test]
fn uv_tree_watcher_new_errors() {
assert!(UvTreeWatcher::new("/tmp").is_err());
}
#[test]
fn uv_tree_watcher_events_initial_state_is_modified() {
let w = UvTreeWatcherEvents::new("/tmp");
let _ = &w.basedir;
assert!(*w.modified.lock().unwrap());
}
#[test]
fn uv_tree_watcher_events_check_pops_modified_flag() {
let w = UvTreeWatcherEvents::new("/tmp");
assert!(w.check());
assert!(!w.check());
}
#[test]
fn uv_tree_watcher_events_record_event_sets_modified() {
let w = UvTreeWatcherEvents::new("/tmp");
let _ = w.check();
assert!(!w.check());
let _ = w.record_event("/tmp/file", "x.txt", 1);
assert!(w.check());
}
#[test]
fn uv_tree_watcher_events_ignored_event_does_not_set_modified() {
let w = UvTreeWatcherEvents::new("/tmp");
let _ = w.check();
w.ignore("/tmp/file", "x.txt");
let _ = w.record_event("/tmp/file", "x.txt", 1);
assert!(!w.check());
}
#[test]
fn uv_tree_watcher_events_is_dummy_false() {
const _: () = assert!(!UvTreeWatcherEvents::IS_DUMMY);
}
#[test]
fn uv_tree_watcher_events_watch_directory_walks_supplied_list() {
let w = UvTreeWatcherEvents::new("/tmp");
w.watch_directory(vec!["/tmp/a".to_string(), "/tmp/b".to_string()]);
assert!(w.watcher.watch_count() >= 2);
}
#[test]
fn uv_thread_new_starts_unjoined() {
let t = UvThread::new();
assert!(t.daemon);
assert!(!t.is_joined());
}
#[test]
fn uv_thread_join_flips_state() {
let t = UvThread::new();
t.join();
assert!(t.is_joined());
}
#[test]
fn uv_thread_run_is_noop_without_panic() {
let t = UvThread::new();
t.run();
}
#[test]
fn uv_thread_async_cb_flips_joined() {
let t = UvThread::new();
assert!(!t.is_joined());
t._async_cb();
assert!(t.is_joined());
}
#[test]
fn uv_watcher_start_watch_1_x_inserts_path() {
let w = UvWatcher::new();
assert!(w._start_watch_1_x("/tmp/a"));
assert!(!w._start_watch_1_x("/tmp/a"));
}
#[test]
fn uv_watcher_start_watch_0_x_inserts_path() {
let w = UvWatcher::new();
assert!(w._start_watch_0_x("/tmp/b"));
assert!(!w._start_watch_0_x("/tmp/b"));
}
}