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()
}
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);
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| {
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"));
}