#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
use kaish_kernel::{plan_program, Kernel, KernelConfig};
fn kernel_at(dir: &std::path::Path) -> Kernel {
Kernel::new(KernelConfig::repl().with_cwd(dir.to_path_buf()).with_trash(false))
.expect("kernel")
}
fn tempdir() -> tempfile::TempDir {
tempfile::Builder::new()
.prefix("plan-program-")
.tempdir_in(env!("CARGO_TARGET_TMPDIR"))
.expect("tempdir under CARGO_TARGET_TMPDIR")
}
#[test]
fn plan_program_needs_no_kernel() {
let plans = plan_program("echo before\nrm f.txt\necho after").expect("parses");
assert_eq!(
plans
.iter()
.map(|p| (p.index, p.plan.rendered.as_str()))
.collect::<Vec<_>>(),
vec![(0, "echo before"), (1, "rm f.txt"), (2, "echo after")],
);
}
#[tokio::test]
async fn planning_executes_nothing() {
let dir = tempdir();
let target = dir.path().join("f.txt");
std::fs::write(&target, "keep me").expect("write");
let kernel = kernel_at(dir.path());
let plans = kernel.plan_program("rm f.txt").expect("plans");
assert_eq!(plans[0].plan.commands[0].name, "rm");
assert!(target.exists(), "planning must not run the statement");
}
#[tokio::test]
async fn free_variables_feed_the_get_var_peek_loop() {
let dir = tempdir();
let kernel = kernel_at(dir.path());
kernel
.execute("TARGET=precious.txt")
.await
.expect("assignment");
let plans = kernel.plan_program("rm ${TARGET}").expect("plans");
assert_eq!(plans[0].plan.free_variables, vec!["TARGET".to_string()]);
assert!(plans[0].plan.bound_variables.is_empty());
let value = kernel
.get_var(&plans[0].plan.free_variables[0])
.await
.expect("TARGET is set");
assert_eq!(value, kaish_types::Value::String("precious.txt".to_string()));
}
#[test]
fn bound_names_never_read_as_free() {
let plans =
plan_program("for f in $(ls ${DIR}); do rm $f; done").expect("parses");
assert_eq!(plans[0].plan.free_variables, vec!["DIR".to_string()]);
assert_eq!(plans[0].plan.bound_variables, vec!["f".to_string()]);
}
#[test]
fn a_command_substitution_inside_arithmetic_plans_its_own_commands() {
let plans = plan_program("echo $((1 + $(touch PROBE; echo 1)))").expect("parses");
let names: Vec<&str> = plans[0].plan.commands.iter().map(|c| c.name.as_str()).collect();
assert_eq!(
names,
vec!["echo", "touch", "echo"],
"both commands inside $(( $(...) )) must be planned, in source order"
);
}
#[test]
fn a_command_substitution_inside_a_based_expansion_plans_its_own_command() {
let plans = plan_program("echo $((10#$(printf 42)))").expect("parses");
let names: Vec<&str> = plans[0].plan.commands.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["echo", "printf"]);
}
#[test]
fn a_command_substitution_inside_an_arithmetic_default_plans_its_own_command() {
let plans = plan_program("echo $((${cnt:-$(printf 9)}))").expect("parses");
let names: Vec<&str> = plans[0].plan.commands.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["echo", "printf"]);
}
#[test]
fn a_heredoc_reached_through_arithmetic_keeps_the_flat_address_invariant() {
let source = "cat <<'A' && echo $((1 + $(cat <<'B'\nbodyB\nB\n))) && cat <<'C'\nbodyA\nA\nbodyC\nC";
let plans = plan_program(source).expect("parses");
let heredocs: Vec<kaish_types::plan::PlannedHeredoc> = plans[0]
.plan
.commands
.iter()
.flat_map(|c| c.heredocs.clone())
.collect();
assert_eq!(
heredocs.iter().map(|h| h.index).collect::<Vec<_>>(),
vec![0, 1, 2],
"flat indices across the arithmetic boundary"
);
assert_eq!(
heredocs.iter().map(|h| h.body.display()).collect::<Vec<_>>(),
vec!["bodyA\n".to_string(), "bodyB\n".to_string(), "bodyC\n".to_string()]
);
for heredoc in &heredocs {
let addr = kaish_types::plan::FragmentAddr::new(0, heredoc.index);
let expanded = kaish_kernel::expand_fragment(source, addr, &[])
.unwrap_or_else(|e| panic!("address {addr:?} unresolvable: {e}"));
assert_eq!(
expanded,
kaish_types::plan::Expansion::Complete(heredoc.body.display()),
"the resolver must agree with the address the plan published for index {}",
heredoc.index
);
}
}
#[test]
fn background_propagates_through_a_command_substitution_inside_arithmetic() {
let direct = plan_program("echo $(echo hi) &").expect("parses");
let via_arith = plan_program("echo $((1 + $(echo hi))) &").expect("parses");
let direct_flags: Vec<bool> =
direct[0].plan.commands.iter().map(|c| c.background).collect();
let arith_flags: Vec<bool> =
via_arith[0].plan.commands.iter().map(|c| c.background).collect();
assert_eq!(direct_flags, vec![true, true], "direct form: both commands backgrounded");
assert_eq!(
arith_flags, direct_flags,
"the arithmetic form must agree with the direct form on backgrounding"
);
let not_backgrounded = plan_program("echo $((1 + $(echo hi)))").expect("parses");
let control_flags: Vec<bool> =
not_backgrounded[0].plan.commands.iter().map(|c| c.background).collect();
assert_eq!(
control_flags,
vec![false, false],
"without `&` neither command is backgrounded — control for a hardcoded `true`"
);
}