#![cfg(unix)]
use super::{ObjectReader, start_object_reader};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{ObjectId, parse_object_id};
use crate::test_support::{executable, fixture, remove};
use std::path::{Path, PathBuf};
fn stand_in(root: &Path, name: &str, reply: &str) -> PathBuf {
let path: PathBuf = root.join(name);
executable(
path.as_path(),
format!("#!/bin/sh\nread -r request\nprintf '{reply}'\nexit 0\n").as_bytes(),
);
return path;
}
fn wait_until_exited(process: u32) {
for _attempt in 0..500 {
let stat: String =
std::fs::read_to_string(format!("/proc/{process}/stat")).expect("process entry");
if stat.contains(") Z ") {
return;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
panic!("the stand-in did not exit within the bound");
}
#[test]
fn head_that_is_not_a_commit_is_refused() {
let root: PathBuf = fixture("reader-head-kind");
let name: String = "a".repeat(40);
let program: PathBuf = stand_in(
root.as_path(),
"git",
format!("{name} tree 0\\n\\n").as_str(),
);
let mut subject: ObjectReader =
start_object_reader(program.as_path(), &[], &[]).expect("stand-in starts");
let error: CandidateError = subject.head_commit().expect_err("tree as HEAD");
assert_eq!(error.failure, CandidateFailure::ObjectKindUnexpected);
assert!(
error.message.contains("HEAD does not name a commit"),
"{error}"
);
drop(subject);
remove(root.as_path());
}
#[test]
fn reply_cut_short_by_a_dying_process_closes_the_reader() {
let root: PathBuf = fixture("reader-dies");
let name: String = "a".repeat(40);
let object: ObjectId = parse_object_id(name.as_bytes()).expect("name");
let program: PathBuf = stand_in(
root.as_path(),
"git",
format!("{name} blob 100\\nonly part").as_str(),
);
let mut subject: ObjectReader =
start_object_reader(program.as_path(), &[], &[]).expect("stand-in starts");
let error: CandidateError = subject.blob(&object).expect_err("truncated reply");
assert_eq!(error.failure, CandidateFailure::ReplyTruncated);
assert!(subject.pipes.is_none());
assert!(subject.blobs.is_empty());
assert_eq!(
subject.blob(&object).expect_err("closed reader").failure,
CandidateFailure::ReaderEnded
);
assert_eq!(
subject.head_commit().expect_err("closed reader").failure,
CandidateFailure::ReaderEnded
);
drop(subject);
remove(root.as_path());
}
#[test]
fn reply_for_another_object_closes_the_reader() {
let root: PathBuf = fixture("reader-mismatch");
let requested: ObjectId = parse_object_id(&[b'a'; 40]).expect("name");
let other: String = "b".repeat(40);
let program: PathBuf = stand_in(
root.as_path(),
"git",
format!("{other} blob 5\\nwrong\\n").as_str(),
);
let mut subject: ObjectReader =
start_object_reader(program.as_path(), &[], &[]).expect("stand-in starts");
assert_eq!(
subject
.blob(&requested)
.expect_err("mismatched reply")
.failure,
CandidateFailure::ReplyMismatched
);
assert!(subject.pipes.is_none());
assert!(subject.blobs.is_empty());
drop(subject);
remove(root.as_path());
}
#[test]
fn process_that_answers_nothing_is_a_reader_failure() {
let root: PathBuf = fixture("reader-silent");
let program: PathBuf = stand_in(root.as_path(), "git", "");
let mut subject: ObjectReader =
start_object_reader(program.as_path(), &[], &[]).expect("stand-in starts");
let error: CandidateError = subject
.blob(&parse_object_id(&[b'a'; 40]).expect("name"))
.expect_err("no reply");
assert_eq!(error.failure, CandidateFailure::ReaderEnded);
drop(subject);
remove(root.as_path());
}
#[test]
fn request_to_an_exited_process_is_a_reader_failure() {
let root: PathBuf = fixture("reader-exited");
let program: PathBuf = root.join("git");
executable(program.as_path(), b"#!/bin/sh\nexit 0\n");
let mut subject: ObjectReader =
start_object_reader(program.as_path(), &[], &[]).expect("stand-in starts");
wait_until_exited(subject.child.id());
let error: CandidateError = subject
.blob(&parse_object_id(&[b'a'; 40]).expect("name"))
.expect_err("exited process");
assert_eq!(error.failure, CandidateFailure::ReaderEnded);
assert!(
error.message.contains("stopped accepting requests")
|| error.message.contains("ended before answering a request"),
"{error}"
);
assert!(subject.pipes.is_none());
drop(subject);
remove(root.as_path());
}
#[test]
fn unstartable_git_is_reported() {
let error: CandidateError =
match start_object_reader(Path::new("/nonexistent-directory/git"), &[], &[]) {
Ok(_) => panic!("a missing program must not start"),
Err(failure) => failure,
};
assert_eq!(error.failure, CandidateFailure::GitNotStarted);
assert!(
error
.message
.starts_with("cli-git could not start git cat-file --batch to read Git objects: "),
"{error}"
);
}