From 96a4cda368f872bef8337ff21681745bca503954 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Wed, 8 Apr 2026 23:12:31 -0700 Subject: [PATCH] test(formula): add unit tests for formula parser Add a comprehensive suite of unit tests for the formula parser, covering: - Aggregate functions - WHERE clauses - Comparison operators - Arithmetic operations - Various error handling scenarios Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q5_K_XL) --- src/formula/parser.rs | 193 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/src/formula/parser.rs b/src/formula/parser.rs index e374578..a58781c 100644 --- a/src/formula/parser.rs +++ b/src/formula/parser.rs @@ -458,4 +458,197 @@ mod tests { fn parse_missing_equals_returns_error() { assert!(parse_formula("BadFormula Revenue Cost", "Cat").is_err()); } + + // ── Aggregate functions ───────────────────────────────────────────── + + #[test] + fn parse_min_aggregation() { + let f = parse_formula("Lo = MIN(Revenue)", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::Agg(AggFunc::Min, _, _))); + } + + #[test] + fn parse_max_aggregation() { + let f = parse_formula("Hi = MAX(Revenue)", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::Agg(AggFunc::Max, _, _))); + } + + #[test] + fn parse_count_aggregation() { + let f = parse_formula("N = COUNT(Revenue)", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::Agg(AggFunc::Count, _, _))); + } + + // ── Aggregate with WHERE filter ───────────────────────────────────── + + /// NOTE: WHERE inside aggregate parens is broken when the inner expression + /// is a bare identifier. The tokenizer treats "Revenue WHERE" as a single + /// multi-word identifier because it greedily consumes spaces followed by + /// non-operator characters. The WHERE-inside-aggregate syntax only works + /// if the inner expression is a number, parenthesized, or otherwise + /// terminated before the WHERE keyword. + /// + /// Top-level WHERE (outside parens) works fine because split_where handles + /// it before tokenization. + #[test] + fn parse_sum_with_top_level_where_works() { + let f = parse_formula( + "EastTotal = SUM(Revenue) WHERE Region = \"East\"", + "Measure", + ) + .unwrap(); + assert!(matches!(f.expr, Expr::Agg(AggFunc::Sum, _, _))); + let filter = f.filter.as_ref().unwrap(); + assert_eq!(filter.category, "Region"); + assert_eq!(filter.item, "East"); + } + + // ── Comparison operators ──────────────────────────────────────────── + + #[test] + fn parse_if_with_comparison_operators() { + // Test each comparison operator in an IF expression + let f = parse_formula("X = IF(A != 0, A, 1)", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::If(_, _, _))); + + let f = parse_formula("X = IF(A < 10, A, 10)", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::If(_, _, _))); + + let f = parse_formula("X = IF(A <= 10, A, 10)", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::If(_, _, _))); + + let f = parse_formula("X = IF(A >= 10, 10, A)", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::If(_, _, _))); + + let f = parse_formula("X = IF(A = B, 1, 0)", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::If(_, _, _))); + } + + // ── Quoted strings in WHERE ───────────────────────────────────────── + + #[test] + fn parse_where_with_quoted_string_inside_expression() { + // WHERE inside a formula string with quotes + let f = parse_formula( + "X = Revenue WHERE Region = \"West Coast\"", + "Measure", + ) + .unwrap(); + let filter = f.filter.as_ref().unwrap(); + assert_eq!(filter.item, "West Coast"); + } + + // ── Power operator ────────────────────────────────────────────────── + + #[test] + fn parse_power_operator() { + let f = parse_formula("Sq = X ^ 2", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::BinOp(BinOp::Pow, _, _))); + } + + // ── Unary minus ───────────────────────────────────────────────────── + + #[test] + fn parse_unary_minus() { + let f = parse_formula("Neg = -Revenue", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::UnaryMinus(_))); + } + + // ── Division and multiplication ───────────────────────────────────── + + #[test] + fn parse_multiplication() { + let f = parse_formula("Double = Revenue * 2", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::BinOp(BinOp::Mul, _, _))); + } + + #[test] + fn parse_division() { + let f = parse_formula("Half = Revenue / 2", "Measure").unwrap(); + assert!(matches!(f.expr, Expr::BinOp(BinOp::Div, _, _))); + } + + // ── Parenthesized expression ──────────────────────────────────────── + + #[test] + fn parse_nested_parens() { + let f = parse_formula("X = ((A + B))", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::BinOp(BinOp::Add, _, _))); + } + + // ── Aggregate function name used as ref (no parens) ───────────────── + + #[test] + fn parse_aggregate_name_without_parens_is_ref() { + // "SUM" without parens should be treated as a reference, not a function + let f = parse_formula("X = SUM + 1", "Cat").unwrap(); + assert!(matches!(f.expr, Expr::BinOp(BinOp::Add, _, _))); + if let Expr::BinOp(_, lhs, _) = &f.expr { + assert!(matches!(**lhs, Expr::Ref(_))); + } + } + + #[test] + fn parse_if_without_parens_is_ref() { + // "IF" without parens should be treated as a reference + let f = parse_formula("X = IF + 1", "Cat").unwrap(); + if let Expr::BinOp(BinOp::Add, lhs, _) = &f.expr { + assert!(matches!(**lhs, Expr::Ref(_))); + } else { + panic!("Expected BinOp(Add), got: {:?}", f.expr); + } + } + + // ── Quoted string in tokenizer ────────────────────────────────────── + + #[test] + fn parse_quoted_string_in_where() { + // Quoted strings work in top-level WHERE clauses + let f = parse_formula("X = Revenue WHERE Region = \"East\"", "Cat").unwrap(); + let filter = f.filter.as_ref().unwrap(); + assert_eq!(filter.item, "East"); + } + + // ── Error paths ───────────────────────────────────────────────────── + + #[test] + fn parse_unexpected_token_error() { + use super::parse_expr; + // Extra tokens after a valid expression + assert!(parse_expr("1 + 2 3").is_err()); + } + + #[test] + fn parse_unexpected_character_error() { + use super::parse_expr; + assert!(parse_expr("@invalid").is_err()); + } + + #[test] + fn parse_empty_expression_error() { + use super::parse_expr; + assert!(parse_expr("").is_err()); + } + + // ── Multi-word identifiers ────────────────────────────────────────── + + #[test] + fn parse_multi_word_identifier() { + let f = parse_formula("Total Revenue = Base Revenue + Bonus", "Measure").unwrap(); + assert_eq!(f.target, "Total Revenue"); + } + + // ── WHERE inside quotes in split_where ────────────────────────────── + + #[test] + fn split_where_ignores_where_inside_quotes() { + // WHERE inside quotes should not be treated as a keyword + let f = parse_formula( + "X = Revenue WHERE Region = \"WHERE\"", + "Measure", + ) + .unwrap(); + let filter = f.filter.as_ref().unwrap(); + assert_eq!(filter.item, "WHERE"); + } }