Skip to content

Commit ccc95d7

Browse files
authored
test(native-watcher): init native watcher tests (#12185)
* test: init * chore: clippy * fix: fix * fix: use condvar
1 parent 33f4af2 commit ccc95d7

File tree

6 files changed

+449
-1
lines changed

6 files changed

+449
-1
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rspack_paths/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ impl DerefMut for ArcPath {
9090
}
9191
}
9292

93+
impl AsRef<Path> for ArcPath {
94+
fn as_ref(&self) -> &Path {
95+
&self.path
96+
}
97+
}
98+
9399
impl From<PathBuf> for ArcPath {
94100
fn from(value: PathBuf) -> Self {
95101
ArcPath::new(value.into())

crates/rspack_watcher/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] }
1919

2020
[target.'cfg(not(target_family = "wasm"))'.dependencies]
2121
tokio = { workspace = true, features = ["rt", "macros", "sync", "fs"] }
22+
23+
[dev-dependencies]
24+
tempfile = "3.23.0"

crates/rspack_watcher/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub trait EventHandler {
7272
}
7373

7474
/// `FsWatcherOptions` contains options for configuring the file system watcher.
75-
#[derive(Debug)]
75+
#[derive(Debug, Default)]
7676
pub struct FsWatcherOptions {
7777
/// Whether to follow symbolic links when watching files.
7878
pub follow_symlinks: bool,
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
#![allow(clippy::unwrap_used)]
2+
3+
use std::{
4+
mem::ManuallyDrop,
5+
path::PathBuf,
6+
sync::{
7+
Arc, Condvar, Mutex,
8+
mpsc::{Receiver, Sender},
9+
},
10+
time::SystemTime,
11+
};
12+
13+
use rspack_paths::{ArcPath, Utf8PathBuf};
14+
use rspack_util::fx_hash::FxHashSet;
15+
use rspack_watcher::{EventAggregateHandler, EventHandler, FsWatcher};
16+
use tempfile::TempDir;
17+
use tokio::sync::RwLock;
18+
19+
pub struct TestHelper {
20+
/// Temporary directory for testing
21+
temp_dir: ManuallyDrop<TempDir>,
22+
/// Canonicalized path of the temporary directory
23+
///
24+
/// on macOS, TempDir::path() returns a path with symlink (/var -> /private/var),
25+
/// which causes issues when matching paths. Therefore, we use the canonicalized path.
26+
canonicalized_temp_dir: PathBuf,
27+
/// File system watcher instance
28+
watcher: Arc<RwLock<FsWatcher>>,
29+
}
30+
31+
#[derive(Debug, Clone)]
32+
pub struct AggregatedEvent {
33+
pub changed_files: FxHashSet<String>,
34+
pub deleted_files: FxHashSet<String>,
35+
}
36+
37+
impl AggregatedEvent {
38+
pub fn assert_changed(&self, expected: impl AsRef<str>) {
39+
assert!(
40+
self
41+
.changed_files
42+
.iter()
43+
.any(|path| path == expected.as_ref()),
44+
"Expected changed files to contain a path of '{}', but got '{:?}'",
45+
expected.as_ref(),
46+
self.changed_files
47+
);
48+
}
49+
50+
pub fn assert_deleted(&self, expected: impl AsRef<str>) {
51+
assert!(
52+
self
53+
.deleted_files
54+
.iter()
55+
.any(|path| path == expected.as_ref()),
56+
"Expected deleted files to contain a path of '{}', but got '{:?}'",
57+
expected.as_ref(),
58+
self.deleted_files
59+
);
60+
}
61+
}
62+
63+
#[derive(Debug, Clone)]
64+
pub enum ChangedEvent {
65+
Changed(String),
66+
Deleted(String),
67+
}
68+
69+
impl ChangedEvent {
70+
pub fn assert_changed(&self, expected: impl AsRef<str>) {
71+
match self {
72+
ChangedEvent::Changed(path) => assert_eq!(
73+
path,
74+
expected.as_ref(),
75+
"Expected changed path to be '{}', but got '{}'",
76+
expected.as_ref(),
77+
path
78+
),
79+
ChangedEvent::Deleted(_) => panic!(
80+
"Expected changed event, but got deleted event for '{}'",
81+
expected.as_ref()
82+
),
83+
}
84+
}
85+
86+
pub fn assert_deleted(&self, expected: impl AsRef<str>) {
87+
match self {
88+
ChangedEvent::Deleted(path) => assert_eq!(
89+
path,
90+
expected.as_ref(),
91+
"Expected deleted path to be '{}', but got '{}'",
92+
expected.as_ref(),
93+
path
94+
),
95+
ChangedEvent::Changed(_) => panic!(
96+
"Expected deleted event, but got changed event for '{}'",
97+
expected.as_ref()
98+
),
99+
}
100+
}
101+
102+
pub fn assert_path(&self, expected: impl AsRef<str>) {
103+
match self {
104+
ChangedEvent::Changed(path) | ChangedEvent::Deleted(path) => assert_eq!(
105+
path,
106+
expected.as_ref(),
107+
"Expected path to be '{}', but got '{}'",
108+
expected.as_ref(),
109+
path
110+
),
111+
}
112+
}
113+
}
114+
115+
pub enum Event {
116+
Aggregated(AggregatedEvent),
117+
Changed(ChangedEvent),
118+
}
119+
120+
#[derive(Debug)]
121+
pub struct WatchResult {
122+
pub aggregated_events: Vec<AggregatedEvent>,
123+
pub changed_events: Vec<ChangedEvent>,
124+
}
125+
126+
static TOKIO_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
127+
std::sync::LazyLock::new(|| {
128+
tokio::runtime::Builder::new_multi_thread()
129+
.enable_all()
130+
.build()
131+
.expect("Failed to create Tokio runtime")
132+
});
133+
134+
impl TestHelper {
135+
/// Creates a new `TestHelper` instance
136+
pub fn new(watcher: impl FnOnce() -> FsWatcher) -> Self {
137+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
138+
let canonicalized_temp_dir = temp_dir.path().canonicalize().unwrap();
139+
let watcher = watcher();
140+
Self {
141+
temp_dir: ManuallyDrop::new(temp_dir),
142+
canonicalized_temp_dir,
143+
watcher: Arc::new(RwLock::new(watcher)),
144+
}
145+
}
146+
147+
pub fn join(&self, name: &str) -> Utf8PathBuf {
148+
Utf8PathBuf::from(self.canonicalized_temp_dir.join(name).to_str().unwrap())
149+
}
150+
151+
pub fn file(&self, name: &str) {
152+
let path = self.join(name);
153+
std::fs::write(
154+
path,
155+
format!(
156+
"{}",
157+
SystemTime::now()
158+
.duration_since(SystemTime::UNIX_EPOCH)
159+
.unwrap()
160+
.as_millis()
161+
),
162+
)
163+
.unwrap();
164+
}
165+
166+
pub fn collect_events(
167+
&self,
168+
rx: Receiver<Event>,
169+
mut on_changed: impl FnMut(&ChangedEvent, &mut bool),
170+
mut on_aggregated: impl FnMut(&AggregatedEvent, &mut bool),
171+
) {
172+
while let Ok(event) = rx.recv_timeout(std::time::Duration::from_secs(10)) {
173+
match event {
174+
Event::Aggregated(agg_event) => {
175+
let mut abort = false;
176+
on_aggregated(&agg_event, &mut abort);
177+
if abort {
178+
break;
179+
}
180+
}
181+
Event::Changed(chg_event) => {
182+
let mut abort = false;
183+
on_changed(&chg_event, &mut abort);
184+
if abort {
185+
break;
186+
}
187+
}
188+
}
189+
}
190+
}
191+
192+
pub fn tick(&self, f: impl FnOnce()) {
193+
std::thread::sleep(std::time::Duration::from_millis(200));
194+
f();
195+
}
196+
197+
/// Watches the specified files, directories, and missing paths.
198+
///
199+
/// All paths are relative to the temporary directory.
200+
pub fn watch(
201+
&mut self,
202+
files: (impl Iterator<Item = ArcPath>, impl Iterator<Item = ArcPath>),
203+
directories: (impl Iterator<Item = ArcPath>, impl Iterator<Item = ArcPath>),
204+
missing: (impl Iterator<Item = ArcPath>, impl Iterator<Item = ArcPath>),
205+
) -> Receiver<Event> {
206+
let (tx, rx) = std::sync::mpsc::channel();
207+
208+
#[derive(Default)]
209+
struct State(Arc<Mutex<bool>>, Condvar);
210+
211+
let state = Arc::new(State::default());
212+
213+
macro_rules! collect_paths {
214+
($expr:expr) => {{
215+
let left = $expr
216+
.0
217+
.map(|p| ArcPath::from(self.canonicalized_temp_dir.join(p)))
218+
.collect::<Vec<_>>();
219+
let right = $expr
220+
.1
221+
.map(|p| ArcPath::from(self.canonicalized_temp_dir.join(p)))
222+
.collect::<Vec<_>>();
223+
(left, right)
224+
}};
225+
}
226+
227+
// Collect and map relative paths to absolute paths
228+
let files = collect_paths!(files);
229+
let directories = collect_paths!(directories);
230+
let missing = collect_paths!(missing);
231+
232+
macro_rules! paths_to_iter {
233+
($paths:expr) => {{ ($paths.0.into_iter(), $paths.1.into_iter()) }};
234+
}
235+
236+
let watcher = self.watcher.clone();
237+
238+
std::thread::spawn({
239+
let state = state.clone();
240+
move || {
241+
let (ready, cvar) = (&state.0, &state.1);
242+
243+
let handle = TOKIO_RUNTIME.handle();
244+
handle.block_on(async {
245+
watcher
246+
.write()
247+
.await
248+
.watch(
249+
paths_to_iter!(files),
250+
paths_to_iter!(directories),
251+
paths_to_iter!(missing),
252+
SystemTime::now(),
253+
Box::new(AggregateHandler(tx.clone())),
254+
Box::new(ChangeHandler(tx)),
255+
)
256+
.await;
257+
258+
let mut started = ready.lock().unwrap();
259+
*started = true;
260+
cvar.notify_one();
261+
})
262+
}
263+
});
264+
265+
struct AggregateHandler(Sender<Event>);
266+
267+
impl EventAggregateHandler for AggregateHandler {
268+
fn on_event_handle(
269+
&self,
270+
changed_files: FxHashSet<String>,
271+
deleted_files: FxHashSet<String>,
272+
) {
273+
let _ = self.0.send(Event::Aggregated(AggregatedEvent {
274+
changed_files,
275+
deleted_files,
276+
}));
277+
}
278+
}
279+
280+
struct ChangeHandler(Sender<Event>);
281+
282+
impl EventHandler for ChangeHandler {
283+
fn on_change(&self, changed_file: String) -> rspack_error::Result<()> {
284+
let _ = self
285+
.0
286+
.send(Event::Changed(ChangedEvent::Changed(changed_file)));
287+
Ok(())
288+
}
289+
290+
fn on_delete(&self, deleted_file: String) -> rspack_error::Result<()> {
291+
let _ = self
292+
.0
293+
.send(Event::Changed(ChangedEvent::Deleted(deleted_file)));
294+
Ok(())
295+
}
296+
}
297+
298+
// Wait until the watcher is started
299+
let (ready, cvar) = (&state.0, &state.1);
300+
let mut started = ready.lock().unwrap();
301+
while !*started {
302+
started = cvar.wait(started).unwrap();
303+
}
304+
305+
rx
306+
}
307+
}
308+
309+
impl Drop for TestHelper {
310+
fn drop(&mut self) {
311+
TOKIO_RUNTIME.handle().block_on(async {
312+
let _ = self.watcher.write().await.close().await;
313+
});
314+
// SAFETY: ManuallyDrop is not used afterwards.
315+
let temp_dir = unsafe { ManuallyDrop::take(&mut self.temp_dir) };
316+
317+
match temp_dir.close() {
318+
Ok(_) => {}
319+
Err(e) => eprintln!("Failed to delete temp dir: {}", e),
320+
}
321+
}
322+
}

0 commit comments

Comments
 (0)