mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
ci: guard test-unit clauses and the rust-cache workspace root
Fail just test-unit when any combined-filter clause selects no tests, so a renamed module or target cannot silently drop out of the union. Add a cargo-metadata check that no dependency manifest sits under the workspace root, run in Unit Tests before rust-cache can save, and select desktop jobs for bin/hermit.hcl changes. Refresh comments that described removed flags. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
This commit is contained in:
6
.github/workflows/_ci-rust.yml
vendored
6
.github/workflows/_ci-rust.yml
vendored
@@ -64,6 +64,12 @@ jobs:
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
with:
|
||||
tool: cargo-nextest@0.9.136
|
||||
# Every Hermit + rust-cache job resolves the same CARGO_HOME (Hermit
|
||||
# re-applies bin/hermit.hcl on each exec), so checking once here covers
|
||||
# them. Runs on PRs too, and on main fails before rust-cache's
|
||||
# success()-gated post-save can store an empty root-workspace cache.
|
||||
- name: Rust cache workspace-root contract
|
||||
run: cargo metadata --all-features --format-version 1 | node scripts/check-rust-cache-workspace-root.mjs
|
||||
- name: Unit tests
|
||||
run: just test-unit
|
||||
|
||||
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -70,6 +70,7 @@ jobs:
|
||||
desktop:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/_ci-desktop.yml'
|
||||
- 'bin/hermit.hcl'
|
||||
- 'scripts/ci-apt-retry.sh'
|
||||
- 'Justfile'
|
||||
- 'scripts/generate-macos-icon.py'
|
||||
@@ -135,7 +136,7 @@ jobs:
|
||||
- name: CI required-context isolation contract
|
||||
run: scripts/test-ci-required-context-isolation.sh
|
||||
- name: CI path selection regression tests
|
||||
run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs scripts/ci-apt-retry.test.mjs
|
||||
run: node --test scripts/ci-selection.test.mjs scripts/ci-required-gates.test.mjs scripts/ci-apt-retry.test.mjs scripts/check-rust-cache-workspace-root.test.mjs
|
||||
- name: File size policy
|
||||
run: just file-size-check
|
||||
|
||||
|
||||
30
Justfile
30
Justfile
@@ -416,7 +416,7 @@ test-unit:
|
||||
'package(buzz-cli)'
|
||||
# buzz-sdk builder/validation unit tests: pure event-builder and input
|
||||
# validation (e.g. the canvas writer-discipline/skew guard and the
|
||||
# canvas_write_survived predicate), no infra. `--lib` runs all unit
|
||||
# canvas_write_survived predicate), no infra. kind(lib) runs all unit
|
||||
# tests without the rustdoc dependency-resolution flake the full-package
|
||||
# invocation hits. Enumerated explicitly because nothing in CI runs
|
||||
# `cargo test --workspace` — membership buys clippy/check, not tests.
|
||||
@@ -433,13 +433,13 @@ test-unit:
|
||||
# additive migration set; legacy cutover/backfill remains an operator
|
||||
# script, not startup state) and the tenant-scoping lints. The
|
||||
# Postgres-backed buzz-db tests are
|
||||
# #[ignore]d, so --lib runs only the infra-free set. Without this gate a
|
||||
# #[ignore]d, so kind(lib) runs only the infra-free set. Without this gate a
|
||||
# stray file in migrations/ or a broken lint ships green.
|
||||
'package(buzz-db) & kind(lib)'
|
||||
# buzz-db source-policy tests: infra-free scans of the crate sources
|
||||
# that enforce the event-write admission chokepoint, metric/provenance
|
||||
# contracts, and their fixtures. They live in an integration-test
|
||||
# binary, so `--lib` above does not run them.
|
||||
# binary, so kind(lib) above does not run them.
|
||||
'package(buzz-db) & binary_id(buzz-db::observability_source)'
|
||||
# Storage accounting crosses three crates whose focused regression
|
||||
# suites are otherwise absent from the infra-free unit lane.
|
||||
@@ -450,7 +450,7 @@ test-unit:
|
||||
# Multi-tenant conformance gate (buzz-conformance): the independent
|
||||
# replay checker + golden fixtures. No infra — pure in-process trace
|
||||
# replay — so it belongs in the unit job. Run all targets (lib + the
|
||||
# tests/replay_fixtures.rs integration test), not just --lib.
|
||||
# tests/replay_fixtures.rs integration test), not just kind(lib).
|
||||
'package(buzz-conformance)'
|
||||
# Gateway unit and black-box HTTP tests are infra-free. Postgres-backed
|
||||
# contract/race tests run in the dedicated CI job below.
|
||||
@@ -486,7 +486,7 @@ test-unit:
|
||||
# #[ignore]d Postgres suites — so these non-ignored tests ran in no lane
|
||||
# and a red one could ship green (exactly how a broken admin test slipped
|
||||
# past every gate once). Scoped to api::admin, not the whole buzz-relay
|
||||
# --lib, because api::media has non-ignored tests that require Postgres.
|
||||
# kind(lib), because api::media has non-ignored tests that require Postgres.
|
||||
# DB-backed api::admin tests are #[ignore]d and run in the PostgreSQL
|
||||
# lane; the non-ignored ones reject before touching the database. Any
|
||||
# new non-ignored test here must stay DB-free: without a database, a
|
||||
@@ -510,9 +510,10 @@ test-unit:
|
||||
# neighbours: nip_fi_http, nip_fi_config, router, api::parse_query_tests,
|
||||
# and the Git transport off_mode_precedence_tests. All are infra-free
|
||||
# and finish in well under a second with no DATABASE_URL, so none wait
|
||||
# out the sqlx acquire timeout. `--bin buzz-relay` adds main.rs's
|
||||
# `tests::` module (JWKS refresh cadence) and `composition_tests::`
|
||||
# (JWKS source + refresh-loop composition), which `--lib` cannot reach
|
||||
# out the sqlx acquire timeout. binary_id(buzz-relay::bin/buzz-relay)
|
||||
# adds main.rs's `tests::` module (JWKS refresh cadence) and
|
||||
# `composition_tests::` (JWKS source + refresh-loop composition),
|
||||
# which kind(lib) cannot reach
|
||||
# because they live in the binary target; the nested
|
||||
# `tests::postgres_tests::` stays in the PostgreSQL lane.
|
||||
# The fourth clause adds the startup step timers (startup_steps) and the
|
||||
@@ -593,13 +594,24 @@ test-unit:
|
||||
package=${clause#package(}
|
||||
unit_packages+=(-p "${package%%)*}")
|
||||
done
|
||||
# Each old per-package command failed when its selection went empty
|
||||
# (nextest exits 4 on zero tests, cargo on a missing --test target).
|
||||
# In the union an empty clause would be absorbed silently, so check
|
||||
# every clause still selects a test. Same -p list: no rebuild.
|
||||
for clause in "${unit_filters[@]}"; do
|
||||
if ! cargo nextest list "${unit_packages[@]}" -E "$clause" \
|
||||
--message-format oneline --cargo-quiet | grep -q .; then
|
||||
echo "test-unit: clause selects no tests: $clause" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
unit_expr=$(printf ' | (%s)' "${unit_filters[@]}")
|
||||
cargo nextest run "${unit_packages[@]}" -E "${unit_expr# | }"
|
||||
# buzz-auth NIP-FI verifier doctests. The sealed-authority
|
||||
# `compile_fail` doctests prove the default-feature public API alone
|
||||
# cannot forge the issuer→JWKS authority; nextest does not run
|
||||
# doctests, hence this separate step. The verifier's regression suite
|
||||
# lives in the in-crate `#[cfg(test)] mod tests`, so `--lib` above
|
||||
# lives in the in-crate `#[cfg(test)] mod tests`, so kind(lib) above
|
||||
# already runs it.
|
||||
cargo test -p buzz-auth --doc
|
||||
# buzz-db `AdmittedTx` doctests. The `compile_fail` cases prove code
|
||||
|
||||
@@ -4,6 +4,8 @@ manage-git = true
|
||||
# which made Swatinem/rust-cache treat every registry crate as a workspace
|
||||
# member and save an empty target/ for the root workspace. A shared home also
|
||||
# lets worktrees reuse one registry. scripts/test-rust-cache-contract.sh pins this.
|
||||
# ~/.cargo-hermit/bin comes first on PATH, so a `cargo install` from any buzz
|
||||
# checkout shadows same-named Hermit-pinned tools in every buzz checkout.
|
||||
env = {
|
||||
"CARGO_HOME": "${HOME}/.cargo-hermit",
|
||||
"PATH": "${HOME}/.cargo-hermit/bin:${PATH}",
|
||||
|
||||
39
scripts/check-rust-cache-workspace-root.mjs
Executable file
39
scripts/check-rust-cache-workspace-root.mjs
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
// Fails when Swatinem/rust-cache would mistake dependencies for workspace
|
||||
// members. rust-cache keeps target/ outputs only for packages whose
|
||||
// manifest_path is outside the workspace root; a dependency whose manifest
|
||||
// sits under the root (a registry under the checkout, a vendored source, a
|
||||
// path dependency into a nested directory) is treated as workspace code and
|
||||
// cleaned away. When that covers every dependency the root-workspace cache
|
||||
// saves an empty target/, and the exact-key hit blocks every later save.
|
||||
//
|
||||
// Usage: cargo metadata --all-features --format-version 1 | node scripts/check-rust-cache-workspace-root.mjs
|
||||
import { readFileSync } from "node:fs";
|
||||
import { sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
export function dependenciesUnderWorkspaceRoot(metadata) {
|
||||
const members = new Set(metadata.workspace_members);
|
||||
const root = metadata.workspace_root.endsWith(sep)
|
||||
? metadata.workspace_root
|
||||
: metadata.workspace_root + sep;
|
||||
return metadata.packages
|
||||
.filter((pkg) => !members.has(pkg.id) && pkg.manifest_path.startsWith(root))
|
||||
.map((pkg) => `${pkg.name} ${pkg.version} (${pkg.manifest_path})`);
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const metadata = JSON.parse(readFileSync(0, "utf8"));
|
||||
const inside = dependenciesUnderWorkspaceRoot(metadata);
|
||||
if (inside.length > 0) {
|
||||
console.error(
|
||||
`${inside.length} non-member package(s) have a manifest under ${metadata.workspace_root}; ` +
|
||||
"rust-cache would treat them as workspace code and save an empty target/. " +
|
||||
"Keep CARGO_HOME and vendored sources outside the checkout (see bin/hermit.hcl).",
|
||||
);
|
||||
for (const line of inside.slice(0, 10)) console.error(` ${line}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const outside = metadata.packages.length - metadata.workspace_members.length;
|
||||
console.log(`rust-cache workspace root ok: ${outside} dependency package(s) outside ${metadata.workspace_root}`);
|
||||
}
|
||||
63
scripts/check-rust-cache-workspace-root.test.mjs
Normal file
63
scripts/check-rust-cache-workspace-root.test.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { dependenciesUnderWorkspaceRoot } from "./check-rust-cache-workspace-root.mjs";
|
||||
|
||||
const script = fileURLToPath(
|
||||
new URL("./check-rust-cache-workspace-root.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
function metadata(registry) {
|
||||
return {
|
||||
workspace_root: "/work/buzz",
|
||||
workspace_members: ["path+file:///work/buzz/crates/buzz-core#0.1.0"],
|
||||
packages: [
|
||||
{
|
||||
id: "path+file:///work/buzz/crates/buzz-core#0.1.0",
|
||||
name: "buzz-core",
|
||||
version: "0.1.0",
|
||||
manifest_path: "/work/buzz/crates/buzz-core/Cargo.toml",
|
||||
},
|
||||
{
|
||||
id: "registry+https://github.com/rust-lang/crates.io-index#serde@1.0.0",
|
||||
name: "serde",
|
||||
version: "1.0.0",
|
||||
manifest_path: `${registry}/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.0/Cargo.toml`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function check(meta) {
|
||||
return spawnSync("node", [script], {
|
||||
input: JSON.stringify(meta),
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
|
||||
test("a registry outside the checkout passes", () => {
|
||||
const meta = metadata("/home/runner/.cargo-hermit");
|
||||
assert.deepEqual(dependenciesUnderWorkspaceRoot(meta), []);
|
||||
const result = check(meta);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /1 dependency package\(s\) outside/);
|
||||
});
|
||||
|
||||
test("Hermit's default in-checkout registry fails", () => {
|
||||
const meta = metadata("/work/buzz/.hermit/rust");
|
||||
assert.deepEqual(dependenciesUnderWorkspaceRoot(meta), [
|
||||
"serde 1.0.0 (/work/buzz/.hermit/rust/registry/src/index.crates.io-1949cf8c6b5b557f/serde-1.0.0/Cargo.toml)",
|
||||
]);
|
||||
const result = check(meta);
|
||||
assert.equal(result.status, 1);
|
||||
assert.match(result.stderr, /1 non-member package\(s\) have a manifest under \/work\/buzz/);
|
||||
});
|
||||
|
||||
test("a sibling directory sharing the root prefix is outside the root", () => {
|
||||
assert.deepEqual(
|
||||
dependenciesUnderWorkspaceRoot(metadata("/work/buzz-cargo")),
|
||||
[],
|
||||
);
|
||||
});
|
||||
@@ -233,6 +233,7 @@ const scenarios = [
|
||||
["desktop", "mobile", "rust"],
|
||||
],
|
||||
["Justfile", ["Justfile"], ["desktop", "rust"]],
|
||||
["Hermit environment", ["bin/hermit.hcl"], ["desktop", "rust"]],
|
||||
["CI apt retry", ["scripts/ci-apt-retry.sh"], ["desktop", "rust"]],
|
||||
["integration services", ["docker-compose.yml"], ["rust"]],
|
||||
["CI integration images", ["docker-compose.ci.yml"], ["rust"]],
|
||||
|
||||
Reference in New Issue
Block a user