use super::support::{
Fixture, Observed, TIMED_OUT_SIGNAL, bounded, bounded_for, copy_executable, direct, executable,
fixture, link_real_git, observe, remove,
};
use std::ffi::OsString;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::Command;
fn path_of(fixture: &Fixture, entries: &[&str]) -> OsString {
let mut path: OsString = OsString::new();
for entry in entries {
if !path.is_empty() {
path.push(":");
}
if entry.starts_with('/') {
path.push(entry);
} else {
path.push(fixture.root.join(entry));
}
}
return path;
}
fn plant_wrapper_forms(fixture: &Fixture) {
for directory in ["hard", "copy-early", "copy-late", "chain", "nested"] {
std::fs::create_dir(fixture.root.join(directory)).expect("directory");
}
std::fs::hard_link(&fixture.wrapper, fixture.root.join("hard/git")).expect("hard link");
copy_executable(
fixture.wrapper.as_path(),
fixture.root.join("copy-early/git").as_path(),
);
copy_executable(
fixture.wrapper.as_path(),
fixture.root.join("copy-late/git").as_path(),
);
std::os::unix::fs::symlink(fixture.root.join("bin/git"), fixture.root.join("chain/git"))
.expect("chained link");
std::os::unix::fs::symlink(
fixture.root.join("bin"),
fixture.root.join("nested/bin-link"),
)
.expect("directory link");
}
#[test]
fn time_bound_detects_a_forwarding_loop() {
let fixture: Fixture = fixture("loop-control");
std::fs::create_dir(fixture.root.join("loop")).expect("directory");
let looping: PathBuf = fixture.root.join("loop/git");
executable(looping.as_path(), b"#!/bin/sh\nexec \"$0\" \"$@\"\n");
let status = bounded_for(
&fixture,
looping.as_path(),
path_of(&fixture, &["loop"]).as_os_str(),
"1",
)
.arg("--version")
.status()
.expect("bounded loop");
assert_eq!(
status.signal(),
Some(TIMED_OUT_SIGNAL),
"a forwarding loop must be killed at the bound"
);
remove(&fixture);
}
#[test]
fn wrapper_never_selects_itself_or_a_copy_of_itself() {
let fixture: Fixture = fixture("self-exclusion");
plant_wrapper_forms(&fixture);
link_real_git(&fixture);
let expected: Observed = observe(
direct(&fixture).args(["rev-parse", "--sq-quote", "reached real git"]),
b"",
);
assert_eq!(expected.code, Some(0));
assert!(expected.stdout.ends_with(b"reached real git'\n"));
let path: OsString = path_of(
&fixture,
&[
"bin",
"bin",
"chain",
"hard",
"copy-early",
"nested/bin-link",
"missing",
"real",
"copy-late",
"bin",
],
);
for started in [
"bin/git",
"chain/git",
"hard/git",
"copy-early/git",
"copy-late/git",
"nested/bin-link/git",
"wrapper/cli-git-native",
] {
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join(started).as_path(),
path.as_os_str(),
)
.args(["rev-parse", "--sq-quote", "reached real git"]),
b"",
);
assert_eq!(observed, expected, "{started}");
}
let relative: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
OsString::from("bin:copy-early::real").as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(relative.stdout, b"git version 2.56.0\n");
assert_eq!(relative.code, Some(0));
remove(&fixture);
}
#[test]
fn only_wrappers_on_path_is_a_reported_failure() {
let fixture: Fixture = fixture("no-real-git");
plant_wrapper_forms(&fixture);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "hard", "copy-early", "chain", "missing"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(
observed,
Observed {
code: Some(2),
stdout: Vec::<u8>::new(),
stderr: b"cli-git: Could not find a real Git executable after examining 5 PATH \
candidates and skipping 4 cli-git wrappers. Ensure Git is installed and \
PATH/PATHEXT expose its executable.\n"
.to_vec(),
}
);
remove(&fixture);
}
#[test]
fn typescript_wrapper_launcher_is_skipped() {
let fixture: Fixture = fixture("typescript-shim");
link_real_git(&fixture);
std::fs::create_dir(fixture.root.join("shim")).expect("directory");
executable(
fixture.root.join("shim/git").as_path(),
b"#!/bin/sh\n# node_modules/@monochromatic-dev/git-policy-cli/dist/final/node/index.mjs\ntouch \"$(dirname \"$0\")/executed\"\nexit 99\n",
);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "shim", "real"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(observed.stdout, b"git version 2.56.0\n");
assert_eq!(observed.code, Some(0));
assert!(!fixture.root.join("shim/executed").exists());
remove(&fixture);
}
#[test]
fn different_wrapper_build_stops_instead_of_looping() {
let fixture: Fixture = fixture("other-build");
std::fs::create_dir(fixture.root.join("other")).expect("directory");
let other: PathBuf = fixture.root.join("other/git");
let mut bytes: Vec<u8> = std::fs::read(&fixture.wrapper).expect("wrapper bytes");
bytes.push(0);
executable(other.as_path(), bytes.as_slice());
let ran = Command::new(&other)
.arg("--version")
.env_clear()
.env("PATH", "/usr/bin")
.output();
assert_eq!(
ran.expect("modified build still runs").stdout,
b"git version 2.56.0\n"
);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other"]).as_os_str(),
)
.arg("--version"),
b"",
);
let mut expected: Vec<u8> =
b"cli-git: another cli-git wrapper selected this cli-git executable (".to_vec();
expected.extend_from_slice(other.as_os_str().as_bytes());
expected.extend_from_slice(
b") as real Git. Forwarding again would never reach Git. Remove the extra cli-git from \
PATH or place the real Git executable on PATH.\n",
);
assert_eq!(
observed,
Observed {
code: Some(2),
stdout: Vec::<u8>::new(),
stderr: expected,
}
);
link_real_git(&fixture);
let with_git: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other", "real"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(with_git.code, Some(2));
assert_eq!(with_git.stdout, Vec::<u8>::new());
let conventional: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other", "/usr/bin"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(conventional.stdout, b"git version 2.56.0\n");
assert_eq!(conventional.code, Some(0));
remove(&fixture);
}