summaryrefslogtreecommitdiff
path: root/internal/rpn/operations_hyper_test.go
blob: 2a1607a1dc481aa9cddd0d2941cd6ed93aa03e49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Paul Buetow

package rpn

import (
	"math"
	"strconv"
	"testing"
)

// TestHyperMetricAwareOperations tests [+-%] ops that preserve metric categories.
func TestHyperMetricAwareOperations(t *testing.T) {
	reg := GetMetricRegistry()

	tests := []struct {
		name    string
		expr    string
		wantNum float64
		wantMet string // empty skips metric check
		tol     float64
		wantErr bool
	}{
		// Addition
		{
			name: "add same category", expr: "100Mbps 50Mbps 25Mbps [+]",
			wantNum: 175, wantMet: "Mbps", tol: 0.001,
		},
		{
			name: "add mixed units", expr: "1km 500m 100m [+]",
			wantNum: 1.6, wantMet: "km", tol: 0.001,
		},
		{
			name: "add cool absorbing", expr: "5 100Mbps 10Mbps [+]",
			wantNum: 115, wantMet: "Mbps", tol: 0.001,
		},
		{
			name: "add all cool", expr: "1 2 3 [+]",
			wantNum: 6, wantMet: "Cool", tol: 0.001,
		},
		{
			name: "add incompatible", expr: "100Mbps 2hr [+]",
			wantErr: true,
		},
		// Subtraction
		{
			name: "sub same category", expr: "1000Mbps 100Mbps 50Mbps [-]",
			wantNum: 850, wantMet: "Mbps", tol: 0.001,
		},
		{
			name: "sub mixed units", expr: "2km 500m 100m [-]",
			wantNum: 1.4, wantMet: "km", tol: 0.001,
		},
		{
			name: "sub cool absorbing", expr: "100km 5 [-]",
			wantNum: 95, wantMet: "km", tol: 1,
		},
		{
			name: "sub negative", expr: "1km 2km [-]",
			wantNum: -1, wantMet: "km", tol: 0.1,
		},
		{
			name: "sub incompatible", expr: "100km 2hr [-]",
			wantErr: true,
		},
		// Modulo
		{
			name: "mod same category", expr: "10km 3km 2km [%]",
			wantNum: 1, wantMet: "km", tol: 0.001,
		},
		{
			name: "mod mixed units", expr: "1000m 300m 200m [%]",
			wantNum: 100, tol: 0.001,
		},
		{
			name: "mod incompatible", expr: "100km 2hr [%]",
			wantErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			vars := NewVariables()
			rpn := NewRPN(vars, nil)
			result, err := rpn.ParseAndEvaluate(tc.expr)
			if tc.wantErr {
				if err == nil {
					t.Fatal("expected error, got none")
				}
				return
			}
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			got, _ := strconv.ParseFloat(result, 64)
			tol := tc.tol
			if tol == 0 {
				tol = 0.001
			}
			if got < tc.wantNum-tol || got > tc.wantNum+tol {
				t.Errorf("result = %g, want %g (tol %g)", got, tc.wantNum, tol)
			}

			if tc.wantMet != "" {
				stack := rpn.GetCurrentStack()
				m := stack[len(stack)-1].Metric()
				want, _ := reg.Find(tc.wantMet)
				if m != want {
					t.Errorf("metric = %v, want %s", m, tc.wantMet)
				}
			}
		})
	}
}

// TestHyperCoolResultOperations tests [*][/][^][lg][log][ln] ops that always
// produce a Cool metric result.
func TestHyperCoolResultOperations(t *testing.T) {
	reg := GetMetricRegistry()

	tests := []struct {
		name    string
		expr    string
		wantNum float64
		tol     float64
	}{
		{
			name: "multiply basic", expr: "3 4 5 [*]",
			wantNum: 60, tol: 0.001,
		},
		{
			name: "multiply mixed metrics", expr: "100Mbps 2hr [*]",
			wantNum: 200, tol: 0.001,
		},
		{
			name: "divide basic", expr: "100 20 2 [/]",
			wantNum: 2.5, tol: 0.001,
		},
		{
			name: "power basic", expr: "2 3 2 [^]",
			wantNum: 64, tol: 0.001,
		},
		{
			name: "log2 basic", expr: "2 4 8 [lg]",
			wantNum: 6, tol: 0.001,
		},
		{
			name: "log10 basic", expr: "10 100 [log]",
			wantNum: 3, tol: 0.001,
		},
		{
			name: "ln basic", expr: "2.718281828459045 7.38905609893065 [ln]",
			wantNum: 3, tol: 0.001,
		},
		{
			name: "log2 with metrics", expr: "100Mbps 1000Mbps [lg]",
			wantNum: math.Log2(100) + math.Log2(1000), tol: 0.01,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			vars := NewVariables()
			rpn := NewRPN(vars, nil)
			result, err := rpn.ParseAndEvaluate(tc.expr)
			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}

			got, _ := strconv.ParseFloat(result, 64)
			if got < tc.wantNum-tc.tol || got > tc.wantNum+tc.tol {
				t.Errorf("result = %g, want %g (tol %g)", got, tc.wantNum, tc.tol)
			}

			// All results must be Cool
			stack := rpn.GetCurrentStack()
			m := stack[len(stack)-1].Metric()
			cool, _ := reg.Find("Cool")
			if m != cool {
				t.Errorf("metric = %v, want Cool", m)
			}
		})
	}
}

// TestHyperErrorCases covers expressions that must fail evaluation.
func TestHyperErrorCases(t *testing.T) {
	tests := []struct {
		name string
		expr string
	}{
		{"modulo by zero", "10km 0km [%]"},
		{"divide by zero", "10 0 [/]"},
		{"log2 non-positive", "0 5 [lg]"},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			vars := NewVariables()
			rpn := NewRPN(vars, nil)
			_, err := rpn.ParseAndEvaluate(tc.expr)
			if err == nil {
				t.Fatal("expected error, got none")
			}
		})
	}
}