#![allow(clippy::unwrap_used, clippy::expect_used)]
use rstest::rstest;
use kaish_kernel::lexer::{tokenize, Token};
mod common;
fn lex(input: &str) -> Vec<Token> {
tokenize(input)
.expect("lexing should succeed")
.into_iter()
.map(|s| s.token)
.filter(|t| !matches!(t, Token::Newline))
.collect()
}
#[tokio::test]
async fn hash_in_word_does_not_swallow_the_rest_of_the_line() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, r#"echo "bare:" abc#3; echo "two:" x"#).await;
assert_eq!(out, "bare: abc#3\ntwo: x");
assert_eq!(code, 0);
}
#[tokio::test]
async fn short_id_shaped_argument_survives_intact() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, "echo 2d25fb02#3").await;
assert_eq!(out, "2d25fb02#3");
assert_eq!(code, 0);
}
#[test]
fn hash_mid_word_lexes_as_one_word() {
assert_eq!(lex("abc#3"), vec![Token::Ident("abc#3".to_string())]);
}
#[test]
fn trailing_hash_stays_in_the_word() {
assert_eq!(
lex("abc# 3"),
vec![Token::Ident("abc#".to_string()), Token::Int(3)]
);
}
#[test]
fn repeated_hashes_stay_in_the_word() {
assert_eq!(lex("a#b#c"), vec![Token::Ident("a#b#c".to_string())]);
}
#[test]
fn hash_in_a_path_stays_in_the_path() {
assert_eq!(lex("/foo/bar#1"), vec![Token::Path("/foo/bar#1".to_string())]);
}
#[test]
fn hash_in_a_digit_leading_word_stays_in_the_word() {
assert_eq!(
lex("2d25fb02#3"),
vec![Token::NumberIdent("2d25fb02#3".to_string())]
);
}
#[test]
fn hash_after_whitespace_is_a_comment() {
assert_eq!(lex("abc #3"), vec![Token::Ident("abc".to_string())]);
}
#[test]
fn hash_at_start_of_input_is_a_comment() {
assert_eq!(lex("# comment"), vec![]);
}
#[test]
fn hash_after_a_separator_is_a_comment() {
assert_eq!(
lex("a;#c"),
vec![Token::Ident("a".to_string()), Token::Semi]
);
}
#[rstest]
#[case::pipe("echo a |# c", "a")]
#[case::background_and("true &&# c\necho a", "a")]
#[case::redirect_in("echo a <# c", "")]
#[case::open_paren("(# c\necho a)", "a")]
#[tokio::test]
async fn each_accept_set_member_opens_a_comment(#[case] script: &str, #[case] _want: &str) {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
if let Err(e) = kernel.execute(script).await {
let text = e.to_string();
assert!(
!text.contains("start of a word"),
"`#` should have opened a comment in {script:?}: {text}"
);
}
}
#[tokio::test]
async fn spaced_hash_still_comments_the_rest_of_the_line() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, "echo one #two\necho three").await;
assert_eq!(out, "one\nthree");
assert_eq!(code, 0);
}
#[tokio::test]
async fn hash_after_a_variable_reference_is_a_loud_error() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let error = kernel
.execute("x=q; echo $x#3; echo second")
.await
.expect_err("mid-word `#` after a variable reference must be rejected");
let text = error.to_string();
assert!(
text.contains("start of a word"),
"error must teach the rule: {text}"
);
assert!(
text.contains(r#""$x#3""#),
"error must show the quote fix: {text}"
);
}
#[tokio::test]
async fn hash_after_a_quoted_string_is_a_loud_error() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let error = kernel
.execute(r##"echo "abc"#3"##)
.await
.expect_err("mid-word `#` after a quoted string must be rejected");
assert!(error.to_string().contains("start of a word"));
}
#[tokio::test]
async fn hash_after_a_closing_paren_is_a_loud_error() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let error = kernel
.execute("echo $(echo a)#3")
.await
.expect_err("mid-word `#` after `)` must be rejected");
assert!(error.to_string().contains("start of a word"));
}
#[tokio::test]
async fn arithmetic_after_a_mid_word_hash_still_expands() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, "echo abc# $((1+2))").await;
assert_eq!(out, "abc# 3");
assert_eq!(code, 0);
}
#[tokio::test]
async fn heredoc_after_a_mid_word_hash_still_collects() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, "cat <<EOF # a#b\nbody\nEOF").await;
assert_eq!(out, "body");
assert_eq!(code, 0);
}
#[tokio::test]
async fn hash_glued_to_an_arithmetic_expansion_is_a_loud_error() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let error = kernel
.execute("echo $((1+2))#3")
.await
.expect_err("`#` glued to an arithmetic expansion must be rejected");
let text = error.to_string();
assert!(
text.contains("start of a word"),
"must be the mid-word `#` error, not a generic parse error: {text}"
);
}
#[tokio::test]
async fn hash_in_an_assignment_target_is_rejected() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let text = match kernel.execute("abc#3=5").await {
Ok(r) => r.text_out().to_string(),
Err(e) => e.to_string(),
};
assert!(
text.contains("E018") || text.contains("not a valid assignment target"),
"must refuse an unreadable name: {text}"
);
}
#[tokio::test]
async fn quoting_keeps_a_leading_hash_literal() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let (out, code) = common::run(&kernel, r##"echo "#3" '#4'"##).await;
assert_eq!(out, "#3 #4");
assert_eq!(code, 0);
}