#![cfg(all(
feature = "cli",
feature = "embed",
feature = "test-support",
feature = "unix-runtime"
))]
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Clone, Debug)]
struct ReferenceShell {
name: &'static str,
program: &'static str,
args: &'static [&'static str],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Expectation {
Exact,
IgnoreStderr,
AllowKnownDifference,
}
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn list_shell_scripts(dir: &Path, skip: &[&str]) -> Vec<PathBuf> {
let mut scripts: Vec<PathBuf> = fs::read_dir(dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", dir.display()))
.filter_map(|entry| entry.ok().map(|e| e.path()))
.filter(|path| path.extension() == Some(OsStr::new("sh")))
.filter(|path| {
let name = path.file_name().and_then(OsStr::to_str).unwrap_or_default();
!skip.contains(&name)
})
.collect();
scripts.sort();
scripts
}
fn reference_shells() -> Vec<ReferenceShell> {
[
ReferenceShell {
name: "sh",
program: "/bin/sh",
args: &[],
},
ReferenceShell {
name: "dash",
program: "/bin/dash",
args: &[],
},
ReferenceShell {
name: "bash-posix",
program: "/bin/bash",
args: &["--posix"],
},
]
.into_iter()
.filter(|shell| Path::new(shell.program).is_file())
.collect()
}
fn run_shell(shell: &ReferenceShell, script_path: &Path) -> (i32, String, String) {
let output = Command::new(shell.program)
.args(shell.args)
.arg(script_path)
.output()
.unwrap_or_else(|err| {
panic!(
"failed to run {} ({}) {}: {err}",
shell.name,
shell.program,
script_path.display()
)
});
let code = output.status.code().unwrap_or(128);
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
(code, stdout, stderr)
}
fn nocapture_enabled() -> bool {
if std::env::args().any(|arg| arg == "--nocapture") {
return true;
}
std::env::var("RUST_TEST_NOCAPTURE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
fn maybe_print_script(script_path: &Path) {
if !nocapture_enabled() {
return;
}
println!("+ SCRIPT {}", script_path.display());
let text = fs::read_to_string(script_path).unwrap_or_default();
for line in text.lines() {
println!("+ {line}");
}
}
fn expectation_for(shell: &ReferenceShell, script: &Path) -> Expectation {
let name = script
.file_name()
.and_then(OsStr::to_str)
.unwrap_or_default();
match shell.name {
"sh" => match name {
"2.2.3-alias-expansion.fail.sh" => Expectation::AllowKnownDifference,
_ => Expectation::Exact,
},
"dash" => match name {
"pipeline.sh"
| "read.sh"
| "2.2.2-nested-single-quotes.fail.sh"
| "2.2.3-dquote-nonterminated-backquote.undefined.sh" => Expectation::IgnoreStderr,
"2.2.3-backquote-nonterminated-dquote.undefined.sh"
| "2.2.3-backquote-nonterminated-squote.undefined.sh" => {
Expectation::AllowKnownDifference
}
_ => Expectation::Exact,
},
"bash-posix" => match name {
"pipeline.sh" | "2.2.2-nested-single-quotes.fail.sh" => Expectation::IgnoreStderr,
"2.2.3-alias-expansion.fail.sh" => Expectation::AllowKnownDifference,
_ => Expectation::Exact,
},
_ => Expectation::Exact,
}
}
fn outputs_match(
expectation: Expectation,
mx_rc: i32,
mx_out: &str,
mx_err: &str,
sh_rc: i32,
sh_out: &str,
sh_err: &str,
) -> bool {
match expectation {
Expectation::Exact => mx_rc == sh_rc && mx_out == sh_out && mx_err == sh_err,
Expectation::IgnoreStderr => mx_rc == sh_rc && mx_out == sh_out,
Expectation::AllowKnownDifference => true,
}
}
fn compare_suite(scripts: &[PathBuf]) {
let mxsh = env!("CARGO_BIN_EXE_mxsh");
let reference_shells = reference_shells();
assert!(
!reference_shells.is_empty(),
"expected at least one reference shell to be available"
);
let mut failures = Vec::new();
for shell in reference_shells {
for script in scripts {
maybe_print_script(script);
let (mx_rc, mx_out, mx_err) = run_shell(
&ReferenceShell {
name: "mxsh",
program: mxsh,
args: &[],
},
script,
);
let (sh_rc, sh_out, sh_err) = run_shell(&shell, script);
let expectation = expectation_for(&shell, script);
if !outputs_match(
expectation,
mx_rc,
&mx_out,
&mx_err,
sh_rc,
&sh_out,
&sh_err,
) {
failures.push(format!(
"{} against {} ({expectation:?}) rc={mx_rc}/{sh_rc}\n--- {} stdout ---\n{sh_out}\n--- mxsh stdout ---\n{mx_out}\n--- {} stderr ---\n{sh_err}\n--- mxsh stderr ---\n{mx_err}",
script.display(),
shell.name,
shell.name,
shell.name,
));
}
}
}
if !failures.is_empty() {
panic!(
"script compatibility failures ({}):\n\n{}",
failures.len(),
failures.join("\n\n")
);
}
}
#[test]
fn mrsh_test_suite_matches_bin_sh() {
let root = project_root();
let scripts = list_shell_scripts(&root.join("mrsh/test"), &["harness.sh"]);
compare_suite(&scripts);
}
#[test]
fn mrsh_conformance_suite_matches_bin_sh() {
let root = project_root();
let scripts = list_shell_scripts(&root.join("mrsh/test/conformance"), &["harness.sh"]);
compare_suite(&scripts);
}