use std::path::PathBuf;
use std::process::Command;
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_xpile"))
}
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn have_python_and_sh() -> bool {
let py = Command::new("python3").arg("--version").output().is_ok();
let sh = Command::new("/bin/sh")
.arg("-c")
.arg("true")
.output()
.is_ok();
py && sh
}
fn run_cpython(fixture_path: &std::path::Path, entry: &str) -> Result<String, String> {
let src = fixture_path.to_str().ok_or("non-utf8 fixture path")?;
let prog = format!("import subprocess; exec(open(r'{src}').read()); {entry}()");
let out = Command::new("python3")
.args(["-c", &prog])
.output()
.map_err(|e| format!("spawn python3: {e}"))?;
if !out.status.success() {
return Err(format!(
"python3 exited non-zero: stderr={}",
String::from_utf8_lossy(&out.stderr)
));
}
Ok(String::from_utf8_lossy(&out.stdout).trim_end().to_string())
}
fn run_shell(fixture_path: &std::path::Path) -> Result<String, String> {
let transpile = Command::new(bin())
.args([
"transpile",
fixture_path.to_str().unwrap(),
"--target",
"shell",
])
.output()
.map_err(|e| format!("spawn xpile: {e}"))?;
if !transpile.status.success() {
return Err(format!(
"xpile transpile failed: stderr={}",
String::from_utf8_lossy(&transpile.stderr)
));
}
let shell_source = String::from_utf8_lossy(&transpile.stdout).to_string();
let run = Command::new("/bin/sh")
.arg("-c")
.arg(&shell_source)
.output()
.map_err(|e| format!("spawn /bin/sh: {e}"))?;
if !run.status.success() {
return Err(format!(
"/bin/sh exited non-zero (script ran but commands failed?):\n\
=== stderr ===\n{}\n\
=== transpiled source ===\n{shell_source}",
String::from_utf8_lossy(&run.stderr)
));
}
Ok(String::from_utf8_lossy(&run.stdout).trim_end().to_string())
}
const REALISTIC_DEMO_EXPECTED: &str = "hello world\nhow are you\nHi, Noah Gift\nstarted zero done";
#[test]
fn shell_diff_demo_realistic_shell_input_round_trip() {
if !have_python_and_sh() {
eprintln!(
"warning: skipping PMAT-052 — /bin/sh not on PATH. CI environments \
with /bin/sh will still run this gate."
);
return;
}
let sh_path = fixture("bashrs_realistic_demo.sh");
let actual = run_shell(&sh_path).expect("shell run");
assert_eq!(
actual, REALISTIC_DEMO_EXPECTED,
"bashrs realistic demo output diverged. The transpiled .sh \
emit produced a different stdout than expected. Likely cause: \
one of the Layer B parser / renderer paths regressed.\n\
=== expected ===\n{REALISTIC_DEMO_EXPECTED}\n\
=== actual ===\n{actual}"
);
}
#[test]
fn shell_diff_demo_cpython_vs_bashrs_emit_agree() {
if !have_python_and_sh() {
eprintln!(
"warning: skipping PMAT-043 shell_diff_exec — python3 and/or /bin/sh \
not on PATH. CI environments with both will still run this gate."
);
return;
}
let py_path = fixture("bashrs_diff_demo.py");
let py_out = run_cpython(&py_path, "demo").expect("CPython run");
let sh_out = run_shell(&py_path).expect("shell run");
assert_eq!(
py_out, sh_out,
"CPython and bashrs-emitted shell diverge on bashrs_diff_demo.py:\n\
=== CPython ===\n{py_out}\n\
=== Shell ===\n{sh_out}\n\
The Runtime-stratum witness for C-BASHRS-POSIX-IDEMPOTENCE has \
broken. Either depyler-frontend's subprocess.run lowering or \
bashrs-backend's emit changed in a way that no longer matches \
the Python observable behaviour."
);
assert!(
py_out.contains("starting") && py_out.contains("done"),
"expected `starting` and `done` lines in output; got: {py_out}"
);
}