openbim-dt 0.2.0

Lossless ISO 23387 data-template model and XML codec for Rust.
Documentation
use std::{fs, process::Command};

const FIXTURE: &str = concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/tests/fixtures/synthetic-library.xml"
);

#[test]
fn inspect_reports_dt_root_and_diagnostics() {
    let output = Command::new(env!("CARGO_BIN_EXE_openbim-dt"))
        .args(["inspect", FIXTURE])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("root=Library"));
    assert!(stdout.contains("errors=0"));
}

#[test]
fn validate_uses_a_distinct_exit_code_for_semantic_errors() {
    let malformed =
        std::env::temp_dir().join(format!("openbim-dt-invalid-{}.xml", std::process::id()));
    fs::write(
        &malformed,
        r#"<dt:Property xmlns:dt="https://standards.iso.org/iso/23387/ed-2/en/" dt:GUID="bad"/>"#,
    )
    .unwrap();
    let output = Command::new(env!("CARGO_BIN_EXE_openbim-dt"))
        .arg("validate")
        .arg(&malformed)
        .output()
        .unwrap();
    let _ = fs::remove_file(&malformed);
    assert_eq!(output.status.code(), Some(2));
    assert!(String::from_utf8_lossy(&output.stdout).contains("InvalidGuid"));
}

#[test]
fn rewrite_emits_a_reparseable_document() {
    let output_path =
        std::env::temp_dir().join(format!("openbim-dt-rewrite-{}.xml", std::process::id()));
    let output = Command::new(env!("CARGO_BIN_EXE_openbim-dt"))
        .args(["rewrite", FIXTURE, output_path.to_str().unwrap()])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let xml = fs::read_to_string(&output_path).unwrap();
    let _ = fs::remove_file(&output_path);
    openbim_dt::Document::parse(&xml).unwrap();
}

#[cfg(unix)]
#[test]
fn rewrite_does_not_follow_a_precreated_temporary_symlink() {
    let directory = std::env::temp_dir().join(format!("openbim-dt-symlink-{}", std::process::id()));
    fs::create_dir(&directory).unwrap();
    let victim = directory.join("victim");
    fs::write(&victim, "DO-NOT-OVERWRITE\n").unwrap();

    let script = r#"set -eu
printf '%s' "$$" > "$1/child-pid"
ln -s "$1/victim" "$1/.output.xml.$$.tmp"
exec "$2" rewrite "$3" "$1/output.xml"
"#;
    let output = Command::new("bash")
        .args(["-c", script, "sh"])
        .arg(&directory)
        .arg(env!("CARGO_BIN_EXE_openbim-dt"))
        .arg(FIXTURE)
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(fs::read_to_string(&victim).unwrap(), "DO-NOT-OVERWRITE\n");
    let output_type = fs::symlink_metadata(directory.join("output.xml"))
        .unwrap()
        .file_type();
    assert!(output_type.is_file() && !output_type.is_symlink());

    let child_pid = fs::read_to_string(directory.join("child-pid")).unwrap();

    for path in [
        directory.join("output.xml"),
        directory.join(format!(".output.xml.{child_pid}.tmp")),
        directory.join("child-pid"),
        victim,
    ] {
        let _ = fs::remove_file(path);
    }
    let _ = fs::remove_dir(directory);
}