#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
use kaish_kernel::{Kernel, KernelConfig};
use rstest::rstest;
fn kernel() -> Kernel {
Kernel::new(KernelConfig::repl().with_trash(false)).expect("failed to create kernel")
}
#[rstest]
#[case::for_into_wc("for f in a b; do echo $f; done | wc -l", "2\n")]
#[case::for_into_grep("for f in a b c; do echo $f; done | grep b", "b\n")]
#[case::while_no_iterations("while [[ -n \"\" ]]; do echo x; done | wc -l", "0\n")]
#[case::if_then("if true; then echo x; fi | cat", "x\n")]
#[case::if_else("if false; then echo x; else echo y; fi | cat", "y\n")]
#[case::case_stmt("case a in a) echo hit ;; esac | cat", "hit\n")]
#[case::for_through_two_pipes("for f in a b; do echo $f; done | cat | cat", "a\nb\n")]
#[tokio::test]
async fn a_compound_statement_can_feed_a_pipe(#[case] source: &str, #[case] expected: &str) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(
result.text_out(),
expected,
"`{source}` (exit {}, stderr {:?})",
result.code,
result.err
);
}
#[rstest]
#[case::while_consumes_a_pipe(
"printf \"a\\nb\\n\" | while read l; do echo \"got $l\"; done",
"got a\ngot b\n"
)]
#[case::if_in_last_stage("echo x | if true; then cat; fi", "x\n")]
#[case::case_in_last_stage("echo hit | case a in a) cat ;; esac", "hit\n")]
#[case::compound_both_ends(
"for f in a b; do echo $f; done | while read l; do echo \"L:$l\"; done",
"L:a\nL:b\n"
)]
#[tokio::test]
async fn a_compound_can_be_a_later_stage(#[case] source: &str, #[case] expected: &str) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(
result.text_out(),
expected,
"`{source}` (exit {}, stderr {:?})",
result.code,
result.err
);
}
#[tokio::test]
async fn a_buffered_compound_stage_still_produces_the_right_answer() {
let kernel = kernel();
let result = kernel
.execute("for f in a b c; do echo $f; done | head -n 1")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "a\n");
}
#[tokio::test]
async fn a_compound_stage_leaves_the_previous_statement_alone() {
let kernel = kernel();
let result = kernel
.execute("echo pre | cat; for f in a; do echo $f; done | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "pre\na\n");
}
#[rstest]
#[case::case_alternation("case b in a|b) echo hit ;; esac", "hit\n")]
#[case::case_alternation_into_pipe("case b in a|b) echo hit ;; esac | cat", "hit\n")]
#[tokio::test]
async fn case_alternation_still_parses(#[case] source: &str, #[case] expected: &str) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}` regressed");
}
#[rstest]
#[case::plain_pipeline("echo plain | cat", "plain\n")]
#[case::bare_compound("for f in a b; do echo $f; done", "a\nb\n")]
#[case::compound_with_and("for f in a; do echo $f; done && echo after", "a\nafter\n")]
#[tokio::test]
async fn the_forms_that_already_worked_keep_working(
#[case] source: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}` regressed");
}
#[rstest]
#[case::last_stage_command_succeeds("for f in a; do false; done | cat", 0)]
#[case::last_stage_command_fails("for f in a b; do echo $f; done | grep zzz", 1)]
#[case::last_stage_loop_takes_its_last_command("echo x | for f in a; do false; done", 1)]
#[case::last_stage_if_with_no_branch_taken("echo x | if false; then echo a; fi", 0)]
#[tokio::test]
async fn the_last_stage_sets_the_exit_code(#[case] source: &str, #[case] expected: i64) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.code, expected, "`{source}` (stderr {:?})", result.err);
}
#[tokio::test]
async fn a_cd_in_a_non_last_compound_stage_does_not_leak() {
let kernel = kernel();
let before = kernel.cwd().await;
kernel
.execute("for f in a; do cd /; done | cat")
.await
.expect("execution failed");
assert_eq!(kernel.cwd().await, before);
}
#[tokio::test]
async fn a_compound_stage_nests() {
let kernel = kernel();
let result = kernel
.execute("for f in a b; do for g in 1 2; do echo \"$f$g\"; done; done | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "a1\na2\nb1\nb2\n");
}
#[tokio::test]
async fn scatter_gather_refuses_a_compound_stage_by_name() {
let kernel = kernel();
let result = kernel
.execute("for f in a b; do echo $f; done | scatter | echo x | gather")
.await
.expect("execution failed");
assert_eq!(result.code, 2, "stderr {:?}", result.err);
assert!(
result.err.contains("scatter/gather cannot share a pipeline"),
"the error must name the condition, got {:?}",
result.err
);
}
#[rstest]
#[case::first_stage("for f in a b; do rm $f; done | wc -l", vec!["rm", "wc"])]
#[case::last_stage("echo x | while read l; do rm $l; done", vec!["echo", "read", "rm"])]
#[test]
fn a_compound_stage_is_not_opaque_to_the_plan(#[case] source: &str, #[case] expected: Vec<&str>) {
let plans = kaish_kernel::plan_program(source).expect("parses");
let names: Vec<&str> = plans[0]
.plan
.commands
.iter()
.map(|c| c.name.as_str())
.collect();
assert_eq!(names, expected, "`{source}`");
}
#[rstest]
#[case::bare_compound_backgrounded("for f in a b; do echo $f; done", "a\nb\n")]
#[case::compound_into_pipe_backgrounded("for f in a b; do echo $f; done | wc -l", "2\n")]
#[tokio::test]
async fn a_backgrounded_compound_still_produces_its_output(
#[case] body: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let announcement = kernel
.execute(&format!("{body} &"))
.await
.expect("execution failed");
assert!(
announcement.err.contains("[1]"),
"expected a job announcement, got {:?}",
announcement.err
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
let status = kernel
.execute("cat /v/jobs/1/status")
.await
.expect("status check failed");
if status.text_out().trim().starts_with("done:") {
break;
}
assert!(std::time::Instant::now() < deadline, "job 1 never finished");
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let out = kernel
.execute("cat /v/jobs/1/stdout")
.await
.expect("reading job stdout failed");
assert_eq!(out.text_out(), expected, "`{body} &`");
}
#[tokio::test]
async fn a_compound_can_be_a_middle_stage() {
let kernel = kernel();
let result = kernel
.execute("printf \"a\\nb\\n\" | while read l; do echo \"got $l\"; done | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "got a\ngot b\n");
}
#[rstest]
#[case::structured_into_a_compound("seq 1 3 | while read l; do echo \"n=$l\"; done", "n=1\nn=2\nn=3\n")]
#[case::structured_through_a_compound("seq 1 3 | while read l; do echo $l; done | wc -l", "3\n")]
#[tokio::test]
async fn structured_data_reaches_a_compound_stage(#[case] source: &str, #[case] expected: &str) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}`");
}
#[tokio::test]
async fn continue_inside_a_compound_stage_skips_only_that_iteration() {
let kernel = kernel();
let result = kernel
.execute("for f in a b c; do if [[ $f == b ]]; then continue; fi; echo $f; done | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "a\nc\n");
}
#[rstest]
#[case::exit_in_a_non_last_stage("for f in a b; do exit 7; done | cat; echo after=$?", "after=0\n")]
#[case::exit_in_the_last_stage("echo x | for f in a; do exit 7; done; echo after=$?", "after=7\n")]
#[tokio::test]
async fn exit_inside_a_compound_stage_stops_at_the_stage(
#[case] source: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}`");
}
#[tokio::test]
async fn return_inside_a_compound_stage_does_not_return_from_the_function() {
let kernel = kernel();
let result = kernel
.execute("f() { for x in 1 2; do echo $x; return; done | cat; echo after; }; f")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "1\nafter\n");
}
#[rstest]
#[case::timeout_redispatch("for f in a b; do timeout 5 echo $f; done | cat", "a\nb\n")]
#[case::user_tool_body("g() { echo \"g:$1\"; }; for f in a b; do g $f; done | cat", "g:a\ng:b\n")]
#[case::nested_pipeline("for f in a b; do echo $f | cat; done | cat", "a\nb\n")]
#[case::command_substitution("for f in a b; do echo $(echo $f); done | cat", "a\nb\n")]
#[tokio::test]
async fn a_nested_dispatch_inside_a_compound_stage_cannot_steal_the_pipe(
#[case] source: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}`");
}
#[tokio::test]
async fn a_sourced_script_inside_a_compound_stage_cannot_steal_the_pipe() {
let dir = tempfile::tempdir().expect("tempdir");
let kernel = Kernel::new(
KernelConfig::repl()
.with_cwd(dir.path().to_path_buf())
.with_trash(false),
)
.expect("failed to create kernel");
kernel
.execute("echo 'echo from-script' > inner.kai")
.await
.expect("writing the fixture failed");
let result = kernel
.execute("for f in a b; do source inner.kai; done | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "from-script\nfrom-script\n");
}