Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run test fixture scripts on Windows with Git Bash #1712

Merged
merged 6 commits into from
Dec 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 95 additions & 25 deletions tests/tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ impl Drop for GitDaemon {
}

static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));

static EXCLUDE_LUT: Lazy<Mutex<Option<gix_worktree::Stack>>> = Lazy::new(|| {
let cache = (|| {
let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?;
Expand Down Expand Up @@ -86,8 +87,31 @@ static EXCLUDE_LUT: Lazy<Mutex<Option<gix_worktree::Stack>>> = Lazy::new(|| {
})();
Mutex::new(cache)
});

#[cfg(windows)]
const GIT_PROGRAM: &str = "git.exe";
#[cfg(not(windows))]
const GIT_PROGRAM: &str = "git";

static GIT_CORE_DIR: Lazy<PathBuf> = Lazy::new(|| {
let output = std::process::Command::new(GIT_PROGRAM)
.arg("--exec-path")
.output()
.expect("can execute `git --exec-path`");

assert!(output.status.success(), "`git --exec-path` failed");

output
.stdout
.strip_suffix(b"\n")
.expect("`git --exec-path` output to be well-formed")
.to_os_str()
.expect("no invalid UTF-8 in `--exec-path` except as OS allows")
.into()
});

/// The major, minor and patch level of the git version on the system.
pub static GIT_VERSION: Lazy<(u8, u8, u8)> = Lazy::new(|| parse_gix_version().expect("git version to be parsable"));
pub static GIT_VERSION: Lazy<(u8, u8, u8)> = Lazy::new(|| parse_git_version().expect("git version to be parsable"));

/// Define how [`scripted_fixture_writable_with_args()`] uses produces the writable copy.
pub enum Creation {
Expand Down Expand Up @@ -116,10 +140,8 @@ pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u
*GIT_VERSION < (major, minor, patch)
}

fn parse_gix_version() -> Result<(u8, u8, u8)> {
let gix_program = cfg!(windows).then(|| "git.exe").unwrap_or("git");
let output = std::process::Command::new(gix_program).arg("--version").output()?;

fn parse_git_version() -> Result<(u8, u8, u8)> {
let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?;
git_version_from_bytes(&output.stdout)
}

Expand Down Expand Up @@ -173,25 +195,14 @@ impl Drop for AutoRevertToPreviousCWD {

/// Run `git` in `working_dir` with all provided `args`.
pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> {
std::process::Command::new("git")
std::process::Command::new(GIT_PROGRAM)
.current_dir(working_dir)
.args(args)
.status()
}

/// Spawn a git daemon process to host all repository at or below `working_dir`.
pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
static EXEC_PATH: Lazy<PathBuf> = Lazy::new(|| {
let path = std::process::Command::new("git")
.arg("--exec-path")
.stderr(std::process::Stdio::null())
.output()
.expect("can execute `git --exec-path`")
.stdout;
String::from_utf8(path.trim().into())
.expect("no invalid UTF8 in exec-path")
.into()
});
let mut ports: Vec<_> = (9419u16..9419 + 100).collect();
fastrand::shuffle(&mut ports);
let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port));
Expand All @@ -200,11 +211,12 @@ pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDae
listener.local_addr().expect("listener address is available").port()
};

let child = std::process::Command::new(EXEC_PATH.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" }))
.current_dir(working_dir)
.args(["--verbose", "--base-path=.", "--export-all", "--user-path"])
.arg(format!("--port={free_port}"))
.spawn()?;
let child =
std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" }))
.current_dir(working_dir)
.args(["--verbose", "--base-path=.", "--export-all", "--user-path"])
.arg(format!("--port={free_port}"))
.spawn()?;

let server_addr = addr_at(free_port);
for time in gix_lock::backoff::Exponential::default_with_random() {
Expand Down Expand Up @@ -556,7 +568,7 @@ fn scripted_fixture_read_only_with_args_inner(
Err(err)
if err.kind() == std::io::ErrorKind::PermissionDenied || err.raw_os_error() == Some(193) /* windows */ =>
{
cmd = std::process::Command::new("bash");
cmd = std::process::Command::new(bash_program());
configure_command(cmd.arg(script_absolute_path), &args, &script_result_directory).output()?
}
Err(err) => return Err(err.into()),
Expand Down Expand Up @@ -632,6 +644,22 @@ fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
.env("GIT_CONFIG_VALUE_3", "always")
}

fn bash_program() -> &'static Path {
if cfg!(windows) {
static GIT_BASH: Lazy<Option<PathBuf>> = Lazy::new(|| {
GIT_CORE_DIR
.parent()?
.parent()?
.parent()
.map(|installation_dir| installation_dir.join("bin").join("bash.exe"))
.filter(|bash| bash.is_file())
});
GIT_BASH.as_deref().unwrap_or(Path::new("bash.exe"))
} else {
Path::new("bash")
}
}

fn write_failure_marker(failure_marker: &Path) {
std::fs::write(failure_marker, []).ok();
}
Expand Down Expand Up @@ -738,7 +766,10 @@ fn populate_meta_dir(destination_dir: &Path, script_identity: u32) -> std::io::R
)?;
std::fs::write(
meta_dir.join(META_GIT_VERSION),
std::process::Command::new("git").arg("--version").output()?.stdout,
std::process::Command::new(GIT_PROGRAM)
.arg("--version")
.output()?
.stdout,
)?;
Ok(meta_dir)
}
Expand Down Expand Up @@ -951,7 +982,7 @@ mod tests {
let temp = tempfile::TempDir::new().expect("can create temp dir");
populate_ad_hoc_config_files(temp.path());

let mut cmd = std::process::Command::new("git");
let mut cmd = std::process::Command::new(GIT_PROGRAM);
cmd.env("GIT_CONFIG_SYSTEM", SCOPE_ENV_VALUE);
cmd.env("GIT_CONFIG_GLOBAL", SCOPE_ENV_VALUE);
configure_command(&mut cmd, ["config", "-l", "--show-origin"], temp.path());
Expand All @@ -968,4 +999,43 @@ mod tests {
assert_eq!(lines, Vec::<&str>::new(), "should be no config variables from files");
assert_eq!(status, 0, "reading the config should succeed");
}

#[test]
#[cfg(windows)]
fn bash_program_ok_for_platform() {
let path = bash_program();
assert!(path.is_absolute());

let for_version = std::process::Command::new(path)
.arg("--version")
.output()
.expect("can pass it `--version`");
assert!(for_version.status.success(), "passing `--version` succeeds");
let version_line = for_version
.stdout
.lines()
.nth(0)
.expect("`--version` output has first line");
assert!(
version_line.ends_with(b"-pc-msys)"), // On Windows, "-pc-linux-gnu)" would be WSL.
"it is an MSYS bash (such as Git Bash)"
);

let for_uname_os = std::process::Command::new(path)
.args(["-c", "uname -o"])
.output()
.expect("can tell it to run `uname -o`");
assert!(for_uname_os.status.success(), "telling it to run `uname -o` succeeds");
assert_eq!(
for_uname_os.stdout.trim_end(),
b"Msys",
"it runs commands in an MSYS environment"
);
}

#[test]
#[cfg(not(windows))]
fn bash_program_ok_for_platform() {
assert_eq!(bash_program(), Path::new("bash"));
}
}
Loading