use super::budget_trace::{TRACED_CALLS, count};
use super::commit::{assert_nothing_left, keep_maintenance_in_the_foreground, remove_settled, wrapped_commit};
use super::fork_failure::{strace_available, strace_program};
use super::support::{Fixture, REAL_GIT, fixture, git, repository};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const COMMIT: [&str; 5] = ["commit", "--quiet", "--message=x", "--", "a.txt"];
fn repository_asking_for_every_flush(fixture: &Fixture, name: &str) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new(name));
std::fs::write(repo.join("a.txt"), b"one\n").expect("a");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
keep_maintenance_in_the_foreground(fixture, repo.as_path());
git(fixture, repo.as_path(), &["config", "core.fsync", "all"]);
return repo;
}
fn require_landed(label: &str, fixture: &Fixture, repo: &Path, run: &Output) {
assert_eq!(run.status.code(), Some(0), "{label}: the commit failed: {}", String::from_utf8_lossy(&run.stderr));
let head_count = git(fixture, repo, &["rev-list", "--count", "HEAD"]);
assert_eq!(String::from_utf8_lossy(&head_count.stdout).trim(), "2", "{label}: commits");
assert_nothing_left(fixture, repo);
}
fn fsync_settings_by_process(trace: &str) -> Vec<(String, Vec<String>)> {
let mut names: BTreeMap<String, String> = BTreeMap::new();
let mut settings: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut order: Vec<String> = Vec::new();
for line in trace.lines() {
let Ok(event) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
let Some(process) = event["sid"].as_str() else {
continue;
};
match event["event"].as_str() {
Some("cmd_name") => {
names.insert(process.to_string(), event["name"].as_str().unwrap_or("").to_string());
order.push(process.to_string());
}
Some("def_param") if event["param"] == "core.fsync" => {
let scope: &str = event["scope"].as_str().unwrap_or("");
let value: &str = event["value"].as_str().unwrap_or("");
settings.entry(process.to_string()).or_default().push(format!("{scope}:{value}"));
}
_ => {}
}
}
return order
.into_iter()
.map(|process| {
let seen: Vec<String> = settings.remove(&process).unwrap_or_default();
return (names.remove(&process).unwrap_or_default(), seen);
})
.collect();
}
#[test]
fn only_the_migration_index_pack_gets_the_fsync_override() {
let fixture: Fixture = fixture("index-pack-fsync-config");
let repo: PathBuf = repository_asking_for_every_flush(&fixture, "config");
let trace: PathBuf = fixture.root.join("trace2.jsonl");
let mut command: Command = wrapped_commit(&fixture, repo.as_path(), &COMMIT);
command.env("GIT_TRACE2_EVENT", &trace).env("GIT_TRACE2_CONFIG_PARAMS", "core.fsync");
let run: Output = command.output().expect("the commit starts");
require_landed("config", &fixture, repo.as_path(), &run);
let text: String = std::fs::read_to_string(&trace).expect("the trace2 log");
let processes: Vec<(String, Vec<String>)> = fsync_settings_by_process(text.as_str());
let index_packs: Vec<&Vec<String>> =
processes.iter().filter(|(name, _)| { return name == "index-pack"; }).map(|(_, seen)| { return seen; }).collect();
assert_eq!(index_packs.len(), 1, "one migration index-pack starts in a commit: {processes:?}");
assert_eq!(
index_packs[0].as_slice(),
[String::from("local:all"), String::from("command:-pack,-pack-metadata")],
"the user's setting reaches the child and the override is applied after it, so the override is effective"
);
for (name, seen) in processes.iter().filter(|(name, _)| { return name != "index-pack"; }) {
assert!(
seen.iter().all(|setting| { return !setting.starts_with("command:"); }),
"git {name} must not get a core.fsync from the command line: {seen:?}"
);
}
remove_settled(&fixture);
}
#[test]
fn the_fsync_override_beats_a_users_core_fsync() {
if !strace_available() {
eprintln!("skipped: strace is not installed, so no sync call was counted");
return;
}
let fixture: Fixture = fixture("index-pack-fsync-count");
let repo: PathBuf = repository_asking_for_every_flush(&fixture, "count");
let trace: PathBuf = fixture.root.join("trace.txt");
let inner: Command = wrapped_commit(&fixture, repo.as_path(), &COMMIT);
let mut outer: Command = Command::new(strace_program().expect("strace is installed"));
outer
.args(["--follow-forks", "--quiet", "--quiet", "--string-limit=4096", "--no-abbrev"])
.arg(format!("--output={}", trace.display()))
.arg(format!("--trace={TRACED_CALLS}"))
.arg(inner.get_program())
.args(inner.get_args())
.env_clear();
for (name, setting) in inner.get_envs() {
if let Some(text) = setting {
outer.env(name, text);
}
}
outer.current_dir(repo.as_path());
let run: Output = outer.output().expect("strace starts");
require_landed("count", &fixture, repo.as_path(), &run);
let text: String = std::fs::read_to_string(&trace).expect("trace");
let counts = count(text.as_str(), fixture.root.to_str().expect("UTF-8 root"), REAL_GIT);
assert_eq!(counts.git_starts.get("index-pack"), Some(&1), "one migration index-pack starts: {:?}", counts.commands);
assert_eq!(
counts.git_syncs.get("index-pack").copied().unwrap_or(0),
0,
"the migration index-pack flushed although the override removes the pack components: {:?}",
counts.commands
);
remove_settled(&fixture);
}