use super::budget_trace::{Counts, TRACED_CALLS, count};
use super::commit::{assert_nothing_left, git_text, 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};
struct Budget {
git_starts: &'static [(&'static str, usize)],
other_starts: &'static [(&'static str, usize)],
wrapper_syncs: usize,
git_syncs: &'static [(&'static str, usize)],
}
const NO_HOOKS: Budget = Budget {
git_starts: &[
("add", 1),
("cat-file", 3),
("commit", 1),
("config", 1),
("diff", 1),
("diff-index", 4),
("diff-tree", 1),
("for-each-ref", 1),
("hash-object", 0),
("hook run", 0),
("index-pack", 1),
("init", 1),
("ls-files", 4),
("maintenance run", 1),
("pack-objects", 1),
("read-tree", 1),
("remote", 1),
("reset", 1),
("rev-parse", 8),
("show", 1),
("symbolic-ref", 1),
("update-index", 1),
("update-ref", 2),
("version", 1),
("write-tree", 1),
],
other_starts: &[("findmnt", 0), ("sh", 0), ("stat", 0)],
wrapper_syncs: 19,
git_syncs: &[("index-pack", 0)],
};
const ONE_CONFIG_HOOK: Budget = Budget {
git_starts: &[
("add", 1),
("cat-file", 3),
("commit", 1),
("config", 1),
("diff", 1),
("diff-index", 4),
("diff-tree", 1),
("for-each-ref", 1),
("hash-object", 0),
("hook run", 4),
("index-pack", 1),
("init", 1),
("ls-files", 4),
("maintenance run", 1),
("pack-objects", 1),
("read-tree", 1),
("remote", 1),
("reset", 1),
("rev-parse", 8),
("show", 2),
("symbolic-ref", 1),
("update-index", 1),
("update-ref", 2),
("version", 1),
("write-tree", 1),
],
other_starts: &[("findmnt", 0), ("sh", 1), ("stat", 0)],
wrapper_syncs: 23,
git_syncs: &[("index-pack", 0)],
};
const AMEND: Budget = Budget {
git_starts: &[
("add", 1),
("cat-file", 3),
("commit", 1),
("config", 1),
("diff", 1),
("diff-index", 4),
("diff-tree", 1),
("for-each-ref", 1),
("hash-object", 0),
("hook run", 0),
("index-pack", 1),
("init", 1),
("ls-files", 4),
("maintenance run", 1),
("pack-objects", 1),
("read-tree", 1),
("remote", 1),
("reset", 1),
("rev-parse", 8),
("show", 1),
("symbolic-ref", 1),
("update-index", 0),
("update-ref", 2),
("version", 1),
("write-tree", 1),
],
other_starts: &[("findmnt", 0), ("sh", 0), ("stat", 0)],
wrapper_syncs: 19,
git_syncs: &[("index-pack", 0)],
};
const WARM: Budget = Budget {
git_starts: &[
("add", 1),
("cat-file", 3),
("commit", 1),
("config", 1),
("diff", 1),
("diff-index", 4),
("diff-tree", 1),
("for-each-ref", 1),
("hash-object", 0),
("hook run", 0),
("index-pack", 1),
("init", 1),
("ls-files", 4),
("maintenance run", 1),
("multi-pack-index write", 1),
("pack-objects", 3),
("read-tree", 1),
("remote", 1),
("repack", 1),
("reset", 1),
("rev-parse", 8),
("show", 1),
("symbolic-ref", 1),
("update-index", 0),
("update-ref", 2),
("version", 0),
("write-tree", 1),
],
other_starts: &[("findmnt", 0), ("sh", 0), ("stat", 0)],
wrapper_syncs: 14,
git_syncs: &[("index-pack", 0), ("multi-pack-index write", 1), ("pack-objects", 3)],
};
#[derive(Clone, Copy)]
enum Scenario {
NoHooks,
OneConfigHook,
Warm,
Amend,
}
fn prepared_repository(fixture: &Fixture, scenario: Scenario) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new("budget"));
keep_maintenance_in_the_foreground(fixture, repo.as_path());
std::fs::write(repo.join("a.txt"), b"one\n").expect("a");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
match scenario {
Scenario::NoHooks => {}
Scenario::OneConfigHook => {
git(fixture, repo.as_path(), &["config", "hook.early.event", "pre-commit"]);
let log: String = format!("echo ran >> '{}'", fixture.root.join("hook.log").display());
git(fixture, repo.as_path(), &["config", "hook.early.command", log.as_str()]);
}
Scenario::Warm => {
let first: Output = wrapped_commit(fixture, repo.as_path(), &["commit", "--quiet", "--message=first", "--", "a.txt"])
.output()
.expect("the first wrapped commit runs");
assert_eq!(first.status.code(), Some(0), "the first wrapped commit: {}", String::from_utf8_lossy(&first.stderr));
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit a");
}
Scenario::Amend => {
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base", "--", "a.txt"]);
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit a");
}
}
return repo;
}
fn traced(inner: &Command, trace: &Path) -> Command {
let mut outer: Command = Command::new(strace_program().expect("strace is installed"));
outer
.arg("--follow-forks")
.arg("--quiet")
.arg("--quiet")
.arg("--string-limit=4096")
.arg("--no-abbrev")
.arg(format!("--output={}", trace.display()))
.arg(format!("--trace={TRACED_CALLS}"))
.arg(inner.get_program())
.args(inner.get_args());
outer.env_clear();
for (name, setting) in inner.get_envs() {
if let Some(text) = setting {
outer.env(name, text);
}
}
if let Some(directory) = inner.get_current_dir() {
outer.current_dir(directory);
}
return outer;
}
type Figures = BTreeMap<String, usize>;
fn observed_figures(counts: &Counts) -> Figures {
let mut figures: Figures = Figures::new();
for (name, seen) in &counts.git_starts {
figures.insert(format!("starts of git {name}"), *seen);
}
for (name, seen) in &counts.other_starts {
figures.insert(format!("starts of {name}"), *seen);
}
figures.insert(String::from("sync calls by the wrapper"), counts.wrapper_syncs);
for (name, seen) in &counts.git_syncs {
figures.insert(format!("sync calls by git {name}"), *seen);
}
for (name, seen) in &counts.other_syncs {
figures.insert(format!("sync calls by {name}"), *seen);
}
return figures;
}
fn budget_figures(budget: &Budget, other_starts: &[(&str, usize)]) -> Figures {
let mut figures: Figures = Figures::new();
for (name, allowed) in budget.git_starts {
figures.insert(format!("starts of git {name}"), *allowed);
}
for (name, allowed) in other_starts {
figures.insert(format!("starts of {name}"), *allowed);
}
figures.insert(String::from("sync calls by the wrapper"), budget.wrapper_syncs);
for (name, allowed) in budget.git_syncs {
figures.insert(format!("sync calls by git {name}"), *allowed);
}
return figures;
}
fn differences(budgeted: &Figures, observed: &Figures) -> Vec<String> {
let mut keys: Vec<&String> = budgeted.keys().chain(observed.keys()).collect();
keys.sort();
keys.dedup();
let mut lines: Vec<String> = Vec::new();
for key in keys {
let allowed: usize = budgeted.get(key).copied().unwrap_or(0);
let seen: usize = observed.get(key).copied().unwrap_or(0);
if allowed != seen {
let unlisted: &str = if budgeted.contains_key(key) { "" } else { " (not in the budget)" };
lines.push(format!(" {key}: observed {seen}, budget {allowed}{unlisted}"));
}
}
return lines;
}
fn require_budget(label: &str, scenario: Scenario, budget: &Budget) {
if !strace_available() {
eprintln!("skipped: strace is not installed, so no process start or sync call was counted");
return;
}
let fixture: Fixture = fixture(format!("budget-{label}").as_str());
let repo: PathBuf = prepared_repository(&fixture, scenario);
let trace: PathBuf = fixture.root.join("trace.txt");
let arguments: &[&str] = match scenario {
Scenario::Amend => &["commit", "--amend", "--quiet", "--message=x", "--", "a.txt"],
Scenario::NoHooks | Scenario::OneConfigHook | Scenario::Warm => &["commit", "--quiet", "--message=x", "--", "a.txt"],
};
let inner: Command = wrapped_commit(&fixture, repo.as_path(), arguments);
let run: Output = traced(&inner, trace.as_path()).output().expect("strace starts");
let commits: &str = if matches!(scenario, Scenario::Warm) { "3" } else { "2" };
assert_eq!(run.status.code(), Some(0), "{label}: the commit failed: {}", String::from_utf8_lossy(&run.stderr));
assert_eq!(git_text(&fixture, repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), commits, "{label}: commits");
if matches!(scenario, Scenario::OneConfigHook) {
let ran: String = std::fs::read_to_string(fixture.root.join("hook.log")).unwrap_or_default();
assert_eq!(ran, "ran\n", "{label}: the configured hook must have run once");
}
assert_nothing_left(&fixture, repo.as_path());
let text: String = std::fs::read_to_string(&trace).expect("trace");
let counts: Counts = count(text.as_str(), fixture.root.to_str().expect("UTF-8 root"), REAL_GIT);
let version: String = String::from_utf8_lossy(&Command::new(REAL_GIT).arg("--version").output().expect("git --version").stdout)
.trim()
.to_string();
let differing: Vec<String> =
differences(&budget_figures(budget, budget.other_starts), &observed_figures(&counts));
assert!(
differing.is_empty(),
"{label} ({version}): the counts differ from the budget in binary_budget_tests.rs. A saving lowers the constant; \
a rise needs a recorded reason.\n{}\nstarted Git commands:\n{}\nstderr: {}",
differing.join("\n"),
counts.commands.join("\n"),
String::from_utf8_lossy(&run.stderr),
);
remove_settled(&fixture);
}
#[test]
fn a_commit_with_no_hooks_stays_within_its_budget() {
require_budget("no-hooks", Scenario::NoHooks, &NO_HOOKS);
}
#[test]
fn a_commit_with_one_config_hook_stays_within_its_budget() {
require_budget("one-config-hook", Scenario::OneConfigHook, &ONE_CONFIG_HOOK);
}
#[test]
fn a_warm_commit_stays_within_its_budget() {
require_budget("warm", Scenario::Warm, &WARM);
}
#[test]
fn an_amending_commit_stays_within_its_budget() {
require_budget("amend", Scenario::Amend, &AMEND);
}