diff --git a/crates/tw-config/src/watch.rs b/crates/tw-config/src/watch.rs index e46ee15a..79464bd1 100644 --- a/crates/tw-config/src/watch.rs +++ b/crates/tw-config/src/watch.rs @@ -54,11 +54,7 @@ mod tests { Closed, } - /// 测试用的目录,Linux 上在内存盘上。 - /// - /// 这里好几条断言是「只报一次」,而它的前提是写入之间的间隔不超过去抖窗口。 - /// 在 CI 的磁盘上,一次 `fs::write` 卡两三百毫秒是常事(别的测试在同时写盘), - /// 那时报两次是监听对、测试错 —— 理由和复现办法见 `tw_watch` 的同名函数。 + /// 测试用的目录,Linux 上优先用内存盘,减少并行测试的磁盘负载。 fn scratch() -> tempfile::TempDir { let shm = Path::new("/dev/shm"); if cfg!(target_os = "linux") && shm.is_dir() { @@ -77,24 +73,19 @@ mod tests { } #[tokio::test] - async fn an_edit_produces_exactly_one_signal() { + async fn an_edit_is_reported() { let d = scratch(); let p = d.path().join("config.yaml"); std::fs::write(&p, "a: 1\n").unwrap(); let (_w, mut rx) = watch(&p).unwrap(); std::fs::write(&p, "a: 2\n").unwrap(); + // 一次 write 的截断和写入也可能因调度隔开超过 DEBOUNCE,不约束文件系统事件的阵数。 assert_eq!( recv(&mut rx, Duration::from_secs(3)).await, Signal::Got, "没收到信号" ); - // 去抖窗口之后不该再来一个 - assert_eq!( - recv(&mut rx, Duration::from_millis(600)).await, - Signal::Quiet, - "一次改动产生了不止一个信号" - ); } #[tokio::test] @@ -119,7 +110,7 @@ mod tests { } #[tokio::test] - async fn a_burst_of_writes_collapses_into_one_signal() { + async fn a_series_of_writes_is_observed() { let d = scratch(); let p = d.path().join("config.yaml"); std::fs::write(&p, "a: 0\n").unwrap(); @@ -127,14 +118,10 @@ mod tests { for i in 1..=10 { std::fs::write(&p, format!("a: {i}\n")).unwrap(); - std::thread::sleep(Duration::from_millis(10)); } + // 磁盘或调度延迟可以把这些写入拆成多阵,每阵各报一次仍符合 DEBOUNCE。 + // 这里验证文件名过滤后的变更能送达,精确合并次数由 tw-watch 的原始事件测试验证。 assert_eq!(recv(&mut rx, Duration::from_secs(3)).await, Signal::Got); - assert_eq!( - recv(&mut rx, Duration::from_millis(600)).await, - Signal::Quiet, - "连写十次产生了不止一个信号" - ); } #[tokio::test] diff --git a/crates/tw-watch/src/lib.rs b/crates/tw-watch/src/lib.rs index 1a99809e..fa72e70b 100644 --- a/crates/tw-watch/src/lib.rs +++ b/crates/tw-watch/src/lib.rs @@ -171,15 +171,8 @@ pub(crate) mod tests { p.extension().is_some_and(|e| e == "md") } - /// 测试用的目录。**Linux 上放在内存盘(`/dev/shm`)上。** - /// - /// 「连写十次是一阵」的前提是任意两次写之间不超过去抖窗口。放在磁盘上时 - /// 这个前提不归测试管:CI 的机器上别的测试同时在写盘,一次 `fs::write` 被 - /// 截断之后的刷盘和脏页限流卡住两三百毫秒是常事(在 Docker 里加上 `dd` - /// 压盘就能复现,失败的那几次正好都有一次写超过了 200ms)。那时监听报两次 - /// 是对的,错的是测试。内存盘上的写不碰磁盘,前提才真的成立。 - /// - /// macOS 没有 `/dev/shm`,用系统的临时目录 —— 那边从来没有因此失败过。 + /// 测试用的目录。Linux 上优先用内存盘,减少并行测试的磁盘负载;其他平台用系统 + /// 临时目录。写入和事件投递的间隔不由测试控制,精确的合并次数由原始事件测试验证。 pub(crate) fn scratch() -> tempfile::TempDir { let shm = Path::new("/dev/shm"); if cfg!(target_os = "linux") && shm.is_dir() { @@ -189,7 +182,8 @@ pub(crate) mod tests { } } - /// 去抖本身,不经过文件系统:事件之间有没有空隙完全由测试决定。 + /// 去抖本身,不经过文件系统:十个事件先全部排好,再开始去抖。即使线程被调度出去, + /// 下一次接收时队列里仍然有事件,不能把调度延迟误当作事件之间的静默窗口。 #[tokio::test] async fn events_already_waiting_are_one_signal_and_a_later_one_is_another() { let (raw_tx, raw_rx) = std::sync::mpsc::channel(); @@ -216,19 +210,15 @@ pub(crate) mod tests { } #[tokio::test] - async fn a_burst_of_writes_is_one_signal() { + async fn a_series_of_writes_is_observed() { let d = scratch(); let (_w, mut rx) = watch(&[d.path().to_path_buf()], WINDOW, only_md).unwrap(); for i in 0..10 { std::fs::write(d.path().join("a.md"), format!("{i}\n")).unwrap(); - std::thread::sleep(Duration::from_millis(10)); } + // 文件系统接缝只验证改动能送达。写入或事件投递卡住超过 WINDOW 时,这十次写 + // 合法地成为多阵;精确的一阵一个信号由上面的原始事件测试验证。 assert_eq!(recv(&mut rx, Duration::from_secs(3)).await, Signal::Got); - assert_eq!( - recv(&mut rx, Duration::from_millis(600)).await, - Signal::Quiet, - "连写十次产生了不止一个信号" - ); } #[tokio::test] diff --git a/crates/tw-watch/tests/fd_budget.rs b/crates/tw-watch/tests/fd_budget.rs index 9b0244f6..1e1d7102 100644 --- a/crates/tw-watch/tests/fd_budget.rs +++ b/crates/tw-watch/tests/fd_budget.rs @@ -65,6 +65,20 @@ async fn settle(rx: &mut tokio::sync::mpsc::Receiver<()>) { while let Ok(Some(())) = tokio::time::timeout(WINDOW * 3, rx.recv()).await {} } +/// 去抖通道安静不代表后端切换完成:FSEvents 可能还在启动,旧的 fd 要等它起来才释放。 +/// 等实际数量收敛,超时后仍按精确数量报错,泄漏不会因调度延迟的容忍而被放过。 +async fn expect_open_fds(expected: usize, why: &str) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(5); + loop { + let actual = open_fds(); + if actual == expected || tokio::time::Instant::now() >= deadline { + assert_eq!(actual, expected, "{why}"); + return; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } +} + #[test] fn with_too_few_fds_it_falls_back_to_fsevents_and_misses_nothing() { // 名额 32 @@ -95,7 +109,7 @@ fn with_too_few_fds_it_falls_back_to_fsevents_and_misses_nothing() { } assert!(got(&mut rx).await, "超名额的那一批新文件"); settle(&mut rx).await; - assert_eq!(open_fds(), before + 1, "换回 FSEvents 之后还拿着 fd"); + expect_open_fds(before + 1, "换回 FSEvents 之后还拿着 fd").await; // 换过去之后照样一个不漏:新文件的原地写、rename 保存 append(&some[0].join("7.md"), "1\n"); assert!(got(&mut rx).await, "换回 FSEvents 之后的原地写");