use std::{
io::Write,
path::PathBuf,
process::{Command, Stdio},
};
use tempfile::NamedTempFile;
fn find_sqryd_binary() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_sqryd") {
let p = PathBuf::from(path);
if p.is_file() {
return Some(p);
}
}
let binary_name = format!("sqryd{}", std::env::consts::EXE_SUFFIX);
let exe = std::env::current_exe().ok()?;
let deps_dir = exe.parent()?; let candidate = deps_dir.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
let profile_dir = deps_dir.parent()?; let candidate = profile_dir.join(&binary_name);
if candidate.is_file() {
return Some(candidate);
}
None
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "install-systemd-user is Linux-only; test skipped on this platform"
)]
fn install_systemd_user_output_parses_as_valid_unit_file() {
let Some(sqryd) = find_sqryd_binary() else {
eprintln!(
"SKIP install_systemd_user_output_parses_as_valid_unit_file: \
sqryd binary not found — run `cargo build -p sqry-daemon --bin sqryd` first"
);
return;
};
let output = Command::new(&sqryd)
.arg("install-systemd-user")
.env("RUST_LOG", "off")
.output()
.unwrap_or_else(|e| panic!("failed to spawn {:?}: {e}", sqryd));
assert!(
output.status.success(),
"sqryd install-systemd-user must exit 0; status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let unit_text = String::from_utf8_lossy(&output.stdout);
assert!(
!unit_text.trim().is_empty(),
"sqryd install-systemd-user must produce non-empty output"
);
assert!(
unit_text.contains("[Unit]"),
"unit file must contain [Unit] section; got:\n{unit_text}"
);
assert!(
unit_text.contains("[Service]"),
"unit file must contain [Service] section; got:\n{unit_text}"
);
assert!(
unit_text.contains("[Install]"),
"unit file must contain [Install] section; got:\n{unit_text}"
);
assert!(
unit_text.contains("Type=notify"),
"unit file must contain Type=notify; got:\n{unit_text}"
);
assert!(
unit_text.contains("foreground"),
"ExecStart must reference 'foreground' subcommand; got:\n{unit_text}"
);
assert!(
unit_text.contains("WantedBy=default.target"),
"user unit must target default.target; got:\n{unit_text}"
);
let analyze_available = Command::new("systemd-analyze")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if analyze_available {
let mut tmp = NamedTempFile::with_suffix(".service")
.expect("create temp .service file for systemd-analyze verify");
tmp.write_all(output.stdout.as_slice())
.expect("write unit text to temp file");
tmp.flush().expect("flush temp .service file");
let verify = Command::new("systemd-analyze")
.arg("verify")
.arg(tmp.path())
.output()
.expect("spawn systemd-analyze verify");
assert!(
verify.status.success(),
"systemd-analyze verify rejected the generated unit file (exit {}):\n\
stdout: {}\nstderr: {}\nunit text:\n{}",
verify.status,
String::from_utf8_lossy(&verify.stdout),
String::from_utf8_lossy(&verify.stderr),
unit_text,
);
} else {
eprintln!(
"NOTE: systemd-analyze not available — skipping verify step; \
structural content assertions still run"
);
}
}
#[test]
#[cfg_attr(
not(target_os = "linux"),
ignore = "install-systemd-system is Linux-only; test skipped on this platform"
)]
fn install_systemd_system_output_contains_percent_i_template() {
#[cfg(target_os = "linux")]
{
let Some(sqryd) = find_sqryd_binary() else {
eprintln!(
"SKIP install_systemd_system_output_contains_percent_i_template: \
sqryd binary not found — run `cargo build -p sqry-daemon --bin sqryd` first"
);
return;
};
let username = {
let os_name = users::get_current_username()
.and_then(|n| n.into_string().ok())
.filter(|n| !n.is_empty());
let env_name = std::env::var("USER").ok().filter(|n| !n.is_empty());
match os_name.or(env_name) {
Some(u) => u,
None => {
eprintln!(
"SKIP install_systemd_system_output_contains_percent_i_template: \
cannot determine current username via OS lookup or $USER"
);
return;
}
}
};
let output = Command::new(&sqryd)
.args(["install-systemd-system", "--user", &username])
.env("RUST_LOG", "off")
.output()
.unwrap_or_else(|e| panic!("failed to spawn {:?}: {e}", sqryd));
assert!(
output.status.success(),
"sqryd install-systemd-system --user {username} must exit 0; status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let unit_text = String::from_utf8_lossy(&output.stdout);
assert!(
unit_text.contains("User=%i"),
"system unit must contain User=%i; got:\n{unit_text}"
);
assert!(
unit_text.contains("Group=%i"),
"system unit must contain Group=%i; got:\n{unit_text}"
);
assert!(
unit_text.contains("WantedBy=multi-user.target"),
"system unit must target multi-user.target; got:\n{unit_text}"
);
}
}
#[test]
#[cfg_attr(
not(target_os = "macos"),
ignore = "install-launchd is macOS-only; test skipped on this platform"
)]
fn install_launchd_output_parses_as_valid_plist_xml() {
let Some(sqryd) = find_sqryd_binary() else {
eprintln!(
"SKIP install_launchd_output_parses_as_valid_plist_xml: \
sqryd binary not found — run `cargo build -p sqry-daemon --bin sqryd` first"
);
return;
};
let output = Command::new(&sqryd)
.arg("install-launchd")
.env("RUST_LOG", "off")
.output()
.unwrap_or_else(|e| panic!("failed to spawn {:?}: {e}", sqryd));
assert!(
output.status.success(),
"sqryd install-launchd must exit 0; status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let plist_text = String::from_utf8_lossy(&output.stdout);
assert!(
!plist_text.trim().is_empty(),
"sqryd install-launchd must produce non-empty output"
);
assert!(
plist_text.contains("<?xml version=\"1.0\""),
"plist must begin with the XML declaration; got:\n{plist_text}"
);
assert!(
plist_text.contains("<plist version=\"1.0\">"),
"plist must contain the root <plist> element; got:\n{plist_text}"
);
assert!(
plist_text.contains("ai.verivus.sqry.sqryd"),
"plist must contain the sqryd label; got:\n{plist_text}"
);
let plist_key_is_true = |key: &str| -> bool {
let key_tag = format!("<key>{key}</key>");
let Some(after_key) = plist_text
.find(key_tag.as_str())
.map(|pos| &plist_text[pos + key_tag.len()..])
else {
return false;
};
let next_key_pos = after_key.find("<key>").unwrap_or(after_key.len());
let between = &after_key[..next_key_pos];
between.contains("<true/>")
};
assert!(
plist_key_is_true("KeepAlive"),
"plist must set KeepAlive to <true/>; got:\n{plist_text}"
);
assert!(
plist_key_is_true("RunAtLoad"),
"plist must set RunAtLoad to <true/>; got:\n{plist_text}"
);
assert!(
plist_text.contains("</plist>"),
"plist must close the root element; got:\n{plist_text}"
);
#[cfg(target_os = "macos")]
let plutil_available = std::path::Path::new("/usr/bin/plutil").is_file();
#[cfg(not(target_os = "macos"))]
let plutil_available = false;
if plutil_available {
let mut tmp =
NamedTempFile::with_suffix(".plist").expect("create temp .plist file for plutil lint");
tmp.write_all(output.stdout.as_slice())
.expect("write plist text to temp file");
tmp.flush().expect("flush temp .plist file");
let lint = Command::new("plutil")
.args(["-lint", tmp.path().to_str().expect("temp path is UTF-8")])
.output()
.expect("spawn plutil -lint");
assert!(
lint.status.success(),
"plutil -lint rejected the generated plist (exit {}):\n\
stdout: {}\nstderr: {}\nplist text:\n{}",
lint.status,
String::from_utf8_lossy(&lint.stdout),
String::from_utf8_lossy(&lint.stderr),
plist_text,
);
} else {
eprintln!(
"NOTE: plutil not available — skipping lint step; \
structural content assertions still run"
);
}
}
#[test]
#[cfg_attr(
not(target_os = "windows"),
ignore = "install-windows is Windows-only; test skipped on this platform"
)]
fn install_windows_emits_both_variants() {
let Some(sqryd) = find_sqryd_binary() else {
eprintln!(
"SKIP install_windows_emits_both_variants: \
sqryd binary not found — run `cargo build -p sqry-daemon --bin sqryd` first"
);
return;
};
let output = Command::new(&sqryd)
.arg("install-windows")
.env("RUST_LOG", "off")
.output()
.unwrap_or_else(|e| panic!("failed to spawn {:?}: {e}", sqryd));
assert!(
output.status.success(),
"sqryd install-windows must exit 0; status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let combined = String::from_utf8_lossy(&output.stdout);
assert!(
!combined.trim().is_empty(),
"sqryd install-windows must produce non-empty output"
);
assert!(
combined.contains("-- sc.exe create command --"),
"output must contain the exact sc.exe create command section header; got:\n{combined}"
);
assert!(
combined.contains("sc.exe create sqryd"),
"output must include the sc.exe create sqryd command; got:\n{combined}"
);
assert!(
combined.contains("start= auto"),
"sc.exe create command must set start= auto; got:\n{combined}"
);
assert!(
combined.contains("-- Task Scheduler XML --"),
"output must contain the exact Task Scheduler XML section header; got:\n{combined}"
);
assert!(
combined.contains("<?xml version=\"1.0\""),
"output must contain the XML declaration for the task XML; got:\n{combined}"
);
assert!(
combined.contains("<Task "),
"output must contain the Task Scheduler <Task> root element; got:\n{combined}"
);
assert!(
combined.contains("http://schemas.microsoft.com/windows/2004/02/mit/task"),
"Task Scheduler XML must include the correct namespace; got:\n{combined}"
);
assert!(
combined.contains("</Task>"),
"Task Scheduler XML must be closed; got:\n{combined}"
);
const XML_SEPARATOR: &str = "-- Task Scheduler XML --";
let sc_section = combined.split(XML_SEPARATOR).next().unwrap_or(&combined);
assert!(
!sc_section.contains("<?xml version="),
"sc.exe section must not contain XML declarations; got:\n{sc_section}"
);
let xml_section = combined.split(XML_SEPARATOR).nth(1).unwrap_or_default();
assert!(
!xml_section.contains("sc.exe create"),
"Task Scheduler XML section must not contain sc.exe commands; got:\n{xml_section}"
);
}
#[test]
fn print_config_emits_valid_toml_with_expected_keys() {
let Some(sqryd) = find_sqryd_binary() else {
eprintln!(
"SKIP print_config_emits_valid_toml_with_expected_keys: \
sqryd binary not found — run `cargo build -p sqry-daemon --bin sqryd` first"
);
return;
};
let output = Command::new(&sqryd)
.arg("print-config")
.env("RUST_LOG", "off")
.output()
.unwrap_or_else(|e| panic!("failed to spawn {:?}: {e}", sqryd));
assert!(
output.status.success(),
"sqryd print-config must exit 0; status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
let toml_text = String::from_utf8_lossy(&output.stdout);
assert!(
!toml_text.trim().is_empty(),
"sqryd print-config must produce non-empty output"
);
let parsed: toml::Value = toml::from_str(&toml_text).unwrap_or_else(|e| {
panic!("sqryd print-config output is not valid TOML: {e}\noutput:\n{toml_text}")
});
assert!(
parsed.get("memory_limit_mb").is_some(),
"TOML must contain memory_limit_mb; parsed:\n{toml_text}"
);
assert!(
parsed.get("debounce_ms").is_some(),
"TOML must contain debounce_ms; parsed:\n{toml_text}"
);
assert!(
parsed.get("idle_timeout_minutes").is_some(),
"TOML must contain idle_timeout_minutes; parsed:\n{toml_text}"
);
}