feat(ci): migrate Rust test runners to nextest

This commit is contained in:
t8y2
2026-09-20 01:23:15 +08:00
parent d16866f65b
commit 8b757b97bc
12 changed files with 116 additions and 19 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ EOF
TDENGINE_INTEGRATION=1 \
TDENGINE_TEST_HOST=127.0.0.1 \
TDENGINE_TEST_PORT=6041 \
cargo test --manifest-path drivers/tdengine/Cargo.toml --locked --test live -- --nocapture
cargo nextest run --manifest-path drivers/tdengine/Cargo.toml --locked --test live --no-capture --no-fail-fast
;;
cassandra)
cd "$root/agents/drivers/cassandra-go"
+8 -1
View File
@@ -85,7 +85,7 @@ for (const entry of integrationCases) {
assert.equal(tests[0].env.DBX_ZOOKEEPER_TEST_USERNAME, "dbx");
assert.ok(start.args.some((arg) => arg.includes("sessionRequireClientSASLAuth=true")));
} else if (entry.scenario === "tdengine") {
assert.deepEqual(tests[0].args, ["test", "--manifest-path", "drivers/tdengine/Cargo.toml", "--locked", "--test", "live", "--", "--nocapture"]);
assert.deepEqual(tests[0].args, ["nextest", "run", "--manifest-path", "drivers/tdengine/Cargo.toml", "--locked", "--test", "live", "--no-capture", "--no-fail-fast"]);
assert.equal(tests[0].env.TDENGINE_INTEGRATION, "1");
assert.equal(tests[0].env.TDENGINE_TEST_PORT, "6041");
} else if (entry.scenario === "cassandra") assert.ok(tests[0].args.includes("^TestCassandraIntegration$"));
@@ -104,6 +104,13 @@ test("failed live tests stay failed and still clean their container", () => with
assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
}, { CI_STUB_FAIL: "go" }));
test("failed nextest live tests propagate failure and still clean their container", () => withToolStubs((env, commands) => {
const result = spawnSync("/bin/bash", [".github/scripts/ci-agent-integration.sh", "tdengine", "3.4.2.2"], { cwd: root, env, encoding: "utf8" });
assert.equal(result.status, 23);
assert.deepEqual(commands().find((command) => command.tool === "cargo").args.slice(0, 2), ["nextest", "run"]);
assert.deepEqual(commands().at(-1).args.slice(0, 2), ["rm", "-fv"]);
}, { CI_STUB_FAIL: "cargo" }));
test("lock preflight resolves full graphs, keeps --locked and propagates failures", () => {
const plan = { rust: true, agent_rust: { include: [{ driver: "duckdb" }, { driver: "tdengine" }] } };
assert.deepEqual(lockfileManifests(plan), ["Cargo.toml", "agents/drivers/duckdb/Cargo.toml", "agents/drivers/tdengine/Cargo.toml"]);
+19
View File
@@ -139,6 +139,7 @@ test("test groups partition every current workspace package exactly once", () =>
test("workspace Cargo flags preserve full versus fast coverage and strict clippy", () => {
for (const mode of ["fast", "full"]) {
const command = rustCommand("test", "workspace", mode);
assert.deepEqual(command.slice(0, 3), ["nextest", "run", "--no-fail-fast"]);
const features = command.at(-1).split(",");
assert.ok(command.includes("--workspace"));
assert.ok(command.includes("--locked"));
@@ -147,12 +148,30 @@ test("workspace Cargo flags preserve full versus fast coverage and strict clippy
assert.equal(features.includes(`${pkg}/system-fonts`), mode === "full");
}
assert.deepEqual(rustCommand("clippy", "workspace", mode).slice(-3), ["--", "-D", "warnings"]);
assert.equal(rustCommand("clippy", "workspace", mode)[0], "clippy");
}
for (const group of Object.keys(rustGroups)) assert.ok(rustCommand("test", group, "fast").includes("--package"));
assert.throws(() => rustCommand("test", "unknown", "full"));
assert.throws(() => rustCommand("clippy", "foundation", "full"));
});
test("nextest, doctests and coverage select identical packages and features in every Rust lane", () => {
for (const group of ["workspace", ...Object.keys(rustGroups)]) {
for (const mode of ["fast", "full"]) {
const nextest = rustCommand("test", group, mode);
const doctest = rustCommand("doctest", group, mode);
assert.deepEqual(nextest.slice(0, 3), ["nextest", "run", "--no-fail-fast"]);
assert.deepEqual(doctest, ["test", "--doc", ...nextest.slice(3)]);
assert.deepEqual(rustCommand("tree", group, mode), ["tree", ...nextest.slice(3)]);
assert.ok(nextest.includes("--no-default-features"));
assert.ok(nextest.includes("--locked"));
}
}
assert.throws(() => rustCommand("doctest", "unknown", "full"));
assert.throws(() => rustCommand("doctest", "workspace", "unknown"));
assert.throws(() => rustCommand("unknown", "workspace", "full"));
});
function results(files) {
const selected = plan(files);
return { changes: { result: "success", outputs: { plan: JSON.stringify(selected) } },
+1 -2
View File
@@ -29,8 +29,7 @@ export function assertCoverage(coverage) {
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
const coverage = {};
for (const group of ["workspace", ...Object.keys(rustGroups)]) {
const args = rustCommand("test", group, "fast");
args[0] = "tree";
const args = rustCommand("tree", group, "fast");
args.push("--depth", "0", "--prefix", "none", "--format", "{p}|{f}");
coverage[group] = parseCoverage(execFileSync("cargo", args, { encoding: "utf8", maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "inherit"] }));
}
+3 -2
View File
@@ -4,7 +4,7 @@ import { pathToFileURL } from "node:url";
import { rustGroups } from "./ci-config.mjs";
export function rustCommand(action, group, mode) {
if (!["test", "clippy"].includes(action) || !["full", "fast"].includes(mode)
if (!["test", "doctest", "clippy", "tree"].includes(action) || !["full", "fast"].includes(mode)
|| (group !== "workspace" && !Object.hasOwn(rustGroups, group)) || (action === "clippy" && group !== "workspace")) {
throw new Error(`Unknown Rust CI configuration: ${action}/${group}/${mode}`);
}
@@ -18,7 +18,8 @@ export function rustCommand(action, group, mode) {
"dbx-sqlite-worker/runtime", "dbx-types/openapi", "dbx-sql/openapi",
] : appFeatures;
const packages = group === "workspace" ? ["--workspace"] : rustGroups[group].flatMap((name) => ["--package", name]);
return [action, ...packages, "--locked", ...(action === "clippy" ? ["--all-targets"] : []),
const command = action === "test" ? ["nextest", "run", "--no-fail-fast"] : action === "doctest" ? ["test", "--doc"] : [action];
return [...command, ...packages, "--locked", ...(action === "clippy" ? ["--all-targets"] : []),
"--no-default-features", "--features", features.join(","), ...(action === "clippy" ? ["--", "-D", "warnings"] : [])];
}
+36 -3
View File
@@ -3,8 +3,8 @@ import { readFileSync } from "node:fs";
import test from "node:test";
const workflow = readFileSync(new URL("../workflows/ci.yml", import.meta.url), "utf8");
function job(name) {
const jobs = workflow.slice(workflow.indexOf("\njobs:\n") + 7);
function job(name, content = workflow) {
const jobs = content.slice(content.indexOf("\njobs:\n") + 7);
const pattern = /^ ([\w-]+):\s*$/gm;
const definitions = [...jobs.matchAll(pattern)];
const index = definitions.findIndex((match) => match[1] === name);
@@ -62,7 +62,8 @@ test("every old Agent stage has an independent owner and Java packaging remains
assert.ok(job("agent-checks").includes("bump-agent-versions.test.mjs"));
assert.ok(job("agent-java").includes("./gradlew test shadowJar --continue"));
assert.ok(job("agent-java").includes("python3 scripts/validate_agent_jars.py"));
assert.ok(job("agent-rust").includes('cargo test --manifest-path "drivers/$DRIVER/Cargo.toml" --locked'));
assert.ok(job("agent-rust").includes('cargo nextest run --manifest-path "drivers/$DRIVER/Cargo.toml" --locked --no-fail-fast'));
assert.ok(job("agent-rust").includes('cargo test --doc --manifest-path "drivers/$DRIVER/Cargo.toml" --locked'));
assert.ok(job("agent-rust").includes("cargo build --manifest-path drivers/tdengine/Cargo.toml --locked --release --bin dbx-tdengine-driver"));
assert.ok(job("agent-go").includes("ci-agent-go.sh"));
assert.ok(job("agent-integration").includes("ci-agent-integration.sh"));
@@ -78,6 +79,38 @@ test("native Rust driver caches exclude failed build artifacts", () => {
assert.ok(content.includes("cache-on-failure: false"));
});
test("Rust test jobs install pinned nextest and retain separate doctests", () => {
const pluginDevHost = readFileSync(new URL("../workflows/plugin-dev-host.yml", import.meta.url), "utf8");
const pluginRelease = readFileSync(new URL("../workflows/plugin-cli-release.yml", import.meta.url), "utf8");
const consumers = [job("rust-test"), job("agent-rust"), job("agent-integration"),
job("test", pluginDevHost), job("prepare", pluginRelease)];
for (const content of consumers) {
assert.ok(content.includes("uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3"));
assert.ok(content.includes("tool: cargo-nextest@0.9.137"));
assert.doesNotMatch(content, /cargo test (?!.*--doc)/);
const testCommand = content.indexOf("cargo nextest run") >= 0 ? "cargo nextest run"
: content.includes("ci-rust.mjs test") ? "ci-rust.mjs test" : "ci-agent-integration.sh";
assert.ok(content.indexOf("tool: cargo-nextest@0.9.137") < content.indexOf(testCommand));
}
assert.ok(job("rust-test").includes('ci-rust.mjs doctest "$RUST_TEST_GROUP" "$RUST_FEATURE_MODE"'));
for (const content of [job("test", pluginDevHost), job("prepare", pluginRelease)]) {
assert.ok(content.includes("cargo nextest run --locked --manifest-path plugins/sdk/cli/Cargo.toml --no-fail-fast"));
assert.ok(content.includes("cargo test --doc --locked --manifest-path plugins/sdk/cli/Cargo.toml"));
}
assert.match(job("agent-integration"), /name: Install nextest\s+if: matrix\.driver == 'tdengine'/);
});
test("local Rust entrypoints and live TDengine tests use nextest", () => {
const makefile = readFileSync(new URL("../../Makefile", import.meta.url), "utf8");
assert.ok(makefile.includes("RUST_MIN_STACK=8388608 cargo nextest run --no-default-features --features sqlite-bundled --no-fail-fast"));
assert.ok(makefile.includes("RUST_MIN_STACK=8388608 cargo test --doc --no-default-features --features sqlite-bundled"));
const cli = JSON.parse(readFileSync(new URL("../../packages/cli/package.json", import.meta.url), "utf8"));
assert.equal(cli.scripts.test, "cargo nextest run -p dbx-cli --no-default-features --no-fail-fast");
const integration = readFileSync(new URL("./ci-agent-integration.sh", import.meta.url), "utf8");
assert.ok(integration.includes("cargo nextest run --manifest-path drivers/tdengine/Cargo.toml --locked --test live --no-capture --no-fail-fast"));
assert.doesNotMatch(integration, /cargo test/);
});
test("DuckDB Windows builds persist Rust and C++ compiler results", () => {
const content = job("duckdb-windows-driver");
assert.ok(content.includes('SCCACHE_GHA_ENABLED: "true"'));
+22 -2
View File
@@ -600,12 +600,21 @@ jobs:
# PR caches are large and branch-scoped; restore them from main without saving per-PR copies.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
with:
tool: cargo-nextest@0.9.137
# mold -run wraps the whole process tree (see the packages job note):
# test-binary linking dominates this job and sccache cannot cache links.
- name: Cargo test
- name: Nextest
run: |-
mold -run node .github/scripts/ci-rust.mjs test "$RUST_TEST_GROUP" "$RUST_FEATURE_MODE"
- name: Rust doc tests
run: |-
mold -run node .github/scripts/ci-rust.mjs doctest "$RUST_TEST_GROUP" "$RUST_FEATURE_MODE"
- name: Show sccache stats
if: always()
continue-on-error: true
@@ -1047,12 +1056,18 @@ jobs:
shared-key: ci-agent-rust-v2-${{ matrix.driver }}
cache-on-failure: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
with:
tool: cargo-nextest@0.9.137
# mold -run wraps the whole process tree (see the packages job note);
# the duckdb sidecar links a large bundled C++ static library.
- name: Native Rust driver tests
env:
DRIVER: ${{ matrix.driver }}
run: mold -run cargo test --manifest-path "drivers/$DRIVER/Cargo.toml" --locked
run: |
mold -run cargo nextest run --manifest-path "drivers/$DRIVER/Cargo.toml" --locked --no-fail-fast
mold -run cargo test --doc --manifest-path "drivers/$DRIVER/Cargo.toml" --locked
- name: TDengine native agent build
if: matrix.driver == 'tdengine'
run: mold -run cargo build --manifest-path drivers/tdengine/Cargo.toml --locked --release --bin dbx-tdengine-driver
@@ -1113,6 +1128,11 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@1.97.1
if: matrix.driver == 'tdengine'
- name: Install nextest
if: matrix.driver == 'tdengine'
uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
with:
tool: cargo-nextest@0.9.137
- name: TDengine Rust cache
if: matrix.driver == 'tdengine'
uses: swatinem/rust-cache@v2
+9 -1
View File
@@ -129,8 +129,16 @@ jobs:
exit 1
fi
- name: Install nextest
uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
with:
tool: cargo-nextest@0.9.137
- name: Test Rust CLI
run: cargo test --locked --manifest-path plugins/sdk/cli/Cargo.toml
run: cargo nextest run --locked --manifest-path plugins/sdk/cli/Cargo.toml --no-fail-fast
- name: Test Rust CLI docs
run: cargo test --doc --locked --manifest-path plugins/sdk/cli/Cargo.toml
- name: Prepare and test plugin development runtime
run: |
+5 -1
View File
@@ -33,7 +33,11 @@ jobs:
- run: npm run fmt:check --prefix plugins/sdk/dev-host
- run: npm test --prefix plugins/sdk/dev-host
timeout-minutes: 5
- run: cargo test --locked --manifest-path plugins/sdk/cli/Cargo.toml
- uses: taiki-e/install-action@9114bf4d891761788c546334fd37538eae1bf8b3
with:
tool: cargo-nextest@0.9.137
- run: cargo nextest run --locked --manifest-path plugins/sdk/cli/Cargo.toml --no-fail-fast
- run: cargo test --doc --locked --manifest-path plugins/sdk/cli/Cargo.toml
- run: npm test --prefix packages/plugin-cli
- run: node scripts/verify-plugin-cli-package.mjs
env:
+2 -1
View File
@@ -113,7 +113,8 @@ cargo-check-fast:
cargo check --no-default-features --features sqlite-bundled
cargo-test-fast:
RUST_MIN_STACK=8388608 cargo test --no-default-features --features sqlite-bundled
RUST_MIN_STACK=8388608 cargo nextest run --no-default-features --features sqlite-bundled --no-fail-fast
RUST_MIN_STACK=8388608 cargo test --doc --no-default-features --features sqlite-bundled
db-list:
@$(PNPM) db:env -- list
+9 -4
View File
@@ -100,6 +100,11 @@ core 保留原来的 default feature 集合,并向实际实现 crate 转发
Linux 的文件权限回归测试应由非 root 用户执行;root 可绕过目录写权限,会让测试
前提失效。测试进程的代理应排除 localhost,避免本地 HTTP 模拟器请求被转发。
Rust 单元与集成测试使用 `cargo nextest run`,CI 固定 nextest `0.9.137`。
首次使用执行 `cargo install cargo-nextest --locked --version 0.9.137`。
文档测试仍单独使用 `cargo test --doc`;`make cargo-test-fast` 会依次运行 nextest
和文档测试,并保留跳过 DuckDB、启用 bundled SQLite 的本地验证配置。
```sh
pnpm test:architecture
cargo check -p dbx-core --no-default-features --all-targets
@@ -107,14 +112,14 @@ cargo check -p dbx-core --no-default-features --all-targets
env -u DOCKER_CONTEXT \
DOCKER_HOST=unix:///tmp/dbx-disabled-docker.sock \
RUST_MIN_STACK=33554432 \
cargo test -p dbx-core -p dbx-drivers -p dbx-sql \
cargo nextest run -p dbx-core -p dbx-drivers -p dbx-sql \
-p dbx-types -p dbx-formats -p dbx-platform \
-p dbx-ai-provider -p dbx-plugin-runtime \
--no-default-features --features dbx-core/sqlite-bundled --lib
--no-default-features --features dbx-core/sqlite-bundled --lib --no-fail-fast
cargo test -p dbx-core --no-default-features --features sqlite-bundled \
cargo nextest run -p dbx-core --no-default-features --features sqlite-bundled \
--test public_api_compatibility --test connection_url_compatibility \
--test agent_recovery_contract
--test agent_recovery_contract --no-fail-fast
```
交付验证还应覆盖桌面/Web/CLI/MCP 消费者、合法 feature 组合、前端类型与源码契约、
+1 -1
View File
@@ -29,7 +29,7 @@
"type": "module",
"scripts": {
"start": "cargo run -p dbx-cli --no-default-features --",
"test": "cargo test -p dbx-cli --no-default-features",
"test": "cargo nextest run -p dbx-cli --no-default-features --no-fail-fast",
"build": "cargo build -p dbx-cli --release --no-default-features",
"prepublishOnly": "node --check bin/dbx.js"
},