imodfile 0.2.2

A pure-Rust IMOD model file decoder/encoder — binary & ASCII, with Python bindings
Documentation
//! Property-based roundtrip tests using proptest.
//!
//! Generates random valid models and verifies that:
//!   - Binary write → binary read preserves all structure
//!   - psize is always consistent with pts.len()

use std::io::Cursor;

use imodfile::*;
use proptest::prelude::*;

// ── Strategies ──

/// Strategy for generating an `Ipoint` with coordinates in [-1000, 1000].
fn any_point() -> impl Strategy<Value = Ipoint> {
    (-1000.0f32..=1000.0, -1000.0f32..=1000.0, -1000.0f32..=1000.0)
        .prop_map(|(x, y, z)| Ipoint { x, y, z })
}

/// Strategy for generating a valid `Icont` where `psize` matches `pts.len()`.
fn valid_contour() -> impl Strategy<Value = Icont> {
    (
        proptest::collection::vec(any_point(), 0..=10),
        any::<u32>(),
        any::<i32>(),
        any::<i32>(),
    )
        .prop_map(|(pts, flags, time, surf)| {
            let psize = pts.len() as i32;
            Icont {
                pts,
                psize,
                flags,
                time,
                surf,
                ..Icont::default()
            }
        })
}

/// Strategy for generating a valid `Iobj` where `contsize` matches `cont.len()`.
fn valid_object() -> impl Strategy<Value = Iobj> {
    (
        "[A-Za-z0-9_ ]{0,32}",
        proptest::collection::vec(valid_contour(), 0..=5),
        any::<u32>(),
        0.0f32..=1.0,
        0.0f32..=1.0,
        0.0f32..=1.0,
    )
        .prop_map(|(name, cont, flags, red, green, blue)| {
            let contsize = cont.len() as i32;
            Iobj {
                name,
                cont,
                contsize,
                flags,
                red,
                green,
                blue,
                ..Iobj::default()
            }
        })
}

/// Strategy for generating a valid `Imod` where `objsize` matches `obj.len()`.
fn valid_model() -> impl Strategy<Value = Imod> {
    (
        "[A-Za-z0-9_ ]{0,32}",
        proptest::collection::vec(valid_object(), 0..=5),
        any::<u32>(),
        -1000i32..=1000,
        -1000i32..=1000,
        -1000i32..=1000,
    )
        .prop_map(|(name, obj, flags, xmax, ymax, zmax)| {
            let objsize = obj.len() as i32;
            Imod {
                name,
                obj,
                objsize,
                flags,
                xmax,
                ymax,
                zmax,
                ..Imod::default()
            }
        })
}

// ── Property tests ──

proptest! {
    /// Binary write → binary read should produce an identical model
    /// for every valid generated model.
    #[test]
    fn binary_roundtrip(model in valid_model()) {
        // Write to binary
        let mut buf = Vec::new();
        let mut cursor = Cursor::new(&mut buf);
        prop_assert!(write_binary(&mut cursor, &model).is_ok());

        // Read back
        cursor.set_position(0);
        let model2 = read_binary(&mut cursor).expect("read_binary failed");

        // ── Compare top-level model fields ──
        prop_assert_eq!(&model.name, &model2.name);
        prop_assert_eq!(model.objsize, model2.objsize);
        prop_assert_eq!(model.xmax, model2.xmax);
        prop_assert_eq!(model.ymax, model2.ymax);
        prop_assert_eq!(model.zmax, model2.zmax);

        // The binary writer always ORs these three flags into the stored value
        // (see writer.rs `write_binary`), so we must account for that.
        let expected_flags = model.flags
            | IMODF_MAT1_IS_BYTES
            | IMODF_MULTIPLE_CLIP
            | IMODF_HAS_MESH_THICK;
        prop_assert_eq!(expected_flags, model2.flags);

        prop_assert_eq!(model.obj.len(), model2.obj.len());

        // ── Compare each object ──
        for (oi, (o1, o2)) in model.obj.iter().zip(model2.obj.iter()).enumerate() {
            prop_assert_eq!(&o1.name, &o2.name, "obj {} name mismatch", oi);
            prop_assert_eq!(o1.contsize, o2.contsize, "obj {} contsize mismatch", oi);
            prop_assert_eq!(o1.red, o2.red, "obj {} red mismatch", oi);
            prop_assert_eq!(o1.green, o2.green, "obj {} green mismatch", oi);
            prop_assert_eq!(o1.blue, o2.blue, "obj {} blue mismatch", oi);
            prop_assert_eq!(o1.flags, o2.flags, "obj {} flags mismatch", oi);
            prop_assert_eq!(o1.cont.len(), o2.cont.len(), "obj {} cont len mismatch", oi);

            // ── Compare each contour ──
            for (ci, (c1, c2)) in o1.cont.iter().zip(o2.cont.iter()).enumerate() {
                prop_assert_eq!(c1.psize, c2.psize, "obj {} cont {} psize mismatch", oi, ci);
                prop_assert_eq!(c1.surf, c2.surf, "obj {} cont {} surf mismatch", oi, ci);
                prop_assert_eq!(c1.flags, c2.flags, "obj {} cont {} flags mismatch", oi, ci);
                prop_assert_eq!(c1.time, c2.time, "obj {} cont {} time mismatch", oi, ci);
                prop_assert_eq!(c1.pts.len(), c2.pts.len(), "obj {} cont {} pts len mismatch", oi, ci);

                // ── Compare each point ──
                for (pi, (p1, p2)) in c1.pts.iter().zip(c2.pts.iter()).enumerate() {
                    let tol = 1.0e-5;
                    prop_assert!(
                        (p1.x - p2.x).abs() < tol,
                        "obj {} cont {} pt {} x mismatch: {} != {}", oi, ci, pi, p1.x, p2.x
                    );
                    prop_assert!(
                        (p1.y - p2.y).abs() < tol,
                        "obj {} cont {} pt {} y mismatch: {} != {}", oi, ci, pi, p1.y, p2.y
                    );
                    prop_assert!(
                        (p1.z - p2.z).abs() < tol,
                        "obj {} cont {} pt {} z mismatch: {} != {}", oi, ci, pi, p1.z, p2.z
                    );
                }
            }
        }
    }

    /// For any valid model, every contour's `psize` must equal `pts.len()`.
    #[test]
    fn point_size_consistency(model in valid_model()) {
        for (oi, obj) in model.obj.iter().enumerate() {
            for (ci, cont) in obj.cont.iter().enumerate() {
                prop_assert_eq!(
                    cont.psize as usize,
                    cont.pts.len(),
                    "obj {} cont {}: psize {} != pts.len() {}",
                    oi, ci, cont.psize, cont.pts.len()
                );
            }
        }
    }
}