use super::commit::{assert_nothing_left, git_text, keep_maintenance_in_the_foreground, remove_settled, wrapped_commit};
use super::support::{Fixture, Observed, decline_in_process_lfs_scan, executable, fixture, git, observe, repository, stderr_of};
use git_policy_cli::bounded_push::{PUSH_DEADLINE_VARIABLE, STOP_GRACE};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
const SHORT_DEADLINE_SECONDS: u64 = 2;
const HOLD_SECONDS: u64 = 3_000;
const LOAD_ALLOWANCE: Duration = Duration::from_secs(60);
fn world(fixture: &Fixture, source: &str) -> (PathBuf, PathBuf) {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
let origin: PathBuf = fixture.root.join("origin.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", "origin.git"]);
git(fixture, repo.as_path(), &["remote", "add", "origin", origin.to_str().expect("UTF-8")]);
git(fixture, repo.as_path(), &["config", "lfs.url", "http://127.0.0.1:9/never-contacted"]);
git(fixture, repo.as_path(), &["push", "--quiet", "--no-verify", "--set-upstream", "origin", "main"]);
keep_maintenance_in_the_foreground(fixture, repo.as_path());
decline_in_process_lfs_scan(fixture, repo.as_path());
executable(fixture.root.join("bin/git-lfs").as_path(), source.as_bytes());
return (repo, origin);
}
fn commit_file(fixture: &Fixture, directory: &Path, name: &str, environment: &[(&str, String)]) -> Observed {
std::fs::write(directory.join(name), format!("{name}\n")).expect("file");
git(fixture, directory, &["add", "--", name]);
for (key, value) in environment {
super::support::give_service_environment(fixture, key, value);
}
let mut command = wrapped_commit(fixture, directory, &["commit", "--quiet", &format!("--message={name}"), "--", name]);
for (key, value) in environment {
command.env(key, value);
}
return observe(&mut command, b"");
}
#[test]
fn two_worktrees_committing_at_once_with_a_slow_upload_both_finish_without_waiting_for_each_other() {
let fixture: Fixture = fixture("lfs-hang-two-worktrees");
let marks: PathBuf = fixture.root.join("marks");
std::fs::create_dir(&marks).expect("marks directory");
let barrier: String = format!(
"#!/bin/sh\ntouch '{dir}/started.'$$\ni=0\nwhile [ \"$(ls '{dir}' | grep -c '^started\\.')\" -lt 2 ] && [ $i -lt {tries} ]; do sleep 0.1; i=$((i+1)); done\n\
if [ \"$(ls '{dir}' | grep -c '^started\\.')\" -lt 2 ]; then touch '{dir}/alone'; fi\nexit 0\n",
dir = marks.display(),
tries = 1
);
let (repo, origin) = world(&fixture, barrier.as_str());
let second: PathBuf = fixture.root.join("second");
git(&fixture, repo.as_path(), &["worktree", "add", "--quiet", "-b", "other", second.to_str().expect("UTF-8")]);
git(&fixture, second.as_path(), &["push", "--quiet", "--no-verify", "--set-upstream", "origin", "other"]);
let (first_seen, second_seen) = std::thread::scope(|scope| {
let first = scope.spawn(|| {
return commit_file(&fixture, repo.as_path(), "first.txt", &[]);
});
let other = scope.spawn(|| {
return commit_file(&fixture, second.as_path(), "second.txt", &[]);
});
return (first.join().expect("first commit"), other.join().expect("second commit"));
});
for (label, seen) in [("first", &first_seen), ("second", &second_seen)] {
assert_eq!(seen.code, Some(0), "{label}: {}", stderr_of(seen));
assert!(!stderr_of(seen).contains("waiting for"), "{label} waited for the other: {}", stderr_of(seen));
}
for (branch, worktree) in [("main", repo.as_path()), ("other", second.as_path())] {
let published: String = git_text(&fixture, origin.as_path(), &["rev-parse", &format!("refs/heads/{branch}")]);
let landed: String = git_text(&fixture, worktree, &["rev-parse", "HEAD"]);
assert_eq!(published.trim(), landed.trim(), "{branch}: the new commit, not the earlier one, reached the origin");
}
assert_nothing_left(&fixture, repo.as_path());
remove_settled(&fixture);
}
#[test]
fn a_detached_upload_helper_holding_the_output_open_does_not_hold_the_commit() {
let fixture: Fixture = fixture("lfs-hang-detached");
let pid_file: PathBuf = fixture.root.join("detached.pid");
let source: String = format!(
"#!/bin/sh\ncat > /dev/null\n( setsid sh -c 'echo $$ > \"{}\"; exec sleep {HOLD_SECONDS}' & ) \nexec sleep {HOLD_SECONDS}\n",
pid_file.display()
);
let (repo, _origin) = world(&fixture, source.as_str());
let started: Instant = Instant::now();
let seen: Observed = commit_file(
&fixture,
repo.as_path(),
"a.txt",
&[(PUSH_DEADLINE_VARIABLE, SHORT_DEADLINE_SECONDS.to_string())],
);
let elapsed: Duration = started.elapsed();
let stderr: String = stderr_of(&seen);
assert_eq!(seen.code, Some(0), "the commit itself succeeds: {stderr}");
let report: String = super::support::push_report(repo.as_path(), &stderr);
assert!(report.contains(&format!("made no progress for {SHORT_DEADLINE_SECONDS} seconds and was stopped")), "{report}");
assert!(
elapsed < Duration::from_secs(SHORT_DEADLINE_SECONDS) + STOP_GRACE + LOAD_ALLOWANCE,
"the wrapper returned at the deadline plus the grace, not when the detached helper ended: {elapsed:?}"
);
let recorded: String = std::fs::read_to_string(&pid_file).unwrap_or_default();
if let Ok(pid) = recorded.trim().parse::<i32>() {
unsafe { libc::kill(pid, libc::SIGKILL) };
}
remove_settled(&fixture);
}