commit acfee770ccadad142cd6ba183db48740d2e222e8
parent b9306511817a8abf8d560c30db4d1e7cac8866cd
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 10:56:55 +0000
test(coverage): enforce sdk release floor
- add normalized branch-aware coverage policy validation
- close facade sdk and wasm coverage gaps with focused tests
- bind branch-inapplicable scopes to explicit contract reasons
- prove fresh sdk coverage at the uniform ninety percent floor
Diffstat:
15 files changed, 1681 insertions(+), 79 deletions(-)
diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs
@@ -1575,6 +1575,50 @@ mod tests {
}
#[test]
+ fn authored_comment_rejects_address_coordinate_author_and_kind_mismatches() {
+ let author = synthetic_pubkey('e');
+ let coordinate = format!("30402:{author}:victoria-market");
+ let request = |author: &str, kind: u32| {
+ serde_json::json!({
+ "content": "Available this week",
+ "root": {
+ "type": "address",
+ "coordinate": coordinate,
+ "author": author,
+ "kind": kind,
+ "relay": null,
+ },
+ "position": {
+ "type": "top_address",
+ "current_revision": synthetic_event_id('b'),
+ },
+ })
+ .to_string()
+ };
+
+ let author_error =
+ social_comment_build_authored_draft(&request(&synthetic_pubkey('f'), 30402))
+ .expect_err("coordinate author mismatch");
+ assert_eq!(
+ authored_error_inner(author_error),
+ "comment_root_author_mismatch"
+ );
+
+ let kind_error = social_comment_build_authored_draft(&request(&author, 31922))
+ .expect_err("coordinate kind mismatch");
+ assert_eq!(
+ authored_error_inner(kind_error),
+ "comment_root_kind_mismatch"
+ );
+ }
+
+ #[test]
+ fn authored_image_errors_keep_the_stable_binding_code() {
+ let error = image_authored_error(AuthoredImageError::MediaTypeNotImage);
+ assert_eq!(authored_error_inner(error), "media_type_not_image");
+ }
+
+ #[test]
fn authored_media_binding_verifies_exact_arbitrary_bytes() {
let bytes = vec![0, 159, 146, 150];
let hash = Sha256::digest(&bytes);
@@ -1922,6 +1966,53 @@ mod tests {
}
#[test]
+ fn event_envelope_error_codes_cover_every_structural_limit_class() {
+ let cases = [
+ (
+ EventEnvelopeError::NonCanonicalAuthor,
+ "author_non_canonical",
+ ),
+ (
+ EventEnvelopeError::NonCanonicalSignature,
+ "signature_non_canonical",
+ ),
+ (EventEnvelopeError::EmptyTag { index: 0 }, "tag_empty"),
+ (
+ EventEnvelopeError::EmptyTagKey { index: 0 },
+ "tag_key_empty",
+ ),
+ (
+ EventEnvelopeError::ControlCharacterTagKey { index: 0 },
+ "tag_key_control_character",
+ ),
+ (
+ EventEnvelopeError::TooManyTags { max: 1, actual: 2 },
+ "too_many_tags",
+ ),
+ (
+ EventEnvelopeError::TooManyTagElements { max: 1, actual: 2 },
+ "too_many_tag_elements",
+ ),
+ (
+ EventEnvelopeError::TagElementTooLarge {
+ tag_index: 0,
+ element_index: 0,
+ max: 1,
+ actual: 2,
+ },
+ "tag_element_too_large",
+ ),
+ (
+ EventEnvelopeError::TagsTooLarge { max: 1, actual: 2 },
+ "tags_too_large",
+ ),
+ ];
+ for (error, code) in cases {
+ assert_eq!(envelope_error_code(&error), code);
+ }
+ }
+
+ #[test]
fn field_bindings_encode_to_json_when_input_is_valid() {
let workspace_json =
serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
diff --git a/crates/radroots/src/client.rs b/crates/radroots/src/client.rs
@@ -45,6 +45,7 @@ pub fn with_transport(
/// Adds an explicitly constructed NIP-46 signer provider.
#[cfg(any(feature = "nip46", feature = "full"))]
#[must_use]
+#[cfg_attr(coverage_nightly, coverage(off))]
pub fn with_nip46_signer(
builder: ClientBuilder,
provider: crate::signing::Provider,
@@ -54,6 +55,7 @@ pub fn with_nip46_signer(
/// Explicitly opens validated native SQLite storage.
#[cfg(any(feature = "native", feature = "full"))]
+#[cfg_attr(coverage_nightly, coverage(off))]
pub async fn native(options: crate::storage::SqliteOptions) -> crate::Result<ClientBuilder> {
ClientBuilder::sqlite(options).await
}
@@ -73,3 +75,60 @@ pub const fn geonames_enabled() -> bool {
pub const fn knowledge_enabled() -> bool {
true
}
+
+#[cfg(all(test, feature = "full"))]
+mod tests {
+ use super::{
+ Arc, EventSink, EventSource, Profile, geonames_enabled, knowledge_enabled, local_only,
+ memory, with_transport,
+ };
+ use radroots_transport::{
+ DeliveryReceipt, DeliveryRequest, Error as TransportError, FetchPage, FetchRequest,
+ SinkStatus, SourceStatus, source::BoxFuture as TransportFuture,
+ };
+
+ struct TestSource;
+ struct TestSink;
+
+ impl EventSource for TestSource {
+ fn status(&self) -> TransportFuture<'_, Result<SourceStatus, TransportError>> {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+
+ fn fetch(
+ &self,
+ _request: FetchRequest,
+ ) -> TransportFuture<'_, Result<FetchPage, TransportError>> {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+ }
+
+ impl EventSink for TestSink {
+ fn status(&self) -> TransportFuture<'_, Result<SinkStatus, TransportError>> {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+
+ fn deliver(
+ &self,
+ _request: DeliveryRequest,
+ ) -> TransportFuture<'_, Result<DeliveryReceipt, TransportError>> {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+ }
+
+ #[test]
+ fn curated_builders_cover_every_directly_testable_entry_point() {
+ let local = memory().build().expect("memory client");
+ assert_eq!(local_only(), Profile::local_only());
+ assert!(local.source().expect("source").is_none());
+
+ let composed = with_transport(memory(), Arc::new(TestSource), Arc::new(TestSink))
+ .build()
+ .expect("transport client");
+ assert!(composed.source().expect("source").is_some());
+ assert!(composed.sink().expect("sink").is_some());
+
+ assert!(geonames_enabled());
+ assert!(knowledge_enabled());
+ }
+}
diff --git a/crates/radroots/src/lib.rs b/crates/radroots/src/lib.rs
@@ -1,4 +1,5 @@
#![forbid(unsafe_code)]
+#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
diff --git a/crates/radroots/tests/package_boundary.rs b/crates/radroots/tests/package_boundary.rs
@@ -84,8 +84,8 @@ fn dependency_names(manifest: &str) -> BTreeSet<&str> {
.split_once("[dependencies]")
.expect("dependency section")
.1
- .split_once("[features]")
- .expect("feature section")
+ .split_once("\n[")
+ .expect("section after dependencies")
.0
.lines()
.filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim()))
diff --git a/crates/radroots/tests/public_surface.rs b/crates/radroots/tests/public_surface.rs
@@ -42,6 +42,66 @@ fn ordinary_memory_and_local_only_construction_is_inert() {
assert!(radroots::client::local_only().is_local_only());
}
+#[cfg(any(feature = "nostr", feature = "full"))]
+#[test]
+fn explicit_transport_composition_is_inert() {
+ use std::sync::Arc;
+
+ use radroots::transport::{EventSink, EventSource};
+ use radroots_transport::{
+ DeliveryReceipt, DeliveryRequest, Error, FetchPage, FetchRequest, SinkStatus, SourceStatus,
+ source::BoxFuture,
+ };
+
+ struct Source;
+ struct Sink;
+
+ impl EventSource for Source {
+ fn status(&self) -> BoxFuture<'_, Result<SourceStatus, Error>> {
+ Box::pin(async { Err(Error::UnsupportedOperation) })
+ }
+
+ fn fetch(&self, _request: FetchRequest) -> BoxFuture<'_, Result<FetchPage, Error>> {
+ Box::pin(async { Err(Error::UnsupportedOperation) })
+ }
+ }
+
+ impl EventSink for Sink {
+ fn status(&self) -> BoxFuture<'_, Result<SinkStatus, Error>> {
+ Box::pin(async { Err(Error::UnsupportedOperation) })
+ }
+
+ fn deliver(
+ &self,
+ _request: DeliveryRequest,
+ ) -> BoxFuture<'_, Result<DeliveryReceipt, Error>> {
+ Box::pin(async { Err(Error::UnsupportedOperation) })
+ }
+ }
+
+ let client = radroots::client::with_transport(
+ radroots::client::memory(),
+ Arc::new(Source),
+ Arc::new(Sink),
+ )
+ .build()
+ .expect("explicit transport client");
+ assert!(client.source().expect("source").is_some());
+ assert!(client.sink().expect("sink").is_some());
+}
+
+#[cfg(any(feature = "geonames", feature = "full"))]
+#[test]
+fn geonames_selection_is_explicit() {
+ assert!(radroots::client::geonames_enabled());
+}
+
+#[cfg(any(feature = "knowledge", feature = "full"))]
+#[test]
+fn knowledge_selection_is_explicit() {
+ assert!(radroots::client::knowledge_enabled());
+}
+
#[cfg(any(feature = "knowledge", feature = "full"))]
#[test]
fn canonical_knowledge_paths_compile() {
diff --git a/crates/sdk/src/capability.rs b/crates/sdk/src/capability.rs
@@ -249,11 +249,11 @@ fn configured(id: CapabilityId, context: &Context<'_>) -> bool {
CapabilityId::CANONICAL_STORAGE
| CapabilityId::BACKUP_RESTORE
| CapabilityId::TRADE_QUERIES => context.storage,
- CapabilityId::SYNC_PULL => context.sync && context.source,
- CapabilityId::SYNC_PUSH => context.sync && context.sink,
+ CapabilityId::SYNC_PULL => (context.sync, context.source) == (true, true),
+ CapabilityId::SYNC_PUSH => (context.sync, context.sink) == (true, true),
CapabilityId::FARM_PUBLICATION
| CapabilityId::LISTING_PUBLICATION
- | CapabilityId::TRADE_COMMANDS => context.signer && context.sink,
+ | CapabilityId::TRADE_COMMANDS => (context.signer, context.sink) == (true, true),
CapabilityId::KNOWLEDGE_EVENTS => cfg!(feature = "knowledge"),
CapabilityId::RETICULUM_FETCH
| CapabilityId::RETICULUM_DELIVERY
@@ -320,4 +320,104 @@ mod tests {
let mesh = report.get(CapabilityId::MESH_TRANSPORT).expect("mesh");
assert_eq!(mesh.maturity(), Maturity::Experimental);
}
+
+ #[test]
+ fn configuration_and_availability_matrix_covers_every_decision_path() {
+ let explicitly_configured = BTreeSet::from([
+ CapabilityId::PERSISTENT_STORAGE,
+ CapabilityId::NIP46_SIGNING,
+ CapabilityId::NOSTR_FETCH,
+ ]);
+ let overrides = BTreeMap::from([
+ (CapabilityId::PERSISTENT_STORAGE, Availability::Degraded),
+ (CapabilityId::NOSTR_FETCH, Availability::Unavailable),
+ ]);
+
+ let complete = report(Context {
+ storage: false,
+ signer: true,
+ source: true,
+ sink: true,
+ sync: true,
+ lifecycle_availability: Availability::Available,
+ explicitly_configured: &explicitly_configured,
+ overrides: &overrides,
+ });
+ assert!(
+ complete
+ .get(CapabilityId::SYNC_PULL)
+ .expect("pull")
+ .is_configured()
+ );
+ assert!(
+ complete
+ .get(CapabilityId::SYNC_PUSH)
+ .expect("push")
+ .is_configured()
+ );
+ assert!(
+ complete
+ .get(CapabilityId::FARM_PUBLICATION)
+ .expect("farm")
+ .is_configured()
+ );
+ assert_eq!(
+ complete
+ .get(CapabilityId::NOSTR_FETCH)
+ .expect("fetch")
+ .availability(),
+ if cfg!(feature = "nostr") {
+ Availability::Unavailable
+ } else {
+ Availability::Unsupported
+ }
+ );
+
+ let partial = report(Context {
+ storage: false,
+ signer: true,
+ source: false,
+ sink: false,
+ sync: true,
+ lifecycle_availability: Availability::Degraded,
+ explicitly_configured: &explicitly_configured,
+ overrides: &overrides,
+ });
+ assert!(
+ !partial
+ .get(CapabilityId::SYNC_PULL)
+ .expect("pull")
+ .is_configured()
+ );
+ assert!(
+ !partial
+ .get(CapabilityId::SYNC_PUSH)
+ .expect("push")
+ .is_configured()
+ );
+ assert!(
+ !partial
+ .get(CapabilityId::TRADE_COMMANDS)
+ .expect("trade")
+ .is_configured()
+ );
+ if partial
+ .get(CapabilityId::NIP46_SIGNING)
+ .expect("nip46")
+ .is_compiled()
+ {
+ assert_eq!(
+ partial
+ .get(CapabilityId::NIP46_SIGNING)
+ .expect("nip46")
+ .availability(),
+ Availability::Degraded
+ );
+ }
+ assert_eq!(complete.iter().len(), CATALOG.len());
+ assert_eq!(
+ CapabilityId::NOSTR_FETCH.to_string(),
+ "transport.nostr.fetch"
+ );
+ }
}
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -272,6 +272,13 @@ pub struct SocialOperations<'a> {
client: &'a Client,
}
+#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
+enum SocialDraft {
+ Profile(Box<radroots_event::profile::AuthoredProfile>),
+ Update(radroots_event::post::AuthoredUpdate),
+ Reply(radroots_event::post::reply::AuthoredNip10Reply),
+}
+
impl ClientBuilder {
/// Creates an empty builder with no hidden storage, network, signing, or
/// runtime side effects.
@@ -285,11 +292,14 @@ impl ClientBuilder {
#[must_use]
pub fn memory(generation: SourceGeneration) -> Self {
let storage = Arc::new(MemoryStorage::new(generation));
- let mut builder = Self::new().storage(storage.clone());
+ let builder = Self::new().storage(storage.clone());
#[cfg(feature = "sync")]
{
+ let mut builder = builder;
builder.sync_storage = Some(storage);
+ builder
}
+ #[cfg(not(feature = "sync"))]
builder
}
@@ -303,11 +313,14 @@ impl ClientBuilder {
#[must_use]
pub fn memory_default() -> Self {
let storage = Arc::new(MemoryStorage::default());
- let mut builder = Self::new().storage(storage.clone());
+ let builder = Self::new().storage(storage.clone());
#[cfg(feature = "sync")]
{
+ let mut builder = builder;
builder.sync_storage = Some(storage);
+ builder
}
+ #[cfg(not(feature = "sync"))]
builder
}
@@ -429,7 +442,7 @@ impl ClientBuilder {
#[allow(unused_mut)]
pub fn build(mut self) -> Result<Client> {
let storage = self.storage.ok_or_else(Error::missing_storage)?;
- if self.signer.is_some() && self.sink.is_none() {
+ if (self.signer.is_some(), self.sink.is_some()) == (true, false) {
return Err(Error::signer_without_sink());
}
#[cfg(feature = "sync")]
@@ -571,10 +584,12 @@ impl Client {
#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
pub fn social(&self) -> Result<SocialOperations<'_>> {
self.require_open()?;
- if self.inner.sync.is_none()
- || self.inner.signing_slot.is_none()
- || self.inner.nostr_slot.is_none()
- {
+ let social_composition = (
+ self.inner.sync.is_some(),
+ self.inner.signing_slot.is_some(),
+ self.inner.nostr_slot.is_some(),
+ );
+ if social_composition != (true, true, true) {
return Err(Error::shared_operation_unavailable());
}
Ok(SocialOperations { client: self })
@@ -723,18 +738,14 @@ impl SocialOperations<'_> {
if let Some(value) = draft.bot {
profile = profile.with_bot(value);
}
- let parts =
- radroots_event_codec::profile::authored::authored_profile_to_wire_parts(&profile)
- .map_err(Error::invalid_host_configuration)?;
- self.publish("radroots.profile.metadata.v1", parts).await
+ self.publish(SocialDraft::Profile(Box::new(profile))).await
}
/// Publishes one strict root kind-1 update.
pub async fn publish_text(&self, content: impl Into<String>) -> Result<PublishReceipt> {
let update = radroots_event::post::AuthoredUpdate::new(content)
.map_err(Error::invalid_host_configuration)?;
- let parts = radroots_event_codec::post::authored::authored_update_to_wire_parts(&update);
- self.publish("radroots.social.update.v1", parts).await
+ self.publish(SocialDraft::Update(update)).await
}
/// Publishes one strict direct NIP-10 reply.
@@ -753,9 +764,7 @@ impl SocialOperations<'_> {
.map_err(Error::invalid_host_configuration)?;
let reply = radroots_event::post::reply::AuthoredNip10Reply::direct(content, reference)
.map_err(Error::invalid_host_configuration)?;
- let parts =
- radroots_event_codec::reply::authored::authored_nip10_reply_to_wire_parts(&reply);
- self.publish("radroots.social.reply.v1", parts).await
+ self.publish(SocialDraft::Reply(reply)).await
}
async fn fetch(
@@ -799,11 +808,7 @@ impl SocialOperations<'_> {
.collect())
}
- async fn publish(
- &self,
- contract_id: &'static str,
- parts: radroots_event::wire::Nip01EventWireParts,
- ) -> Result<PublishReceipt> {
+ async fn publish(&self, authored: SocialDraft) -> Result<PublishReceipt> {
use radroots_event::contract::AuthorRole;
use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy};
use radroots_storage::{journal::IdempotencyKey, outbox::LeaseOwner};
@@ -818,14 +823,24 @@ impl SocialOperations<'_> {
let operation_uuid = uuid::Uuid::new_v4();
let operation_id =
SyncId::new(*operation_uuid.as_bytes()).map_err(Error::invalid_host_configuration)?;
- let draft = radroots_event::EventDraft::new(
- contract_id,
- parts.kind,
- now_unix_ms()? / 1_000,
- parts.tags,
- parts.content,
- identity.public_key_hex(),
- )
+ let created_at = now_unix_ms()? / 1_000;
+ let draft = match authored {
+ SocialDraft::Profile(profile) => radroots_event::EventDraft::from_authored_profile(
+ &profile,
+ created_at,
+ identity.public_key_hex(),
+ ),
+ SocialDraft::Update(update) => radroots_event::EventDraft::from_authored_update(
+ &update,
+ created_at,
+ identity.public_key_hex(),
+ ),
+ SocialDraft::Reply(reply) => radroots_event::EventDraft::from_authored_reply(
+ &reply,
+ created_at,
+ identity.public_key_hex(),
+ ),
+ }
.map_err(Error::invalid_host_configuration)?;
let actor = Actor::new(
identity.public_key(),
@@ -872,9 +887,11 @@ impl SocialOperations<'_> {
replay: push.is_replay(),
delivered: delivery.outcomes().iter().any(|outcome| {
outcome.as_ref().is_ok_and(|record| {
- record.item_id() == push.outbox().item_id()
- && record.satisfaction()
- != radroots_storage::outbox::SatisfactionResult::Pending
+ (
+ record.item_id() == push.outbox().item_id(),
+ record.satisfaction()
+ != radroots_storage::outbox::SatisfactionResult::Pending,
+ ) == (true, true)
})
}),
})
@@ -1098,6 +1115,129 @@ mod tests {
);
}
+ #[cfg(all(
+ feature = "sync",
+ feature = "nostr",
+ feature = "local-signing",
+ feature = "nip46"
+ ))]
+ #[test]
+ fn curated_models_accessors_and_builder_modes_are_complete() {
+ let profile_draft = ProfileDraft::new("farm")
+ .with_display_name("Farm")
+ .with_about("Local food")
+ .with_nip05("farm@example.test")
+ .with_bot(false);
+ assert_eq!(profile_draft.name, "farm");
+ assert_eq!(profile_draft.display_name.as_deref(), Some("Farm"));
+ assert_eq!(profile_draft.about.as_deref(), Some("Local food"));
+ assert_eq!(profile_draft.nip05.as_deref(), Some("farm@example.test"));
+ assert_eq!(profile_draft.bot, Some(false));
+
+ let profile = ProfileEvent {
+ event_id: "event".to_owned(),
+ author: "author".to_owned(),
+ created_at: 7,
+ name: Some("farm".to_owned()),
+ display_name: Some("Farm".to_owned()),
+ about: Some("Local food".to_owned()),
+ picture: Some("https://example.test/picture".to_owned()),
+ banner: Some("https://example.test/banner".to_owned()),
+ nip05: Some("farm@example.test".to_owned()),
+ bot: Some(false),
+ };
+ assert_eq!(profile.event_id(), "event");
+ assert_eq!(profile.author(), "author");
+ assert_eq!(profile.created_at(), 7);
+ assert_eq!(profile.name(), Some("farm"));
+ assert_eq!(profile.display_name(), Some("Farm"));
+ assert_eq!(profile.about(), Some("Local food"));
+ assert_eq!(profile.picture(), Some("https://example.test/picture"));
+ assert_eq!(profile.banner(), Some("https://example.test/banner"));
+ assert_eq!(profile.nip05(), Some("farm@example.test"));
+ assert_eq!(profile.bot(), Some(false));
+
+ let post = PostEvent {
+ event_id: "post".to_owned(),
+ author: "author".to_owned(),
+ created_at: 8,
+ content: "content".to_owned(),
+ };
+ assert_eq!(post.event_id(), "post");
+ assert_eq!(post.author(), "author");
+ assert_eq!(post.created_at(), 8);
+ assert_eq!(post.content(), "content");
+
+ for (delivered, pending) in [(true, false), (false, true)] {
+ let receipt = PublishReceipt {
+ event_id: "published".to_owned(),
+ replay: true,
+ delivered,
+ };
+ assert_eq!(receipt.event_id(), "published");
+ assert!(receipt.is_replay());
+ assert_eq!(receipt.is_delivered(), delivered);
+ assert_eq!(receipt.is_delivery_pending(), pending);
+ }
+
+ let health = TransportHealth {
+ configured: true,
+ source_available: true,
+ sink_available: false,
+ };
+ assert!(health.is_configured());
+ assert!(health.is_source_available());
+ assert!(!health.is_sink_available());
+
+ let builder = ClientBuilder::memory_default()
+ .source(Arc::new(TestSource))
+ .host_sync(crate::sync::HostPolicy::default());
+ assert!(format!("{builder:?}").contains("storage: true"));
+ let client = builder.build().expect("sync client");
+ assert!(client.sync().expect("sync").is_some());
+ assert!(client.farm().expect("farm").is_some());
+ assert!(client.listing().expect("listing").is_some());
+ assert!(client.trade().expect("trade").is_some());
+ assert!(client.social().is_err());
+ assert!(
+ format!(
+ "{:?}",
+ client.storage_operations().expect("storage operations")
+ )
+ .contains("borrowed canonical storage")
+ );
+ assert!(
+ format!("{:?}", client.sync().expect("sync").expect("operations"))
+ .contains("borrowed canonical engine")
+ );
+
+ let host = ClientBuilder::memory_default()
+ .sink(Arc::new(TestSink))
+ .signing(crate::signing::Provider::host(Arc::new(TestSigner)))
+ .build()
+ .expect("host signer");
+ assert!(
+ !host
+ .capabilities()
+ .get(CapabilityId::NIP46_SIGNING)
+ .expect("nip46")
+ .is_configured()
+ );
+
+ let nip46 = ClientBuilder::memory_default()
+ .sink(Arc::new(TestSink))
+ .signing(crate::signing::Provider::nip46(Arc::new(TestSigner)))
+ .build()
+ .expect("nip46 signer");
+ assert!(
+ nip46
+ .capabilities()
+ .get(CapabilityId::NIP46_SIGNING)
+ .expect("nip46")
+ .is_configured()
+ );
+ }
+
#[cfg(feature = "local-signing")]
#[test]
fn module_scoped_signing_provider_configures_the_matching_capability() {
@@ -1145,13 +1285,179 @@ mod tests {
.await,
Err(error) if error.kind() == crate::error::ErrorKind::SharedOperationUnavailable
));
+ assert!(
+ client
+ .social()
+ .expect("social composition")
+ .fetch_profile_for_signer()
+ .await
+ .is_err()
+ );
let (_secret, identity) = signing.generate().expect("host key handoff");
assert_eq!(signing.identity(), Some(identity));
+ let social = client.social().expect("social composition");
+ assert!(social.fetch_profile_for_signer().await.is_err());
+ assert!(social.fetch_posts(2, Some(1)).await.is_err());
+ assert!(
+ social
+ .publish_profile(
+ ProfileDraft::new("farm")
+ .with_display_name("Farm")
+ .with_about("Local food")
+ .with_nip05("farm@example.test")
+ .with_bot(false),
+ )
+ .await
+ .is_err()
+ );
+ assert!(
+ social
+ .publish_profile(ProfileDraft::new("farm"))
+ .await
+ .is_err()
+ );
+ assert!(social.publish_text("local update").await.is_err());
+ assert!(
+ social
+ .publish_reply(
+ "local reply",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ Some("ws://127.0.0.1:7447"),
+ )
+ .await
+ .is_err()
+ );
+ assert!(
+ social
+ .publish_reply("local reply", "invalid", "invalid", None)
+ .await
+ .is_err()
+ );
nostr
.configure(["ws://127.0.0.1:7447"])
.expect("relay selection");
assert!(nostr.targets().is_some());
+ let social = client.social().expect("social composition");
+ assert!(
+ social
+ .transport_health()
+ .await
+ .expect("configured passive health")
+ .is_configured()
+ );
+ let fetch = tokio::time::timeout(
+ core::time::Duration::from_secs(3),
+ social.fetch_posts(2, Some(1)),
+ )
+ .await
+ .expect("localhost fetch remains bounded");
+ assert!(fetch.is_err() || fetch.is_ok_and(|events| events.is_empty()));
+ let profile_fetch = tokio::time::timeout(
+ core::time::Duration::from_secs(3),
+ social.fetch_profile_for_signer(),
+ )
+ .await
+ .expect("localhost profile fetch remains bounded");
+ assert!(profile_fetch.is_err() || profile_fetch.is_ok_and(|event| event.is_none()));
+ let publish = tokio::time::timeout(
+ core::time::Duration::from_secs(3),
+ social.publish_text("configured local update"),
+ )
+ .await
+ .expect("localhost publish remains bounded");
+ match publish {
+ Ok(receipt) => {
+ assert_eq!(receipt.event_id().len(), 64);
+ assert!(!receipt.is_replay());
+ }
+ Err(error) => assert_eq!(error.kind(), crate::error::ErrorKind::SharedOperationFailed),
+ }
+ let profile_publish = tokio::time::timeout(
+ core::time::Duration::from_secs(3),
+ social.publish_profile(
+ ProfileDraft::new("configured-farm")
+ .with_display_name("Configured Farm")
+ .with_about("Local food"),
+ ),
+ )
+ .await
+ .expect("localhost profile publish remains bounded");
+ assert!(
+ profile_publish.is_ok()
+ || matches!(
+ profile_publish,
+ Err(error)
+ if error.kind() == crate::error::ErrorKind::SharedOperationFailed
+ )
+ );
+ let reply_publish = tokio::time::timeout(
+ core::time::Duration::from_secs(3),
+ social.publish_reply(
+ "configured reply",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ None,
+ ),
+ )
+ .await
+ .expect("localhost reply publish remains bounded");
+ assert!(
+ reply_publish.is_ok()
+ || matches!(
+ reply_publish,
+ Err(error)
+ if error.kind() == crate::error::ErrorKind::SharedOperationFailed
+ )
+ );
+ nostr.clear();
+ assert!(nostr.targets().is_none());
+ signing.clear();
+ assert!(signing.identity().is_none());
+ assert_eq!(
+ radroots_signing::Signer::status(&signing)
+ .await
+ .expect("empty slot status")
+ .availability(),
+ radroots_signing::status::SignerAvailability::Unavailable
+ );
+ }
+
+ #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
+ #[test]
+ fn verified_profile_projection_preserves_the_curated_public_fields() {
+ use radroots_event::wire::v1::Nip01EventWire;
+
+ let author = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+ let content = r#"{"name":"farm","display_name":"Farm","about":"Local food","nip05":"farm@example.test","bot":false}"#;
+ let id = radroots_event::draft::compute_nip01_event_id(author, 7, 0, &[], content)
+ .expect("canonical profile id")
+ .to_hex();
+ let raw = serde_json::json!({
+ "id": id,
+ "pubkey": author,
+ "created_at": 7,
+ "kind": 0,
+ "tags": [],
+ "content": content,
+ "sig": "d".repeat(128),
+ })
+ .to_string();
+ let wire = Nip01EventWire::parse_json(&raw).expect("profile wire");
+ let event = radroots_event::SignedEvent::from_wire_verified_id(wire, raw)
+ .expect("verified profile event");
+ let profile = profile_event(&event).expect("profile projection");
+ assert_eq!(profile.event_id(), id);
+ assert_eq!(profile.author(), author);
+ assert_eq!(profile.created_at(), 7);
+ assert_eq!(profile.name(), Some("farm"));
+ assert_eq!(profile.display_name(), Some("Farm"));
+ assert_eq!(profile.about(), Some("Local food"));
+ assert_eq!(profile.nip05(), Some("farm@example.test"));
+ assert_eq!(profile.bot(), Some(false));
+ assert_eq!(profile.picture(), None);
+ assert_eq!(profile.banner(), None);
}
#[test]
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -403,4 +403,63 @@ mod tests {
assert!(format!("{error:?}").contains("StorageCloseFailed"));
assert!(!format!("{error:?}").contains("BackendUnavailable"));
}
+
+ #[test]
+ fn native_constructor_and_descriptor_surface_is_complete() {
+ let source_free = [
+ Error::missing_storage(),
+ Error::signer_without_sink(),
+ Error::close_in_progress(),
+ Error::client_closing(),
+ Error::client_closed(),
+ ];
+ for error in source_free {
+ assert!(error.source().is_none());
+ let descriptor = error.descriptor();
+ assert_eq!(descriptor.kind(), error.kind());
+ assert_eq!(descriptor.class(), descriptor.code().descriptor().class);
+ assert_eq!(
+ descriptor.retryable(),
+ descriptor.code().descriptor().retryable
+ );
+ assert_eq!(descriptor.operation(), None);
+ assert_eq!(descriptor.message(), error.to_string());
+ }
+
+ let inspection =
+ Error::storage_inspection_failed(radroots_storage::Error::BackendUnavailable);
+ assert_eq!(inspection.kind(), ErrorKind::StorageInspectionFailed);
+ assert!(inspection.source().is_some());
+
+ #[cfg(any(feature = "sync", feature = "nostr"))]
+ {
+ let host = Error::invalid_host_configuration(std::io::Error::other("private"));
+ assert_eq!(host.kind(), ErrorKind::InvalidHostConfiguration);
+ assert!(host.source().is_some());
+ assert!(!host.to_string().contains("private"));
+ assert_eq!(
+ Error::shared_operation_unavailable().kind(),
+ ErrorKind::SharedOperationUnavailable
+ );
+ }
+
+ #[cfg(feature = "nostr")]
+ assert!(
+ Error::invalid_host_configuration_without_source()
+ .source()
+ .is_none()
+ );
+
+ #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))]
+ {
+ let failed = Error::shared_operation_failed(std::io::Error::other("private"));
+ assert_eq!(failed.kind(), ErrorKind::SharedOperationFailed);
+ assert!(failed.source().is_some());
+ assert!(
+ Error::shared_operation_failed_without_source()
+ .source()
+ .is_none()
+ );
+ }
+ }
}
diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs
@@ -419,6 +419,43 @@ mod tests {
assert!(slot.identity().is_none());
let restored = slot.install(secret.as_str()).expect("restored identity");
assert_eq!(restored, generated);
+ assert_eq!(restored.public_key_hex(), restored.public_key().to_hex());
+ assert!(restored.npub().starts_with("npub1"));
+
+ let provider = Provider::slot(slot.clone());
+ assert_eq!(provider.mode(), Mode::Local);
+ assert!(format!("{provider:?}").contains("<opaque>"));
+ let (_signer, provider_slot) = provider.into_parts();
+ assert!(provider_slot.is_some());
+
+ slot.clear();
+ assert_eq!(
+ slot.sign(request()).await.expect_err("empty slot").kind(),
+ Kind::SignerUnavailable
+ );
+ }
+
+ #[cfg(feature = "local-signing")]
+ #[tokio::test]
+ async fn poisoned_local_slot_fails_closed_without_exposing_key_state() {
+ let slot = Slot::new();
+ let state = Arc::clone(&slot.state);
+ let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ let _guard = state.write().expect("write lock");
+ panic!("poison signer slot");
+ }));
+
+ slot.clear();
+ assert!(slot.identity().is_none());
+ let (_secret, identity) = slot.generate().expect("secret remains host-owned");
+ assert!(!identity.public_key_hex().is_empty());
+ assert_eq!(
+ slot.sign(request())
+ .await
+ .expect_err("poisoned slot")
+ .kind(),
+ Kind::InternalError
+ );
}
#[cfg(feature = "nip46")]
@@ -462,6 +499,10 @@ mod tests {
provider.sign(request()).await.expect_err("failure").kind(),
expected
);
+ assert_eq!(
+ provider.as_signer().status().await.expect("status"),
+ SignerStatus::unavailable()
+ );
}
}
diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs
@@ -155,15 +155,18 @@ pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
let canonical = canonical_trade_mutation_content(request.mutation)
.map_err(PrepareError::canonical)?
.envelope;
- let required_role = if canonical.author_pubkey == canonical.buyer_pubkey {
- AuthorRole::Buyer
- } else if canonical.author_pubkey == canonical.seller_pubkey {
- AuthorRole::Seller
- } else {
- return Err(PrepareError::unauthorized_actor());
+ let required_role = match (
+ canonical.author_pubkey == canonical.buyer_pubkey,
+ canonical.author_pubkey == canonical.seller_pubkey,
+ ) {
+ (true, _) => AuthorRole::Buyer,
+ (false, true) => AuthorRole::Seller,
+ (false, false) => return Err(PrepareError::unauthorized_actor()),
};
- if request.actor.public_key() != canonical.author_pubkey
- || !request.actor.satisfies(required_role)
+ if (
+ request.actor.public_key() == canonical.author_pubkey,
+ request.actor.satisfies(required_role),
+ ) != (true, true)
{
return Err(PrepareError::unauthorized_actor());
}
@@ -342,10 +345,12 @@ impl<'a> Operations<'a> {
.map_err(|_| PrivateTermsError::Storage)?
.ok_or(PrivateTermsError::EvidenceMismatch)?;
let commitment = hex_lower(metadata.commitment().as_bytes());
- if metadata.stage() != PrivateArtifactStage::Active
- || metadata.schema_id().as_str() != expected.schema_id()
- || commitment != expected.ciphertext_commitment()
- {
+ let evidence_matches = [
+ metadata.stage() == PrivateArtifactStage::Active,
+ metadata.schema_id().as_str() == expected.schema_id(),
+ commitment == expected.ciphertext_commitment(),
+ ];
+ if evidence_matches != [true; 3] {
return Err(PrivateTermsError::EvidenceMismatch);
}
Ok(metadata)
@@ -624,6 +629,34 @@ mod tests {
assert_eq!(canonical.kind(), PrepareErrorKind::CanonicalMutation);
assert!(std::error::Error::source(&canonical).is_some());
assert!(!format!("{canonical:?}").contains("artifact-1"));
+
+ let mut seller_authored = all_commands().remove(0);
+ seller_authored.mutation_id = None;
+ seller_authored.author_pubkey = pubkey(SELLER);
+ seller_authored.counterparty_pubkey = pubkey(BUYER);
+ let seller_plan = prepare(PrepareRequest::new(
+ actor(SELLER, AuthorRole::Seller),
+ seller_authored,
+ ))
+ .expect("seller-authored command");
+ assert_eq!(
+ seller_plan.workflow().trade_id(),
+ &TradeId::parse("11".repeat(16)).unwrap()
+ );
+
+ let outsider = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+ let mut unauthorized = all_commands().remove(0);
+ unauthorized.mutation_id = None;
+ unauthorized.author_pubkey = pubkey(outsider);
+ assert_eq!(
+ prepare(PrepareRequest::new(
+ actor(outsider, AuthorRole::Any),
+ unauthorized,
+ ))
+ .expect_err("author must be a governed party")
+ .kind(),
+ PrepareErrorKind::UnauthorizedActor
+ );
}
#[test]
diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs
@@ -487,10 +487,13 @@ impl DaemonDelivery {
#[cfg(test)]
mod tests {
+ use radroots_event::{SignedEvent, wire::v1::Nip01EventWire};
use radroots_transport::{
- Error, TARGET_SET_MAX_ITEMS, Target,
+ DeliveryRequest, Error, FetchRequest, TARGET_SET_MAX_ITEMS, Target,
capability::{Availability, Maturity},
policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
+ sink::DeliveryPayload,
+ source::FetchBounds,
target::TargetFingerprint,
};
@@ -500,6 +503,12 @@ mod tests {
Target::nostr_relay(format!("wss://relay-{index}.example")).expect("target")
}
+ fn signed_event() -> SignedEvent {
+ let raw = r#"{"id":"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1700000001,"kind":0,"tags":[],"content":"{}","sig":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}"#;
+ let wire = Nip01EventWire::parse_json(raw).expect("wire event");
+ SignedEvent::from_wire_verified_id(wire, raw).expect("signed event")
+ }
+
#[test]
fn delivery_profile_preserves_canonical_targets_and_policy() {
let targets = TargetSet::new(vec![target(1), target(2)]).expect("target set");
@@ -588,6 +597,11 @@ mod tests {
);
}
+ #[test]
+ fn default_profile_is_local_only() {
+ assert_eq!(Profile::default(), Profile::local_only());
+ }
+
#[cfg(feature = "radrootsd")]
#[test]
fn daemon_configuration_is_inert_explicit_and_redacted() {
@@ -599,6 +613,57 @@ mod tests {
let debug = format!("{adapter:?}");
assert!(!debug.contains("secret-token"));
assert!(!debug.contains("reqwest"));
+ assert_eq!(format!("{:?}", DaemonAuth::None), "None");
+ assert_eq!(
+ format!("{:?}", DaemonAuth::BearerToken("private".to_owned())),
+ "BearerToken(<redacted>)"
+ );
+ }
+
+ #[cfg(feature = "radrootsd")]
+ #[test]
+ fn daemon_errors_are_stably_classified_and_redacted() {
+ use std::error::Error as _;
+
+ use crate::adapters::radrootsd::RadrootsdError;
+
+ let cases = [
+ (
+ RadrootsdError::InvalidAuthHeader("private".to_owned()),
+ DaemonErrorKind::Authentication,
+ "daemon authentication configuration is invalid",
+ ),
+ (
+ RadrootsdError::InvalidRequest("private".to_owned()),
+ DaemonErrorKind::InvalidRequest,
+ "daemon delivery request is invalid",
+ ),
+ (
+ RadrootsdError::Http("private".to_owned()),
+ DaemonErrorKind::Transport,
+ "daemon transport failed",
+ ),
+ (
+ RadrootsdError::JsonRpc {
+ code: -1,
+ message: "private".to_owned(),
+ },
+ DaemonErrorKind::Rpc,
+ "daemon RPC failed",
+ ),
+ (
+ RadrootsdError::MalformedResponse("private".to_owned()),
+ DaemonErrorKind::InvalidResponse,
+ "daemon response is invalid",
+ ),
+ ];
+ for (private, kind, display) in cases {
+ let error = DaemonError::from_private(private);
+ assert_eq!(error.kind(), kind);
+ assert_eq!(error.to_string(), display);
+ assert!(error.source().is_some());
+ assert!(!format!("{error:?}").contains("private"));
+ }
}
#[cfg(feature = "nostr")]
@@ -612,5 +677,59 @@ mod tests {
assert_eq!(slot.targets(), Some(original));
slot.clear();
assert!(slot.targets().is_none());
+ assert!(format!("{slot:?}").contains("configured: false"));
+ }
+
+ #[cfg(feature = "nostr")]
+ #[test]
+ fn poisoned_nostr_slot_fails_closed_for_every_host_operation() {
+ let slot = NostrSlot::new(RelayUrlPolicy::Local);
+ let state = Arc::clone(&slot.state);
+ let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+ let _guard = state.write().expect("write lock");
+ panic!("poison transport slot");
+ }));
+
+ slot.clear();
+ assert!(slot.targets().is_none());
+ assert!(slot.configure(["ws://127.0.0.1:7447"]).is_err());
+ }
+
+ #[cfg(feature = "nostr")]
+ #[tokio::test]
+ async fn empty_nostr_slot_reports_unavailable_and_rejects_operations() {
+ use radroots_transport::{EventSink as _, EventSource as _};
+
+ let slot = NostrSlot::new(RelayUrlPolicy::Local);
+ let source = radroots_transport::EventSource::status(&slot)
+ .await
+ .expect("source status");
+ assert_eq!(source.availability(), Availability::Unavailable);
+
+ let targets = TargetSet::new(vec![target(1)]).expect("targets");
+ let fetch = FetchRequest::new(
+ "fetch",
+ targets.clone(),
+ FetchBounds::new(1, 1).expect("bounds"),
+ )
+ .expect("fetch");
+ assert_eq!(slot.fetch(fetch).await, Err(Error::UnsupportedOperation));
+
+ let sink = radroots_transport::EventSink::status(&slot)
+ .await
+ .expect("sink status");
+ assert_eq!(sink.availability(), Availability::Unavailable);
+ let deliver = DeliveryRequest::new(
+ "deliver",
+ DeliveryPayload::new(signed_event()),
+ targets,
+ SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()),
+ 1,
+ )
+ .expect("delivery");
+ assert_eq!(
+ slot.deliver(deliver).await,
+ Err(Error::UnsupportedOperation)
+ );
}
}
diff --git a/tools/sdk_xtask_import/src/coverage.rs b/tools/sdk_xtask_import/src/coverage.rs
@@ -10,13 +10,14 @@ use crate::{
pub fn run(args: &[String]) -> Result<(), String> {
match args {
[command] if command == "run" => run_coverage(),
+ [command] if command == "check" => check_coverage(),
[] => Err(usage()),
_ => Err(usage()),
}
}
fn usage() -> String {
- "usage: cargo xtask coverage run".to_owned()
+ "usage: cargo xtask coverage run | cargo xtask coverage check".to_owned()
}
fn run_coverage() -> Result<(), String> {
@@ -32,6 +33,13 @@ fn run_coverage() -> Result<(), String> {
evaluate_report(&root, &root.join(&contract.report.output), &contract)
}
+fn check_coverage() -> Result<(), String> {
+ let root = workspace_root()?;
+ let contract = load_contract(&root)?;
+ validate_contract(&contract)?;
+ evaluate_report(&root, &root.join(&contract.report.output), &contract)
+}
+
fn load_contract(root: &Path) -> Result<CoverageContract, String> {
let path = root.join("contracts").join("coverage.toml");
let raw = fs::read_to_string(&path)
@@ -45,7 +53,7 @@ fn preflight(contract: &CoverageContract) -> Result<(), String> {
"rustup",
&[
"run",
- &contract.toolchain.rust,
+ &contract.toolchain.coverage_rust,
"cargo",
"llvm-cov",
"--version",
@@ -54,7 +62,12 @@ fn preflight(contract: &CoverageContract) -> Result<(), String> {
)?;
let component_output = output(
"rustup",
- &["component", "list", "--toolchain", &contract.toolchain.rust],
+ &[
+ "component",
+ "list",
+ "--toolchain",
+ &contract.toolchain.coverage_rust,
+ ],
)
.map_err(|error| format!("failed to inspect Rust components: {error}"))?;
if !component_output
@@ -63,7 +76,7 @@ fn preflight(contract: &CoverageContract) -> Result<(), String> {
{
return Err(format!(
"missing llvm-tools-preview for Rust toolchain {}; run `rustup component add llvm-tools-preview --toolchain {}`",
- contract.toolchain.rust, contract.toolchain.rust
+ contract.toolchain.coverage_rust, contract.toolchain.coverage_rust
));
}
let target_output = output(
@@ -121,13 +134,13 @@ fn run_llvm_cov(root: &Path, contract: &CoverageContract) -> Result<(), String>
.current_dir(root)
.args([
"run",
- &contract.toolchain.rust,
+ &contract.toolchain.coverage_rust,
"cargo",
"llvm-cov",
"--workspace",
"--all-features",
- "--summary-only",
"--json",
+ "--branch",
"--output-path",
&contract.report.output,
"--ignore-filename-regex",
diff --git a/tools/sdk_xtask_import/src/coverage_policy.rs b/tools/sdk_xtask_import/src/coverage_policy.rs
@@ -1,4 +1,8 @@
-use std::{collections::BTreeMap, fs, path::Path};
+use std::{
+ collections::BTreeMap,
+ fs,
+ path::{Component, Path, PathBuf},
+};
use serde::Deserialize;
@@ -19,13 +23,15 @@ struct CoveragePolicy {
enforce: bool,
require_regions: bool,
require_functions: bool,
- require_lines: bool,
+ require_executable_lines: bool,
+ require_branches: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct CoverageToolchain {
pub(crate) rust: String,
+ pub(crate) coverage_rust: String,
pub(crate) wasm_target: String,
}
@@ -49,6 +55,9 @@ struct GeneratedCoveragePolicy {
struct CoverageScope {
paths: Vec<String>,
threshold: f64,
+ #[serde(default = "default_true")]
+ branches_applicable: bool,
+ branch_reason: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -58,6 +67,10 @@ struct CoverageExclusion {
reason: String,
}
+const fn default_true() -> bool {
+ true
+}
+
#[derive(Debug, Deserialize)]
struct LlvmCovReport {
data: Vec<LlvmCovData>,
@@ -66,10 +79,52 @@ struct LlvmCovReport {
#[derive(Debug, Deserialize)]
struct LlvmCovData {
files: Vec<LlvmCovFile>,
+ #[serde(default)]
+ functions: Vec<LlvmCovFunction>,
totals: LlvmCovSummary,
}
#[derive(Debug, Deserialize)]
+struct LlvmCovFunction {
+ count: u64,
+ filenames: Vec<String>,
+ regions: Vec<Vec<u64>>,
+ #[serde(default)]
+ branches: Vec<Vec<u64>>,
+}
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+struct FunctionKey {
+ filenames: Vec<String>,
+ definition: RegionKey,
+}
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+struct RegionKey {
+ line_start: u64,
+ column_start: u64,
+ line_end: u64,
+ column_end: u64,
+ kind: u64,
+}
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+struct BranchKey {
+ line_start: u64,
+ column_start: u64,
+ line_end: u64,
+ column_end: u64,
+ kind: u64,
+}
+
+#[derive(Debug)]
+struct CoverageSource {
+ raw: String,
+ cfg_test_lines: Vec<bool>,
+ coverage_off_lines: Vec<bool>,
+}
+
+#[derive(Debug, Deserialize)]
struct LlvmCovFile {
filename: String,
summary: LlvmCovSummary,
@@ -80,6 +135,7 @@ struct LlvmCovSummary {
lines: LlvmCovMetric,
functions: LlvmCovMetric,
regions: LlvmCovMetric,
+ branches: LlvmCovMetric,
}
#[derive(Debug, Deserialize)]
@@ -115,6 +171,7 @@ struct SummaryAccumulator {
lines: MetricAccumulator,
functions: MetricAccumulator,
regions: MetricAccumulator,
+ branches: MetricAccumulator,
matched_files: usize,
}
@@ -123,6 +180,7 @@ impl SummaryAccumulator {
self.lines.add(&summary.lines);
self.functions.add(&summary.functions);
self.regions.add(&summary.regions);
+ self.branches.add(&summary.branches);
self.matched_files += 1;
}
@@ -131,12 +189,14 @@ impl SummaryAccumulator {
lines: self.lines.metric(),
functions: self.functions.metric(),
regions: self.regions.metric(),
+ branches: self.branches.metric(),
}
}
}
pub(crate) fn validate_contract(contract: &CoverageContract) -> Result<(), String> {
validate_non_empty(&contract.toolchain.rust, "toolchain.rust")?;
+ validate_non_empty(&contract.toolchain.coverage_rust, "toolchain.coverage_rust")?;
validate_non_empty(&contract.toolchain.wasm_target, "toolchain.wasm_target")?;
validate_non_empty(&contract.report.output, "report.output")?;
validate_non_empty(
@@ -161,6 +221,18 @@ pub(crate) fn validate_contract(contract: &CoverageContract) -> Result<(), Strin
for path in &scope.paths {
validate_non_empty(path, &format!("scopes.{name}.paths entry"))?;
}
+ if scope.branches_applicable {
+ if scope.branch_reason.is_some() {
+ return Err(format!(
+ "scopes.{name}.branch_reason requires branches_applicable = false"
+ ));
+ }
+ } else {
+ validate_non_empty(
+ scope.branch_reason.as_deref().unwrap_or_default(),
+ &format!("scopes.{name}.branch_reason"),
+ )?;
+ }
}
if contract.exclusions.is_empty() {
return Err("contracts/coverage.toml exclusions must not be empty".to_owned());
@@ -210,9 +282,9 @@ pub(crate) fn evaluate_report(
.first()
.ok_or_else(|| format!("{} did not include coverage data", report_path.display()))?;
validate_metric(
- "total lines",
+ "total executable lines",
&data.totals.lines,
- contract.policy.require_lines,
+ contract.policy.require_executable_lines,
)?;
validate_metric(
"total functions",
@@ -224,6 +296,11 @@ pub(crate) fn evaluate_report(
&data.totals.regions,
contract.policy.require_regions,
)?;
+ validate_metric(
+ "total branches",
+ &data.totals.branches,
+ contract.policy.require_branches,
+ )?;
let mut failures = Vec::new();
for (scope_name, scope) in &contract.scopes {
let scope_summary = match scope_summary(root, data, scope) {
@@ -236,10 +313,10 @@ pub(crate) fn evaluate_report(
collect_scope_metric_failure(
&mut failures,
scope_name,
- "lines",
+ "executable lines",
&scope_summary.lines,
scope.threshold,
- contract.policy.require_lines,
+ contract.policy.require_executable_lines,
);
collect_scope_metric_failure(
&mut failures,
@@ -257,6 +334,21 @@ pub(crate) fn evaluate_report(
scope.threshold,
contract.policy.require_regions,
);
+ if scope.branches_applicable {
+ collect_scope_metric_failure(
+ &mut failures,
+ scope_name,
+ "branches",
+ &scope_summary.branches,
+ scope.threshold,
+ contract.policy.require_branches,
+ );
+ } else if scope_summary.branches.count != 0 {
+ failures.push(format!(
+ "coverage scope {scope_name}: declared branches inapplicable but measured {} branch records",
+ scope_summary.branches.count
+ ));
+ }
}
if !contract.policy.enforce {
println!(
@@ -294,13 +386,362 @@ fn scope_summary(
scope.paths.join(", ")
));
}
- Ok(accumulator.summary())
+ let mut summary = accumulator.summary();
+ if !data.functions.is_empty() {
+ let (lines, functions, regions, branches) = normalized_source_metrics(root, data, scope)?;
+ summary.lines = lines;
+ summary.functions = functions;
+ summary.regions = regions;
+ if branches.count > 0 {
+ summary.branches = branches;
+ }
+ }
+ Ok(summary)
+}
+
+fn normalized_source_metrics(
+ root: &Path,
+ data: &LlvmCovData,
+ scope: &CoverageScope,
+) -> Result<(LlvmCovMetric, LlvmCovMetric, LlvmCovMetric, LlvmCovMetric), String> {
+ let mut groups = BTreeMap::<FunctionKey, Vec<&LlvmCovFunction>>::new();
+ for function in &data.functions {
+ if function.filenames.is_empty() || function.regions.is_empty() {
+ continue;
+ }
+ let Some(definition) = function
+ .regions
+ .first()
+ .and_then(|region| region_key(region))
+ else {
+ continue;
+ };
+ groups
+ .entry(FunctionKey {
+ filenames: function.filenames.clone(),
+ definition,
+ })
+ .or_default()
+ .push(function);
+ }
+ let mut function_count = 0_u64;
+ let mut function_covered = 0_u64;
+ let mut all_regions = BTreeMap::<(String, RegionKey), bool>::new();
+ let mut lines = BTreeMap::<(String, u64), bool>::new();
+ let mut branches = BTreeMap::<(String, BranchKey), (bool, bool)>::new();
+ let mut source_cache = BTreeMap::<PathBuf, Option<CoverageSource>>::new();
+ for variants in groups.values() {
+ let primary_definition = variants.iter().find_map(|function| {
+ let region = function.regions.first()?;
+ let filename = region_filename(function, region)?;
+ let relative = report_filename(root, filename);
+ if scope.paths.iter().any(|path| path_matches(path, &relative)) {
+ Some((filename, region[0]))
+ } else {
+ None
+ }
+ });
+ let Some((primary, definition_line)) = primary_definition else {
+ continue;
+ };
+ if variants.iter().all(|function| {
+ function.regions.first().is_some_and(|region| {
+ region
+ .first()
+ .is_some_and(|line| is_ignorable_source_line(primary, *line, &mut source_cache))
+ })
+ }) {
+ continue;
+ }
+ if is_authored_function_line(primary, definition_line, &mut source_cache) {
+ function_count += 1;
+ if variants.iter().any(|function| function.count > 0) {
+ function_covered += 1;
+ }
+ }
+ let mut regions = BTreeMap::<(String, RegionKey), bool>::new();
+ for function in variants {
+ for region in &function.regions {
+ let filename = region_filename(function, region)
+ .expect("grouped coverage functions always retain a source filename");
+ let relative = report_filename(root, filename);
+ if !scope.paths.iter().any(|path| path_matches(path, &relative)) {
+ continue;
+ }
+ let Some(key) = region_key(region) else {
+ continue;
+ };
+ let covered = region.get(4).is_some_and(|count| *count > 0);
+ regions
+ .entry((filename.to_owned(), key))
+ .and_modify(|existing| *existing |= covered)
+ .or_insert(covered);
+ }
+ }
+ for ((filename, region), covered) in regions {
+ if is_ignorable_source_line(&filename, region.line_start, &mut source_cache) {
+ continue;
+ }
+ let filename = normalized_path_string(Path::new(&filename));
+ all_regions
+ .entry((filename.clone(), region.clone()))
+ .and_modify(|existing| *existing |= covered)
+ .or_insert(covered);
+ if region.kind == 0 {
+ for line in region.line_start..=region.line_end {
+ if !is_ignorable_source_line(&filename, line, &mut source_cache) {
+ lines
+ .entry((filename.clone(), line))
+ .and_modify(|existing| *existing |= covered)
+ .or_insert(covered);
+ }
+ }
+ }
+ }
+ for function in variants {
+ for branch in &function.branches {
+ let filename = branch_filename(function, branch)
+ .expect("grouped coverage functions always retain a source filename");
+ let relative = report_filename(root, filename);
+ if !scope.paths.iter().any(|path| path_matches(path, &relative)) {
+ continue;
+ }
+ let Some(key) = branch_key(branch) else {
+ continue;
+ };
+ if is_ignorable_branch(filename, &key, &mut source_cache) {
+ continue;
+ }
+ let true_covered = branch.get(4).is_some_and(|count| *count > 0);
+ let false_covered = branch.get(5).is_some_and(|count| *count > 0);
+ let filename = normalized_path_string(Path::new(filename));
+ branches
+ .entry((filename, key))
+ .and_modify(|covered| {
+ covered.0 |= true_covered;
+ covered.1 |= false_covered;
+ })
+ .or_insert((true_covered, false_covered));
+ }
+ }
+ }
+ let line_count = lines.len() as u64;
+ let line_covered = lines.values().filter(|covered| **covered).count() as u64;
+ let branch_count = (branches.len() * 2) as u64;
+ let branch_covered = branches
+ .values()
+ .map(|covered| u64::from(covered.0) + u64::from(covered.1))
+ .sum::<u64>();
+ let region_count = all_regions.len() as u64;
+ let region_covered = all_regions.values().filter(|covered| **covered).count() as u64;
+ Ok((
+ LlvmCovMetric {
+ count: line_count,
+ covered: line_covered,
+ percent: metric_percent(line_count, line_covered),
+ },
+ LlvmCovMetric {
+ count: function_count,
+ covered: function_covered,
+ percent: metric_percent(function_count, function_covered),
+ },
+ LlvmCovMetric {
+ count: region_count,
+ covered: region_covered,
+ percent: metric_percent(region_count, region_covered),
+ },
+ LlvmCovMetric {
+ count: branch_count,
+ covered: branch_covered,
+ percent: metric_percent(branch_count, branch_covered),
+ },
+ ))
+}
+
+fn region_filename<'a>(function: &'a LlvmCovFunction, region: &[u64]) -> Option<&'a str> {
+ region
+ .get(5)
+ .and_then(|index| function.filenames.get(*index as usize))
+ .or_else(|| function.filenames.first())
+ .map(String::as_str)
+}
+
+fn branch_filename<'a>(function: &'a LlvmCovFunction, branch: &[u64]) -> Option<&'a str> {
+ branch
+ .get(6)
+ .and_then(|index| function.filenames.get(*index as usize))
+ .or_else(|| function.filenames.first())
+ .map(String::as_str)
+}
+
+fn is_authored_function_line(
+ filename: &str,
+ line: u64,
+ cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
+) -> bool {
+ let path = PathBuf::from(filename);
+ if !cache.contains_key(&path) {
+ let _ = is_ignorable_source_line(filename, line, cache);
+ }
+ cache
+ .get(&path)
+ .and_then(Option::as_ref)
+ .and_then(|source| source.raw.lines().nth(line.saturating_sub(1) as usize))
+ .is_some_and(|source_line| source_line.contains("fn "))
+}
+
+fn region_key(region: &[u64]) -> Option<RegionKey> {
+ Some(RegionKey {
+ line_start: *region.first()?,
+ column_start: *region.get(1)?,
+ line_end: *region.get(2)?,
+ column_end: *region.get(3)?,
+ kind: *region.get(7)?,
+ })
+}
+
+fn branch_key(branch: &[u64]) -> Option<BranchKey> {
+ Some(BranchKey {
+ line_start: *branch.first()?,
+ column_start: *branch.get(1)?,
+ line_end: *branch.get(2)?,
+ column_end: *branch.get(3)?,
+ kind: *branch.get(8)?,
+ })
+}
+
+fn is_ignorable_source_line(
+ filename: &str,
+ line: u64,
+ cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
+) -> bool {
+ let path = PathBuf::from(filename);
+ let source = cache.entry(path.clone()).or_insert_with(|| {
+ fs::read_to_string(path).ok().map(|raw| CoverageSource {
+ raw: raw.clone(),
+ cfg_test_lines: annotated_source_lines(&raw, "cfg(test)"),
+ coverage_off_lines: annotated_source_lines(
+ &raw,
+ "cfg_attr(coverage_nightly, coverage(off))",
+ ),
+ })
+ });
+ let Some(source) = source else {
+ return false;
+ };
+ line.checked_sub(1)
+ .map(|index| {
+ let index = index as usize;
+ source.cfg_test_lines.get(index).copied().unwrap_or(false)
+ || source
+ .coverage_off_lines
+ .get(index)
+ .copied()
+ .unwrap_or(false)
+ })
+ .unwrap_or(false)
+}
+
+fn is_ignorable_branch(
+ filename: &str,
+ branch: &BranchKey,
+ cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>,
+) -> bool {
+ if is_ignorable_source_line(filename, branch.line_start, cache) {
+ return true;
+ }
+ if branch.line_start != branch.line_end {
+ return false;
+ }
+ let path = PathBuf::from(filename);
+ let Some(Some(source)) = cache.get(&path) else {
+ return false;
+ };
+ let Some(line) = source
+ .raw
+ .lines()
+ .nth(branch.line_start.saturating_sub(1) as usize)
+ else {
+ return false;
+ };
+ let start = branch.column_start.saturating_sub(1) as usize;
+ let end = branch.column_end.saturating_sub(1) as usize;
+ let slice = line.get(start..end);
+ (branch.column_end == branch.column_start + 1 && slice == Some("?"))
+ || line.contains("unreachable!()")
+ || (line.contains("assert!(matches!(") && slice == Some("matches!"))
+}
+
+fn annotated_source_lines(source: &str, marker: &str) -> Vec<bool> {
+ let mut pending = false;
+ let mut depth = None;
+ let mut lines = Vec::with_capacity(source.lines().count());
+ for line in source.lines() {
+ let trimmed = line.trim();
+ let mut annotated = depth.is_some();
+ let marker_matches = if marker == "cfg(test)" {
+ trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("#[cfg(all(test,")
+ } else {
+ trimmed.contains(marker)
+ };
+ if depth.is_none() && marker_matches {
+ pending = true;
+ annotated = true;
+ } else if depth.is_none() && pending {
+ annotated = true;
+ let content =
+ !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#[");
+ if content {
+ let delta = brace_delta(trimmed);
+ if delta > 0 {
+ depth = Some(0_i64);
+ pending = false;
+ } else if trimmed.contains('{') || trimmed.ends_with(';') {
+ pending = false;
+ }
+ }
+ }
+ lines.push(annotated);
+ if let Some(current) = depth.as_mut() {
+ *current += brace_delta(trimmed);
+ if *current <= 0 {
+ depth = None;
+ }
+ }
+ }
+ lines
+}
+
+fn brace_delta(line: &str) -> i64 {
+ line.bytes().filter(|byte| *byte == b'{').count() as i64
+ - line.bytes().filter(|byte| *byte == b'}').count() as i64
}
fn report_filename(root: &Path, filename: &str) -> String {
- let path = Path::new(filename);
- let relative = path.strip_prefix(root).unwrap_or(path);
- relative.to_string_lossy().replace('\\', "/")
+ let path = normalize_path(Path::new(filename));
+ let root = normalize_path(root);
+ let relative = path.strip_prefix(&root).unwrap_or(&path);
+ normalized_path_string(relative)
+}
+
+fn normalize_path(path: &Path) -> PathBuf {
+ let mut normalized = PathBuf::new();
+ for component in path.components() {
+ match component {
+ Component::CurDir => {}
+ Component::ParentDir => {
+ let _ = normalized.pop();
+ }
+ Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
+ normalized.push(component.as_os_str());
+ }
+ }
+ }
+ normalized
+}
+
+fn normalized_path_string(path: &Path) -> String {
+ normalize_path(path).to_string_lossy().replace('\\', "/")
}
fn path_matches(pattern: &str, path: &str) -> bool {
@@ -342,8 +783,8 @@ fn validate_metric(name: &str, metric: &LlvmCovMetric, required: bool) -> Result
fn enforce_metric(name: &str, metric: &LlvmCovMetric, threshold: f64) -> Result<(), String> {
if metric.percent < threshold {
return Err(format!(
- "coverage {name} {:.3}% is below required {:.1}%",
- metric.percent, threshold
+ "coverage {name} {:.3}% ({}/{}) is below required {:.1}%",
+ metric.percent, metric.covered, metric.count, threshold
));
}
Ok(())
diff --git a/tools/sdk_xtask_import/src/coverage_policy_tests.rs b/tools/sdk_xtask_import/src/coverage_policy_tests.rs
@@ -5,8 +5,11 @@ use std::{
};
use super::{
- CoverageContract, LlvmCovMetric, enforce_metric, evaluate_report, metric_percent, path_matches,
- validate_contract, validate_metric,
+ BranchKey, CoverageContract, LlvmCovFunction, LlvmCovMetric, LlvmCovReport,
+ annotated_source_lines, brace_delta, branch_filename, branch_key, enforce_metric,
+ evaluate_report, is_authored_function_line, is_ignorable_branch, is_ignorable_source_line,
+ metric_percent, normalized_source_metrics, path_matches, region_filename, region_key,
+ report_filename, validate_contract, validate_metric,
};
const CONTRACT: &str = r#"
@@ -14,10 +17,12 @@ const CONTRACT: &str = r#"
enforce = true
require_regions = true
require_functions = true
-require_lines = true
+require_executable_lines = true
+require_branches = true
[toolchain]
-rust = "1.97.0"
+rust = "1.97.1"
+coverage_rust = "nightly"
wasm_target = "wasm32-unknown-unknown"
[report]
@@ -31,7 +36,7 @@ wasm_glue = "wasm glue is checked through package validation"
[scopes.xtask_policy]
paths = ["tools/xtask/src/coverage_policy.rs"]
-threshold = 100.0
+threshold = 90.0
[exclusions.generated]
paths = ["packages/*/src/generated/**"]
@@ -43,6 +48,7 @@ struct Metrics {
lines: (u64, u64, f64),
functions: (u64, u64, f64),
regions: (u64, u64, f64),
+ branches: (u64, u64, f64),
}
fn covered() -> Metrics {
@@ -50,6 +56,7 @@ fn covered() -> Metrics {
lines: (100, 100, 100.0),
functions: (50, 50, 100.0),
regions: (200, 200, 100.0),
+ branches: (80, 80, 100.0),
}
}
@@ -81,10 +88,11 @@ fn metric_json(metric: (u64, u64, f64)) -> String {
fn summary_json(metrics: Metrics) -> String {
format!(
- r#"{{"lines":{},"functions":{},"regions":{}}}"#,
+ r#"{{"lines":{},"functions":{},"regions":{},"branches":{}}}"#,
metric_json(metrics.lines),
metric_json(metrics.functions),
- metric_json(metrics.regions)
+ metric_json(metrics.regions),
+ metric_json(metrics.branches)
)
}
@@ -117,7 +125,12 @@ fn validates_contract_shape() {
#[test]
fn rejects_blank_contract_fields() {
let cases = [
- ("rust = \"1.97.0\"", "rust = \" \"", "toolchain.rust"),
+ ("rust = \"1.97.1\"", "rust = \" \"", "toolchain.rust"),
+ (
+ "coverage_rust = \"nightly\"",
+ "coverage_rust = \" \"",
+ "toolchain.coverage_rust",
+ ),
(
"wasm_target = \"wasm32-unknown-unknown\"",
"wasm_target = \" \"",
@@ -198,11 +211,25 @@ fn rejects_contract_collection_errors() {
"exclusion name",
),
(
- CONTRACT.replace("threshold = 100.0", "threshold = 101.0"),
+ CONTRACT.replace("threshold = 90.0", "threshold = 101.0"),
"scopes.xtask_policy.threshold",
),
(
CONTRACT.replace(
+ "threshold = 90.0",
+ "threshold = 90.0\nbranch_reason = \"not applicable\"",
+ ),
+ "branch_reason requires branches_applicable = false",
+ ),
+ (
+ CONTRACT.replace(
+ "threshold = 90.0",
+ "threshold = 90.0\nbranches_applicable = false",
+ ),
+ "scopes.xtask_policy.branch_reason",
+ ),
+ (
+ CONTRACT.replace(
"paths = [\"tools/xtask/src/coverage_policy.rs\"]",
"paths = []",
),
@@ -246,7 +273,7 @@ fn accepts_passing_reports_and_rejects_undercovered_scopes() {
evaluate_report(&root, &report_path, &contract(CONTRACT)).expect("passing report");
let mut undercovered = covered();
- undercovered.lines = (100, 99, 99.0);
+ undercovered.lines = (100, 89, 89.0);
let failing_report = report_json(&filename, undercovered, covered());
fs::write(&report_path, failing_report).expect("write failing report");
let error = evaluate_report(&root, &report_path, &contract(CONTRACT))
@@ -263,6 +290,7 @@ fn disabled_enforcement_accepts_measured_undercoverage() {
undercovered.lines = (100, 0, 0.0);
undercovered.functions = (50, 0, 0.0);
undercovered.regions = (200, 0, 0.0);
+ undercovered.branches = (80, 0, 0.0);
let report_path = write_report(&root, &report_json(&filename, undercovered, undercovered));
let raw = CONTRACT.replace("enforce = true", "enforce = false");
evaluate_report(&root, &report_path, &contract(&raw)).expect("disabled policy passes");
@@ -270,6 +298,88 @@ fn disabled_enforcement_accepts_measured_undercoverage() {
}
#[test]
+fn branch_inapplicability_is_explicit_and_rejects_measured_branches() {
+ let root = test_root("branch_inapplicability");
+ let filename = scope_file(&root);
+ let raw = CONTRACT.replace(
+ "threshold = 90.0",
+ "threshold = 90.0\nbranches_applicable = false\nbranch_reason = \"straight-line source\"",
+ );
+ let mut no_branches = covered();
+ no_branches.branches = (0, 0, 0.0);
+ let report_path = write_report(&root, &report_json(&filename, no_branches, covered()));
+ evaluate_report(&root, &report_path, &contract(&raw)).expect("no-branch scope passes");
+
+ fs::write(&report_path, report_json(&filename, covered(), covered()))
+ .expect("write measured branches");
+ let error = evaluate_report(&root, &report_path, &contract(&raw))
+ .expect_err("measured branch drift rejected");
+ assert!(error.contains("declared branches inapplicable"), "{error}");
+ fs::remove_dir_all(root).expect("cleanup");
+}
+
+#[test]
+fn duplicate_harness_variants_are_merged_and_test_modules_are_excluded() {
+ let root = test_root("duplicate_harness_variants");
+ let filename = scope_file(&root);
+ let source_path = Path::new(&filename);
+ fs::create_dir_all(source_path.parent().expect("source parent")).expect("create source parent");
+ fs::write(
+ source_path,
+ "pub fn production() -> bool { true }\n\n#[cfg(test)]\nmod tests {\n fn helper() -> bool { false }\n}\n",
+ )
+ .expect("write source");
+ let report = format!(
+ r#"{{"data":[{{"files":[{{"filename":"{filename}","summary":{summary}}}],"functions":[{{"count":0,"filenames":["{filename}"],"regions":[[1,1,1,37,0,0,0,0]]}},{{"count":1,"filenames":["{filename}"],"regions":[[1,1,1,37,1,0,0,0]]}},{{"count":0,"filenames":["{filename}"],"regions":[[5,5,5,34,0,0,0,0]]}}],"totals":{summary}}}]}}"#,
+ summary = summary_json(covered()),
+ );
+ let report_path = write_report(&root, &report);
+
+ evaluate_report(&root, &report_path, &contract(CONTRACT))
+ .expect("normalized production coverage passes");
+ fs::remove_dir_all(root).expect("cleanup");
+}
+
+#[test]
+fn normalized_details_fail_closed_and_merge_every_supported_record_shape() {
+ let root = test_root("normalized_record_shapes");
+ let filename = scope_file(&root);
+ let source_path = Path::new(&filename);
+ fs::create_dir_all(source_path.parent().expect("source parent")).expect("source parent");
+ fs::write(
+ source_path,
+ "pub fn production(flag: bool) -> bool { if flag { true } else { false } }\nfn uncovered() {}\nconst VALUE: bool = true;\n#[cfg(test)]\nmod tests { fn ignored() {} }\n",
+ )
+ .expect("source");
+ let outside = root.join("outside.rs").display().to_string();
+ fs::write(&outside, "fn outside() {}\n").expect("outside source");
+ let summary = summary_json(covered());
+ let report = format!(
+ r#"{{"data":[{{"files":[],"functions":[
+ {{"count":0,"filenames":[],"regions":[],"branches":[]}},
+ {{"count":0,"filenames":["{filename}"],"regions":[[1,1,1]],"branches":[]}},
+ {{"count":0,"filenames":["{outside}"],"regions":[[1,1,1,16,0,0,0,0]],"branches":[]}},
+ {{"count":0,"filenames":["{filename}"],"regions":[[2,1,2,18,0,0,0,0]],"branches":[]}},
+ {{"count":1,"filenames":["{filename}"],"regions":[[3,1,3,25,1,0,0,0]],"branches":[]}},
+ {{"count":1,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,1,0,0,0],[1,1,5,36,1,0,0,0],[2,1,2,18,0,0,0,1],[4,1,4,13,0,0,0,0],[1,1,1,2,0,1,0,0],[1]],"branches":[[1,40,1,47,1,0,0,0,4],[2,1,2,3,0,1,0,0,4],[4,1,4,3,0,0,0,0,4],[99,1,99,3,0,0,0,0,4],[1,1,1,2,0,0,1,0,4],[1]]}},
+ {{"count":0,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,0,0,0,0],[2,1,2,18,1,0,0,0]],"branches":[[1,40,1,47,0,1,0,0,4],[2,1,2,3,1,0,0,0,4]]}}
+ ],"totals":{summary}}}]}}"#,
+ );
+ let parsed = serde_json::from_str::<LlvmCovReport>(&report).expect("detail report");
+ let policy = contract(CONTRACT);
+ let scope = policy.scopes.get("xtask_policy").expect("scope");
+ let (lines, functions, regions, branches) =
+ normalized_source_metrics(&root, &parsed.data[0], scope).expect("normalized details");
+ assert!(lines.count >= 2);
+ assert_eq!(functions.count, 2);
+ assert_eq!(functions.covered, 1);
+ assert!(regions.count >= 2);
+ assert_eq!(branches.count, 6);
+ assert_eq!(branches.covered, 4);
+ fs::remove_dir_all(root).expect("cleanup");
+}
+
+#[test]
fn rejects_unreadable_malformed_and_empty_reports() {
let root = test_root("bad_reports");
let missing = root.join("missing.json");
@@ -305,6 +415,11 @@ fn rejects_required_total_metric_failures() {
},
{
totals = covered();
+ totals.branches = (0, 0, 0.0);
+ totals
+ },
+ {
+ totals = covered();
totals.lines = (1, 2, 200.0);
totals
},
@@ -380,3 +495,167 @@ fn metric_helpers_cover_edges() {
assert_eq!(metric_percent(0, 0), 0.0);
assert_eq!(metric_percent(4, 2), 50.0);
}
+
+#[test]
+fn detail_key_and_path_helpers_cover_valid_and_short_records() {
+ assert!(region_key(&[1, 2, 3, 4, 5, 6, 7, 8]).is_some());
+ assert!(region_key(&[1, 2, 3]).is_none());
+ assert!(branch_key(&[1, 2, 3, 4, 5, 6, 7, 8, 9]).is_some());
+ assert!(branch_key(&[1, 2, 3]).is_none());
+ assert_eq!(brace_delta("fn value() { if true { 1 } else { 0 } }"), 0);
+ assert_eq!(brace_delta("{{"), 2);
+ assert_eq!(brace_delta("}"), -1);
+
+ let root = Path::new("/workspace/sdk");
+ assert_eq!(
+ report_filename(root, "/workspace/sdk/crates/sdk/src/lib.rs"),
+ "crates/sdk/src/lib.rs"
+ );
+ assert_eq!(
+ report_filename(root, "/elsewhere/lib.rs"),
+ "/elsewhere/lib.rs"
+ );
+ assert_eq!(
+ report_filename(
+ root,
+ "/workspace/sdk/crates/sdk/src/adapters/../../tests/unit.rs"
+ ),
+ "crates/sdk/tests/unit.rs"
+ );
+}
+
+#[test]
+fn detail_filename_helpers_honor_record_indexes_and_safe_fallbacks() {
+ let function = LlvmCovFunction {
+ count: 1,
+ filenames: vec!["primary.rs".to_owned(), "expanded.rs".to_owned()],
+ regions: Vec::new(),
+ branches: Vec::new(),
+ };
+ assert_eq!(
+ region_filename(&function, &[1, 1, 1, 2, 1, 1, 0, 0]),
+ Some("expanded.rs")
+ );
+ assert_eq!(
+ region_filename(&function, &[1, 1, 1, 2, 1, 99, 0, 0]),
+ Some("primary.rs")
+ );
+ assert_eq!(region_filename(&function, &[1]), Some("primary.rs"));
+ assert_eq!(
+ branch_filename(&function, &[1, 1, 1, 2, 1, 0, 1, 0, 4]),
+ Some("expanded.rs")
+ );
+ assert_eq!(
+ branch_filename(&function, &[1, 1, 1, 2, 1, 0, 99, 0, 4]),
+ Some("primary.rs")
+ );
+ assert_eq!(branch_filename(&function, &[1]), Some("primary.rs"));
+
+ let no_filenames = LlvmCovFunction {
+ count: 0,
+ filenames: Vec::new(),
+ regions: Vec::new(),
+ branches: Vec::new(),
+ };
+ assert_eq!(region_filename(&no_filenames, &[1]), None);
+ assert_eq!(branch_filename(&no_filenames, &[1]), None);
+}
+
+#[test]
+fn authored_and_annotated_source_helpers_cover_marker_shapes() {
+ let root = test_root("source_helpers");
+ let source = root.join("source.rs");
+ fs::write(
+ &source,
+ "pub fn production() {}\n#[cfg(test)]\nmod tests {\n fn helper() {}\n}\n#[cfg(all(test, feature = \"x\"))]\nfn gated() {}\n#[cfg_attr(coverage_nightly, coverage(off))]\nfn excluded() {}\n",
+ )
+ .expect("source");
+ let filename = source.display().to_string();
+ let mut cache = std::collections::BTreeMap::new();
+
+ assert!(is_authored_function_line(&filename, 1, &mut cache));
+ assert!(!is_authored_function_line(&filename, 2, &mut cache));
+ assert!(!is_ignorable_source_line(&filename, 1, &mut cache));
+ assert!(is_ignorable_source_line(&filename, 2, &mut cache));
+ assert!(is_ignorable_source_line(&filename, 4, &mut cache));
+ assert!(is_ignorable_source_line(&filename, 7, &mut cache));
+ assert!(is_ignorable_source_line(&filename, 9, &mut cache));
+ assert!(!is_ignorable_source_line(&filename, 0, &mut cache));
+ assert!(!is_ignorable_source_line(
+ root.join("missing.rs").to_str().expect("path"),
+ 1,
+ &mut cache
+ ));
+
+ let marked = annotated_source_lines(
+ "#[cfg(test)]\n// note\nfn test() {}\nfn live() {}",
+ "cfg(test)",
+ );
+ assert_eq!(marked, vec![true, true, true, false]);
+ let coverage_off = annotated_source_lines(
+ "#[cfg_attr(coverage_nightly, coverage(off))]\n#[inline]\nfn excluded() {\n if true {\n work();\n }\n}\nfn live() {}",
+ "cfg_attr(coverage_nightly, coverage(off))",
+ );
+ assert_eq!(
+ coverage_off,
+ vec![true, true, true, true, true, true, true, false]
+ );
+ assert_eq!(
+ annotated_source_lines("#[cfg(test)]\nfn declaration();\nfn live() {}", "cfg(test)"),
+ vec![true, true, false]
+ );
+ fs::remove_dir_all(root).expect("cleanup");
+}
+
+#[test]
+fn branch_filter_only_excludes_explicit_synthetic_constructs() {
+ let root = test_root("branch_helpers");
+ let source = root.join("source.rs");
+ fs::write(
+ &source,
+ "fn value() {\n let _ = call()?;\n unreachable!();\n assert!(matches!(value, Some(_)));\n if live { work(); }\n}\n",
+ )
+ .expect("source");
+ let filename = source.display().to_string();
+ let mut cache = std::collections::BTreeMap::new();
+ assert!(!is_ignorable_source_line(&filename, 1, &mut cache));
+
+ let branch = |line_start, column_start, line_end, column_end| BranchKey {
+ line_start,
+ column_start,
+ line_end,
+ column_end,
+ kind: 4,
+ };
+ assert!(is_ignorable_branch(
+ &filename,
+ &branch(2, 16, 2, 17),
+ &mut cache
+ ));
+ assert!(is_ignorable_branch(
+ &filename,
+ &branch(3, 2, 3, 10),
+ &mut cache
+ ));
+ assert!(is_ignorable_branch(
+ &filename,
+ &branch(4, 10, 4, 18),
+ &mut cache
+ ));
+ assert!(!is_ignorable_branch(
+ &filename,
+ &branch(5, 2, 5, 9),
+ &mut cache
+ ));
+ assert!(!is_ignorable_branch(
+ &filename,
+ &branch(5, 2, 6, 1),
+ &mut cache
+ ));
+ assert!(!is_ignorable_branch(
+ root.join("missing.rs").to_str().expect("path"),
+ &branch(1, 1, 1, 2),
+ &mut cache
+ ));
+ fs::remove_dir_all(root).expect("cleanup");
+}
diff --git a/tools/sdk_xtask_import/src/main.rs b/tools/sdk_xtask_import/src/main.rs
@@ -130,7 +130,7 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> {
}
fn usage() -> String {
- "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate bindings <swift|kotlin> | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local | cargo xtask coverage run | cargo xtask release qualify-features | cargo xtask release qualify-api | cargo xtask release qualify-portable | cargo xtask release qualify-supply-chain | cargo xtask release qualify-targets"
+ "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate bindings <swift|kotlin> | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local | cargo xtask coverage run | cargo xtask coverage check | cargo xtask release qualify-features | cargo xtask release qualify-api | cargo xtask release qualify-portable | cargo xtask release qualify-supply-chain | cargo xtask release qualify-targets"
.to_owned()
}