guix-scheme 0.2.0

Read, edit, and generate Guix Scheme files (channels.scm, system config.scm) from Rust
Documentation
//! Module-layer read/write coverage driven by the real guix fixtures.

use guix_scheme::ModuleFile;
use std::path::Path;

fn fx(dir: &str, name: &str) -> String {
    let p = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../scheme-edit/tests/fixtures")
        .join(dir)
        .join(name);
    std::fs::read_to_string(p).unwrap()
}

fn diff_lines(a: &str, b: &str) -> usize {
    a.lines().zip(b.lines()).filter(|(x, y)| x != y).count()
}

fn assert_roundtrip_stable(out: &str) {
    assert_eq!(ModuleFile::parse(out).unwrap().to_string(), out);
}

#[test]
fn cpio_reads() {
    let src = fx("packages", "cpio.scm");
    let mf = ModuleFile::parse(&src).unwrap();
    assert_eq!(mf.to_string(), src);
    assert!(mf.package_names().contains(&"cpio".to_string()));
    let p = mf.package("cpio").unwrap();
    assert_eq!(p.name().as_deref(), Some("cpio"));
    assert_eq!(p.version().as_deref(), Some("2.15"));
    assert_eq!(
        p.synopsis().as_deref(),
        Some("Manage cpio and tar file archives")
    );
    assert_eq!(
        p.home_page().as_deref(),
        Some("https://www.gnu.org/software/cpio/")
    );
    assert_eq!(
        p.origin_sha256_base32().as_deref(),
        Some("1nvmj2mc3nagpig75sgzvkqgzg1p01wgnlw54v4ix6ijgjwi0xlk")
    );
    // cpio has no inputs fields at all.
    assert!(p.inputs().is_empty());
    assert!(p.native_inputs().is_empty());
    assert!(p.propagated_inputs().is_empty());
}

#[test]
fn cpio_set_version_is_single_line_diff() {
    let src = fx("packages", "cpio.scm");
    let mut mf = ModuleFile::parse(&src).unwrap();
    mf.package_mut("cpio").unwrap().set_version("2.16");
    let out = mf.to_string();
    assert!(out.contains("(version \"2.16\")"));
    assert_eq!(diff_lines(&src, &out), 1);
    assert_roundtrip_stable(&out);
    let re = ModuleFile::parse(&out).unwrap();
    assert_eq!(
        re.package("cpio").unwrap().version().as_deref(),
        Some("2.16")
    );
}

#[test]
fn cpio_set_hash_is_single_line_diff() {
    let src = fx("packages", "cpio.scm");
    let new_hash = "0000000000000000000000000000000000000000000000000000";
    let mut mf = ModuleFile::parse(&src).unwrap();
    assert!(mf
        .package_mut("cpio")
        .unwrap()
        .set_origin_sha256_base32(new_hash));
    let out = mf.to_string();
    assert_eq!(diff_lines(&src, &out), 1);
    assert_roundtrip_stable(&out);
    let re = ModuleFile::parse(&out).unwrap();
    assert_eq!(
        re.package("cpio")
            .unwrap()
            .origin_sha256_base32()
            .as_deref(),
        Some(new_hash)
    );
}

#[test]
fn abiword_inputs_read() {
    let src = fx("packages", "abiword.scm");
    let mf = ModuleFile::parse(&src).unwrap();
    let p = mf.package("abiword").unwrap();
    assert_eq!(p.version().as_deref(), Some("3.0.7"));
    let inputs = p.inputs();
    // 23 entries in the file; `(librsvg-for-system)` is a call, skipped.
    assert_eq!(inputs.len(), 22);
    for known in ["aspell", "boost", "gtk+", "readline", "zlib"] {
        assert!(inputs.contains(&known.to_string()), "{known}");
    }
    let native = p.native_inputs();
    // 15 entries; the quasiquoted `(,glib "bin") pair is skipped.
    assert_eq!(native.len(), 14);
    assert!(native.contains(&"pkg-config".to_string()));
    assert!(p.propagated_inputs().is_empty());
}

#[test]
fn abiword_replace_input_is_single_line_diff() {
    let src = fx("packages", "abiword.scm");
    let mut mf = ModuleFile::parse(&src).unwrap();
    assert!(mf
        .package_mut("abiword")
        .unwrap()
        .replace_input("readline", "libedit"));
    let out = mf.to_string();
    assert_eq!(diff_lines(&src, &out), 1);
    assert_roundtrip_stable(&out);
    let re = ModuleFile::parse(&out).unwrap();
    let inputs = re.package("abiword").unwrap().inputs();
    assert!(!inputs.contains(&"readline".to_string()));
    assert!(inputs.contains(&"libedit".to_string()));
    // every non-edited line byte-identical
    let changed: Vec<(&str, &str)> = src
        .lines()
        .zip(out.lines())
        .filter(|(a, b)| a != b)
        .collect();
    assert_eq!(changed.len(), 1);
    assert!(changed[0].0.contains("readline"));
    assert!(changed[0].1.contains("libedit"));
}

#[test]
fn diagram_let_wrapped_packages() {
    let src = fx("packages", "diagram.scm");
    let mf = ModuleFile::parse(&src).unwrap();
    let names = mf.package_names();
    assert_eq!(names, vec!["pikchr".to_string(), "dpic".to_string()]);
    for name in &names {
        let p = mf.package(name).unwrap();
        assert!(p.name().is_some_and(|n| !n.is_empty()));
        // Both diagram packages compute their version (string-append /
        // git-version), so the string read is None by design.
        assert_eq!(p.version(), None);
    }
    assert_eq!(
        mf.package("dpic").unwrap().home_page().as_deref(),
        Some("https://gitlab.com/aplevich/dpic")
    );
    assert_eq!(
        mf.package("pikchr").unwrap().synopsis().as_deref(),
        Some("Markup language for diagrams in technical documentation")
    );
}

#[test]
fn service_modules_record_types() {
    let avahi = ModuleFile::parse(&fx("services", "avahi.scm")).unwrap();
    assert_eq!(
        avahi.record_type_names(),
        vec!["<avahi-configuration>".to_string()]
    );
    assert!(avahi.package_names().is_empty());

    let auditd = ModuleFile::parse(&fx("services", "auditd.scm")).unwrap();
    assert_eq!(
        auditd.record_type_names(),
        vec!["<auditd-configuration>".to_string()]
    );
}

#[test]
fn hurd_module_parses_and_roundtrips() {
    let src = fx("services", "hurd.scm");
    let mf = ModuleFile::parse(&src).unwrap();
    assert_eq!(mf.to_string(), src);
    assert_eq!(
        mf.record_type_names(),
        vec![
            "<hurd-console-configuration>".to_string(),
            "<hurd-getty-configuration>".to_string(),
        ]
    );
}