#![allow(clippy::unwrap_used, clippy::expect_used)]
use kaish_kernel::{Kernel, KernelConfig};
use rstest::rstest;
fn kernel() -> Kernel {
Kernel::new(KernelConfig::isolated()).expect("kernel")
}
#[rstest]
#[case("test a = a -o b = c")]
#[case("test a = a -a b = b")]
#[case("test -f a -a -f b")]
#[case("test '(' a = a ')'")]
#[case("test -z \"\" -o -z x")]
#[tokio::test]
async fn compound_operators_are_refused_before_running(#[case] script: &str) {
let err = kernel()
.execute(script)
.await
.expect_err("the statement must not run")
.to_string();
assert!(err.contains("E020"), "`{script}` should carry the code: {err}");
assert!(
err.contains("&&") && err.contains("||"),
"`{script}` should name the fix: {err}"
);
}
#[tokio::test]
async fn a_compound_in_a_condition_stops_the_statement() {
let err = kernel()
.execute("if test a = a -o b = c; then echo BRANCH_THEN; else echo BRANCH_ELSE; fi")
.await
.expect_err("must not choose a branch")
.to_string();
assert!(err.contains("E020"), "{err}");
assert!(
!err.contains("BRANCH_THEN") && !err.contains("BRANCH_ELSE"),
"no branch ran: {err}"
);
}
#[tokio::test]
async fn validation_binds_raw_argv_in_source_order() {
use kaish_kernel::tools::{register_builtins, ToolRegistry};
let mut registry = ToolRegistry::new();
register_builtins(&mut registry);
let tool = registry.get("test").expect("test builtin");
let refused = kernel().execute("test a = a -o b = c").await;
assert!(refused.is_err(), "an operator in the operator slot is refused");
let allowed = kernel()
.execute("test \"-o\" = \"-o\"")
.await
.expect("an operator word as an operand must run");
assert_eq!(allowed.code, 0);
let mut flags_only = kaish_kernel::tools::ToolArgs::new();
flags_only.flags.insert("o".to_string());
assert!(
tool.validate(&flags_only).is_empty(),
"nothing routes `-o` into `flags` any more; reading it there would \
resurrect the position-blind check"
);
}
#[rstest]
#[case("test a = a", 0)]
#[case("test a = b", 1)]
#[case("test ! a = a", 1)]
#[case("test ! a = b", 0)]
#[case("test ! ! x", 0)]
#[case("test -n x", 0)]
#[case("test -z \"\"", 0)]
#[case("test = = =", 0)]
#[case("test -n = -n", 0)]
#[tokio::test]
async fn supported_forms_are_unchanged(#[case] script: &str, #[case] expected: i64) {
let r = kernel().execute(script).await.expect("kernel execute");
assert_eq!(r.code, expected, "`{script}`");
}
#[rstest]
#[case("test -e \"\"", 1)]
#[case("test -f \"\"", 1)]
#[case("test -d \"\"", 1)]
#[case("test", 1)]
#[tokio::test]
async fn conformance_fixes_kept(#[case] script: &str, #[case] expected: i64) {
let r = kernel().execute(script).await.expect("kernel execute");
assert_eq!(r.code, expected, "`{script}`");
}
#[rstest]
#[case("[[ -e \"\" ]]", 1)]
#[case("[[ -f \"\" ]]", 1)]
#[case("[[ -d \"\" ]]", 1)]
#[case("[[ -r \"\" ]]", 1)]
#[case("[[ -w \"\" ]]", 1)]
#[case("[[ -x \"\" ]]", 1)]
#[tokio::test]
async fn double_bracket_answers_the_empty_path_like_test(
#[case] script: &str,
#[case] expected: i64,
) {
let r = kernel().execute(script).await.expect("kernel execute");
assert_eq!(r.code, expected, "`{script}`");
}
#[tokio::test]
async fn both_spellings_agree_on_a_real_path() {
let k = kernel();
let r = k.execute("[[ -d \"/\" ]]").await.expect("kernel execute");
assert_eq!(r.code, 0, "`[[ -d / ]]`");
let r = k.execute("test -d \"/\"").await.expect("kernel execute");
assert_eq!(r.code, 0, "`test -d /`");
}
#[rstest]
#[case("test -f")]
#[case("test -z")]
#[case("test !")]
#[case("test \"-a\"")]
#[case("test -a")]
#[case("test \"-o\"")]
#[case("test \"(\"")]
#[case("test \")\"")]
#[tokio::test]
async fn a_lone_operator_is_loud_not_a_string(#[case] script: &str) {
let r = kernel().execute(script).await.expect("kernel execute");
assert_eq!(r.code, 2, "`{script}` should be a usage error, not bash's true");
assert!(r.err.contains("operand"), "should name the problem: {}", r.err);
}
#[tokio::test]
async fn set_dash_o_reports_every_option() {
let k = kernel();
let out = k.execute("set -o").await.expect("kernel execute").text_out().into_owned();
for name in ["errexit", "glob", "output-limit", "trash"] {
assert!(out.contains(name), "`set -o` should list {name}: {out:?}");
}
assert!(out.contains("on") && out.contains("off"), "states missing: {out:?}");
}
#[tokio::test]
async fn set_dash_o_reflects_a_change() {
let k = kernel();
let before = k.execute("set -o").await.expect("exec").text_out().into_owned();
assert!(before.contains("trash\toff"), "{before:?}");
let after = k
.execute("set -o trash; set -o")
.await
.expect("exec")
.text_out()
.into_owned();
assert!(after.contains("trash\ton"), "{after:?}");
}
#[tokio::test]
async fn set_dash_o_is_structured() {
let k = kernel();
let out = k.execute("set -o --json").await.expect("exec").text_out().into_owned();
let rows: serde_json::Value = serde_json::from_str(&out).expect("parses as JSON");
let rows = rows.as_array().expect("array");
assert_eq!(rows.len(), 5, "{rows:?}");
for name in ["errexit", "glob", "pipefail", "output-limit", "trash"] {
assert!(
rows.iter().any(|r| r["OPTION"] == name),
"`{name}` must be reportable: {rows:?}"
);
}
}
#[tokio::test]
async fn set_dash_o_with_a_name_still_applies() {
let k = kernel();
assert_eq!(k.execute("set -o trash").await.expect("exec").code, 0);
assert_eq!(k.execute("set -o bogus").await.expect("exec").code, 1);
}
#[rstest]
#[case("test -f \"-a\"", 1)]
#[case("test \"-a\" = \"-a\"", 0)]
#[case("test \"-o\" = \"-o\"", 0)]
#[case("test \"(\" = \"(\"", 0)]
#[case("test \"-a\" != \"-o\"", 0)]
#[case("test -n \"-o\"", 0)]
#[tokio::test]
async fn an_operator_word_in_operand_position_is_data(
#[case] script: &str,
#[case] expected: i64,
) {
let r = kernel()
.execute(script)
.await
.unwrap_or_else(|e| panic!("`{script}` must not be refused: {e}"));
assert_eq!(r.code, expected, "`{script}`");
}
#[rstest]
#[case("test \"-a\" = 1", 1)]
#[case("test \"-o\" = 1", 1)]
#[case("test 1 = \"-a\"", 1)]
#[case("test \"(\" = 1", 1)]
#[tokio::test]
async fn a_numeric_operand_does_not_shift_the_slots(#[case] script: &str, #[case] expected: i64) {
let r = kernel()
.execute(script)
.await
.unwrap_or_else(|e| panic!("`{script}` must not be refused: {e}"));
assert_eq!(r.code, expected, "`{script}`");
}
#[rstest]
#[case("test -a 1")]
#[case("test -o 1")]
#[case("test 1 -a 2")]
#[tokio::test]
async fn a_compound_operator_is_caught_even_beside_a_number(#[case] script: &str) {
let err = kernel()
.execute(script)
.await
.expect_err("must be refused before running")
.to_string();
assert!(err.contains("E020"), "`{script}`: {err}");
}