caexfer 0.2.0

Transfer finite-element meshes, voxel occupancy, and results across CAE formats
Documentation
//! Adapter tests for msh.

use super::*;
use crate::core::{Cell, CellKind, Dataset, Field, FieldLocation, Mesh, Point};

/// Keep numeric node and element fields through MSH serialization.
#[test]
fn numeric_point_and_cell_fields_survive_msh_roundtrip() {
    let dataset = Dataset {
        mesh: Mesh {
            points: vec![
                Point {
                    id: 10,
                    position: [0., 0., 0.],
                },
                Point {
                    id: 20,
                    position: [1., 0., 0.],
                },
                Point {
                    id: 30,
                    position: [0., 1., 0.],
                },
            ],
            cells: vec![Cell {
                id: 50,
                kind: CellKind::Triangle3,
                connectivity: vec![0, 1, 2],
                property_id: None,
            }],
            ..Mesh::default()
        },
        fields: vec![
            Field {
                name: "Displacement".into(),
                location: FieldLocation::Point,
                components: vec!["C1".into(), "C2".into()],
                values: vec![0., 1., 2., 3., 4., 5.],
                step: Some(1),
                time: Some(0.25),
            },
            Field {
                name: "Energy".into(),
                location: FieldLocation::Cell,
                components: vec!["C1".into()],
                values: vec![9.],
                step: Some(1),
                time: Some(0.25),
            },
        ],
    };
    let mut bytes = Vec::new();
    write(&dataset, &mut bytes).unwrap();
    assert_eq!(read(std::str::from_utf8(&bytes).unwrap()).unwrap(), dataset);
    let mut bytes_22 = Vec::new();
    write_22(&dataset, &mut bytes_22).unwrap();
    assert_eq!(
        read(std::str::from_utf8(&bytes_22).unwrap()).unwrap(),
        dataset
    );
}

/// Report MSH 2.2 tags and reject declared counts beyond input bounds.
#[test]
fn msh22_tags_are_reported_and_counts_are_bounded() {
    let source = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n$Nodes\n2\n10 0 0 0\n20 1 0 0\n$EndNodes\n$Elements\n1\n30 1 2 7 8 10 20\n$EndElements\n";
    let projection = read_projection(source).unwrap();
    assert_eq!(projection.tagged_elements, 1);
    assert_eq!(projection.generated_group_names, 1);
    assert_eq!(projection.dataset.mesh.cells[0].id, 30);
    assert_eq!(projection.dataset.mesh.cells[0].connectivity, [0, 1]);
    assert_eq!(projection.dataset.mesh.cell_sets[0].name, "physical_1_7");
    assert!(read(&source.replace("$Nodes\n2", "$Nodes\n999999999")).is_err());
    assert!(read(&source.replace("30 1 2", "30 1 999999999")).is_err());
    assert!(read(&source.replace("30 1 2 7 8", "30 1 2 -7 8")).is_err());
}

/// Preserve overlapping named physical selections on boundary cells.
#[test]
fn msh41_physical_names_select_overlapping_boundary_cells() {
    let source = include_str!("../../../../tests/fixtures/named-boundary.msh");
    let dataset = read(source).unwrap();
    assert_eq!(dataset.mesh.cell_sets.len(), 3);
    assert_eq!(dataset.mesh.cell_sets[0].name, "inlet");
    assert_eq!(dataset.mesh.cell_sets[0].cell_ids, [1]);
    assert_eq!(dataset.mesh.cell_sets[1].name, "outer wall");
    assert_eq!(dataset.mesh.cell_sets[1].cell_ids, [1]);
    assert_eq!(write(&dataset, Vec::new()).unwrap_err().code, "E_MSH");
    let mut boundary_only = dataset.clone();
    boundary_only
        .mesh
        .cell_sets
        .retain(|set| set.dimension == 1);
    let mut output = Vec::new();
    crate::formats::su2::write_data(&boundary_only, &mut output).unwrap();
    assert!(
        std::str::from_utf8(&output)
            .unwrap()
            .contains("MARKER_TAG= outer wall")
    );
}

/// Map a named physical point group to a node set.
#[test]
fn msh41_physical_point_name_becomes_node_set() {
    let source = "$MeshFormat\n4.1 0 8\n$EndMeshFormat\n$PhysicalNames\n1\n0 42 \"fixed\"\n$EndPhysicalNames\n$Entities\n1 0 0 0\n1 0 0 0 1 42\n$EndEntities\n$Nodes\n1 1 10 10\n0 1 0 1\n10\n0 0 0\n$EndNodes\n$Elements\n0 0 0 0\n$EndElements\n";
    let projection = read_projection(source).unwrap();
    assert_eq!(projection.generated_group_names, 0);
    assert_eq!(projection.dataset.mesh.node_sets[0].name, "fixed");
    assert_eq!(projection.dataset.mesh.node_sets[0].point_ids, [10]);

    // Physical tags remain visible when the producer omits display names.
    let unnamed = source.replace("$PhysicalNames\n1\n0 42 \"fixed\"\n$EndPhysicalNames\n", "");
    let projection = read_projection(&unnamed).unwrap();
    assert_eq!(projection.generated_group_names, 1);
    assert_eq!(projection.dataset.mesh.node_sets[0].name, "physical_0_42");
}

/// Read all supported topologies from an external Gmsh 2.2 fixture.
#[test]
fn gmsh_written_msh22_fixture_preserves_all_topologies() {
    let projection = read_projection(include_str!(
        "../../../../tests/fixtures/gmsh-2.2-mixed.msh"
    ))
    .unwrap();
    assert_eq!(projection.tagged_elements, 0);
    let dataset = projection.dataset;
    assert_eq!(dataset.mesh.points.len(), 9);
    assert_eq!(dataset.mesh.cells.len(), 7);
    assert_eq!(dataset.mesh.cells[0].id, 1);
    assert_eq!(dataset.mesh.cells[6].kind, CellKind::Pyramid5);
}

/// Retain original node IDs in MSH 2.2 and 4.1 without elements.
#[test]
fn nodes_only_meshes_preserve_ids_in_both_dialects() {
    let dataset = Dataset {
        mesh: Mesh {
            points: vec![
                Point {
                    id: 10,
                    position: [0., 0., 0.],
                },
                Point {
                    id: 20,
                    position: [1., 2., 3.],
                },
            ],
            cells: vec![],
            ..Mesh::default()
        },
        fields: vec![],
    };
    for version in [Version::V4_1, Version::V2_2] {
        let mut output = Vec::new();
        write_version(&dataset, version, &mut output).unwrap();
        let decoded = read(std::str::from_utf8(&output).unwrap()).unwrap();
        assert_eq!(decoded.mesh.points, dataset.mesh.points);
        assert_eq!(decoded.mesh.cells, []);
    }
}

/// Reject broken MSH field blocks before exposing partial results.
#[test]
fn malformed_field_blocks_cannot_create_partial_results() {
    let base = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n$Nodes\n2\n10 0 0 0\n20 1 0 0\n$EndNodes\n$Elements\n1\n30 1 0 10 20\n$EndElements\n$NodeData\n1\n\"temperature\"\n1\n0.5\n3\n2\n1\n2\n10 1\n20 2\n$EndNodeData\n";
    let valid = read(base).unwrap();
    assert_eq!(valid.fields[0].values, [1., 2.]);
    for (old, new) in [
        ("$NodeData\n1", "$NodeData\n2"),
        ("\"temperature\"", "temperature"),
        ("\"temperature\"\n1\n0.5", "\"temperature\"\n2\n0.5"),
        ("0.5\n3\n2\n1\n2", "0.5\n2\n2\n1\n2"),
        ("0.5\n3\n2\n1\n2", "0.5\n3\n2\n0\n2"),
        ("0.5\n3\n2\n1\n2", "0.5\n3\n2\n1\n1"),
        ("20 2\n$EndNodeData", "10 2\n$EndNodeData"),
        ("20 2\n$EndNodeData", "99 2\n$EndNodeData"),
        ("$EndNodeData", ""),
    ] {
        assert!(base.contains(old));
        assert!(read(&base.replacen(old, new, 1)).is_err(), "{new}");
    }
}

/// Refuse malformed MSH 2.2 records and IDs outside writer limits.
#[test]
fn msh22_rejects_extra_tokens_and_unrepresentable_ids() {
    let base = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n$Nodes\n2\n10 0 0 0\n20 1 0 0\n$EndNodes\n$Elements\n1\n30 1 0 10 20\n$EndElements\n";
    for (old, new) in [
        ("20 1 0 0\n$EndNodes", "20 1 0 0 99\n$EndNodes"),
        (
            "30 1 0 10 20\n$EndElements",
            "30 1 0 10 20 99\n$EndElements",
        ),
        ("30 1 0 10 20", "30 1 0 10 99"),
        ("2.2 0 8", "2.2 1 8"),
    ] {
        assert!(read(&base.replacen(old, new, 1)).is_err(), "{new}");
    }
    let mut dataset = read(base).unwrap();
    dataset.mesh.points[0].id = i32::MAX as u64 + 1;
    let mut output = Vec::new();
    assert_eq!(write_22(&dataset, &mut output).unwrap_err().code, "E_MSH");
    assert_eq!(output, []);
}

/// Empty 2.2 geometry is a valid explicit mesh, not a missing section.
#[test]
fn empty_msh22_sections_are_preserved() {
    let source =
        "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n$Nodes\n0\n$EndNodes\n$Elements\n0\n$EndElements\n";
    let dataset = read(source).unwrap();
    assert_eq!(dataset.mesh.points, []);
    assert_eq!(dataset.mesh.cells, []);
}

/// Malformed physical metadata must fail before attaching misleading groups.
#[test]
fn malformed_physical_names_and_entities_are_rejected() {
    const HEADER: &str = "$MeshFormat\n4.1 0 8\n$EndMeshFormat\n";
    for (section, message) in [
        (
            "$PhysicalNames\n9999\n$EndPhysicalNames\n",
            "physical name count",
        ),
        (
            "$PhysicalNames\n1\n4 7 \"bad\"\n$EndPhysicalNames\n",
            "invalid physical name",
        ),
        (
            "$PhysicalNames\n1\n1 7 bad\n$EndPhysicalNames\n",
            "must be quoted",
        ),
        (
            "$PhysicalNames\n2\n1 7 \"edge\"\n1 7 \"duplicate\"\n$EndPhysicalNames\n",
            "duplicate physical group",
        ),
        (
            "$PhysicalNames\n0\n1 7 \"extra\"\n$EndPhysicalNames\n",
            "extra physical name",
        ),
        ("$Entities\n1 0 0\n$EndEntities\n", "four entity counts"),
        (
            "$Entities\n1 0 0 0\n1 0\n$EndEntities\n",
            "short entity record",
        ),
        (
            "$Entities\n1 0 0 0\n1 0 0 0 2 7\n$EndEntities\n",
            "entity physical count exceeds",
        ),
    ] {
        let error = read_projection(&format!("{HEADER}{section}")).unwrap_err();
        assert!(error.message.contains(message), "{section:?}: {error}");
    }
}

/// Reject inconsistent MSH 4.1 block metadata before assigning identity.
#[test]
fn msh41_rejects_inconsistent_entity_node_and_element_blocks() {
    // Git may check out text fixtures with CRLF on Windows; mutation anchors
    // describe logical MSH lines rather than checkout line endings.
    let base = include_str!("../../../../tests/fixtures/named-boundary.msh").replace("\r\n", "\n");
    for (old, new, expected) in [
        (
            "0 1 1 0\n1 0",
            "999999 1 1 0\n1 0",
            "entity count exceeds input",
        ),
        ("0 1 1 0\n1 0", "0 2 1 0\n1 0", "duplicate entity tag"),
        (
            "0 1 1 0\n1 0",
            "0 1 1 0\n1 0 0 0 1 0 0 0\n1 0",
            "extra entity records",
        ),
        ("4.1 0 8", "4.1 1 8", "only ASCII MSH with 8-byte"),
        (
            "1 3 1 3\n2 1 0 3",
            "999999 3 1 3\n2 1 0 3",
            "node counts exceed",
        ),
        ("2 1 0 3", "2 1 1 3", "parametric nodes are unsupported"),
        ("1\n2\n3\n0 0 0", "1\n1\n3\n0 0 0", "duplicate node tag"),
        (
            "2 2 1 2\n1 1 1 1",
            "999999 2 1 2\n1 1 1 1",
            "element counts exceed",
        ),
        (
            "1 1 1 1\n1 1 2",
            "2 1 1 1\n1 1 2",
            "element dimension mismatch",
        ),
    ] {
        assert!(base.contains(old), "missing fixture anchor {old:?}");
        let changed = base.replacen(old, new, 1);
        let error = read_projection(&changed).unwrap_err();
        assert!(error.message.contains(expected), "{new}: {error}");
    }
}

/// Both MSH dialects reject source property IDs and unquotable field names.
#[test]
fn writers_reject_unrepresentable_properties_and_field_names() {
    let original = read(include_str!(
        "../../../../tests/fixtures/gmsh-2.2-mixed.msh"
    ))
    .unwrap();
    let mut invalid = original.clone();
    invalid.mesh.cells[0].property_id = Some(7);
    assert!(
        write(&invalid, Vec::new())
            .unwrap_err()
            .message
            .contains("property IDs")
    );
    assert!(
        write_22(&invalid, Vec::new())
            .unwrap_err()
            .message
            .contains("property IDs")
    );
    invalid = original;
    invalid.fields.push(Field {
        name: "bad\"name".into(),
        location: FieldLocation::Point,
        components: vec!["C1".into()],
        values: vec![0.0; invalid.mesh.points.len()],
        step: None,
        time: None,
    });
    assert!(
        write(&invalid, Vec::new())
            .unwrap_err()
            .message
            .contains("field name")
    );
    assert!(
        write_22(&invalid, Vec::new())
            .unwrap_err()
            .message
            .contains("field name")
    );
}