Run the stand-in ssh for tests without writing a script file

This commit is contained in:
2026-10-15 15:13:02 +02:00
parent 48e5e8c8ce
commit 16b0363d07
2 changed files with 31 additions and 26 deletions
+9 -7
View File
@@ -1,6 +1,6 @@
use serde::Serialize;
use crate::location::{Location, remote_command};
use std::ffi::OsStr;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
@@ -53,13 +53,14 @@ pub fn git(loc: &Location, args: &[&str]) -> Result<String, String> {
// Tests swap in a script that acts like ssh.
#[cfg(test)]
if let Some(fake) = crate::test_ssh::SSH_OVERRIDE.get() {
return git_via(fake.as_os_str(), loc, args);
return git_via(fake, loc, args);
}
git_via(OsStr::new("ssh"), loc, args)
git_via(&[OsString::from("ssh")], loc, args)
}
/// Does the work of `git`, starting the program `ssh` for a remote location.
pub fn git_via(ssh: &OsStr, loc: &Location, args: &[&str]) -> Result<String, String> {
/// Does the work of `git`. For a remote location it starts `ssh`: the first word of `ssh` is
/// the program and the other words come before the ssh options (tests use them).
pub fn git_via(ssh: &[OsString], loc: &Location, args: &[&str]) -> Result<String, String> {
let mut cmd = match loc {
Location::Local(dir) => {
let mut cmd = Command::new("git");
@@ -67,7 +68,8 @@ pub fn git_via(ssh: &OsStr, loc: &Location, args: &[&str]) -> Result<String, Str
cmd
}
Location::Ssh { target, path } => {
let mut cmd = Command::new(ssh);
let mut cmd = Command::new(&ssh[0]);
cmd.args(&ssh[1..]);
// Never ask for a password: gites has no terminal to ask on.
cmd.args(["-o", "BatchMode=yes", "-o", "ConnectTimeout=10"]);
// Later commands to the same server reuse one connection for a while. Windows ssh
@@ -294,7 +296,7 @@ mod tests {
#[test]
fn reports_a_missing_ssh_program() {
let loc = Location::Ssh { target: "myserver".into(), path: "/x".into() };
let err = git_via(OsStr::new("/no/such/ssh"), &loc, &["status"]).unwrap_err();
let err = git_via(&[OsString::from("/no/such/ssh")], &loc, &["status"]).unwrap_err();
assert!(err.starts_with("could not run ssh:"), "{err}");
}
}
+22 -19
View File
@@ -1,25 +1,28 @@
//! A stand-in for ssh, so tests can run the remote code path without a server.
use std::path::PathBuf;
use std::ffi::OsString;
use std::sync::OnceLock;
/// The program `git::git` starts instead of `ssh` while tests run.
pub static SSH_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
/// The words `git::git` starts instead of `ssh` while tests run: a program, then its arguments.
pub static SSH_OVERRIDE: OnceLock<Vec<OsString>> = OnceLock::new();
/// Writes (once per test run) a script that acts like ssh: it skips the `-o` options,
/// the `--` and the target, then runs the command on this machine. Returns its path.
pub fn fake_ssh() -> PathBuf {
SSH_OVERRIDE
.get_or_init(|| {
let path = std::env::temp_dir().join(format!("gites-fake-ssh-{}", std::process::id()));
let script = "#!/bin/sh\nwhile [ \"$1\" = \"-o\" ]; do shift 2; done\nshift 2\nexec sh -c \"$1\"\n";
std::fs::write(&path, script).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
path
})
.clone()
/// The shell script that acts like ssh. It checks the arguments the way a real ssh would
/// need them (so a wrong argument list fails a test), skips the options and the target,
/// then runs the command on this machine.
const SCRIPT: &str = r#"
ok=
while [ "$1" = "-o" ]; do
[ "$2" = "BatchMode=yes" ] && ok=1
shift 2
done
[ -n "$ok" ] || { echo "fake ssh: BatchMode=yes is missing" >&2; exit 98; }
[ "$1" = "--" ] || { echo "fake ssh: expected -- before the target" >&2; exit 99; }
shift 2
exec sh -c "$1"
"#;
/// Makes `git::git` use the stand-in for the rest of the test run. No file is written:
/// the script is given to `sh` on its command line.
pub fn fake_ssh() {
SSH_OVERRIDE.get_or_init(|| ["sh", "-c", SCRIPT, "fake-ssh"].map(OsString::from).to_vec());
}