use super::*;
use crate::service_messages::render;
use crate::test_support::{fixture, remove};
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
pub(super) struct Recorder {
calls: Arc<Mutex<Vec<String>>>,
failure: Arc<Mutex<Option<CallError>>>,
stop_failure: Arc<Mutex<Option<CallError>>>,
stop_alone_failure: Arc<Mutex<Option<CallError>>>,
recreate: Arc<Mutex<Option<PathBuf>>>,
states: Arc<Mutex<std::collections::HashMap<String, Vec<String>>>>,
state_failure: Arc<Mutex<Option<CallError>>>,
budgets: Arc<Mutex<Vec<std::time::Duration>>>,
}
impl Manager for Recorder {
fn within(&mut self, wait: std::time::Duration) {
self.budgets.lock().expect("lock").push(wait);
}
fn reload(&mut self) -> Result<(), CallError> {
self.calls.lock().expect("lock").push(String::from("reload"));
return self.failure.lock().expect("lock").clone().map_or(Ok(()), Err);
}
fn reset_failed(&mut self, unit: &str) -> Result<(), CallError> {
self.calls.lock().expect("lock").push(format!("reset-failed {unit}"));
return self.failure.lock().expect("lock").clone().map_or(Ok(()), Err);
}
fn start(&mut self, unit: &str) -> Result<(), CallError> {
self.calls.lock().expect("lock").push(format!("start {unit}"));
return self.failure.lock().expect("lock").clone().map_or(Ok(()), Err);
}
fn stop(&mut self, unit: &str) -> Result<(), CallError> {
self.calls.lock().expect("lock").push(format!("stop {unit}"));
if let Some(refusal) = self.stop_failure.lock().expect("lock").clone() {
return Err(refusal);
}
return self.failure.lock().expect("lock").clone().map_or(Ok(()), Err);
}
fn stop_alone(&mut self, unit: &str) -> Result<(), CallError> {
self.calls.lock().expect("lock").push(format!("stop-alone {unit}"));
self.recreate_socket();
return self.stop_alone_failure.lock().expect("lock").clone().map_or(Ok(()), Err);
}
fn active_state(&mut self, unit: &str) -> Result<Option<String>, CallError> {
self.calls.lock().expect("lock").push(format!("state {unit}"));
if unit == earlier_socket_unit_instance(INSTANCE) {
self.recreate_socket();
}
if let Some(failure) = self.state_failure.lock().expect("lock").clone() {
return Err(failure);
}
let mut scripts = self.states.lock().expect("lock");
let Some(script) = scripts.get_mut(unit) else { return Ok(None) };
return Ok(Some(if script.len() > 1 { script.remove(0) } else { script[0].clone() }));
}
}
fn with_earlier_instance_questions(mut calls: Vec<String>) -> Vec<String> {
calls.push(format!("state {}", earlier_service_unit_instance(INSTANCE)));
calls.push(format!("state {}", earlier_socket_unit_instance(INSTANCE)));
return calls;
}
const INSTANCE: &str = "0123456789abcdef0123456789abcdef";
fn instance_socket() -> String {
return socket_unit_instance(INSTANCE);
}
fn instance_service() -> String {
return service_unit_instance(INSTANCE);
}
impl Recorder {
fn recreate_socket(&self) {
if let Some(file) = self.recreate.lock().expect("lock").as_ref() {
std::fs::create_dir_all(file.parent().expect("socket directory")).expect("directories");
std::fs::write(file, b"live").expect("the socket made again");
}
}
pub(super) fn calls(&self) -> Vec<String> {
return self.calls.lock().expect("lock").clone();
}
}
pub(super) struct Machine {
pub(super) root: PathBuf,
pub(super) host: ProvisionHost,
}
pub(super) fn machine(name: &str) -> Machine {
let root: PathBuf = fixture(name);
let host: ProvisionHost = ProvisionHost {
runtime_dir: root.join("run"),
user_unit_dir: Some(root.join("config/systemd/user")),
bus: root.join("run/bus"),
service_binary: root.join("programs/monochromatic-git-policy-cli"),
git: PathBuf::from("/usr/bin/git"),
instance: String::from(INSTANCE),
retirement_wait: std::time::Duration::from_millis(400),
};
return Machine { root, host };
}
fn written(machine: &Machine, name: &str) -> String {
return std::fs::read_to_string(runtime_unit_dir(machine.host.runtime_dir.as_path()).join(name)).expect("unit");
}
#[test]
fn unit_files_of_both_spellings_are_the_wrappers_own_and_the_new_ones_name_the_new_program_only() {
use crate::service_provision_unit_file_ownership_and_writes::carries_own_marker;
assert!(carries_own_marker("# Written by the git wrapper for its commit service (cli-git-unit); the wrapper rewrites this file.\n"));
assert!(carries_own_marker("# Written by the git wrapper for its commit service (monochromatic-git-policy-cli-unit); the wrapper rewrites this file.\n"));
assert!(!carries_own_marker("# A unit someone else wrote for their own service.\n"));
for text in [SOCKET_UNIT, SERVICE_UNIT] {
assert!(!text.contains("cli-git"), "a unit this release writes names the new program only: {text}");
}
}
#[test]
fn the_first_provisioning_writes_the_units_and_starts_without_a_reload() {
let machine: Machine = machine("provision-first-lib");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
for (name, text) in unit_texts(&machine.host) {
assert_eq!(written(&machine, name), text);
assert!(!text.contains(BINARY_PLACEHOLDER) && !text.contains(GIT_PLACEHOLDER), "no placeholder is left in {name}");
assert!(OWN_MARKERS.iter().any(|marker| { return text.contains(marker); }), "{name} carries the marker");
}
assert!(written(&machine, SERVICE_UNIT_NAME).contains(machine.host.service_binary.to_string_lossy().as_ref()));
assert_eq!(recorder.calls(), with_earlier_instance_questions(vec![format!("start {}", instance_socket())]));
remove(machine.root.as_path());
}
#[test]
fn a_second_provisioning_changes_nothing_and_clears_failed_state_before_the_start() {
let machine: Machine = machine("provision-second-lib");
let mut first: Recorder = Recorder::default();
provision_with(&machine.host, &mut first).expect("provisioned");
let stamps: Vec<std::time::SystemTime> = [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME]
.map(|name| { return std::fs::metadata(runtime_unit_dir(machine.host.runtime_dir.as_path()).join(name)).expect("unit").modified().expect("time"); })
.to_vec();
let mut second: Recorder = Recorder::default();
provision_with(&machine.host, &mut second).expect("provisioned again");
assert_eq!(
second.calls(),
with_earlier_instance_questions(vec![format!("reset-failed {}", instance_service()), format!("reset-failed {}", instance_socket()), format!("start {}", instance_socket())])
);
let after: Vec<std::time::SystemTime> = [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME]
.map(|name| { return std::fs::metadata(runtime_unit_dir(machine.host.runtime_dir.as_path()).join(name)).expect("unit").modified().expect("time"); })
.to_vec();
assert_eq!(stamps, after);
remove(machine.root.as_path());
}
pub(super) fn add_socket(machine: &Machine) {
let socket: PathBuf = socket_path(machine.host.runtime_dir.as_path(), machine.host.instance.as_str());
std::fs::create_dir_all(socket.parent().expect("socket directory")).expect("directories");
std::fs::write(&socket, b"").expect("socket stand-in");
}
#[test]
fn units_are_in_place_only_when_they_match_and_the_socket_exists() {
let mut machine: Machine = machine("provision-in-place-lib");
assert!(!in_place(&machine.host), "nothing installed");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
assert!(!in_place(&machine.host), "units without the socket");
add_socket(&machine);
assert!(in_place(&machine.host), "units that match and the socket");
machine.host.git = PathBuf::from("/opt/other/git");
assert!(!in_place(&machine.host), "a different real Git is a different service unit");
machine.host.git = PathBuf::from("/usr/bin/git");
assert!(in_place(&machine.host));
remove(machine.root.as_path());
}
#[test]
fn only_the_wrappers_own_earlier_units_keep_an_installation_from_being_in_place() {
let machine: Machine = machine("provision-in-place-earlier-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
assert!(in_place(&machine.host));
let runtime_directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::write(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME), FOREIGN_UNIT).expect("someone's unit");
assert!(in_place(&machine.host), "a unit someone else wrote is not an earlier service of the wrapper's");
std::fs::write(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME), OWN_UNIT).expect("an earlier unit of the wrapper's");
assert!(!in_place(&machine.host), "an earlier unit of the wrapper's in the runtime directory");
std::fs::remove_file(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME)).expect("removed");
assert!(in_place(&machine.host));
let user_directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
std::fs::create_dir_all(&user_directory).expect("user directory");
std::fs::write(user_directory.join(LEGACY_SOCKET_UNIT_NAME), OWN_UNIT).expect("an earlier unit of the wrapper's");
assert!(!in_place(&machine.host), "an earlier unit of the wrapper's in the user's own directory");
remove(machine.root.as_path());
}
#[test]
fn a_unit_naming_a_removed_program_is_repaired_with_a_reload_and_a_reset() {
let machine: Machine = machine("provision-removed-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
let unit: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path()).join(SERVICE_UNIT_NAME);
let removed: &str = "/home/a/.local/share/toolchain/cargo/cli/0.5.2/bin/cli-git-service";
std::fs::write(&unit, written(&machine, SERVICE_UNIT_NAME).replace(machine.host.service_binary.to_string_lossy().as_ref(), removed)).expect("older unit");
assert!(!in_place(&machine.host));
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("repaired");
assert!(!written(&machine, SERVICE_UNIT_NAME).contains(removed));
assert!(in_place(&machine.host));
assert_eq!(
recorder.calls(),
with_earlier_instance_questions(vec![
String::from("reload"),
format!("reset-failed {}", instance_service()),
format!("reset-failed {}", instance_socket()),
format!("start {}", instance_socket())
])
);
remove(machine.root.as_path());
}
#[test]
fn a_unit_from_another_release_is_rewritten() {
let machine: Machine = machine("provision-release-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
let dir: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
let other: String = written(&machine, SERVICE_UNIT_NAME).replace("Restart=on-failure", "Restart=always");
std::fs::write(dir.join(SERVICE_UNIT_NAME), &other).expect("other release's unit");
assert!(!in_place(&machine.host));
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("rewritten");
assert_eq!(written(&machine, SERVICE_UNIT_NAME), unit_texts(&machine.host)[1].1);
assert_eq!(recorder.calls().first().map(String::as_str), Some("reload"));
remove(machine.root.as_path());
}
#[test]
fn a_refused_reload_leaves_no_unit_that_looks_installed() {
let machine: Machine = machine("provision-withdrawn-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
let unit: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path()).join(SERVICE_UNIT_NAME);
std::fs::write(&unit, format!("{}# a line an older release wrote\n", written(&machine, SERVICE_UNIT_NAME))).expect("older unit");
let mut refusing: Recorder = Recorder::default();
*refusing.failure.lock().expect("lock") = Some(CallError::Manager { name: String::from("org.freedesktop.systemd1.AccessDenied"), text: String::new() });
provision_with(&machine.host, &mut refusing).expect_err("refused");
assert!(!in_place(&machine.host), "the next commit provisions again");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned again");
assert!(in_place(&machine.host));
remove(machine.root.as_path());
}
#[test]
fn a_moved_program_rewrites_the_service_unit_and_reloads() {
let mut machine: Machine = machine("provision-moved-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
let socket_before: String = written(&machine, SOCKET_UNIT_NAME);
machine.host.service_binary = machine.root.join("upgraded/monochromatic-git-policy-cli");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(written(&machine, SERVICE_UNIT_NAME).contains(machine.host.service_binary.to_string_lossy().as_ref()));
assert_eq!(written(&machine, SOCKET_UNIT_NAME), socket_before);
assert_eq!(recorder.calls().first().map(String::as_str), Some("reload"));
remove(machine.root.as_path());
}
#[test]
fn earlier_units_are_removed_with_their_link() {
let machine: Machine = machine("provision-earlier-lib");
let directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
std::fs::create_dir_all(directory.join("sockets.target.wants")).expect("directories");
std::fs::write(directory.join(LEGACY_SOCKET_UNIT_NAME), "[Unit]\nDescription=Commit service socket for the git wrapper\n").expect("earlier socket");
std::fs::write(directory.join(LEGACY_SERVICE_UNIT_NAME), "[Unit]\nDescription=Commit service for the git wrapper\n").expect("earlier service");
std::os::unix::fs::symlink("../cli-git.socket", directory.join("sockets.target.wants").join(LEGACY_SOCKET_UNIT_NAME)).expect("link");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(!directory.join(LEGACY_SOCKET_UNIT_NAME).exists() && !directory.join(LEGACY_SERVICE_UNIT_NAME).exists());
assert!(std::fs::symlink_metadata(directory.join("sockets.target.wants").join(LEGACY_SOCKET_UNIT_NAME)).is_err());
let calls: Vec<String> = recorder.calls();
assert!(position(&calls, format!("stop {LEGACY_SERVICE_UNIT_NAME}").as_str()) < position(&calls, "reload"), "{calls:?}");
remove(machine.root.as_path());
}
#[test]
fn a_foreign_unit_is_left_alone_and_named() {
let machine: Machine = machine("provision-foreign-lib");
let directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
std::fs::create_dir_all(&directory).expect("directory");
let foreign: PathBuf = directory.join(LEGACY_SERVICE_UNIT_NAME);
std::fs::write(&foreign, "[Service]\nExecStart=/usr/bin/true\n").expect("foreign unit");
let mut recorder: Recorder = Recorder::default();
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("refused");
assert_eq!(failure, ClientFailure::UnitInTheWay(foreign.display().to_string()));
assert!(render(&failure).contains(foreign.to_string_lossy().as_ref()));
assert_eq!(std::fs::read_to_string(&foreign).expect("still there"), "[Service]\nExecStart=/usr/bin/true\n");
assert!(!runtime_unit_dir(machine.host.runtime_dir.as_path()).exists(), "nothing was written");
assert!(recorder.calls().is_empty());
remove(machine.root.as_path());
}
#[test]
fn manager_failures_are_plain() {
let machine: Machine = machine("provision-failures-lib");
let mut recorder: Recorder = Recorder::default();
*recorder.failure.lock().expect("lock") =
Some(CallError::Manager { name: String::from("org.freedesktop.systemd1.AccessDenied"), text: String::from("permission denied") });
let refused: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("refused");
let text: String = render(&refused);
assert!(text.contains("permission denied") && !text.contains("org.freedesktop"), "{text}");
*recorder.failure.lock().expect("lock") = Some(CallError::Connection(String::from("broken pipe")));
assert_eq!(provision_with(&machine.host, &mut recorder), Err(ClientFailure::NoActivation(platform_without_manager())));
remove(machine.root.as_path());
}
#[test]
fn racing_first_commits_agree_without_a_lock() {
let machine: Machine = machine("provision-race-lib");
let shared: Arc<ProvisionHost> = Arc::new(machine.host.clone());
let recorder: Recorder = Recorder::default();
let handles: Vec<std::thread::JoinHandle<Result<(), ClientFailure>>> = (0..8)
.map(|_| {
let for_thread: Arc<ProvisionHost> = Arc::clone(&shared);
let mut manager: Recorder = recorder.clone();
return std::thread::spawn(move || { return provision_with(&for_thread, &mut manager); });
})
.collect();
for handle in handles {
assert_eq!(handle.join().expect("thread"), Ok(()));
}
for (name, text) in unit_texts(&machine.host) {
assert_eq!(written(&machine, name), text);
}
let leftovers: Vec<String> = std::fs::read_dir(runtime_unit_dir(machine.host.runtime_dir.as_path()))
.expect("directory")
.flatten()
.map(|entry| { return entry.file_name().to_string_lossy().into_owned(); })
.filter(|name| { return name.contains(".tmp-"); })
.collect();
assert!(leftovers.is_empty(), "{leftovers:?}");
remove(machine.root.as_path());
}
#[test]
fn the_unit_names_agree_with_the_templates() {
assert!(SERVICE_UNIT.contains(&format!("Requires={}", SOCKET_UNIT_NAME.replace('@', "@%i"))));
assert!(SERVICE_UNIT.contains(BINARY_PLACEHOLDER) && SERVICE_UNIT.contains(GIT_PLACEHOLDER));
}
#[test]
fn the_earlier_single_service_is_stopped_and_its_units_removed() {
let machine: Machine = machine("provision-retire-earlier-lib");
let directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::create_dir_all(&directory).expect("unit directory");
for name in [LEGACY_SOCKET_UNIT_NAME, LEGACY_SERVICE_UNIT_NAME] {
std::fs::write(directory.join(name), "# Written by the git wrapper for its commit service (cli-git-unit)\n[Unit]\n").expect("earlier unit");
}
let earlier_socket: PathBuf = machine.host.runtime_dir.join(LEGACY_SOCKET_RELATIVE);
std::fs::create_dir_all(earlier_socket.parent().expect("socket directory")).expect("directories");
std::fs::write(&earlier_socket, b"").expect("the earlier socket's file");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert_eq!(
recorder.calls(),
with_earlier_instance_questions(vec![
format!("stop {LEGACY_SOCKET_UNIT_NAME}"),
format!("state {LEGACY_SERVICE_UNIT_NAME}"),
format!("stop {LEGACY_SERVICE_UNIT_NAME}"),
format!("state {LEGACY_SOCKET_UNIT_NAME}"),
format!("state {LEGACY_SERVICE_UNIT_NAME}"),
String::from("reload"),
format!("start {}", instance_socket())
])
);
assert!(!directory.join(LEGACY_SOCKET_UNIT_NAME).exists() && !directory.join(LEGACY_SERVICE_UNIT_NAME).exists());
assert!(!earlier_socket.exists(), "the earlier socket's file is gone");
add_socket(&machine);
assert!(in_place(&machine.host));
remove(machine.root.as_path());
}
pub(super) fn earlier_service_files(machine: &Machine, directory: &Path) -> PathBuf {
std::fs::create_dir_all(directory).expect("unit directory");
for name in [LEGACY_SOCKET_UNIT_NAME, LEGACY_SERVICE_UNIT_NAME] {
std::fs::write(directory.join(name), "# Written by the git wrapper for its commit service (cli-git-unit)\n[Unit]\n").expect("earlier unit");
}
let earlier_socket: PathBuf = machine.host.runtime_dir.join(LEGACY_SOCKET_RELATIVE);
std::fs::create_dir_all(earlier_socket.parent().expect("socket directory")).expect("directories");
std::fs::write(&earlier_socket, b"").expect("the earlier socket's file");
return earlier_socket;
}
fn script(recorder: &Recorder, unit: &str, states: &[&str]) {
recorder.states.lock().expect("lock").insert(unit.to_string(), states.iter().map(|state| { return (*state).to_string(); }).collect());
}
fn position(calls: &[String], wanted: &str) -> usize {
return calls.iter().position(|call| { return call == wanted; }).unwrap_or_else(|| { panic!("{wanted} was not asked: {calls:?}") });
}
#[test]
fn an_instance_starts_only_after_the_earlier_service_has_stopped() {
let machine: Machine = machine("provision-retire-wait-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["active", "deactivating", "inactive"]);
script(&recorder, LEGACY_SOCKET_UNIT_NAME, &["inactive"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
let service_states: Vec<usize> =
calls.iter().enumerate().filter(|(_, call)| { return call.as_str() == format!("state {LEGACY_SERVICE_UNIT_NAME}"); }).map(|(index, _)| { return index; }).collect();
assert!(service_states.len() >= 3, "asked until it was inactive: {calls:?}");
assert!(position(&calls, format!("start {}", instance_socket()).as_str()) > service_states[2], "{calls:?}");
remove(machine.root.as_path());
}
#[test]
fn an_earlier_service_that_does_not_stop_in_time_fails_the_commit() {
let machine: Machine = machine("provision-retire-late-lib");
let directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
let earlier_socket: PathBuf = earlier_service_files(&machine, directory.as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["deactivating"]);
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("the stop did not finish");
assert!(matches!(&failure, ClientFailure::CouldNotStart(reason) if reason.contains(LEGACY_SERVICE_UNIT_NAME)), "{failure:?}");
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("start ") || call == "reload"; }), "{:?}", recorder.calls());
assert!(directory.join(LEGACY_SERVICE_UNIT_NAME).exists() && earlier_socket.exists(), "the evidence stays for the next commit");
remove(machine.root.as_path());
}
#[test]
fn a_state_question_that_outlasts_the_retirement_wait_is_a_slow_stop_and_not_a_missing_manager() {
let machine: Machine = machine("provision-retire-question-late-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
*recorder.state_failure.lock().expect("lock") = Some(CallError::TimedOut);
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("the question ran out of time");
assert!(matches!(&failure, ClientFailure::CouldNotStart(reason) if reason.contains(LEGACY_SERVICE_UNIT_NAME) || reason.contains(LEGACY_SOCKET_UNIT_NAME)), "{failure:?}");
assert_ne!(failure, ClientFailure::NoActivation(platform_without_manager()));
remove(machine.root.as_path());
}
#[test]
fn a_call_that_times_out_outside_the_retirement_wait_is_a_manager_that_does_not_answer() {
let machine: Machine = machine("provision-timeout-lib");
let mut recorder: Recorder = Recorder::default();
*recorder.failure.lock().expect("lock") = Some(CallError::TimedOut);
assert_eq!(provision_with(&machine.host, &mut recorder), Err(ClientFailure::NoActivation(platform_without_manager())));
remove(machine.root.as_path());
}
#[test]
fn the_retirement_questions_and_the_start_have_budgets_of_their_own() {
let machine: Machine = machine("provision-budgets-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["deactivating", "inactive"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let budgets: Vec<std::time::Duration> = recorder.budgets.lock().expect("lock").clone();
assert_eq!(budgets.first(), Some(&EXCHANGE_WAIT), "the stop calls get a whole exchange's time: {budgets:?}");
assert_eq!(budgets[budgets.len() - 2], EXCHANGE_WAIT, "the start gets a whole exchange's time again: {budgets:?}");
let questions: &[std::time::Duration] = &budgets[1..budgets.len() - 2];
assert!(!questions.is_empty(), "{budgets:?}");
assert!(questions.iter().all(|budget| { return *budget <= machine.host.retirement_wait; }), "{budgets:?}");
remove(machine.root.as_path());
}
#[test]
fn a_refused_stop_of_the_earlier_service_fails_the_commit() {
let machine: Machine = machine("provision-retire-refused-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
*recorder.stop_failure.lock().expect("lock") =
Some(CallError::Manager { name: String::from("org.freedesktop.systemd1.AccessDenied"), text: String::from("permission denied") });
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("the stop was refused");
assert!(matches!(&failure, ClientFailure::CouldNotStart(reason) if reason.contains("permission denied")), "{failure:?}");
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("start "); }), "{:?}", recorder.calls());
remove(machine.root.as_path());
}
#[test]
fn an_earlier_service_installed_in_the_persistent_unit_directory_is_stopped() {
let machine: Machine = machine("provision-retire-persistent-lib");
let directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
earlier_service_files(&machine, directory.as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["active", "inactive"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
assert!(position(&calls, format!("stop {LEGACY_SOCKET_UNIT_NAME}").as_str()) < position(&calls, format!("stop {LEGACY_SERVICE_UNIT_NAME}").as_str()));
assert!(position(&calls, format!("stop {LEGACY_SERVICE_UNIT_NAME}").as_str()) < position(&calls, "reload"), "{calls:?}");
assert!(!directory.join(LEGACY_SERVICE_UNIT_NAME).exists());
remove(machine.root.as_path());
}
#[test]
fn an_earlier_socket_file_without_unit_files_still_retires_the_service() {
let machine: Machine = machine("provision-retire-socket-only-lib");
let earlier_socket: PathBuf = machine.host.runtime_dir.join(LEGACY_SOCKET_RELATIVE);
std::fs::create_dir_all(earlier_socket.parent().expect("socket directory")).expect("directories");
std::fs::write(&earlier_socket, b"").expect("the earlier socket's file");
let mut recorder: Recorder = Recorder::default();
*recorder.stop_failure.lock().expect("lock") =
Some(CallError::Manager { name: String::from("org.freedesktop.systemd1.NoSuchUnit"), text: String::from("not loaded") });
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(recorder.calls().contains(&format!("stop {LEGACY_SERVICE_UNIT_NAME}")), "{:?}", recorder.calls());
assert!(!earlier_socket.exists(), "the earlier socket's file is gone");
remove(machine.root.as_path());
}
const OWN_UNIT: &str = "# Written by the git wrapper for its commit service (cli-git-unit)\n[Unit]\n";
const FOREIGN_UNIT: &str = "[Service]\nExecStart=/usr/bin/true\n";
#[test]
fn an_owned_persistent_unit_does_not_authorize_removing_a_foreign_runtime_file() {
let machine: Machine = machine("provision-retire-shadow-lib");
let user_directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
let runtime_directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::create_dir_all(&user_directory).expect("user directory");
std::fs::create_dir_all(&runtime_directory).expect("runtime directory");
std::fs::write(user_directory.join(LEGACY_SERVICE_UNIT_NAME), OWN_UNIT).expect("owned unit");
std::fs::write(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME), FOREIGN_UNIT).expect("foreign unit");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(recorder.calls().contains(&format!("stop {LEGACY_SERVICE_UNIT_NAME}")), "the unit that runs is stopped: {:?}", recorder.calls());
assert_eq!(std::fs::read_to_string(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME)).expect("still there"), FOREIGN_UNIT);
assert!(!user_directory.join(LEGACY_SERVICE_UNIT_NAME).exists(), "the owned file is removed");
remove(machine.root.as_path());
}
#[test]
fn an_owned_runtime_file_does_not_authorize_stopping_a_foreign_persistent_unit() {
let machine: Machine = machine("provision-retire-foreign-effective-lib");
let user_directory: PathBuf = machine.host.user_unit_dir.clone().expect("unit directory");
let runtime_directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::create_dir_all(&user_directory).expect("user directory");
std::fs::create_dir_all(&runtime_directory).expect("runtime directory");
let foreign: PathBuf = user_directory.join(LEGACY_SERVICE_UNIT_NAME);
std::fs::write(&foreign, FOREIGN_UNIT).expect("foreign unit");
std::fs::write(runtime_directory.join(LEGACY_SERVICE_UNIT_NAME), OWN_UNIT).expect("owned unit");
let mut recorder: Recorder = Recorder::default();
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("refused");
assert_eq!(failure, ClientFailure::UnitInTheWay(foreign.display().to_string()));
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("stop "); }), "{:?}", recorder.calls());
assert_eq!(std::fs::read_to_string(&foreign).expect("still there"), FOREIGN_UNIT);
remove(machine.root.as_path());
}
#[test]
fn a_unit_file_that_vanished_after_the_ownership_check_is_a_finished_removal() {
let machine: Machine = machine("provision-vanished-lib");
std::fs::create_dir_all(&machine.root).expect("root");
assert_eq!(remove_settled(machine.root.join("gone.service").as_path()), Ok(()));
let blocked: PathBuf = machine.root.join("directory.service");
std::fs::create_dir_all(&blocked).expect("a directory where a file was expected");
assert!(matches!(remove_settled(blocked.as_path()), Err(ClientFailure::CouldNotStart(reason)) if reason.contains(blocked.to_string_lossy().as_ref())));
remove(machine.root.as_path());
}
#[test]
fn an_earlier_unit_the_wrapper_did_not_write_is_not_stopped() {
let machine: Machine = machine("provision-retire-foreign-lib");
let directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::create_dir_all(&directory).expect("unit directory");
std::fs::write(directory.join(LEGACY_SERVICE_UNIT_NAME), "[Service]\nExecStart=/usr/bin/true\n").expect("someone's unit");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("stop "); }), "{:?}", recorder.calls());
assert!(directory.join(LEGACY_SERVICE_UNIT_NAME).exists());
remove(machine.root.as_path());
}
#[test]
fn an_earlier_service_that_is_still_working_is_asked_to_retire_and_never_stopped_by_the_wrapper() {
let machine: Machine = machine("provision-retire-polite-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["active"]);
let failure: ClientFailure = provision_with(&machine.host, &mut recorder).expect_err("the service kept working");
assert!(matches!(&failure, ClientFailure::CouldNotStart(reason) if reason.contains(LEGACY_SERVICE_UNIT_NAME)), "{failure:?}");
let calls: Vec<String> = recorder.calls();
assert!(!calls.contains(&format!("stop {LEGACY_SERVICE_UNIT_NAME}")), "a service that may be landing is not stopped: {calls:?}");
remove(machine.root.as_path());
}
#[test]
fn the_earlier_service_is_stopped_only_after_it_has_left() {
let machine: Machine = machine("provision-retire-after-leaving-lib");
earlier_service_files(&machine, runtime_unit_dir(machine.host.runtime_dir.as_path()).as_path());
let mut recorder: Recorder = Recorder::default();
script(&recorder, LEGACY_SERVICE_UNIT_NAME, &["active", "deactivating", "inactive"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
let stop: usize = position(&calls, format!("stop {LEGACY_SERVICE_UNIT_NAME}").as_str());
let service_states: Vec<usize> =
calls.iter().enumerate().filter(|(_, call)| { return call.as_str() == format!("state {LEGACY_SERVICE_UNIT_NAME}"); }).map(|(index, _)| { return index; }).collect();
assert!(service_states.iter().filter(|index| { return **index < stop; }).count() >= 3, "it was asked until it was inactive before the stop: {calls:?}");
remove(machine.root.as_path());
}
const OTHER_INSTANCE: &str = "fedcba9876543210fedcba9876543210";
fn earlier_socket() -> String {
return earlier_socket_unit_instance(INSTANCE);
}
fn earlier_service() -> String {
return earlier_service_unit_instance(INSTANCE);
}
fn earlier_templates(machine: &Machine) -> Vec<PathBuf> {
let directory: PathBuf = runtime_unit_dir(machine.host.runtime_dir.as_path());
std::fs::create_dir_all(&directory).expect("unit directory");
let mut files: Vec<PathBuf> = Vec::new();
for name in ["cli-git@.socket", "cli-git@.service"] {
let file: PathBuf = directory.join(name);
std::fs::write(&file, "# Written by the git wrapper for its commit service (cli-git-unit)\n[Unit]\n").expect("earlier template");
files.push(file);
}
return files;
}
fn earlier_socket_file(machine: &Machine, instance: &str) -> PathBuf {
let file: PathBuf = earlier_socket_path(machine.host.runtime_dir.as_path(), instance);
std::fs::create_dir_all(file.parent().expect("socket directory")).expect("directories");
std::fs::write(&file, b"").expect("the earlier socket's file");
return file;
}
#[test]
fn the_same_worktrees_earlier_socket_is_stopped_after_the_new_instance_started() {
let machine: Machine = machine("provision-earlier-socket-stopped-lib");
let templates: Vec<PathBuf> = earlier_templates(&machine);
let socket_file: PathBuf = earlier_socket_file(&machine, INSTANCE);
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket().as_str(), &["active", "inactive"]);
script(&recorder, earlier_service().as_str(), &["inactive"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(socket_file.exists(), "the file is the manager's and an earlier wrapper's, not this program's to unlink");
let calls: Vec<String> = recorder.calls();
assert!(
position(&calls, format!("stop-alone {}", earlier_socket()).as_str()) > position(&calls, format!("start {}", instance_socket()).as_str()),
"{calls:?}"
);
assert!(templates.iter().all(|file| { return file.exists(); }), "the templates of the other worktrees' instances stay");
remove(machine.root.as_path());
}
#[test]
fn the_earlier_socket_is_stopped_without_stopping_the_unit_that_requires_it() {
let machine: Machine = machine("provision-earlier-socket-alone-lib");
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket().as_str(), &["active"]);
let socket_file: PathBuf = earlier_socket_file(&machine, INSTANCE);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
assert!(socket_file.exists(), "a socket the manager still reports active keeps its file; the next commit looks again");
assert!(calls.contains(&format!("stop-alone {}", earlier_socket())), "{calls:?}");
assert!(!calls.contains(&format!("stop {}", earlier_socket())), "a plain stop would stop the service too: {calls:?}");
assert!(!calls.iter().any(|call| { return call.starts_with("stop") && call.ends_with(earlier_service().as_str()); }), "{calls:?}");
remove(machine.root.as_path());
}
#[test]
fn another_worktrees_earlier_socket_is_left_alone() {
let machine: Machine = machine("provision-earlier-socket-other-lib");
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket_unit_instance(OTHER_INSTANCE).as_str(), &["active"]);
script(&recorder, earlier_service_unit_instance(OTHER_INSTANCE).as_str(), &["active"]);
let other_file: PathBuf = earlier_socket_file(&machine, OTHER_INSTANCE);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
assert!(!calls.iter().any(|call| { return call.contains(OTHER_INSTANCE); }), "{calls:?}");
assert!(other_file.exists());
assert!(!calls.iter().any(|call| { return call.starts_with("stop"); }), "nothing else is stopped: {calls:?}");
remove(machine.root.as_path());
}
#[test]
fn a_missing_earlier_unit_is_not_an_error() {
let machine: Machine = machine("provision-earlier-socket-missing-lib");
let mut recorder: Recorder = Recorder::default();
let entries = crate::service_timing::capture::entries_of(|| { provision_with(&machine.host, &mut recorder).expect("provisioned"); });
assert!(recorder.calls().contains(&format!("state {}", earlier_socket())), "the manager was asked: {:?}", recorder.calls());
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("stop"); }), "{:?}", recorder.calls());
assert!(entries.is_empty(), "nothing to say: {entries:?}");
remove(machine.root.as_path());
let vanished: Machine = self::machine("provision-earlier-socket-vanished-lib");
let mut vanishing: Recorder = Recorder::default();
script(&vanishing, earlier_socket().as_str(), &["active"]);
*vanishing.stop_alone_failure.lock().expect("lock") = Some(CallError::Manager { name: NO_SUCH_UNIT.to_string(), text: String::from("not loaded") });
let vanished_entries = crate::service_timing::capture::entries_of(|| { provision_with(&vanished.host, &mut vanishing).expect("provisioned"); });
assert!(vanished_entries.is_empty(), "a unit that is gone is what the stop was for: {vanished_entries:?}");
remove(vanished.root.as_path());
}
#[test]
fn a_failure_to_stop_the_earlier_socket_is_logged_and_never_fails_provisioning() {
let failures: [CallError; 4] = [
CallError::Connection(String::from("broken pipe")),
CallError::TimedOut,
CallError::Manager { name: String::from("org.freedesktop.DBus.Error.AccessDenied"), text: String::from("permission denied") },
CallError::Malformed(String::from("odd reply")),
];
for failure in failures {
let machine: Machine = machine("provision-earlier-socket-failure-lib");
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket().as_str(), &["active"]);
*recorder.stop_alone_failure.lock().expect("lock") = Some(failure.clone());
let entries = crate::service_timing::capture::entries_of(|| { provision_with(&machine.host, &mut recorder).expect("provisioned"); });
assert!(entries.iter().any(|entry| { return entry.message.contains(earlier_socket().as_str()); }), "{failure:?}: logged plainly, naming the unit: {entries:?}");
assert!(recorder.calls().contains(&format!("start {}", instance_socket())), "the instance of this release was started");
remove(machine.root.as_path());
}
let machine: Machine = machine("provision-earlier-socket-question-lib");
let mut recorder: Recorder = Recorder::default();
*recorder.state_failure.lock().expect("lock") = Some(CallError::TimedOut);
let entries = crate::service_timing::capture::entries_of(|| { provision_with(&machine.host, &mut recorder).expect("provisioned"); });
assert!(!entries.is_empty(), "a question that cannot be answered is logged too");
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("stop"); }), "nothing is stopped on a guess");
remove(machine.root.as_path());
}
#[test]
fn the_questions_about_the_earlier_socket_have_a_budget_of_their_own() {
let machine: Machine = machine("provision-earlier-socket-budget-lib");
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket().as_str(), &["active"]);
provision_with(&machine.host, &mut recorder).expect("provisioned");
let budgets = recorder.budgets.lock().expect("lock").clone();
assert_eq!(budgets.len(), 2, "one for the start and one for the earlier socket: {budgets:?}");
assert_eq!(budgets.last().copied(), Some(EXCHANGE_WAIT));
remove(machine.root.as_path());
}
#[test]
fn a_running_earlier_helper_is_not_stopped_and_its_socket_is_kept() {
for state in ["active", "activating", "deactivating", "reloading"] {
let machine: Machine = machine("provision-earlier-helper-running-lib");
let mut recorder: Recorder = Recorder::default();
script(&recorder, earlier_socket().as_str(), &["active"]);
script(&recorder, earlier_service().as_str(), &[state]);
let socket_file: PathBuf = earlier_socket_file(&machine, INSTANCE);
let entries = crate::service_timing::capture::entries_of(|| { provision_with(&machine.host, &mut recorder).expect("provisioned"); });
let calls: Vec<String> = recorder.calls();
assert!(calls.contains(&format!("state {}", earlier_service())), "{state}: the helper was looked for before anything was stopped: {calls:?}");
assert!(!calls.iter().any(|call| { return call.starts_with("stop"); }), "{state}: {calls:?}");
assert!(entries.is_empty(), "{state}: {entries:?}");
assert!(socket_file.exists(), "{state}: the helper's socket file stays");
remove(machine.root.as_path());
}
}
#[test]
fn a_socket_file_left_by_an_unloaded_earlier_unit_is_not_unlinked() {
let machine: Machine = machine("provision-earlier-socket-stale-lib");
let own: PathBuf = earlier_socket_file(&machine, INSTANCE);
let other: PathBuf = earlier_socket_file(&machine, OTHER_INSTANCE);
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert!(own.exists() && other.exists());
assert!(!recorder.calls().iter().any(|call| { return call.starts_with("stop"); }), "{:?}", recorder.calls());
remove(machine.root.as_path());
}
#[test]
fn a_socket_the_earlier_wrapper_makes_again_during_provisioning_is_left_whole() {
let machine: Machine = machine("provision-earlier-socket-recreated-lib");
let file: PathBuf = earlier_socket_path(machine.host.runtime_dir.as_path(), INSTANCE);
for states in [&["active", "inactive"][..], &["active"][..], &["inactive"][..]] {
let mut recorder: Recorder = Recorder::default();
*recorder.recreate.lock().expect("lock") = Some(file.clone());
script(&recorder, earlier_socket().as_str(), states);
provision_with(&machine.host, &mut recorder).expect("provisioned");
assert_eq!(std::fs::read(&file).expect("the earlier wrapper's socket is still there"), b"live", "{states:?}");
std::fs::remove_file(&file).expect("reset");
}
remove(machine.root.as_path());
}
fn proc_with_sockets(machine: &Machine, lines: &[String]) -> PathBuf {
let proc_root: PathBuf = machine.root.join("proc");
std::fs::create_dir_all(proc_root.join("net")).expect("net folder");
std::fs::write(proc_root.join("net/unix"), format!("Num RefCount Protocol Flags Type St Inode Path\n{}", lines.concat())).expect("table");
return proc_root;
}
fn socket_line(flags: &str, path: &str) -> String {
return format!("000000002982a48d: 00000002 00000000 {flags} 0001 01 2269401 {path}\n");
}
#[test]
fn an_earlier_socket_that_listens_keeps_the_installation_from_being_in_place_and_a_leftover_file_does_not() {
let machine: Machine = machine("provision-earlier-socket-in-place-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
let runtime: String = machine.host.runtime_dir.display().to_string();
let own_path: String = format!("{runtime}/cli-git/{INSTANCE}.sock");
let other_path: String = format!("{runtime}/cli-git/{OTHER_INSTANCE}.sock");
let own_file: PathBuf = earlier_socket_file(&machine, INSTANCE);
let quiet: PathBuf = proc_with_sockets(&machine, &[]);
assert!(in_place_with(&machine.host, quiet.as_path()), "a leftover file of a stopped socket is no reason to ask the manager");
let listening: PathBuf = proc_with_sockets(&machine, &[socket_line("00010000", own_path.as_str())]);
assert!(!in_place_with(&machine.host, listening.as_path()), "this worktree's earlier socket listens");
let connected: PathBuf = proc_with_sockets(&machine, &[socket_line("00000000", own_path.as_str())]);
assert!(in_place_with(&machine.host, connected.as_path()), "a socket that does not listen starts no helper");
let other: PathBuf = proc_with_sockets(&machine, &[socket_line("00010000", other_path.as_str())]);
assert!(in_place_with(&machine.host, other.as_path()), "another worktree's earlier socket says nothing about this one");
std::fs::remove_file(machine.root.join("proc/net/unix")).expect("table removed");
assert!(!in_place_with(&machine.host, machine.root.join("proc").as_path()), "a table that cannot be read leaves the question open");
assert!(own_file.exists());
remove(machine.root.as_path());
}
fn shared_earlier_socket(machine: &Machine) -> PathBuf {
return machine.host.runtime_dir.join("cli-git").join("service.sock");
}
#[test]
fn the_shared_earlier_socket_is_in_the_earlier_programs_folder() {
assert_eq!(LEGACY_SOCKET_RELATIVE, "cli-git/service.sock");
}
#[test]
fn the_shared_earlier_socket_file_keeps_the_installation_from_being_in_place() {
let machine: Machine = machine("provision-shared-earlier-in-place-lib");
provision_with(&machine.host, &mut Recorder::default()).expect("provisioned");
add_socket(&machine);
assert!(in_place(&machine.host));
let elsewhere: PathBuf = machine.host.runtime_dir.join("monochromatic-git-policy-cli").join("service.sock");
std::fs::write(&elsewhere, b"").expect("a file of this release's folder");
assert!(in_place(&machine.host), "this release's folder is not the single service's");
std::fs::remove_file(&elsewhere).expect("removed");
let shared: PathBuf = shared_earlier_socket(&machine);
std::fs::create_dir_all(shared.parent().expect("socket directory")).expect("directories");
std::fs::write(&shared, b"").expect("the single service's socket file");
assert!(!in_place(&machine.host));
remove(machine.root.as_path());
}
#[test]
fn a_shared_earlier_socket_file_without_unit_files_retires_the_service_at_its_own_path() {
let machine: Machine = machine("provision-shared-earlier-retire-lib");
let shared: PathBuf = shared_earlier_socket(&machine);
std::fs::create_dir_all(shared.parent().expect("socket directory")).expect("directories");
std::fs::write(&shared, b"").expect("the single service's socket file");
let mut recorder: Recorder = Recorder::default();
provision_with(&machine.host, &mut recorder).expect("provisioned");
let calls: Vec<String> = recorder.calls();
assert!(calls.contains(&format!("stop {LEGACY_SOCKET_UNIT_NAME}")) && calls.contains(&format!("stop {LEGACY_SERVICE_UNIT_NAME}")), "{calls:?}");
assert!(!shared.exists(), "its socket file is gone");
remove(machine.root.as_path());
}