use super::commit::{git_text, remove_settled, wrapped_commit};
use super::support::{Fixture, FixtureService, Observed, fixture, git, observe, repository, start_fixture_service};
use git_policy_cli::service_messages::{ClientFailure, render};
use git_policy_cli::transaction_phase::PHASE_SIGNAL_VARIABLE;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
const PATIENCE: Duration = Duration::from_secs(60);
struct Case {
fixture: Fixture,
repo: PathBuf,
service: FixtureService,
blob: String,
markers: PathBuf,
}
fn case(name: &str) -> Case {
let fixture: Fixture = fixture(name);
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
std::fs::write(repo.join("a.txt"), "base\n").expect("a");
git(&fixture, &repo, &["add", "a.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(&fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
std::fs::write(repo.join("a.txt"), "base\nmore\n").expect("edit");
let blob: String = git_text(&fixture, &repo, &["hash-object", "a.txt"]).trim().to_string();
let markers: PathBuf = fixture.root.join("markers");
std::fs::create_dir(&markers).expect("markers");
let service: FixtureService = start_fixture_service(&fixture);
return Case { fixture, repo, service, blob, markers };
}
fn remove_blob(repo: &Path, blob: &str) {
let _removed = std::fs::remove_file(repo.join(".git/objects").join(&blob[..2]).join(&blob[2..]));
}
fn answer_phase(case: &Case, phase: &'static str, occurrences: usize, remove: fn(usize) -> bool) -> std::thread::JoinHandle<Vec<bool>> {
let markers: PathBuf = case.markers.clone();
let repo: PathBuf = case.repo.clone();
let blob: String = case.blob.clone();
return std::thread::spawn(move || {
let mut removed: Vec<bool> = Vec::new();
for number in 1..=occurrences {
let reached: PathBuf = markers.join(format!("{phase}-{number}.reached"));
let began: Instant = Instant::now();
while !reached.exists() {
if began.elapsed() > PATIENCE || markers.join("finished").exists() {
return removed;
}
std::thread::sleep(Duration::from_millis(5));
}
let doomed: bool = remove(number);
if doomed {
remove_blob(repo.as_path(), blob.as_str());
}
removed.push(doomed);
std::fs::write(markers.join(format!("{phase}-{number}.release")), "").expect("release");
}
return removed;
});
}
fn commit_paused(case: &Case, phase: &str, answer: std::thread::JoinHandle<Vec<bool>>) -> (Observed, Vec<bool>) {
let mut command: Command = wrapped_commit(&case.fixture, &case.repo, &["commit", "--quiet", "-m", "more", "--", "a.txt"]);
command.env("XDG_RUNTIME_DIR", &case.service.runtime);
command.env(PHASE_SIGNAL_VARIABLE, format!("{phase}:pause:{}", case.markers.display()));
let seen: Observed = observe(&mut command, b"");
std::fs::write(case.markers.join("finished"), "").expect("finished");
let removed: Vec<bool> = answer.join().expect("the answering thread");
return (seen, removed);
}
fn stderr_text(seen: &Observed) -> String {
return String::from_utf8_lossy(seen.stderr.as_slice()).into_owned();
}
fn tip_subject(case: &Case) -> String {
return git_text(&case.fixture, &case.repo, &["log", "-1", "--format=%s"]).trim().to_string();
}
fn assert_plain_failure(case: &Case, seen: &Observed) {
let all: String = stderr_text(seen);
let text: String = all.lines().filter(|line| { return !line.starts_with("[warn] "); }).collect::<Vec<&str>>().join("\n");
assert_eq!(seen.code, Some(1), "{text}");
assert_eq!(text.trim_end(), render(&ClientFailure::ContentPruned), "{text}");
for leaked in ["schemaVersion", "engine-failure", "{\""] {
assert!(!text.contains(leaked), "{leaked} reached the person: {text}");
}
assert!(text.contains("--prune=now") && text.contains("Nothing in the repository changed"), "{text}");
assert_eq!(tip_subject(case), "base", "a failed commit changed the branch");
assert_eq!(std::fs::read_to_string(case.repo.join("a.txt")).expect("a"), "base\nmore\n", "the person's edit is intact");
}
#[test]
fn content_removed_while_staged_is_prepared_again_and_the_commit_lands() {
let case: Case = case("pruned-staged-retry");
let answer = answer_phase(&case, "service-staged", 2, |number| { return number == 1; });
let (seen, removed) = commit_paused(&case, "service-staged", answer);
let text: String = stderr_text(&seen);
assert_eq!(removed.first(), Some(&true), "the content was removed once");
assert_eq!(seen.code, Some(0), "{text}");
assert!(!text.contains("schemaVersion"), "{text}");
assert_eq!(tip_subject(&case), "more");
remove_settled(&case.fixture);
}
#[test]
fn content_removed_after_staging_twice_fails_plainly_without_a_machine_event() {
let case: Case = case("pruned-staged-twice");
let answer = answer_phase(&case, "service-staged", 2, |_| { return true; });
let (seen, removed) = commit_paused(&case, "service-staged", answer);
assert_eq!(removed, vec![true, true], "the commit was prepared again once");
assert_plain_failure(&case, &seen);
remove_settled(&case.fixture);
}
#[test]
fn content_removed_before_landing_is_prepared_again_and_the_commit_lands() {
let case: Case = case("pruned-landing-retry");
let answer = answer_phase(&case, "service-prepared", 2, |number| { return number == 1; });
let (seen, removed) = commit_paused(&case, "service-prepared", answer);
let text: String = stderr_text(&seen);
assert_eq!(removed.first(), Some(&true), "the content was removed once");
assert_eq!(seen.code, Some(0), "{text}");
assert!(!text.contains("schemaVersion") && !text.contains("could not finish"), "{text}");
assert_eq!(tip_subject(&case), "more");
remove_settled(&case.fixture);
}
#[test]
fn content_removed_before_landing_twice_fails_plainly() {
let case: Case = case("pruned-landing-twice");
let answer = answer_phase(&case, "service-prepared", 2, |_| { return true; });
let (seen, removed) = commit_paused(&case, "service-prepared", answer);
assert_eq!(removed, vec![true, true], "the commit was prepared again once");
assert_plain_failure(&case, &seen);
remove_settled(&case.fixture);
}