use super::commit::{assert_nothing_left, git_text, keep_maintenance_in_the_foreground, run_commit};
use super::commit::COMMIT_BOUND_SECONDS;
use super::strace::strace_program;
use super::support::{Fixture, Observed, bounded_for, git, repository, wrapper_first_path};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
#[derive(Clone, Copy, Debug)]
pub(super) enum Effect {
SlowReturn(u64),
}
#[derive(Clone, Copy, Debug)]
pub(super) enum When {
FromNth(usize),
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Injection {
pub(super) syscall: &'static str,
pub(super) effect: Effect,
pub(super) when: When,
pub(super) follow_forks: bool,
}
impl Injection {
fn specification(&self) -> String {
let effect: String = match self.effect {
Effect::SlowReturn(microseconds) => format!("delay_exit={microseconds}us"),
};
let when: String = match self.when {
When::FromNth(call) => format!("{call}+"),
};
return format!("{}:{effect}:when={when}", self.syscall);
}
}
pub(super) struct Ran {
pub(super) output: Output,
pub(super) trace: String,
}
impl Ran {
pub(super) fn injected(&self) -> bool {
return self.trace.contains("(INJECTED)")
|| self.trace.contains("(DELAYED)")
|| self.trace.contains("+++ killed by SIGKILL +++");
}
pub(super) fn text(&self) -> String {
return String::from_utf8_lossy(&[self.output.stdout.as_slice(), self.output.stderr.as_slice()].concat())
.into_owned();
}
pub(super) fn code(&self) -> i32 {
return self.output.status.code().unwrap_or(-1);
}
}
pub(super) fn staged_repository(fixture: &Fixture, name: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
std::fs::write(repo.join("a.txt"), b"one\n").expect("a");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
keep_maintenance_in_the_foreground(fixture, repo.as_path());
return repo;
}
fn traced(
fixture: &Fixture,
repo: &Path,
arguments: &[&str],
calls: &str,
injection: Option<&Injection>,
follow_forks: bool,
trace: &Path,
) -> Command {
let mut command: Command = bounded_for(
fixture,
strace_program().expect("strace is installed").as_path(),
wrapper_first_path(fixture).as_os_str(),
COMMIT_BOUND_SECONDS,
);
if follow_forks {
command.arg("--follow-forks");
}
command
.arg("--quiet")
.arg("--quiet")
.arg(format!("--output={}", trace.display()))
.arg(format!("--trace={calls}"));
if let Some(fault) = injection {
command.arg(format!("--inject={}", fault.specification()));
}
command.arg(fixture.root.join("bin/git")).args(arguments).current_dir(repo);
return command;
}
pub(super) fn run_to_end(command: &mut Command) -> Output {
return command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("the traced command starts");
}
pub(super) const COMMIT_ARGUMENTS: [&str; 5] = ["commit", "--quiet", "--message=x", "--", "a.txt"];
pub(super) fn run_injected_with_environment(
fixture: &Fixture,
repo: &Path,
label: &str,
fault: &Injection,
environment: &[(&str, &str)],
) -> Ran {
let trace: PathBuf = fixture.root.join(format!("trace-{label}.txt"));
let mut command: Command = traced(
fixture,
repo,
&COMMIT_ARGUMENTS,
fault.syscall,
Some(fault),
fault.follow_forks,
trace.as_path(),
);
for (name, value) in environment {
command.env(name, value);
}
let output: Output = run_to_end(&mut command);
let log: String = std::fs::read_to_string(&trace).unwrap_or_default();
return Ran { output, trace: log };
}
fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(text) = payload.downcast_ref::<String>() {
return text.clone();
}
if let Some(text) = payload.downcast_ref::<&str>() {
return String::from(*text);
}
return String::from("a check failed without a message");
}
fn names_in_git_directory(repo: &Path) -> String {
let mut names: Vec<String> = Vec::new();
for directory in [repo.join(".git"), repo.join(".git/cli-git-transactions")] {
let Ok(entries) = std::fs::read_dir(&directory) else {
continue;
};
for entry in entries.flatten() {
let name: String = entry.file_name().to_string_lossy().into_owned();
if name.contains("lock") || name.starts_with("cli-git") || directory.ends_with("cli-git-transactions") {
names.push(format!("{}/{name}", directory.strip_prefix(repo).unwrap_or(&directory).display()));
}
}
}
names.sort();
return names.join(", ");
}
pub(super) fn verify_recovery(fixture: &Fixture, repo: &Path, label: &str, said: &str) -> Result<(), String> {
let next: Observed = run_commit(fixture, repo, &["status", "--porcelain"]);
if next.code != Some(0) {
return Err(format!(
"{label}: the next command must succeed, exit {:?}: {}\nfirst run said: {}",
next.code,
String::from_utf8_lossy(&next.stderr),
said.chars().take(600).collect::<String>()
));
}
let commits: String = git_text(fixture, repo, &["rev-list", "--count", "HEAD"]);
let status: String = String::from_utf8_lossy(next.stdout.as_slice()).into_owned();
match commits.trim() {
"1" if status == "A a.txt\n" => {}
"2" if status.is_empty() => {}
other => return Err(format!("{label}: {other} commits with status {status:?}; first run said: {said}")),
}
let checked: Output = Command::new("/usr/bin/git")
.current_dir(repo)
.args(["fsck", "--strict", "--no-dangling"])
.env_clear()
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.output()
.expect("git fsck starts");
if !checked.status.success() || !checked.stderr.is_empty() {
return Err(format!("{label}: git fsck: {}", String::from_utf8_lossy(&checked.stderr)));
}
std::fs::write(repo.join("b.txt"), b"two\n").expect("b");
let staged: Observed = run_commit(fixture, repo, &["add", "--", "b.txt"]);
if staged.code != Some(0) {
return Err(format!(
"{label}: staging the next file failed after the first run, exit {:?}: {}\nfirst run said: {said}\nleft in .git: {}",
staged.code,
String::from_utf8_lossy(&staged.stderr),
names_in_git_directory(repo)
));
}
let again: Observed = run_commit(fixture, repo, &["commit", "--quiet", "--message=y", "--", "b.txt"]);
if again.code != Some(0) {
return Err(format!(
"{label}: a further commit must land, exit {:?}: {}\nfirst run said: {said}",
again.code,
String::from_utf8_lossy(&again.stderr)
));
}
let leftovers = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { assert_nothing_left(fixture, repo); }));
if let Err(payload) = leftovers {
return Err(format!("{label}: {}; first run said: {said}", panic_text(payload.as_ref())));
}
return Ok(());
}