cadmpeg-ir 0.6.2

CAD data model, validation, diffing, and codec API for cadmpeg.
// SPDX-License-Identifier: Apache-2.0
#![allow(clippy::unwrap_used)]

use super::check_annotations;
use crate::report::check::Check;
use crate::validate::validate_neutral;
use crate::{examples::unit_cube, NativeNamespace, NativeRecord};
use serde_json::{Map, Value};

#[test]
fn model_entity_wins_when_native_id_collides() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    let id = ir.model.points[0].id.as_str().to_owned();
    let mut namespace = NativeNamespace::default();
    namespace.arenas_mut().insert(
        "records".into(),
        vec![NativeRecord::new(
            crate::ids::Identity::new(id.clone()).expect("valid identity"),
            Map::from_iter([("native_only".into(), Value::Bool(true))]),
        )
        .expect("valid native identity")],
    );
    ir.native.0.insert("collision".into(), namespace);
    let ctx = cadmpeg_test_support::service_decode_context();
    let all_ids = super::BorrowedIdentities::build(|add| add(id.as_str(), ()));
    let mut builder = crate::AnnotationBuilder::new();
    builder.derived(&id, "position").unwrap();
    builder.derived(&id, "native_only").unwrap();
    let mut findings = Vec::new();
    check_annotations(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &builder.build(),
        &all_ids,
        &mut findings,
    )
    .unwrap();
    assert!(!findings
        .iter()
        .any(|finding| finding.message.contains("`position`")));
    assert!(findings
        .iter()
        .any(|finding| finding.message.contains("`native_only`")));
}

#[test]
fn annotation_keys_and_field_paths_are_checked() {
    let ir = unit_cube().expect("valid unit cube fixture");
    let mut source_fidelity = crate::SourceFidelity::default();
    let mut annotations = crate::AnnotationBuilder::new();
    let stream = crate::annotations::StreamHandle::new(crate::stream_name!("test:source"));
    annotations.note("missing", &stream, 0, None);
    annotations
        .derived(ir.model.edges[0].id.as_str(), "not_a_serialized_field")
        .expect("nonempty exactness field");
    source_fidelity.annotations = annotations.build();
    let findings = crate::validate_neutral_with_source_fidelity(&ir, &source_fidelity, Vec::new())
        .expect("validate the neutral model")
        .findings;
    assert!(findings.iter().any(|finding| {
        finding.check == Check::Annotations && finding.severity == crate::report::Severity::Error
    }));
    assert!(findings.iter().any(|finding| {
        finding.check == Check::Annotations && finding.severity == crate::report::Severity::Warning
    }));
}

#[test]
fn native_topology_link_must_resolve() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    ir.native.namespace_mut("f3d").arenas_mut().insert(
        "sketch_curve_links".into(),
        vec![NativeRecord::new(
            crate::ids::Identity::new("native:test:link#0").expect("valid identity"),
            serde_json::from_value(serde_json::json!({"links": ["missing"]})).unwrap(),
        )
        .expect("valid native identity")],
    );
    ir.native.finalize();
    assert!(validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings
        .iter()
        .any(|finding| finding.check == Check::NativeLinks));
}

#[test]
fn parameter_native_ref_must_resolve() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    let id = crate::features::ParameterId::mint("synthetic:test:parameter#native-ref")
        .expect("identity grammar");
    ir.model.parameters.push(crate::features::DesignParameter {
        id: id.clone(),
        owner: Some(
            crate::features::FeatureId::mint("synthetic:test:feature#missing")
                .expect("identity grammar"),
        ),
        ordinal: 0,
        name: "D1".into(),
        expression: "1mm".into(),
        display: None,
        value: None,
        dependencies: crate::features::DistinctMembers::default(),
        properties: std::collections::BTreeMap::new(),
        pmi: Some(crate::features::ParameterPmi {
            subtype: crate::features::PmiDimensionSubtype::Linear,
            precision: 2,
            display_text: None,
            basic: false,
            inspection: false,
            reference_only: false,
            native_ref: "native:pmi-missing#0".into(),
        }),
        native_ref: Some("native:missing#0".into()),
    });
    assert!(validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings
        .iter()
        .any(|finding| {
            finding.check == Check::NativeLinks && finding.entity.as_deref() == Some(id.as_str())
        }));
    assert!(validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings
        .iter()
        .any(|finding| {
            finding.check == Check::NativeLinks
                && finding.message.contains("PMI native_ref")
                && finding.entity.as_deref() == Some(id.as_str())
        }));
}

#[test]
fn unresolved_unknown_record_link_is_reported_once() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    ir.set_native_unknowns(
        &cadmpeg_test_support::service_decode_context(),
        "test",
        &[crate::NativeUnknownRecord {
            id: crate::ids::UnknownId::mint("test:model:unknown#0").expect("valid identity"),
            links: vec!["test:model:missing#0".try_into().unwrap()],
        }],
    )
    .expect("store unknown record");

    let findings = validate_neutral(&ir, Vec::new())
        .expect("validate the neutral model")
        .findings;
    let reported = findings
        .iter()
        .filter(|finding| {
            finding.check == Check::NativeLinks && finding.message.contains("test:model:missing#0")
        })
        .collect::<Vec<_>>();
    assert_eq!(reported.len(), 1);
    assert_eq!(reported[0].entity.as_deref(), Some("test:model:unknown#0"));
}

#[test]
fn complete_document_native_link_validation_reports_malformed_shapes() {
    for links in [
        serde_json::json!(null),
        serde_json::json!(true),
        serde_json::json!(3),
        serde_json::json!("test:model:body#0"),
        serde_json::json!({}),
        serde_json::json!([null]),
        serde_json::json!([false]),
        serde_json::json!([3]),
        serde_json::json!([{}]),
        serde_json::json!([[]]),
    ] {
        let mut wire = serde_json::to_value(unit_cube().unwrap()).unwrap();
        wire["native"] = serde_json::json!({"test": {"unknowns": [{
            "id": "test:source:unknown#malformed", "links": links
        }]}});
        let ir = crate::CadIr::from_json(&wire.to_string()).unwrap();
        let findings = validate_neutral(&ir, Vec::new())
            .expect("validate the neutral model")
            .findings;
        let found = findings
            .iter()
            .filter(|finding| {
                finding.check == Check::NativeLinks
                    && finding.entity.as_deref() == Some("test:source:unknown#malformed")
            })
            .collect::<Vec<_>>();
        assert_eq!(found.len(), 1, "{findings:?}");
    }
}

#[test]
fn codec_owned_link_payloads_do_not_inherit_unknown_record_shape() {
    for links in [
        serde_json::json!(null),
        serde_json::json!(false),
        serde_json::json!(3),
        serde_json::json!("source-specific value"),
        serde_json::json!({"target": 42}),
        serde_json::json!([null]),
        serde_json::json!([{"object": "source-object", "subelements": ["Face1"]}]),
        serde_json::json!(["source-specific value", {"row": 7}]),
    ] {
        let mut wire = serde_json::to_value(unit_cube().unwrap()).unwrap();
        wire["native"] = serde_json::json!({"test": {"properties": [{
            "id": "test:native:property#links", "links": links
        }]}});
        let ir = crate::CadIr::from_json(&wire.to_string()).unwrap();
        let findings = validate_neutral(&ir, Vec::new())
            .expect("validate the neutral model")
            .findings;
        assert!(
            findings
                .iter()
                .all(|finding| finding.check != Check::NativeLinks),
            "{findings:?}"
        );
        assert_eq!(serde_json::to_value(&ir).unwrap()["native"], wire["native"]);
    }
}

#[test]
fn native_annotation_paths_report_only_unresolvable_paths() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    let id = "test:native:record#payload";
    let mut ir = crate::CadIr::empty();
    ir.native.namespace_mut("test").arenas_mut().insert(
        "records".into(),
        vec![NativeRecord::new(
            crate::ids::Identity::new(id).unwrap(),
            Map::from_iter([
                (
                    "payload".into(),
                    Value::Array((0..10_000).map(Value::from).collect()),
                ),
                (
                    "nested".into(),
                    serde_json::json!({"null": null, "rows": [{"value": true}]}),
                ),
            ]),
        )
        .unwrap()],
    );
    let mut builder = crate::AnnotationBuilder::new();
    for path in [
        "id",
        "payload.9999",
        "nested.null",
        "nested.rows.0.value",
        "payload.10000",
        "missing",
        "id.child",
    ] {
        builder.derived(id, path).unwrap();
    }
    let annotations = builder.build();
    let arena = DecodeArena::new();
    let policy = DecodePolicy::service();
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let all_ids = super::BorrowedIdentities::build(|add| add(id, ()));
    let mut findings = Vec::new();
    check_annotations(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &annotations,
        &all_ids,
        &mut findings,
    )
    .unwrap();
    assert_eq!(findings.len(), 3);
    for path in ["payload.10000", "missing", "id.child"] {
        assert!(findings
            .iter()
            .any(|finding| finding.message.contains(&format!("`{path}`"))));
    }
}

#[test]
fn many_native_annotation_paths_parse_their_record_once() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    // Scanning the record per path would read 2^28 members.
    const FIELDS: usize = 1 << 14;
    let id = "test:native:record#wide";
    let mut ir = crate::CadIr::empty();
    ir.native.namespace_mut("test").arenas_mut().insert(
        "records".into(),
        vec![NativeRecord::new(
            crate::ids::Identity::new(id).unwrap(),
            (0..FIELDS)
                .map(|index| (format!("field{index:05}"), Value::from(index)))
                .collect(),
        )
        .unwrap()],
    );
    let mut builder = crate::AnnotationBuilder::new();
    for index in 0..FIELDS {
        builder.derived(id, &format!("field{index:05}")).unwrap();
    }
    builder.derived(id, "field99999").unwrap();
    let annotations = builder.build();
    let arena = DecodeArena::new();
    let policy = DecodePolicy::service();
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let all_ids = super::BorrowedIdentities::build(|add| add(id, ()));
    let mut findings = Vec::new();
    check_annotations(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &annotations,
        &all_ids,
        &mut findings,
    )
    .unwrap();
    let messages: Vec<_> = findings
        .iter()
        .map(|finding| finding.message.as_str())
        .collect();
    assert_eq!(
        messages,
        ["exactness field path `field99999` does not resolve"]
    );
}

#[test]
fn annotations_on_many_model_points_resolve_without_findings() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    let mut ir = crate::CadIr::empty();
    let prototype = unit_cube().unwrap().model.points.remove(0);
    let mut builder = crate::AnnotationBuilder::new();
    for ordinal in 0..2_000 {
        let mut point = prototype.clone();
        point.id = crate::ids::PointId::mint(format!("test:model:point#{ordinal}")).unwrap();
        builder.derived(point.id.as_str(), "position.x").unwrap();
        ir.model.points.push(point);
    }
    let annotations = builder.build();
    let arena = DecodeArena::new();
    let policy = DecodePolicy::service();
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let all_ids = super::BorrowedIdentities::build(|add| {
        for point in &ir.model.points {
            add(point.id.as_str(), ());
        }
    });
    let mut findings = Vec::new();
    check_annotations(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &annotations,
        &all_ids,
        &mut findings,
    )
    .unwrap();
    assert!(findings.is_empty());
}