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)
This commit is contained in:
Edward Langley
2026-04-08 23:12:31 -07:00
parent 5f71321980
commit 96a4cda368

View File

@ -458,4 +458,197 @@ mod tests {
fn parse_missing_equals_returns_error() { fn parse_missing_equals_returns_error() {
assert!(parse_formula("BadFormula Revenue Cost", "Cat").is_err()); 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");
}
} }