use openjd_expr::{ExprValue, ParsedExpression, SymbolTable, DEFAULT_OPERATION_LIMIT};
fn eval(expr: &str) -> ExprValue {
ParsedExpression::new(expr)
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap()
}
#[test]
fn returns_expr_value() {
assert_eq!(eval("42").to_display_string(), "42");
}
#[test]
fn has_type() {
assert_eq!(eval("42").expr_type().to_string(), "int");
}
fn eval_bounded(
expr: &str,
mem: usize,
) -> Result<openjd_expr::EvalResult, openjd_expr::ExpressionError> {
ParsedExpression::new(expr).and_then(|p| {
p.with_memory_limit(mem)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
}
fn eval_peak(expr: &str) -> usize {
ParsedExpression::new(expr)
.and_then(|p| {
p.with_memory_limit(usize::MAX)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
.unwrap()
.peak_memory
}
fn eval_peak_with(expr: &str, st: &SymbolTable) -> usize {
ParsedExpression::new(expr)
.and_then(|p| {
p.with_memory_limit(usize::MAX)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[st])
})
.unwrap()
.peak_memory
}
#[test]
fn string_mul_exceeds_limit() {
let e = eval_bounded("\"a\" * 10000000", 1000)
.unwrap_err()
.to_string();
assert!(e.contains("exceeded limit (1000 bytes)"), "got:\n{e}");
assert!(e.contains("\"a\" * 10000000"), "got:\n{e}");
}
#[test]
fn list_mul_exceeds_limit() {
let e = eval_bounded("[1, 2, 3] * 10000000", 10000)
.unwrap_err()
.to_string();
assert!(
e.contains(
&[
"Expression operation count (10000001) exceeded limit (10000000)\n",
" [1, 2, 3] * 10000000\n",
" ~~~~~~~~~~^~~~~~~~~~",
]
.concat()
),
"got:\n{e}"
);
}
#[test]
fn range_exceeds_limit() {
let e = eval_bounded("range(10000000)", 1000)
.unwrap_err()
.to_string();
assert!(
e.contains(
&[
"Expression operation count (10000001) exceeded limit (10000000)\n",
" range(10000000)\n",
" ^~~~~~~~~~~~~~~",
]
.concat()
),
"got:\n{e}"
);
}
#[test]
fn range_start_stop_exceeds_limit() {
let e = eval_bounded("range(0, 10000000)", 1000)
.unwrap_err()
.to_string();
assert!(
e.contains(
&[
"Expression operation count (10000001) exceeded limit (10000000)\n",
" range(0, 10000000)\n",
" ^~~~~~~~~~~~~~~~~~",
]
.concat()
),
"got:\n{e}"
);
}
#[test]
fn range_start_stop_step_exceeds_limit() {
let e = eval_bounded("range(0, 10000000, 1)", 1000)
.unwrap_err()
.to_string();
assert!(
e.contains(
&[
"Expression operation count (10000001) exceeded limit (10000000)\n",
" range(0, 10000000, 1)\n",
" ^~~~~~~~~~~~~~~~~~~~~",
]
.concat()
),
"got:\n{e}"
);
}
#[test]
fn normal_within_limit() {
assert_eq!(eval("1 + 2 + 3").to_display_string(), "6");
}
#[test]
fn small_string_mul_within_limit() {
let r = eval_bounded("\"ab\" * 5", 10000).unwrap();
assert_eq!(r.value.to_display_string(), "ababababab");
}
#[test]
fn small_range_within_limit() {
let r = eval_bounded("range(5)", 10000).unwrap();
assert_eq!(r.value.to_display_string(), "[0, 1, 2, 3, 4]");
}
#[test]
fn peak_memory_returned() {
assert!(eval_peak("1 + 2") > 0);
}
#[test]
fn peak_memory_increases_with_complexity() {
let simple = eval_peak("1");
let complex = eval_peak("[1, 2, 3, 4, 5]");
assert!(complex > simple);
}
#[test]
fn peak_memory_for_string() {
let short = eval_peak("\"a\"");
let long = eval_peak("\"a\" * 100");
assert!(long > short);
}
#[test]
fn intermediate_values_released() {
let r = ParsedExpression::new("(1 + 2) + (3 + 4)")
.and_then(|p| {
p.with_memory_limit(usize::MAX)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
.unwrap();
assert_eq!(r.value.to_display_string(), "10");
assert!(r.peak_memory > 0);
}
#[test]
fn peak_memory_resets_each_call() {
let mut st = SymbolTable::new();
st.set("Param.X", ExprValue::String("a".repeat(1000)))
.unwrap();
let large = eval_peak_with("Param.X * 100", &st);
let mut st2 = SymbolTable::new();
st2.set("Param.X", ExprValue::String("b".to_string()))
.unwrap();
let small = eval_peak_with("Param.X * 100", &st2);
assert!(small < large);
}
#[test]
fn nested_comprehension_releases_inner_lists() {
let single = eval_peak("len([i for i in range(100)])");
let multi = eval_peak("[len([i for i in range(100)]) for k in range(100)]");
assert!(
multi < single * 5,
"multi={multi}, single={single}, ratio={}",
multi / single.max(1)
);
}
#[test]
fn deeply_nested_comprehension_bounded_memory() {
let r = ParsedExpression::new(
"[len([i for i in [len(range(100)) for j in range(100)]]) for k in range(100)]",
)
.and_then(|p| {
p.with_memory_limit(usize::MAX)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
.unwrap();
assert!(r.peak_memory < 1_000_000, "peak_memory={}", r.peak_memory);
}
#[test]
fn comprehension_function_call_releases_args() {
let multi = eval_peak("[len(sorted(range(50))) for i in range(50)]");
assert!(multi < 50_000, "multi={multi}");
}
#[test]
fn peak_memory_int_literal() {
let peak = eval_peak("50");
let ev_size = std::mem::size_of::<ExprValue>();
assert_eq!(
peak, ev_size,
"int literal should be one ExprValue, got {peak}"
);
}
#[test]
fn peak_memory_range_50() {
let peak = eval_peak("range(50)");
let ev_size = std::mem::size_of::<ExprValue>();
assert!(
peak >= ev_size + 50 * 8,
"range(50) peak={peak}, expected >= {} (ExprValue + 50 i64s)",
ev_size + 50 * 8
);
}
#[test]
fn peak_memory_max_range_50() {
let peak = eval_peak("max(range(50))");
let ev_size = std::mem::size_of::<ExprValue>();
assert!(
peak >= ev_size + 50 * 8,
"max(range(50)) peak={peak}, expected >= {}",
ev_size + 50 * 8
);
}
#[test]
fn peak_memory_range_concat_list() {
let peak = eval_peak("range(50) + [1, 2]");
let ev_size = std::mem::size_of::<ExprValue>();
assert!(
peak >= ev_size + 52 * 8,
"range(50)+[1,2] peak={peak}, expected >= {}",
ev_size + 52 * 8
);
}
#[test]
fn peak_memory_range_concat_range() {
let peak = eval_peak("range(50) + range(50)");
let ev_size = std::mem::size_of::<ExprValue>();
assert!(
peak >= ev_size + 100 * 8,
"range(50)+range(50) peak={peak}, expected >= {}",
ev_size + 100 * 8
);
}
const TIGHT_MEM: usize = 1_000;
fn err_msg(expr: &str, mem: usize) -> String {
eval_bounded(expr, mem).unwrap_err().to_string()
}
fn assert_memory_exceeded(expr: &str, mem: usize) {
let e = err_msg(expr, mem);
assert!(
e.contains("Expression memory usage")
&& e.contains(&format!("exceeded limit ({mem} bytes)")),
"expected memory-limit error, got:\n{e}"
);
}
#[test]
fn make_list_checked_list_literal_evaluator() {
assert_memory_exceeded(
"[\"abcdefghijklmnopqrstuvwxyz\" for i in range(1000)]",
TIGHT_MEM,
);
}
#[test]
fn make_list_checked_list_comprehension_evaluator() {
assert_memory_exceeded("[i * i for i in range(10000)]", TIGHT_MEM);
}
#[test]
fn make_list_checked_range_fn() {
let e = ParsedExpression::new("range(100000)")
.and_then(|p| {
p.with_memory_limit(TIGHT_MEM)
.with_operation_limit(10_000_000)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
.unwrap_err()
.to_string();
assert!(
e.contains("exceeded limit"),
"expected a bound-exceeded error, got:\n{e}"
);
}
#[test]
fn make_list_checked_sorted_fn() {
let mut st = SymbolTable::new();
st.set(
"Param.Items",
ExprValue::ListString(
(0..1000).map(|i| format!("item_{i:04}")).collect(),
0,
),
)
.unwrap();
let e = ParsedExpression::new("sorted(Param.Items)")
.and_then(|p| {
p.with_memory_limit(TIGHT_MEM)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&st])
})
.unwrap_err()
.to_string();
assert!(
e.contains("exceeded limit"),
"expected memory-limit error from sorted(), got:\n{e}"
);
}
#[test]
fn make_list_checked_mul_list_fn() {
let e = ParsedExpression::new("[\"aaaa\"] * 100000")
.and_then(|p| {
p.with_memory_limit(TIGHT_MEM)
.with_operation_limit(10_000_000)
.evaluate_with_metrics(&[&SymbolTable::new()])
})
.unwrap_err()
.to_string();
assert!(
e.contains("exceeded limit"),
"expected bound-exceeded error from list * n, got:\n{e}"
);
}
#[test]
fn make_list_checked_split_fn() {
let mut st = SymbolTable::new();
let src = "a,".repeat(5000) + "a";
st.set("Param.S", ExprValue::String(src)).unwrap();
let e = ParsedExpression::new("split(Param.S, \",\")")
.and_then(|p| {
p.with_memory_limit(TIGHT_MEM)
.with_operation_limit(DEFAULT_OPERATION_LIMIT)
.evaluate_with_metrics(&[&st])
})
.unwrap_err()
.to_string();
assert!(
e.contains("exceeded limit"),
"expected memory-limit error from split(), got:\n{e}"
);
}
#[test]
fn make_list_checked_small_lists_succeed() {
let r = eval_bounded("[1, 2, 3, 4, 5]", 10_000).unwrap();
assert_eq!(r.value.to_display_string(), "[1, 2, 3, 4, 5]");
let r = eval_bounded("sorted([3, 1, 2])", 10_000).unwrap();
assert_eq!(r.value.to_display_string(), "[1, 2, 3]");
let r = eval_bounded("range(10)", 10_000).unwrap();
assert_eq!(
r.value.to_display_string(),
"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
);
let r = eval_bounded("[\"a\", \"b\"] * 3", 10_000).unwrap();
assert_eq!(
r.value.to_display_string(),
"[\"a\", \"b\", \"a\", \"b\", \"a\", \"b\"]"
);
}
#[test]
fn estimate_list_heap_size_is_upper_bound() {
use openjd_expr::ExprType;
fn check(elements: Vec<ExprValue>, hint: ExprType) {
let estimate_size = elements.len() * std::mem::size_of::<ExprValue>()
+ elements
.iter()
.map(|e| e.memory_size() - std::mem::size_of::<ExprValue>())
.sum::<usize>();
let list = ExprValue::make_list(elements, hint).unwrap();
let actual = list.memory_size() - std::mem::size_of::<ExprValue>();
assert!(
estimate_size >= actual,
"estimator {estimate_size} < actual {actual}"
);
}
check(vec![ExprValue::Int(1), ExprValue::Int(2)], ExprType::INT);
check(
vec![
ExprValue::String("hello".into()),
ExprValue::String("world".into()),
],
ExprType::STRING,
);
check(vec![], ExprType::INT);
}