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
25 changes: 6 additions & 19 deletions crates/tw-config/src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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]
Expand All @@ -119,22 +110,18 @@ 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();
let (_w, mut rx) = watch(&p).unwrap();

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]
Expand Down
24 changes: 7 additions & 17 deletions crates/tw-watch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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();
Expand All @@ -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]
Expand Down
16 changes: 15 additions & 1 deletion crates/tw-watch/tests/fd_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 之后的原地写");
Expand Down
Loading