use super::*;
use crate::test_support::{fixture, git, git_output, remove};
const FILES: usize = 1500;
const SHORT: usize = 4;
#[test]
fn the_setting_follows_what_git_reads_from_core_abbrev() {
assert_eq!(setting_of(None), Setting::Auto);
for auto in ["auto", "AUTO", " Auto "] {
assert_eq!(setting_of(Some(auto.as_bytes())), Setting::Auto, "{auto}");
}
assert_eq!(setting_of(Some(b"12")), Setting::Length(12));
assert_eq!(setting_of(Some(MINIMUM_ABBREVIATION.to_string().as_bytes())), Setting::Length(usize::try_from(MINIMUM_ABBREVIATION).expect("small")));
for whole in ["", "no", "false", "off", "OFF"] {
assert_eq!(setting_of(Some(whole.as_bytes())), Setting::Length(LONGEST_NAME), "{whole:?}");
}
for unusable in ["3", "-1", "twelve", "1.5"] {
assert_eq!(setting_of(Some(unusable.as_bytes())), Setting::Auto, "{unusable}");
}
}
#[test]
fn the_automatic_length_grows_with_the_object_count() {
assert_eq!(automatic_length(0), FALLBACK_ABBREVIATION);
assert_eq!(automatic_length(1), FALLBACK_ABBREVIATION);
assert_eq!(automatic_length(379_598), 10);
let bit_boundary: u64 = 1 << 19;
assert_eq!(automatic_length(bit_boundary - 1), 10);
assert_eq!(automatic_length(bit_boundary), 10);
assert_eq!(automatic_length(bit_boundary << 1), 11);
let mut previous: usize = 0;
for bits in 0..40 {
let length: usize = automatic_length(1_u64 << bits);
assert!(length >= previous, "the length never shrinks as the count grows");
previous = length;
}
}
fn crowded(root: &Path, name: &str) -> PathBuf {
let repo: PathBuf = root.join(name);
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
std::fs::create_dir(repo.join("many")).expect("many");
for number in 0..FILES {
std::fs::write(repo.join(format!("many/file-{number}.txt")), format!("content of file {number}\n")).expect("file");
}
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=many"]);
return repo;
}
fn names_of(repo: &Path, argument: Option<&str>) -> Vec<String> {
let mut arguments: Vec<&str> = vec!["ls-tree", "-r"];
arguments.extend(argument);
arguments.push("HEAD");
let listing = git_output(repo, arguments.as_slice());
assert!(listing.status.success(), "{}", String::from_utf8_lossy(&listing.stderr));
return String::from_utf8(listing.stdout)
.expect("UTF-8")
.lines()
.map(|line| { return line.split_once('\t').expect("a tab").0.split(' ').nth(2).expect("a name").to_string(); })
.collect();
}
fn agrees_with_git(repo: &Path, objects: &Path, label: &str) -> usize {
let whole: Vec<String> = names_of(repo, None);
let asked: Vec<String> = names_of(repo, Some(format!("--abbrev={SHORT}").as_str()));
let automatic: Vec<String> = names_of(repo, Some("--abbrev"));
assert!(whole.len() >= FILES, "{label}");
let mut lengthened: usize = 0;
for (index, name) in whole.iter().enumerate() {
let short: String = abbreviate(objects, Setting::Length(SHORT), name).unwrap_or_else(|| panic!("{label}: {name} was not abbreviated"));
assert_eq!(short, asked[index], "{label}: asked length for {name}");
if short.len() > SHORT {
lengthened += 1;
}
let by_count: String = abbreviate(objects, Setting::Auto, name).unwrap_or_else(|| panic!("{label}: {name} was not abbreviated"));
assert_eq!(by_count, automatic[index], "{label}: automatic length for {name}");
}
return lengthened;
}
#[test]
fn loose_objects_agree_with_git() {
let root: PathBuf = fixture("abbrev-loose");
let repo: PathBuf = crowded(root.as_path(), "repo");
let lengthened: usize = agrees_with_git(repo.as_path(), repo.join(".git/objects").as_path(), "loose");
assert!(lengthened > 0, "the fixture must make four characters ambiguous, or the control proves nothing");
remove(root.as_path());
}
#[test]
fn packed_objects_agree_with_git() {
let root: PathBuf = fixture("abbrev-packed");
let repo: PathBuf = crowded(root.as_path(), "repo");
git(repo.as_path(), &["repack", "--quiet", "-a", "-d"]);
let lengthened: usize = agrees_with_git(repo.as_path(), repo.join(".git/objects").as_path(), "packed");
assert!(lengthened > 0);
remove(root.as_path());
}
fn index_names_equal_git(repo: &Path, version: u8) {
let pack: PathBuf = repo.join(".git/objects/pack");
let indexes: Vec<PathBuf> = std::fs::read_dir(&pack)
.expect("pack directory")
.map(|entry| { return entry.expect("entry").path(); })
.filter(|path| { return path.extension().is_some_and(|extension| { return extension == "idx"; }); })
.collect();
assert_eq!(indexes.len(), 1, "one pack after the repack");
let magic: Vec<u8> = std::fs::read(&indexes[0]).expect("index").into_iter().take(8).collect();
assert_eq!(magic.as_slice() == b"\xfftOc\0\0\0\x02", version == 2, "the control needs a version {version} index");
let listing = git_output(repo, &["cat-file", "--batch-all-objects", "--batch-check=%(objectname)"]);
assert!(listing.status.success(), "{}", String::from_utf8_lossy(&listing.stderr));
let listed: Vec<String> = String::from_utf8(listing.stdout).expect("UTF-8").lines().map(str::to_string).collect();
let names: SortedNames = open_index(&indexes[0], 20).expect("a plain index");
assert_eq!(names.count, listed.len() as u64, "version {version}: the count");
for (position, expected) in listed.iter().enumerate() {
let read: Vec<u8> = names.name(position as u64).expect("a name");
let hex: String = read.iter().map(|byte| { return format!("{byte:02x}"); }).collect();
assert_eq!(&hex, expected, "version {version}: name {position}");
}
}
#[test]
fn every_name_of_a_version_2_pack_index_is_read_where_git_has_it() {
let root: PathBuf = fixture("abbrev-index-v2");
let repo: PathBuf = crowded(root.as_path(), "repo");
git(repo.as_path(), &["-c", "pack.indexVersion=2", "repack", "--quiet", "-a", "-d"]);
index_names_equal_git(repo.as_path(), 2);
remove(root.as_path());
}
#[test]
fn every_name_of_a_version_1_pack_index_is_read_where_git_has_it() {
let root: PathBuf = fixture("abbrev-index-v1");
let repo: PathBuf = crowded(root.as_path(), "repo");
git(repo.as_path(), &["-c", "pack.indexVersion=1", "repack", "--quiet", "-a", "-d"]);
index_names_equal_git(repo.as_path(), 1);
remove(root.as_path());
}
#[test]
fn a_multi_pack_index_and_loose_objects_agree_with_git() {
let root: PathBuf = fixture("abbrev-midx");
let repo: PathBuf = crowded(root.as_path(), "repo");
git(repo.as_path(), &["repack", "--quiet", "-a", "-d"]);
std::fs::write(repo.join("second.txt"), "second pack\n").expect("second");
git(repo.as_path(), &["add", "second.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=second"]);
git(repo.as_path(), &["repack", "--quiet", "-d"]);
git(repo.as_path(), &["multi-pack-index", "write"]);
assert!(repo.join(".git/objects/pack/multi-pack-index").exists(), "the control needs the index");
std::fs::write(repo.join("third.txt"), "loose after the index\n").expect("third");
git(repo.as_path(), &["add", "third.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=third"]);
let lengthened: usize = agrees_with_git(repo.as_path(), repo.join(".git/objects").as_path(), "midx");
assert!(lengthened > 0);
remove(root.as_path());
}
#[test]
fn objects_of_an_alternate_agree_with_git() {
let root: PathBuf = fixture("abbrev-alternate");
let origin: PathBuf = crowded(root.as_path(), "origin");
git(origin.as_path(), &["repack", "--quiet", "-a", "-d"]);
let clone: PathBuf = root.join("clone");
git(root.as_path(), &["clone", "--quiet", "--shared", origin.to_str().expect("UTF-8"), clone.to_str().expect("UTF-8")]);
assert!(clone.join(".git/objects/info/alternates").exists(), "the control needs the alternate");
let lengthened: usize = agrees_with_git(clone.as_path(), clone.join(".git/objects").as_path(), "alternate");
assert!(lengthened > 0);
remove(root.as_path());
}
#[test]
fn an_incremental_multi_pack_index_chain_is_declined() {
let root: PathBuf = fixture("abbrev-chain");
let repo: PathBuf = crowded(root.as_path(), "repo");
git(repo.as_path(), &["repack", "--quiet", "-a", "-d"]);
std::fs::create_dir(repo.join(".git/objects/pack/multi-pack-index.d")).expect("chain directory");
let name: String = names_of(repo.as_path(), None).remove(0);
assert_eq!(abbreviate(repo.join(".git/objects").as_path(), Setting::Auto, name.as_str()), None);
remove(root.as_path());
}
#[test]
fn names_and_directories_that_cannot_be_read_are_not_answered() {
let root: PathBuf = fixture("abbrev-unreadable");
let repo: PathBuf = crowded(root.as_path(), "repo");
let objects: PathBuf = repo.join(".git/objects");
assert_eq!(abbreviate(objects.as_path(), Setting::Auto, "not a name"), None);
assert_eq!(abbreviate(objects.as_path(), Setting::Auto, "ABCDEF0123456789ABCDEF0123456789ABCDEF01"), None);
assert_eq!(abbreviate(root.join("missing").as_path(), Setting::Auto, "0123456789abcdef0123456789abcdef01234567"), None);
remove(root.as_path());
}
#[test]
fn a_length_of_the_whole_name_returns_it() {
let name: &str = "0123456789abcdef0123456789abcdef01234567";
let nowhere: PathBuf = PathBuf::from("/nonexistent-objects");
assert_eq!(abbreviate(nowhere.as_path(), Setting::Length(LONGEST_NAME), name), None, "an unreadable directory is declined before the setting is looked at");
let root: PathBuf = fixture("abbrev-whole");
let repo: PathBuf = crowded(root.as_path(), "repo");
assert_eq!(abbreviate(repo.join(".git/objects").as_path(), Setting::Length(LONGEST_NAME), name).as_deref(), Some(name));
remove(root.as_path());
}