use super::commit::{await_marker, remove_settled, wrapped_commit};
use super::fault::{staged_repository, verify_recovery};
use super::support::{Fixture, fixture, stalling_editor};
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
const PHASE_MARKER: &str = "CLI_GIT_TEST_ONLY_PHASE_SIGNAL";
const PHASES: [&str; 6] = [
"capture-locked",
"preparation-done",
"landing-locked",
"objects-migrated",
"ref-updated",
"index-installed",
];
fn send(target: i32, signal: i32) {
let status: libc::c_int = unsafe { libc::kill(target, signal) };
assert_eq!(status, 0, "kill({target}, {signal}) failed: {}", std::io::Error::last_os_error());
}
const ANNOUNCEMENT_BOUND_MILLISECONDS: libc::c_int = 90_000;
fn announcement_pipe(path: &Path) -> std::fs::File {
let name: std::ffi::CString = std::ffi::CString::new(path.as_os_str().as_bytes()).expect("a path has no NUL");
let made: libc::c_int = unsafe { libc::mkfifo(name.as_ptr(), 0o600) };
assert_eq!(made, 0, "mkfifo failed: {}", std::io::Error::last_os_error());
return std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK)
.open(path)
.expect("the read end of the announcement pipe opens");
}
fn await_announcement(pipe: &std::fs::File, path: &Path) {
let mut watched: libc::pollfd = libc::pollfd { fd: pipe.as_raw_fd(), events: libc::POLLIN, revents: 0 };
let ready: libc::c_int = unsafe { libc::poll(&raw mut watched, 1, ANNOUNCEMENT_BOUND_MILLISECONDS) };
assert!(ready > 0, "the editor never announced itself on {}", path.display());
}
fn signal_during_the_editor(name: &str, signal: i32) {
let fixture: Fixture = fixture(name);
let started: PathBuf = fixture.root.join("editor-started");
let announcement: std::fs::File = announcement_pipe(started.as_path());
let repo: PathBuf = staged_repository(&fixture, "repo");
let editor: PathBuf = stalling_editor(&fixture, started.as_path());
let child: Child = wrapped_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--", "a.txt"])
.env("GIT_EDITOR", &editor)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("the commit starts");
await_announcement(&announcement, started.as_path());
send(i32::try_from(child.id()).expect("a process ID fits"), signal);
let finished = child.wait_with_output().expect("the commit ends");
assert_eq!(
finished.status.code(),
Some(128 + signal),
"{}",
String::from_utf8_lossy(finished.stderr.as_slice())
);
let verdict: Result<(), String> = verify_recovery(&fixture, repo.as_path(), name, "");
assert_eq!(verdict, Ok(()));
remove_settled(&fixture);
}
#[test]
fn a_hangup_during_the_editor_ends_the_commit_and_leaves_nothing() {
signal_during_the_editor("signal-hup-editor", libc::SIGHUP);
}
#[test]
fn an_interrupt_during_the_editor_ends_the_commit_and_leaves_nothing() {
signal_during_the_editor("signal-int-editor", libc::SIGINT);
}
#[test]
fn a_quit_during_the_editor_ends_the_commit_and_leaves_nothing() {
signal_during_the_editor("signal-quit-editor", libc::SIGQUIT);
}
#[test]
fn a_hangup_of_the_process_group_at_each_phase_leaves_a_recoverable_repository() {
let fixture: Fixture = fixture("signal-group-hup");
for phase in PHASES {
let repo: PathBuf = staged_repository(&fixture, &format!("repo-{phase}"));
let markers: PathBuf = fixture.root.join(format!("markers-{phase}"));
std::fs::create_dir(&markers).expect("marker directory");
let mut command: Command = wrapped_commit(&fixture, repo.as_path(), &["commit", "--quiet", "--message=x", "--", "a.txt"]);
command
.env(PHASE_MARKER, format!("{phase}:pause:{}", markers.display()))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
unsafe {
command.pre_exec(|| {
libc::setsid();
return Ok(());
});
}
let child: Child = command.spawn().expect("the commit starts");
await_marker(markers.join(format!("{phase}.reached")).as_path());
send(-i32::try_from(child.id()).expect("a process ID fits"), libc::SIGHUP);
let finished = child.wait_with_output().expect("the commit ends");
let said: String = String::from_utf8_lossy(finished.stderr.as_slice()).into_owned();
let verdict: Result<(), String> = verify_recovery(&fixture, repo.as_path(), phase, said.as_str());
assert_eq!(verdict, Ok(()), "hangup at {phase}");
}
remove_settled(&fixture);
}