From 8b757b97bc3a8ca13ce45172fbe8cae20c6cf7c4 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 20 Sep 2026 01:23:14 +0800 Subject: [PATCH] feat(ci): migrate Rust test runners to nextest --- .github/scripts/ci-agent-integration.sh | 2 +- .github/scripts/ci-execution.test.mjs | 9 +++++- .github/scripts/ci-plan.test.mjs | 19 ++++++++++++ .github/scripts/ci-rust-coverage.mjs | 3 +- .github/scripts/ci-rust.mjs | 5 +-- .github/scripts/ci-workflow.test.mjs | 39 ++++++++++++++++++++++-- .github/workflows/ci.yml | 24 +++++++++++++-- .github/workflows/plugin-cli-release.yml | 10 +++++- .github/workflows/plugin-dev-host.yml | 6 +++- Makefile | 3 +- crates/ARCHITECTURE.md | 13 +++++--- packages/cli/package.json | 2 +- 12 files changed, 116 insertions(+), 19 deletions(-) diff --git a/.github/scripts/ci-agent-integration.sh b/.github/scripts/ci-agent-integration.sh index 7ff5ac2367..b0ab86575a 100644 --- a/.github/scripts/ci-agent-integration.sh +++ b/.github/scripts/ci-agent-integration.sh @@ -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" diff --git a/.github/scripts/ci-execution.test.mjs b/.github/scripts/ci-execution.test.mjs index b8d37a853e..bc503d6f1a 100644 --- a/.github/scripts/ci-execution.test.mjs +++ b/.github/scripts/ci-execution.test.mjs @@ -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"]); diff --git a/.github/scripts/ci-plan.test.mjs b/.github/scripts/ci-plan.test.mjs index 1fb15a9cd4..60edf33ab1 100644 --- a/.github/scripts/ci-plan.test.mjs +++ b/.github/scripts/ci-plan.test.mjs @@ -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) } }, diff --git a/.github/scripts/ci-rust-coverage.mjs b/.github/scripts/ci-rust-coverage.mjs index 149c1e7366..9ce356e499 100644 --- a/.github/scripts/ci-rust-coverage.mjs +++ b/.github/scripts/ci-rust-coverage.mjs @@ -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"] })); } diff --git a/.github/scripts/ci-rust.mjs b/.github/scripts/ci-rust.mjs index 9a190f79ca..abecfdd123 100644 --- a/.github/scripts/ci-rust.mjs +++ b/.github/scripts/ci-rust.mjs @@ -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"] : [])]; } diff --git a/.github/scripts/ci-workflow.test.mjs b/.github/scripts/ci-workflow.test.mjs index f30cc84268..0c9a331a14 100644 --- a/.github/scripts/ci-workflow.test.mjs +++ b/.github/scripts/ci-workflow.test.mjs @@ -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"')); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90b6912e29..0a4ec2b2c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/plugin-cli-release.yml b/.github/workflows/plugin-cli-release.yml index 1eb557f976..0b65d7aec1 100644 --- a/.github/workflows/plugin-cli-release.yml +++ b/.github/workflows/plugin-cli-release.yml @@ -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: | diff --git a/.github/workflows/plugin-dev-host.yml b/.github/workflows/plugin-dev-host.yml index b2ddec1cce..8b5d9b26bd 100644 --- a/.github/workflows/plugin-dev-host.yml +++ b/.github/workflows/plugin-dev-host.yml @@ -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: diff --git a/Makefile b/Makefile index 72bc57a872..9d9b541bcb 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/crates/ARCHITECTURE.md b/crates/ARCHITECTURE.md index af7ea536ba..4908b2d735 100644 --- a/crates/ARCHITECTURE.md +++ b/crates/ARCHITECTURE.md @@ -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 组合、前端类型与源码契约、 diff --git a/packages/cli/package.json b/packages/cli/package.json index db7e01cc17..97e17e6a83 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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" },