Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .zed/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,21 @@
"command": "cargo nextest run -p $ZED_CUSTOM_RUST_PACKAGE $ZED_STEM::$ZED_SYMBOL",
"tags": ["rust-mod-test"],
},
{
"label": "cargo watch nextest $ZED_STEM (package: $ZED_CUSTOM_RUST_PACKAGE)",
"command": "cargo watch -q -c -w $ZED_FILE -x 'nextest run -p $ZED_CUSTOM_RUST_PACKAGE $ZED_STEM::$ZED_SYMBOL'",
"tags": ["rust-mod-test"],
},
{
"label": "cargo nextest tests (crate root)",
"command": "cargo nextest run -E 'test(/^tests/)'",
"tags": ["rust-mod-test"],
},
{
"label": "cargo watch nextest tests (crate root)",
"command": "cargo watch -q -c -w $ZED_FILE",
"tags": ["rust-mod-test"],
},
{
"label": "cargo nextest run --workspace",
"command": "cargo nextest run --workspace",
Expand Down
4 changes: 2 additions & 2 deletions lsp/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};

use anyhow::Result;
Expand Down Expand Up @@ -59,7 +59,7 @@ impl Backend {
Ok(())
}

fn unwatch_path(&self, path: &PathBuf) -> Result<()> {
fn unwatch_path(&self, path: &Path) -> Result<()> {
let info_msg = format!("Unwatching path: {}", path.display());

#[allow(clippy::expect_used)]
Expand Down
3 changes: 2 additions & 1 deletion lsp/src/watching/path_store.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::path::Path;
use std::{path::PathBuf, pin::Pin, sync::Arc};

use anyhow::Result;
Expand Down Expand Up @@ -69,7 +70,7 @@ impl PathStore {
self.watched_set.watch(file_path)
}

pub fn unwatch(&mut self, file_path: &PathBuf) -> anyhow::Result<()> {
pub fn unwatch(&mut self, file_path: &Path) -> anyhow::Result<()> {
self.watched_set.unwatch(file_path)
}
}
Expand Down
38 changes: 36 additions & 2 deletions lsp/src/watching/path_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct PathWatcher {
event_handler: Option<DebugIgnore<EventHandler>>, // DebugIgnore because Fn traits can't implement Debug
}

#[cfg_attr(test, mockall::automock)]
impl PathWatcher {
pub fn new(event_handler: EventHandler) -> Result<Self> {
let (tx, rx) = channel(1);
Expand Down Expand Up @@ -68,7 +69,7 @@ impl PathWatcher {
});
}

pub fn watch<P: AsRef<Path>>(&self, path: P) -> Result<()> {
pub fn watch(&self, path: &Path) -> Result<()> {
self.watcher
.lock()
.map_err(|_| anyhow!("Path watcher mutex is poisoned"))?
Expand All @@ -77,7 +78,7 @@ impl PathWatcher {
Ok(())
}

pub fn unwatch<P: AsRef<Path>>(&self, path: P) -> Result<()> {
pub fn unwatch(&self, path: &Path) -> Result<()> {
self.watcher
.lock()
.map_err(|_| anyhow!("Path watcher mutex is poisoned"))?
Expand All @@ -86,3 +87,36 @@ impl PathWatcher {
Ok(())
}
}

mod tests {

/*
Tests TODO
- events handling
- test create file does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), maybe with an already existing file
- create a new file in that dir
- ensure event was not triggered (MockGithubClient)
- test delete file does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), with an already existing file
- delete the file
- ensure event was not triggered (MockGithubClient)
- test modify file data triggers event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), with an already existing file
- modify the file data
- ensure event was triggered (MockGithubClient)
- test modify file data outside of watched paths does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir)
- create another assert_fs::TempDir with an existing file
- modify that file data
- ensure event was not triggered (MockGithubClient)
*/
}
202 changes: 169 additions & 33 deletions lsp/src/watching/watched_set.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use std::{collections::HashSet, path::PathBuf, sync::Mutex};
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Mutex,
};

use anyhow::{Result, bail};
use mockall_double::double;

use crate::watching::{EventHandler, PathWatcher};
use crate::watching::EventHandler;
#[double]
use crate::watching::PathWatcher;

#[derive(Debug)]
pub struct WatchedSet {
Expand Down Expand Up @@ -44,7 +51,7 @@ impl WatchedSet {
Ok(())
}

pub fn unwatch(&mut self, path: &PathBuf) -> Result<()> {
pub fn unwatch(&mut self, path: &Path) -> Result<()> {
#[allow(clippy::expect_used)]
let _lock = self.mx.lock().expect("Watched set mutex is poisoned");

Expand All @@ -64,34 +71,163 @@ impl WatchedSet {

#[cfg(test)]
mod tests {
/*
Tests TODO

- events handling
- test create file does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), maybe with an already existing file
- create a new file in that dir
- ensure event was not triggered (MockGithubClient)
- test delete file does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), with an already existing file
- delete the file
- ensure event was not triggered (MockGithubClient)
- test modify file data triggers event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir), with an already existing file
- modify the file data
- ensure event was triggered (MockGithubClient)
- test modify file data outside of watched paths does not trigger event handler
- create a store with the MockGithubClient passed
- start watcher
- add a new path to watch (assert_fs::TempDir)
- create another assert_fs::TempDir with an existing file
- modify that file data
- ensure event was not triggered (MockGithubClient)
*/
#![allow(clippy::unwrap_used, clippy::expect_used)]

use std::{path::PathBuf, sync::OnceLock};

use anyhow::Result;
use notify::{Event, EventKind};
use tokio::runtime::Runtime;

use crate::watching::{EventHandler, MockPathWatcher, WatchedSet};

fn new_watched_set() -> Result<WatchedSet> {
WatchedSet::new(Box::new(|_ev: Event| Box::pin(async {})))
}

#[test]
fn test_new_successful() -> Result<()> {
static EVENT_HANDLER_CALLED: OnceLock<bool> = OnceLock::new();

let event_handler: EventHandler = Box::new(|_| {
Box::pin(async {
EVENT_HANDLER_CALLED
.set(true)
.expect("Flag was already set");
})
});

let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|event_handler| {
let rt = Runtime::new()?;
rt.block_on(async {
event_handler(Event::new(EventKind::Any)).await;
});
Ok(MockPathWatcher::default())
});

let set = WatchedSet::new(event_handler)?;
assert!(set.paths.is_empty());
assert!(EVENT_HANDLER_CALLED.get().expect("Flag was not set")); // testing that WatchedSet passes the event handler to PathWatcher

Ok(())
}

#[test]
fn test_start_watcher_successful() -> Result<()> {
let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|_| {
let mut mock_path_watcher = MockPathWatcher::default();
mock_path_watcher.expect_start().return_once(|| ());
Ok(mock_path_watcher)
});

let mut set = new_watched_set()?;
set.start_watcher();

Ok(())
}

#[test]
fn test_watch_successful() -> Result<()> {
let path = PathBuf::from("/hello/there");
let path_clone = path.clone();

let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|_| {
let mut mock_path_watcher = MockPathWatcher::default();
mock_path_watcher.expect_watch().return_once(move |path| {
assert_eq!(path, path_clone);
Ok(())
});

Ok(mock_path_watcher)
});

let mut set = new_watched_set()?;
set.watch(path.clone())?;
assert!(set.paths.contains(&path));

Ok(())
}

#[test]
fn test_watch_failure_if_already_watched() -> Result<()> {
let path = PathBuf::from("/hello/there");
let path_clone = path.clone();

let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|_| {
let mut mock_path_watcher = MockPathWatcher::default();
mock_path_watcher.expect_watch().return_once(move |path| {
assert_eq!(path, path_clone);
Ok(())
});

Ok(mock_path_watcher)
});

let mut set = new_watched_set()?;
set.watch(path.clone())?;
assert_eq!(
set.watch(path.clone()).unwrap_err().to_string(),
"Path is already being watched: /hello/there"
);

Ok(())
}

#[test]
fn test_unwatch_successful() -> Result<()> {
let path = PathBuf::from("/hello/there");
let path_clone_to_watch = path.clone();
let path_clone_to_unwatch = path.clone();

let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|_| {
let mut mock_path_watcher = MockPathWatcher::default();
mock_path_watcher.expect_watch().return_once(move |path| {
assert_eq!(path, path_clone_to_watch);
Ok(())
});
mock_path_watcher.expect_unwatch().return_once(move |path| {
assert_eq!(path, path_clone_to_unwatch);
Ok(())
});

Ok(mock_path_watcher)
});

let mut set = new_watched_set()?;
set.watch(path.clone())?;
set.unwatch(&path)?;
assert!(!set.paths.contains(&path));

Ok(())
}

#[test]
fn test_unwatch_failure_if_not_watched() -> Result<()> {
let path = PathBuf::from("/hello/there");
let path_clone_to_unwatch = path.clone();

let ctx = MockPathWatcher::new_context();
ctx.expect().return_once(|_| {
let mut mock_path_watcher = MockPathWatcher::default();
mock_path_watcher.expect_unwatch().return_once(move |path| {
assert_eq!(path, path_clone_to_unwatch);
Ok(())
});

Ok(mock_path_watcher)
});

let mut set = new_watched_set()?;
assert_eq!(
set.unwatch(&path).unwrap_err().to_string(),
"Path is not being watched, failed to unwatch: /hello/there"
);

Ok(())
}
}