fusible 0.19.1

Electronic linter: a fuse that blows to block firmware code generation
use std::path::{Path, PathBuf};

use super::eject::update_manifest;
use super::eject_files::{publish_new, staging_path, write_atomic};
use super::BodyError;
use crate::config::FusibleConfig;

const MANIFEST: &str = "\
# my rover
boards:
  main: { entry: boards/main.json }
bodies:
  lid:
    generate: enclosure  # regenerated
    wall_thickness: 1.5
";

fn fresh(name: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!("fusible_eject_{}_{name}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

fn leftovers(dir: &Path) -> Vec<String> {
    let names = std::fs::read_dir(dir).unwrap().flatten();
    let names = names.map(|e| e.file_name().to_string_lossy().into_owned());
    names.filter(|n| n.ends_with(".tmp")).collect()
}

#[test]
fn staging_file_sits_next_to_the_final_path() {
    let staged = staging_path(Path::new("bodies/lid.step"));
    assert_eq!(staged, Path::new("bodies/lid.step.tmp"));
}

#[test]
fn the_manifest_is_replaced_whole_and_keeps_its_comments() {
    let dir = fresh("update");
    let manifest = dir.join("fusible.yml");
    std::fs::write(&manifest, MANIFEST).unwrap();
    update_manifest(&manifest, "lid", "bodies/lid.step").unwrap();
    let text = std::fs::read_to_string(&manifest).unwrap();
    let cfg = FusibleConfig::from_str(&text).unwrap();
    assert_eq!(
        cfg.bodies["lid"].source.as_deref(),
        Some(Path::new("bodies/lid.step"))
    );
    assert!(cfg.bodies["lid"].generate.is_none());
    assert!(text.starts_with("# my rover\n") && !text.contains("wall_thickness"));
    assert!(leftovers(&dir).is_empty());
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn an_unsafe_manifest_fails_with_the_lines_to_change_and_stays_untouched() {
    let dir = fresh("unsafe");
    let manifest = dir.join("fusible.yml");
    let crlf = MANIFEST.replace('\n', "\r\n");
    std::fs::write(&manifest, &crlf).unwrap();
    let err = update_manifest(&manifest, "lid", "bodies/lid.step").unwrap_err();
    assert!(matches!(&err, BodyError::ManifestUnchanged { name, .. } if name == "lid"));
    let help = miette::Diagnostic::help(&err).unwrap().to_string();
    assert!(help.contains("add `source: bodies/lid.step`"), "{help}");
    assert_eq!(std::fs::read_to_string(&manifest).unwrap(), crlf);
    assert!(leftovers(&dir).is_empty());
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn publishing_never_replaces_an_existing_file() {
    let dir = fresh("publish");
    let (staged, path) = (dir.join("lid.step.tmp"), dir.join("lid.step"));
    std::fs::write(&staged, "generated").unwrap();
    std::fs::write(&path, "mine").unwrap();
    let err = publish_new(&staged, &path).unwrap_err();
    assert!(matches!(err, BodyError::Exists { .. }));
    assert_eq!(std::fs::read_to_string(&path).unwrap(), "mine");
    assert!(!staged.exists());
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn atomic_write_replaces_the_file_and_keeps_its_permissions() {
    let dir = fresh("atomic");
    let path = dir.join("fusible.yml");
    std::fs::write(&path, "old").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).unwrap();
    }
    write_atomic(&path, "new").unwrap();
    assert_eq!(std::fs::read_to_string(&path).unwrap(), "new");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mode = std::fs::metadata(&path).unwrap().permissions().mode();
        assert_eq!(mode & 0o777, 0o640);
    }
    assert!(leftovers(&dir).is_empty());
    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn publishing_moves_the_staged_file_into_place() {
    let dir = fresh("move");
    let (staged, path) = (dir.join("lid.step.tmp"), dir.join("lid.step"));
    std::fs::write(&staged, "generated").unwrap();
    publish_new(&staged, &path).unwrap();
    assert_eq!(std::fs::read_to_string(&path).unwrap(), "generated");
    assert!(leftovers(&dir).is_empty());
    std::fs::remove_dir_all(dir).unwrap();
}