use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
pub use crate::support_fixture_directories::*;
pub use crate::support_fixture_service::*;
pub use crate::support_observation::*;
pub use crate::support_program_names_and_time_bound::*;
pub use crate::support_resource_limits::*;
#[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 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 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();
}
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(),
};
}