use super::commit::{remove_settled, wrapped_commit};
use super::fault::{staged_repository, verify_recovery};
use super::support::{Fixture, bounded, close_on_exec_pipe, direct, fixture, git, repository, wrapper_first_path};
use std::os::fd::{FromRawFd, OwnedFd};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
#[derive(Clone, Copy, Debug)]
enum StreamFault {
Closed,
BrokenPipe,
FullDevice,
}
const OUTPUT_FAULTS: [StreamFault; 3] = [StreamFault::Closed, StreamFault::BrokenPipe, StreamFault::FullDevice];
fn broken_pipe() -> OwnedFd {
let ends: [libc::c_int; 2] = close_on_exec_pipe();
let (read_end, write_end): (OwnedFd, OwnedFd) = unsafe { (OwnedFd::from_raw_fd(ends[0]), OwnedFd::from_raw_fd(ends[1])) };
drop(read_end);
return write_end;
}
fn faulty_output(fault: StreamFault) -> Stdio {
match fault {
StreamFault::Closed => return Stdio::null(),
StreamFault::BrokenPipe => return Stdio::from(broken_pipe()),
StreamFault::FullDevice => {
return Stdio::from(std::fs::OpenOptions::new().write(true).open("/dev/full").expect("/dev/full"));
}
}
}
fn close_standard_streams(command: &mut Command) {
unsafe {
command.pre_exec(|| {
libc::close(0);
libc::close(1);
libc::close(2);
return Ok(());
});
}
}
fn apply(command: &mut Command, fault: StreamFault) {
match fault {
StreamFault::Closed => {
command.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
close_standard_streams(command);
}
StreamFault::BrokenPipe | StreamFault::FullDevice => {
command.stdin(Stdio::null()).stdout(faulty_output(fault)).stderr(faulty_output(fault));
}
}
}
const SUMMARY_COMMIT: [&str; 4] = ["commit", "--message=x", "--", "a.txt"];
fn commit_with_faulty_streams(name: &str, fault: StreamFault) {
let fixture: Fixture = fixture(name);
let repo: PathBuf = staged_repository(&fixture, "repo");
let mut command: Command = wrapped_commit(&fixture, repo.as_path(), &SUMMARY_COMMIT);
apply(&mut command, fault);
let finished: ExitStatus = command.status().expect("the commit starts");
assert_ne!(finished.signal(), Some(9), "{fault:?}: the commit hit its time bound");
assert_ne!(finished.code(), Some(101), "{fault:?}: the wrapper panicked");
let verdict: Result<(), String> = verify_recovery(&fixture, repo.as_path(), name, format!("{finished:?}").as_str());
assert_eq!(verdict, Ok(()), "{fault:?}");
remove_settled(&fixture);
}
#[test]
fn a_commit_with_closed_standard_streams_lands() {
commit_with_faulty_streams("stream-closed", StreamFault::Closed);
}
#[test]
fn a_commit_with_broken_output_pipes_lands() {
commit_with_faulty_streams("stream-pipe", StreamFault::BrokenPipe);
}
#[test]
fn a_commit_with_full_output_devices_lands() {
commit_with_faulty_streams("stream-full", StreamFault::FullDevice);
}
#[test]
fn a_commit_with_faulty_streams_ends_as_git_does() {
let fixture: Fixture = fixture("stream-exit");
for fault in OUTPUT_FAULTS {
let wrapped_repo: PathBuf = staged_repository(&fixture, &format!("wrapped-{fault:?}"));
let mut through_wrapper: Command = wrapped_commit(&fixture, wrapped_repo.as_path(), &SUMMARY_COMMIT);
apply(&mut through_wrapper, fault);
let wrapped: ExitStatus = through_wrapper.status().expect("the wrapped commit starts");
let direct_repo: PathBuf = staged_repository(&fixture, &format!("direct-{fault:?}"));
let mut straight: Command = direct(&fixture);
straight.current_dir(direct_repo.as_path()).args(SUMMARY_COMMIT);
apply(&mut straight, fault);
let oracle: ExitStatus = straight.status().expect("the direct commit starts");
assert_eq!(
shell_status(wrapped),
shell_status(oracle),
"{fault:?}: the wrapped commit ended with {wrapped:?}, Git started directly with {oracle:?}"
);
}
remove_settled(&fixture);
}
fn shell_status(status: ExitStatus) -> i32 {
return status.code().unwrap_or_else(|| { return 128 + status.signal().unwrap_or(0); });
}
fn repository_with_a_big_file(fixture: &Fixture) -> PathBuf {
let repo: PathBuf = repository(fixture, std::ffi::OsStr::new("big"));
std::fs::write(repo.join("big.txt"), "x".repeat(1 << 20)).expect("big file");
git(fixture, repo.as_path(), &["add", "--", "big.txt"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=big"]);
return repo;
}
fn show_both_ways(fixture: &Fixture, repo: &Path, fault: StreamFault) -> (ExitStatus, ExitStatus) {
let mut through_wrapper: Command = bounded(fixture, fixture.root.join("bin/git").as_path(), wrapper_first_path(fixture).as_os_str());
through_wrapper.current_dir(repo).args(["show", "HEAD:big.txt"]);
apply(&mut through_wrapper, fault);
let mut straight: Command = direct(fixture);
straight.current_dir(repo).args(["show", "HEAD:big.txt"]);
apply(&mut straight, fault);
return (
through_wrapper.status().expect("the wrapped command starts"),
straight.status().expect("the direct command starts"),
);
}
#[test]
fn a_forwarded_command_into_a_broken_pipe_ends_as_git_does() {
let fixture: Fixture = fixture("stream-forward-pipe");
let repo: PathBuf = repository_with_a_big_file(&fixture);
let (wrapped, straight) = show_both_ways(&fixture, repo.as_path(), StreamFault::BrokenPipe);
assert_eq!(straight.signal(), Some(libc::SIGPIPE), "the control: Git started directly ends by SIGPIPE");
assert_eq!(wrapped, straight);
remove_settled(&fixture);
}
#[test]
fn a_forwarded_command_into_a_full_device_ends_as_git_does() {
let fixture: Fixture = fixture("stream-forward-full");
let repo: PathBuf = repository_with_a_big_file(&fixture);
let (wrapped, straight) = show_both_ways(&fixture, repo.as_path(), StreamFault::FullDevice);
assert_ne!(straight.code(), Some(0), "the control: Git started directly reports a write error");
assert_eq!(wrapped, straight);
remove_settled(&fixture);
}
#[test]
fn a_forwarded_command_with_closed_streams_ends_as_git_does() {
let fixture: Fixture = fixture("stream-forward-closed");
let repo: PathBuf = repository_with_a_big_file(&fixture);
let (wrapped, straight) = show_both_ways(&fixture, repo.as_path(), StreamFault::Closed);
assert_eq!(wrapped, straight);
remove_settled(&fixture);
}