diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 000000000..b838adfd6 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,24 @@ +# CI 用 cargo-nextest 跑测试(`cargo test --workspace` 本地照常能用,这里的分组它不认)。 +# +# nextest 把每个测试放进独立进程、跨二进制并行。cargo test 是 140 个二进制**串行**、 +# 各自内部并行——多数二进制只有一两个测试,机器大部分时间在等一个测试跑完再起下一个进程; +# 换 nextest 后运行阶段本地 136s → 60s。代价是 cargo test 里隐含的两条隔离假设不再成立, +# 下面两条规则就是把它们显式写出来: +# +# 1. store 的集成测试之间**不能并行**。它们共用一个库,好几处断言的是全库状态—— +# 「只排了一个向量索引构建任务」、timeline 重算时的行锁——别的文件同时写就会多数或死锁。 +# cargo test 时它们各在自己的二进制里顺序跑,天然满足;这里用一个 max-threads = 1 的组复现。 +# 2. a_queued_job_wakes_an_idle_worker 的三个测试共用 utopia_jobs 通知频道,靠进程内的 +# 静态 Mutex 串行,而且要求库里没有别的进程在排任务。每个测试一个进程后 Mutex 失效, +# 别的测试入队的通知也会把它叫醒。threads-required = num-cpus 让它独占全部并发槽: +# 跑它时别的测试一个都不跑,比 Mutex 更强。 +[test-groups] +store-serial = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'binary(a_queued_job_wakes_an_idle_worker)' +threads-required = "num-cpus" + +[[profile.default.overrides]] +filter = 'package(utopia-store)' +test-group = 'store-serial' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87890571d..22ca1a09d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,11 +76,22 @@ jobs: run: cargo run -p utopia-store --example migrate env: UTOPIA_DATABASE_URL: postgres://utopia:utopia@localhost:5432/utopia + # 用 nextest 跑:cargo test 把 140 个测试二进制串行执行,运行阶段 86s;nextest 跨二进制 + # 并行,本地 136s → 60s。隔离规则在 .config/nextest.toml 里,那里解释了为什么 + # store 的测试要串行、哪一个要独占。装的是官方预编译二进制(几秒),用 cargo install + # 要编三四分钟;版本钉死,升级要有人看一眼;装过的话在 ~/.cargo/bin 缓存里,跳过 + - name: 装 nextest + run: | + command -v cargo-nextest && exit 0 + curl -LsSf https://get.nexte.st/0.9.146/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin" - name: Test - run: cargo test --workspace # 装了 Poppler 就必须真的用它测:没有它时 PDF 回退的那个测试会跳过, # 而一个静静跳过的测试等于没写(#248)。库同理:有库就**必须**连 - # (UTOPIA_TEST_REQUIRE_DB),连库测试没跑成要红,不能跳过显示绿色 + # (UTOPIA_TEST_REQUIRE_DB),连库测试没跑成要红,不能跳过显示绿色。 + # nextest 不跑 doctest,第二条补上——现在一个都没有,但有了不该悄悄不跑 + run: | + cargo nextest run --workspace + cargo test --workspace --doc env: UTOPIA_TEST_REQUIRE_PDFTOTEXT: "1" UTOPIA_DATABASE_URL: postgres://utopia:utopia@localhost:5432/utopia diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md index 36ca8ac78..4514a344b 100644 --- a/CONTRIBUTING.zh-CN.md +++ b/CONTRIBUTING.zh-CN.md @@ -79,10 +79,12 @@ let Ok(url) = std::env::var("UTOPIA_DATABASE_URL") else { ```bash export UTOPIA_DATABASE_URL=postgres://utopia:utopia@localhost:5432/utopia -cargo run -p utopia-store --example migrate # 空库先迁移;CI 的 backend job 也是这么做的 +cargo run -p utopia-store --example migrate # 空库先迁移;CI 的 test job 也是这么做的 cargo test --workspace ``` +CI 实际用的是 `cargo nextest run --workspace`(快一倍,跨二进制并行),隔离规则在 `.config/nextest.toml`。本地装了 nextest 的话用它更接近 CI;没装 `cargo test` 也一样能过。 + ## 几条会被 review 拦下来的 **迁移编号别撞。** `migrations/` 按序号前滚。开 PR 前看一眼 `main` 上最新的号 —— 两个分支各写一个 `0011_` 已经发生过一次,合并之后谁都跑不起来。 diff --git a/crates/utopia-server/src/api/mcp_tests.rs b/crates/utopia-server/src/api/mcp_tests.rs index 13e6316e3..e940803e7 100644 --- a/crates/utopia-server/src/api/mcp_tests.rs +++ b/crates/utopia-server/src/api/mcp_tests.rs @@ -193,8 +193,21 @@ impl Fixture { .await?; let dir = self.dir.clone(); drop(self); - std::fs::remove_dir_all(dir)?; - Ok(()) + // drop 掉的 tantivy IndexWriter 不等合并线程:它们还会往 search/ 里写几个段文件, + // 立刻删目录会撞 "Directory not empty"——nextest 下每个测试独占进程、时序更紧, + // 五次里能碰上一次。等它们写完再删,几十毫秒的事;其余 fixture 都是 `let _ =` + // 直接忽略,这里保留报错是为了目录真删不掉时看得见 + let mut attempt = 0; + loop { + match std::fs::remove_dir_all(&dir) { + Ok(()) => return Ok(()), + Err(_) if attempt < 50 => { + attempt += 1; + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + Err(e) => return Err(e.into()), + } + } } }