#![cfg(unix)]
use crate::auto_push_single_flight::{PushAttempt, PushGit};
use crate::bounded_push::{PUSH_DEADLINE_VARIABLE, Patience, STOPPED_EXIT_CODE, WAIT_NOTE_PHRASE, run_bounded_push};
use crate::lfs_bounded_push::run_push;
use crate::smart_http_support::SmartHttp;
use crate::test_support::{REAL_GIT, fixture, git, git_text, remove, repository, scratch_environment};
use std::io::Read;
use std::net::TcpListener;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::ffi::OsString;
use std::process::Command;
use std::time::{Duration, Instant};
const STALL: Duration = Duration::from_secs(1);
const SILENCE_WINDOWS: u32 = 4;
const GRACE: Duration = Duration::from_secs(2);
const STOP_SLACK: Duration = Duration::from_secs(30);
fn silent_remote(projects: &std::path::Path, silence: Duration) -> PathBuf {
let remote: PathBuf = projects.join("remote.git");
git(projects, &["init", "--bare", "--quiet", remote.to_str().expect("utf-8")]);
git(remote.as_path(), &["config", "http.receivepack", "true"]);
git(remote.as_path(), &["config", "receive.keepAlive", "0"]);
let hook: PathBuf = remote.join("hooks").join("pre-receive");
std::fs::write(hook.as_path(), format!("#!/bin/sh\nexec sleep {}\n", silence.as_secs())).expect("hook");
std::fs::set_permissions(hook.as_path(), std::fs::Permissions::from_mode(0o755)).expect("hook mode");
return remote;
}
fn push_command(repo: &std::path::Path) -> Command {
let mut command: Command = Command::new(REAL_GIT);
command
.args(["push", "origin", "main"])
.current_dir(repo)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_TERMINAL_PROMPT", "0");
return command;
}
fn run(repo: &std::path::Path, patience: Patience) -> (PushAttempt, Duration) {
let started: Instant = Instant::now();
let attempt: PushAttempt = run_bounded_push(push_command(repo), patience, GRACE, scratch_environment().as_slice()).expect("runs");
return (attempt, started.elapsed());
}
#[test]
fn a_push_over_http_waits_for_a_silent_server_check_and_lands() {
let root: PathBuf = fixture("slow-server-http");
let projects: PathBuf = root.join("projects");
std::fs::create_dir(&projects).expect("projects");
let remote: PathBuf = silent_remote(projects.as_path(), STALL * SILENCE_WINDOWS);
let server: SmartHttp = SmartHttp::start(projects.as_path());
let repo: PathBuf = repository(root.as_path(), "repo");
git(repo.as_path(), &["remote", "add", "origin", format!("http://127.0.0.1:{}/remote.git", server.port()).as_str()]);
let (attempt, elapsed): (PushAttempt, Duration) = run(repo.as_path(), Patience::Progress { stall: STALL });
assert_eq!(attempt.exit_code, 0, "{}", attempt.output);
assert!(elapsed >= STALL * SILENCE_WINDOWS, "the push waited out the server's silence: {elapsed:?}");
assert!(attempt.output.contains(WAIT_NOTE_PHRASE), "the output names the wait: {}", attempt.output);
assert_eq!(git_text(remote.as_path(), &["rev-parse", "main"]), git_text(repo.as_path(), &["rev-parse", "main"]), "the commit arrived");
drop(server);
remove(root.as_path());
}
#[test]
fn the_same_silence_behind_a_local_path_is_still_a_stall() {
let root: PathBuf = fixture("slow-server-local");
let remote: PathBuf = silent_remote(root.as_path(), STALL * SILENCE_WINDOWS);
let repo: PathBuf = repository(root.as_path(), "repo");
git(repo.as_path(), &["remote", "add", "origin", remote.to_str().expect("utf-8")]);
let (attempt, elapsed): (PushAttempt, Duration) = run(repo.as_path(), Patience::Progress { stall: STALL });
assert_eq!(attempt.exit_code, STOPPED_EXIT_CODE, "{}", attempt.output);
assert!(attempt.output.contains("limit=stall"), "{}", attempt.output);
assert!(!attempt.output.contains(WAIT_NOTE_PHRASE), "{}", attempt.output);
assert!(elapsed >= STALL, "{elapsed:?}");
assert!(elapsed < STALL * SILENCE_WINDOWS + GRACE + STOP_SLACK, "{elapsed:?}");
remove(root.as_path());
}
#[test]
fn a_server_that_holds_the_connection_without_answering_is_waited_for() {
let listener: TcpListener = TcpListener::bind("127.0.0.1:0").expect("a local listener");
let port: u16 = listener.local_addr().expect("its address").port();
let silence: Duration = STALL * SILENCE_WINDOWS;
let silent = std::thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
let mut sink: [u8; 4096] = [0; 4096];
let _ = stream.set_read_timeout(Some(silence));
let began: Instant = Instant::now();
while began.elapsed() < silence && stream.read(&mut sink).is_ok_and(|count| { return count > 0; }) {}
std::thread::sleep(silence.saturating_sub(began.elapsed()));
}
});
let root: PathBuf = fixture("slow-server-holds-connection");
let repo: PathBuf = repository(root.as_path(), "repo");
git(repo.as_path(), &["remote", "add", "origin", format!("http://127.0.0.1:{port}/silent.git").as_str()]);
let (attempt, elapsed): (PushAttempt, Duration) = run(repo.as_path(), Patience::Progress { stall: STALL });
assert_ne!(attempt.exit_code, STOPPED_EXIT_CODE, "the wrapper did not stop a connected push: {}", attempt.output);
assert!(!attempt.output.contains("limit=stall"), "{}", attempt.output);
assert!(elapsed >= silence - STALL, "the push stayed while the connection was open: {elapsed:?}");
remove(root.as_path());
drop(silent);
}
#[test]
fn a_push_over_http_is_not_stopped_for_time_while_the_server_checks() {
let root: PathBuf = fixture("slow-server-no-ceiling");
let projects: PathBuf = root.join("projects");
std::fs::create_dir(&projects).expect("projects");
let remote: PathBuf = silent_remote(projects.as_path(), STALL * SILENCE_WINDOWS);
let server: SmartHttp = SmartHttp::start(projects.as_path());
let repo: PathBuf = repository(root.as_path(), "repo");
git(repo.as_path(), &["remote", "add", "origin", format!("http://127.0.0.1:{}/remote.git", server.port()).as_str()]);
let (attempt, elapsed): (PushAttempt, Duration) = run(repo.as_path(), Patience::Progress { stall: STALL });
assert_eq!(attempt.exit_code, 0, "{}", attempt.output);
assert!(elapsed >= STALL * SILENCE_WINDOWS, "the push waited out the server's silence: {elapsed:?}");
assert_eq!(git_text(remote.as_path(), &["rev-parse", "main"]), git_text(repo.as_path(), &["rev-parse", "main"]), "the commit arrived");
drop(server);
remove(root.as_path());
}
const FORMER_LOW_SPEED_SECONDS: u64 = 60;
const FORMER_LOW_SPEED_BYTES: &str = "1000";
const PAST_THE_CUTOFF: Duration = Duration::from_secs(5);
const PERSON_LOW_SPEED_SECONDS: u64 = 2;
const PERSON_SILENCE_FACTOR: u32 = 6;
fn push_git(repo: &std::path::Path, person: &[(&str, &str)]) -> PushGit {
let mut environment: Vec<(OsString, OsString)> = [
("PATH", "/usr/bin:/bin"),
("GIT_CONFIG_NOSYSTEM", "1"),
("GIT_CONFIG_GLOBAL", "/nonexistent-global-config"),
("GIT_TERMINAL_PROMPT", "0"),
(PUSH_DEADLINE_VARIABLE, "1"),
]
.iter()
.chain(person.iter())
.map(|(name, value)| { return (OsString::from(name), OsString::from(value)); })
.collect();
environment.extend(scratch_environment());
return PushGit { real_git: PathBuf::from(REAL_GIT), cwd: repo.to_path_buf(), environment, lfs: false };
}
fn http_stage(name: &str, silence: Duration) -> (PathBuf, PathBuf, PathBuf, SmartHttp) {
let root: PathBuf = fixture(name);
let projects: PathBuf = root.join("projects");
std::fs::create_dir(&projects).expect("projects");
let remote: PathBuf = silent_remote(projects.as_path(), silence);
let server: SmartHttp = SmartHttp::start(projects.as_path());
let repo: PathBuf = repository(root.as_path(), "repo");
git(repo.as_path(), &["remote", "add", "origin", format!("http://127.0.0.1:{}/remote.git", server.port()).as_str()]);
return (root, repo, remote, server);
}
fn run_wrapper_push(git: &PushGit) -> (PushAttempt, Duration) {
let started: Instant = Instant::now();
let arguments: Vec<OsString> = ["push", "origin", "main"].iter().map(OsString::from).collect();
let attempt: PushAttempt = run_push(git, arguments.as_slice()).expect("runs");
return (attempt, started.elapsed());
}
#[test]
fn a_push_over_http_waits_past_the_old_low_speed_cutoff_and_lands() {
let cutoff: Duration = Duration::from_secs(FORMER_LOW_SPEED_SECONDS);
let (root, repo, remote, server) = http_stage("slow-server-past-cutoff", cutoff + PAST_THE_CUTOFF);
let (attempt, elapsed): (PushAttempt, Duration) = run_wrapper_push(&push_git(repo.as_path(), &[]));
assert_eq!(attempt.exit_code, 0, "{}", attempt.output);
assert!(elapsed >= cutoff, "the push waited past the cutoff: {elapsed:?}");
assert!(attempt.output.contains(WAIT_NOTE_PHRASE), "{}", attempt.output);
assert_eq!(git_text(remote.as_path(), &["rev-parse", "main"]), git_text(repo.as_path(), &["rev-parse", "main"]), "the commit arrived");
drop(server);
remove(root.as_path());
}
#[test]
fn a_low_speed_setting_the_person_made_still_ends_a_silent_http_wait() {
let person_time: Duration = Duration::from_secs(PERSON_LOW_SPEED_SECONDS);
let silence: Duration = person_time * PERSON_SILENCE_FACTOR;
let (root, repo, _remote, server) = http_stage("slow-server-person-cutoff", silence);
let seconds: String = PERSON_LOW_SPEED_SECONDS.to_string();
let git: PushGit = push_git(repo.as_path(), &[("GIT_HTTP_LOW_SPEED_LIMIT", FORMER_LOW_SPEED_BYTES), ("GIT_HTTP_LOW_SPEED_TIME", seconds.as_str())]);
let (attempt, elapsed): (PushAttempt, Duration) = run_wrapper_push(&git);
assert_ne!(attempt.exit_code, 0, "{}", attempt.output);
assert_ne!(attempt.exit_code, STOPPED_EXIT_CODE, "Git ended it, not the wrapper: {}", attempt.output);
assert!(attempt.output.to_lowercase().contains("too slow"), "{}", attempt.output);
assert!(elapsed < silence, "the person's cutoff ended the wait before the server's check did: {elapsed:?}");
drop(server);
remove(root.as_path());
}