use openjd_expr::*;
fn eval_err(expr: &str) -> String {
ParsedExpression::new(expr)
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap_err()
.to_string()
}
fn eval_err_with(expr: &str, st: &SymbolTable) -> String {
ParsedExpression::new(expr)
.and_then(|p| p.evaluate(st))
.unwrap_err()
.to_string()
}
fn assert_err(expr: &str, expected: &[&str]) {
let e = eval_err(expr);
let joined = expected.concat();
assert!(e.contains(&joined), "got:\n{e}\nexpected:\n{joined}");
}
fn assert_err_with(expr: &str, st: &SymbolTable, expected: &[&str]) {
let e = eval_err_with(expr, st);
let joined = expected.concat();
assert!(e.contains(&joined), "got:\n{e}\nexpected:\n{joined}");
}
#[test]
fn type_error_in_middle() {
assert_err(
"1 + int('bad') + 2",
&[
"Cannot convert 'bad' to int\n",
" 1 + int('bad') + 2\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn type_error_at_start() {
assert_err(
"int('bad') + 1 + 2",
&[
"Cannot convert 'bad' to int\n",
" int('bad') + 1 + 2\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn type_error_at_end() {
assert_err(
"1 + 2 + int('bad')",
&[
"Cannot convert 'bad' to int\n",
" 1 + 2 + int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn operator_error_friendly_name() {
assert_err(
"'hello' + 5",
&[
"Cannot use '+' operator with string and int\n",
" 'hello' + 5\n",
" ~~~~~~~~^~~",
],
);
}
#[test]
fn operator_error_in_middle() {
assert_err(
"1 + 'hello' + 5",
&[
"Cannot use '+' operator with int and string\n",
" 1 + 'hello' + 5\n",
" ~~^~~~~~~~~",
],
);
}
#[test]
fn division_by_zero_in_middle() {
assert_err(
"10 + 5 / 0 + 2",
&["Division by zero\n", " 10 + 5 / 0 + 2\n", " ~~^~~"],
);
}
#[test]
fn index_out_of_bounds_shows_length() {
assert_err(
"[1,2,3][10]",
&[
"Index 10 out of bounds for list of length 3\n",
" [1,2,3][10]\n",
" ~~~~~~~^~~~",
],
);
}
#[test]
fn unknown_property_friendly_name() {
assert_err(
"path('/a/b').unknown",
&[
"Cannot access attribute 'unknown' on path\n",
" path('/a/b').unknown\n",
" ~~~~~~~~~~~~~^~~~~~~",
],
);
}
#[test]
fn unknown_property_in_chain() {
assert_err(
"path('/a/b').parent.unknown",
&[
"Cannot access attribute 'unknown' on path\n",
" path('/a/b').parent.unknown\n",
" ~~~~~~~~~~~~~~~~~~~~^~~~~~~",
],
);
}
#[test]
fn min_empty_list_error() {
assert_err(
"1 + min([]) + 2",
&[
"min() requires a non-empty list\n",
" 1 + min([]) + 2\n",
" ^~~~~~~",
],
);
}
#[test]
fn split_empty_separator() {
assert_err(
"10 + 'x'.split('') + 5",
&[
"split failed: empty separator\n",
" 10 + 'x'.split('') + 5\n",
" ~~~~^~~~~~~~~",
],
);
}
#[test]
fn deeply_nested_error() {
assert_err(
"1 + (2 + (3 + int('x')))",
&[
"Cannot convert 'x' to int\n",
" 1 + (2 + (3 + int('x')))\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_function_argument() {
assert_err(
"min(1, int('x'), 3)",
&[
"Cannot convert 'x' to int\n",
" min(1, int('x'), 3)\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_condition() {
assert_err(
"1 if int('x') else 2",
&[
"Cannot convert 'x' to int\n",
" 1 if int('x') else 2\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_comprehension_body() {
assert_err(
"[int('x') for i in [1,2]]",
&[
"Cannot convert 'x' to int\n",
" [int('x') for i in [1,2]]\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_comprehension_filter() {
assert_err(
"[i for i in [1,2] if int('x')]",
&[
"Cannot convert 'x' to int\n",
" [i for i in [1,2] if int('x')]\n",
" ^~~~~~~~",
],
);
}
#[test]
fn chained_method_error() {
assert_err(
"'hello'.upper().nonexistent()",
&[
"Unknown function: 'nonexistent'\n",
" 'hello'.upper().nonexistent()\n",
" ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~",
],
);
}
#[test]
fn undefined_variable() {
assert_err("X + 1", &["Undefined variable: 'X'.\n", " X + 1\n", " ^"]);
}
#[test]
fn undefined_variable_with_suggestion() {
let mut st = openjd_expr::SymbolTable::new();
st.set("Param.Frame", openjd_expr::ExprValue::Int(1))
.unwrap();
st.set(
"Param.Scene",
openjd_expr::ExprValue::String("forest".into()),
)
.unwrap();
let result =
openjd_expr::ParsedExpression::new("Param.Frane + 1").and_then(|p| p.evaluate(&st));
let err = result.unwrap_err().to_string();
assert!(
err.contains("Did you mean: Param.Frame"),
"Expected suggestion in: {err}"
);
}
#[test]
fn undefined_variable_no_suggestion_when_distant() {
let mut st = openjd_expr::SymbolTable::new();
st.set("Param.Frame", openjd_expr::ExprValue::Int(1))
.unwrap();
let result =
openjd_expr::ParsedExpression::new("CompletelyWrong + 1").and_then(|p| p.evaluate(&st));
let err = result.unwrap_err().to_string();
assert!(
!err.contains("Did you mean"),
"Should not suggest for distant names: {err}"
);
}
#[test]
fn float_literal_infinity() {
assert_err(
"1e3000",
&[
"Float operation produced infinity\n",
" 1e3000\n",
" ^~~~~~",
],
);
}
#[test]
fn float_literal_infinity_in_expression() {
assert_err(
"1e3000 + 1",
&[
"Float operation produced infinity\n",
" 1e3000 + 1\n",
" ^~~~~~",
],
);
}
#[test]
fn leading_whitespace_stripped() {
assert_err(
" 1 + int('bad')",
&[
"Cannot convert 'bad' to int\n",
" 1 + int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn leading_whitespace_error_in_middle() {
assert_err(
" 1 + int('bad') + 2",
&[
"Cannot convert 'bad' to int\n",
" 1 + int('bad') + 2\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn exact_division_by_zero_format() {
assert_err("5 / 0", &["Division by zero\n", " 5 / 0\n", " ~~^~~"]);
}
#[test]
fn exact_int_conversion_error_format() {
assert_err(
"int('bad')",
&[
"Cannot convert 'bad' to int\n",
" int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn exact_modulo_by_zero_format() {
assert_err("10 % 0", &["Modulo by zero\n", " 10 % 0\n", " ~~~^~~"]);
}
#[test]
fn exact_fail_format() {
assert_err(
"fail(\"oops\")",
&["oops\n", " fail(\"oops\")\n", " ^~~~~~~~~~~~"],
);
}
#[test]
fn exact_index_out_of_bounds_format() {
assert_err(
"[1, 2, 3][10]",
&[
"Index 10 out of bounds for list of length 3\n",
" [1, 2, 3][10]\n",
" ~~~~~~~~~^~~~",
],
);
}
#[test]
fn exact_condition_must_be_bool() {
assert_err(
"1 if 'hello' else 2",
&[" 1 if 'hello' else 2\n", " ^~~~~~~"],
);
}
#[test]
fn wrong_arg_count_zero() {
assert_err(
"len()",
&[
"len() takes 1 argument(s), but 0 were given\n",
" len()\n",
" ^~~~~",
],
);
}
#[test]
fn wrong_arg_count_too_many() {
assert_err(
"len('a', 'b')",
&[
"len() takes 1 argument(s), but 2 were given\n",
" len('a', 'b')\n",
" ^~~~~~~~~~~~~",
],
);
}
#[test]
fn wrong_arg_count_multiple_arities() {
assert_err(
"min()",
&[
"min() takes 1, 2, 3 arguments, but 0 were given\n",
" min()\n",
" ^~~~~",
],
);
}
#[test]
fn method_on_wrong_type() {
assert_err(
"path(\"/a/b\").startswith(\"/a\")",
&[
"startswith() is not available for path. Available for: string\n",
" path(\"/a/b\").startswith(\"/a\")\n",
" ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~",
],
);
}
#[test]
fn method_on_wrong_type_upper() {
assert_err(
"(5).upper()",
&[
"upper() is not available for int. Available for: string\n",
" (5).upper()\n",
" ~~~^~~~~~~~",
],
);
}
#[test]
fn property_on_wrong_type() {
assert_err(
"True.stem",
&[
"'stem' property is not available for bool. Available for: path\n",
" True.stem\n",
" ~~~~~^~~~",
],
);
}
#[test]
fn property_on_wrong_type_parent() {
assert_err(
"'hello'.parent",
&[
"'parent' property is not available for string. Available for: path\n",
" 'hello'.parent\n",
" ~~~~~~~~^~~~~~",
],
);
}
#[test]
fn attribute_without_call_suggests_parens() {
assert_err(
"'hello'.upper",
&[
"'upper' is a method, not a property. Did you mean upper()?\n",
" 'hello'.upper\n",
" ~~~~~~~~^~~~~",
],
);
}
#[test]
fn attribute_without_call_split() {
assert_err(
"'hello'.split",
&[
"'split' is a method, not a property. Did you mean split()?\n",
" 'hello'.split\n",
" ~~~~~~~~^~~~~",
],
);
}
#[test]
fn property_called_as_method() {
assert_err(
"path('/a/b').stem()",
&[
"'stem' is a property, not a method. Use .stem instead of .stem()\n",
" path('/a/b').stem()\n",
" ~~~~~~~~~~~~~^~~~~~",
],
);
}
#[test]
fn division_by_zero_caret() {
assert_err("10 / 0", &["Division by zero\n", " 10 / 0\n", " ~~~^~~"]);
}
#[test]
fn index_out_of_bounds_caret() {
let mut st = SymbolTable::new();
st.set(
"L",
ExprValue::make_list(
vec![ExprValue::Int(1), ExprValue::Int(2), ExprValue::Int(3)],
ExprType::INT,
)
.unwrap(),
)
.unwrap();
assert_err_with(
"L[10]",
&st,
&[
"Index 10 out of bounds for list of length 3\n",
" L[10]\n",
" ~^~~~",
],
);
}
#[test]
fn operator_error_caret_at_operator() {
assert_err(
"'hello' + 5",
&[
"Cannot use '+' operator with string and int\n",
" 'hello' + 5\n",
" ~~~~~~~~^~~",
],
);
}
#[test]
fn function_call_error_caret() {
assert_err(
"int('bad')",
&[
"Cannot convert 'bad' to int\n",
" int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn method_call_error_caret() {
assert_err(
"'x'.split('')",
&[
"split failed: empty separator\n",
" 'x'.split('')\n",
" ~~~~^~~~~~~~~",
],
);
}
#[test]
fn power_error_caret() {
assert_err(
"0 ** -1",
&[
"Cannot raise zero to a negative power\n",
" 0 ** -1\n",
" ~~^~~~~",
],
);
}
#[test]
fn fail_function_caret() {
assert_err(
"fail('boom')",
&["boom\n", " fail('boom')\n", " ^~~~~~~~~~~~"],
);
}
#[test]
fn leading_whitespace_preserved() {
assert_err(" 1 / 0", &["Division by zero\n", " 1 / 0\n", " ~~^~~"]);
}
#[test]
fn syntax_error_has_message() {
assert_err(
"1 +",
&["Syntax error: Expected an expression\n", " 1 +\n", " ^"],
);
}
#[test]
fn unclosed_paren() {
assert_err(
"(1 + 2",
&[
"Syntax error: unexpected EOF while parsing\n",
" (1 + 2\n",
" ^",
],
);
}
#[test]
fn unclosed_bracket() {
assert_err(
"[1, 2",
&[
"Syntax error: unexpected EOF while parsing\n",
" [1, 2\n",
" ^",
],
);
}
#[test]
fn unclosed_string() {
assert_err(
"'hello",
&[
"Syntax error: missing closing quote in string literal\n",
" 'hello\n",
" ^",
],
);
}
#[test]
fn multiline_error_in_parens() {
assert_err(
"(\n 1 + int('bad')\n)",
&[
"Cannot convert 'bad' to int\n",
" 1 + int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn multiline_error_in_list() {
assert_err(
"[\n 1,\n int('bad'),\n 3\n]",
&[
"Cannot convert 'bad' to int\n",
" int('bad'),\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn multiline_error_on_first_line() {
assert_err(
"(int('bad') +\n 1)",
&[
"Cannot convert 'bad' to int\n",
" (int('bad') +\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn multiline_error_shows_correct_line() {
assert_err(
"(\n 1 +\n int('bad') +\n 3\n)",
&[
"Cannot convert 'bad' to int\n",
" int('bad') +\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn multiline_addition() {
let r = ParsedExpression::new("(\n 1 +\n 2 +\n 3\n)")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "6");
}
#[test]
fn multiline_comparison() {
let r = ParsedExpression::new("(\n 1 <\n 2\n)")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "true");
}
#[test]
fn multiline_three_lines() {
let r = ParsedExpression::new("(\n 'hello' +\n ' ' +\n 'world'\n)")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "hello world");
}
#[test]
fn multiline_error_shows_correct_line_v2() {
assert_err(
"(\n 1 +\n int('bad') +\n 3\n)",
&[
"Cannot convert 'bad' to int\n",
" int('bad') +\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn deeply_nested_multiline() {
assert_err(
"(\n 1 +\n (2 + int('x'))\n)",
&[
"Cannot convert 'x' to int\n",
" (2 + int('x'))\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_parentheses_multiline() {
assert_err(
"(\n 1 + int('bad')\n)",
&[
"Cannot convert 'bad' to int\n",
" 1 + int('bad')\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn error_in_list_multiline() {
assert_err(
"[\n 1,\n int('bad'),\n 3\n]",
&[
"Cannot convert 'bad' to int\n",
" int('bad'),\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn error_on_first_line_multiline() {
assert_err(
"(int('bad') +\n 1)",
&[
"Cannot convert 'bad' to int\n",
" (int('bad') +\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn multiline_addition_works() {
let r = ParsedExpression::new("1 +\n2")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "3");
}
#[test]
fn multiline_comparison_works() {
let r = ParsedExpression::new("1 <\n2")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "true");
}
#[test]
fn multiline_three_lines_works() {
let r = ParsedExpression::new("1 +\n2 +\n3")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "6");
}
fn eval_err_obj(expr: &str) -> ExpressionError {
ParsedExpression::new(expr)
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap_err()
}
fn eval_err_obj_with(expr: &str, st: &SymbolTable) -> ExpressionError {
ParsedExpression::new(expr)
.and_then(|p| p.evaluate(st))
.unwrap_err()
}
#[test]
fn prefix_binop_type_error() {
let st = symtab! { "Param.Frame" => 42 };
let err = eval_err_obj_with("Param.Frame + \"oops\"", &st);
let prefixed = err.message_with_expr_prefix("x = ");
assert!(
prefixed.contains("x = Param.Frame + \"oops\""),
"got:\n{prefixed}"
);
let lines: Vec<&str> = prefixed.lines().collect();
let caret_line = lines.last().unwrap();
let caret_pos = caret_line.find('^').unwrap();
assert_eq!(caret_pos, 18, "caret at wrong position in:\n{prefixed}");
}
#[test]
fn prefix_let_binding_style() {
let err = eval_err_obj("1 / 0");
let prefixed = err.message_with_expr_prefix("result = ");
assert!(prefixed.contains("result = 1 / 0"), "got:\n{prefixed}");
}
#[test]
fn prefix_single_caret() {
let err = eval_err_obj("xyz");
let prefixed = err.message_with_expr_prefix("let v = ");
assert!(prefixed.contains("let v = xyz"), "got:\n{prefixed}");
let lines: Vec<&str> = prefixed.lines().collect();
let caret_line = lines.last().unwrap();
let caret_pos = caret_line.find('^').unwrap();
assert!(caret_pos >= 10, "caret at wrong position in:\n{prefixed}");
}
#[test]
fn prefix_no_context_falls_back() {
let err = ExpressionError::new("bare error");
let prefixed = err.message_with_expr_prefix("ignored = ");
assert_eq!(prefixed, "bare error");
}
#[test]
fn prefix_empty_string() {
let err = eval_err_obj("1 + \"x\"");
let normal = err.to_string();
let prefixed = err.message_with_expr_prefix("");
assert_eq!(prefixed, normal);
}
#[test]
fn prefix_preserves_tilde_span() {
let st = symtab! { "Param.X" => 42 };
let err = eval_err_obj_with("Param.X + \"bad\"", &st);
let prefixed = err.message_with_expr_prefix("val = ");
let lines: Vec<&str> = prefixed.lines().collect();
let caret_line = lines.last().unwrap().trim();
assert!(caret_line.contains('~'), "expected tildes in: {caret_line}");
assert!(caret_line.contains('^'), "expected caret in: {caret_line}");
}
#[test]
fn operator_error_in_middle_with_symtab() {
let st = symtab! { "Param.A" => 5, "Param.B" => "hello" };
assert_err_with(
"1 + (Param.A + Param.B) + 2",
&st,
&[
"Cannot use '+' operator with int and string\n",
" 1 + (Param.A + Param.B) + 2\n",
" ~~~~~~~~^~~~~~~~~",
],
);
}
#[test]
fn index_out_of_bounds_with_symtab() {
let mut st = SymbolTable::new();
st.set(
"Param.List",
ExprValue::make_list(
vec![ExprValue::Int(1), ExprValue::Int(2), ExprValue::Int(3)],
ExprType::INT,
)
.unwrap(),
)
.unwrap();
assert_err_with(
"Param.List[10] + 1",
&st,
&[
"Index 10 out of bounds for list of length 3\n",
" Param.List[10] + 1\n",
" ~~~~~~~~~~^~~~",
],
);
}
#[test]
fn unknown_property_in_chain_with_path_symtab() {
let mut st = SymbolTable::new();
st.set(
"Param.X",
ExprValue::new_path("/test/file.exr", PathFormat::host()),
)
.unwrap();
assert_err_with(
"Param.X.name.unknown",
&st,
&[
"Cannot access attribute 'unknown' on string\n",
" Param.X.name.unknown\n",
" ~~~~~~~~~~~~~^~~~~~~",
],
);
}
#[test]
fn error_in_comprehension_body_python() {
assert_err(
"[x + int('y') for x in [1,2,3]]",
&[
"Cannot convert 'y' to int\n",
" [x + int('y') for x in [1,2,3]]\n",
" ^~~~~~~~",
],
);
}
#[test]
fn error_in_comprehension_filter_python() {
assert_err(
"[x for x in [1,2,3] if int('bad')]",
&[
"Cannot convert 'bad' to int\n",
" [x for x in [1,2,3] if int('bad')]\n",
" ^~~~~~~~~~",
],
);
}
#[test]
fn chained_method_error_python() {
assert_err(
"'a'.upper() + 'b'.split('')[0]",
&[
"split failed: empty separator\n",
" 'a'.upper() + 'b'.split('')[0]\n",
" ~~~~^~~~~~~~~",
],
);
}
#[test]
fn undefined_dotted_variable_with_suggestion() {
let mut st = SymbolTable::new();
st.set(
"Param.InputFile",
ExprValue::new_path("/test/file.exr", PathFormat::Posix),
)
.unwrap();
assert_err_with(
"Param.InputFiel + 1",
&st,
&[
"Undefined variable: 'Param.InputFiel'. Did you mean: Param.InputFile\n",
" Param.InputFiel + 1\n",
" ~~~~~~^~~~~~~~~",
],
);
}
#[test]
fn multiline_type_error_in_parens() {
assert_err(
"(\n 1 + 'x'\n)",
&[
"Cannot use '+' operator with int and string\n",
" 1 + 'x'\n",
" ~~~^~~~",
],
);
}
#[test]
fn multiline_type_error_in_list() {
assert_err(
"[\n 1,\n 2 + 'x',\n 3\n]",
&[
"Cannot use '+' operator with int and string\n",
" 2 + 'x',\n",
" ~~~^~~~",
],
);
}
#[test]
fn multiline_type_error_on_first_line() {
assert_err(
"1 + 'x' + (\n2)",
&[
"Cannot use '+' operator with int and string\n",
" 1 + 'x' + (\n",
" ~~~^~~~",
],
);
}
#[test]
fn multiline_type_error_deeply_nested() {
assert_err(
"(\n [\n 1 + 'x'\n ]\n)",
&[
"Cannot use '+' operator with int and string\n",
" 1 + 'x'\n",
" ~~~^~~~",
],
);
}
#[test]
fn bare_multiline_error_shows_correct_line() {
assert_err(
"1 +\n'x'",
&[
"Cannot use '+' operator with int and string\n",
" 1 +\n",
" ~~^",
],
);
}
#[test]
fn ifelse_both_branches_fail_exact_caret_format() {
let mut st = SymbolTable::new();
st.set("cond", ExprValue::unresolved(ExprType::BOOL))
.unwrap();
st.set("X", ExprValue::unresolved(ExprType::INT)).unwrap();
st.set("Y", ExprValue::unresolved(ExprType::PATH)).unwrap();
let e = eval_err_with("X + 'a' if cond else Y * 'b'", &st);
let expected = "\
Both branches fail in the if/else:
if-branch: Cannot use '+' operator with int and string
X + 'a' if cond else Y * 'b'
~~^~~~~
else-branch: Cannot use '*' operator with path and string
X + 'a' if cond else Y * 'b'
~~^~~~~
X + 'a' if cond else Y * 'b'
^~~~~~~~~~~~~~~~~~~~~~~~~~~~";
assert_eq!(e, expected);
}
#[test]
fn format_string_parse_error_carries_caret() {
let err = FormatString::new("hello {{ 1 + }} world").unwrap_err();
let expected = "\
Failed to parse interpolation expression at [6, 15]. Reason: Syntax error: Expected an expression
hello {{ 1 + }} world
^~~~~~~~~";
assert_eq!(err.to_string(), expected);
}
#[test]
fn format_string_unclosed_interpolation_carries_caret() {
let err = FormatString::new("prefix {{unclosed").unwrap_err();
let expected = "\
Failed to parse interpolation expression at [7, 17]. Reason: Braces mismatch.
prefix {{unclosed
^~";
assert_eq!(err.to_string(), expected);
}
#[test]
fn range_expr_parse_error_carries_caret() {
let err = eval_err("range_expr('1-xx,5')");
let expected = "\
Expected integer in '1-xx,5'
range_expr('1-xx,5')
^~~~~~~~~~~~~~~~~~~~";
assert_eq!(err, expected);
}
#[test]
fn range_expr_parse_error_has_parse_error_kind() {
let err = ParsedExpression::new("range_expr('1-xx,5')")
.and_then(|p| p.evaluate(&SymbolTable::new()))
.unwrap_err();
assert!(
matches!(err.kind(), ExpressionErrorKind::ParseError(_)),
"expected ParseError kind, got {:?}",
err.kind()
);
}
#[test]
fn listcomp_filter_int_is_error() {
assert_err(
"[x for x in [1, 2, 3] if x]",
&[
"List comprehension filter must be a boolean, got int\n",
" [x for x in [1, 2, 3] if x]\n",
" ^",
],
);
}
#[test]
fn listcomp_filter_string_is_error() {
assert_err(
"[x for x in ['a', 'b', 'c'] if x]",
&[
"List comprehension filter must be a boolean, got string\n",
" [x for x in ['a', 'b', 'c'] if x]\n",
" ^",
],
);
}
#[test]
fn listcomp_filter_float_is_error() {
assert_err(
"[x for x in [1.0, 2.0] if x]",
&[
"List comprehension filter must be a boolean, got float\n",
" [x for x in [1.0, 2.0] if x]\n",
" ^",
],
);
}
#[test]
fn listcomp_filter_bool_happy_path() {
let r = openjd_expr::ParsedExpression::new("[x for x in [1, 2, 3] if x > 1]")
.and_then(|p| p.evaluate(&openjd_expr::SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "[2, 3]");
}
#[test]
fn listcomp_no_filter_happy_path() {
let r = openjd_expr::ParsedExpression::new("[x for x in [1, 2, 3]]")
.and_then(|p| p.evaluate(&openjd_expr::SymbolTable::new()))
.unwrap();
assert_eq!(r.to_display_string(), "[1, 2, 3]");
}
#[test]
fn ifelse_both_branches_fail_has_sub_errors() {
let mut st = SymbolTable::new();
st.set("cond", ExprValue::unresolved(ExprType::BOOL))
.unwrap();
st.set("X", ExprValue::unresolved(ExprType::INT)).unwrap();
st.set("Y", ExprValue::unresolved(ExprType::PATH)).unwrap();
let err = ParsedExpression::new("X + 'a' if cond else Y * 'b'")
.and_then(|p| p.evaluate(&st))
.unwrap_err();
assert_eq!(err.sub_errors().len(), 2);
let if_err = &err.sub_errors()[0];
assert_eq!(
if_err.message(),
"Cannot use '+' operator with int and string"
);
assert!(if_err.col_offset().is_some());
assert!(if_err.end_col_offset().is_some());
let else_err = &err.sub_errors()[1];
assert_eq!(
else_err.message(),
"Cannot use '*' operator with path and string"
);
assert!(else_err.col_offset().is_some());
assert!(else_err.end_col_offset().is_some());
}
#[test]
fn format_string_validation_carries_expression_error_with_sub_errors() {
let mut st = SymbolTable::new();
st.set("cond", ExprValue::unresolved(ExprType::BOOL))
.unwrap();
st.set("X", ExprValue::unresolved(ExprType::INT)).unwrap();
st.set("Y", ExprValue::unresolved(ExprType::PATH)).unwrap();
let fs = FormatString::new("{{X + 'a' if cond else Y * 'b'}}").unwrap();
let lib = FunctionLibrary::default();
let err = fs.validate_expressions(&st, &lib).unwrap_err();
assert_eq!(err.start, 0);
assert_eq!(err.end, 32);
let expr_err = err
.expression_error
.as_ref()
.expect("expression_error should be Some");
assert_eq!(expr_err.sub_errors().len(), 2);
assert!(!expr_err.sub_errors()[0].message().is_empty());
assert!(!expr_err.sub_errors()[1].message().is_empty());
assert!(expr_err.sub_errors()[0].col_offset().is_some());
assert!(expr_err.sub_errors()[1].col_offset().is_some());
}
#[test]
fn simple_error_has_no_sub_errors() {
let st = SymbolTable::new();
let err = ParsedExpression::new("Param.Undefined")
.and_then(|p| p.evaluate(&st))
.unwrap_err();
assert!(err.sub_errors().is_empty());
}