initial commit [141.14.140.180,mike]
This commit is contained in:
+295
@@ -0,0 +1,295 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user