use super::*;
use crate::diagnostics::{BoolFailure, BoolFailureReason, BoolOp};
use crate::rect_fast::RectFastStats;
#[test]
fn aggregate_empty_is_all_zero() {
let d = aggregate_diagnostics(
ClassificationStats::default(),
&FxHashMap::default(),
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert_eq!(d.total_csg_failures, 0);
assert_eq!(d.total_unsupported_items, 0);
assert!(d.unsupported_items_by_type.is_empty());
assert_eq!(d.products_with_failures, 0);
assert!(d.failures_by_reason.is_empty());
assert!(d.worst_hosts.is_empty());
assert!(!d.has_issues());
}
#[test]
fn aggregate_summarizes_failures_hosts_classification_and_silent_noops() {
let mut csg: FxHashMap<u32, Vec<BoolFailure>> = FxHashMap::default();
csg.insert(
5,
vec![BoolFailure::new(BoolOp::Difference, BoolFailureReason::DifferenceEmptiedHost)],
);
csg.insert(
7,
vec![
BoolFailure::new(BoolOp::Difference, BoolFailureReason::DifferenceEmptiedHost),
BoolFailure::new(BoolOp::Difference, BoolFailureReason::KernelOutputInvalid),
],
);
let mut hosts: FxHashMap<u32, HostOpeningDiagnostic> = FxHashMap::default();
hosts.insert(
7,
HostOpeningDiagnostic {
host_type: "IfcWallStandardCase".into(),
csg_failure_count: 2,
first_failure_label: Some("DifferenceEmptiedHost".into()),
tris_before: Some(120),
tris_after: Some(120),
rect_boxes_processed: 1,
host_bounds: Some(((0.0, 0.0, 0.0), (1.0, 2.0, 3.0))),
..Default::default()
},
);
hosts.insert(
9,
HostOpeningDiagnostic {
host_type: "IfcSlab".into(),
csg_failure_count: 0,
tris_before: Some(50),
tris_after: Some(50),
rect_boxes_processed: 2,
..Default::default()
},
);
let cls = ClassificationStats { rectangular: 3, diagonal: 1, non_rectangular: 0 };
let rf = RectFastStats { fired: 2, openings_cut: 4, ..Default::default() };
let d = aggregate_diagnostics(cls, &csg, &hosts, rf, 16, 0, &FxHashMap::default());
assert_eq!(d.total_csg_failures, 3);
assert_eq!(d.products_with_failures, 2);
assert_eq!(d.hosts_with_openings, 2);
assert_eq!(d.classification.total, 4);
assert_eq!(d.classification.rectangular, 3);
assert_eq!(d.silent_no_ops, 1);
assert_eq!(d.rect_fast.fired, 2);
assert_eq!(d.failures_by_reason[0].reason, "DifferenceEmptiedHost");
assert_eq!(d.failures_by_reason[0].count, 2);
assert_eq!(d.worst_hosts.len(), 1);
assert_eq!(d.worst_hosts[0].product_id, 7);
assert_eq!(d.worst_hosts[0].csg_failures, 2);
let bbox = d.worst_hosts[0].bbox.expect("host_bounds captured");
assert_eq!(bbox.min, [0.0, 0.0, 0.0]);
assert_eq!(bbox.max, [1.0, 2.0, 3.0]);
assert_eq!(d.worst_hosts[0].triangle_count, Some(120));
assert!(d.has_issues());
}
#[test]
fn worst_host_triangle_count_falls_back_to_tris_before_when_no_cut_ran() {
let mut hosts: FxHashMap<u32, HostOpeningDiagnostic> = FxHashMap::default();
hosts.insert(
3,
HostOpeningDiagnostic {
host_type: "IfcSlab".into(),
csg_failure_count: 1,
first_failure_label: Some("EmptyOperand".into()),
tris_before: Some(80),
tris_after: None,
..Default::default()
},
);
let mut csg: FxHashMap<u32, Vec<BoolFailure>> = FxHashMap::default();
csg.insert(3, vec![BoolFailure::new(BoolOp::Difference, BoolFailureReason::EmptyOperand)]);
let d = aggregate_diagnostics(
ClassificationStats::default(),
&csg,
&hosts,
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert_eq!(d.worst_hosts[0].triangle_count, Some(80));
assert!(d.worst_hosts[0].bbox.is_none());
}
#[test]
fn serializes_camelcase_keys_matching_the_ts_contract() {
let mut hosts: FxHashMap<u32, HostOpeningDiagnostic> = FxHashMap::default();
hosts.insert(
7,
HostOpeningDiagnostic {
host_type: "IfcWall".into(),
csg_failure_count: 1,
first_failure_label: Some("KernelError".into()),
tris_before: Some(40),
tris_after: Some(36),
host_bounds: Some(((-1.0, -1.0, 0.0), (1.0, 1.0, 3.0))),
..Default::default()
},
);
let mut csg: FxHashMap<u32, Vec<BoolFailure>> = FxHashMap::default();
csg.insert(7, vec![BoolFailure::new(BoolOp::Difference, BoolFailureReason::KernelOutputInvalid)]);
let mut unsupported: FxHashMap<String, u64> = FxHashMap::default();
unsupported.insert("IfcSectionedSpine".to_string(), 3);
let d = aggregate_diagnostics(
ClassificationStats { rectangular: 1, ..Default::default() },
&csg,
&hosts,
RectFastStats::default(),
16,
0,
&unsupported,
);
assert_eq!(d.total_unsupported_items, 3);
assert_eq!(d.unsupported_items_by_type[0].reason, "IfcSectionedSpine");
assert_eq!(d.unsupported_items_by_type[0].count, 3);
let v = serde_json::to_value(&d).expect("serializes");
for key in [
"totalCsgFailures",
"productsWithFailures",
"hostsWithOpenings",
"classification",
"failuresByReason",
"silentNoOps",
"rectFast",
"worstHosts",
"oversizedRefDrops",
"totalUnsupportedItems",
"unsupportedItemsByType",
] {
assert!(v.get(key).is_some(), "missing top-level key {key}");
}
assert!(v["classification"].get("nonRectangular").is_some());
assert!(v["rectFast"].get("deferHostNotBox").is_some());
let wh = &v["worstHosts"][0];
for key in [
"productId",
"ifcType",
"openings",
"csgFailures",
"firstFailureLabel",
"bbox",
"triangleCount",
] {
assert!(wh.get(key).is_some(), "missing worstHosts key {key}");
}
assert!(wh["bbox"].get("min").is_some() && wh["bbox"].get("max").is_some());
let fr = &v["failuresByReason"][0];
assert!(fr.get("reason").is_some() && fr.get("count").is_some());
}
#[test]
fn oversized_ref_drops_passes_through_and_defeats_is_empty() {
let d = aggregate_diagnostics(
ClassificationStats::default(),
&FxHashMap::default(),
&FxHashMap::default(),
RectFastStats::default(),
16,
3,
&FxHashMap::default(),
);
assert_eq!(d.oversized_ref_drops, 3);
assert!(
!d.is_empty(),
"a nonzero oversized_ref_drops must defeat is_empty so the diagnostics object is attached"
);
}
#[test]
fn zero_oversized_ref_drops_stays_empty() {
let d = aggregate_diagnostics(
ClassificationStats::default(),
&FxHashMap::default(),
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert!(d.is_empty());
}
#[test]
fn schema_version_round_trips_and_defaults() {
let d = GeometryDiagnostics::default();
assert_eq!(d.schema_version, GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION);
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("\"schemaVersion\":3"), "serialized unconditionally: {json}");
let back: GeometryDiagnostics = serde_json::from_str(&json).unwrap();
assert_eq!(back.schema_version, GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION);
let legacy: GeometryDiagnostics =
serde_json::from_str(&json.replace("\"schemaVersion\":3,", "")).unwrap();
assert_eq!(legacy.schema_version, 0);
}
#[test]
fn worst_host_optional_fields_are_omitted_when_none_and_present_when_some() {
let none = WorstHost {
product_id: 1,
ifc_type: "IfcWall".to_string(),
openings: 2,
csg_failures: 1,
first_failure_label: None,
bbox: None,
triangle_count: None,
};
let obj = serde_json::to_value(&none).unwrap();
let obj = obj.as_object().expect("serializes to a JSON object");
for key in ["firstFailureLabel", "bbox", "triangleCount"] {
assert!(
!obj.contains_key(key),
"`{key}` must be absent when None, not null: {obj:?}"
);
}
let some = WorstHost {
first_failure_label: Some("difference_emptied_host".to_string()),
bbox: Some(HostBbox { min: [0.0, 0.0, 0.0], max: [1.0, 2.0, 3.0] }),
triangle_count: Some(42),
..none
};
let obj = serde_json::to_value(&some).unwrap();
let obj = obj.as_object().expect("serializes to a JSON object");
for key in ["firstFailureLabel", "bbox", "triangleCount"] {
assert!(
obj.contains_key(key),
"`{key}` must be present when Some: {obj:?}"
);
}
assert_eq!(obj["triangleCount"], serde_json::json!(42));
let legacy = serde_json::json!({
"productId": 1, "ifcType": "IfcWall", "openings": 2, "csgFailures": 1,
"firstFailureLabel": null, "bbox": null, "triangleCount": null,
});
let back: WorstHost = serde_json::from_value(legacy).unwrap();
assert!(back.bbox.is_none() && back.triangle_count.is_none());
}
#[test]
fn the_unattributed_bucket_counts_in_the_totals_but_is_not_a_product() {
let mut csg: FxHashMap<u32, Vec<BoolFailure>> = FxHashMap::default();
csg.insert(
42,
vec![BoolFailure::new(BoolOp::Difference, BoolFailureReason::DifferenceEmptiedHost)],
);
csg.insert(
UNATTRIBUTED_PRODUCT_ID,
vec![BoolFailure::new(
BoolOp::Unknown,
BoolFailureReason::UnsupportedOperand("IfcSectionedSpine".to_string()),
)],
);
let d = aggregate_diagnostics(
ClassificationStats::default(),
&csg,
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert_eq!(d.products_with_failures, 1, "the synthetic bucket is not a product");
assert_eq!(d.total_csg_failures, 2, "but its records still count as failures");
let reasons: Vec<&str> = d.failures_by_reason.iter().map(|r| r.reason.as_str()).collect();
assert!(
reasons.contains(&"UnsupportedOperand"),
"and still appear in the reason breakdown: {reasons:?}"
);
}
#[test]
fn an_unattributed_only_pass_reports_zero_products_and_still_has_issues() {
let mut csg: FxHashMap<u32, Vec<BoolFailure>> = FxHashMap::default();
csg.insert(
UNATTRIBUTED_PRODUCT_ID,
vec![BoolFailure::new(BoolOp::Union, BoolFailureReason::EmptyOperand)],
);
let d = aggregate_diagnostics(
ClassificationStats::default(),
&csg,
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert_eq!(d.products_with_failures, 0);
assert_eq!(d.total_csg_failures, 1);
assert!(d.has_issues(), "an unowned failure is still a failure");
assert!(!d.is_empty(), "and must not be skipped as an empty diagnostic");
}
#[test]
fn unsupported_items_pass_through_and_defeat_is_empty() {
let mut unsupported: FxHashMap<String, u64> = FxHashMap::default();
unsupported.insert("IfcGeometricSet".to_string(), 2);
let d = aggregate_diagnostics(
ClassificationStats::default(),
&FxHashMap::default(),
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&unsupported,
);
assert_eq!(d.total_unsupported_items, 2);
assert_eq!(d.unsupported_items_by_type[0].reason, "IfcGeometricSet");
assert!(
!d.is_empty(),
"a nonzero total_unsupported_items must defeat is_empty, or processor/mod.rs drops the \
diagnostics object and the drop is invisible again"
);
}
#[test]
fn zero_unsupported_items_stays_empty() {
let d = aggregate_diagnostics(
ClassificationStats::default(),
&FxHashMap::default(),
&FxHashMap::default(),
RectFastStats::default(),
16,
0,
&FxHashMap::default(),
);
assert!(d.is_empty());
}
#[test]
fn opening_diagnostic_field_list_is_pinned() {
struct ExpectedShape {
_opening_id: u32,
_kind: OpeningKindDiag,
_vertex_count: usize,
}
assert_eq!(
core::mem::size_of::<OpeningDiagnostic>(),
core::mem::size_of::<ExpectedShape>(),
"OpeningDiagnostic changed size, so its field list moved. A new field of \
any visibility is a MAJOR semver break on a crate-root re-exported struct \
that is not #[non_exhaustive] (#4192)."
);
}