package calc_test import ( "errors" "strings" "testing" "sneak.berlin/go/simplexcalc/internal/calc" ) // TestEvaluate covers the two examples the bot was specified with, and // the arithmetic around them. func TestEvaluate(t *testing.T) { t.Parallel() cases := map[string]string{ // The specification's own examples. "2 + 2": "4", "5 * 5/2": "12.5", "2+2": "4", " 7 - 10 \n": "-3", "-3 * 2": "-6", "+4": "4", "-(-4)": "4", "(1 + 2) * 3": "9", "1 + 2 * 3": "7", "((2))": "2", "8 / 2 / 2": "2", "10 - 2 - 3": "5", "7 / 2": "3.5", "25/2": "12.5", "1 / 3": "0.3333333333333333", "2 / 3": "0.6666666666666666", "0.1 + 0.2": "0.3", "1.5 * 2": "3", "2.50 * 2": "5", ".5 + .5": "1", "3. * 2": "6", "1e3 + 1": "1001", "2.5e-1": "0.25", "010 + 1": "11", "-0": "0", "0 / 5": "0", // Exact: a float64 would print 99999999980000000000. "9999999999 * 9999999999": "99999999980000000001", "1e21": "1e+21", "1e20": "100000000000000000000", "1 / 1e7": "1e-07", "1 / 1e6": "0.000001", "1234567.5": "1234567.5", "-1 / 4": "-0.25", "1e300 * 1e8": "1e+308", } for in, want := range cases { t.Run(in, func(t *testing.T) { t.Parallel() got, err := calc.Evaluate(in) if err != nil { t.Fatalf("Evaluate(%q) failed: %v", in, err) } if got != want { t.Errorf("Evaluate(%q) = %q, want %q", in, got, want) } }) } } // TestEvaluateRefuses covers what must be answered with an error rather // than a number, and never with a panic. func TestEvaluateRefuses(t *testing.T) { t.Parallel() cases := map[string]error{ "": calc.ErrNotArithmetic, " ": calc.ErrNotArithmetic, "hello": calc.ErrNotArithmetic, "/help": calc.ErrNotArithmetic, "2 +": calc.ErrNotArithmetic, "2 2": calc.ErrNotArithmetic, "2 + 2 =": calc.ErrNotArithmetic, "x + 1": calc.ErrNotArithmetic, "len(\"abc\")": calc.ErrNotArithmetic, "\"a\" + \"b\"": calc.ErrNotArithmetic, "'a' + 1": calc.ErrNotArithmetic, "2i * 2i": calc.ErrNotArithmetic, "0x10 + 1": calc.ErrNotArithmetic, "1_000 + 1": calc.ErrNotArithmetic, "7 % 2": calc.ErrNotArithmetic, "2 ^ 3": calc.ErrNotArithmetic, "1 << 10": calc.ErrNotArithmetic, "1 == 1": calc.ErrNotArithmetic, "!1": calc.ErrNotArithmetic, "func() int { return 1 }()": calc.ErrNotArithmetic, "1 / 0": calc.ErrDivisionByZero, "1 / (2 - 2)": calc.ErrDivisionByZero, "5 / 0.0": calc.ErrDivisionByZero, "1e400": calc.ErrTooLarge, "1e300 * 1e300": calc.ErrTooLarge, "1e999999999 * 1e999999999": calc.ErrTooLarge, "1 / 1e-400": calc.ErrTooLarge, } for in, want := range cases { t.Run(in, func(t *testing.T) { t.Parallel() got, err := calc.Evaluate(in) if !errors.Is(err, want) { t.Errorf("Evaluate(%q) = %q, %v; want error %v", in, got, err, want) } }) } } // TestEvaluateCapsInput: the length cap is what bounds the work a // message can cause, so it must hold exactly at the boundary. func TestEvaluateCapsInput(t *testing.T) { t.Parallel() // "1+1+...+1" with the last term padded to land exactly on the cap. longest := strings.Repeat("1+", calc.MaxInputLength/2-1) + "10" if len(longest) != calc.MaxInputLength { t.Fatalf("test setup: expression is %d bytes, want %d", len(longest), calc.MaxInputLength) } got, err := calc.Evaluate(longest) if err != nil { t.Fatalf("an expression of exactly MaxInputLength bytes was refused: %v", err) } if want := "137"; got != want { t.Errorf("Evaluate(longest) = %q, want %q", got, want) } _, err = calc.Evaluate(longest + "0") if !errors.Is(err, calc.ErrTooLong) { t.Errorf("an expression over MaxInputLength gave %v, want ErrTooLong", err) } // Surrounding whitespace is not part of the expression. _, err = calc.Evaluate(" " + longest + "\n") if err != nil { t.Errorf("whitespace around a maximal expression counted against the cap: %v", err) } }