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:
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user