use super::*;
use crate::test_support::{fixture, git_context, remove, repository};
use crate::transaction_git::{GitRequest, run_git};
use std::path::{Path, PathBuf};
const VALUES: [&str; 22] = [
"", "none", "all", "added", "committed", "objects", "loose-object", "pack", "-pack", "-all", "all,-pack", "pack-metadata",
"derived-metadata", "loose-object,none", "none,loose-object", "p", "reference,-reference", "bogus", " pack ,\tnone", "-",
"pack,-pack-metadata", "commit-graph,pack-metadata,none",
];
fn flushes(repo: &Path, trace: &Path, value: Option<&str>, arguments: &[&str], input: Option<&[u8]>) -> u64 {
let _gone = std::fs::remove_file(trace);
let setting: String = value.map(|text| { return format!("core.fsync={text}"); }).unwrap_or_default();
let mut full: Vec<&str> = Vec::new();
if value.is_some() {
full.extend(["-c", setting.as_str()]);
}
full.extend(arguments);
let mut request: GitRequest = GitRequest::new(repo, full.as_slice());
request.environment.push((std::ffi::OsString::from("GIT_TRACE2_EVENT"), trace.as_os_str().to_os_string()));
request.input = input.map(<[u8]>::to_vec);
let output = run_git(&git_context(), &request).expect("Git starts");
assert!(output.succeeded(), "{full:?}: {}", output.error_text());
let events: String = std::fs::read_to_string(trace).unwrap_or_default();
return events
.lines()
.filter(|line| { return line.contains("\"event\":\"counter\"") && line.contains("\"category\":\"fsync\""); })
.filter_map(|line| { return line.split("\"count\":").nth(1)?.trim_end_matches('}').parse::<u64>().ok(); })
.sum();
}
fn pack_flushes(repo: &Path, trace: &Path, value: Option<&str>, pack: &[u8]) -> u64 {
return flushes(repo, trace, value, &["index-pack", "--stdin", "--strict"], Some(pack));
}
fn history_pack(repo: &Path) -> Vec<u8> {
let mut request: GitRequest = GitRequest::new(repo, &["pack-objects", "--stdout", "--revs", "--quiet"]);
request.input = Some(b"HEAD\n".to_vec());
return run_git(&git_context(), &request).expect("pack-objects starts").stdout;
}
fn import_flushes(repo: &Path, trace: &Path, value: Option<&str>, name: &str) -> u64 {
let stream: String = format!("commit refs/probe/import-{name}\nmark :1\ncommitter T <t@example.invalid> 1793535889 +0000\ndata {length}\n{name}\nM 100644 inline f\ndata {length}\n{name}\n\ndone\n", length = name.len());
return flushes(repo, trace, value, &["-c", "fastimport.unpackLimit=0", "fast-import", "--quiet", "--done"], Some(stream.as_bytes()));
}
fn loose_flushes(repo: &Path, trace: &Path, value: Option<&str>, name: &str) -> u64 {
return flushes(repo, trace, value, &["hash-object", "-w", "--stdin"], Some(format!("loose object {name}").as_bytes()));
}
fn reference_flushes(repo: &Path, trace: &Path, value: Option<&str>, name: &str) -> u64 {
return flushes(repo, trace, value, &["update-ref", format!("refs/probe/{name}").as_str(), "HEAD"], None);
}
#[test]
fn a_value_names_the_components_git_flushes() {
let root: PathBuf = fixture("config-fsync-reading");
let repo: PathBuf = repository(root.as_path(), "repo");
let trace: PathBuf = root.join("trace.json");
let pack: Vec<u8> = history_pack(repo.as_path());
for (position, value) in VALUES.iter().enumerate() {
let set: FsyncComponents = FsyncComponents::parse(value);
let name: String = format!("v{position}");
assert_eq!(loose_flushes(repo.as_path(), trace.as_path(), Some(value), name.as_str()) > 0, set.names_loose_object(), "loose objects under {value:?}");
assert_eq!(reference_flushes(repo.as_path(), trace.as_path(), Some(value), name.as_str()) > 0, set.names_reference(), "references under {value:?}");
assert_eq!(pack_flushes(repo.as_path(), trace.as_path(), Some(value), pack.as_slice()) > 0, set.names_pack(), "packs under {value:?}");
if !set.names_reference() {
assert_eq!(import_flushes(repo.as_path(), trace.as_path(), Some(value), name.as_str()) > 0, set.names_pack(), "the import's pack under {value:?}");
}
}
remove(root.as_path());
}
#[test]
fn an_unset_key_is_the_default_set() {
let root: PathBuf = fixture("config-fsync-default");
let repo: PathBuf = repository(root.as_path(), "repo");
let trace: PathBuf = root.join("trace.json");
let pack: Vec<u8> = history_pack(repo.as_path());
let set: FsyncComponents = FsyncComponents::default_set();
assert_eq!(loose_flushes(repo.as_path(), trace.as_path(), None, "unset") > 0, set.names_loose_object());
assert_eq!(reference_flushes(repo.as_path(), trace.as_path(), None, "unset") > 0, set.names_reference());
assert_eq!(pack_flushes(repo.as_path(), trace.as_path(), None, pack.as_slice()) > 0, set.names_pack());
assert!(set.names_pack() && !set.names_loose_object(), "the default flushes packs and not loose objects");
remove(root.as_path());
}
#[test]
fn the_value_without_packs_keeps_the_other_components_and_flushes_no_pack() {
let root: PathBuf = fixture("config-fsync-without-packs");
let repo: PathBuf = repository(root.as_path(), "repo");
let trace: PathBuf = root.join("trace.json");
let pack: Vec<u8> = history_pack(repo.as_path());
for (position, value) in VALUES.iter().enumerate() {
let set: FsyncComponents = FsyncComponents::parse(value);
let spelled: String = set.without_packs().value();
let name: String = format!("w{position}");
assert_eq!(FsyncComponents::parse(spelled.as_str()).bits, set.without_packs().bits & !OBJECT_MAP, "{value:?} spelled as {spelled:?} reads back the same, but for the mapping between hash algorithms that no word names alone");
assert_eq!(pack_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), pack.as_slice()), 0, "no pack flush under {spelled:?} (from {value:?})");
if !set.names_reference() {
assert_eq!(import_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), name.as_str()), 0, "the import flushes nothing under {spelled:?} (from {value:?})");
}
assert_eq!(
loose_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), name.as_str()) > 0,
set.names_loose_object(),
"loose objects under {spelled:?} (from {value:?})"
);
assert_eq!(
reference_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), name.as_str()) > 0,
set.names_reference(),
"references under {spelled:?} (from {value:?})"
);
}
remove(root.as_path());
}
#[test]
fn only_index_added_and_all_name_the_index() {
for value in VALUES {
let named: bool = ["index", "added", "all"].iter().any(|word| { return value.contains(word); });
let set: FsyncComponents = FsyncComponents::parse(value);
if !named {
assert!(!set.names_index(), "{value:?}");
}
}
assert!(FsyncComponents::parse("added,-index").names_index(), "an added component stays when it is also removed, as in Git");
assert!(!FsyncComponents::parse("-index").names_index());
}
#[test]
fn the_value_with_packs_flushes_the_pack_and_its_index_and_keeps_the_other_components() {
let root: PathBuf = fixture("config-fsync-with-packs");
let repo: PathBuf = repository(root.as_path(), "repo");
let trace: PathBuf = root.join("trace.json");
let pack: Vec<u8> = history_pack(repo.as_path());
for (position, value) in VALUES.iter().enumerate() {
let set: FsyncComponents = FsyncComponents::parse(value);
let spelled: String = set.with_packs().value();
let name: String = format!("p{position}");
assert_eq!(FsyncComponents::parse(spelled.as_str()).bits, set.with_packs().bits & !OBJECT_MAP, "{value:?} spelled as {spelled:?}");
assert!(set.with_packs().names_pack_and_metadata());
assert_eq!(
pack_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), pack.as_slice()),
pack_flushes(repo.as_path(), trace.as_path(), Some("pack,pack-metadata,none"), pack.as_slice()),
"the pack and its index under {spelled:?} (from {value:?})"
);
assert_eq!(
loose_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), name.as_str()) > 0,
set.names_loose_object(),
"loose objects under {spelled:?} (from {value:?})"
);
assert_eq!(
reference_flushes(repo.as_path(), trace.as_path(), Some(spelled.as_str()), name.as_str()) > 0,
set.names_reference(),
"references under {spelled:?} (from {value:?})"
);
}
remove(root.as_path());
}