From c22cd367c9b6edc9744701fe7f912de487ec0953 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 22 May 2026 12:46:49 +0300 Subject: fix(rpn): guard resultMetricForHyperAdd against empty slice; add missing hyper metric tests - resultMetricForHyperAdd: handle empty/nil metrics defensively (returns Cool metric instead of potential panic) - Add tests for Cool absorbing in HyperSubtract, negative result with metric, and result metric assertion in mixed units --- internal/rpn/operations_hyper.go | 9 +++++- internal/rpn/operations_hyper_test.go | 55 +++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/internal/rpn/operations_hyper.go b/internal/rpn/operations_hyper.go index b0bb24b..b261fd2 100644 --- a/internal/rpn/operations_hyper.go +++ b/internal/rpn/operations_hyper.go @@ -303,7 +303,14 @@ func resultMetricForHyperAdd(metrics []*Metric) *Metric { return m } } - return metrics[0] + // All Universal (Cool) or empty slice — default to Cool. + // In practice, metrics is never empty (popAll enforces >= 2 operands), + // but we handle it defensively. + if len(metrics) > 0 && metrics[0] != nil { + return metrics[0] + } + m, _ := GetMetricRegistry().Find("Cool") + return m } // validateSameCategory checks that all metrics belong to the same category. diff --git a/internal/rpn/operations_hyper_test.go b/internal/rpn/operations_hyper_test.go index 700614f..d6f8205 100644 --- a/internal/rpn/operations_hyper_test.go +++ b/internal/rpn/operations_hyper_test.go @@ -157,6 +157,7 @@ func TestHyperSubtractMetricAware(t *testing.T) { } func TestHyperSubtractMixedUnits(t *testing.T) { + reg := GetMetricRegistry() tolerance := 0.001 // 2km 500m 100m [-] = (2000 - 500 - 100)m = 1400m = 1.4km @@ -170,6 +171,57 @@ func TestHyperSubtractMixedUnits(t *testing.T) { if resultVal < 1.4-tolerance || resultVal > 1.4+tolerance { t.Errorf("result = %g, want 1.4", resultVal) } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + km, _ := reg.Find("km") + if m != km { + t.Errorf("metric = %v, want km", m) + } +} + +func TestHyperSubtractCoolAbsorbing(t *testing.T) { + reg := GetMetricRegistry() + + // 100km 5 [-] = 100km - 5m = 99.995km + // Cool (factor 1.0) contributes 5 base units (meters) = 0.005km + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("100km 5 [-]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < 99.99 || resultVal > 99.996 { + t.Errorf("result = %g, want ~99.995", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + km, _ := reg.Find("km") + if m != km { + t.Errorf("metric = %v, want km", m) + } +} + +func TestHyperSubtractNegativeResult(t *testing.T) { + reg := GetMetricRegistry() + + // 1km 2km [-] = -1km + vars := NewVariables() + rpn := NewRPN(vars) + result, err := rpn.ParseAndEvaluate("1km 2km [-]") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + resultVal, _ := strconv.ParseFloat(result, 64) + if resultVal < -1.1 || resultVal > -0.9 { + t.Errorf("result = %g, want -1", resultVal) + } + stack := rpn.GetCurrentStack() + m := stack[len(stack)-1].Metric() + km, _ := reg.Find("km") + if m != km { + t.Errorf("metric = %v, want km", m) + } } func TestHyperSubtractIncompatible(t *testing.T) { @@ -249,8 +301,6 @@ func TestHyperModuloMetricAware(t *testing.T) { } func TestHyperModuloMixedUnits(t *testing.T) { - tolerance := 0.001 - // 1000m 300m 200m [%] = ((1000 % 300) % 200)m = (100 % 200)m = 100m = 0.1km // Result uses first operand's metric (m) vars := NewVariables() @@ -260,6 +310,7 @@ func TestHyperModuloMixedUnits(t *testing.T) { t.Fatalf("unexpected error: %v", err) } resultVal, _ := strconv.ParseFloat(result, 64) + tolerance := 0.001 if resultVal < 100-tolerance || resultVal > 100+tolerance { t.Errorf("result = %g, want 100", resultVal) } -- cgit v1.2.3