diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-22 10:49:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-22 10:49:43 +0300 |
| commit | c97587eea0abc2e8d3e7d81e1f1c64cd1b7c0ae2 (patch) | |
| tree | 601bf9664ebcf6f4f4b503aaf2c335289374399b | |
| parent | ac870d207e20d14753084d7c9b6f346f63f3b899 (diff) | |
feat(rpn): handle @-prefixed standalone metrics (@GB, @Mbps)
Token @GB pushes Number(1, mode, GB) onto the stack.
Uses FindWithAliases for metric lookup (exact, alias, case-insensitive).
Returns error for unknown metrics.
No conflict with : symbols (@ is distinct from : prefix).
Includes unit and integration tests.
| -rw-r--r-- | internal/rpn/metric_test.go | 63 | ||||
| -rw-r--r-- | internal/rpn/rpn_parse.go | 14 |
2 files changed, 77 insertions, 0 deletions
diff --git a/internal/rpn/metric_test.go b/internal/rpn/metric_test.go index 88b8c3b..9eb1cf7 100644 --- a/internal/rpn/metric_test.go +++ b/internal/rpn/metric_test.go @@ -780,3 +780,66 @@ func TestParseNumberWithMetricExactMatch(t *testing.T) { t.Error("parseNumberWithMetric(100Bps) should fail (B = bytes)") } } + +func TestAtPrefixMetricParsing(t *testing.T) { + reg := GetMetricRegistry() + + tests := []struct { + token string + wantMetric string + wantOK bool + }{ + {"@GB", "GB", true}, + {"@Mbps", "Mbps", true}, + {"@hr", "hr", true}, + {"@sec", "s", true}, // alias + {"@foot", "ft", true}, // alias + {"@nope", "", false}, + {"@", "", false}, + } + + for _, tt := range tests { + // Simulate the @ prefix logic from rpn_parse.go + if len(tt.token) <= 1 || tt.token[0] != '@' { + continue + } + metricName := tt.token[1:] + metric, ok := reg.FindWithAliases(metricName) + if ok != tt.wantOK { + t.Errorf("@%s ok = %v, want %v", metricName, ok, tt.wantOK) + continue + } + if ok && tt.wantMetric != "" { + expected, _ := reg.Find(tt.wantMetric) + if metric != expected { + t.Errorf("@%s = %q, want %q", metricName, metric.Name, tt.wantMetric) + } + } + } +} + +func TestAtPrefixIntegration(t *testing.T) { + // Test that @GB parses correctly through the full RPN pipeline + vars := NewVariables() + rpn := NewRPN(vars) + + // Parse a standalone @ metric + result, err := rpn.ParseAndEvaluate("@GB") + if err != nil { + t.Fatalf("ParseAndEvaluate(@GB) failed: %v", err) + } + // Should push 1 with GB metric; display is "1" + if result != "1" { + t.Errorf("@GB result = %q, want %q", result, "1") + } + + // The stack should have a number with the GB metric + stack := rpn.GetCurrentStack() + if len(stack) != 1 { + t.Fatalf("expected 1 item on stack, got %d", len(stack)) + } + m := stack[0].Metric() + if m == nil || m.Name != "GB" { + t.Errorf("stack[0].Metric() = %v, want GB", m) + } +} diff --git a/internal/rpn/rpn_parse.go b/internal/rpn/rpn_parse.go index 40ff432..f115090 100644 --- a/internal/rpn/rpn_parse.go +++ b/internal/rpn/rpn_parse.go @@ -335,6 +335,20 @@ func (r *RPN) evaluate(input string, tokens []string) (string, error) { continue } + // Check for @ prefix: standalone metric (e.g., @GB, @Mbps) + // Pushes a Number with value 1 and the looked-up metric + if len(token) > 1 && token[0] == '@' { + metricName := token[1:] + if metric, ok := GetMetricRegistry().FindWithAliases(metricName); ok { + if stack.Len() >= r.maxStack { + return "", fmt.Errorf("stack overflow") + } + stack.Push(NewNumber(1, r.mode, metric)) + continue + } + return "", fmt.Errorf("unknown metric %q in %q", metricName, token) + } + // Check if this is a variable name for assignment (:= or =:) // For := (right assignment): name value := - first token is always a variable name // For =: (left assignment): value name =: - token before =: is a variable name |
