guix-scheme 0.2.0

Read, edit, and generate Guix Scheme files (channels.scm, system config.scm) from Rust
Documentation
//! Services view and record-type view coverage on the real fixtures.

use guix_scheme::record::RecordViewMut;
use guix_scheme::{ModuleFile, ServicesView, ServicesViewMut};
use scheme_edit::Document;
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()
}

/// Runs F on a mutable services view of the operating-system form.
fn with_services<F: FnOnce(&mut ServicesViewMut)>(doc: &mut Document, f: F) {
    let os = doc
        .forms_mut()
        .find(|n| n.head_symbol() == Some("operating-system"))
        .unwrap();
    let mut rv = RecordViewMut::new(os).unwrap();
    let services = rv.field_mut("services").unwrap();
    let mut sv = ServicesViewMut::new(services);
    f(&mut sv);
}

#[test]
fn live_panther_remove_then_add_service() {
    let src = fx("system", "live-panther.scm");
    let mut doc = Document::parse(&src).unwrap();
    with_services(&mut doc, |sv| {
        assert_eq!(
            sv.service_types(),
            vec!["openssh-service-type", "xfce-desktop-service-type"]
        );
        sv.remove_service("xfce-desktop-service-type").unwrap();
        sv.add_service("plasma-desktop-service-type", None).unwrap();
        assert!(sv.has_service("plasma-desktop-service-type"));
        assert!(!sv.has_service("xfce-desktop-service-type"));
    });
    let out = doc.to_string();
    let re = Document::parse(&out).unwrap();
    assert_eq!(re.to_string(), out);
    // openssh line and tail byte-intact
    assert!(out.contains("                          (service openssh-service-type)"));
    assert!(out.contains("                    %os-desktop-services)))"));
    assert!(!out.contains("xfce"));
}

#[test]
fn live_panther_replace_service_type_single_line_diff() {
    let src = fx("system", "live-panther.scm");
    let mut doc = Document::parse(&src).unwrap();
    with_services(&mut doc, |sv| {
        assert!(sv.replace_service_type("xfce-desktop-service-type", "plasma-desktop-service-type"));
    });
    let out = doc.to_string();
    assert!(out.contains("(service plasma-desktop-service-type)"));
    assert!(!out.contains("xfce"));
    assert_eq!(diff_lines(&src, &out), 1);
}

#[test]
fn add_service_with_config_to_list() {
    let mut doc = Document::parse("(list)").unwrap();
    {
        let form = doc.forms_mut().next().unwrap();
        let mut sv = ServicesViewMut::new(form);
        sv.add_service(
            "openssh-service-type",
            Some("(openssh-configuration (permit-root-login 'prohibit-password))"),
        )
        .unwrap();
    }
    let out = doc.to_string();
    Document::parse(&out).unwrap();
    assert!(out.contains("openssh-service-type"));
    assert!(out.contains("openssh-configuration"));
    assert!(out.contains("'prohibit-password"));
    let re = Document::parse(&out).unwrap();
    let v = ServicesView::new(re.forms().next().unwrap());
    assert_eq!(v.service_types(), vec!["openssh-service-type"]);
}

#[test]
fn add_service_to_bare_symbol_rewrites_to_append() {
    let src = "(operating-system\n  (services %desktop-services))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        assert!(sv.service_types().is_empty());
        sv.add_service("openssh-service-type", None).unwrap();
    });
    let out = doc.to_string();
    assert!(out.contains("(append (list (service openssh-service-type)) %desktop-services)"));
    assert!(out.lines().all(|l| l.len() <= 80), "long line in:\n{out}");
    let re = Document::parse(&out).unwrap();
    assert_eq!(re.to_string(), out);
}

#[test]
fn modify_services_reads_safely_and_refuses_edits() {
    let src = "(operating-system\n  (services (modify-services %desktop-services\n               (guix-service-type config => config))))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        assert!(sv.service_types().is_empty());
        assert!(!sv.has_service("guix-service-type"));
        assert!(sv.add_service("openssh-service-type", None).is_err());
        assert!(sv.remove_service("guix-service-type").is_err());
        assert!(!sv.replace_service_type("guix-service-type", "other"));
    });
    assert_eq!(doc.to_string(), src, "tree must be untouched");
}

#[test]
fn remove_last_service_from_cons_collapses() {
    let src = "(operating-system\n  (services (cons (service foo-service-type) %base-services)))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        sv.remove_service("foo-service-type").unwrap();
        assert!(!sv.has_service("foo-service-type"));
    });
    let out = doc.to_string();
    assert!(out.contains("(cons* %base-services)"), "got:\n{out}");
    assert!(!out.contains("(cons %base-services)"));
    assert!(!out.contains("foo-service-type"));
    Document::parse(&out).unwrap();
}

#[test]
fn remove_last_service_from_cons_star_collapses() {
    let src = "(operating-system\n  (services (cons* (service foo-service-type) %base-services)))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        sv.remove_service("foo-service-type").unwrap();
    });
    let out = doc.to_string();
    assert!(out.contains("(cons* %base-services)"), "got:\n{out}");
    Document::parse(&out).unwrap();
}

#[test]
fn remove_from_cons_with_two_services_does_not_collapse() {
    let src = "(operating-system\n  (services (cons (service a-service-type) %base-services)))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        sv.add_service("b-service-type", None).unwrap();
        sv.remove_service("a-service-type").unwrap();
        assert!(sv.has_service("b-service-type"));
    });
    let out = doc.to_string();
    assert!(out.contains("(service b-service-type)"));
    assert!(out.contains("%base-services"));
    assert!(
        !out.contains("(cons* %base-services)"),
        "must not collapse:\n{out}"
    );
    let re = Document::parse(&out).unwrap();
    assert_eq!(re.to_string(), out);
}

#[test]
fn append_with_list_not_first_is_refused_for_edits() {
    let src =
        "(operating-system\n  (services (append %base-services (list (service a-service-type)))))";
    let mut doc = Document::parse(src).unwrap();
    with_services(&mut doc, |sv| {
        // reads stay lenient
        assert_eq!(sv.service_types(), vec!["a-service-type"]);
        assert!(sv.add_service("b-service-type", None).is_err());
        assert!(sv.remove_service("a-service-type").is_err());
        assert!(!sv.replace_service_type("a-service-type", "b-service-type"));
    });
    assert_eq!(doc.to_string(), src, "tree must be untouched");
}

#[test]
fn avahi_record_type_fields() {
    let mf = ModuleFile::parse(&fx("services", "avahi.scm")).unwrap();
    let rt = mf.record_type("<avahi-configuration>").unwrap();
    assert_eq!(rt.name(), Some("<avahi-configuration>"));
    assert_eq!(rt.constructor(), Some("avahi-configuration"));
    assert_eq!(rt.predicate(), Some("avahi-configuration?"));
    let fields = rt.fields();
    let names: Vec<_> = fields.iter().filter_map(|f| f.name()).collect();
    assert_eq!(
        names,
        vec![
            "avahi",
            "debug?",
            "host-name",
            "publish?",
            "publish-workstation?",
            "ipv4?",
            "ipv6?",
            "wide-area?",
            "domains-to-browse",
        ]
    );
    assert_eq!(fields[0].getter(), Some("avahi-configuration-avahi"));
    assert_eq!(fields[1].getter(), Some("avahi-configuration-debug?"));
    assert_eq!(fields[0].default().as_deref(), Some("avahi"));
    assert_eq!(fields[1].default().as_deref(), Some("#f"));
    assert_eq!(fields[8].default().as_deref(), Some("'()"));
}

#[test]
fn auditd_record_type_fields() {
    let mf = ModuleFile::parse(&fx("services", "auditd.scm")).unwrap();
    let rt = mf.record_type("<auditd-configuration>").unwrap();
    assert_eq!(rt.constructor(), Some("auditd-configuration"));
    assert_eq!(rt.predicate(), Some("auditd-configuration?"));
    let fields = rt.fields();
    let names: Vec<_> = fields.iter().filter_map(|f| f.name()).collect();
    assert_eq!(names, vec!["audit", "configuration-directory"]);
    assert_eq!(fields[0].default().as_deref(), Some("audit"));
    assert_eq!(fields[1].default(), None, "field without default clause");
}

#[test]
fn hurd_record_type_field_counts() {
    let mf = ModuleFile::parse(&fx("services", "hurd.scm")).unwrap();
    let console = mf.record_type("<hurd-console-configuration>").unwrap();
    assert_eq!(console.fields().len(), 1);
    let getty = mf.record_type("<hurd-getty-configuration>").unwrap();
    let fields = getty.fields();
    assert_eq!(fields.len(), 3);
    let names: Vec<_> = fields.iter().filter_map(|f| f.name()).collect();
    assert_eq!(names, vec!["hurd", "tty", "baud-rate"]);
    assert_eq!(fields[1].default(), None);
    assert_eq!(fields[2].default().as_deref(), Some("38400"));
}