caexfer 0.1.0

Exchange linear finite-element meshes and results across CAE formats
Documentation
//! BDF geometry parser tests, including malformed input and lossy projection.

use crate::core::CellKind;
use crate::formats::bdf;
use std::fmt::Write as _;

/// Small deck with stable GRID and element IDs for read/write checks.
const TRI: &str =
    "GRID,10,,0.,0.,0.\nGRID,20,,1.,0.,0.\nGRID,30,,0.,1.,0.\nCTRIA3,100,7,10,20,30\n";

/// Build one fixed-width BDF physical line with an optional continuation label.
fn fixed(head: &str, fields: &[&str], width: usize, tail: &str) -> String {
    let capacity = if width == 16 { 4 } else { 8 };
    let mut text = format!("{head:<8}");
    for index in 0..capacity {
        write!(text, "{:>width$}", fields.get(index).copied().unwrap_or("")).unwrap();
    }
    writeln!(text, "{tail:<8}").unwrap();
    text
}

/// Keep BDF GRID IDs and coordinates across free and fixed field syntax.
#[test]
fn free_and_fixed_geometry_keep_ids_and_coordinates() {
    let projection = bdf::mesh::read(TRI).unwrap();
    assert_eq!(
        projection
            .mesh
            .points
            .iter()
            .map(|point| point.id)
            .collect::<Vec<_>>(),
        [10, 20, 30]
    );
    assert_eq!(projection.mesh.cells[0].kind, CellKind::Triangle3);
    assert_eq!(projection.mesh.cells[0].connectivity, [0, 1, 2]);
    assert_eq!(projection.mesh.cells[0].property_id, Some(7));

    let small = fixed("GRID", &["7", "", "1.2-3", "2.", "3."], 8, "");
    assert_eq!(
        bdf::mesh::read(small).unwrap().mesh.points[0]
            .position
            .map(f64::to_bits),
        [0.0012_f64, 2.0, 3.0].map(f64::to_bits)
    );
    let large = fixed("GRID*", &["7", "", "1.234567890123", "2."], 16, "*A")
        + &fixed("*A", &["3.", "", "", ""], 16, "");
    assert_eq!(
        bdf::mesh::read(large).unwrap().mesh.points[0]
            .position
            .map(f64::to_bits),
        [1.234_567_890_123_f64, 2.0, 3.0].map(f64::to_bits)
    );
}

/// Accept supported deck wrappers and reject malformed continuation structure.
#[test]
fn full_decks_and_continuations_are_bounded() {
    // A complete deck needs exactly one bulk marker; punch input needs none.
    let full = "SOL 101\nCEND\nBEGIN,BULK\nGRID,1\nENDDATA\n";
    assert_eq!(bdf::mesh::read(full).unwrap().mesh.points.len(), 1);
    assert_eq!(
        bdf::mesh::read("SOL 101\nCEND\nGRID,1\n").unwrap_err().code,
        "E_DECK"
    );
    assert_eq!(
        bdf::mesh::read("BEGIN BULK\nGRID,1\nBEGIN BULK\nGRID,2\n")
            .unwrap_err()
            .code,
        "E_DECK"
    );
    assert_eq!(
        bdf::mesh::read("+,1,2\n").unwrap_err().code,
        "E_CONTINUATION"
    );
    let mismatch = fixed("GRID", &["1"], 8, "+A") + &fixed("+B", &["2"], 8, "");
    assert_eq!(
        bdf::mesh::read(mismatch).unwrap_err().code,
        "E_CONTINUATION"
    );
    assert_eq!(
        bdf::mesh::read("INCLUDE 'unfinished\n").unwrap_err().code,
        "E_INCLUDE"
    );
}

/// Refuse BDF geometry whose frames or cards cannot be projected safely.
#[test]
fn unsupported_geometry_and_frames_fail() {
    for (source, code) in [
        ("GRID,1,7,1.,2.,3.\n", "E_COORDINATE_SYSTEM"),
        ("GRDSET,,7\nGRID,1\n", "E_GRDSET"),
        ("INCLUDE 'other.bdf'\nGRID,1\n", "E_INCLUDE_UNRESOLVED"),
        ("GRID,1\nCBUSH,10,7,1,2\n", "E_UNSUPPORTED_CARD"),
        (
            "GRID,1\nCTETRA,10,7,1,2,3,4,5,6\n+,7,8,9,10\n",
            "E_HIGH_ORDER",
        ),
    ] {
        assert_eq!(bdf::mesh::read(source).unwrap_err().code, code, "{source}");
    }
}

/// Reject invalid IDs and references without returning partial BDF geometry.
#[test]
fn malformed_ids_and_connectivity_fail_without_partial_mesh() {
    // Each case isolates a different condition that would make projected
    // geometry incomplete or ambiguous if parsing continued.
    for (source, code) in [
        ("GRID,1,,0,0,0\nGRID,1,,1,1,1\n", "E_DUPLICATE_GRID"),
        ("GRID,1\nCROD,10,7,1,999\n", "E_MISSING_GRID"),
        ("GRID,1\nCROD,10,7,1,1\n", "E_DEGENERATE_CONNECTIVITY"),
        ("GRID,1,,not-a-real,0,0\n", "E_REAL"),
        ("GRID,1,,\t0,0,0\n", "E_TAB"),
        ("GRID,1,,π,0,0\n", "E_ENCODING"),
        ("GRID,0\n", "E_INTEGER"),
        ("GRID,1,,0,0,0,-2\n", "E_INTEGER"),
        ("GRID,1,,0,0,0,,11\n", "E_GRID_PS"),
        ("GRID,1,,0,0,0,,,0\n+,extra\n", "E_GRID_FIELDS"),
        ("GRID,1,,0,0,0,,,x\n", "E_INTEGER"),
        (
            "GRID,1,,0,0,0,,,,\nGRID,2\nCROD,10,7,1,2\nCROD,10,7,1,2\n",
            "E_DUPLICATE_ELEMENT",
        ),
        ("GRID,1,,0,0,0,,,1\n", "E_SUPERELEMENT"),
    ] {
        assert_eq!(bdf::mesh::read(source).unwrap_err().code, code, "{source}");
    }
    assert_eq!(
        bdf::mesh::read("GRID,1\nCROD,10,7,1,999\n")
            .unwrap_err()
            .line,
        Some(2)
    );
}

/// Report solver cards omitted from BDF geometry and detect nonbasic output frames.
#[test]
fn opaque_solver_cards_are_reported() {
    let projection = bdf::mesh::read(format!("{TRI}MAT1,7,not-a-number\n")).unwrap();
    assert!(
        projection
            .omissions
            .iter()
            .any(|item| item.category == "MAT1")
    );
    assert!(!projection.has_nonbasic_output_frame);
    let nonbasic = bdf::mesh::read("GRID,1,,0,0,0,7\n").unwrap();
    assert!(nonbasic.has_nonbasic_output_frame);
}

/// Enforce BDF input limits and read back a geometry-only writer result.
#[test]
fn byte_limit_and_writer_are_explicit() {
    assert_eq!(
        bdf::mesh::read_from(TRI.as_bytes(), 5).unwrap_err().code,
        "E_LIMIT"
    );
    let projection = bdf::mesh::read(TRI).unwrap();
    let mut output = Vec::new();
    bdf::mesh::write(&projection.mesh, &mut output).unwrap();
    assert_eq!(bdf::mesh::read(&output).unwrap().mesh.cells[0].id, 100);
    assert_ne!(output, TRI.as_bytes());
}

/// Sort projected IDs and round-trip every supported linear BDF family.
#[test]
fn deterministic_ids_and_all_linear_families() {
    // Reverse source order so sorted output cannot pass by accident.
    let mut nodes = String::new();
    for id in (1..=8).rev() {
        writeln!(&mut nodes, "GRID,{id},,{id},0,0").unwrap();
    }
    let source = nodes
        + "CROD,1,7,1,2\nCONROD,2,1,2,8,1.\nCBAR,3,7,1,2,0.,1.,0.\nCBEAM,4,7,1,2,0.,1.,0.\nCTRIA3,5,7,1,2,3\nCQUAD4,6,7,1,2,3,4\nCTETRA,7,7,1,2,3,4\nCHEXA,8,7,1,2,3,4,5,6\n+,7,8\nCPENTA,9,7,1,2,3,4,5,6\nCPYRAM,10,7,1,2,3,4,5\n";
    let mesh = bdf::mesh::read(source).unwrap().mesh;
    assert_eq!(
        mesh.points.iter().map(|point| point.id).collect::<Vec<_>>(),
        [1, 2, 3, 4, 5, 6, 7, 8]
    );
    assert_eq!(mesh.cells.len(), 10);
    assert_eq!(mesh.cells[1].property_id, None);
    assert_eq!(mesh.cells[7].kind, CellKind::Hex8);
    let mut output = Vec::new();
    bdf::mesh::write(&mesh, &mut output).unwrap();
    let rewritten = bdf::mesh::read(&output).unwrap().mesh;
    assert_eq!(rewritten.cells.len(), 10);
    assert_eq!(rewritten.cells[7].kind, CellKind::Hex8);
}

/// Keep byte-level BDF parsing panic-free for every one-byte input.
#[test]
fn arbitrary_short_inputs_do_not_panic() {
    for byte in 0..=255_u8 {
        let _ = bdf::mesh::read([byte]);
    }
}