package main import ( "encoding/json" "math" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/shopspring/decimal" ) func TestEvaluateExpr(t *testing.T) { initUnits() tests := []struct { expr string wantVal float64 wantErr bool }{ // Basic arithmetic {"2 + 3", 5, false}, {"2 - 3", -1, false}, {"2 * 3", 6, false}, {"6 / 3", 2, false}, {"2 * 3 + 4", 10, false}, {"2 + 3 * 4", 14, false}, {"(2 + 3) * 4", 20, false}, {"-5 + 10", 5, false}, {"-(5 + 10)", -15, false}, // Division by zero {"6 / 0", 0, true}, // Hex, binary, and octal literals {"0xFF", 255, false}, {"0b1010", 10, false}, {"0o77", 63, false}, {"0XF", 15, false}, {"0B11", 3, false}, {"0O10", 8, false}, // Exponentiation (**) {"2 ** 3", 8, false}, {"2 ** 3 ** 2", 512, false}, {"2 * 3 ** 2", 18, false}, {"3 ** 2 * 2", 18, false}, // Scientific functions {"abs(-5)", 5, false}, {"sqrt(9)", 3, false}, {"ceil(4.2)", 5, false}, {"floor(4.8)", 4, false}, {"round(4.5)", 5, false}, {"sin(0)", 0, false}, {"cos(0)", 1, false}, {"tan(0)", 0, false}, {"asin(0)", 0, false}, {"acos(1)", 0, false}, {"atan(0)", 0, false}, {"sinh(0)", 0, false}, {"cosh(0)", 1, false}, {"tanh(0)", 0, false}, {"ln(E)", 1, false}, {"log(10)", 1, false}, {"log2(8)", 3, false}, {"exp(1)", math.E, false}, {"pow(2, 3)", 8, false}, {"pow(2 + 1, 3)", 27, false}, {"min(5, 2, 7)", 2, false}, {"max(1, 8, 3)", 8, false}, {"mod(10, 3)", 1, false}, {"fact(5)", 120, false}, {"fact(0)", 1, false}, // Function error cases {"fact(-1)", 0, true}, {"fact(3.5)", 0, true}, // Constants {"PI", math.Pi, false}, {"E", math.E, false}, // Bitwise {"5 & 3", 1, false}, // 101 & 011 = 001 {"5 | 3", 7, false}, // 101 | 011 = 111 {"5 ^ 3", 6, false}, // 101 ^ 011 = 110 {"~5", -6, false}, {"1 << 8", 256, false}, {"1024 >> 2", 256, false}, {"1 << 2 + 3", 32, false}, // precedence test: 1 << (2 + 3) {"1 << -1", 0, true}, // error: negative shift count // Modulo operator {"10 % 3", 1, false}, {"10 % 0", 0, true}, // Implicit multiplication with units and parentheses {"2m", 2, false}, {"2 m", 2, false}, {"2km", 2000, false}, {"2(3 + 4)", 14, false}, {"(2 + 3)(4 + 5)", 45, false}, } for _, tt := range tests { res, err := evaluateExpr(tt.expr) if (err != nil) != tt.wantErr { t.Errorf("evaluateExpr(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr) continue } if !tt.wantErr { f, _ := res.Value.Float64() if math.Abs(f-tt.wantVal) > 1e-9 { t.Errorf("evaluateExpr(%q) = %v, want %v", tt.expr, f, tt.wantVal) } } } } func TestEvaluateConversions(t *testing.T) { initUnits() unitRegistry["USD"] = UnitInfo{Factor: decimal.NewFromFloat(0.85), Dimension: DimCurrency, Label: "USD"} unitRegistry["EUR"] = UnitInfo{Factor: decimal.NewFromFloat(1.0), Dimension: DimCurrency, Label: "EUR"} tests := []struct { expr string wantVal float64 wantUnit string wantErr bool }{ // Unit conversions (Length) {"10 mi to km", 16.09344, "KM", false}, {"10 dm to m", 1, "M", false}, {"1e9 nm to m", 1, "M", false}, {"1000 um to mm", 1, "MM", false}, {"1 nmi to m", 1852, "M", false}, // Unit conversions (Mass) {"1000g in kg", 1, "KG", false}, {"1 t to kg", 1000, "KG", false}, {"5 ct to g", 1, "G", false}, // Unit conversions (Time) {"1h to min", 60, "MIN", false}, {"1e9 ns to s", 1, "S", false}, {"1 wk to d", 7, "D", false}, {"1 yr to d", 365, "D", false}, // Unit conversions (Digital) {"1 GB in MB", 1024, "MB", false}, {"1 PB to TB", 1024, "TB", false}, {"1 EB to PB", 1024, "PB", false}, // Unit conversions (Area) {"1 ha to m2", 10000, "M2", false}, {"10000 m2 in ha", 1, "HA", false}, // Currency conversions {"10 USD to EUR", 8.5, "EUR", false}, {"10$ to EUR", 8.5, "EUR", false}, {"10 USD to €", 8.5, "€", false}, {"10$ to €", 8.5, "€", false}, // Temperature conversions {"0 C to K", 273.15, "K", false}, {"100 C in F", 212, "F", false}, {"32 F to C", 0, "C", false}, {"293.15 K to C", 20, "C", false}, {"20 C + 5 K", 298.15, "", false}, // evaluates internally to 298.15 K {"20 CELSIUS to KELVIN", 293.15, "KELVIN", false}, // Angle conversions & trigonometry {"90 deg to rad", math.Pi / 2, "RAD", false}, {"180 deg in rad", math.Pi, "RAD", false}, {"pi rad to deg", 180, "DEG", false}, {"sin(90 deg)", 1, "", false}, {"cos(pi rad)", -1, "", false}, {"asin(1) to deg", 90, "DEG", false}, {"asin(0.5) to deg", 30, "DEG", false}, // Error cases {"10m to kg", 0, "", true}, {"10 USD to m", 0, "", true}, {"10m + 5kg", 0, "", true}, {"20 C to m", 0, "", true}, {"10m / 2s", 0, "", true}, {"10 / (2m)", 0, "", true}, {"10m % 3", 0, "", true}, {"sin(10m)", 0, "", true}, {"asin(10m)", 0, "", true}, } for _, tt := range tests { res, target, err := evaluate(tt.expr) if (err != nil) != tt.wantErr { t.Errorf("evaluate(%q) error = %v, wantErr %v", tt.expr, err, tt.wantErr) continue } if !tt.wantErr { if target != tt.wantUnit { t.Errorf("evaluate(%q) target unit = %q, want %q", tt.expr, target, tt.wantUnit) } f, _ := res.Value.Float64() if math.Abs(f-tt.wantVal) > 1e-9 { t.Errorf("evaluate(%q) = %v, want %v", tt.expr, f, tt.wantVal) } } } } func TestVariableAssignments(t *testing.T) { initUnits() variables = make(map[string]Result) variables["x"] = Result{d(5), DimNone} res, err := evaluateExpr("x * 2") if err != nil { t.Fatalf("unexpected error: %v", err) } if !res.Value.Equal(d(10)) { t.Errorf("x * 2 = %v, want 10", res.Value) } variables["y"] = Result{d(10), DimLength} val, target, err := evaluate("y to cm") if err != nil { t.Fatalf("unexpected error: %v", err) } if target != "CM" || !val.Value.Equal(d(1000)) { t.Errorf("evaluate('y to cm') = %v %s, want 1000 CM", val.Value, target) } } func TestFetchRatesMock(t *testing.T) { initUnits() mockResponse := struct { Rates map[string]float64 `json:"rates"` }{ Rates: map[string]float64{ "USD": 1.1, "GBP": 0.85, }, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(mockResponse) })) defer server.Close() oldRatesAPIURL := ratesAPIURL ratesAPIURL = server.URL defer func() { ratesAPIURL = oldRatesAPIURL }() delete(unitRegistry, "USD") delete(unitRegistry, "GBP") // remove old cache file to force fetch home, _ := os.UserHomeDir() os.Remove(filepath.Join(home, ".goca_rates.json")) err := fetchRates() if err != nil { t.Fatalf("fetchRates failed: %v", err) } usd, ok := unitRegistry["USD"] if !ok { t.Fatalf("USD was not added to unitRegistry") } expectedUSD := d(1.0).Div(d(1.1)) if !usd.Factor.Equal(expectedUSD) { t.Errorf("USD factor = %v, want %v", usd.Factor, expectedUSD) } } func FuzzEvaluateExpr(f *testing.F) { initUnits() // Add seed corpus f.Add("2 + 3") f.Add("sin(pi rad)") f.Add("10 km to mi") f.Add("0xFF + 0b10") f.Add("x = 5") f.Add("1 << 4") f.Add("((2+3)*4)/5") f.Fuzz(func(t *testing.T, expr string) { // Fuzzing ensures no panics occur on malformed input evaluate(expr) }) }