use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
pub const WRAPPER: &str = env!("CARGO_BIN_EXE_cli-git-native");
pub const REAL_GIT: &str = "/usr/bin/git";
pub const TIME_BOUND_SECONDS: &str = super::supervisor::SHARED_BACKSTOP_SECONDS;
pub const TIMED_OUT_SIGNAL: i32 = 9;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Observed {
pub code: Option<i32>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub struct Fixture {
pub root: PathBuf,
pub wrapper: PathBuf,
pub service: std::sync::Mutex<Option<FixtureService>>,
pub service_environment: std::sync::Mutex<Vec<(OsString, OsString)>>,
pub service_launch: std::sync::Mutex<ServiceLaunch>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LimitKind {
OpenFiles,
Processes,
AddressSpace,
CoreFile,
}
impl LimitKind {
pub fn apply(self, soft: u64, hard: u64) -> std::io::Result<()> {
let cap: libc::rlimit = libc::rlimit { rlim_cur: soft, rlim_max: hard };
let resource = match self {
LimitKind::OpenFiles => libc::RLIMIT_NOFILE,
LimitKind::Processes => libc::RLIMIT_NPROC,
LimitKind::AddressSpace => libc::RLIMIT_AS,
LimitKind::CoreFile => libc::RLIMIT_CORE,
};
if unsafe { libc::setrlimit(resource, &raw const cap) } != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
}
pub fn current_hard(self) -> u64 {
let mut held: libc::rlimit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
let resource = match self {
LimitKind::OpenFiles => libc::RLIMIT_NOFILE,
LimitKind::Processes => libc::RLIMIT_NPROC,
LimitKind::AddressSpace => libc::RLIMIT_AS,
LimitKind::CoreFile => libc::RLIMIT_CORE,
};
let read: libc::c_int = unsafe { libc::getrlimit(resource, &raw mut held) };
assert_eq!(read, 0, "getrlimit failed: {}", std::io::Error::last_os_error());
return held.rlim_max;
}
#[cfg(target_os = "linux")]
pub fn lift_for(self, pid: u32, soft: u64, hard: u64) -> std::io::Result<()> {
let cap: libc::rlimit = libc::rlimit { rlim_cur: soft, rlim_max: hard };
let resource = match self {
LimitKind::OpenFiles => libc::RLIMIT_NOFILE,
LimitKind::Processes => libc::RLIMIT_NPROC,
LimitKind::AddressSpace => libc::RLIMIT_AS,
LimitKind::CoreFile => libc::RLIMIT_CORE,
};
if unsafe { libc::prlimit(i32::try_from(pid).expect("a process ID fits"), resource, &raw const cap, std::ptr::null_mut()) } != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
}
}
#[derive(Clone, Debug, Default)]
pub struct ServiceLaunch {
pub prefix: Vec<OsString>,
pub limits: Vec<(LimitKind, u64, u64)>,
pub single_cpu: bool,
pub may_fail_to_start: bool,
}
#[cfg(target_os = "linux")]
pub fn restrict_to_one_cpu() -> std::io::Result<()> {
let mut allowed: libc::cpu_set_t = unsafe { std::mem::zeroed() };
if unsafe { libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &raw mut allowed) } != 0 {
return Err(std::io::Error::last_os_error());
}
let first: usize = (0..libc::CPU_SETSIZE as usize)
.find(|cpu| { return unsafe { libc::CPU_ISSET(*cpu, &allowed) }; })
.ok_or_else(|| { return std::io::Error::other("no allowed processor"); })?;
let mut one: libc::cpu_set_t = unsafe { std::mem::zeroed() };
unsafe { libc::CPU_SET(first, &mut one) };
if unsafe { libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &raw const one) } != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
}
struct OwnedGroups {
groups: std::cell::RefCell<Vec<libc::pid_t>>,
}
impl Drop for OwnedGroups {
fn drop(&mut self) {
for group in self.groups.borrow().iter() {
unsafe { libc::killpg(*group, libc::SIGKILL) };
}
}
}
thread_local! {
static OWNED_GROUPS: OwnedGroups = const { OwnedGroups { groups: std::cell::RefCell::new(Vec::new()) } };
}
pub fn own_group(child: &Child) {
let group: libc::pid_t = libc::pid_t::try_from(child.id()).expect("a pid fits the pid type");
OWNED_GROUPS.with(|owned| { owned.groups.borrow_mut().push(group); });
}
pub fn spawn_owned(command: &mut Command) -> std::io::Result<Child> {
use std::os::unix::process::CommandExt as _;
command.process_group(0);
let child: Child = command.spawn()?;
own_group(&child);
return Ok(child);
}
pub struct FixtureService {
children: std::sync::Mutex<std::collections::HashMap<PathBuf, Child>>,
pub runtime: PathBuf,
}
impl Drop for FixtureService {
fn drop(&mut self) {
if let Ok(mut children) = self.children.lock() {
for child in children.values_mut() {
kill_with_everything_it_started(child);
}
}
let _removed = std::fs::remove_dir_all(&self.runtime);
}
}
fn kill_with_everything_it_started(child: &mut Child) {
let group: i32 = -i32::try_from(child.id()).expect("a process ID fits");
unsafe { libc::kill(child.id().try_into().expect("a process ID fits"), libc::SIGTERM) };
let asked: std::time::Instant = std::time::Instant::now();
while child.try_wait().is_ok_and(|status| { return status.is_none(); }) && asked.elapsed() < std::time::Duration::from_secs(2) {
std::thread::sleep(std::time::Duration::from_millis(5));
}
unsafe { libc::kill(group, libc::SIGKILL) };
let _killed = child.kill();
let _waited = child.wait();
}
const SERVICE_PROGRAM: &str = env!("CARGO_BIN_EXE_cli-git-service");
static RUNTIME_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn start_fixture_service(fixture: &Fixture) -> FixtureService {
let _ = fixture;
let home: PathBuf = PathBuf::from(std::env::var_os("HOME").expect("a home directory for the service's runtime directory"));
let number: usize = RUNTIME_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let runtime: PathBuf = home.join("temp/agent").join(format!("rt-{}-{number}", std::process::id()));
std::fs::create_dir_all(runtime.join("cli-git")).expect("runtime directory");
return FixtureService { children: std::sync::Mutex::new(std::collections::HashMap::new()), runtime };
}
fn spawn_for_the_process_lifetime(command: Command) -> std::io::Result<Child> {
type Job = (Command, std::sync::mpsc::Sender<std::io::Result<Child>>);
static SPAWNER: std::sync::OnceLock<std::sync::Mutex<std::sync::mpsc::Sender<Job>>> = std::sync::OnceLock::new();
let spawner = SPAWNER.get_or_init(|| {
let (sender, receiver) = std::sync::mpsc::channel::<Job>();
std::thread::spawn(move || {
for (mut job, reply) in receiver {
let _sent = reply.send(job.spawn());
}
});
return std::sync::Mutex::new(sender);
});
let (reply, answer) = std::sync::mpsc::channel();
spawner.lock().expect("the spawner's sender").send((command, reply)).expect("the spawner thread runs");
return answer.recv().expect("the spawner thread answers");
}
const SOCKET_RELEASE_PATIENCE: std::time::Duration = std::time::Duration::from_secs(60);
fn wait_until_nothing_listens(socket: &Path) {
let began: std::time::Instant = std::time::Instant::now();
loop {
match std::os::unix::net::UnixStream::connect(socket) {
Err(error) if matches!(error.kind(), std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound) => return,
Ok(_) | Err(_) => {}
}
assert!(began.elapsed() < SOCKET_RELEASE_PATIENCE, "something still answers at {} although the fixture ended its service", socket.display());
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
impl FixtureService {
pub fn serve(&self, fixture: &Fixture, git_dir: &Path) {
use std::os::unix::process::CommandExt;
let key: PathBuf = std::fs::canonicalize(git_dir).unwrap_or_else(|_| { return git_dir.to_path_buf(); });
let mut children = self.children.lock().expect("the fixture's services");
if children.contains_key(&key) {
return;
}
let socket: PathBuf = git_policy_cli::service_instance::socket_for(self.runtime.as_path(), key.as_path());
wait_until_nothing_listens(&socket);
let launch: ServiceLaunch = fixture.service_launch.lock().expect("the service launch").clone();
let mut command: Command = match launch.prefix.split_first() {
Some((program, rest)) => {
let mut prefixed: Command = Command::new(program);
prefixed.args(rest).arg(SERVICE_PROGRAM);
prefixed
}
None => Command::new(SERVICE_PROGRAM),
};
command
.args(["--journal"])
.arg(fixture.root.join("journal"))
.arg("--path")
.arg(&socket)
.arg("--instance")
.arg(git_policy_cli::service_instance::instance_of(key.as_path()))
.env_clear()
.env("PATH", format!("{}:/usr/bin:/bin", fixture.root.join("bin").display()))
.env("HOME", fixture.root.join("home"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit());
for (name, value) in fixture.service_environment.lock().expect("the service environment").iter() {
command.env(name, value);
}
command.process_group(0);
unsafe {
command.pre_exec(|| {
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
return Ok(());
});
}
if launch.single_cpu {
unsafe {
command.pre_exec(restrict_to_one_cpu);
}
}
for (kind, soft, hard) in launch.limits {
unsafe {
command.pre_exec(move || { return kind.apply(soft, hard); });
}
}
let child: Child = spawn_for_the_process_lifetime(command).expect("the fixture's commit service starts");
children.insert(key.clone(), child);
let began: std::time::Instant = std::time::Instant::now();
while std::os::unix::net::UnixStream::connect(&socket).is_err() {
let ended: Option<std::process::ExitStatus> = children.get_mut(&key).and_then(|started| { return started.try_wait().ok().flatten(); });
if launch.may_fail_to_start {
if ended.is_some() || began.elapsed() > std::time::Duration::from_secs(20) {
return;
}
} else if let Some(status) = ended {
children.remove(&key);
panic!("the fixture's commit service ended ({status}) before it listened at {}; what it wrote to its log is in this test's output", socket.display());
}
assert!(began.elapsed() < std::time::Duration::from_secs(60), "the fixture's commit service never listened");
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
pub fn process_ids(&self) -> Vec<u32> {
let children = self.children.lock().expect("the fixture's services");
return children.values().map(Child::id).collect();
}
pub fn stop_all(&self) {
let mut children = self.children.lock().expect("the fixture's services");
for (key, child) in children.iter_mut() {
kill_with_everything_it_started(child);
wait_until_nothing_listens(git_policy_cli::service_instance::socket_for(self.runtime.as_path(), key.as_path()).as_path());
}
children.clear();
}
pub fn forget_ended(&self) -> bool {
let mut children = self.children.lock().expect("the fixture's services");
let ended: Vec<PathBuf> =
children.iter_mut().filter_map(|(key, child)| { return child.try_wait().ok().flatten().map(|_| { return key.clone(); }); }).collect();
for key in &ended {
children.remove(key);
}
return !ended.is_empty();
}
pub fn serve_worktrees(&self, fixture: &Fixture) {
for git_dir in worktree_git_dirs(fixture.root.as_path(), 4) {
self.serve(fixture, git_dir.as_path());
}
}
}
pub fn worktree_git_dirs(directory: &Path, depth: usize) -> Vec<PathBuf> {
let mut found: Vec<PathBuf> = Vec::new();
let Ok(entries) = std::fs::read_dir(directory) else { return found };
for entry in entries.flatten() {
let path: PathBuf = entry.path();
let name = entry.file_name();
if name == ".git" {
if path.is_dir() {
found.push(path);
} else if let Ok(text) = std::fs::read_to_string(&path)
&& let Some(target) = text.lines().find_map(|line| { return line.strip_prefix("gitdir: "); })
{
found.push(directory.join(target));
}
} else if depth > 0 && name != "node_modules" && path.is_dir() && !path.is_symlink() {
found.extend(worktree_git_dirs(path.as_path(), depth - 1));
}
}
return found;
}
pub fn restart_ended_services(fixture: &Fixture) {
let service = fixture.service.lock().expect("the fixture's service lock");
let Some(services) = service.as_ref() else { return };
if services.forget_ended() {
*fixture.service_launch.lock().expect("the service launch") = ServiceLaunch::default();
services.serve_worktrees(fixture);
}
}
pub fn service_runtime(fixture: &Fixture) -> Option<PathBuf> {
let mut service = fixture.service.lock().expect("the fixture's service lock");
if service.is_none() {
*service = Some(start_fixture_service(fixture));
}
let started = service.as_ref()?;
started.serve_worktrees(fixture);
return Some(started.runtime.clone());
}
#[cfg(target_os = "linux")]
pub fn close_on_exec_pipe() -> [libc::c_int; 2] {
let mut ends: [libc::c_int; 2] = [0, 0];
let made: libc::c_int = unsafe { libc::pipe2(ends.as_mut_ptr(), libc::O_CLOEXEC) };
assert_eq!(made, 0, "pipe2 failed: {}", std::io::Error::last_os_error());
return ends;
}
#[cfg(all(unix, not(target_os = "linux")))]
pub fn close_on_exec_pipe() -> [libc::c_int; 2] {
let mut ends: [libc::c_int; 2] = [0, 0];
let made: libc::c_int = unsafe { libc::pipe(ends.as_mut_ptr()) };
assert_eq!(made, 0, "pipe failed: {}", std::io::Error::last_os_error());
for end in ends {
let flagged: libc::c_int = unsafe { libc::fcntl(end, libc::F_SETFD, libc::FD_CLOEXEC) };
assert_eq!(flagged, 0, "fcntl failed: {}", std::io::Error::last_os_error());
}
return ends;
}
#[cfg(target_os = "linux")]
pub fn pseudo_terminal_slave_name(descriptor: libc::c_int) -> std::ffi::CString {
let mut buffer: [libc::c_char; 256] = [0; 256];
let named: libc::c_int = unsafe { libc::ptsname_r(descriptor, buffer.as_mut_ptr(), buffer.len()) };
assert_eq!(named, 0, "the slave has a name");
return unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }.to_owned();
}
#[cfg(all(unix, not(target_os = "linux")))]
pub fn pseudo_terminal_slave_name(descriptor: libc::c_int) -> std::ffi::CString {
let mut buffer: [libc::c_char; 128] = [0; 128];
let named: libc::c_int = unsafe { libc::ioctl(descriptor, libc::TIOCPTYGNAME as libc::c_ulong, buffer.as_mut_ptr()) };
assert_eq!(named, 0, "the slave has a name");
return unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }.to_owned();
}
pub const KEEP_FIXTURES_VARIABLE: &str = "CLI_GIT_KEEP_FIXTURES";
pub const FIXTURE_ROOT_VARIABLE: &str = "CLI_GIT_TEST_ROOT";
pub fn fixture_root() -> PathBuf {
let root: PathBuf = std::env::var_os(FIXTURE_ROOT_VARIABLE)
.map_or_else(|| { return PathBuf::from(env!("CARGO_TARGET_TMPDIR")); }, PathBuf::from);
std::fs::create_dir_all(&root).expect("fixture root");
return root;
}
pub fn settle_fixture(root: &Path, keep: bool) {
if keep {
return;
}
for _ in 0..50 {
match std::fs::remove_dir_all(root) {
Ok(()) => return,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
}
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _stopped = self.service.lock().map(|mut service| { return service.take(); });
end_processes_working_in(self.root.as_path());
settle_fixture(self.root.as_path(), std::env::var_os(KEEP_FIXTURES_VARIABLE).is_some());
}
}
fn warm_wrapper(linked: &Path) {
static WARMED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
WARMED.get_or_init(|| {
use std::io::Read;
let mut file: std::fs::File = std::fs::File::open(linked).expect("open the fixture wrapper");
let mut chunk: Vec<u8> = vec![0; 1 << 20];
while file.read(chunk.as_mut_slice()).expect("read the built wrapper") > 0 {}
});
}
fn link_built_wrapper(built: &Path, to: &Path, store_root: &Path) {
if let Err(error) = std::fs::hard_link(built, to) {
let stored: std::io::Result<()> = if error.raw_os_error() == Some(libc::EXDEV) {
super::wrapper_store::link_stored(built, store_root, to, &mut |_| {})
} else {
Err(error)
};
if let Err(reason) = stored {
eprintln!("fixture wrapper: no hard link of {} at {} ({reason}); copying it instead", built.display(), to.display());
copy_executable(built, to);
}
}
warm_wrapper(to);
}
pub fn fixture(name: &str) -> Fixture {
return fixture_in(fixture_root().as_path(), name);
}
pub fn fixture_in(directory: &Path, name: &str) -> Fixture {
forbid_core_dumps();
std::fs::create_dir_all(directory).expect("fixture directory root");
let created: PathBuf = directory.join(format!("native-binary-{}-{name}", std::process::id()));
if created.exists() {
std::fs::remove_dir_all(&created).expect("remove stale fixture");
}
std::fs::create_dir(&created).expect("fresh fixture");
let root: PathBuf = std::fs::canonicalize(&created).expect("canonical fixture");
for subdirectory in ["wrapper", "bin", "home"] {
std::fs::create_dir(root.join(subdirectory)).expect("fixture directory");
}
let wrapper: PathBuf = root.join("wrapper/cli-git-native");
link_built_wrapper(Path::new(WRAPPER), wrapper.as_path(), directory);
std::os::unix::fs::symlink(&wrapper, root.join("bin/git")).expect("git-named link");
return Fixture { root, wrapper, service: std::sync::Mutex::new(None), service_environment: std::sync::Mutex::new(Vec::new()), service_launch: std::sync::Mutex::new(ServiceLaunch::default()) };
}
const REMOVAL_TRIES: usize = 50;
const REMOVAL_PAUSE: std::time::Duration = std::time::Duration::from_millis(100);
pub fn remove_patiently(root: &Path, attempt: &dyn Fn(&Path) -> std::io::Result<()>, tries: usize, pause: std::time::Duration) -> std::io::Result<()> {
let mut last: std::io::Result<()> = Ok(());
for _ in 0..tries {
match attempt(root) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
last = Err(error);
std::thread::sleep(pause);
}
Err(error) => return Err(error),
}
}
return last;
}
pub fn remove(fixture: &Fixture) {
remove_patiently(&fixture.root, &|root| { return std::fs::remove_dir_all(root); }, REMOVAL_TRIES, REMOVAL_PAUSE).expect("remove only the fixture");
}
pub fn hermetic(command: &mut Command, fixture: &Fixture, path: &OsStr) {
command
.env_clear()
.env("PATH", path)
.env("HOME", fixture.root.join("home"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.env(git_policy_cli::journald_native::TEST_SOCKET_VARIABLE, "@cli-git-test-journal-nobody-hermetic")
.current_dir(&fixture.root);
if let Some(verbose) = std::env::var_os("MONOCHROMATIC_VERBOSE") {
command.env("MONOCHROMATIC_VERBOSE", verbose);
}
}
pub fn wrapper_first_path(fixture: &Fixture) -> OsString {
let mut path: OsString = fixture.root.join("bin").into_os_string();
path.push(":/usr/bin:/bin");
return path;
}
pub fn bounded_for(fixture: &Fixture, program: &Path, path: &OsStr, seconds: &str) -> Command {
if seconds == TIME_BOUND_SECONDS {
super::supervisor::ensure_monitor();
}
let mut command: Command = Command::new("/usr/bin/timeout");
command.arg("--signal=KILL").arg(seconds).arg(program);
hermetic(&mut command, fixture, path);
if let Some(runtime) = service_runtime(fixture) {
command.env("XDG_RUNTIME_DIR", runtime);
}
return command;
}
pub fn bounded(fixture: &Fixture, program: &Path, path: &OsStr) -> Command {
return bounded_for(fixture, program, path, TIME_BOUND_SECONDS);
}
pub fn stub_manual_viewer(fixture: &Fixture) {
executable(
fixture.root.join("bin/man").as_path(),
b"#!/bin/sh\necho \"manual page requested: $*\"\n",
);
}
pub fn link_real_git(fixture: &Fixture) {
std::fs::create_dir(fixture.root.join("real")).expect("real directory");
std::os::unix::fs::symlink(REAL_GIT, fixture.root.join("real/git")).expect("real Git link");
}
pub fn wrapped(fixture: &Fixture) -> Command {
return bounded(
fixture,
fixture.root.join("bin/git").as_path(),
wrapper_first_path(fixture).as_os_str(),
);
}
pub fn direct(fixture: &Fixture) -> Command {
let mut command: Command = Command::new(REAL_GIT);
hermetic(&mut command, fixture, OsStr::new("/usr/bin:/bin"));
return command;
}
pub fn decline_in_process_lfs_scan(fixture: &Fixture, repo: &Path) {
let mut hashes: Vec<String> = Vec::new();
for (name, text) in [("replace-from.txt", "from\n"), ("replace-to.txt", "to\n")] {
let file: PathBuf = fixture.root.join(name);
std::fs::write(&file, text).expect("blob source");
let output = direct(fixture).current_dir(repo).args(["hash-object", "-w"]).arg(&file).output().expect("hash-object starts");
assert!(output.status.success(), "{}", String::from_utf8_lossy(output.stderr.as_slice()));
hashes.push(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned());
}
let replaced = direct(fixture).current_dir(repo).args(["replace", hashes[0].as_str(), hashes[1].as_str()]).output().expect("replace starts");
assert!(replaced.status.success(), "{}", String::from_utf8_lossy(replaced.stderr.as_slice()));
}
pub fn without_hint_fields(bytes: &[u8]) -> Vec<u8> {
let marker: &[u8] = b",\"hint\":\"";
let mut kept: Vec<u8> = Vec::with_capacity(bytes.len());
let mut index: usize = 0;
while index < bytes.len() {
if bytes[index..].starts_with(marker) {
index += marker.len();
while index < bytes.len() && bytes[index] != b'"' {
index += if bytes[index] == b'\\' { 2 } else { 1 };
}
index += 1;
} else {
kept.push(bytes[index]);
index += 1;
}
}
return kept;
}
pub const WAIT_NOTICE_PREFIX: &[u8] = b"cli-git: still waiting for ";
pub fn without_wait_notices(bytes: &[u8]) -> Vec<u8> {
return bytes
.split_inclusive(|byte| { return *byte == b'\n'; })
.filter(|line| { return !line.starts_with(WAIT_NOTICE_PREFIX); })
.flatten()
.copied()
.collect();
}
pub fn observe(command: &mut Command, input: &[u8]) -> Observed {
let raw: Observed = observe_raw(command, input);
return Observed {
code: raw.code,
stdout: without_hint_fields(raw.stdout.as_slice()),
stderr: without_hint_fields(raw.stderr.as_slice()),
};
}
enum SharedBoundMisuse {
CommitTransaction,
ManualPage,
}
fn shared_bound_misuse(command: &Command) -> Option<SharedBoundMisuse> {
let arguments: Vec<OsString> = command.get_args().map(OsStr::to_os_string).collect();
let [signal, bound, program, wrapped @ ..] = arguments.as_slice() else {
return None;
};
if signal != "--signal=KILL" || bound != TIME_BOUND_SECONDS {
return None;
}
let stripped = git_policy_cli::wrapper_invocation::strip_wrapper_controls(wrapped);
if stripped.layout.outcome != git_policy_cli::global_arguments::GlobalOutcome::Command {
return None;
}
let region: &[OsString] = git_policy_cli::wrapper_invocation::command_region(&stripped);
let word: &[u8] = git_policy_cli::wrapper_invocation::command_word(&stripped);
if word == b"commit"
&& git_policy_cli::command_commit::parse_commit_region(region, &[]).is_ok_and(|facts| { return !facts.dry_run; })
{
return Some(SharedBoundMisuse::CommitTransaction);
}
let has_manual_stub: bool = Path::new(program)
.parent()
.is_some_and(|directory| { return directory.join("man").exists(); });
let asks_for_help: bool = region
.iter()
.take_while(|token| { return *token != "--"; })
.any(|token| { return token == "--help"; });
if word != b"cli-git" && asks_for_help && !has_manual_stub {
return Some(SharedBoundMisuse::ManualPage);
}
return None;
}
pub fn supervised_wait(mut child: Child, input: &[u8], cleanup: impl FnOnce()) -> Output {
let child_id: u32 = child.id();
let mut stdin = child.stdin.take().expect("piped stdin");
let owned: Vec<u8> = input.to_vec();
let writer = std::thread::spawn(move || { let _ = stdin.write_all(owned.as_slice()); });
let output: Output = child.wait_with_output().expect("process finishes");
let _joined = writer.join();
if let Some(message) = super::supervisor::take_kill(child_id) {
panic!("{message}");
}
cleanup();
return output;
}
pub fn observe_raw(command: &mut Command, input: &[u8]) -> Observed {
match shared_bound_misuse(command) {
Some(SharedBoundMisuse::CommitTransaction) => panic!(
"a commit under the shared bound: the work of a landed commit happens in the commit service, outside the command's \
process tree, so the supervisor would see a tree that waits and end it; run it with `run_commit` (the {}-second \
wall-clock bound of binary_commit_tests.rs)",
super::commit::COMMIT_BOUND_SECONDS
),
Some(SharedBoundMisuse::ManualPage) => panic!(
"a `--help` under the shared bound renders a manual page through man and groff (9 process starts) that is Git's \
work and the host's `man`; put an executable `man` stub in the fixture's bin directory first (`stub_manual_viewer`)"
),
None => {}
}
let child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("process starts");
let output: Output = supervised_wait(child, input, || {
if let Some(directory) = command.get_current_dir() {
settle_service_work(directory);
}
});
assert_ne!(
output.status.signal(),
Some(TIMED_OUT_SIGNAL),
"the command hit its wall-clock bound: {:?} in {:?}",
command.get_args().collect::<Vec<&OsStr>>(),
command.get_current_dir()
);
return Observed {
code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
};
}
const SERVICE_WORK_PATIENCE: std::time::Duration = std::time::Duration::from_secs(60);
fn work_listing(work: &Path) -> String {
return ["pending", "claimed", "recovering", "done", "failed"]
.iter()
.map(|state| {
let mut names: Vec<String> = std::fs::read_dir(work.join(state))
.map(|listing| { return listing.flatten().map(|entry| { return entry.file_name().to_string_lossy().into_owned(); }).collect(); })
.unwrap_or_default();
names.sort();
return format!("{state}={names:?}");
})
.collect::<Vec<String>>()
.join(" ");
}
pub fn settle_service_work(directory: &Path) {
let Some(git_dir) = directory.ancestors().map(|candidate| { return candidate.join(".git"); }).find(|candidate| { return candidate.is_dir(); }) else {
return;
};
let work: PathBuf = git_dir.join(git_policy_cli::work_item::WORK_DIRECTORY);
let open = |state: &str| -> usize { return std::fs::read_dir(work.join(state)).map_or(0, Iterator::count); };
let began: std::time::Instant = std::time::Instant::now();
while open("pending") + open("claimed") + open("recovering") > 0 {
assert!(began.elapsed() < SERVICE_WORK_PATIENCE, "the service never settled the after-commit work in {}: {}", work.display(), work_listing(work.as_path()));
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
pub fn give_service_environment(fixture: &Fixture, name: &str, value: &str) {
let mut given = fixture.service_environment.lock().expect("the service environment");
let present = fixture.service.lock().expect("the fixture's service lock").is_some();
let already: bool = given.iter().any(|(held, text)| { return held == name && text == value; });
assert!(already || !present, "the fixture's service already started without {name}={value}");
if !already {
given.push((OsString::from(name), OsString::from(value)));
}
}
pub fn push_failed_words() -> &'static str {
return git_policy_cli::auto_push::PUSH_FAILED_NOTE;
}
pub fn push_report(directory: &Path, stderr: &str) -> String {
let mut report: String = stderr.to_string();
settle_service_work(directory);
let git_dir: Option<PathBuf> = directory.ancestors().map(|candidate| { return candidate.join(".git"); }).find(|candidate| { return candidate.is_dir(); });
if let Some(found) = git_dir {
for notice in git_policy_cli::push_failure_notice::take_push_failures(found.as_path()) {
report.push_str(notice.as_str());
report.push('\n');
}
}
return report;
}
pub fn stalling_editor(fixture: &Fixture, started: &Path) -> PathBuf {
let path: PathBuf = fixture.root.join("stalling-editor");
executable(
path.as_path(),
format!(
"#!/bin/sh\nstall=\ntrap 'kill $stall 2>/dev/null; exit 130' INT\ntrap 'kill $stall 2>/dev/null; exit 131' QUIT\nprintf started > '{}'\nsleep 60 </dev/null >/dev/null 2>&1 &\nstall=$!\nwait $stall\n",
started.display()
)
.as_bytes(),
);
return path;
}
pub fn git<S: AsRef<OsStr>>(fixture: &Fixture, directory: &Path, arguments: &[S]) -> Output {
let output: Output = direct(fixture)
.current_dir(directory)
.args(arguments)
.output()
.expect("native Git fixture command");
assert!(
output.status.success(),
"git failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return output;
}
pub fn repository(fixture: &Fixture, name: &OsStr) -> PathBuf {
let root: PathBuf = fixture.root.join(name);
std::fs::create_dir(&root).expect("repository directory");
git(
fixture,
root.as_path(),
&["init", "--quiet", "--initial-branch=main"],
);
git(
fixture,
root.as_path(),
&["commit", "--quiet", "--allow-empty", "--message=initial"],
);
return root;
}
pub fn run_wrapped(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(wrapped(fixture).current_dir(directory).args(arguments), b"");
}
pub fn run_direct(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(direct(fixture).current_dir(directory).args(arguments), b"");
}
pub fn porcelain(fixture: &Fixture, repo: &Path) -> String {
let output: Output = git(
fixture,
repo,
&["status", "--porcelain=v1", "--untracked-files=all"],
);
return String::from_utf8_lossy(&output.stdout).into_owned();
}
pub fn stopped_with(stderr: &str) -> Observed {
return Observed {
code: Some(2),
stdout: Vec::<u8>::new(),
stderr: stderr.as_bytes().to_vec(),
};
}
pub fn silent_success() -> Observed {
return Observed {
code: Some(0),
stdout: Vec::<u8>::new(),
stderr: Vec::<u8>::new(),
};
}
pub fn stderr_of(observed: &Observed) -> String {
return String::from_utf8_lossy(&observed.stderr).into_owned();
}
#[path = "../testing/fixture_executable.rs"]
mod fixture_executable;
pub use fixture_executable::executable;
#[path = "../testing/test_alive.rs"]
mod test_alive;
pub use test_alive::{end_processes_working_in, while_test_lives};
pub fn copy_executable(from: &Path, to: &Path) {
let copied: bool = Command::new("cp")
.arg("--reflink=never")
.arg("--")
.arg(from)
.arg(to)
.status()
.expect("start fixture copy")
.success();
assert!(copied, "fixture copy failed: {from:?} to {to:?}");
}
pub const TRANSACTION_ID: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
pub fn registered_transaction(repo: &Path) -> PathBuf {
let registry: PathBuf = repo.join(".git/cli-git-transactions");
std::fs::create_dir_all(registry.join(TRANSACTION_ID)).expect("a transaction directory an earlier release left");
return registry;
}
pub fn dead_copy_owner() -> git_policy_cli::worktree_copy_owner::CopyOwner {
let mut child: Child = Command::new("sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("sleep starts");
let pid: i64 = i64::from(child.id());
let birth: String = git_policy_cli::process_identity::process_birth_identity(pid)
.expect("identity readable")
.expect("the child runs");
child.kill().expect("child is killed");
child.wait().expect("child is reaped");
return git_policy_cli::worktree_copy_owner::CopyOwner {
pid,
birth_identity: birth,
pid_namespace: git_policy_cli::pid_namespace::current_pid_namespace(),
};
}
fn forbid_core_dumps() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
let none: libc::rlimit = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
let _set = unsafe { libc::setrlimit(libc::RLIMIT_CORE, &none) };
});
}