use super::commit::{remove_settled, wrapped_commit};
use super::fault::{Effect, SWEEP_WIDTH, sweep, sweep_persistent, run_to_end, staged_repository, verify_recovery};
use super::support::Fixture;
use super::support::fixture;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn a_descriptor_limit_hit_at_any_open_leaves_a_recoverable_repository() {
sweep("emfile-openat", "openat", Effect::Fail("EMFILE"), false);
}
#[test]
fn a_descriptor_limit_hit_at_any_pipe_leaves_a_recoverable_repository() {
sweep("emfile-pipe", "pipe2", Effect::Fail("EMFILE"), false);
}
#[test]
fn a_process_limit_that_stays_exhausted_from_any_start_leaves_a_recoverable_repository() {
sweep_persistent("nproc-clone3", "clone3", Effect::Fail("EAGAIN"), false, 6);
}
fn check_limited_run(label: &str, output: &Output) -> Result<(), String> {
let text: String = String::from_utf8_lossy(&[output.stdout.as_slice(), output.stderr.as_slice()].concat()).into_owned();
if output.status.signal().is_some() {
return Err(format!("{label}: the command was ended by signal {:?}: {text}", output.status.signal()));
}
let code: i32 = output.status.code().unwrap_or(-1);
if !matches!(code, 0 | 1 | 2 | 126 | 127) {
return Err(format!("{label}: unexpected exit {code}: {text}"));
}
if code != 0 && text.trim().is_empty() {
return Err(format!("{label}: a failed commit must say why"));
}
return Ok(());
}
fn commit_with_open_file_limit(fixture: &Fixture, repo: &std::path::Path, limit: u64) -> Command {
let mut command: Command = wrapped_commit(fixture, repo, &["commit", "--quiet", "--message=x", "--", "a.txt"]);
unsafe {
command.pre_exec(move || {
let cap: libc::rlimit = libc::rlimit { rlim_cur: limit, rlim_max: limit };
if libc::setrlimit(libc::RLIMIT_NOFILE, &cap) != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
});
}
return command;
}
fn commit_with_file_size_limit(fixture: &Fixture, repo: &std::path::Path, limit: u64) -> Command {
let mut command: Command = wrapped_commit(fixture, repo, &["commit", "--quiet", "--message=x", "--", "a.txt"]);
unsafe {
command.pre_exec(move || {
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
let cap: libc::rlimit = libc::rlimit { rlim_cur: limit, rlim_max: limit };
if libc::setrlimit(libc::RLIMIT_FSIZE, &cap) != 0 {
return Err(std::io::Error::last_os_error());
}
return Ok(());
});
}
return command;
}
fn sweep_limits(label: &str, values: &[u64], limited: fn(&Fixture, &std::path::Path, u64) -> Command, refusal: &str) {
let fixture: Fixture = fixture(label);
let failures: Mutex<Vec<String>> = Mutex::new(Vec::new());
let refused: AtomicUsize = AtomicUsize::new(0);
let landed: AtomicUsize = AtomicUsize::new(0);
for batch in values.chunks(SWEEP_WIDTH) {
std::thread::scope(|scope| {
for value in batch {
let shared_fixture: &Fixture = &fixture;
let collected: &Mutex<Vec<String>> = &failures;
let refusals: &AtomicUsize = &refused;
let landings: &AtomicUsize = &landed;
scope.spawn(move || {
let case_label: String = format!("{label}-{value}");
let repo: PathBuf = staged_repository(shared_fixture, &format!("case-{value}"));
let output: Output = run_to_end(&mut limited(shared_fixture, repo.as_path(), *value));
let said: String = String::from_utf8_lossy(&[output.stdout.as_slice(), output.stderr.as_slice()].concat()).into_owned();
if said.contains(refusal) {
refusals.fetch_add(1, Ordering::SeqCst);
}
if output.status.code() == Some(0) {
landings.fetch_add(1, Ordering::SeqCst);
}
let verdict: Result<(), String> = check_limited_run(case_label.as_str(), &output)
.and_then(|()| { return verify_recovery(shared_fixture, repo.as_path(), case_label.as_str(), said.as_str()); });
if let Err(message) = verdict {
collected.lock().expect("failure list").push(message);
}
});
}
});
}
let found: Vec<String> = failures.into_inner().expect("failure list");
assert!(found.is_empty(), "{} of {} limits left the repository unrecovered:\n{}", found.len(), values.len(), found.join("\n"));
assert!(refused.load(Ordering::SeqCst) > 0, "no limit made a run report {refusal:?}, so the limits tested nothing");
assert!(landed.load(Ordering::SeqCst) > 0, "no limit let the commit land, so none was close to exhaustion");
remove_settled(&fixture);
}
#[test]
fn a_descriptor_limit_close_to_exhaustion_leaves_a_recoverable_repository() {
let values: Vec<u64> = (4..=48).collect();
sweep_limits("rlimit-nofile", values.as_slice(), commit_with_open_file_limit, "Too many open files");
}
#[test]
fn a_file_size_limit_that_cuts_each_file_leaves_a_recoverable_repository() {
let values: Vec<u64> = vec![0, 1, 8, 32, 64, 100, 128, 160, 200, 256, 300, 400, 512, 700, 1000, 1500, 2000, 3000, 5000, 10_000];
sweep_limits("rlimit-fsize", values.as_slice(), commit_with_file_size_limit, "File too large");
}