use std::path::Path;
use std::process::Command;
use kaish_kernel::{Kernel, KernelConfig};
#[allow(dead_code)] pub fn kernel_at(dir: &Path) -> Kernel {
let config = KernelConfig::repl()
.with_cwd(dir.to_path_buf())
.with_latch(false)
.with_trash(false);
Kernel::new(config).expect("failed to create kernel")
}
#[allow(dead_code)]
pub async fn run(kernel: &Kernel, script: &str) -> (String, i64) {
let result = kernel.execute(script).await.expect("kernel execute");
(result.text_out().trim().to_string(), result.code)
}
#[allow(dead_code)] pub struct BashOutput {
pub stdout: String,
#[allow(dead_code)]
pub code: i64,
}
#[allow(dead_code)] pub fn run_bash_if_enabled(script: &str) -> Option<BashOutput> {
if std::env::var_os("KAISH_BASH_COMPAT").is_none() {
return None;
}
let output = Command::new("bash")
.arg("-c")
.arg(script)
.output()
.expect("KAISH_BASH_COMPAT set but failed to run bash; install bash or unset the var");
Some(BashOutput {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
code: output.status.code().map(i64::from).unwrap_or(-1),
})
}
#[macro_export]
macro_rules! shell_compat_kaish_assert {
($out:expr, $code:expr,) => {};
($out:expr, $code:expr, eq: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out.trim(),
$e,
"kaish stdout mismatch:\n--- got ---\n{}\n-----------",
$out,
);
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, eq_exact: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out,
$e,
"kaish stdout (exact) mismatch:\n--- got ---\n{:?}\n-----------",
$out,
);
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, kaish_eq: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out.trim(),
$e,
"kaish stdout mismatch:\n--- got ---\n{}\n-----------",
$out,
);
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, bash_eq: $e:expr $(, $($r:tt)*)?) => {
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, contains: $n:expr $(, $($r:tt)*)?) => {
assert!(
$out.contains($n),
"kaish missing substring {:?}:\n--- got ---\n{}\n-----------",
$n,
$out,
);
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, absent: $m:expr $(, $($r:tt)*)?) => {
assert!(
!$out.contains($m),
"kaish unexpected substring {:?}:\n--- got ---\n{}\n-----------",
$m,
$out,
);
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, exit: $c:expr $(, $($r:tt)*)?) => {
assert_eq!($code, $c as i64, "kaish exit code mismatch");
$( $crate::shell_compat_kaish_assert!($out, $code, $($r)*); )?
};
}
/// Bash-side mirror of `shell_compat_kaish_assert!`. Ignores `kaish_eq:`,
/// honors `bash_eq:`, and otherwise applies the same shared clauses.
#[macro_export]
macro_rules! shell_compat_bash_assert {
($out:expr, $code:expr,) => {};
($out:expr, $code:expr, eq: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out.trim(),
$e,
"bash stdout mismatch:\n--- got ---\n{}\n-----------",
$out,
);
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, eq_exact: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out,
$e,
"bash stdout (exact) mismatch:\n--- got ---\n{:?}\n-----------",
$out,
);
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, kaish_eq: $e:expr $(, $($r:tt)*)?) => {
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, bash_eq: $e:expr $(, $($r:tt)*)?) => {
assert_eq!(
$out.trim(),
$e,
"bash stdout mismatch:\n--- got ---\n{}\n-----------",
$out,
);
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, contains: $n:expr $(, $($r:tt)*)?) => {
assert!(
$out.contains($n),
"bash missing substring {:?}:\n--- got ---\n{}\n-----------",
$n,
$out,
);
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, absent: $m:expr $(, $($r:tt)*)?) => {
assert!(
!$out.contains($m),
"bash unexpected substring {:?}:\n--- got ---\n{}\n-----------",
$m,
$out,
);
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
($out:expr, $code:expr, exit: $c:expr $(, $($r:tt)*)?) => {
assert_eq!($code, $c as i64, "bash exit code mismatch");
$( $crate::shell_compat_bash_assert!($out, $code, $($r)*); )?
};
}
/// Generate a pair of tests for a single shell scenario.
///
/// Layout:
/// ```ignore
/// shell_compat! {
/// name: my_scenario,
/// script: "echo hi",
/// eq: "hi",
/// }
/// ```
///
/// Generates `my_scenario::kaish` (always runs) and `my_scenario::bash`
/// (gated on `KAISH_BASH_COMPAT=1`). Both apply the same clauses to the
/// stdout/exit-code their side produced.
#[macro_export]
macro_rules! shell_compat {
(
name: $name:ident,
script: $script:expr,
$($body:tt)*
) => {
mod $name {
use ::kaish_kernel::{Kernel, KernelConfig};
#[tokio::test]
#[allow(unused_variables)]
async fn kaish() {
// Skip validation so scripts that reference unset vars (heredoc
// bodies, $NOT_A_VAR, etc.) reach the runtime — the compat
// suite compares runtime behavior with bash, not validator
// strictness.
let kernel = Kernel::new(
KernelConfig::transient().with_skip_validation(true),
).expect("kernel");
let result = kernel.execute($script).await.expect("kaish execute");
let out: String = result.text_out().into_owned();
let code: i64 = result.code;
$crate::shell_compat_kaish_assert!(out, code, $($body)*);
}
#[test]
#[allow(unused_variables)]
fn bash() {
let Some(b) = $crate::common::run_bash_if_enabled($script) else {
return;
};
$crate::shell_compat_bash_assert!(b.stdout, b.code, $($body)*);
}
}
};
}