use cadmpeg_test_support::wire;
use crate::report::decode::{DecodeReport, DecodeTransfer, TransferLedger};
use cadmpeg_core::dialect::DialectLayers;
use std::collections::BTreeMap;
use crate::report::decode::{TransferDisposition, TransferOutcome, TransferRecord};
#[test]
fn admitted_coverage_refuses_new_node_and_static_name_limits() {
use crate::report::decode::{Coverage, CoverageKey};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let key = CoverageKey::new("one_count");
for (items, bytes, dimension, operation) in [
(
0,
u64::MAX,
ResourceDimension::CollectionItems,
"decode coverage nodes",
),
(
1,
0,
ResourceDimension::RetainedBytes,
"decode coverage names",
),
] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_collection_items = items;
policy.limits.max_retained_bytes = bytes;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let error = Coverage::default()
.record(&ctx, key, 7)
.expect_err("below-need coverage cap");
assert!(matches!(error, CodecError::ResourceLimit(resource)
if resource.dimension == dimension && resource.operation == operation));
}
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let mut coverage = Coverage::default();
coverage.record(&ctx, key, 7).expect("service node");
coverage.record(&ctx, key, 8).expect("existing node");
assert_eq!(coverage.get("one_count"), Some(&8));
}
#[test]
fn admitted_indexed_coverage_refuses_temporary_name_and_new_node() {
use crate::report::decode::{Coverage, IndexedCoverageKey};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let key = IndexedCoverageKey::decimal("type_", "_count");
for (materialized, items, dimension, operation) in [
(
0,
1,
ResourceDimension::MaterializedBytes,
"decode indexed coverage name",
),
(
u64::MAX,
0,
ResourceDimension::CollectionItems,
"decode coverage nodes",
),
] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_materialized_bytes = materialized;
policy.limits.max_collection_items = items;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let error = Coverage::default()
.record_indexed(&ctx, key, 4, 7)
.expect_err("below-need coverage cap");
assert!(matches!(error, CodecError::ResourceLimit(resource)
if resource.dimension == dimension && resource.operation == operation));
}
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let mut coverage = Coverage::default();
coverage
.record_indexed(&ctx, key, 4, 7)
.expect("service node");
assert_eq!(coverage.get("type_4_count"), Some(&7));
}
#[test]
fn admitted_hex_coverage_refuses_retained_name_limit() {
use crate::report::decode::{Coverage, HexByteCoverageKey};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let key = HexByteCoverageKey::new("type_", "_count");
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let error = Coverage::default()
.record_hex_byte(&ctx, key, 0x0a, 7)
.expect_err("retained name exceeds cap");
assert!(matches!(error, CodecError::ResourceLimit(resource)
if resource.dimension == ResourceDimension::RetainedBytes
&& resource.operation == "decode hexadecimal coverage name"));
let arena = DecodeArena::new();
let policy = DecodePolicy::service();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let mut coverage = Coverage::default();
coverage
.record_hex_byte(&ctx, key, 0x0a, 7)
.expect("service node");
assert_eq!(coverage.get("type_0a_count"), Some(&7));
}
#[test]
fn owned_coverage_record_preserves_declared_key() {
let key = crate::report::decode::CoverageKey::new("decoded_entities");
let mut coverage = crate::report::decode::Coverage::default();
let arena = cadmpeg_core::decode::DecodeArena::new();
let policy = cadmpeg_core::decode::DecodePolicy::service();
let (ctx, _) = cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy)
.expect("service context");
assert!(coverage
.record_owned(&ctx, key, "other_entities".to_owned(), 3)
.is_err());
assert!(coverage.is_empty());
coverage
.record_owned(&ctx, key, "decoded_entities".to_owned(), 3)
.expect("declared key is admitted");
assert_eq!(coverage.get("decoded_entities"), Some(&3));
}
#[cfg(feature = "schema")]
#[test]
fn current_decode_report_schema_requires_its_format_identity() {
let schema = serde_json::to_value(schemars::schema_for!(crate::report::decode::DecodeReport))
.expect("decode report schema serializes");
let required = schema["required"]
.as_array()
.expect("decode report schema has required fields");
assert!(
required.iter().any(|field| field == "identity"),
"{schema:#}"
);
}
#[test]
fn transfer_record_keeps_the_nested_outcome_wire_shape() {
let record = TransferRecord {
source: "D1".into(),
outcome: TransferOutcome::Retained {
target: "iges:entity:directory#1".into(),
note: Some("retained".into()),
},
};
let wire = serde_json::to_value(&record).expect("transfer record serializes");
assert_eq!(
wire,
serde_json::json!({
"source": "D1",
"outcome": {
"disposition": "retained",
"target": "iges:entity:directory#1",
"note": "retained"
}
})
);
assert_eq!(
serde_json::from_value::<TransferRecord>(wire).expect("transfer record deserializes"),
record
);
}
#[test]
fn transfer_record_wire_rejects_disposition_target_disagreement() {
for wire in [
serde_json::json!({
"source": "D1",
"outcome": {"disposition": "retained"}
}),
serde_json::json!({
"source": "D1",
"outcome": {"disposition": "omitted", "target": "point:1"}
}),
serde_json::json!({
"source": "D1",
"outcome": {"disposition": "emitted", "target": "point:1", "note": "n"}
}),
serde_json::json!({
"source": "D1",
"outcome": {"disposition": "emitted"}
}),
] {
assert!(serde_json::from_value::<TransferRecord>(wire).is_err());
}
let omitted: TransferRecord = serde_json::from_value(serde_json::json!({
"source": "D2",
"outcome": {"disposition": "omitted", "note": "unsupported"}
}))
.expect("omitted record has no target");
assert_eq!(omitted.target(), None);
assert_eq!(
wire::field::<crate::report::decode::TransferDisposition>(
&(omitted.outcome),
"disposition"
),
TransferDisposition::Omitted
);
assert_eq!(
wire::field_or_default::<Option<String>>(&(omitted.outcome), "note").as_deref(),
Some("unsupported")
);
}
#[test]
fn classified_report_wire_requires_its_primary_format() {
let report = DecodeReport::classified(
DialectLayers::of(cadmpeg_core::dialect::DialectMatch::admitted(
cadmpeg_core::dialect_id!("rhino:archive-80"),
)),
DecodeTransfer::full(true),
BTreeMap::new(),
Vec::new(),
Vec::new(),
TransferLedger::default(),
);
let golden = serde_json::to_string(&report).unwrap();
assert_eq!(
golden,
r#"{"identity":{"classification":"classified","dialects":{"primary":{"dialect":"rhino:archive-80","admission":"admitted"},"extra":[]}},"transfer":{"transfer":"full","geometry_transferred":true},"losses":[],"notes":[]}"#
);
assert_eq!(
serde_json::from_str::<DecodeReport>(&golden).unwrap(),
report
);
let contradictory = golden.replacen(
"\"transfer\":\"full\"",
"\"transfer\":\"container_only\"",
1,
);
let error = serde_json::from_str::<DecodeReport>(&contradictory)
.expect_err("a container-only report has no geometry outcome to state");
assert!(
error.to_string().contains("geometry_transferred"),
"{error}"
);
let restated = golden.replacen(
"\"classification\":\"classified\"",
"\"classification\":\"classified\",\"format\":\"step\"",
1,
);
let error = serde_json::from_str::<DecodeReport>(&restated)
.expect_err("a classified identity carries no second format");
assert!(error.to_string().contains("format"), "{error}");
}
#[test]
fn container_only_report_wire_preserves_the_coherent_transfer_state() {
let report = DecodeReport::unclassified(
"test",
DecodeTransfer::ContainerOnly {},
BTreeMap::new(),
Vec::new(),
Vec::new(),
TransferLedger::default(),
);
let rendered = serde_json::to_string(&report).unwrap();
assert!(
rendered.contains("\"transfer\":\"container_only\""),
"{rendered}"
);
assert!(!rendered.contains("geometry_transferred"), "{rendered}");
assert_eq!(
serde_json::from_str::<DecodeReport>(&rendered).unwrap(),
report
);
}
#[test]
fn a_decode_transfer_states_its_scope_and_carries_only_its_own_keys() {
let base = serde_json::json!({
"identity": {"classification": "unclassified", "format": "rhino"},
"transfer": {"transfer": "full", "geometry_transferred": true},
"losses": [],
"notes": [],
});
let report = serde_json::from_value::<DecodeReport>(base.clone()).expect("a full decode");
assert_eq!(report.transfer, DecodeTransfer::full(true));
assert_eq!(
serde_json::to_value(&report).unwrap()["transfer"]["transfer"],
"full"
);
let mut container_only = base.clone();
container_only["transfer"]["transfer"] = serde_json::json!("container_only");
let error = serde_json::from_value::<DecodeReport>(container_only.clone())
.expect_err("a container-only report has no geometry outcome");
assert!(
error.to_string().contains("geometry_transferred"),
"{error}"
);
container_only["transfer"]
.as_object_mut()
.expect("map")
.remove("geometry_transferred");
let report = serde_json::from_value::<DecodeReport>(container_only).expect("container only");
assert_eq!(report.transfer, DecodeTransfer::ContainerOnly {});
let wire = serde_json::to_value(&report).unwrap();
assert_eq!(wire["transfer"]["transfer"], "container_only");
assert!(wire["transfer"].get("geometry_transferred").is_none());
let mut without_outcome = base;
without_outcome["transfer"]
.as_object_mut()
.expect("map")
.remove("geometry_transferred");
let error = serde_json::from_value::<DecodeReport>(without_outcome)
.expect_err("a full decode states its geometry outcome");
assert!(
error.to_string().contains("geometry_transferred"),
"{error}"
);
}
#[test]
fn coverage_entry_refuses_retained_map_storage() {
use crate::report::decode::{Coverage, CoverageKey};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let key = CoverageKey::new("one_count");
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
let error = cadmpeg_test_support::refusal::resource_limit_at(
ResourceDimension::RetainedBytes,
"decode coverage nodes",
|cap| {
policy.limits.max_retained_bytes = cap;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
Coverage::default().record(&ctx, key, 7)
},
);
let cadmpeg_core::CodecError::ResourceLimit(limit) = error else {
panic!("coverage storage must refuse");
};
policy.limits.max_retained_bytes = limit.used + limit.additional - 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let mut coverage = Coverage::default();
assert!(
matches!(coverage.record(&ctx, key, 7), Err(cadmpeg_core::CodecError::ResourceLimit(limit))
if limit.dimension == ResourceDimension::RetainedBytes && limit.operation == "decode coverage nodes")
);
assert!(coverage.is_empty());
policy.limits.max_retained_bytes += 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
coverage.record(&ctx, key, 7).expect("one admitted entry");
coverage
.record(&ctx, key, 8)
.expect("replacement makes no allocation");
assert_eq!(coverage.get(key.as_str()), Some(&8));
}
#[test]
fn coverage_entry_refuses_lookup_work() {
use crate::report::decode::{Coverage, CoverageKey};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_work_units = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).expect("root");
let mut coverage = Coverage::default();
assert!(
matches!(coverage.record(&ctx, CoverageKey::new("one_count"), 7), Err(cadmpeg_core::CodecError::ResourceLimit(limit))
if limit.dimension == ResourceDimension::WorkUnits && limit.operation == "decode coverage lookup")
);
assert!(coverage.is_empty());
}