use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
pub(crate) const REAL_GIT: &str = "/usr/bin/git";
#[derive(Clone, Default)]
pub(crate) struct SharedSink(pub(crate) std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl SharedSink {
pub(crate) fn text(&self) -> String {
let bytes: Vec<u8> = self.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clone();
return String::from_utf8_lossy(bytes.as_slice()).into_owned();
}
}
impl std::io::Write for SharedSink {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner).extend_from_slice(bytes);
return Ok(bytes.len());
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
pub(crate) struct ActionSink {
shared: SharedSink,
lines: usize,
after: usize,
action: Option<Box<dyn FnOnce() + Send>>,
}
impl ActionSink {
pub(crate) fn new(shared: &SharedSink, after: usize, action: impl FnOnce() + Send + 'static) -> Self {
return Self { shared: shared.clone(), lines: 0, after, action: Some(Box::new(action)) };
}
}
impl std::io::Write for ActionSink {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
std::io::Write::write(&mut self.shared, bytes)?;
self.lines += bytes.iter().filter(|byte| { return **byte == b'\n'; }).count();
if self.lines >= self.after && let Some(action) = self.action.take() {
action();
}
return Ok(bytes.len());
}
fn flush(&mut self) -> std::io::Result<()> {
return Ok(());
}
}
const RUN_LIMIT: std::time::Duration = std::time::Duration::from_secs(120);
pub(crate) fn arm_watchdog() {
static ARMED: std::sync::Once = std::sync::Once::new();
ARMED.call_once(|| {
std::thread::spawn(|| {
std::thread::sleep(RUN_LIMIT);
eprintln!("the unit-test run exceeded {RUN_LIMIT:?}: a test is stuck");
std::process::abort();
});
});
}
pub(crate) 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) };
});
}
pub(crate) const KEEP_FIXTURES_VARIABLE: &str = "CLI_GIT_KEEP_FIXTURES";
pub(crate) const FIXTURE_ROOT_VARIABLE: &str = "CLI_GIT_TEST_ROOT";
pub(crate) fn fixture_root() -> PathBuf {
let root: PathBuf = std::env::var_os(FIXTURE_ROOT_VARIABLE).map_or_else(
|| {
let target: Option<PathBuf> = this_test_binary()
.ok()
.and_then(|executable| { return executable.ancestors().nth(3).map(Path::to_path_buf); });
return target
.unwrap_or_else(|| { return PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target"); })
.join("test-fixtures");
},
PathBuf::from,
);
std::fs::create_dir_all(&root).expect("fixture root");
return root;
}
pub(crate) 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)),
}
}
}
struct ThreadFixtures {
roots: std::cell::RefCell<Vec<PathBuf>>,
groups: std::cell::RefCell<Vec<libc::pid_t>>,
}
impl Drop for ThreadFixtures {
fn drop(&mut self) {
for group in self.groups.borrow().iter() {
unsafe { libc::killpg(*group, libc::SIGKILL) };
}
let keep: bool = std::env::var_os(KEEP_FIXTURES_VARIABLE).is_some();
for root in self.roots.borrow().iter() {
end_processes_working_in(root.as_path());
settle_fixture(root.as_path(), keep);
}
}
}
thread_local! {
static THREAD_FIXTURES: ThreadFixtures = const { ThreadFixtures { roots: std::cell::RefCell::new(Vec::new()), groups: std::cell::RefCell::new(Vec::new()) } };
}
pub(crate) fn spawn_owned(command: &mut Command) -> std::io::Result<std::process::Child> {
use std::os::unix::process::CommandExt as _;
command.process_group(0);
let child: std::process::Child = command.spawn()?;
own_group(&child);
return Ok(child);
}
pub(crate) fn own_group(child: &std::process::Child) {
let group: libc::pid_t = libc::pid_t::try_from(child.id()).expect("a pid fits the pid type");
THREAD_FIXTURES.with(|fixtures| { fixtures.groups.borrow_mut().push(group); });
}
#[allow(clippy::disallowed_methods, reason = "identifies the running test binary so a control can start a helper copy of it; the wrapper is never started this way")]
pub(crate) fn this_test_binary() -> std::io::Result<PathBuf> {
return std::env::current_exe();
}
pub(crate) fn fixture_with_prefix(prefix: &str, name: &str) -> PathBuf {
arm_watchdog();
forbid_core_dumps();
let root: PathBuf = fixture_root().join(format!("{prefix}-{}-{name}", std::process::id()));
if root.exists() {
std::fs::remove_dir_all(&root).expect("remove stale fixture");
}
std::fs::create_dir(&root).expect("fresh fixture");
let canonical: PathBuf = std::fs::canonicalize(&root).expect("canonical fixture");
THREAD_FIXTURES.with(|fixtures| { fixtures.roots.borrow_mut().push(canonical.clone()); });
return canonical;
}
pub(crate) fn fixture(name: &str) -> PathBuf {
return fixture_with_prefix("native-unit", name);
}
pub(crate) fn age(path: &Path) {
use std::os::unix::ffi::OsStrExt as _;
let name: std::ffi::CString = std::ffi::CString::new(path.as_os_str().as_bytes()).expect("no NUL in a fixture path");
let past: libc::time_t = (std::time::SystemTime::now() - std::time::Duration::from_secs(120))
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("after the epoch")
.as_secs() as libc::time_t;
let times: [libc::timespec; 2] = [libc::timespec { tv_sec: past, tv_nsec: 0 }, libc::timespec { tv_sec: past, tv_nsec: 0 }];
let result: libc::c_int = unsafe { libc::utimensat(libc::AT_FDCWD, name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) };
assert_eq!(result, 0, "age {}", path.display());
}
pub(crate) fn remove(root: &Path) {
std::fs::remove_dir_all(root).expect("remove only the fixture");
}
pub(crate) fn git_output<S: AsRef<OsStr>>(directory: &Path, arguments: &[S]) -> Output {
return Command::new(REAL_GIT)
.current_dir(directory)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.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")
.args(arguments)
.output()
.expect("native Git fixture command");
}
pub(crate) fn git<S: AsRef<OsStr>>(directory: &Path, arguments: &[S]) -> Output {
let output: Output = git_output(directory, arguments);
assert!(
output.status.success(),
"git {:?} failed: {}",
arguments.iter().map(AsRef::as_ref).collect::<Vec<&OsStr>>(),
String::from_utf8_lossy(&output.stderr)
);
return output;
}
pub(crate) fn repository(parent: &Path, name: &str) -> PathBuf {
let root: PathBuf = parent.join(name);
std::fs::create_dir(&root).expect("repository directory");
git(
root.as_path(),
&["init", "--quiet", "--initial-branch=main"],
);
git(
root.as_path(),
&["commit", "--quiet", "--allow-empty", "--message=initial"],
);
return root;
}
#[cfg(unix)]
#[path = "fixture_executable.rs"]
mod fixture_executable;
#[cfg(unix)]
pub(crate) use fixture_executable::executable;
#[cfg(unix)]
#[path = "test_alive.rs"]
mod test_alive;
#[cfg(unix)]
pub(crate) use test_alive::{end_processes_working_in, while_test_lives};
pub(crate) fn isolated_overlay() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut pairs: Vec<(std::ffi::OsString, std::ffi::OsString)> = Vec::new();
for (name, value) in [
("GIT_CONFIG_NOSYSTEM", "1"),
("GIT_CONFIG_GLOBAL", "/nonexistent-global-config"),
("GIT_AUTHOR_NAME", "Fixture"),
("GIT_AUTHOR_EMAIL", "fixture@example.invalid"),
("GIT_COMMITTER_NAME", "Fixture"),
("GIT_COMMITTER_EMAIL", "fixture@example.invalid"),
] {
pairs.push((
std::ffi::OsString::from(name),
std::ffi::OsString::from(value),
));
}
return pairs;
}
pub(crate) fn git_context() -> crate::transaction_git::GitContext {
return crate::transaction_git::GitContext {
real_git: PathBuf::from(REAL_GIT),
overlay: isolated_overlay(),
global_prefix: Vec::new(),
common_dir: Some(fixture_root()),
};
}
pub(crate) fn scratch_environment() -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
return vec![(
std::ffi::OsString::from("HOME"),
fixture_root().join("scratch-home").into_os_string(),
)];
}
pub(crate) fn git_text<S: AsRef<OsStr>>(directory: &Path, arguments: &[S]) -> String {
let output: Output = git(directory, arguments);
let text: String = String::from_utf8(output.stdout).expect("UTF-8 Git output");
return String::from(text.strip_suffix('\n').unwrap_or(text.as_str()));
}
pub(crate) fn strip_hint_fields(text: &str) -> String {
let marker: &str = ",\"hint\":\"";
let mut rest: String = text.to_owned();
while let Some(start) = rest.find(marker) {
let bytes: &[u8] = rest.as_bytes();
let mut end: usize = start + marker.len();
while bytes[end] != b'"' {
end += if bytes[end] == b'\\' { 2 } else { 1 };
}
rest = format!("{}{}", &rest[..start], &rest[end + 1..]);
}
return rest;
}
pub(crate) const LIMITED_DIRECTORY_VARIABLE: &str = "CLI_GIT_LIMITED_CHILD_DIRECTORY";
#[cfg(unix)]
pub(crate) fn run_child_under_file_size_limit(test_name: &str, limit: u64, directory: &Path) -> std::process::ExitStatus {
use std::os::unix::process::CommandExt;
let mut command: Command = Command::new(this_test_binary().expect("test binary"));
command
.args(["--exact", test_name, "--ignored", "--nocapture"])
.env(LIMITED_DIRECTORY_VARIABLE, directory)
.stdin(std::process::Stdio::null());
unsafe {
command.pre_exec(move || {
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
let cap: libc::rlimit = libc::rlimit { rlim_cur: limit, rlim_max: limit };
if libc::setrlimit(libc::RLIMIT_FSIZE, &cap) != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
});
}
return command.status().expect("the child test starts");
}
#[path = "test_support_tests.rs"]
mod tests;
pub(crate) fn dead_copy_owner() -> crate::worktree_copy_owner::CopyOwner {
use std::process::Stdio;
let mut child: std::process::Child = spawn_owned(
Command::new("sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null()),
)
.expect("sleep starts");
let pid: i64 = i64::from(child.id());
let birth: String = crate::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 crate::worktree_copy_owner::CopyOwner {
pid,
birth_identity: birth,
pid_namespace: crate::pid_namespace::current_pid_namespace(),
};
}