commit e7eb1e4ad114630fe71de5596c9df2cc6d4e67e3
parent 48f91406bb5c8c00df603e5443af841e08f27c0a
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Mar 2026 05:17:16 +0000
coverage: harden strict coverage and nix validation
- centralize required crate coverage policy and pin the dedicated coverage toolchain
- drive release-preflight from measured gate reports and xtask-owned blocking artifacts
- fix darwin nix shells and strict crate tests needed to keep 100/100/100/100 green
- verify with cargo test -p xtask, cargo run -q -p xtask -- sdk validate, cargo check --workspace, nix run .#release-preflight, and nix flake check
Diffstat:
51 files changed, 2475 insertions(+), 498 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,30 +1,30 @@
[workspace]
members = [
- "crates/core",
- "crates/events",
- "crates/events-codec",
- "crates/events-codec-wasm",
- "crates/events-indexed",
- "crates/identity",
- "crates/log",
- "crates/net",
- "crates/net-core",
- "crates/nostr",
- "crates/nostr-accounts",
- "crates/nostr-ndb",
- "crates/nostr-runtime",
- "crates/runtime",
- "crates/sql-wasm-bridge",
- "crates/sql-wasm-core",
- "crates/sql-core",
- "crates/replica-db-schema",
- "crates/replica-sync",
- "crates/replica-sync-wasm",
- "crates/replica-db",
- "crates/replica-db-wasm",
- "crates/trade",
- "crates/types",
- "crates/xtask",
+ "crates/core",
+ "crates/events",
+ "crates/events-codec",
+ "crates/events-codec-wasm",
+ "crates/events-indexed",
+ "crates/identity",
+ "crates/log",
+ "crates/net",
+ "crates/net-core",
+ "crates/nostr",
+ "crates/nostr-accounts",
+ "crates/nostr-ndb",
+ "crates/nostr-runtime",
+ "crates/runtime",
+ "crates/sql-wasm-bridge",
+ "crates/sql-wasm-core",
+ "crates/sql-core",
+ "crates/replica-db-schema",
+ "crates/replica-sync",
+ "crates/replica-sync-wasm",
+ "crates/replica-db",
+ "crates/replica-db-wasm",
+ "crates/trade",
+ "crates/types",
+ "crates/xtask",
]
resolver = "2"
@@ -72,13 +72,21 @@ directories = { version = "6" }
futures = { version = "0.3" }
hex = { version = "0.4" }
js-sys = { version = "0.3" }
-keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "windows-native", "linux-native-sync-persistent", "vendored"] }
+keyring = { version = "3.6.3", default-features = false, features = [
+ "apple-native",
+ "windows-native",
+ "linux-native-sync-persistent",
+ "vendored",
+] }
nostr = { version = "0.44.2" }
nostr-relay-pool = { version = "0.44.0" }
nostr-sdk = { version = "0.44.1" }
num_cpus = { version = "1.17.0" }
secrecy = { version = "0.10.3" }
-serde = { version = "1", default-features = false, features = ["derive"] }
+serde = { version = "1", default-features = false, features = [
+ "derive",
+ "alloc",
+] }
serde_json = { version = "1", default-features = false, features = ["alloc"] }
serde-wasm-bindgen = { version = "0.6" }
sha2 = { version = "0.10", default-features = false }
@@ -96,7 +104,7 @@ tracing-log = { version = "0.2" }
tracing-subscriber = { version = "0.3" }
ts-rs = { version = "11.1" }
typeshare = { version = "1" }
-url = { version = "2" }
+url = { version = "2" }
uuid = { version = "1.22.0", features = ["v4", "v7"] }
zeroize = { version = "1" }
uniffi = { version = "=0.29.4" }
diff --git a/contract/coverage/POLICY.md b/contract/coverage/POLICY.md
@@ -24,7 +24,8 @@ All four thresholds are release-blocking.
- run coverage checks per crate, not only aggregate workspace totals
- a crate cannot be promoted to required unless it is at 100/100/100/100
-- once required, the crate remains blocking on every pull request and push to `master`
+- once required, the crate remains blocking on every canonical release-preflight run and any external automation that wraps that run
+- `coverage-refresh.tsv` must be generated from measured per-crate gate reports, not from synthetic pass rows
## required crate contract
diff --git a/contract/manifest.toml b/contract/manifest.toml
@@ -11,12 +11,8 @@ model_crates = [
"radroots-trade",
"radroots-identity",
]
-algorithm_crates = [
- "radroots-events-codec",
-]
-wasm_crates = [
- "radroots-events-codec-wasm",
-]
+algorithm_crates = ["radroots-events-codec"]
+wasm_crates = ["radroots-events-codec-wasm"]
[surface.internal_replica_crates]
schema = "radroots-replica-db-schema"
diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml
@@ -30,9 +30,7 @@ crates = [
]
[internal]
-crates = [
- "xtask",
-]
+crates = ["xtask"]
[publish_order]
crates = [
diff --git a/contract/release/runbook.md b/contract/release/runbook.md
@@ -7,15 +7,18 @@ This runbook applies to the crates listed in `contract/release/publish-set.toml`
## preflight
```bash
-./scripts/ci/release_preflight.sh
+nix run .#release-preflight
```
This command validates:
- sdk contract integrity and release policy parity
-- required crate coverage at `100/100/100`
+- required crate coverage at `100/100/100/100`
- publish crate metadata required for crates.io
+The underlying repo-owned entrypoint is `./scripts/ci/release_preflight.sh`.
+External release automation should call the canonical local preflight and must not replace it with forge-specific logic.
+
## release tag
Create an annotated tag whose version matches `release.version` in `contract/release/publish-set.toml`.
@@ -29,7 +32,7 @@ git tag -a "v$(awk -F '\"' '/^version = / { print $2; exit }' contract/release/p
## publish simulation
```bash
-./publish-crates.sh --dry-run
+nix run .#publish-dry-run
```
This runs `cargo publish --dry-run` in release order and reports deferred crates when they depend on earlier crates that are not yet published.
@@ -37,7 +40,7 @@ This runs `cargo publish --dry-run` in release order and reports deferred crates
## publish
```bash
-./publish-crates.sh --publish
+nix run .#publish-crates -- --publish
```
This publishes in `publish_order` and waits for each crate version to become visible on crates.io before continuing.
diff --git a/contract/version.toml b/contract/version.toml
@@ -20,11 +20,7 @@ minor_on = [
"add_conformance_vector",
"add_replica_transport_adapter",
]
-patch_on = [
- "fix_docs",
- "fix_non_behavioral_codegen",
- "fix_packaging_metadata",
-]
+patch_on = ["fix_docs", "fix_non_behavioral_codegen", "fix_packaging_metadata"]
[compatibility]
requires_conformance_pass = true
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-core"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "core value types and unit semantics for radroots sdk contracts"
@@ -22,7 +24,10 @@ ts-rs = ["dep:ts-rs"]
[dependencies]
rust_decimal = { workspace = true, default-features = false }
rust_decimal_macros = { workspace = true }
-serde = { workspace = true, default-features = false, features = ["alloc", "derive"], optional = true }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+ "derive",
+], optional = true }
typeshare = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true }
diff --git a/crates/events-codec-wasm/Cargo.toml b/crates/events-codec-wasm/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-events-codec-wasm"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "wasm bindings for radroots event encoding and decoding"
@@ -15,11 +17,19 @@ readme.workspace = true
crate-type = ["cdylib", "rlib"]
[dependencies]
-radroots-events = { workspace = true, default-features = false, features = ["std", "serde"] }
-radroots-events-codec = { workspace = true, default-features = false, features = ["std", "serde_json"] }
+radroots-events = { workspace = true, default-features = false, features = [
+ "std",
+ "serde",
+] }
+radroots-events-codec = { workspace = true, default-features = false, features = [
+ "std",
+ "serde_json",
+] }
serde = { workspace = true }
serde_json = { workspace = true }
wasm-bindgen = { workspace = true }
[dev-dependencies]
-radroots-core = { workspace = true, default-features = false, features = ["std"] }
+radroots-core = { workspace = true, default-features = false, features = [
+ "std",
+] }
diff --git a/crates/events-codec/Cargo.toml b/crates/events-codec/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-events-codec"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "canonical encoders and decoders for radroots nostr event payloads"
@@ -21,6 +23,10 @@ nostr = ["dep:nostr", "std"]
[dependencies]
radroots-core = { workspace = true, default-features = false }
radroots-events = { workspace = true, default-features = false }
-serde = { workspace = true, default-features = false, features = ["alloc"], optional = true }
-serde_json = { workspace = true, default-features = false, features = ["alloc"], optional = true }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+], optional = true }
+serde_json = { workspace = true, default-features = false, features = [
+ "alloc",
+], optional = true }
nostr = { workspace = true, optional = true }
diff --git a/crates/events-indexed/Cargo.toml b/crates/events-indexed/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-events-indexed"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "indexed manifest and checkpoint models for radroots event archives"
@@ -18,7 +20,10 @@ typeshare = ["dep:typeshare"]
std = []
[dependencies]
-serde = { workspace = true, default-features = false, features = ["alloc", "derive"], optional = true }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+ "derive",
+], optional = true }
typeshare = { workspace = true, optional = true }
[dev-dependencies]
diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-events"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "nostr event models, kinds, and tag helpers for radroots integrations"
@@ -21,7 +23,10 @@ typeshare = ["dep:typeshare", "radroots-core/typeshare"]
[dependencies]
radroots-core = { workspace = true, default-features = false }
-serde = { workspace = true, default-features = false, features = ["alloc", "derive"], optional = true }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+ "derive",
+], optional = true }
ts-rs = { workspace = true, optional = true }
typeshare = { workspace = true, optional = true }
diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-identity"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "identity model and profile utilities for radroots nostr applications"
@@ -23,7 +25,9 @@ ts-rs = ["dep:ts-rs"]
[dependencies]
radroots-runtime = { workspace = true, optional = true }
-radroots-events = { workspace = true, optional = true, default-features = false, features = ["serde"] }
+radroots-events = { workspace = true, optional = true, default-features = false, features = [
+ "serde",
+] }
nostr = { workspace = true }
secrecy = { workspace = true, optional = true }
serde = { workspace = true }
diff --git a/crates/log/Cargo.toml b/crates/log/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-log"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "tracing-based logging primitives for radroots runtime crates"
@@ -18,5 +20,8 @@ std = ["dep:thiserror", "dep:tracing-subscriber", "dep:tracing-appender"]
[dependencies]
tracing = { workspace = true, default-features = false }
thiserror = { workspace = true, optional = true }
-tracing-subscriber = { workspace = true, optional = true, features = ["fmt", "env-filter"] }
+tracing-subscriber = { workspace = true, optional = true, features = [
+ "fmt",
+ "env-filter",
+] }
tracing-appender = { workspace = true, optional = true }
diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-net-core"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "network orchestration primitives and runtime interfaces for the radroots sdk"
@@ -16,26 +18,36 @@ default = ["std"]
std = ["serde/std"]
rt = ["std", "dep:tokio"]
nostr-client = [
- "std",
- "dep:radroots-events",
- "dep:radroots-events-codec",
- "radroots-events/serde",
- "dep:radroots-nostr-accounts",
- "dep:secrecy",
- "dep:hex",
- "dep:tempfile",
- "dep:serde_json",
- "dep:radroots-nostr"
+ "std",
+ "dep:radroots-events",
+ "dep:radroots-events-codec",
+ "radroots-events/serde",
+ "dep:radroots-nostr-accounts",
+ "dep:secrecy",
+ "dep:hex",
+ "dep:tempfile",
+ "dep:serde_json",
+ "dep:radroots-nostr",
]
directories = ["std", "dep:directories"]
fs-persistence = ["std"]
[dependencies]
-radroots-events = { workspace = true, optional = true, default-features = true, features = ["std", "serde", "typeshare"] }
+radroots-events = { workspace = true, optional = true, default-features = true, features = [
+ "std",
+ "serde",
+ "typeshare",
+] }
radroots-log = { workspace = true, features = ["std"] }
radroots-nostr-accounts = { workspace = true, optional = true, default-features = true }
-radroots-events-codec = { workspace = true, optional = true, default-features = true, features = ["std"] }
-radroots-nostr = { workspace = true, optional = true, default-features = true, features = ["client", "events", "codec"] }
+radroots-events-codec = { workspace = true, optional = true, default-features = true, features = [
+ "std",
+] }
+radroots-nostr = { workspace = true, optional = true, default-features = true, features = [
+ "client",
+ "events",
+ "codec",
+] }
directories = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
secrecy = { workspace = true, optional = true }
diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-net"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "unified networking interface for the radroots sdk"
diff --git a/crates/nostr-accounts/Cargo.toml b/crates/nostr-accounts/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-nostr-accounts"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "nostr protocol account primitives and vault interfaces for the radroots sdk"
@@ -13,7 +15,12 @@ readme.workspace = true
[features]
default = ["std", "file-store", "memory-vault"]
-std = ["dep:serde", "dep:serde_json", "dep:radroots-identity", "dep:radroots-runtime"]
+std = [
+ "dep:serde",
+ "dep:serde_json",
+ "dep:radroots-identity",
+ "dep:radroots-runtime",
+]
file-store = ["std"]
memory-vault = ["std"]
os-keyring = ["std", "dep:keyring"]
@@ -21,8 +28,16 @@ ndb-bridge = ["std", "dep:radroots-nostr-ndb"]
[dependencies]
keyring = { workspace = true, optional = true }
-radroots-identity = { workspace = true, optional = true, default-features = false, features = ["std", "profile", "json-file"] }
-radroots-nostr-ndb = { workspace = true, optional = true, default-features = false, features = ["ndb", "giftwrap", "rt"] }
+radroots-identity = { workspace = true, optional = true, default-features = false, features = [
+ "std",
+ "profile",
+ "json-file",
+] }
+radroots-nostr-ndb = { workspace = true, optional = true, default-features = false, features = [
+ "ndb",
+ "giftwrap",
+ "rt",
+] }
radroots-runtime = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }
diff --git a/crates/nostr-accounts/src/manager.rs b/crates/nostr-accounts/src/manager.rs
@@ -495,6 +495,24 @@ mod tests {
.join();
}
+ fn status_kind(status: &RadrootsNostrSelectedAccountStatus) -> &'static str {
+ match status {
+ RadrootsNostrSelectedAccountStatus::NotConfigured => "not-configured",
+ RadrootsNostrSelectedAccountStatus::PublicOnly { .. } => "public-only",
+ RadrootsNostrSelectedAccountStatus::Ready { .. } => "ready",
+ }
+ }
+
+ fn status_account(
+ status: &RadrootsNostrSelectedAccountStatus,
+ ) -> Option<&RadrootsNostrAccountRecord> {
+ match status {
+ RadrootsNostrSelectedAccountStatus::NotConfigured => None,
+ RadrootsNostrSelectedAccountStatus::PublicOnly { account }
+ | RadrootsNostrSelectedAccountStatus::Ready { account } => Some(account),
+ }
+ }
+
#[test]
fn manager_persists_selection_and_restores_signing_identity() {
let temp = tempfile::tempdir().expect("tempdir");
@@ -549,15 +567,12 @@ mod tests {
.expect("signing")
.is_none()
);
- match manager
+ let status = manager
.selected_account_status()
- .expect("selected account status")
- {
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- assert_eq!(account.label.as_deref(), Some("watch"));
- }
- other => panic!("unexpected account status: {other:?}"),
- }
+ .expect("selected account status");
+ assert_eq!(status_kind(&status), "public-only");
+ let account = status_account(&status).expect("account");
+ assert_eq!(account.label.as_deref(), Some("watch"));
}
#[test]
@@ -567,16 +582,13 @@ mod tests {
.generate_identity(Some("primary".into()), true)
.expect("generate");
- match manager
+ let status = manager
.selected_account_status()
- .expect("selected account status")
- {
- RadrootsNostrSelectedAccountStatus::Ready { account } => {
- assert_eq!(account.account_id, selected_id);
- assert_eq!(account.label.as_deref(), Some("primary"));
- }
- other => panic!("unexpected account status: {other:?}"),
- }
+ .expect("selected account status");
+ assert_eq!(status_kind(&status), "ready");
+ let account = status_account(&status).expect("account");
+ assert_eq!(account.account_id, selected_id);
+ assert_eq!(account.label.as_deref(), Some("primary"));
}
#[test]
@@ -675,12 +687,11 @@ mod tests {
.expect("selected signing")
.is_none()
);
- assert!(matches!(
- manager
- .selected_account_status()
- .expect("selected account status"),
- RadrootsNostrSelectedAccountStatus::NotConfigured
- ));
+ let status = manager
+ .selected_account_status()
+ .expect("selected account status");
+ assert_eq!(status_kind(&status), "not-configured");
+ assert!(status_account(&status).is_none());
let missing_id = RadrootsIdentity::generate().id();
assert!(
@@ -702,15 +713,12 @@ mod tests {
.remove_secret(&account_id)
.expect("remove secret");
- match manager
+ let status = manager
.selected_account_status()
- .expect("selected account status")
- {
- RadrootsNostrSelectedAccountStatus::PublicOnly { account } => {
- assert_eq!(account.account_id, account_id);
- }
- other => panic!("unexpected account status: {other:?}"),
- }
+ .expect("selected account status");
+ assert_eq!(status_kind(&status), "public-only");
+ let account = status_account(&status).expect("account");
+ assert_eq!(account.account_id, account_id);
let wrong_identity = RadrootsIdentity::generate();
manager
@@ -725,6 +733,60 @@ mod tests {
}
#[test]
+ fn selected_account_status_propagates_store_vault_and_secret_parse_errors() {
+ let poisoned_manager = RadrootsNostrAccountsManager::new_in_memory();
+ poison_manager_state(&poisoned_manager);
+ let selected_err = poisoned_manager
+ .selected_account_status()
+ .expect_err("selected status poisoned");
+ assert!(selected_err.to_string().starts_with("store error:"));
+
+ let mut load_error_state = RadrootsNostrAccountStoreState::default();
+ let load_error_public = RadrootsIdentity::generate().to_public();
+ load_error_state
+ .accounts
+ .push(RadrootsNostrAccountRecord::new(
+ load_error_public.clone(),
+ Some("watch".into()),
+ 1,
+ ));
+ load_error_state.selected_account_id = Some(load_error_public.id.clone());
+ let load_error_store = Arc::new(RadrootsNostrMemoryAccountStore::new());
+ load_error_store
+ .save(&load_error_state)
+ .expect("save state");
+ let vault_load_error_manager =
+ RadrootsNostrAccountsManager::new(load_error_store, Arc::new(VaultLoadError))
+ .expect("manager");
+ let vault_load_error = vault_load_error_manager
+ .selected_account_status()
+ .expect_err("vault load error");
+ assert!(vault_load_error.to_string().starts_with("vault error:"));
+
+ let mut invalid_secret_state = RadrootsNostrAccountStoreState::default();
+ let invalid_secret_public = RadrootsIdentity::generate().to_public();
+ invalid_secret_state
+ .accounts
+ .push(RadrootsNostrAccountRecord::new(
+ invalid_secret_public.clone(),
+ Some("invalid".into()),
+ 1,
+ ));
+ invalid_secret_state.selected_account_id = Some(invalid_secret_public.id.clone());
+ let invalid_secret_store = Arc::new(RadrootsNostrMemoryAccountStore::new());
+ invalid_secret_store
+ .save(&invalid_secret_state)
+ .expect("save state");
+ let invalid_secret_manager =
+ RadrootsNostrAccountsManager::new(invalid_secret_store, Arc::new(VaultInvalidSecret))
+ .expect("manager");
+ let invalid_secret = invalid_secret_manager
+ .selected_account_status()
+ .expect_err("invalid secret");
+ assert!(invalid_secret.to_string().starts_with("identity error:"));
+ }
+
+ #[test]
fn select_remove_export_and_lookup_paths() {
let manager = RadrootsNostrAccountsManager::new_in_memory();
let first_id = manager
diff --git a/crates/nostr-ndb/Cargo.toml b/crates/nostr-ndb/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-nostr-ndb"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "nostrdb adapter primitives and runtime interfaces for the radroots sdk"
@@ -20,8 +22,14 @@ runtime-adapter = ["std", "dep:radroots-nostr-runtime"]
giftwrap = ["std", "ndb"]
[dependencies]
-radroots-nostr = { workspace = true, default-features = false, features = ["std"] }
-radroots-nostr-runtime = { workspace = true, optional = true, default-features = false, features = ["std", "rt", "nostr-client"] }
+radroots-nostr = { workspace = true, default-features = false, features = [
+ "std",
+] }
+radroots-nostr-runtime = { workspace = true, optional = true, default-features = false, features = [
+ "std",
+ "rt",
+ "nostr-client",
+] }
futures = { workspace = true, optional = true }
hex = { workspace = true }
nostrdb = { version = "0.9.0", optional = true }
diff --git a/crates/nostr-runtime/Cargo.toml b/crates/nostr-runtime/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-nostr-runtime"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "nostr runtime primitives and orchestration interfaces for the radroots sdk"
@@ -19,7 +21,9 @@ nostr-client = ["std", "dep:radroots-nostr"]
nostr-ndb = ["nostr-client"]
[dependencies]
-radroots-nostr = { workspace = true, optional = true, default-features = true, features = ["client"] }
+radroots-nostr = { workspace = true, optional = true, default-features = true, features = [
+ "client",
+] }
futures = { workspace = true, optional = true }
thiserror = { workspace = true }
tokio = { workspace = true, optional = true, features = ["rt", "sync", "time"] }
diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-nostr"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "nostr protocol primitives and adapter interfaces for the radroots sdk"
@@ -15,13 +17,17 @@ readme.workspace = true
default = ["std"]
std = []
client = ["std", "dep:nostr-sdk", "dep:radroots-identity"]
-codec = ["dep:radroots-events", "dep:radroots-events-codec", "radroots-events-codec/nostr"]
+codec = [
+ "dep:radroots-events",
+ "dep:radroots-events-codec",
+ "radroots-events-codec/nostr",
+]
events = [
- "dep:radroots-events",
- "dep:radroots-events-codec",
- "radroots-events/std",
- "radroots-events/serde",
- "radroots-events-codec/std",
+ "dep:radroots-events",
+ "dep:radroots-events-codec",
+ "radroots-events/std",
+ "radroots-events/serde",
+ "radroots-events-codec/std",
]
http = ["dep:reqwest"]
nip17 = ["std", "codec", "nostr/nip44", "nostr/nip59"]
@@ -32,7 +38,10 @@ radroots-events-codec = { workspace = true, optional = true, default-features =
radroots-identity = { workspace = true, optional = true, default-features = true }
nostr = { workspace = true, features = ["nip04"] }
nostr-sdk = { workspace = true, optional = true }
-reqwest = { workspace = true, optional = true, default-features = false, features = ["json", "rustls-tls"] }
+reqwest = { workspace = true, optional = true, default-features = false, features = [
+ "json",
+ "rustls-tls",
+] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
diff --git a/crates/replica-db-schema/Cargo.toml b/crates/replica-db-schema/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-replica-db-schema"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "replica schema models and relational interfaces for radroots data layers"
diff --git a/crates/replica-db-wasm/Cargo.toml b/crates/replica-db-wasm/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-replica-db-wasm"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "wasm bindings for replica database runtime interfaces in radroots data layers"
@@ -16,7 +18,9 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
radroots-sql-core = { workspace = true, features = ["bridge"] }
-radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] }
+radroots-sql-wasm-core = { workspace = true, default-features = false, features = [
+ "bridge",
+] }
radroots-replica-db = { workspace = true }
radroots-replica-db-schema = { workspace = true }
radroots-replica-sync = { workspace = true }
diff --git a/crates/replica-db/Cargo.toml b/crates/replica-db/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-replica-db"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "replica sql runtime and migration interfaces for radroots data layers"
diff --git a/crates/replica-sync-wasm/Cargo.toml b/crates/replica-sync-wasm/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-replica-sync-wasm"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "wasm bindings for replica synchronization interfaces in radroots data layers"
@@ -16,9 +18,13 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
base64 = { workspace = true }
-radroots-events = { workspace = true, default-features = false, features = ["serde"] }
+radroots-events = { workspace = true, default-features = false, features = [
+ "serde",
+] }
radroots-sql-core = { workspace = true, features = ["bridge"] }
-radroots-sql-wasm-core = { workspace = true, default-features = false, features = ["bridge"] }
+radroots-sql-wasm-core = { workspace = true, default-features = false, features = [
+ "bridge",
+] }
radroots-replica-sync = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
diff --git a/crates/replica-sync/Cargo.toml b/crates/replica-sync/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-replica-sync"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "replica event ingest and synchronization interfaces for radroots data layers"
@@ -17,22 +19,31 @@ crate-type = ["rlib"]
[features]
default = ["std"]
std = [
- "radroots-events/std",
- "radroots-events-codec/std",
- "dep:base64",
- "dep:uuid",
+ "radroots-events/std",
+ "radroots-events-codec/std",
+ "dep:base64",
+ "dep:uuid",
]
[dependencies]
-radroots-events = { workspace = true, default-features = false, features = ["serde"] }
-radroots-events-codec = { workspace = true, default-features = false, features = ["serde_json"] }
+radroots-events = { workspace = true, default-features = false, features = [
+ "serde",
+] }
+radroots-events-codec = { workspace = true, default-features = false, features = [
+ "serde_json",
+] }
radroots-sql-core = { workspace = true }
radroots-replica-db-schema = { workspace = true }
radroots-replica-db = { workspace = true }
radroots-types = { workspace = true }
hex = { workspace = true }
-serde = { workspace = true, default-features = false, features = ["alloc", "derive"] }
-serde_json = { workspace = true, default-features = false, features = ["alloc"] }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+ "derive",
+] }
+serde_json = { workspace = true, default-features = false, features = [
+ "alloc",
+] }
sha2 = { workspace = true, default-features = false }
base64 = { workspace = true, optional = true }
uuid = { workspace = true, optional = true }
diff --git a/crates/replica-sync/src/emit.rs b/crates/replica-sync/src/emit.rs
@@ -962,6 +962,34 @@ mod tests {
}
}
+ struct DuplicateFarmSelectorExecutor<'a> {
+ inner: &'a SqliteExecutor,
+ duplicated_rows_json: String,
+ }
+
+ impl SqlExecutor for DuplicateFarmSelectorExecutor<'_> {
+ fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> {
+ self.inner.exec(sql, params_json)
+ }
+
+ fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> {
+ let _ = (sql, params_json);
+ Ok(self.duplicated_rows_json.clone())
+ }
+
+ fn begin(&self) -> Result<(), SqlError> {
+ self.inner.begin()
+ }
+
+ fn commit(&self) -> Result<(), SqlError> {
+ self.inner.commit()
+ }
+
+ fn rollback(&self) -> Result<(), SqlError> {
+ self.inner.rollback()
+ }
+ }
+
fn seed(exec: &SqliteExecutor) -> (Farm, Plot, Plot) {
migrations::run_all_up(exec).expect("migrations");
let farm = farm::create(
@@ -2307,6 +2335,35 @@ mod tests {
)
.expect("resolve by pair");
assert_eq!(resolved_by_pair.id, farm_row.id);
+ let duplicate_pair = DuplicateFarmSelectorExecutor {
+ inner: &exec,
+ duplicated_rows_json: {
+ let farm_json = serde_json::to_string(&farm_row).expect("farm json");
+ format!("[{farm_json},{farm_json}]")
+ },
+ };
+ duplicate_pair.begin().expect("duplicate begin");
+ duplicate_pair.rollback().expect("duplicate rollback");
+ duplicate_pair.begin().expect("duplicate begin");
+ duplicate_pair.commit().expect("duplicate commit");
+ duplicate_pair
+ .exec("CREATE TABLE duplicate_probe (id INTEGER)", "[]")
+ .expect("duplicate exec");
+ let duplicate_err = resolve_farm(
+ &duplicate_pair,
+ &RadrootsReplicaFarmSelector {
+ id: None,
+ d_tag: Some(farm_row.d_tag.clone()),
+ pubkey: Some(farm_row.pubkey.clone()),
+ },
+ )
+ .map(|_| ())
+ .unwrap_err();
+ assert!(
+ duplicate_err
+ .to_string()
+ .contains("did not resolve to a single farm")
+ );
assert!(
resolve_farm(
&pass,
diff --git a/crates/replica-sync/src/sync_state.rs b/crates/replica-sync/src/sync_state.rs
@@ -1,8 +1,5 @@
#[cfg(not(feature = "std"))]
-use alloc::{
- collections::BTreeMap,
- string::{String, ToString},
-};
+use alloc::{collections::BTreeMap, string::String};
#[cfg(feature = "std")]
use std::collections::BTreeMap;
diff --git a/crates/replica-sync/src/tests.rs b/crates/replica-sync/src/tests.rs
@@ -24,10 +24,9 @@ use radroots_types::types::IError;
use std::panic;
fn unwrap_sql<T>(result: Result<T, IError<SqlError>>, label: &str) -> T {
- match result {
- Ok(value) => value,
- Err(err) => panic!("{label}: {}", err.err),
- }
+ result
+ .map_err(|err| format!("{label}: {}", err.err))
+ .unwrap()
}
#[test]
diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-runtime"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "runtime config, io, and process helpers for radroots services and apps"
@@ -21,9 +23,9 @@ clap = { workspace = true, features = ["derive", "env"], optional = true }
config = { workspace = true }
radroots-log = { workspace = true, features = ["std"] }
serde = { workspace = true }
-serde_json = { workspace = true }
+serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] }
toml = { workspace = true }
tracing = { workspace = true }
-tempfile = { workspace = true }
+tempfile = { workspace = true }
diff --git a/crates/sql-core/Cargo.toml b/crates/sql-core/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-sql-core"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "sql execution and migration primitives for radroots data layers"
@@ -16,7 +18,12 @@ crate-type = ["rlib"]
[features]
web = []
-bridge = ["web", "dep:radroots-sql-wasm-bridge", "dep:serde-wasm-bindgen", "dep:wasm-bindgen"]
+bridge = [
+ "web",
+ "dep:radroots-sql-wasm-bridge",
+ "dep:serde-wasm-bindgen",
+ "dep:wasm-bindgen",
+]
native = ["dep:rusqlite"]
embedded = []
diff --git a/crates/sql-wasm-bridge/Cargo.toml b/crates/sql-wasm-bridge/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-sql-wasm-bridge"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "wasm sql bridge primitives for radroots data layers"
@@ -16,4 +18,4 @@ default = []
[dependencies]
js-sys = { workspace = true }
-wasm-bindgen = { workspace = true }
+wasm-bindgen = { workspace = true }
diff --git a/crates/sql-wasm-core/Cargo.toml b/crates/sql-wasm-core/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-sql-wasm-core"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "wasm sql runtime primitives for radroots data layers"
@@ -22,7 +24,10 @@ embedded = ["dep:rusqlite", "radroots-sql-core/native"]
[dependencies]
radroots-sql-core = { workspace = true }
radroots-sql-wasm-bridge = { workspace = true, optional = true }
-rusqlite = { workspace = true, features = ["bundled", "serialize"], optional = true }
+rusqlite = { workspace = true, features = [
+ "bundled",
+ "serialize",
+], optional = true }
chrono = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
diff --git a/crates/sql-wasm-core/src/embedded.rs b/crates/sql-wasm-core/src/embedded.rs
@@ -308,6 +308,13 @@ mod tests {
}
#[test]
+ fn exec_empty_bind_batch_surfaces_invalid_query() {
+ let engine = EmbeddedSqlEngine::new().unwrap();
+ let err = engine.exec("CREATE TABLE broken (", "[]").unwrap_err();
+ assert_eq!(err.code(), "ERR_INVALID_QUERY");
+ }
+
+ #[test]
fn export_bytes_non_empty() {
let engine = EmbeddedSqlEngine::new().unwrap();
engine.exec(CREATE_TABLE_SQL, "[]").unwrap();
diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-trade"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "trade listing models and tag mappings for radroots nostr flows"
@@ -15,7 +17,12 @@ build = "build.rs"
[features]
default = ["std", "serde", "serde_json", "ts-rs"]
std = []
-serde = ["dep:serde", "radroots-core/serde", "radroots-events/serde", "radroots-events-codec/serde"]
+serde = [
+ "dep:serde",
+ "radroots-core/serde",
+ "radroots-events/serde",
+ "radroots-events-codec/serde",
+]
serde_json = ["serde", "dep:serde_json"]
ts-rs = ["dep:ts-rs", "radroots-events/ts-rs", "radroots-events/std"]
@@ -23,6 +30,11 @@ ts-rs = ["dep:ts-rs", "radroots-events/ts-rs", "radroots-events/std"]
radroots-core = { workspace = true, default-features = false }
radroots-events = { workspace = true, default-features = false }
radroots-events-codec = { workspace = true, default-features = false }
-serde = { workspace = true, default-features = false, features = ["alloc", "derive"], optional = true }
-serde_json = { workspace = true, default-features = false, features = ["alloc"], optional = true }
+serde = { workspace = true, default-features = false, features = [
+ "alloc",
+ "derive",
+], optional = true }
+serde_json = { workspace = true, default-features = false, features = [
+ "alloc",
+], optional = true }
ts-rs = { workspace = true, optional = true }
diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml
@@ -2,7 +2,9 @@
name = "radroots-types"
version = "0.1.0-alpha.1"
edition.workspace = true
-authors = ["Radroots Authors"]
+authors = [
+ "Radroots Authors",
+]
rust-version.workspace = true
license.workspace = true
description = "shared api result and error wrapper types for radroots sdk surfaces"
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -1,6 +1,6 @@
#![forbid(unsafe_code)]
-use crate::coverage::{read_coverage_policy, CoverageThresholds};
+use crate::coverage::{CoverageThresholds, read_coverage_policy};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
@@ -223,17 +223,9 @@ fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String,
Ok(manifests)
}
-#[cfg_attr(not(test), allow(dead_code))]
-fn load_coverage_required(contract_root: &Path) -> Result<CoverageRequiredFile, String> {
- let policy = load_coverage_policy(contract_root)?;
- Ok(CoverageRequiredFile {
- required: CoverageRequiredSection {
- crates: policy.required_crates()?,
- },
- })
-}
-
-fn load_coverage_policy(contract_root: &Path) -> Result<crate::coverage::CoveragePolicyFile, String> {
+fn load_coverage_policy(
+ contract_root: &Path,
+) -> Result<crate::coverage::CoveragePolicyFile, String> {
read_coverage_policy(&contract_root.join("coverage").join("policy.toml"))
}
@@ -576,7 +568,11 @@ fn validate_coverage_policy_parity(
);
}
- let required_packages = policy.required_crates()?.into_iter().collect::<BTreeSet<_>>();
+ let required_packages = policy
+ .required_crate_entries()
+ .iter()
+ .cloned()
+ .collect::<BTreeSet<_>>();
if workspace_packages != required_packages {
let missing = workspace_packages
.difference(&required_packages)
@@ -720,11 +716,14 @@ pub fn validate_release_preflight(workspace_root: &Path) -> Result<(), String> {
validate_contract_bundle(&bundle)?;
let release =
load_release_contract(&bundle.root).expect("validated contract includes release metadata");
- let policy = load_coverage_policy(&bundle.root)
- .expect("validated contract includes coverage metadata");
+ let policy =
+ load_coverage_policy(&bundle.root).expect("validated contract includes coverage metadata");
let publish_crates = collect_unique_set(&release.publish.crates, "publish.crates")
.expect("validated contract enforces unique publish.crates");
- let required_crates = collect_unique_set(&policy.required_crates()?, "required.crates")
+ let required_crate_list = policy
+ .required_crates()
+ .expect("validated contract includes required crates");
+ let required_crates = collect_unique_set(&required_crate_list, "required.crates")
.expect("validated contract enforces unique required.crates");
validate_publish_package_metadata(workspace_root, &publish_crates)?;
validate_required_coverage_summary(workspace_root, &required_crates, policy.thresholds())?;
@@ -1154,13 +1153,17 @@ pub enum RadrootsCoreUnitDimension {
fn coverage_required_crates_match_policy_required_status() {
let root = workspace_root();
let contract_root = root.join("contract");
- let required = load_coverage_required(&contract_root).expect("coverage required");
+ let policy = load_coverage_policy(&contract_root).expect("coverage policy");
+ let required = CoverageRequiredFile {
+ required: CoverageRequiredSection {
+ crates: policy.required_crates().expect("coverage required"),
+ },
+ };
let required_names = required
.required
.crates
.into_iter()
.collect::<BTreeSet<_>>();
- let policy = load_coverage_policy(&contract_root).expect("coverage policy");
let policy_required = policy
.required_crates()
.expect("policy required crates")
@@ -1170,6 +1173,27 @@ pub enum RadrootsCoreUnitDimension {
}
#[test]
+ fn coverage_policy_required_crates_report_policy_errors() {
+ let missing_root = temp_root("load_coverage_required_missing_policy");
+ let missing_err =
+ load_coverage_policy(&missing_root).expect_err("missing policy should fail");
+ assert!(missing_err.contains("policy.toml"));
+ let _ = fs::remove_dir_all(&missing_root);
+
+ let duplicate_root =
+ create_synthetic_workspace("load_coverage_required_duplicate_required");
+ let contract_root = duplicate_root.join("contract");
+ write_file(
+ &contract_root.join("coverage").join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\", \"radroots-a\"]\n",
+ );
+ let duplicate_err =
+ load_coverage_policy(&contract_root).expect_err("duplicate required crates");
+ assert!(duplicate_err.contains("duplicate crate"));
+ let _ = fs::remove_dir_all(&duplicate_root);
+ }
+
+ #[test]
fn package_field_configured_accepts_workspace_table() {
let mut package = toml::value::Table::new();
let mut repository = toml::value::Table::new();
@@ -1347,6 +1371,13 @@ members = ["crates/a", "crates/b"]
write_file(
&coverage_dir.join("coverage-refresh.tsv"),
+ "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tpass\t100\t100\t100\t100\tfile\nradroots-a\tpass\t100\t100\t100\t100\tfile\n",
+ );
+ let duplicate_row = load_coverage_refresh_rows(&root).expect_err("duplicate coverage row");
+ assert!(duplicate_row.contains("duplicate coverage row"));
+
+ write_file(
+ &coverage_dir.join("coverage-refresh.tsv"),
"crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots-a\tfail\t100\t100\t100\t100\tfile\n",
);
let required = ["radroots-a".to_string()]
@@ -1454,6 +1485,57 @@ crates = ["radroots-a", "radroots-b"]
&contract_root.join("coverage").join("policy.toml"),
r#"[gate]
fail_under_exec_lines = 100.0
+fail_under_functions = 99.0
+fail_under_regions = 100.0
+fail_under_branches = 100.0
+require_branches = true
+
+[required]
+crates = ["radroots-a", "radroots-b"]
+"#,
+ );
+ let invalid_functions = validate_coverage_policy_parity(&root, &contract_root)
+ .expect_err("invalid function threshold");
+ assert!(invalid_functions.contains("100/100/100/100"));
+
+ write_file(
+ &contract_root.join("coverage").join("policy.toml"),
+ r#"[gate]
+fail_under_exec_lines = 100.0
+fail_under_functions = 100.0
+fail_under_regions = 99.0
+fail_under_branches = 100.0
+require_branches = true
+
+[required]
+crates = ["radroots-a", "radroots-b"]
+"#,
+ );
+ let invalid_regions = validate_coverage_policy_parity(&root, &contract_root)
+ .expect_err("invalid region threshold");
+ assert!(invalid_regions.contains("100/100/100/100"));
+
+ write_file(
+ &contract_root.join("coverage").join("policy.toml"),
+ r#"[gate]
+fail_under_exec_lines = 100.0
+fail_under_functions = 100.0
+fail_under_regions = 100.0
+fail_under_branches = 99.0
+require_branches = true
+
+[required]
+crates = ["radroots-a", "radroots-b"]
+"#,
+ );
+ let invalid_branches = validate_coverage_policy_parity(&root, &contract_root)
+ .expect_err("invalid branch threshold");
+ assert!(invalid_branches.contains("100/100/100/100"));
+
+ write_file(
+ &contract_root.join("coverage").join("policy.toml"),
+ r#"[gate]
+fail_under_exec_lines = 100.0
fail_under_functions = 100.0
fail_under_regions = 100.0
fail_under_branches = 100.0
@@ -1904,6 +1986,65 @@ edition = "2024"
assert!(parse_err.contains("parse"));
let _ = fs::remove_dir_all(&invalid);
+ let contract_manifest_missing = temp_root("parse_contract_manifest_missing");
+ let contract_manifest_read_err =
+ parse_toml::<ContractManifest>(&contract_manifest_missing.join("manifest.toml"))
+ .expect_err("missing contract manifest");
+ assert!(contract_manifest_read_err.contains("read"));
+ let _ = fs::remove_dir_all(&contract_manifest_missing);
+
+ let contract_manifest_invalid = temp_root("parse_contract_manifest_invalid");
+ write_file(
+ &contract_manifest_invalid.join("manifest.toml"),
+ "[contract",
+ );
+ let contract_manifest_parse_err =
+ parse_toml::<ContractManifest>(&contract_manifest_invalid.join("manifest.toml"))
+ .expect_err("invalid contract manifest");
+ assert!(contract_manifest_parse_err.contains("parse"));
+ let _ = fs::remove_dir_all(&contract_manifest_invalid);
+
+ let version_missing = temp_root("parse_version_policy_missing");
+ let version_read_err = parse_toml::<VersionPolicy>(&version_missing.join("version.toml"))
+ .expect_err("missing version policy");
+ assert!(version_read_err.contains("read"));
+ let _ = fs::remove_dir_all(&version_missing);
+
+ let version_invalid = temp_root("parse_version_policy_invalid");
+ write_file(&version_invalid.join("version.toml"), "[version");
+ let version_parse_err = parse_toml::<VersionPolicy>(&version_invalid.join("version.toml"))
+ .expect_err("invalid version policy");
+ assert!(version_parse_err.contains("parse"));
+ let _ = fs::remove_dir_all(&version_invalid);
+
+ let release_missing = temp_root("parse_release_contract_missing");
+ let release_read_err =
+ parse_toml::<ReleaseContractFile>(&release_missing.join("publish-set.toml"))
+ .expect_err("missing release contract");
+ assert!(release_read_err.contains("read"));
+ let _ = fs::remove_dir_all(&release_missing);
+
+ let release_invalid = temp_root("parse_release_contract_invalid");
+ write_file(&release_invalid.join("publish-set.toml"), "[release");
+ let release_parse_err =
+ parse_toml::<ReleaseContractFile>(&release_invalid.join("publish-set.toml"))
+ .expect_err("invalid release contract");
+ assert!(release_parse_err.contains("parse"));
+ let _ = fs::remove_dir_all(&release_invalid);
+
+ let export_missing = temp_root("parse_export_mapping_missing");
+ let export_read_err = parse_toml::<ExportMapping>(&export_missing.join("model.toml"))
+ .expect_err("missing export mapping");
+ assert!(export_read_err.contains("read"));
+ let _ = fs::remove_dir_all(&export_missing);
+
+ let export_invalid = temp_root("parse_export_mapping_invalid");
+ write_file(&export_invalid.join("model.toml"), "[export");
+ let export_parse_err = parse_toml::<ExportMapping>(&export_invalid.join("model.toml"))
+ .expect_err("invalid export mapping");
+ assert!(export_parse_err.contains("parse"));
+ let _ = fs::remove_dir_all(&export_invalid);
+
let dup = temp_root("publish_flags_duplicate");
write_file(
&dup.join("Cargo.toml"),
@@ -2371,9 +2512,12 @@ crates = ["radroots-a", "radroots-b"]
let required = ["radroots-a".to_string()]
.into_iter()
.collect::<BTreeSet<_>>();
- let missing_refresh_err =
- validate_required_coverage_summary(&missing_refresh_root, &required, strict_thresholds())
- .expect_err("missing refresh should fail");
+ let missing_refresh_err = validate_required_coverage_summary(
+ &missing_refresh_root,
+ &required,
+ strict_thresholds(),
+ )
+ .expect_err("missing refresh should fail");
assert!(missing_refresh_err.contains("coverage-refresh.tsv"));
let _ = fs::remove_dir_all(&missing_refresh_root);
diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs
@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
+use std::ffi::OsString;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
@@ -49,7 +50,7 @@ pub struct CoverageGateResult {
pub fail_reasons: Vec<String>,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageGateReport {
scope: String,
thresholds: CoverageGateReportThresholds,
@@ -58,7 +59,7 @@ struct CoverageGateReport {
result: CoverageGateReportResult,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageGateReportThresholds {
executable_lines: f64,
functions: f64,
@@ -67,7 +68,7 @@ struct CoverageGateReportThresholds {
branches_required: bool,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageGateReportMeasured {
executable_lines_percent: f64,
executable_lines_source: String,
@@ -78,19 +79,19 @@ struct CoverageGateReportMeasured {
summary_regions_percent: f64,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageGateReportCounts {
executable_lines: CoverageCount,
branches: CoverageCount,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageCount {
covered: u64,
total: u64,
}
-#[derive(Debug, Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
struct CoverageGateReportResult {
pass: bool,
fail_reasons: Vec<String>,
@@ -228,7 +229,9 @@ impl CoveragePolicyFile {
let mut seen = BTreeSet::new();
for crate_name in &self.required.crates {
if crate_name.trim().is_empty() {
- return Err("coverage required crates list includes an empty crate name".to_string());
+ return Err(
+ "coverage required crates list includes an empty crate name".to_string()
+ );
}
if !seen.insert(crate_name.clone()) {
return Err(format!(
@@ -238,6 +241,10 @@ impl CoveragePolicyFile {
}
Ok(self.required.crates.clone())
}
+
+ pub(crate) fn required_crate_entries(&self) -> &[String] {
+ &self.required.crates
+ }
}
pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf {
@@ -286,15 +293,20 @@ fn read_required_crates(path: &Path) -> Result<Vec<String>, String> {
}
fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> {
+ let packages = read_workspace_packages(workspace_root)?;
+ Ok(packages.into_iter().map(|(name, _)| name).collect())
+}
+
+fn read_workspace_packages(workspace_root: &Path) -> Result<Vec<(String, PathBuf)>, String> {
let workspace_manifest = parse_toml::<WorkspaceManifest>(&workspace_root.join("Cargo.toml"))?;
if workspace_manifest.workspace.members.is_empty() {
return Err("workspace members list must not be empty".to_string());
}
- let mut names = Vec::with_capacity(workspace_manifest.workspace.members.len());
+ let mut packages = Vec::with_capacity(workspace_manifest.workspace.members.len());
let mut seen = BTreeSet::new();
for member in workspace_manifest.workspace.members {
let package_manifest =
- parse_toml::<PackageManifest>(&workspace_root.join(member).join("Cargo.toml"))?;
+ parse_toml::<PackageManifest>(&workspace_root.join(&member).join("Cargo.toml"))?;
let package_name = package_manifest.package.name;
if package_name.trim().is_empty() {
return Err("workspace includes an empty package name".to_string());
@@ -305,9 +317,9 @@ fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> {
package_name
));
}
- names.push(package_name);
+ packages.push((package_name, PathBuf::from(member)));
}
- Ok(names)
+ Ok(packages)
}
fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
@@ -700,9 +712,43 @@ fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile
}
}
+fn prepend_toolchain_bin_to_path(
+ toolchain_bin: &Path,
+ existing_path: Option<OsString>,
+) -> OsString {
+ match existing_path {
+ Some(existing) => std::env::join_paths(
+ std::iter::once(toolchain_bin.to_path_buf()).chain(std::env::split_paths(&existing)),
+ )
+ .expect("joining PATH entries for coverage toolchain should succeed"),
+ None => OsString::from(toolchain_bin),
+ }
+}
+
+fn configure_coverage_toolchain_env(command: &mut Command, toolchain_bin: &Path) {
+ let joined_path = prepend_toolchain_bin_to_path(toolchain_bin, std::env::var_os("PATH"));
+ command.env("PATH", joined_path);
+
+ for (env_name, binary_name) in [
+ ("RUSTC", "rustc"),
+ ("RUSTDOC", "rustdoc"),
+ ("LLVM_COV", "llvm-cov"),
+ ("LLVM_PROFDATA", "llvm-profdata"),
+ ] {
+ let binary_path = toolchain_bin.join(binary_name);
+ if binary_path.exists() {
+ command.env(env_name, binary_path);
+ }
+ }
+}
+
fn coverage_cargo_command_with_override(override_binary: Option<&str>) -> Command {
if let Some(binary) = override_binary {
- return Command::new(binary);
+ let mut cmd = Command::new(binary);
+ if let Some(toolchain_bin) = Path::new(binary).parent() {
+ configure_coverage_toolchain_env(&mut cmd, toolchain_bin);
+ }
+ return cmd;
}
let mut cmd = Command::new("rustup");
@@ -724,6 +770,55 @@ fn coverage_llvm_cov_command() -> Command {
cmd
}
+const COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX: &str =
+ r"(/\.cargo/registry/|/lib/rustlib/src/rust/)";
+
+fn escape_regex_literal(raw: &str) -> String {
+ let mut escaped = String::with_capacity(raw.len());
+ for ch in raw.chars() {
+ match ch {
+ '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' => {
+ escaped.push('\\');
+ escaped.push(ch);
+ }
+ _ => escaped.push(ch),
+ }
+ }
+ escaped
+}
+
+fn coverage_ignore_filename_regex(
+ workspace_root: &Path,
+ crate_name: &str,
+) -> Result<String, String> {
+ let mut patterns = vec![COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX.to_string()];
+ let mut found_target = false;
+
+ for (package_name, member_path) in read_workspace_packages(workspace_root)? {
+ let absolute_member = workspace_root.join(member_path);
+ if package_name == crate_name {
+ found_target = true;
+ continue;
+ }
+ patterns.push(format!(
+ "^{}/",
+ escape_regex_literal(&absolute_member.display().to_string())
+ ));
+ }
+
+ if !found_target {
+ return Err(format!(
+ "workspace coverage filters could not resolve crate directory for {crate_name}"
+ ));
+ }
+
+ Ok(format!("({})", patterns.join("|")))
+}
+
+fn apply_coverage_report_filters(command: &mut Command, ignore_regex: &str) {
+ command.arg("--ignore-filename-regex").arg(ignore_regex);
+}
+
fn run_crate_with_runner_at_root(
args: &[String],
workspace_root: &Path,
@@ -742,6 +837,7 @@ fn run_crate_with_runner_at_root(
let test_threads = parse_optional_u32_arg(args, "test-threads")?
.or(profile.test_threads)
.unwrap_or(1);
+ let ignore_regex = coverage_ignore_filename_regex(workspace_root, &crate_name)?;
if let Err(err) = fs::create_dir_all(&out_dir) {
return Err(format!("failed to create {}: {err}", out_dir.display()));
@@ -778,6 +874,7 @@ fn run_crate_with_runner_at_root(
{
let mut cmd = coverage_llvm_cov_command();
cmd.arg("report").arg("-p").arg(&crate_name);
+ apply_coverage_report_filters(&mut cmd, &ignore_regex);
cmd.arg("--json")
.arg("--summary-only")
.arg("--branch")
@@ -794,6 +891,7 @@ fn run_crate_with_runner_at_root(
{
let mut cmd = coverage_llvm_cov_command();
cmd.arg("report").arg("-p").arg(&crate_name);
+ apply_coverage_report_filters(&mut cmd, &ignore_regex);
cmd.arg("--lcov")
.arg("--branch")
.arg("--output-path")
@@ -822,7 +920,7 @@ fn run_crate(args: &[String]) -> Result<(), String> {
run_crate_with_runner(args, &mut runner)
}
-fn report_gate(args: &[String]) -> Result<(), String> {
+fn report_gate_with_root(args: &[String], root: &Path) -> Result<(), String> {
let scope = parse_string_arg(args, "scope")?;
let summary_path = PathBuf::from(parse_string_arg(args, "summary")?);
let lcov_path = PathBuf::from(parse_string_arg(args, "lcov")?);
@@ -844,8 +942,7 @@ fn report_gate(args: &[String]) -> Result<(), String> {
.to_string(),
);
}
- let root = workspace_root();
- read_coverage_policy(&coverage_policy_path(&root))?.thresholds()
+ read_coverage_policy(&coverage_policy_path(root))?.thresholds()
} else {
let Some(fail_under_exec_lines) = explicit_exec else {
return Err(
@@ -917,12 +1014,7 @@ fn report_gate(args: &[String]) -> Result<(), String> {
fail_reasons: gate.fail_reasons.clone(),
},
};
-
- let json = serde_json::to_string_pretty(&report)
- .expect("serializing coverage gate report should succeed");
- if let Err(err) = fs::write(&out_path, format!("{json}\n")) {
- return Err(format!("failed to write {}: {err}", out_path.display()));
- }
+ write_gate_report(&out_path, &report)?;
if lcov.branches_available {
eprintln!(
@@ -958,29 +1050,101 @@ fn report_gate(args: &[String]) -> Result<(), String> {
Ok(())
}
+#[cfg_attr(not(test), allow(dead_code))]
+fn report_gate(args: &[String]) -> Result<(), String> {
+ let root = workspace_root();
+ report_gate_with_root(args, &root)
+}
+
+fn report_missing_gate_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let scope = parse_string_arg(args, "scope")?;
+ let out_path = PathBuf::from(parse_string_arg(args, "out")?);
+ let reason = parse_string_arg(args, "reason")?;
+ let thresholds = read_coverage_policy(&coverage_policy_path(root))?.thresholds();
+
+ let report = CoverageGateReport {
+ scope: scope.clone(),
+ thresholds: CoverageGateReportThresholds {
+ executable_lines: thresholds.fail_under_exec_lines,
+ functions: thresholds.fail_under_functions,
+ regions: thresholds.fail_under_regions,
+ branches: thresholds.fail_under_branches,
+ branches_required: thresholds.require_branches,
+ },
+ measured: CoverageGateReportMeasured {
+ executable_lines_percent: 0.0,
+ executable_lines_source: executable_source_label(ExecutableSource::Da).to_string(),
+ functions_percent: 0.0,
+ branches_percent: None,
+ branches_available: false,
+ summary_lines_percent: 0.0,
+ summary_regions_percent: 0.0,
+ },
+ counts: CoverageGateReportCounts {
+ executable_lines: CoverageCount {
+ covered: 0,
+ total: 0,
+ },
+ branches: CoverageCount {
+ covered: 0,
+ total: 0,
+ },
+ },
+ result: CoverageGateReportResult {
+ pass: false,
+ fail_reasons: vec![reason.clone()],
+ },
+ };
+ write_gate_report(&out_path, &report)?;
+ eprintln!("{scope} gate fail: {reason}");
+ Ok(())
+}
+
+fn write_gate_report(out_path: &Path, report: &CoverageGateReport) -> Result<(), String> {
+ let json = serde_json::to_string_pretty(report)
+ .expect("serializing coverage gate report should succeed");
+ if let Err(err) = fs::write(out_path, format!("{json}\n")) {
+ return Err(format!("failed to write {}: {err}", out_path.display()));
+ }
+ Ok(())
+}
+
+fn coverage_report_path(reports_root: &Path, crate_name: &str) -> PathBuf {
+ reports_root
+ .join(crate_name.replace('-', "_"))
+ .join("gate-report.json")
+}
+
+fn read_gate_report(path: &Path) -> Result<CoverageGateReport, String> {
+ let raw = match fs::read_to_string(path) {
+ Ok(raw) => raw,
+ Err(err) => {
+ return Err(format!(
+ "failed to read gate report {}: {err}",
+ path.display()
+ ));
+ }
+ };
+ match serde_json::from_str::<CoverageGateReport>(&raw) {
+ Ok(report) => Ok(report),
+ Err(err) => Err(format!(
+ "failed to parse gate report {}: {err}",
+ path.display()
+ )),
+ }
+}
+
fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
let required_path = coverage_policy_path(root);
let crates = read_required_crates(&required_path)?;
write_crate_names_output(writer, crates, "required crates")
}
-fn list_required_crates() -> Result<(), String> {
- let root = workspace_root();
- let mut stdout = std::io::stdout().lock();
- list_required_crates_with_root(&root, &mut stdout)
-}
-
fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> {
let crates = read_workspace_crates(&root)?;
write_crate_names_output(writer, crates, "workspace crates")
}
-fn list_workspace_crates() -> Result<(), String> {
- let root = workspace_root();
- let mut stdout = std::io::stdout().lock();
- list_workspace_crates_with_root(&root, &mut stdout)
-}
-
fn write_crate_names_output(
writer: &mut dyn Write,
crates: Vec<String>,
@@ -994,24 +1158,99 @@ fn write_crate_names_output(
Ok(())
}
-pub fn run(args: &[String]) -> Result<(), String> {
+fn run_with_root(args: &[String], root: &Path) -> Result<(), String> {
match args.first().map(String::as_str) {
Some("help") => Ok(()),
Some("run-crate") => run_crate(&args[1..]),
- Some("report") => report_gate(&args[1..]),
- Some("required-crates") => list_required_crates(),
- Some("workspace-crates") => list_workspace_crates(),
+ Some("report") => report_gate_with_root(&args[1..], root),
+ Some("report-missing") => report_missing_gate_with_root(&args[1..], root),
+ Some("refresh-summary") => {
+ let reports_root = match parse_optional_string_arg(&args[1..], "reports-root") {
+ Some(raw) => PathBuf::from(raw),
+ None => PathBuf::from("target/coverage"),
+ };
+ let out_path = match parse_optional_string_arg(&args[1..], "out") {
+ Some(raw) => PathBuf::from(raw),
+ None => PathBuf::from("target/coverage/coverage-refresh.tsv"),
+ };
+ let status_out_path = match parse_optional_string_arg(&args[1..], "status-out") {
+ Some(raw) => Some(PathBuf::from(raw)),
+ None => None,
+ };
+ let required_crates = read_required_crates(&coverage_policy_path(root))?;
+
+ let mut refresh_rows =
+ String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n");
+ let mut status_rows = String::from("crate\tstatus\n");
+
+ for crate_name in required_crates {
+ let report_path = coverage_report_path(&reports_root, &crate_name);
+ let report = read_gate_report(&report_path)?;
+ let status = if report.result.pass { "pass" } else { "fail" };
+ let branch = report.measured.branches_percent.unwrap_or(0.0);
+ refresh_rows.push_str(&format!(
+ "{}\t{}\t{:.6}\t{:.6}\t{:.6}\t{:.6}\t{}\n",
+ crate_name,
+ status,
+ report.measured.executable_lines_percent,
+ report.measured.functions_percent,
+ branch,
+ report.measured.summary_regions_percent,
+ report_path.display()
+ ));
+ status_rows.push_str(&format!("{}\t{}\n", crate_name, status));
+ }
+
+ if let Some(parent) = out_path.parent() {
+ if !parent.as_os_str().is_empty() {
+ if let Err(err) = fs::create_dir_all(parent) {
+ return Err(format!("failed to create {}: {err}", parent.display()));
+ }
+ }
+ }
+ fs::write(&out_path, refresh_rows)
+ .map_err(|err| format!("failed to write {}: {err}", out_path.display()))?;
+
+ if let Some(status_out_path) = status_out_path {
+ if let Some(parent) = status_out_path.parent() {
+ if !parent.as_os_str().is_empty() {
+ fs::create_dir_all(parent).map_err(|err| {
+ format!("failed to create {}: {err}", parent.display())
+ })?;
+ }
+ }
+ fs::write(&status_out_path, status_rows).map_err(|err| {
+ format!("failed to write {}: {err}", status_out_path.display())
+ })?;
+ }
+
+ Ok(())
+ }
+ Some("required-crates") => {
+ let mut stdout = std::io::stdout().lock();
+ list_required_crates_with_root(root, &mut stdout)
+ }
+ Some("workspace-crates") => {
+ let mut stdout = std::io::stdout().lock();
+ list_workspace_crates_with_root(root, &mut stdout)
+ }
Some(_) => Err("unknown sdk coverage subcommand".to_string()),
None => Err("missing sdk coverage subcommand".to_string()),
}
}
+pub fn run(args: &[String]) -> Result<(), String> {
+ let root = workspace_root();
+ run_with_root(args, &root)
+}
+
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::{self, Write};
use std::path::Path;
+ use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_file_path(prefix: &str) -> PathBuf {
@@ -1035,6 +1274,37 @@ mod tests {
fs::write(path, content).expect("write file");
}
+ fn cwd_lock() -> &'static Mutex<()> {
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(()))
+ }
+
+ fn recover_lock(lock: &'static Mutex<()>) -> MutexGuard<'static, ()> {
+ match lock.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ }
+ }
+
+ fn lock_cwd() -> MutexGuard<'static, ()> {
+ recover_lock(cwd_lock())
+ }
+
+ fn collect_command_envs(cmd: &Command) -> BTreeMap<String, Option<String>> {
+ let mut envs = BTreeMap::new();
+ for (key, value) in cmd.get_envs() {
+ envs.insert(
+ key.to_string_lossy().to_string(),
+ value.map(|raw| raw.to_string_lossy().to_string()),
+ );
+ }
+ envs
+ }
+
+ fn ok_runner(_cmd: Command, _name: &str) -> Result<(), String> {
+ Ok(())
+ }
+
struct FailingWriter;
impl Write for FailingWriter {
@@ -1088,6 +1358,653 @@ mod tests {
}
#[test]
+ fn read_coverage_policy_rejects_non_finite_and_out_of_range_thresholds() {
+ let non_finite = temp_file_path("coverage_policy_non_finite");
+ write_file(
+ &non_finite,
+ "[gate]\nfail_under_exec_lines = inf\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ let non_finite_err =
+ read_coverage_policy(&non_finite).expect_err("non-finite threshold should fail");
+ assert!(non_finite_err.contains("must be finite"));
+ fs::remove_file(non_finite).expect("remove non-finite policy");
+
+ let out_of_range = temp_file_path("coverage_policy_out_of_range");
+ write_file(
+ &out_of_range,
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 101.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ let out_of_range_err =
+ read_coverage_policy(&out_of_range).expect_err("out-of-range threshold should fail");
+ assert!(out_of_range_err.contains("must be within 0..=100"));
+ fs::remove_file(out_of_range).expect("remove out-of-range policy");
+ }
+
+ #[test]
+ fn report_missing_gate_uses_policy_thresholds() {
+ let root = temp_dir_path("report_missing_gate_root");
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ let out_path = root.join("gate-report.json");
+
+ report_missing_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--reason".to_string(),
+ "missing-coverage-artifacts".to_string(),
+ ],
+ &root,
+ )
+ .expect("report missing gate");
+
+ let report_raw = fs::read_to_string(&out_path).expect("read gate report");
+ let report_json: serde_json::Value =
+ serde_json::from_str(&report_raw).expect("parse gate report json");
+ assert_eq!(
+ report_json["thresholds"]["executable_lines"],
+ serde_json::json!(100.0)
+ );
+ assert_eq!(
+ report_json["thresholds"]["branches_required"],
+ serde_json::json!(true)
+ );
+ assert_eq!(report_json["result"]["pass"], serde_json::json!(false));
+ assert_eq!(
+ report_json["result"]["fail_reasons"],
+ serde_json::json!(["missing-coverage-artifacts"])
+ );
+
+ fs::remove_dir_all(root).expect("remove root");
+ }
+
+ #[test]
+ fn report_missing_gate_reports_argument_policy_and_write_errors() {
+ let root = temp_dir_path("report_missing_gate_error_root");
+ let missing_scope =
+ report_missing_gate_with_root(&[], &root).expect_err("missing scope should fail");
+ assert!(missing_scope.contains("missing --scope"));
+
+ let missing_out = report_missing_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--reason".to_string(),
+ "missing-coverage-artifacts".to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing out should fail");
+ assert!(missing_out.contains("missing --out"));
+
+ let missing_reason = report_missing_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--out".to_string(),
+ root.join("missing-gate.json").display().to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing reason should fail");
+ assert!(missing_reason.contains("missing --reason"));
+
+ let policy_err = report_missing_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--out".to_string(),
+ root.join("missing-gate.json").display().to_string(),
+ "--reason".to_string(),
+ "missing-coverage-artifacts".to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing policy should fail");
+ assert!(policy_err.contains("failed to read coverage policy"));
+
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ let out_path = root.join("gate-report.json");
+ fs::create_dir_all(&out_path).expect("create blocking output dir");
+ let write_err = report_missing_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--reason".to_string(),
+ "missing-coverage-artifacts".to_string(),
+ ],
+ &root,
+ )
+ .expect_err("directory output should fail");
+ assert!(write_err.contains("failed to write"));
+
+ fs::remove_dir_all(root).expect("remove report missing gate error root");
+ }
+
+ #[test]
+ fn refresh_summary_uses_measured_gate_report_values() {
+ let root = temp_dir_path("refresh_summary_root");
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+
+ let reports_root = root.join("target").join("coverage");
+ let crate_dir = reports_root.join("radroots_a");
+ fs::create_dir_all(&crate_dir).expect("create crate dir");
+ write_file(
+ &crate_dir.join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 97.5
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": true,
+ "fail_reasons": []
+ }
+}"#,
+ );
+
+ let refresh_out = reports_root.join("coverage-refresh.tsv");
+ let status_out = reports_root.join("coverage-refresh-status.tsv");
+ run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ reports_root.display().to_string(),
+ "--out".to_string(),
+ refresh_out.display().to_string(),
+ "--status-out".to_string(),
+ status_out.display().to_string(),
+ ],
+ &root,
+ )
+ .expect("write refresh summary");
+
+ let refresh = fs::read_to_string(&refresh_out).expect("read refresh summary");
+ assert!(refresh.contains("crate\tstatus\texec\tfunc\tbranch\tregion\treport"));
+ assert!(
+ refresh.contains("radroots-a\tpass\t100.000000\t100.000000\t100.000000\t97.500000\t")
+ );
+
+ let status = fs::read_to_string(&status_out).expect("read status summary");
+ assert_eq!(status, "crate\tstatus\nradroots-a\tpass\n");
+
+ fs::remove_dir_all(root).expect("remove root");
+
+ let defaults_root = temp_dir_path("refresh_summary_defaults_root");
+ let defaults_coverage_dir = defaults_root.join("contract").join("coverage");
+ fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir");
+ write_file(
+ &defaults_coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ write_file(
+ &defaults_root
+ .join("target")
+ .join("coverage")
+ .join("radroots_a")
+ .join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 100.0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": false,
+ "fail_reasons": ["synthetic-fail"]
+ }
+}"#,
+ );
+
+ let _guard = lock_cwd();
+ let previous_dir = std::env::current_dir().expect("read current dir");
+ std::env::set_current_dir(&defaults_root).expect("set current dir");
+ run_with_root(&["refresh-summary".to_string()], &defaults_root)
+ .expect("write refresh summary defaults");
+ let defaults_refresh = fs::read_to_string(
+ defaults_root
+ .join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv"),
+ )
+ .expect("read defaults refresh summary");
+ assert!(
+ defaults_refresh
+ .contains("radroots-a\tfail\t100.000000\t100.000000\t100.000000\t100.000000\t")
+ );
+
+ let dispatch_root = temp_dir_path("refresh_summary_parentless_root");
+ let dispatch_coverage_dir = dispatch_root.join("contract").join("coverage");
+ fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir");
+ write_file(
+ &dispatch_coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ write_file(
+ &dispatch_root.join("Cargo.toml"),
+ "[workspace]\nmembers = []\nresolver = \"2\"\n",
+ );
+ write_file(
+ &dispatch_root
+ .join("target")
+ .join("coverage")
+ .join("radroots_a")
+ .join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 100.0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": true,
+ "fail_reasons": []
+ }
+}"#,
+ );
+ std::env::set_current_dir(&dispatch_root).expect("set dispatch current dir");
+ run_with_root(
+ &[
+ "report-missing".to_string(),
+ "--scope".to_string(),
+ "radroots-a-blocking".to_string(),
+ "--out".to_string(),
+ "missing-gate.json".to_string(),
+ "--reason".to_string(),
+ "missing-coverage-artifacts".to_string(),
+ ],
+ &dispatch_root,
+ )
+ .expect("dispatch report-missing");
+ run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--out".to_string(),
+ "coverage-refresh.tsv".to_string(),
+ "--status-out".to_string(),
+ "coverage-refresh-status.tsv".to_string(),
+ ],
+ &dispatch_root,
+ )
+ .expect("dispatch refresh-summary");
+ std::env::set_current_dir(previous_dir).expect("restore current dir");
+
+ assert!(dispatch_root.join("missing-gate.json").exists());
+ assert!(dispatch_root.join("coverage-refresh.tsv").exists());
+ assert!(dispatch_root.join("coverage-refresh-status.tsv").exists());
+
+ fs::remove_dir_all(defaults_root).expect("remove defaults root");
+ fs::remove_dir_all(dispatch_root).expect("remove dispatch root");
+ }
+
+ #[test]
+ fn refresh_summary_rejects_empty_output_paths() {
+ let root = temp_dir_path("refresh_summary_empty_paths_root");
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ write_file(
+ &root
+ .join("target")
+ .join("coverage")
+ .join("radroots_a")
+ .join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 100.0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": true,
+ "fail_reasons": []
+ }
+}"#,
+ );
+
+ let out_err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ "--out".to_string(),
+ String::new(),
+ ],
+ &root,
+ )
+ .expect_err("empty out path should fail");
+ assert!(out_err.contains("failed to write"));
+
+ let status_err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ "--out".to_string(),
+ root.join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv")
+ .display()
+ .to_string(),
+ "--status-out".to_string(),
+ String::new(),
+ ],
+ &root,
+ )
+ .expect_err("empty status out path should fail");
+ assert!(status_err.contains("failed to write"));
+
+ fs::remove_dir_all(root).expect("remove empty path root");
+ }
+
+ #[test]
+ fn refresh_summary_reports_output_parent_creation_failure() {
+ let root = temp_dir_path("refresh_summary_out_parent_fail");
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ write_file(
+ &root
+ .join("target")
+ .join("coverage")
+ .join("radroots_a")
+ .join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 100.0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": true,
+ "fail_reasons": []
+ }
+}"#,
+ );
+ write_file(&root.join("out-blocker"), "x");
+
+ let err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ "--out".to_string(),
+ root.join("out-blocker")
+ .join("nested")
+ .join("coverage-refresh.tsv")
+ .display()
+ .to_string(),
+ ],
+ &root,
+ )
+ .expect_err("out parent create failure should bubble up");
+ assert!(err.contains("failed to create"));
+
+ fs::remove_dir_all(root).expect("remove out parent fail root");
+ }
+
+ #[test]
+ fn refresh_summary_reports_status_output_parent_creation_failure() {
+ let root = temp_dir_path("refresh_summary_status_parent_fail");
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ write_file(
+ &root
+ .join("target")
+ .join("coverage")
+ .join("radroots_a")
+ .join("gate-report.json"),
+ r#"{
+ "scope": "radroots-a",
+ "thresholds": {
+ "executable_lines": 100.0,
+ "functions": 100.0,
+ "regions": 100.0,
+ "branches": 100.0,
+ "branches_required": true
+ },
+ "measured": {
+ "executable_lines_percent": 100.0,
+ "executable_lines_source": "da",
+ "functions_percent": 100.0,
+ "branches_percent": 100.0,
+ "branches_available": true,
+ "summary_lines_percent": 100.0,
+ "summary_regions_percent": 100.0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 4,
+ "total": 4
+ },
+ "branches": {
+ "covered": 2,
+ "total": 2
+ }
+ },
+ "result": {
+ "pass": true,
+ "fail_reasons": []
+ }
+}"#,
+ );
+ write_file(&root.join("status-blocker"), "x");
+
+ let err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ "--out".to_string(),
+ root.join("target")
+ .join("coverage")
+ .join("coverage-refresh.tsv")
+ .display()
+ .to_string(),
+ "--status-out".to_string(),
+ root.join("status-blocker")
+ .join("nested")
+ .join("coverage-refresh-status.tsv")
+ .display()
+ .to_string(),
+ ],
+ &root,
+ )
+ .expect_err("status-out parent create failure should bubble up");
+ assert!(err.contains("failed to create"));
+
+ fs::remove_dir_all(root).expect("remove status parent fail root");
+ }
+
+ #[test]
+ fn refresh_summary_reports_policy_and_gate_report_errors() {
+ let root = temp_dir_path("refresh_summary_error_root");
+ let policy_err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing policy should fail");
+ assert!(policy_err.contains("failed to read coverage policy"));
+
+ let coverage_dir = root.join("contract").join("coverage");
+ fs::create_dir_all(&coverage_dir).expect("create coverage dir");
+ write_file(
+ &coverage_dir.join("policy.toml"),
+ "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots-a\"]\n",
+ );
+ let gate_err = run_with_root(
+ &[
+ "refresh-summary".to_string(),
+ "--reports-root".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing gate report should fail");
+ assert!(gate_err.contains("failed to read gate report"));
+
+ fs::remove_dir_all(root).expect("remove refresh summary error root");
+ }
+
+ #[test]
+ fn recover_lock_covers_ok_and_poisoned_paths() {
+ let ok_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(())));
+ let _ok_guard = recover_lock(ok_lock);
+
+ let poisoned_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(())));
+ let handle = std::thread::spawn(move || {
+ let _guard = poisoned_lock.lock().expect("lock poisoned mutex");
+ panic!("poison test mutex");
+ });
+ assert!(handle.join().is_err());
+
+ let _poisoned_guard = recover_lock(poisoned_lock);
+ }
+
+ #[test]
fn read_summary_reports_empty_data_error() {
let path = temp_file_path("summary_empty_data");
write_file(&path, r#"{"data":[]}"#);
@@ -1263,8 +2180,8 @@ test_threads = 4
"#,
)
.expect("write profiles");
- let profile = read_coverage_profile(&root, "radroots-log")
- .expect("valid positive thread profile");
+ let profile =
+ read_coverage_profile(&root, "radroots-log").expect("valid positive thread profile");
assert_eq!(profile.test_threads, Some(4));
fs::remove_dir_all(root).expect("remove root");
}
@@ -1473,6 +2390,42 @@ test_threads = 0
parse_toml::<CoveragePolicyFile>(&invalid).expect_err("invalid toml should fail");
assert!(parse_err.contains("failed to parse"));
fs::remove_file(invalid).expect("remove invalid toml");
+
+ let workspace_missing = temp_file_path("parse_toml_workspace_missing");
+ let workspace_read_err = parse_toml::<WorkspaceManifest>(&workspace_missing)
+ .expect_err("missing workspace manifest should fail");
+ assert!(workspace_read_err.contains("failed to read"));
+
+ let workspace_invalid = temp_file_path("parse_toml_workspace_invalid");
+ write_file(&workspace_invalid, "[workspace");
+ let workspace_parse_err = parse_toml::<WorkspaceManifest>(&workspace_invalid)
+ .expect_err("invalid workspace manifest should fail");
+ assert!(workspace_parse_err.contains("failed to parse"));
+ fs::remove_file(workspace_invalid).expect("remove invalid workspace manifest");
+
+ let package_missing = temp_file_path("parse_toml_package_missing");
+ let package_read_err = parse_toml::<PackageManifest>(&package_missing)
+ .expect_err("missing package manifest should fail");
+ assert!(package_read_err.contains("failed to read"));
+
+ let package_invalid = temp_file_path("parse_toml_package_invalid");
+ write_file(&package_invalid, "[package");
+ let package_parse_err = parse_toml::<PackageManifest>(&package_invalid)
+ .expect_err("invalid package manifest should fail");
+ assert!(package_parse_err.contains("failed to parse"));
+ fs::remove_file(package_invalid).expect("remove invalid package manifest");
+
+ let profiles_missing = temp_file_path("parse_toml_profiles_missing");
+ let profiles_read_err = parse_toml::<CoverageProfilesFile>(&profiles_missing)
+ .expect_err("missing coverage profiles should fail");
+ assert!(profiles_read_err.contains("failed to read"));
+
+ let profiles_invalid = temp_file_path("parse_toml_profiles_invalid");
+ write_file(&profiles_invalid, "[profiles.default");
+ let profiles_parse_err = parse_toml::<CoverageProfilesFile>(&profiles_invalid)
+ .expect_err("invalid coverage profiles should fail");
+ assert!(profiles_parse_err.contains("failed to parse"));
+ fs::remove_file(profiles_invalid).expect("remove invalid coverage profiles");
}
#[test]
@@ -1654,6 +2607,7 @@ test_threads = 0
"3".to_string(),
];
let mut names = Vec::new();
+ let mut rendered_commands = Vec::new();
let mut runner = |cmd: Command, name: &str| {
names.push(name.to_string());
let rendered = cmd
@@ -1662,6 +2616,7 @@ test_threads = 0
.collect::<Vec<_>>()
.join(" ");
assert!(!rendered.is_empty());
+ rendered_commands.push(rendered);
Ok(())
};
run_crate_with_runner(&args, &mut runner).expect("run crate with stub runner");
@@ -1674,16 +2629,44 @@ test_threads = 0
"cargo llvm-cov report --lcov".to_string(),
]
);
+ assert!(
+ rendered_commands
+ .iter()
+ .filter(|rendered| rendered.contains("report -p radroots-core"))
+ .all(|rendered| rendered.contains("--ignore-filename-regex"))
+ );
+ assert!(
+ rendered_commands
+ .iter()
+ .filter(|rendered| rendered.contains("report -p radroots-core"))
+ .all(|rendered| rendered.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX))
+ );
fs::remove_dir_all(out).expect("remove run crate output dir");
}
#[test]
+ fn coverage_ignore_filename_regex_excludes_external_and_sibling_workspace_paths() {
+ let root = workspace_root();
+ let ignore_regex =
+ coverage_ignore_filename_regex(&root, "radroots-core").expect("build ignore regex");
+ assert!(ignore_regex.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX));
+ assert!(ignore_regex.contains("crates/identity"));
+ assert!(!ignore_regex.contains("crates/core/"));
+ }
+
+ #[test]
+ fn escape_regex_literal_escapes_regex_metacharacters() {
+ let escaped = escape_regex_literal(r"\.+*?()|[]{}^$");
+ assert_eq!(escaped, r"\\\.\+\*\?\(\)\|\[\]\{\}\^\$");
+ }
+
+ #[test]
fn coverage_cargo_command_defaults_to_rustup_nightly() {
let cmd = coverage_cargo_command_with_override(None);
- let args = cmd
- .get_args()
- .map(|arg| arg.to_string_lossy().to_string())
- .collect::<Vec<_>>();
+ let mut args = Vec::new();
+ for arg in cmd.get_args() {
+ args.push(arg.to_string_lossy().to_string());
+ }
assert_eq!(cmd.get_program().to_string_lossy(), "rustup");
assert_eq!(
@@ -1697,15 +2680,89 @@ test_threads = 0
}
#[test]
- fn coverage_cargo_command_uses_override_binary_when_present() {
- let cmd = coverage_cargo_command_with_override(Some("/tmp/nightly-cargo"));
- let args = cmd
- .get_args()
- .map(|arg| arg.to_string_lossy().to_string())
- .collect::<Vec<_>>();
+ fn coverage_cargo_command_override_variants_cover_parented_and_parentless_paths() {
+ let toolchain_dir = temp_dir_path("coverage_toolchain_override");
+ fs::create_dir_all(&toolchain_dir).expect("create toolchain dir");
+ for binary in [
+ "nightly-cargo",
+ "rustc",
+ "rustdoc",
+ "llvm-cov",
+ "llvm-profdata",
+ ] {
+ write_file(&toolchain_dir.join(binary), "");
+ }
- assert_eq!(cmd.get_program().to_string_lossy(), "/tmp/nightly-cargo");
- assert!(args.is_empty());
+ let default_cmd = coverage_cargo_command_with_override(None);
+ let mut args = Vec::new();
+ for arg in default_cmd.get_args() {
+ args.push(arg.to_string_lossy().to_string());
+ }
+ assert_eq!(default_cmd.get_program().to_string_lossy(), "rustup");
+ assert_eq!(
+ args,
+ vec![
+ "run".to_string(),
+ "nightly".to_string(),
+ "cargo".to_string()
+ ]
+ );
+
+ let override_binary = toolchain_dir.join("nightly-cargo");
+ let cmd = coverage_cargo_command_with_override(Some(
+ override_binary
+ .to_str()
+ .expect("override path should be utf-8"),
+ ));
+
+ assert_eq!(
+ cmd.get_program().to_string_lossy(),
+ override_binary.to_string_lossy()
+ );
+ assert!(cmd.get_args().next().is_none());
+ let mut envs = collect_command_envs(&cmd);
+ envs.insert("MISSING".to_string(), None);
+ assert_eq!(
+ envs.get("RUSTC"),
+ Some(&Some(
+ toolchain_dir.join("rustc").to_string_lossy().to_string()
+ ))
+ );
+ assert_eq!(
+ envs.get("RUSTDOC"),
+ Some(&Some(
+ toolchain_dir.join("rustdoc").to_string_lossy().to_string()
+ ))
+ );
+ assert_eq!(
+ envs.get("LLVM_COV"),
+ Some(&Some(
+ toolchain_dir.join("llvm-cov").to_string_lossy().to_string()
+ ))
+ );
+ assert_eq!(
+ envs.get("LLVM_PROFDATA"),
+ Some(&Some(
+ toolchain_dir
+ .join("llvm-profdata")
+ .to_string_lossy()
+ .to_string()
+ ))
+ );
+ let path_env = envs
+ .get("PATH")
+ .and_then(|value| value.as_ref())
+ .expect("override binary should prepend PATH");
+ assert!(path_env.starts_with(toolchain_dir.to_string_lossy().as_ref()));
+ let mut cmd = coverage_cargo_command_with_override(Some("/"));
+ cmd.env_remove("RUSTC");
+ cmd.env_remove("LLVM_COV");
+ assert_eq!(cmd.get_program().to_string_lossy(), "/");
+ let envs = collect_command_envs(&cmd);
+ assert_eq!(envs.get("RUSTC"), Some(&None));
+ assert_eq!(envs.get("LLVM_COV"), Some(&None));
+
+ fs::remove_dir_all(toolchain_dir).expect("remove toolchain dir");
}
#[test]
@@ -1715,6 +2772,38 @@ test_threads = 0
let fallback = workspace_root_with_override(Some(""));
assert!(fallback.join("Cargo.toml").exists());
+
+ let default_root = workspace_root_with_override(None);
+ assert!(default_root.join("Cargo.toml").exists());
+ }
+
+ #[test]
+ fn prepend_toolchain_bin_to_path_covers_missing_and_existing_path_inputs() {
+ let toolchain_dir = PathBuf::from("/tmp/radroots-coverage-toolchain");
+ let no_path = prepend_toolchain_bin_to_path(&toolchain_dir, None);
+ assert_eq!(no_path, OsString::from(&toolchain_dir));
+
+ let joined =
+ prepend_toolchain_bin_to_path(&toolchain_dir, Some(OsString::from("/usr/bin:/bin")));
+ let joined = joined.to_string_lossy().to_string();
+ assert!(joined.starts_with("/tmp/radroots-coverage-toolchain"));
+ assert!(joined.contains("/usr/bin"));
+ }
+
+ #[test]
+ fn collect_command_envs_cover_helper_paths() {
+ let mut cmd = Command::new("sh");
+ cmd.env("PRESENT", "value");
+ cmd.env_remove("REMOVED");
+ let envs = collect_command_envs(&cmd);
+ assert_eq!(envs.get("PRESENT"), Some(&Some("value".to_string())));
+ assert_eq!(envs.get("REMOVED"), Some(&None));
+ }
+
+ #[test]
+ fn ok_runner_helper_returns_success() {
+ let cmd = Command::new("true");
+ assert!(ok_runner(cmd, "noop").is_ok());
}
#[test]
@@ -1778,7 +2867,19 @@ test_threads = 0
#[test]
fn run_crate_with_runner_at_root_covers_profile_and_runner_error_paths() {
+ let write_minimal_workspace = |root: &Path| {
+ write_file(
+ &root.join("Cargo.toml"),
+ "[workspace]\nmembers = [\"crates/core\"]\n",
+ );
+ write_file(
+ &root.join("crates").join("core").join("Cargo.toml"),
+ "[package]\nname = \"radroots-core\"\nversion = \"0.1.0-alpha.1\"\nedition = \"2024\"\n",
+ );
+ };
+
let profile_root = temp_dir_path("run_crate_profile_invalid");
+ write_minimal_workspace(&profile_root);
write_file(
&profile_root
.join("contract")
@@ -1800,6 +2901,7 @@ test_threads = 0
let thread_root = temp_dir_path("run_crate_bad_threads");
fs::create_dir_all(&thread_root).expect("create thread root");
+ write_minimal_workspace(&thread_root);
let thread_args = vec![
"--crate".to_string(),
"radroots-core".to_string(),
@@ -1816,6 +2918,7 @@ test_threads = 0
for fail_step in [2usize, 3usize, 4usize] {
let step_root = temp_dir_path("run_crate_step_fail");
+ write_minimal_workspace(&step_root);
let step_args = vec![
"--crate".to_string(),
"radroots-core".to_string(),
@@ -2109,6 +3212,57 @@ test_threads = 0
.expect_err("missing thresholds");
assert!(missing_thresholds.contains("missing coverage thresholds"));
+ let missing_functions = report_gate(&[
+ "--scope".to_string(),
+ "crate".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--fail-under-exec-lines".to_string(),
+ "100".to_string(),
+ ])
+ .expect_err("missing functions threshold");
+ assert!(missing_functions.contains("missing coverage thresholds"));
+
+ let missing_regions = report_gate(&[
+ "--scope".to_string(),
+ "crate".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--fail-under-exec-lines".to_string(),
+ "100".to_string(),
+ "--fail-under-functions".to_string(),
+ "100".to_string(),
+ ])
+ .expect_err("missing regions threshold");
+ assert!(missing_regions.contains("missing coverage thresholds"));
+
+ let missing_branches = report_gate(&[
+ "--scope".to_string(),
+ "crate".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--fail-under-exec-lines".to_string(),
+ "100".to_string(),
+ "--fail-under-functions".to_string(),
+ "100".to_string(),
+ "--fail-under-regions".to_string(),
+ "100".to_string(),
+ ])
+ .expect_err("missing branches threshold");
+ assert!(missing_branches.contains("missing coverage thresholds"));
+
let missing_summary_file = report_gate(&[
"--scope".to_string(),
"crate".to_string(),
@@ -2123,6 +3277,14 @@ test_threads = 0
.expect_err("missing summary file should fail");
assert!(missing_summary_file.contains("failed to read summary"));
+ let missing_gate_report = read_gate_report(&root.join("missing-gate-report.json"))
+ .expect_err("missing gate report should fail");
+ assert!(missing_gate_report.contains("failed to read gate report"));
+
+ write_file(&out_path, "{not-json");
+ let invalid_gate_report = read_gate_report(&out_path).expect_err("invalid gate report");
+ assert!(invalid_gate_report.contains("failed to parse gate report"));
+
let missing_lcov_file = report_gate(&[
"--scope".to_string(),
"crate".to_string(),
@@ -2153,10 +3315,100 @@ test_threads = 0
.expect_err("policy gate mixed with explicit thresholds");
assert!(mixed_policy_gate.contains("cannot be combined"));
+ let mixed_policy_gate_regions = report_gate(&[
+ "--scope".to_string(),
+ "crate".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--policy-gate".to_string(),
+ "--fail-under-regions".to_string(),
+ "100.0".to_string(),
+ ])
+ .expect_err("policy gate mixed with regions threshold");
+ assert!(mixed_policy_gate_regions.contains("cannot be combined"));
+
+ let mixed_policy_gate_branches_flag = report_gate(&[
+ "--scope".to_string(),
+ "crate".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--policy-gate".to_string(),
+ "--require-branches".to_string(),
+ ])
+ .expect_err("policy gate mixed with require-branches");
+ assert!(mixed_policy_gate_branches_flag.contains("cannot be combined"));
+
fs::remove_dir_all(root).expect("remove report arg errors root");
}
#[test]
+ fn coverage_ignore_filename_regex_reports_unknown_crate() {
+ let root = temp_dir_path("coverage_unknown_crate_root");
+ write_file(
+ &root.join("Cargo.toml"),
+ "[workspace]\nmembers = [\"crates/core\"]\n",
+ );
+ write_file(
+ &root.join("crates").join("core").join("Cargo.toml"),
+ "[package]\nname = \"radroots-core\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
+ );
+
+ let err = coverage_ignore_filename_regex(&root, "radroots-missing")
+ .expect_err("unknown crate should fail");
+ assert!(err.contains("could not resolve crate directory"));
+
+ fs::remove_dir_all(root).expect("remove unknown crate root");
+ }
+
+ #[test]
+ fn coverage_ignore_filename_regex_reports_workspace_manifest_errors() {
+ let root = temp_dir_path("coverage_regex_workspace_error_root");
+ let read_err = coverage_ignore_filename_regex(&root, "radroots-core")
+ .expect_err("missing workspace manifest should fail");
+ assert!(read_err.contains("failed to read"));
+
+ write_file(&root.join("Cargo.toml"), "[workspace");
+ let parse_err = coverage_ignore_filename_regex(&root, "radroots-core")
+ .expect_err("invalid workspace manifest should fail");
+ assert!(parse_err.contains("failed to parse"));
+
+ fs::remove_dir_all(root).expect("remove workspace error root");
+ }
+
+ #[test]
+ fn run_crate_with_runner_at_root_reports_ignore_filter_errors() {
+ let root = temp_dir_path("run_crate_ignore_filter_error");
+ write_file(
+ &root.join("Cargo.toml"),
+ "[workspace]\nmembers = [\"crates/other\"]\n",
+ );
+ write_file(
+ &root.join("crates").join("other").join("Cargo.toml"),
+ "[package]\nname = \"radroots-other\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
+ );
+ let args = vec![
+ "--crate".to_string(),
+ "radroots-core".to_string(),
+ "--out".to_string(),
+ root.join("target").join("coverage").display().to_string(),
+ ];
+ let mut runner = ok_runner;
+ let err = run_crate_with_runner_at_root(&args, &root, &mut runner)
+ .expect_err("missing crate coverage filter should fail");
+ assert!(err.contains("could not resolve crate directory"));
+
+ fs::remove_dir_all(root).expect("remove run crate ignore filter root");
+ }
+
+ #[test]
fn run_dispatches_subcommands_and_errors() {
run(&["help".to_string()]).expect("help subcommand");
run(&["required-crates".to_string()]).expect("required crates subcommand");
@@ -2237,4 +3489,36 @@ test_threads = 0
assert!(out_path.exists());
fs::remove_dir_all(root).expect("remove report dispatch root");
}
+
+ #[test]
+ fn report_gate_with_root_reports_policy_read_errors() {
+ let root = temp_dir_path("report_gate_policy_root_error");
+ let summary_path = root.join("summary.json");
+ let lcov_path = root.join("coverage.info");
+ let out_path = root.join("gate-report.json");
+ write_file(
+ &summary_path,
+ r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#,
+ );
+ write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n");
+
+ let err = report_gate_with_root(
+ &[
+ "--scope".to_string(),
+ "crate-x".to_string(),
+ "--summary".to_string(),
+ summary_path.display().to_string(),
+ "--lcov".to_string(),
+ lcov_path.display().to_string(),
+ "--out".to_string(),
+ out_path.display().to_string(),
+ "--policy-gate".to_string(),
+ ],
+ &root,
+ )
+ .expect_err("missing policy should fail");
+ assert!(err.contains("failed to read coverage policy"));
+
+ fs::remove_dir_all(root).expect("remove report gate policy error root");
+ }
}
diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs
@@ -629,10 +629,7 @@ manifest_file = "export-manifest.json"
"#,
);
write_file(
- &root
- .join("contract")
- .join("coverage")
- .join("policy.toml"),
+ &root.join("contract").join("coverage").join("policy.toml"),
r#"[gate]
fail_under_exec_lines = 100.0
fail_under_functions = 100.0
@@ -1104,6 +1101,64 @@ mod tests {
}
#[test]
+ fn export_ts_wasm_artifacts_returns_ok_when_no_wasm_packages_are_selected() {
+ let root = create_synthetic_workspace("export_ts_no_wasm_packages", false);
+ let out_dir = root.join("out");
+ fs::create_dir_all(&out_dir).expect("create output dir");
+
+ export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm without wasm packages");
+ assert!(!out_dir.join("ts").join("packages").exists());
+
+ let _ = fs::remove_dir_all(&root);
+ }
+
+ #[test]
+ fn export_ts_wasm_artifacts_copies_selected_wasm_package_dist() {
+ let root = create_synthetic_workspace("export_ts_with_wasm_dist", false);
+ write_file(
+ &root.join("contract").join("exports").join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a" = "@radroots/a"
+"radroots-a-wasm" = "@radroots/a-wasm"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_file(
+ &root
+ .join("crates")
+ .join("a-wasm")
+ .join("pkg")
+ .join("dist")
+ .join("radroots-a-wasm.js"),
+ "export const probe = true;\n",
+ );
+ let out_dir = root.join("out");
+ fs::create_dir_all(&out_dir).expect("create output dir");
+
+ export_ts_wasm_artifacts(&root, &out_dir).expect("export wasm dist");
+ assert!(
+ out_dir
+ .join("ts")
+ .join("packages")
+ .join("a-wasm")
+ .join("dist")
+ .join("radroots-a-wasm.js")
+ .exists()
+ );
+
+ fs::remove_dir_all(root).expect("remove root");
+ }
+
+ #[test]
fn crate_supports_ts_rs_reflects_manifest_presence() {
let root = unique_temp_dir("crate_supports_ts_rs");
let crate_dir = root.join("crates").join("probe");
diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs
@@ -24,6 +24,12 @@ fn usage() {
eprintln!(
" cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--policy-gate | (--fail-under-exec-lines <pct> --fail-under-functions <pct> --fail-under-regions <pct> --fail-under-branches <pct> [--require-branches])]"
);
+ eprintln!(
+ " cargo xtask sdk coverage report-missing --scope <scope> --out <file> --reason <reason>"
+ );
+ eprintln!(
+ " cargo xtask sdk coverage refresh-summary [--reports-root <dir>] [--out <file>] [--status-out <file>]"
+ );
}
fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf {
@@ -89,40 +95,64 @@ fn parse_crate_out_dir(
Ok((crate_selector, out_dir))
}
+fn export_ts_models_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let out_dir = parse_out_dir(args, root)?;
+ export_ts::export_ts_models(root, &out_dir)
+}
+
fn export_ts_models(args: &[String]) -> Result<(), String> {
let root = workspace_root();
- let out_dir = parse_out_dir(args, &root)?;
- export_ts::export_ts_models(&root, &out_dir)
+ export_ts_models_with_root(args, &root)
+}
+
+fn export_ts_constants_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let out_dir = parse_out_dir(args, root)?;
+ export_ts::export_ts_constants(root, &out_dir)
}
fn export_ts_constants(args: &[String]) -> Result<(), String> {
let root = workspace_root();
+ export_ts_constants_with_root(args, &root)
+}
+
+fn export_ts_wasm_with_root(args: &[String], root: &Path) -> Result<(), String> {
let out_dir = parse_out_dir(args, &root)?;
- export_ts::export_ts_constants(&root, &out_dir)
+ export_ts::export_ts_wasm_artifacts(root, &out_dir)
}
fn export_ts_wasm(args: &[String]) -> Result<(), String> {
let root = workspace_root();
- let out_dir = parse_out_dir(args, &root)?;
- export_ts::export_ts_wasm_artifacts(&root, &out_dir)
+ export_ts_wasm_with_root(args, &root)
+}
+
+fn export_manifest_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let out_dir = parse_out_dir(args, root)?;
+ export_ts::write_ts_export_manifest(root, &out_dir).map(|_| ())
}
fn export_manifest(args: &[String]) -> Result<(), String> {
let root = workspace_root();
- let out_dir = parse_out_dir(args, &root)?;
- export_ts::write_ts_export_manifest(&root, &out_dir).map(|_| ())
+ export_manifest_with_root(args, &root)
+}
+
+fn export_ts_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let out_dir = parse_out_dir(args, root)?;
+ export_ts::export_ts_bundle(root, &out_dir).map(|_| ())
}
fn export_ts(args: &[String]) -> Result<(), String> {
let root = workspace_root();
- let out_dir = parse_out_dir(args, &root)?;
- export_ts::export_ts_bundle(&root, &out_dir).map(|_| ())
+ export_ts_with_root(args, &root)
+}
+
+fn export_ts_crate_with_root(args: &[String], root: &Path) -> Result<(), String> {
+ let (crate_selector, out_dir) = parse_crate_out_dir(args, root)?;
+ export_ts::export_ts_bundle_for_crate(root, &out_dir, &crate_selector).map(|_| ())
}
fn export_ts_crate(args: &[String]) -> Result<(), String> {
let root = workspace_root();
- let (crate_selector, out_dir) = parse_crate_out_dir(args, &root)?;
- export_ts::export_ts_bundle_for_crate(&root, &out_dir, &crate_selector).map(|_| ())
+ export_ts_crate_with_root(args, &root)
}
fn validate_contract() -> Result<(), String> {
@@ -187,7 +217,7 @@ fn main() -> ExitCode {
mod tests {
use super::*;
use std::fs;
- use std::sync::{Mutex, OnceLock};
+ use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
fn workspace_lock() -> &'static Mutex<()> {
@@ -195,6 +225,13 @@ mod tests {
LOCK.get_or_init(|| Mutex::new(()))
}
+ fn lock_workspace() -> MutexGuard<'static, ()> {
+ match workspace_lock().lock() {
+ Ok(guard) => guard,
+ Err(poison) => poison.into_inner(),
+ }
+ }
+
fn unique_temp_dir(prefix: &str) -> PathBuf {
let ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -203,6 +240,168 @@ mod tests {
std::env::temp_dir().join(format!("radroots_xtask_main_{prefix}_{ns}"))
}
+ fn write_file(path: &Path, content: &str) {
+ let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new("")));
+ fs::write(path, content).expect("write file");
+ }
+
+ fn create_synthetic_export_workspace(prefix: &str) -> PathBuf {
+ let root = unique_temp_dir(prefix);
+ fs::create_dir_all(&root).expect("create root");
+ write_file(
+ &root.join("Cargo.toml"),
+ r#"[workspace]
+members = ["crates/a", "crates/b"]
+resolver = "2"
+"#,
+ );
+ write_file(
+ &root.join("crates").join("a").join("Cargo.toml"),
+ r#"[package]
+name = "radroots-a"
+version = "0.1.0"
+edition = "2024"
+description = "crate a"
+repository = "https://example.com/a"
+homepage = "https://example.com/a"
+documentation = "https://docs.example.com/a"
+readme = "README.md"
+
+[features]
+ts-rs = []
+"#,
+ );
+ write_file(
+ &root.join("crates").join("a").join("src").join("lib.rs"),
+ r#"pub fn crate_a() {}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+ use std::path::PathBuf;
+
+ #[test]
+ fn write_ts_exports() {
+ if let Ok(path) = std::env::var("RADROOTS_TS_RS_EXPORT_DIR") {
+ let export_dir = PathBuf::from(path);
+ let _ = fs::create_dir_all(&export_dir);
+ fs::write(
+ export_dir.join("types.ts"),
+ "export type Probe = { id: string };\n",
+ )
+ .expect("write generated types");
+ }
+ }
+}
+"#,
+ );
+ write_file(
+ &root.join("crates").join("b").join("Cargo.toml"),
+ r#"[package]
+name = "radroots-b"
+version = "0.1.0"
+edition = "2024"
+publish = false
+"#,
+ );
+ write_file(
+ &root.join("crates").join("b").join("src").join("lib.rs"),
+ "pub fn crate_b() {}\n",
+ );
+ write_file(
+ &root.join("crates").join("core").join("src").join("unit.rs"),
+ r#"pub enum RadrootsCoreUnitDimension {
+ Count,
+ Mass,
+ Volume,
+}
+"#,
+ );
+ write_file(
+ &root.join("contract").join("manifest.toml"),
+ r#"[contract]
+name = "radroots-contract"
+version = "1.0.0"
+source = "synthetic"
+
+[surface]
+model_crates = ["radroots-a"]
+algorithm_crates = ["radroots-b"]
+wasm_crates = ["radroots-a-wasm"]
+
+[policy]
+exclude_internal_workspace_crates = true
+require_reproducible_exports = true
+require_conformance_vectors = true
+"#,
+ );
+ write_file(
+ &root.join("contract").join("version.toml"),
+ r#"[contract]
+version = "1.0.0"
+stability = "alpha"
+
+[semver]
+major_on = ["breaking"]
+minor_on = ["feature"]
+patch_on = ["fix"]
+
+[compatibility]
+requires_conformance_pass = true
+requires_export_manifest_diff = true
+requires_release_notes = true
+"#,
+ );
+ write_file(
+ &root.join("contract").join("exports").join("ts.toml"),
+ r#"[language]
+id = "ts"
+repository = "sdk-typescript"
+
+[packages]
+"radroots-a" = "@radroots/a"
+
+[artifacts]
+models_dir = "src/generated"
+constants_dir = "src/generated"
+wasm_dist_dir = "dist"
+manifest_file = "export-manifest.json"
+"#,
+ );
+ write_file(
+ &root.join("contract").join("coverage").join("policy.toml"),
+ r#"[gate]
+fail_under_exec_lines = 100.0
+fail_under_functions = 100.0
+fail_under_regions = 100.0
+fail_under_branches = 100.0
+require_branches = true
+
+[required]
+crates = ["radroots-a", "radroots-b"]
+"#,
+ );
+ write_file(
+ &root
+ .join("contract")
+ .join("release")
+ .join("publish-set.toml"),
+ r#"[release]
+version = "1.0.0"
+
+[publish]
+crates = ["radroots-a"]
+
+[internal]
+crates = ["radroots-b"]
+
+[publish_order]
+crates = ["radroots-a"]
+"#,
+ );
+ root
+ }
+
#[test]
fn workspace_root_resolves_and_parse_helpers_cover_branches() {
let root = workspace_root();
@@ -272,6 +471,9 @@ mod tests {
let fallback = workspace_root_with_override(Some(" "));
assert!(fallback.join("Cargo.toml").exists());
+
+ let default_root = workspace_root_with_override(None);
+ assert!(default_root.join("Cargo.toml").exists());
}
#[test]
@@ -289,52 +491,54 @@ mod tests {
#[test]
fn export_wrappers_cover_success_and_error_paths() {
- let _guard = workspace_lock().lock().expect("lock workspace");
- let root = workspace_root();
+ let _guard = lock_workspace();
+ let root = create_synthetic_export_workspace("export_wrappers");
let out_dir = unique_temp_dir("export_wrappers");
fs::create_dir_all(&out_dir).expect("create out dir");
let invalid_args = vec!["--bad".to_string()];
- assert!(export_ts_models(&invalid_args).is_err());
- assert!(export_ts_constants(&invalid_args).is_err());
- assert!(export_ts_wasm(&invalid_args).is_err());
- assert!(export_manifest(&invalid_args).is_err());
- assert!(export_ts(&invalid_args).is_err());
- assert!(export_ts_crate(&invalid_args).is_err());
-
- let ts_rs_root = root.join("target").join("ts-rs");
- fs::create_dir_all(ts_rs_root.join("core")).expect("create ts-rs core dir");
- fs::write(
- ts_rs_root.join("core").join("types.ts"),
- "export type CoreProbe = { id: string };\n",
- )
- .expect("write core types");
+ assert!(export_ts_models_with_root(&invalid_args, &root).is_err());
+ assert!(export_ts_constants_with_root(&invalid_args, &root).is_err());
+ assert!(export_ts_wasm_with_root(&invalid_args, &root).is_err());
+ assert!(export_manifest_with_root(&invalid_args, &root).is_err());
+ assert!(export_ts_with_root(&invalid_args, &root).is_err());
+ assert!(export_ts_crate_with_root(&invalid_args, &root).is_err());
let args = vec!["--out".to_string(), out_dir.display().to_string()];
- export_manifest(&args).expect("export manifest");
- export_ts_wasm(&args).expect("export wasm");
- export_ts_constants(&args).expect("export constants");
- export_ts_models(&args).expect("export models");
+ export_ts_with_root(&args, &root).expect("export ts bundle");
+ export_manifest_with_root(&args, &root).expect("export manifest");
+ export_ts_wasm_with_root(&args, &root).expect("export wasm");
+ export_ts_constants_with_root(&args, &root).expect("export constants");
+ export_ts_models_with_root(&args, &root).expect("export models");
let crate_args = vec![
"--crate".to_string(),
- "core".to_string(),
+ "a".to_string(),
"--out".to_string(),
out_dir.display().to_string(),
];
- export_ts_crate(&crate_args).expect("export ts crate");
-
- let bundle_args = vec!["--out".to_string(), out_dir.display().to_string()];
- export_ts(&bundle_args).expect("export ts bundle");
+ export_ts_crate_with_root(&crate_args, &root).expect("export ts crate");
assert!(out_dir.join("ts").exists());
let _ = fs::remove_dir_all(out_dir);
+ let _ = fs::remove_dir_all(root);
+ }
+
+ #[test]
+ fn lock_workspace_recovers_from_poisoned_mutex() {
+ let handle = std::thread::spawn(|| {
+ let _guard = workspace_lock().lock().expect("lock workspace");
+ panic!("poison workspace lock");
+ });
+ assert!(handle.join().is_err());
+
+ let _guard = lock_workspace();
}
#[test]
fn contract_and_coverage_dispatchers_execute() {
- let _guard = workspace_lock().lock().expect("lock workspace");
+ let _guard = lock_workspace();
let root = workspace_root();
let out_dir = unique_temp_dir("coverage_dispatch");
fs::create_dir_all(&out_dir).expect("create out dir");
@@ -349,12 +553,9 @@ mod tests {
fs::remove_file(&coverage_refresh_path).expect("remove existing coverage refresh");
let parent = coverage_refresh_path.parent().expect("coverage parent");
fs::create_dir_all(parent).expect("create coverage parent");
- let required_raw = fs::read_to_string(
- root.join("contract")
- .join("coverage")
- .join("policy.toml"),
- )
- .expect("read coverage policy contract");
+ let required_raw =
+ fs::read_to_string(root.join("contract").join("coverage").join("policy.toml"))
+ .expect("read coverage policy contract");
let required_toml =
toml::from_str::<toml::Value>(&required_raw).expect("parse coverage policy contract");
let required_crates = required_toml
@@ -434,7 +635,7 @@ mod tests {
#[test]
fn run_sdk_dispatches_export_and_validate_commands() {
- let _guard = workspace_lock().lock().expect("lock workspace");
+ let _guard = lock_workspace();
assert!(run_sdk(&["export-ts".to_string(), "--bad".to_string()]).is_err());
assert!(run_sdk(&["export-ts-crate".to_string(), "--bad".to_string()]).is_err());
assert!(run_sdk(&["export-ts-models".to_string(), "--bad".to_string()]).is_err());
diff --git a/docs/nix.md b/docs/nix.md
@@ -7,7 +7,7 @@ This workspace uses Nix as the canonical development and CI environment contract
macOS:
```bash
-sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install)
+sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon
```
Linux with systemd:
@@ -103,7 +103,7 @@ Repo-aware flows stay behind `nix run` apps because they need a real checkout:
- `sdk export-ts` writes into repo-local `target/`
- sdk sync validation runs `bun` against a checked-out `sdk-typescript` repo path
-- coverage refresh and release preflight produce repo-local artifacts
+- coverage refresh and release preflight produce repo-local artifacts derived from measured per-crate gate reports
- wasm packaging writes package output directories
- publish commands read runtime tokens and the live checkout state
diff --git a/flake.nix b/flake.nix
@@ -15,7 +15,8 @@
};
};
- outputs = inputs@{ flake-parts, ... }:
+ outputs =
+ inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.treefmt-nix.flakeModule ];
systems = [
@@ -47,7 +48,13 @@
treefmt = import ./treefmt.nix;
apps = import ./nix/apps.nix {
- inherit common config pkgs toolchains;
+ inherit
+ common
+ config
+ lib
+ pkgs
+ toolchains
+ ;
};
checks = lib.filterAttrs (_: value: value != null) (
diff --git a/nix/apps.nix b/nix/apps.nix
@@ -1,7 +1,16 @@
-{ common, config, pkgs, toolchains }:
+{
+ common,
+ config,
+ lib,
+ pkgs,
+ toolchains,
+}:
let
stablePath = "export PATH=${toolchains.stable}/bin:$PATH";
coveragePath = "export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH";
+ coverageShellExec = command: ''
+ exec nix develop .#coverage --accept-flake-config -c sh -lc ${lib.escapeShellArg command} sh "$@"
+ '';
mkRepoApp =
{
name,
@@ -92,10 +101,10 @@ in
publish-crates = mkRepoApp {
name = "publish-crates";
description = "Publish crates through the workspace release script";
- runtimeInputs = common.runtimeInputs.release;
- command = ''
- ./publish-crates.sh "$@"
- '';
+ runtimeInputs = [
+ pkgs.nix
+ ];
+ command = coverageShellExec ''./publish-crates.sh "$@"'';
env = common.exportCoverageEnv;
pathPrefix = coveragePath;
};
@@ -103,10 +112,10 @@ in
publish-dry-run = mkRepoApp {
name = "publish-dry-run";
description = "Run a dry-run crates publish through the workspace release script";
- runtimeInputs = common.runtimeInputs.release;
- command = ''
- ./publish-crates.sh --dry-run "$@"
- '';
+ runtimeInputs = [
+ pkgs.nix
+ ];
+ command = coverageShellExec ''./publish-crates.sh --dry-run "$@"'';
env = common.exportCoverageEnv;
pathPrefix = coveragePath;
};
@@ -114,8 +123,10 @@ in
release-preflight = mkRepoApp {
name = "release-preflight";
description = "Run release coverage refresh and preflight validation";
- runtimeInputs = common.runtimeInputs.coverage;
- command = common.releasePreflightCommand;
+ runtimeInputs = [
+ pkgs.nix
+ ];
+ command = coverageShellExec common.releasePreflightCommand;
env = common.exportCoverageEnv;
pathPrefix = coveragePath;
};
diff --git a/nix/common.nix b/nix/common.nix
@@ -1,4 +1,9 @@
-{ crane, lib, pkgs, toolchains }:
+{
+ crane,
+ lib,
+ pkgs,
+ toolchains,
+}:
let
root = ../.;
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
@@ -9,9 +14,8 @@ let
repoSource = lib.sources.cleanSource root;
cargoSource = lib.fileset.toSource {
root = root;
- fileset = lib.fileset.intersection
- (lib.fileset.fromSource repoSource)
- (lib.fileset.unions [
+ fileset = lib.fileset.intersection (lib.fileset.fromSource repoSource) (
+ lib.fileset.unions [
../Cargo.toml
../Cargo.lock
../Makefile
@@ -22,44 +26,65 @@ let
../contract
../crates
../scripts
- ]);
+ ]
+ );
};
- sharedEnv = {
+ baseEnv = {
CARGO_TERM_COLOR = "always";
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
- SODIUM_USE_PKG_CONFIG = "1";
+ }
+ // lib.optionalAttrs pkgs.stdenv.isDarwin {
+ CC = "clang";
+ CXX = "clang++";
+ SDKROOT = pkgs.apple-sdk_14.sdkroot;
+ MACOSX_DEPLOYMENT_TARGET = pkgs.stdenv.hostPlatform.darwinMinVersion;
+ };
+ sharedEnv = baseEnv // {
+ PKG_CONFIG_PATH = lib.makeSearchPathOutput "dev" "lib/pkgconfig" stableRuntimeInputs;
};
coverageEnv = sharedEnv // {
RADROOTS_COVERAGE_CARGO = "${toolchains.coverage}/bin/cargo";
};
+ cargoLlvmCov =
+ (pkgs.callPackage "${pkgs.path}/pkgs/by-name/ca/cargo-llvm-cov/package.nix" { }).overrideAttrs
+ (old: {
+ doCheck = false;
+ meta = old.meta // {
+ broken = false;
+ };
+ });
exportEnv =
env:
lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg value}") env
);
- stableRuntimeInputs = with pkgs; [
- toolchains.stable
- clang
- coreutils
- curl
- findutils
- gawk
- gitMinimal
- gnugrep
- gnumake
- gnused
- jq
- libsodium
- llvmPackages.libclang
- pkg-config
- python3
- ] ++ darwinBuildInputs;
+ stableRuntimeInputs =
+ with pkgs;
+ [
+ toolchains.stable
+ clang
+ coreutils
+ curl
+ findutils
+ gawk
+ gitMinimal
+ gnugrep
+ gnumake
+ gnused
+ jq
+ libsodium
+ llvmPackages.llvm
+ llvmPackages.libclang
+ pkg-config
+ python3
+ ]
+ ++ darwinBuildInputs;
syncRuntimeInputs = stableRuntimeInputs ++ [
pkgs.bun
];
coverageRuntimeInputs = stableRuntimeInputs ++ [
toolchains.coverage
- pkgs.cargo-llvm-cov
+ cargoLlvmCov
];
wasmRuntimeInputs = stableRuntimeInputs ++ [
pkgs.wasm-pack
@@ -92,11 +117,13 @@ let
];
buildInputs = [
pkgs.libsodium
- ] ++ darwinBuildInputs;
+ ]
+ ++ darwinBuildInputs;
inherit (sharedEnv)
CARGO_TERM_COLOR
LIBCLANG_PATH
- SODIUM_USE_PKG_CONFIG;
+ PKG_CONFIG_PATH
+ ;
};
cargoArtifacts = craneLib.buildDepsOnly commonCraneArgs;
xtaskPackage = craneLib.buildPackage (
@@ -186,144 +213,113 @@ let
bun run test
'';
coverageReportCommand = ''
- mkdir -p target/sdk-coverage
- : > target/sdk-coverage/coverage-report-status.txt
+ rm -rf target/sdk-coverage
+ mkdir -p target/sdk-coverage
+ : > target/sdk-coverage/coverage-report-status.txt
- workspace_crates_file="$(mktemp)"
- required_crates_file="$(mktemp)"
- trap 'rm -f "$workspace_crates_file" "$required_crates_file"' EXIT
+ workspace_crates_file="$(mktemp)"
+ required_crates_file="$(mktemp)"
+ trap 'rm -f "$workspace_crates_file" "$required_crates_file"' EXIT
- cargo run -q -p xtask -- sdk coverage workspace-crates > "$workspace_crates_file"
- while IFS= read -r crate; do
- [ -n "''${crate}" ] || continue
- safe_crate="''${crate//-/_}"
- run_dir="target/sdk-coverage/''${safe_crate}"
- mkdir -p "''${run_dir}"
- status="ok"
+ cargo run -q -p xtask -- sdk coverage workspace-crates > "$workspace_crates_file"
+ while IFS= read -r crate; do
+ [ -n "''${crate}" ] || continue
+ safe_crate="''${crate//-/_}"
+ run_dir="target/sdk-coverage/''${safe_crate}"
+ mkdir -p "''${run_dir}"
+ status="ok"
- if ! cargo run -q -p xtask -- sdk coverage run-crate --crate "''${crate}" --out "''${run_dir}"; then
- status="run-failed"
- fi
+ if ! cargo run -q -p xtask -- sdk coverage run-crate --crate "''${crate}" --out "''${run_dir}"; then
+ status="run-failed"
+ fi
- if [ "''${status}" = "ok" ] && ! cargo run -q -p xtask -- sdk coverage report \
- --scope "''${crate}" \
- --summary "''${run_dir}/coverage-summary.json" \
- --lcov "''${run_dir}/coverage-lcov.info" \
- --out "''${run_dir}/coverage-gate-summary.json" \
- --fail-under-exec-lines 0 \
- --fail-under-functions 0 \
- --fail-under-regions 0 \
- --fail-under-branches 0; then
- status="report-failed"
- fi
+ if [ "''${status}" = "ok" ] && ! cargo run -q -p xtask -- sdk coverage report \
+ --scope "''${crate}" \
+ --summary "''${run_dir}/coverage-summary.json" \
+ --lcov "''${run_dir}/coverage-lcov.info" \
+ --out "''${run_dir}/coverage-gate-summary.json" \
+ --fail-under-exec-lines 0 \
+ --fail-under-functions 0 \
+ --fail-under-regions 0 \
+ --fail-under-branches 0; then
+ status="report-failed"
+ fi
- if [ "''${status}" != "ok" ]; then
- cat > "''${run_dir}/coverage-gate-summary.json" <<EOF
- {
- "scope": "''${crate}",
- "thresholds": {
- "executable_lines": 0,
- "functions": 0,
- "regions": 0,
- "branches": 0,
- "branches_required": false
- },
- "measured": {
- "executable_lines_percent": 0,
- "executable_lines_source": "da",
- "functions_percent": 0,
- "branches_percent": null,
- "branches_available": false,
- "summary_lines_percent": 0,
- "summary_regions_percent": 0
- },
- "counts": {
- "executable_lines": {
- "covered": 0,
- "total": 0
- },
- "branches": {
- "covered": 0,
- "total": 0
+ if [ "''${status}" != "ok" ]; then
+ cat > "''${run_dir}/coverage-gate-summary.json" <<EOF
+ {
+ "scope": "''${crate}",
+ "thresholds": {
+ "executable_lines": 0,
+ "functions": 0,
+ "regions": 0,
+ "branches": 0,
+ "branches_required": false
+ },
+ "measured": {
+ "executable_lines_percent": 0,
+ "executable_lines_source": "da",
+ "functions_percent": 0,
+ "branches_percent": null,
+ "branches_available": false,
+ "summary_lines_percent": 0,
+ "summary_regions_percent": 0
+ },
+ "counts": {
+ "executable_lines": {
+ "covered": 0,
+ "total": 0
+ },
+ "branches": {
+ "covered": 0,
+ "total": 0
+ }
+ },
+ "result": {
+ "pass": false,
+ "fail_reasons": [
+ "''${status}"
+ ]
+ }
}
- },
- "result": {
- "pass": false,
- "fail_reasons": [
- "''${status}"
- ]
- }
- }
-EOF
- fi
+ EOF
+ fi
- echo "''${crate}:''${status}" >> target/sdk-coverage/coverage-report-status.txt
- done < "$workspace_crates_file"
+ echo "''${crate}:''${status}" >> target/sdk-coverage/coverage-report-status.txt
+ done < "$workspace_crates_file"
- cargo run -q -p xtask -- sdk coverage required-crates > "$required_crates_file"
- while IFS= read -r crate; do
- [ -n "''${crate}" ] || continue
- safe_crate="''${crate//-/_}"
- crate_dir="target/sdk-coverage/''${safe_crate}"
- crate_status="$(awk -F: -v crate="''${crate}" '$1 == crate { status = $2 } END { print status }' target/sdk-coverage/coverage-report-status.txt)"
+ cargo run -q -p xtask -- sdk coverage required-crates > "$required_crates_file"
+ while IFS= read -r crate; do
+ [ -n "''${crate}" ] || continue
+ safe_crate="''${crate//-/_}"
+ crate_dir="target/sdk-coverage/''${safe_crate}"
+ crate_status="$(awk -F: -v crate="''${crate}" '$1 == crate { status = $2 } END { print status }' target/sdk-coverage/coverage-report-status.txt)"
- if [ ! -f "''${crate_dir}/coverage-summary.json" ] || [ ! -f "''${crate_dir}/coverage-lcov.info" ]; then
- fail_reason="missing-coverage-artifacts"
- if [ -n "''${crate_status}" ] && [ "''${crate_status}" != "ok" ]; then
- fail_reason="''${crate_status}"
- fi
+ if [ ! -f "''${crate_dir}/coverage-summary.json" ] || [ ! -f "''${crate_dir}/coverage-lcov.info" ]; then
+ fail_reason="missing-coverage-artifacts"
+ if [ -n "''${crate_status}" ] && [ "''${crate_status}" != "ok" ]; then
+ fail_reason="''${crate_status}"
+ fi
- cat > "''${crate_dir}/coverage-gate-blocking.json" <<EOF
- {
- "scope": "''${crate}-blocking",
- "thresholds": {
- "executable_lines": 100,
- "functions": 100,
- "regions": 100,
- "branches": 100,
- "branches_required": true
- },
- "measured": {
- "executable_lines_percent": 0,
- "executable_lines_source": "da",
- "functions_percent": 0,
- "branches_percent": null,
- "branches_available": false,
- "summary_lines_percent": 0,
- "summary_regions_percent": 0
- },
- "counts": {
- "executable_lines": {
- "covered": 0,
- "total": 0
- },
- "branches": {
- "covered": 0,
- "total": 0
- }
- },
- "result": {
- "pass": false,
- "fail_reasons": [
- "''${fail_reason}"
- ]
- }
- }
-EOF
- continue
- fi
+ cargo run -q -p xtask -- sdk coverage report-missing \
+ --scope "''${crate}-blocking" \
+ --out "''${crate_dir}/coverage-gate-blocking.json" \
+ --reason "''${fail_reason}"
+ continue
+ fi
- cargo run -q -p xtask -- sdk coverage report \
- --scope "''${crate}-blocking" \
- --summary "''${crate_dir}/coverage-summary.json" \
- --lcov "''${crate_dir}/coverage-lcov.info" \
- --out "''${crate_dir}/coverage-gate-blocking.json" \
- --policy-gate
- done < "$required_crates_file"
+ cargo run -q -p xtask -- sdk coverage report \
+ --scope "''${crate}-blocking" \
+ --summary "''${crate_dir}/coverage-summary.json" \
+ --lcov "''${crate_dir}/coverage-lcov.info" \
+ --out "''${crate_dir}/coverage-gate-blocking.json" \
+ --policy-gate
+ done < "$required_crates_file"
'';
in
{
inherit
+ cargoLlvmCov
cargoArtifacts
checkCommand
commonCraneArgs
@@ -339,7 +335,8 @@ in
validateSdkTypescriptCommand
version
wasmBuildsCommand
- xtaskPackage;
+ xtaskPackage
+ ;
exportCoverageEnv = exportEnv coverageEnv;
exportSharedEnv = exportEnv sharedEnv;
diff --git a/nix/devshells.nix b/nix/devshells.nix
@@ -1,4 +1,8 @@
-{ common, pkgs, toolchains }:
+{
+ common,
+ pkgs,
+ toolchains,
+}:
let
defaultHook = ''
${common.exportSharedEnv}
@@ -12,7 +16,7 @@ in
{
default = pkgs.mkShell {
packages = common.runtimeInputs.wasm ++ [
- pkgs.cargo-llvm-cov
+ common.cargoLlvmCov
];
shellHook = defaultHook;
};
diff --git a/nix/toolchains.nix b/nix/toolchains.nix
@@ -2,7 +2,7 @@
let
toolchain = builtins.fromTOML (builtins.readFile ../rust-toolchain.toml);
stableVersion = toolchain.toolchain.channel;
- stableTargets = toolchain.toolchain.targets or [];
+ stableTargets = toolchain.toolchain.targets or [ ];
stableExtensions = [
"clippy"
"rust-analyzer"
diff --git a/publish-crates.sh b/publish-crates.sh
@@ -6,27 +6,25 @@ cd "$root_dir"
mode="publish"
case "${1:-}" in
- --dry-run)
- mode="dry-run"
- shift
- ;;
- --publish)
- mode="publish"
- shift
- ;;
- "" )
- ;;
- *)
- ;;
+--dry-run)
+ mode="dry-run"
+ shift
+ ;;
+--publish)
+ mode="publish"
+ shift
+ ;;
+"") ;;
+*) ;;
esac
requested="${*:-}"
-if [[ "$mode" == "publish" ]] && [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]] && [[ -n "${CRATES_IO_TOKEN:-}" ]]; then
+if [[ $mode == "publish" ]] && [[ -z ${CARGO_REGISTRY_TOKEN:-} ]] && [[ -n ${CRATES_IO_TOKEN:-} ]]; then
export CARGO_REGISTRY_TOKEN="${CRATES_IO_TOKEN}"
fi
-if [[ "$mode" == "publish" ]] && [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
+if [[ $mode == "publish" ]] && [[ -z ${CARGO_REGISTRY_TOKEN:-} ]]; then
echo "set CARGO_REGISTRY_TOKEN or CRATES_IO_TOKEN before publish"
exit 1
fi
diff --git a/rust-toolchain-coverage.toml b/rust-toolchain-coverage.toml
@@ -1,4 +1,10 @@
[toolchain]
-channel = "nightly-2026-03-19"
-components = ["clippy", "rust-analyzer", "rust-src", "rustfmt", "llvm-tools-preview"]
+channel = "nightly-2026-03-12"
+components = [
+ "clippy",
+ "rust-analyzer",
+ "rust-src",
+ "rustfmt",
+ "llvm-tools-preview",
+]
targets = ["wasm32-unknown-unknown"]
diff --git a/scripts/ci/guard_committed_ts_artifacts.sh b/scripts/ci/guard_committed_ts_artifacts.sh
@@ -3,7 +3,7 @@ set -euo pipefail
tracked_artifacts="$(git ls-files 'crates/*/bindings/**')"
-if [[ -n "$tracked_artifacts" ]]; then
+if [[ -n $tracked_artifacts ]]; then
echo "committed ts artifacts are not allowed under crates/*/bindings/**"
echo "$tracked_artifacts"
exit 1
diff --git a/scripts/ci/guard_no_legacy_identifiers.sh b/scripts/ci/guard_no_legacy_identifiers.sh
@@ -4,11 +4,11 @@ set -euo pipefail
matches="$(
git grep -nI 'tangle' -- . \
':(exclude)AGENTS.md' \
- ':(exclude)scripts/ci/guard_no_legacy_identifiers.sh' \
- || true
+ ':(exclude)scripts/ci/guard_no_legacy_identifiers.sh' ||
+ true
)"
-if [[ -n "$matches" ]]; then
+if [[ -n $matches ]]; then
echo "legacy identifier 'tangle' is forbidden in tracked oss files"
echo "$matches"
exit 1
diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh
@@ -10,11 +10,10 @@ cargo run -q -p xtask -- sdk validate
required_file="$(mktemp)"
trap 'rm -f "$required_file"' EXIT
-cargo run -q -p xtask -- sdk coverage required-crates > "$required_file"
+cargo run -q -p xtask -- sdk coverage required-crates >"$required_file"
+rm -rf target/coverage
mkdir -p target/coverage
-printf "crate\tstatus\texec\tfunc\tbranch\tregion\treport\n" > target/coverage/coverage-refresh.tsv
-printf "crate\tstatus\n" > target/coverage/coverage-refresh-status.tsv
while IFS= read -r crate; do
[ -n "$crate" ] || continue
@@ -29,10 +28,12 @@ while IFS= read -r crate; do
--lcov "${out_dir}/coverage-lcov.info" \
--out "${out_dir}/gate-report.json" \
--policy-gate
+done <"$required_file"
- printf "%s\tpass\t100.0\t100.0\t100.0\t100.0\t%s\n" "$crate" "${out_dir}/gate-report.json" >> target/coverage/coverage-refresh.tsv
- printf "%s\tpass\n" "$crate" >> target/coverage/coverage-refresh-status.tsv
-done < "$required_file"
+cargo run -q -p xtask -- sdk coverage refresh-summary \
+ --reports-root target/coverage \
+ --out target/coverage/coverage-refresh.tsv \
+ --status-out target/coverage/coverage-refresh-status.tsv
cargo run -q -p xtask -- sdk release preflight
echo "release preflight complete"
diff --git a/scripts/ci/release_publish_order.sh b/scripts/ci/release_publish_order.sh
@@ -5,7 +5,7 @@ root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$root_dir"
mode="${1:-publish}"
-if [[ "$mode" != "publish" && "$mode" != "dry-run" ]]; then
+if [[ $mode != "publish" && $mode != "dry-run" ]]; then
echo "usage: scripts/ci/release_publish_order.sh [publish|dry-run] [crate names]"
exit 2
fi
@@ -24,7 +24,7 @@ release_version="$(
' contract/release/publish-set.toml
)"
-if [[ -z "$release_version" ]]; then
+if [[ -z $release_version ]]; then
echo "failed to resolve release.version from contract/release/publish-set.toml"
exit 1
fi
@@ -39,9 +39,9 @@ awk '
gsub(/[" ,]/, "", line)
if (length(line) > 0) print line
}
-' contract/release/publish-set.toml > "$order_file"
+' contract/release/publish-set.toml >"$order_file"
-if [[ ! -s "$order_file" ]]; then
+if [[ ! -s $order_file ]]; then
echo "publish_order.crates list is empty"
exit 1
fi
@@ -102,10 +102,10 @@ publish_with_retry() {
local retry_after
retry_after="$(sed -n 's/.*Please try again after \(.*GMT\).*/\1/p' "$log_file" | head -n1)"
local sleep_secs=0
- if [[ -n "$retry_after" ]]; then
+ if [[ -n $retry_after ]]; then
sleep_secs="$(seconds_until_http_date "$retry_after")"
fi
- if [[ "$sleep_secs" -le 0 ]]; then
+ if [[ $sleep_secs -le 0 ]]; then
sleep_secs=$((30 + attempt * 15))
fi
echo "publish rate-limited for ${crate}; retry ${attempt} in ${sleep_secs}s"
@@ -120,10 +120,10 @@ publish_with_retry() {
done
}
-if [[ -n "$requested_raw" ]]; then
+if [[ -n $requested_raw ]]; then
for token in $requested_raw; do
- [[ -n "$token" ]] || continue
- echo "$token" >> "$requested_file"
+ [[ -n $token ]] || continue
+ echo "$token" >>"$requested_file"
done
sort -u "$requested_file" -o "$requested_file"
@@ -132,21 +132,21 @@ if [[ -n "$requested_raw" ]]; then
echo "requested crate is not in publish_order.crates: ${token}"
exit 1
fi
- done < "$requested_file"
+ done <"$requested_file"
while IFS= read -r crate; do
- [[ -n "$crate" ]] || continue
+ [[ -n $crate ]] || continue
if grep -Fxq "$crate" "$requested_file"; then
- echo "$crate" >> "$selected_file"
+ echo "$crate" >>"$selected_file"
fi
- done < "$order_file"
+ done <"$order_file"
else
cp "$order_file" "$selected_file"
fi
while IFS= read -r crate; do
[ -n "$crate" ] || continue
- if [[ "$mode" == "dry-run" ]]; then
+ if [[ $mode == "dry-run" ]]; then
log_file="$(mktemp)"
if cargo publish --dry-run --locked --allow-dirty -p "$crate" >"$log_file" 2>&1; then
cat "$log_file"
@@ -155,7 +155,7 @@ while IFS= read -r crate; do
fi
missing_dep="$(sed -n 's/.*no matching package named `\([^`]*\)`.*/\1/p' "$log_file" | head -n1)"
- if [[ -n "$missing_dep" ]] && grep -Fxq "$missing_dep" "$order_file"; then
+ if [[ -n $missing_dep ]] && grep -Fxq "$missing_dep" "$order_file"; then
echo "dry-run defer for ${crate}: dependency ${missing_dep} is not yet published"
rm -f "$log_file"
continue
@@ -176,12 +176,12 @@ while IFS= read -r crate; do
if crate_version_visible "$crate"; then
break
fi
- if [[ "$attempt" == "30" ]]; then
+ if [[ $attempt == "30" ]]; then
echo "crate ${crate} version ${release_version} not visible on crates.io after publish"
exit 1
fi
sleep 10
done
-done < "$selected_file"
+done <"$selected_file"
echo "publish sequence complete for release ${release_version}"