#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
mod common;
use std::fs;
use common::{kernel_at, run};
use tempfile::tempdir;
async fn code_of(script: &str) -> i64 {
let dir = tempdir().unwrap();
fs::write(dir.path().join("file.txt"), "hi\n").unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
let kernel = kernel_at(dir.path());
let (_out, code) = run(&kernel, script).await;
code
}
#[tokio::test]
async fn string_emptiness() {
assert_eq!(code_of(r#"test -z """#).await, 0, "-z empty is true");
assert_eq!(code_of("test -z x").await, 1, "-z non-empty is false");
assert_eq!(code_of("test -n x").await, 0, "-n non-empty is true");
assert_eq!(code_of(r#"test -n """#).await, 1, "-n empty is false");
}
#[tokio::test]
async fn one_arg_string_truthiness() {
assert_eq!(code_of("test x").await, 0, "non-empty single arg is true");
assert_eq!(code_of(r#"test """#).await, 1, "empty single arg is false");
}
#[tokio::test]
async fn string_equality() {
assert_eq!(code_of("test a = a").await, 0);
assert_eq!(code_of("test a = b").await, 1);
assert_eq!(code_of("test a == a").await, 0, "== is a literal-equality alias");
assert_eq!(code_of("test a != b").await, 0);
assert_eq!(code_of("test a != a").await, 1);
}
#[tokio::test]
async fn flag_shaped_operands_survive() {
assert_eq!(code_of("test -n = -n").await, 0, r#""-n" equals "-n""#);
assert_eq!(code_of("test x = -f").await, 1, r#""x" != "-f", -f is an operand"#);
assert_eq!(code_of("test -f = -f").await, 0, r#""-f" equals "-f""#);
}
#[tokio::test]
async fn numeric_integer() {
assert_eq!(code_of("test 5 -eq 5").await, 0);
assert_eq!(code_of("test 5 -eq 3").await, 1);
assert_eq!(code_of("test 5 -ne 3").await, 0);
assert_eq!(code_of("test 5 -gt 3").await, 0);
assert_eq!(code_of("test 3 -gt 5").await, 1);
assert_eq!(code_of("test 3 -lt 5").await, 0);
assert_eq!(code_of("test 5 -ge 5").await, 0);
assert_eq!(code_of("test 4 -le 5").await, 0);
assert_eq!(code_of("test 08 -eq 8").await, 0, "leading zero is decimal, not octal");
}
#[tokio::test]
async fn numeric_negative_operands() {
assert_eq!(code_of("test 0 -gt -5").await, 0, "0 > -5, -5 survives as operand");
assert_eq!(code_of("test -5 -lt 0").await, 0, "-5 < 0");
}
#[tokio::test]
async fn numeric_floats_allowed() {
assert_eq!(code_of("test 1.5 -gt 1").await, 0, "float operand compares, not errors");
assert_eq!(code_of("test 1.0 -eq 1").await, 0, "1.0 equals 1 numerically");
assert_eq!(code_of("test 2.5 -lt 2").await, 1, "2.5 is not < 2");
}
#[tokio::test]
async fn numeric_non_numeric_is_loud() {
assert_eq!(code_of("test abc -eq 5").await, 2, "non-numeric string is a usage error");
}
#[tokio::test]
async fn file_tests() {
assert_eq!(code_of("test -e file.txt").await, 0, "-e existing");
assert_eq!(code_of("test -e nope").await, 1, "-e missing");
assert_eq!(code_of("test -f file.txt").await, 0, "-f regular file");
assert_eq!(code_of("test -f sub").await, 1, "-f on a dir is false");
assert_eq!(code_of("test -d sub").await, 0, "-d directory");
assert_eq!(code_of("test -d file.txt").await, 1, "-d on a file is false");
}
#[tokio::test]
async fn builtin_shadows_external_test() {
assert_eq!(code_of("test 1.5 -gt 1").await, 0);
}
#[tokio::test]
async fn negation() {
assert_eq!(code_of("test ! -f nope").await, 0, "! (nope is not a file) => true");
assert_eq!(code_of("test ! -f file.txt").await, 1, "! (file.txt is a file) => false");
assert_eq!(code_of("test ! a = a").await, 1, "! (a==a) => false");
assert_eq!(code_of("test ! a = b").await, 0, "! (a!=b as ==) => true");
assert_eq!(code_of("test ! -z x").await, 0, "! (-z x is false) => true");
}
#[tokio::test]
async fn arity_errors_are_loud() {
assert_eq!(code_of("test").await, 1, "0-arg test is false, like bash");
assert_eq!(code_of("test -f").await, 2, "operator with missing operand is loud");
assert_eq!(code_of("test -z").await, 2, "operator with missing operand is loud");
assert_eq!(code_of("test !").await, 2, "bare ! has no expression");
assert_eq!(code_of("test a b").await, 2, "two barewords, no operator");
}
#[tokio::test]
async fn arity_edge_cases() {
assert_eq!(code_of("test ! ! x").await, 0, "! ! x is identity on non-empty");
assert_eq!(code_of("test ! !").await, 2, "trailing bare ! is a usage error");
assert_eq!(code_of("test ! = x").await, 2, "= is not a unary operator");
assert_eq!(code_of("test = = =").await, 0, r#""=" equals "=""#);
assert_eq!(code_of("test -f -f").await, 1, r#"stats a file literally named "-f""#);
}
#[tokio::test]
async fn compound_operators_are_refused() {
for script in ["test -f file.txt -a -f sub", "test a = a -o b = b"] {
let dir = tempdir().unwrap();
fs::write(dir.path().join("file.txt"), "hi\n").unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
let kernel = kernel_at(dir.path());
let err = kernel
.execute(script)
.await
.expect_err("must not run")
.to_string();
assert!(err.contains("E020"), "`{script}`: {err}");
}
}
#[tokio::test]
async fn shell_chaining_is_still_a_compound_path() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("file.txt"), "hi\n").unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
let kernel = kernel_at(dir.path());
let (out, code) = run(&kernel, "test -f file.txt && test -d sub && echo both").await;
assert_eq!(code, 0, "and-chain of two true tests succeeds: {out:?}");
assert_eq!(out, "both");
let (out, code) = run(&kernel, "test -f nope || echo fallback").await;
assert_eq!(code, 0);
assert_eq!(out, "fallback", "or-chain runs the fallback on a false test");
}
#[tokio::test]
async fn collection_operand_is_loud() {
let dir = tempdir().unwrap();
let kernel = kernel_at(dir.path());
let (_out, code) = run(&kernel, "xs=[1 2 3]; test $xs = foo").await;
assert_eq!(code, 2, "a list operand to = is a loud usage/shape error, not silent");
}
#[tokio::test]
async fn test_as_if_condition() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("file.txt"), "hi\n").unwrap();
let kernel = kernel_at(dir.path());
let (out, code) = run(
&kernel,
r#"if test -f file.txt; then echo present; else echo absent; fi"#,
)
.await;
assert_eq!(code, 0);
assert_eq!(out, "present");
}