caexfer 0.1.0

Exchange linear finite-element meshes and results across CAE formats
Documentation
//! Output installation races and failure cleanup.

use super::*;

struct Scratch(PathBuf);

impl Scratch {
    fn new() -> Self {
        let path = std::env::temp_dir().join(format!(
            "caexfer-cli-output-test-{}-{}",
            std::process::id(),
            COUNTER.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir(&path).unwrap();
        Self(path)
    }

    fn path(&self, name: &str) -> PathBuf {
        self.0.join(name)
    }
}

impl Drop for Scratch {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.0);
    }
}

#[test]
fn no_clobber_install_handles_races_and_missing_staging() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let destination = scratch.path("output.vtu");
    fs::write(&source, b"source").unwrap();

    let pending = PendingOutput::new(&destination, &source, false).unwrap();
    fs::write(pending.temporary(), b"staged").unwrap();
    fs::write(&destination, b"another writer").unwrap();
    assert_eq!(pending.install().unwrap_err().code, "E_EXISTS");
    assert_eq!(fs::read(&destination).unwrap(), b"another writer");
    drop(pending);

    let fresh = scratch.path("fresh.vtu");
    let pending = PendingOutput::new(&fresh, &source, false).unwrap();
    assert_eq!(pending.install().unwrap_err().code, "E_OUTPUT");
    assert!(!fresh.exists());
}

#[test]
fn failed_replacement_preserves_existing_bytes() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let destination = scratch.path("output.vtu");
    fs::write(&source, b"source").unwrap();
    fs::write(&destination, b"existing").unwrap();

    let pending = PendingOutput::new(&destination, &source, true).unwrap();
    fs::write(pending.temporary(), b"staged").unwrap();
    fs::remove_file(pending.temporary()).unwrap();
    assert_eq!(pending.install().unwrap_err().code, "E_OUTPUT");
    assert_eq!(fs::read(&destination).unwrap(), b"existing");
}

#[test]
fn paired_install_checks_both_destinations_before_install() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let first_path = scratch.path("results.op2");
    let second_path = scratch.path("mesh.bdf");
    fs::write(&source, b"source").unwrap();
    let first = PendingOutput::new(&first_path, &source, false).unwrap();
    let second = PendingOutput::new(&second_path, &source, false).unwrap();
    fs::write(first.temporary(), b"results").unwrap();
    fs::write(second.temporary(), b"mesh").unwrap();
    fs::write(&second_path, b"another writer").unwrap();

    let error = install_pair(&first, &second).unwrap_err();
    assert_eq!(error.code, "E_EXISTS");
    assert!(!first_path.exists());
    assert_eq!(fs::read(&second_path).unwrap(), b"another writer");
}

#[test]
fn paired_replacement_updates_both_outputs() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let first_path = scratch.path("results.op2");
    let second_path = scratch.path("mesh.bdf");
    fs::write(&source, b"source").unwrap();
    fs::write(&first_path, b"old results").unwrap();
    fs::write(&second_path, b"old mesh").unwrap();
    let first = PendingOutput::new(&first_path, &source, true).unwrap();
    let second = PendingOutput::new(&second_path, &source, true).unwrap();
    fs::write(first.temporary(), b"new results").unwrap();
    fs::write(second.temporary(), b"new mesh").unwrap();

    install_pair(&first, &second).unwrap();
    assert_eq!(fs::read(&first_path).unwrap(), b"new results");
    assert_eq!(fs::read(&second_path).unwrap(), b"new mesh");
}

#[test]
fn paired_replacement_rechecks_both_before_changing_either() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let first_path = scratch.path("results.op2");
    let second_path = scratch.path("mesh.bdf");
    fs::write(&source, b"source").unwrap();
    fs::write(&first_path, b"old results").unwrap();
    fs::write(&second_path, b"old mesh").unwrap();
    let first = PendingOutput::new(&first_path, &source, true).unwrap();
    let second = PendingOutput::new(&second_path, &source, true).unwrap();
    fs::write(first.temporary(), b"new results").unwrap();
    fs::write(second.temporary(), b"new mesh").unwrap();
    fs::remove_file(&second_path).unwrap();
    fs::create_dir(&second_path).unwrap();

    assert_eq!(install_pair(&first, &second).unwrap_err().code, "E_USAGE");
    assert_eq!(fs::read(&first_path).unwrap(), b"old results");
    assert!(second_path.is_dir());
}

#[test]
fn staging_reports_missing_parent() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    fs::write(&source, b"source").unwrap();
    let destination = scratch.path("missing").join("output.vtu");
    assert_eq!(
        PendingOutput::new(&destination, &source, false)
            .err()
            .unwrap()
            .code,
        "E_IO"
    );
    assert!(!destination.exists());
}

#[cfg(unix)]
#[test]
fn invalid_parent_reports_io_for_both_destination_modes() {
    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let parent = scratch.path("ordinary-file");
    fs::write(&source, b"source").unwrap();
    fs::write(&parent, b"keep").unwrap();
    let destination = parent.join("output.vtu");
    for overwrite in [false, true] {
        assert_eq!(
            PendingOutput::new(&destination, &source, overwrite)
                .err()
                .unwrap()
                .code,
            "E_IO"
        );
    }
    assert_eq!(fs::read(&parent).unwrap(), b"keep");
}

#[cfg(unix)]
#[test]
fn overwrite_rechecks_for_a_symlink_created_during_staging() {
    use std::os::unix::fs::symlink;

    let scratch = Scratch::new();
    let source = scratch.path("source.bdf");
    let target = scratch.path("target.vtu");
    let destination = scratch.path("output.vtu");
    fs::write(&source, b"source").unwrap();
    fs::write(&target, b"keep").unwrap();
    let pending = PendingOutput::new(&destination, &source, true).unwrap();
    fs::write(pending.temporary(), b"staged").unwrap();
    symlink("target.vtu", &destination).unwrap();

    assert_eq!(pending.install().unwrap_err().code, "E_USAGE");
    assert_eq!(fs::read(&target).unwrap(), b"keep");
    assert!(
        fs::symlink_metadata(&destination)
            .unwrap()
            .file_type()
            .is_symlink()
    );
}