Files
goca/main_test.go
T

296 lines
7.2 KiB
Go

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)
})
}