use openjd_expr::{
ExpressionErrorKind, FormatString, FormatStringOptions, ParsedExpression, SymbolTable,
MAX_EXPRESSION_DEPTH,
};
fn expect_too_deep(expr: &str) {
match ParsedExpression::new(expr) {
Ok(parsed) => {
let st = SymbolTable::new();
let err = parsed.evaluate(&st).expect_err(&format!(
"Evaluator must reject deeply-nested expression, but accepted:\n {expr:.80}"
));
assert!(
matches!(err.kind(), ExpressionErrorKind::ExpressionTooDeep { .. }),
"Expected ExpressionTooDeep from evaluator, got: {:?}\n msg: {}",
err.kind(),
err.message()
);
}
Err(err) => {
assert!(
matches!(err.kind(), ExpressionErrorKind::ExpressionTooDeep { .. }),
"Expected ExpressionTooDeep from parser, got: {:?}\n msg: {}",
err.kind(),
err.message()
);
}
}
}
#[test]
fn max_depth_constant_is_64() {
assert_eq!(MAX_EXPRESSION_DEPTH, 64);
}
#[test]
fn parser_survives_deeply_nested_parens() {
let expr = format!("{}1{}", "(".repeat(200), ")".repeat(200));
let parsed = ParsedExpression::new(&expr)
.expect("200 parens must parse (worker thread has ample stack)");
let v = parsed
.evaluate(&SymbolTable::new())
.expect("evaluation must succeed");
assert_eq!(v.to_display_string(), "1");
}
#[test]
fn parser_survives_very_deeply_nested_parens() {
let expr = format!("{}1{}", "(".repeat(5000), ")".repeat(5000));
let parsed = ParsedExpression::new(&expr)
.expect("5000 parens must parse (worker thread has ample stack)");
let v = parsed
.evaluate(&SymbolTable::new())
.expect("evaluation must succeed");
assert_eq!(v.to_display_string(), "1");
}
#[test]
fn reject_oversized_parser_input() {
let expr = format!("{}1{}", "(".repeat(40_000), ")".repeat(40_000));
let err = ParsedExpression::new(&expr).expect_err("oversized parser input must be rejected");
let msg = err.message();
assert!(
msg.contains("maximum") || msg.contains("exceeds"),
"expected size-cap error, got: {msg}"
);
}
#[test]
fn reject_deeply_nested_unary_minus() {
let expr = format!("{}1", "-".repeat(200));
expect_too_deep(&expr);
}
#[test]
fn reject_very_deeply_nested_unary_minus() {
let expr = format!("{}1", "-".repeat(5000));
expect_too_deep(&expr);
}
#[test]
fn reject_deeply_right_nested_power() {
let mut expr = String::from("2");
for _ in 0..200 {
expr = format!("2**({expr})");
}
expect_too_deep(&expr);
}
#[test]
fn reject_long_left_assoc_binop_chain() {
let mut expr = String::from("1");
for _ in 0..200 {
expr.push_str("+1");
}
expect_too_deep(&expr);
}
#[test]
fn reject_very_long_left_assoc_binop_chain() {
let mut expr = String::from("1");
for _ in 0..5000 {
expr.push_str("+1");
}
expect_too_deep(&expr);
}
#[test]
fn reject_long_attribute_chain() {
let mut expr = String::from("a");
for i in 0..200 {
expr.push_str(&format!(".x{}", i));
}
expect_too_deep(&expr);
}
#[test]
fn reject_long_comparison_chain() {
let mut expr = String::from("0");
for i in 1..200 {
expr.push_str(&format!("<{i}"));
}
expect_too_deep(&expr);
}
#[test]
fn reject_nested_list_literal_via_depth() {
let mut expr = String::from("1");
for _ in 0..200 {
expr = format!("({expr})");
}
let wide: Vec<&str> = (0..2000).map(|_| "[1]").collect();
let wide_expr = format!("[{}]", wide.join(","));
let parsed = ParsedExpression::new(&wide_expr).expect("wide-but-shallow list must parse");
let _ = parsed
.with_operation_limit(1_000_000)
.with_memory_limit(100_000_000)
.evaluate(&[&SymbolTable::new()]);
}
#[test]
fn reject_deep_call_chain() {
let mut expr = String::from("1");
for _ in 0..200 {
expr = format!("abs({expr})");
}
expect_too_deep(&expr);
}
#[test]
fn format_string_rejects_deep_interpolation() {
let mut inner = String::from("1");
for _ in 0..200 {
inner.push_str("+1");
}
let fs_src = format!("{{{{{inner}}}}}");
let err = FormatString::new(&fs_src).expect_err("deeply-nested interpolation must be rejected");
let msg = err.message();
assert!(
msg.contains("nesting depth")
|| msg.contains("too deep")
|| msg.to_lowercase().contains("expression"),
"expected depth-related error, got: {msg}"
);
}
#[test]
fn format_string_rejects_many_segments() {
let fs_src = "{{1}}".repeat(2_000);
let err = FormatString::new(&fs_src).expect_err("too many segments must be rejected");
let msg = err.message();
assert!(
msg.contains("segment") || msg.contains("interpolation"),
"expected segment-cap error, got: {msg}"
);
}
#[test]
fn format_string_rejects_oversized_input() {
let fs_src = "a".repeat(2 * 1024 * 1024);
let err = FormatString::new(&fs_src).expect_err("oversized input must be rejected");
let msg = err.message();
assert!(
msg.contains("length") || msg.contains("too long") || msg.contains("size"),
"expected size-cap error, got: {msg}"
);
}
#[test]
fn allow_depth_just_below_limit() {
let expr = format!("{}1{}", "(".repeat(50), ")".repeat(50));
let parsed = ParsedExpression::new(&expr).expect("depth 50 must parse");
let v = parsed
.evaluate(&SymbolTable::new())
.expect("depth 50 must evaluate");
assert_eq!(v.to_display_string(), "1");
}
#[test]
fn allow_modest_binop_chain() {
let mut expr = String::from("1");
for _ in 0..50 {
expr.push_str("+1");
}
let parsed = ParsedExpression::new(&expr).expect("50-term chain must parse");
let v = parsed
.evaluate(&SymbolTable::new())
.expect("50-term chain must evaluate");
assert_eq!(v.to_display_string(), "51");
}
#[test]
fn allow_typical_template_expression() {
let expr = r"sum([Param.Frame * x + 1 for x in range(10)])";
let parsed = ParsedExpression::new(expr).expect("typical template expr must parse");
let mut st = SymbolTable::new();
st.set("Param.Frame", 3).unwrap();
let v = parsed
.evaluate(&st)
.expect("typical template expr must evaluate");
assert!(matches!(v, openjd_expr::ExprValue::Int(_)));
}
#[test]
fn allow_moderately_long_expression_on_fast_path() {
let expr = r"sum([Param.Frame * x + len(Param.Name) - (1 if Param.Flag else 0) for x in range(10) if x > 0]) + abs(Param.Offset) * 2 + 3";
assert!(expr.len() <= 200, "test setup: expr must fit fast path");
let mut st = SymbolTable::new();
st.set("Param.Frame", 5).unwrap();
st.set("Param.Name", "shot_01").unwrap();
st.set("Param.Flag", true).unwrap();
st.set("Param.Offset", -10).unwrap();
let parsed = ParsedExpression::new(expr).expect("150-char expr must parse");
let _ = parsed.evaluate(&st).expect("150-char expr must evaluate");
}
#[test]
fn allow_expression_above_fast_path_threshold() {
let mut expr = String::from("Param.Frame");
while expr.len() < 500 {
expr.push_str(" + Param.Frame");
}
let mut st = SymbolTable::new();
st.set("Param.Frame", 1).unwrap();
let parsed = ParsedExpression::new(&expr).expect("500-char sum must parse");
let _ = parsed.evaluate(&st).expect("500-char sum must evaluate");
}
#[test]
fn allow_moderate_format_string() {
let fs_src = "{{1}}".repeat(100);
let fs = FormatString::new(&fs_src).expect("100 segments must parse");
let st = SymbolTable::new();
let out = fs
.resolve_string_with(&st, &FormatStringOptions::default())
.unwrap();
assert_eq!(out, "1".repeat(100));
}
#[test]
fn allow_reasonable_format_string_size() {
let fs_src = "a".repeat(64 * 1024);
let _ = FormatString::new(&fs_src).expect("64 KB literal must parse");
}
#[test]
fn parser_survives_deeply_nested_subscripts() {
let depth = 2000;
let mut expr = String::from("x");
for _ in 0..depth {
expr.push_str("[0]");
}
expect_too_deep(&expr);
}
#[test]
fn parser_survives_deeply_nested_list_literals() {
let depth = 2000;
let expr = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
expect_too_deep(&expr);
}
#[test]
fn parser_survives_deeply_nested_attribute_chain() {
let depth = 5000;
let mut expr = String::from("a");
for i in 0..depth {
expr.push_str(&format!(".x{i}"));
}
expect_too_deep(&expr);
}