#![allow(clippy::unwrap_used, clippy::expect_used)]
use kaish_kernel::{Kernel, KernelConfig};
mod common;
async fn run(source: &str) -> (i64, String, String) {
let k = Kernel::new(KernelConfig::isolated()).expect("kernel");
let r = k.execute(source).await.expect("kernel execute");
(r.code, r.text_out().trim().to_string(), r.err.clone())
}
async fn err_of(source: &str) -> String {
let k = Kernel::new(KernelConfig::isolated()).expect("kernel").into_arc();
match k.execute(source).await {
Ok(r) => {
assert!(!r.ok(), "{source:?} should fail");
format!("{}{}", r.text_out(), r.err)
}
Err(e) => format!("{e:?}"),
}
}
async fn ok(source: &str, expected: &str) {
let (code, out, err) = run(source).await;
assert_eq!(code, 0, "{source:?} must run: {err:?}");
assert_eq!(out, expected, "{source:?}");
}
async fn errs(source: &str, needle: &str) {
let text = err_of(source).await;
assert!(text.contains(needle), "{source:?}: expected {needle:?} in {text:?}");
}
#[tokio::test]
async fn signed_hex_and_based_literals_combine() {
ok("echo $(( - 16#Ff + + 0X10 ))", "-239").await;
}
#[tokio::test]
async fn based_expansion_from_a_variable_and_a_command() {
ok("digits=7f; echo $((16#$digits + 1))", "128").await;
ok("echo $((8#$(echo 17) + 1))", "16").await;
}
#[tokio::test]
async fn based_expansion_text_takes_no_sign() {
errs(r#"digits="-ff"; echo $((16#$digits))"#, "-16#ff").await;
errs(r#"echo $((16#$(printf -- "-ff")))"#, "-16#ff").await;
}
#[tokio::test]
async fn based_expansion_needs_digits_after_hash() {
errs("echo $((2# 101))", "digits after").await;
}
#[tokio::test]
async fn leading_zero_names_the_octal_fix() {
errs("echo $((077))", "8#77").await;
}
#[tokio::test]
async fn whitespace_padded_signed_based_string_variable() {
ok(r#"x=' -16#F '; echo "$((x + 16#1))""#, "-14").await;
}
#[tokio::test]
async fn default_expression_can_itself_be_a_base_literal() {
ok("echo $((${missing:-0Xf} + 1))", "16").await;
}
#[tokio::test]
async fn plain_string_base_spelling_and_signed_whitespace_padded() {
ok(r#"var="16#10"; echo $(( var + 1 ))"#, "17").await;
ok(r#"v=" -16#Ff "; echo $(( v ))"#, "-255").await;
}
#[tokio::test]
async fn command_output_hex_with_sign() {
ok(r#"echo $(( $(echo "-0x10") * 2 ))"#, "-32").await;
}
#[tokio::test]
async fn integral_float_at_min_converts() {
ok("x=$(fromjson -9223372036854775808.0); echo $((x))", "-9223372036854775808").await;
}
#[tokio::test]
async fn min_literal_direct_unary_operand_every_base() {
ok("echo $(( - 9223372036854775808 ))", "-9223372036854775808").await;
ok("echo $((-0x8000000000000000))", "-9223372036854775808").await;
}
#[tokio::test]
async fn min_magnitude_positive_is_out_of_range() {
errs("echo $((0X8000000000000000))", "64-bit").await;
}
#[tokio::test]
async fn min_magnitude_via_based_expansion_every_base() {
ok("digits=8000000000000000; echo $((-16#$digits))", "-9223372036854775808").await;
ok(r#"digits="9223372036854775808"; echo $((-10#$digits))"#, "-9223372036854775808").await;
ok("digits=5cbfjia3fh26ja8; echo $((-20#$digits))", "-9223372036854775808").await;
}
#[tokio::test]
async fn min_magnitude_via_based_expansion_positive_is_out_of_range() {
errs("digits=8000000000000000; echo $((16#$digits))", "64-bit").await;
}
#[tokio::test]
async fn one_past_min_magnitude_via_based_expansion_is_out_of_range() {
errs("digits=8000000000000001; echo $((-16#$digits))", "64-bit").await;
}
#[tokio::test]
async fn parens_break_the_direct_unary_minus_exception() {
errs("echo $((-(9223372036854775808)))", "64-bit").await;
errs("echo $((-(-9223372036854775808)))", "64-bit").await;
}
#[tokio::test]
async fn max_literal_via_hex() {
ok("echo $((16#7fffffffffffffff))", "9223372036854775807").await;
}
#[tokio::test]
async fn min_div_neg_one_overflows() {
errs("echo $(((-9223372036854775808) / (-1)))", "64-bit").await;
}
#[tokio::test]
async fn min_rem_neg_one_is_zero() {
ok("echo $(((-9223372036854775808) % (-1)))", "0").await;
}
#[tokio::test]
async fn power_just_past_the_boundary_overflows() {
errs("echo $((3037000500 ** 2))", "64-bit").await;
}
#[tokio::test]
async fn negative_two_to_the_63_is_exactly_min() {
ok("echo $((-2 ** 63))", "-9223372036854775808").await;
}
#[tokio::test]
async fn one_past_min_magnitude_is_out_of_range() {
errs("echo $((-9223372036854775809))", "64-bit").await;
}
#[tokio::test]
async fn min_times_neg_one_overflows() {
errs("echo $((-9223372036854775808 * -1))", "64-bit").await;
}
#[tokio::test]
async fn min_magnitude_plus_one_literal_is_out_of_range() {
errs("echo $(( 9223372036854775808 - 1 ))", "64-bit").await;
}
#[tokio::test]
async fn arithmetic_right_shift_sign_extends() {
ok("echo $((-1 >> 63))", "-1").await;
}
#[tokio::test]
async fn nested_arithmetic_as_a_shift_count() {
errs("echo $((1 << $((32 + 32))))", "0..=63").await;
}
#[tokio::test]
async fn ternary_skipped_branch_does_not_divide_by_zero() {
ok("echo $(( 0 ? 1/0 : 42 ))", "42").await;
}
#[tokio::test]
async fn and_or_skipped_side_command_substitution_does_not_run() {
let (code, out, err) = run("echo $((0 && $(echo nope >&2; echo 1)))").await;
assert_eq!(code, 0, "{err:?}");
assert_eq!(out, "0");
assert!(!err.contains("nope"), "the skipped $() must not run: {err:?}");
let (code, out, err) = run("echo $((1 || $(echo nope >&2; echo 1)))").await;
assert_eq!(code, 0, "{err:?}");
assert_eq!(out, "1");
assert!(!err.contains("nope"), "the skipped $() must not run: {err:?}");
}
#[tokio::test]
async fn nested_ternary_skips_both_unselected_command_substitutions() {
let (code, out, err) =
run("echo $((1 ? 0 : 1 ? $(echo nope >&2; echo 1) : $(echo bad >&2; echo 1)))").await;
assert_eq!(code, 0, "{err:?}");
assert_eq!(out, "0");
assert!(!err.contains("nope") && !err.contains("bad"), "{err:?}");
}
#[tokio::test]
async fn power_binds_tighter_than_multiplication_with_nested_arithmetic() {
ok("echo $((3 * $((1 + 2)) ** 2))", "27").await;
}
#[tokio::test]
async fn bitnot_binds_tighter_than_power() {
ok("echo $((~1 ** 3))", "-8").await;
}
#[tokio::test]
async fn bitand_tighter_than_xor_tighter_than_or() {
ok("echo $(( 3 | 5 ^ 6 & 10 ))", "7").await;
}
#[tokio::test]
async fn double_negative_power_towers() {
ok("echo $((-2 ** 3 ** 2))", "-512").await;
}
#[tokio::test]
async fn division_and_modulo_toward_zero_and_dividend_sign() {
ok("echo $(( -7 / 3 + -7 % 3 ))", "-3").await;
}
#[tokio::test]
async fn based_literal_in_a_sum() {
ok("echo $(( 2 + 3#10 ))", "5").await;
}
#[tokio::test]
async fn zero_b_and_zero_o_name_the_kaish_spelling() {
errs("echo $((0b101))", "2#101").await;
errs("echo $((0o77))", "8#77").await;
}
#[tokio::test]
async fn based_prefix_alone_has_no_digits() {
errs("echo $((16#))", "digits after").await;
}
#[tokio::test]
async fn whitespace_splitting_the_hash_operator_is_an_error() {
assert!(!err_of("echo $((16 # ff))").await.is_empty());
}
#[tokio::test]
async fn a_bare_hash_cannot_start_a_value() {
assert!(!err_of("echo $((#12))").await.is_empty());
}
#[tokio::test]
async fn hash_is_never_a_comment_inside_arithmetic() {
assert!(!err_of("echo $((2#10 # comment))").await.is_empty());
}
#[tokio::test]
async fn missing_operands_are_errors() {
errs("echo $((1 + ))", "has no right operand").await;
errs("echo $(( + ))", "has no operand").await;
}
#[tokio::test]
async fn trailing_garbage_after_a_numeral_is_an_error() {
assert!(!err_of("echo $((1 + 2a))").await.is_empty());
assert!(!err_of("echo $((12abc))").await.is_empty());
}
#[tokio::test]
async fn float_literal_names_integer_only() {
errs("echo $((12.0))", "integer").await;
}
#[tokio::test]
async fn negative_exponent_names_the_fix() {
errs("echo $((2 ** -1))", "negative").await;
}
#[tokio::test]
async fn power_overflow_names_the_limit() {
errs("echo $((2 ** 100))", "64-bit").await;
}
#[tokio::test]
async fn power_special_cases() {
ok("echo $((0 ** 0))", "1").await;
ok("echo $((0 ** 5))", "0").await;
ok("echo $((1 ** 999))", "1").await;
ok("echo $(((-1) ** 3))", "-1").await;
ok("echo $((-1 ** 3))", "-1").await;
}
#[tokio::test]
async fn a_huge_exponent_overflows_before_computing_anything() {
errs("echo $((2 ** 4294967296))", "64-bit").await;
}
#[tokio::test]
async fn shift_count_out_of_range_both_directions() {
assert!(!err_of("echo $((1 << -1))").await.is_empty());
assert!(!err_of("echo $((1 << 64))").await.is_empty());
}
#[tokio::test]
async fn command_output_that_is_an_expression_is_refused() {
assert!(!err_of(r#"echo $(( $(echo "1 + 2") ))"#).await.is_empty());
}
#[tokio::test]
async fn bare_true_false_are_variable_names_not_literals() {
errs("echo $(( true + false ))", "unset").await;
}
#[tokio::test]
async fn cmdsubst_comment_inside_arith_should_be_honored() {
ok("echo $(( $(true # )\necho 1) ))", "1").await;
}
#[tokio::test]
async fn cmdsubst_heredoc_inside_arith_is_honored() {
ok(
"echo $(( $(cat <<'EOF' > /dev/null\n)\nEOF\necho 1)\n+ 1 ))",
"2",
)
.await;
}
#[tokio::test]
async fn cmdsubst_heredoc_body_with_double_close_paren_is_honored() {
ok(
"echo $(( $(cat <<'EOF' > /dev/null\na )) b\nEOF\necho 5)\n+ 1 ))",
"6",
)
.await;
}
#[tokio::test]
async fn cmdsubst_heredoc_quoted_delimiter_body_is_literal() {
ok(
"x=WRONG; echo $(( $(cat <<'EOF' > /dev/null\n$x )\nEOF\necho 5)\n+ 1 ))",
"6",
)
.await;
}
#[tokio::test]
async fn cmdsubst_heredoc_unquoted_delimiter_body_is_interpolated() {
ok(
"x=3; echo $(( $(cat <<EOF > /dev/null\nvalue is $x )\nEOF\necho 5)\n+ 1 ))",
"6",
)
.await;
}
#[tokio::test]
async fn bare_arith_condition_shift_inside_cmdsubst_is_not_a_heredoc() {
ok("echo $(( $( (( 1 << 2 )) && echo 9 ) ))", "9").await;
}
#[tokio::test]
async fn control_bare_arith_condition_without_shift_inside_cmdsubst() {
ok("echo $(( $( (( 1 + 2 )) && echo 9 ) ))", "9").await;
}
#[tokio::test]
async fn heredoc_beside_a_bare_arith_condition_still_parses() {
ok(
"echo $(( $(cat <<'EOF' > /dev/null
)
EOF
(( 1 << 2 )) && echo 9) ))",
"9",
)
.await;
}
#[tokio::test]
async fn control_cmdsubst_heredoc_truly_unterminated_still_errors() {
errs(
"echo $(( $(cat <<EOF\nno closing delimiter\n) + 1 ))",
"unterminated heredoc",
)
.await;
}
#[tokio::test]
async fn braced_default_quoted_close_brace_should_be_skipped() {
ok(
r#"echo $(( ${x:-$(echo "}" > /dev/null; echo 5)} + 1 ))"#,
"6",
)
.await;
}
#[tokio::test]
async fn cmdsubst_quoted_paren_balances_and_works() {
ok(
r#"echo $(( $(( $(echo ")" > /dev/null; echo 1) + 1 )) + 1 ))"#,
"3",
)
.await;
}
#[tokio::test]
async fn dquote_open_paren_in_nested_cmdsubst_no_longer_confuses_depth() {
ok(r#"echo $(( $(echo "(" >/dev/null; echo 5) + 1 ))"#, "6").await;
}
#[tokio::test]
async fn squote_open_paren_in_nested_cmdsubst_no_longer_confuses_depth() {
ok("echo $(( $(echo '(' >/dev/null; echo 5) + 1 ))", "6").await;
}
#[tokio::test]
async fn bare_arith_dquote_open_paren_in_nested_cmdsubst() {
let (code, _, _) = run(r#"(( $(echo "(" >/dev/null; echo 5) > 1 ))"#).await;
assert_eq!(code, 0);
}
#[tokio::test]
async fn two_levels_of_nested_cmdsubst_with_quoted_paren() {
ok(
r#"echo $(( $(echo $(echo "(" >/dev/null; echo 3)) + 1 ))"#,
"4",
)
.await;
}
#[tokio::test]
async fn escaped_open_paren_in_body_reaches_the_real_close() {
errs(r"echo $(( \( 1 + 1 ))", "cannot start a value").await;
}
#[tokio::test]
async fn escaped_close_paren_in_body_does_not_close_early() {
errs(r"echo $(( 1 \)) + 1 ))", "cannot start a value").await;
}
#[tokio::test]
async fn control_genuinely_unterminated_arithmetic_still_errors() {
errs("echo $(( 1 + 2", "unterminated arithmetic").await;
}
#[tokio::test]
async fn control_genuinely_unbalanced_paren_still_errors() {
errs("echo $(( ( 1 + 2 ))", "unterminated arithmetic").await;
}