Add end-to-end integration tests
36 tests covering the full lexer → parser → evaluator pipeline:
- Basic arithmetic (single numbers, +, -, *, /)
- Operator precedence (* / before + -)
- Left associativity for subtraction and division
- Parentheses (simple, nested, deeply nested, both sides, complex)
- Complex multi-operator expressions
- Long chained expressions
- Floating point (arithmetic, division, precision)
- Whitespace variations (none, extra, tabs)
- Error cases (div by zero, invalid chars, mismatched parens,
empty parens, trailing/leading/consecutive operators, empty input,
consecutive numbers)
diff --git a/integration_test.go b/integration_test.go
new file mode 100644
index 0000000..31ddd3f
--- /dev/null
+++ b/integration_test.go
@@ -0,0 +1,261 @@
+package matheval_test
+
+import (
+ "math"
+ "strings"
+ "testing"
+
+ "matheval/evaluator"
+ "matheval/lexer"
+ "matheval/parser"
+)
+
+// eval is a helper that runs the full pipeline: lexer → parser → evaluator.
+func eval(t *testing.T, input string) float64 {
+ t.Helper()
+ tokens, err := lexer.Tokenize(input)
+ if err != nil {
+ t.Fatalf("lexer error for %q: %v", input, err)
+ }
+ tree, err := parser.Parse(tokens)
+ if err != nil {
+ t.Fatalf("parser error for %q: %v", input, err)
+ }
+ result, err := evaluator.Eval(tree)
+ if err != nil {
+ t.Fatalf("evaluator error for %q: %v", input, err)
+ }
+ return result
+}
+
+// evalErr is a helper that expects the full pipeline to return an error.
+func evalErr(t *testing.T, input string) error {
+ t.Helper()
+ tokens, err := lexer.Tokenize(input)
+ if err != nil {
+ return err
+ }
+ tree, err := parser.Parse(tokens)
+ if err != nil {
+ return err
+ }
+ _, err = evaluator.Eval(tree)
+ return err
+}
+
+func assertApprox(t *testing.T, input string, expected, got float64) {
+ t.Helper()
+ if math.Abs(expected-got) > 1e-9 {
+ t.Errorf("%q: expected %v, got %v", input, expected, got)
+ }
+}
+
+// --- Basic arithmetic ---
+
+func TestIntegration_SingleNumber(t *testing.T) {
+ assertApprox(t, "42", 42, eval(t, "42"))
+}
+
+func TestIntegration_DecimalNumber(t *testing.T) {
+ assertApprox(t, "3.14", 3.14, eval(t, "3.14"))
+}
+
+func TestIntegration_LeadingDot(t *testing.T) {
+ assertApprox(t, ".5", 0.5, eval(t, ".5"))
+}
+
+func TestIntegration_Addition(t *testing.T) {
+ assertApprox(t, "1 + 2", 3, eval(t, "1 + 2"))
+}
+
+func TestIntegration_Subtraction(t *testing.T) {
+ assertApprox(t, "10 - 4", 6, eval(t, "10 - 4"))
+}
+
+func TestIntegration_Multiplication(t *testing.T) {
+ assertApprox(t, "3 * 7", 21, eval(t, "3 * 7"))
+}
+
+func TestIntegration_Division(t *testing.T) {
+ assertApprox(t, "10 / 4", 2.5, eval(t, "10 / 4"))
+}
+
+// --- Precedence and associativity ---
+
+func TestIntegration_PrecedenceMulOverAdd(t *testing.T) {
+ // 2 + 3 * 4 = 2 + 12 = 14
+ assertApprox(t, "2 + 3 * 4", 14, eval(t, "2 + 3 * 4"))
+}
+
+func TestIntegration_PrecedenceDivOverSub(t *testing.T) {
+ // 10 - 6 / 3 = 10 - 2 = 8
+ assertApprox(t, "10 - 6 / 3", 8, eval(t, "10 - 6 / 3"))
+}
+
+func TestIntegration_LeftAssociativitySub(t *testing.T) {
+ // 10 - 3 - 2 = (10 - 3) - 2 = 5
+ assertApprox(t, "10 - 3 - 2", 5, eval(t, "10 - 3 - 2"))
+}
+
+func TestIntegration_LeftAssociativityDiv(t *testing.T) {
+ // 24 / 4 / 3 = (24 / 4) / 3 = 2
+ assertApprox(t, "24 / 4 / 3", 2, eval(t, "24 / 4 / 3"))
+}
+
+// --- Parentheses ---
+
+func TestIntegration_ParensOverridePrecedence(t *testing.T) {
+ // (2 + 3) * 4 = 20
+ assertApprox(t, "(2 + 3) * 4", 20, eval(t, "(2 + 3) * 4"))
+}
+
+func TestIntegration_NestedParens(t *testing.T) {
+ // ((1 + 2)) = 3
+ assertApprox(t, "((1 + 2))", 3, eval(t, "((1 + 2))"))
+}
+
+func TestIntegration_DeeplyNestedParens(t *testing.T) {
+ // ((((((1 + 2)))))) = 3
+ assertApprox(t, "((((((1 + 2))))))", 3, eval(t, "((((((1 + 2))))))"))
+}
+
+func TestIntegration_ParensOnBothSides(t *testing.T) {
+ // (1 + 2) * (3 + 4) = 3 * 7 = 21
+ assertApprox(t, "(1 + 2) * (3 + 4)", 21, eval(t, "(1 + 2) * (3 + 4)"))
+}
+
+func TestIntegration_ParensNestedComplex(t *testing.T) {
+ // ((2 + 3) * (4 - 1)) / 5 = (5 * 3) / 5 = 3
+ assertApprox(t, "((2 + 3) * (4 - 1)) / 5", 3, eval(t, "((2 + 3) * (4 - 1)) / 5"))
+}
+
+// --- Complex expressions ---
+
+func TestIntegration_AllOperators(t *testing.T) {
+ // 1 + 2 * 3 - 4 / 2 = 1 + 6 - 2 = 5
+ assertApprox(t, "1 + 2 * 3 - 4 / 2", 5, eval(t, "1 + 2 * 3 - 4 / 2"))
+}
+
+func TestIntegration_LongChainedAddition(t *testing.T) {
+ // 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55
+ assertApprox(t, "1+2+3+4+5+6+7+8+9+10", 55, eval(t, "1+2+3+4+5+6+7+8+9+10"))
+}
+
+func TestIntegration_LongChainedMixed(t *testing.T) {
+ // 2 * 3 + 4 * 5 - 6 / 2 + 1 = 6 + 20 - 3 + 1 = 24
+ assertApprox(t, "2 * 3 + 4 * 5 - 6 / 2 + 1", 24, eval(t, "2 * 3 + 4 * 5 - 6 / 2 + 1"))
+}
+
+// --- Floating point ---
+
+func TestIntegration_FloatArithmetic(t *testing.T) {
+ // 1.5 + 2.5 = 4.0
+ assertApprox(t, "1.5 + 2.5", 4.0, eval(t, "1.5 + 2.5"))
+}
+
+func TestIntegration_FloatDivision(t *testing.T) {
+ // 7 / 2 = 3.5
+ assertApprox(t, "7 / 2", 3.5, eval(t, "7 / 2"))
+}
+
+func TestIntegration_FloatPrecision(t *testing.T) {
+ // 0.1 + 0.2 ≈ 0.3 (within tolerance)
+ assertApprox(t, "0.1 + 0.2", 0.3, eval(t, "0.1 + 0.2"))
+}
+
+// --- Whitespace variations ---
+
+func TestIntegration_NoSpaces(t *testing.T) {
+ assertApprox(t, "1+2*3", 7, eval(t, "1+2*3"))
+}
+
+func TestIntegration_ExtraSpaces(t *testing.T) {
+ assertApprox(t, " 1 + 2 ", 3, eval(t, " 1 + 2 "))
+}
+
+func TestIntegration_TabsAndSpaces(t *testing.T) {
+ assertApprox(t, "1\t+\t2", 3, eval(t, "1\t+\t2"))
+}
+
+// --- Error cases ---
+
+func TestIntegration_DivisionByZero(t *testing.T) {
+ err := evalErr(t, "1 / 0")
+ if err == nil {
+ t.Fatal("expected division by zero error")
+ }
+ if !strings.Contains(err.Error(), "division by zero") {
+ t.Errorf("expected 'division by zero' in error, got: %v", err)
+ }
+}
+
+func TestIntegration_DivisionByZeroInSubExpr(t *testing.T) {
+ err := evalErr(t, "1 + 2 / 0")
+ if err == nil {
+ t.Fatal("expected division by zero error")
+ }
+}
+
+func TestIntegration_InvalidCharacter(t *testing.T) {
+ err := evalErr(t, "1 @ 2")
+ if err == nil {
+ t.Fatal("expected error for invalid character")
+ }
+}
+
+func TestIntegration_MismatchedParenLeft(t *testing.T) {
+ err := evalErr(t, "(1 + 2")
+ if err == nil {
+ t.Fatal("expected error for missing closing paren")
+ }
+}
+
+func TestIntegration_MismatchedParenRight(t *testing.T) {
+ err := evalErr(t, "1 + 2)")
+ if err == nil {
+ t.Fatal("expected error for unexpected closing paren")
+ }
+}
+
+func TestIntegration_EmptyParens(t *testing.T) {
+ err := evalErr(t, "()")
+ if err == nil {
+ t.Fatal("expected error for empty parentheses")
+ }
+}
+
+func TestIntegration_TrailingOperator(t *testing.T) {
+ err := evalErr(t, "1 +")
+ if err == nil {
+ t.Fatal("expected error for trailing operator")
+ }
+}
+
+func TestIntegration_LeadingOperator(t *testing.T) {
+ err := evalErr(t, "* 1")
+ if err == nil {
+ t.Fatal("expected error for leading operator")
+ }
+}
+
+func TestIntegration_ConsecutiveOperators(t *testing.T) {
+ err := evalErr(t, "1 + * 2")
+ if err == nil {
+ t.Fatal("expected error for consecutive operators")
+ }
+}
+
+func TestIntegration_EmptyInput(t *testing.T) {
+ // Empty string should produce only EOF, parser should error
+ err := evalErr(t, "")
+ if err == nil {
+ t.Fatal("expected error for empty input")
+ }
+}
+
+func TestIntegration_ConsecutiveNumbers(t *testing.T) {
+ err := evalErr(t, "1 2")
+ if err == nil {
+ t.Fatal("expected error for consecutive numbers without operator")
+ }
+}