use super::support::{Fixture, fixture};
use git_policy_cli::service_client::{Activation, ClientConfig, ScratchWatch, watch_scratch};
use git_policy_cli::service_protocol::{ClientMessage, SCRATCH_PREFIX, encode_client};
use git_policy_cli::service_server::Bind;
use std::io::Write;
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixStream};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
const SERVICE: &str = env!("CARGO_BIN_EXE_cli-git-service");
const PATIENCE: Duration = Duration::from_secs(60);
const SETTLE: Duration = Duration::from_millis(1500);
struct Held(Child);
impl Drop for Held {
fn drop(&mut self) {
let _killed = self.0.kill();
let _waited = self.0.wait();
}
}
fn start(fixture: &Fixture, name: &str) -> Held {
let child: Child = Command::new(SERVICE)
.args(["--journal"])
.arg(fixture.root.join("journal"))
.args(["--abstract", name])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("the service starts");
let began: Instant = Instant::now();
while !SocketAddr::from_abstract_name(name.as_bytes()).and_then(|socket| { return UnixStream::connect_addr(&socket); }).is_ok() {
assert!(began.elapsed() < PATIENCE, "the service never listened");
std::thread::sleep(Duration::from_millis(10));
}
return Held(child);
}
fn config(name: &str) -> ClientConfig {
return ClientConfig { address: Bind::Abstract(name.to_string()), activation: Activation::Managed, start_wait: PATIENCE, commit_wait: PATIENCE };
}
fn scratch_in(root: &Path, identity: &str) -> (PathBuf, PathBuf) {
let git_dir: PathBuf = root.join("repo/.git");
let scratch: PathBuf = git_dir.join(format!("{SCRATCH_PREFIX}{identity}"));
std::fs::create_dir_all(&scratch).expect("scratch directory");
std::fs::write(scratch.join("commit.index"), b"index").expect("file in the scratch directory");
return (git_dir, scratch);
}
fn wait_until_gone(path: &Path) -> bool {
let began: Instant = Instant::now();
while path.exists() {
if began.elapsed() >= PATIENCE {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
return true;
}
#[test]
fn a_scratch_directory_goes_with_the_connection_that_holds_it() {
let fixture: Fixture = fixture("service-scratch");
let name: String = format!("service-scratch-{}", std::process::id());
let _service: Held = start(&fixture, name.as_str());
let (git_dir, scratch) = scratch_in(fixture.root.as_path(), "0a1b2c3d-1");
let watch: ScratchWatch = watch_scratch(&config(name.as_str()), git_dir.as_path(), scratch.as_path()).expect("the service takes the directory");
std::thread::sleep(SETTLE);
assert!(scratch.exists(), "the directory stays while its holder is connected");
drop(watch);
assert!(wait_until_gone(scratch.as_path()), "the directory is removed once the holder is gone");
assert!(git_dir.exists(), "only the scratch directory goes");
super::commit::remove_settled(&fixture);
}
#[test]
fn nothing_but_a_scratch_directory_of_the_repository_is_ever_removed() {
let fixture: Fixture = fixture("service-scratch-hostile");
let name: String = format!("service-scratch-hostile-{}", std::process::id());
let _service: Held = start(&fixture, name.as_str());
let (git_dir, _scratch) = scratch_in(fixture.root.as_path(), "0a1b2c3d-3");
let precious: PathBuf = fixture.root.join("precious");
std::fs::create_dir_all(&precious).expect("precious");
let elsewhere: PathBuf = fixture.root.join(format!("{SCRATCH_PREFIX}elsewhere"));
std::fs::create_dir_all(&elsewhere).expect("outside the repository");
let notes: PathBuf = git_dir.join("notes");
std::fs::create_dir_all(¬es).expect("notes");
for path in [&precious, &elsewhere, ¬es] {
let socket = SocketAddr::from_abstract_name(name.as_bytes()).expect("address");
let mut stream: UnixStream = UnixStream::connect_addr(&socket).expect("connect");
let line: String = encode_client(&ClientMessage::Scratch { git_dir: git_dir.clone(), path: path.clone() });
stream.write_all(format!("{line}\n").as_bytes()).expect("send");
drop(stream);
}
std::thread::sleep(SETTLE);
for path in [&precious, &elsewhere, ¬es] {
assert!(path.exists(), "{} was removed", path.display());
}
super::commit::remove_settled(&fixture);
}
#[test]
fn no_service_means_no_watch() {
let fixture: Fixture = fixture("service-scratch-none");
let (git_dir, scratch) = scratch_in(fixture.root.as_path(), "0a1b2c3d-4");
let nobody: String = format!("service-scratch-nobody-{}", std::process::id());
assert!(watch_scratch(&config(nobody.as_str()), git_dir.as_path(), scratch.as_path()).is_none());
assert!(scratch.exists());
super::commit::remove_settled(&fixture);
}
#[test]
fn a_frozen_service_takes_the_directory_when_it_runs_again() {
let fixture: Fixture = fixture("service-scratch-frozen");
let name: String = format!("service-scratch-frozen-{}", std::process::id());
let service: Held = start(&fixture, name.as_str());
let (git_dir, scratch) = scratch_in(fixture.root.as_path(), "0a1b2c3d-5");
let pid: i32 = i32::try_from(service.0.id()).expect("a process number");
assert_eq!(unsafe { libc::kill(pid, libc::SIGSTOP) }, 0, "the service freezes");
let began: Instant = Instant::now();
let quick = ClientConfig { start_wait: Duration::from_secs(2), ..config(name.as_str()) };
let offered: Option<ScratchWatch> = watch_scratch(&quick, git_dir.as_path(), scratch.as_path());
assert!(began.elapsed() < Duration::from_secs(2), "the client did not wait for the frozen service to greet it");
let watch: ScratchWatch = offered.expect("the announcement is written without waiting for a greeting");
drop(watch);
assert_eq!(unsafe { libc::kill(pid, libc::SIGCONT) }, 0, "the service runs again");
assert!(wait_until_gone(scratch.as_path()), "the directory is removed once the service reads the announcement");
super::commit::remove_settled(&fixture);
}