use super::commit::{remove_settled, run_commit};
use super::fault::{staged_repository, verify_recovery};
use super::fault_run::{is_running, run_watched, send, spawn_watched, unbounded_wrapper};
use super::support::{Fixture, executable, fixture, stalling_editor};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{Duration, Instant};
const GONE_PATIENCE: Duration = Duration::from_secs(30);
fn stand_in_git(fixture: &Fixture, log: &Path, started: &Path, process_id: &Path) -> PathBuf {
let directory: PathBuf = fixture.root.join("shim");
std::fs::create_dir(&directory).expect("shim directory");
let script: String = String::from(
"#!/bin/sh
case \" $* \" in
*\" commit --quiet\"*)
ulimit -c 0
stall=
for pair in INT:130 TERM:143 HUP:129 QUIT:131; do
name=${pair%%:*}
code=${pair##*:}
trap \"/usr/bin/kill \\$stall 2>/dev/null; echo $name >> 'LOG'; exit $code\" $name
done
echo \"$$\" > 'PID'
printf started > 'STARTED'
/usr/bin/sleep 60 </dev/null >/dev/null 2>&1 &
stall=$!
wait $stall
exit 1
;;
esac
exec /usr/bin/git \"$@\"
",
)
.replace("LOG", &log.display().to_string())
.replace("PID", &process_id.display().to_string())
.replace("STARTED", &started.display().to_string());
executable(directory.join("git").as_path(), script.as_bytes());
return directory;
}
fn assert_gone(pid: i32, what: &str) {
let began: Instant = Instant::now();
while is_running(pid) {
assert!(began.elapsed() < GONE_PATIENCE, "{what} (process {pid}) still runs {GONE_PATIENCE:?} after the wrapper ended");
std::thread::sleep(Duration::from_millis(20));
}
}
fn relayed_to_git(name: &str, label: &str, signal: i32) {
let fixture: Fixture = fixture(label);
let repo: PathBuf = staged_repository(&fixture, "repo");
let log: PathBuf = fixture.root.join("received.log");
let started: PathBuf = fixture.root.join("git-started");
let process_id: PathBuf = fixture.root.join("git-pid");
let shim: PathBuf = stand_in_git(&fixture, log.as_path(), started.as_path(), process_id.as_path());
let path: String = format!("{}:{}", fixture.root.join("bin").display(), shim.display());
let mut command: Command = unbounded_wrapper(&fixture, repo.as_path(), &["commit", "--quiet", "--", "a.txt"]);
command.env("PATH", &path).env("GIT_EDITOR", "true");
unsafe {
command.pre_exec(|| { return super::support::LimitKind::CoreFile.apply(0, 0); });
}
let mut running = spawn_watched(&fixture, &mut command);
running.wait_for(started.as_path());
send(running.id(), signal);
let finished: Output = running.finish();
let said: String = String::from_utf8_lossy(finished.stderr.as_slice()).into_owned();
assert_eq!(finished.status.code(), Some(128 + signal), "SIG{name}: {said}");
let received: String = std::fs::read_to_string(&log).expect("the stand-in Git recorded what reached it");
assert_eq!(received, format!("{name}\n"), "exactly the one signal sent to the wrapper reaches Git");
let git_pid: i32 = std::fs::read_to_string(&process_id).expect("the stand-in Git's process ID").trim().parse().expect("a process ID");
assert_gone(git_pid, "the Git the wrapper started");
let verdict: Result<(), String> = verify_recovery(&fixture, repo.as_path(), label, said.as_str());
assert_eq!(verdict, Ok(()));
remove_settled(&fixture);
}
#[test]
fn a_hangup_sent_to_the_wrapper_alone_is_relayed_to_git() {
relayed_to_git("HUP", "relay-hup", libc::SIGHUP);
}
#[test]
fn an_interrupt_sent_to_the_wrapper_alone_is_relayed_to_git() {
relayed_to_git("INT", "relay-int", libc::SIGINT);
}
#[test]
fn a_termination_sent_to_the_wrapper_alone_is_relayed_to_git() {
relayed_to_git("TERM", "relay-term", libc::SIGTERM);
}
#[test]
fn a_quit_sent_to_the_wrapper_alone_is_relayed_to_git() {
relayed_to_git("QUIT", "relay-quit", libc::SIGQUIT);
}
#[test]
fn a_hangup_of_the_whole_group_during_the_editor_ends_the_commit_and_leaves_nothing() {
let fixture: Fixture = fixture("signal-group-hup");
let repo: PathBuf = staged_repository(&fixture, "repo");
let started: PathBuf = fixture.root.join("editor-started");
let editor: PathBuf = stalling_editor(&fixture, started.as_path());
let mut command: Command = unbounded_wrapper(&fixture, repo.as_path(), &["commit", "--quiet", "--", "a.txt"]);
command.env("GIT_EDITOR", &editor);
let mut running = spawn_watched(&fixture, &mut command);
running.wait_for(started.as_path());
send(-running.id(), libc::SIGHUP);
let finished: Output = running.finish();
let said: String = String::from_utf8_lossy(finished.stderr.as_slice()).into_owned();
assert_eq!(finished.status.code(), Some(128 + libc::SIGHUP), "{said}");
assert_eq!(verify_recovery(&fixture, repo.as_path(), "signal-group-hup", said.as_str()), Ok(()));
remove_settled(&fixture);
}
#[test]
fn an_unsignalled_commit_through_the_same_shape_lands() {
let fixture: Fixture = fixture("signal-shape");
let repo: PathBuf = staged_repository(&fixture, "repo");
let output: Output = run_watched(&fixture, &mut unbounded_wrapper(&fixture, repo.as_path(), &["commit", "--quiet", "--message=x", "--", "a.txt"]));
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(output.stderr.as_slice()));
let next = run_commit(&fixture, repo.as_path(), &["status", "--porcelain"]);
assert_eq!(next.code, Some(0));
remove_settled(&fixture);
}