cadmpeg-ir 0.6.1

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

use crate::examples::unit_cube;
use crate::report::check::Check;
use crate::validate::validate_neutral;

#[test]
fn ids_are_globally_unique_across_arenas() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    ir.model.points[0].id = crate::ids::PointId::mint(ir.model.vertices[0].id.as_str()).unwrap();
    assert!(validate_neutral(&ir, Vec::new())
        .expect("resource allocation did not fail")
        .findings
        .iter()
        .any(|finding| finding.check == Check::Identity));
}

#[test]
fn arena_ids_must_be_sorted() {
    let mut ir = unit_cube().expect("valid unit cube fixture");
    ir.model.points.swap(0, 1);
    assert!(validate_neutral(&ir, Vec::new())
        .expect("resource allocation did not fail")
        .findings
        .iter()
        .any(|finding| finding.check == Check::ArenaOrder));
}

#[test]
fn identity_validation_borrows_long_identities_and_releases_group_storage() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    let identity = format!("test:validation:point#{}", "p".repeat(1024));
    let mut ir = crate::CadIr::empty();
    ir.model.points.push(crate::topology::Point::new(
        identity.as_str().try_into().unwrap(),
        crate::features::FinitePoint3::new(crate::math::Point3::new(0.0, 0.0, 0.0)).unwrap(),
        None,
    ));
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_retained_bytes = 0;
    policy.limits.max_materialized_bytes = 4096;
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let mut findings = Vec::new();
    super::check_identity_and_order(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &mut findings,
    )
    .unwrap();
    assert!(findings.is_empty());
    let reservation = ctx.reserve_scoped(4096, "identity scope released").unwrap();
    drop(reservation);
    ctx.finish_session().unwrap();
}

#[test]
fn identity_validation_preserves_original_refusals_before_scans_and_findings() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
    use cadmpeg_core::CodecError;
    let mut ir = unit_cube().unwrap();
    ir.model.points.push(ir.model.points[0].clone());
    for dimension in [
        ResourceDimension::WorkUnits,
        ResourceDimension::CollectionItems,
        ResourceDimension::RetainedBytes,
    ] {
        let arena = DecodeArena::new();
        let mut policy = DecodePolicy::service();
        match dimension {
            ResourceDimension::WorkUnits => policy.limits.max_work_units = 0,
            ResourceDimension::CollectionItems => policy.limits.max_collection_items = 0,
            ResourceDimension::RetainedBytes => policy.limits.max_retained_bytes = 0,
            _ => panic!("test dimension"),
        }
        let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
        let mut findings = Vec::new();
        let Err(CodecError::ResourceLimit(limit)) = super::check_identity_and_order(
            &ctx,
            crate::native::view::NativeView::new(&ir, None),
            &mut findings,
        ) else {
            panic!("identity validation must refuse");
        };
        assert_eq!(limit.dimension, dimension);
        assert!(findings.is_empty());
        assert!(
            matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(sticky)) if sticky == limit)
        );
    }
}

#[test]
fn native_order_groups_preserve_combined_name_and_finding_order() {
    let mut ir = crate::CadIr::empty();
    for (format, arena, ids) in [
        ("a", "z", ["test:native:record#2", "test:native:record#1"]),
        ("a.b", "a", ["test:native:record#4", "test:native:record#3"]),
        ("a", "b.a", ["test:native:record#6", "test:native:record#5"]),
    ] {
        for id in ids {
            ir.native
                .namespace_mut(format)
                .arenas_mut()
                .entry(arena.into())
                .or_default()
                .push(
                    crate::native::NativeRecord::new(
                        id.try_into().unwrap(),
                        serde_json::Map::new(),
                    )
                    .unwrap(),
                );
        }
    }
    let ctx = cadmpeg_test_support::service_decode_context();
    let mut findings = Vec::new();
    super::check_identity_and_order(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &mut findings,
    )
    .unwrap();
    assert_eq!(
        findings
            .iter()
            .map(|finding| (
                finding.check,
                finding.entity.as_deref(),
                finding.message.as_str()
            ))
            .collect::<Vec<_>>(),
        vec![
            (
                Check::ArenaOrder,
                Some("test:native:record#5"),
                "arena `native.a.b.a` is not strictly sorted by id"
            ),
            (
                Check::ArenaOrder,
                Some("test:native:record#1"),
                "arena `native.a.z` is not strictly sorted by id"
            ),
        ]
    );
    ctx.finish_session().unwrap();
}

#[test]
fn large_identity_validation_fits_sort_and_lookup_work_without_per_insert_movement() {
    use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
    let mut ir = crate::CadIr::empty();
    for ordinal in 0..40_000 {
        ir.model.points.push(crate::topology::Point::new(
            format!("test:validation:point#{ordinal:05}")
                .try_into()
                .unwrap(),
            crate::features::FinitePoint3::new(crate::math::Point3::new(0.0, 0.0, 0.0)).unwrap(),
            None,
        ));
    }
    let arena = DecodeArena::new();
    let mut policy = DecodePolicy::service();
    policy.limits.max_work_units = 220_000_000;
    policy.limits.max_retained_bytes = 0;
    policy.limits.max_materialized_bytes = 4 * 1024 * 1024;
    let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
    let mut findings = Vec::new();
    super::check_identity_and_order(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &mut findings,
    )
    .expect("identity construction needs one sort and logarithmic lookups");
    assert!(findings.is_empty());
    drop(
        ctx.reserve_scoped(4 * 1024 * 1024, "identity storage released")
            .unwrap(),
    );
    ctx.finish_session().unwrap();
}

#[test]
fn duplicate_identity_findings_keep_model_then_native_visit_order() {
    let mut ir = crate::CadIr::empty();
    for id in [
        "test:validation:point#a",
        "test:validation:point#a",
        "test:validation:point#b",
    ] {
        ir.model.points.push(crate::topology::Point::new(
            id.try_into().unwrap(),
            crate::features::FinitePoint3::new(crate::math::Point3::new(0.0, 0.0, 0.0)).unwrap(),
            None,
        ));
    }
    for id in ["test:validation:point#b", "test:validation:point#a"] {
        ir.native
            .namespace_mut("test")
            .arenas_mut()
            .entry("records".into())
            .or_default()
            .push(
                crate::native::NativeRecord::new(id.try_into().unwrap(), serde_json::Map::new())
                    .unwrap(),
            );
    }
    let ctx = cadmpeg_test_support::service_decode_context();
    let mut findings = Vec::new();
    super::check_identity_and_order(
        &ctx,
        crate::native::view::NativeView::new(&ir, None),
        &mut findings,
    )
    .unwrap();
    assert_eq!(
        findings
            .iter()
            .map(|finding| (finding.check, finding.entity.as_deref()))
            .collect::<Vec<_>>(),
        [
            (Check::ArenaOrder, Some("test:validation:point#a")),
            (Check::Identity, Some("test:validation:point#a")),
            (Check::Identity, Some("test:validation:point#b")),
            (Check::Identity, Some("test:validation:point#a")),
            (Check::ArenaOrder, Some("test:validation:point#a")),
        ]
    );
    ctx.finish_session().unwrap();
}