prometheus/promql/durations_test.go
Julien Pivotto ee7d5158a7 Add step(), min(a,b) and max(a,b) in promql duration expressions
step() is a new keyword introduced to represent the query step width in duration expressions.

min(a,b) and max(a,b) return the min and max from two duration expressions.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
2025-07-02 11:17:17 +02:00

258 lines
7.3 KiB
Go

// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package promql
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/promql/parser"
)
func TestDurationVisitor(t *testing.T) {
// Enable experimental duration expression parsing.
parser.ExperimentalDurationExpr = true
t.Cleanup(func() {
parser.ExperimentalDurationExpr = false
})
complexExpr := `sum_over_time(
rate(metric[5m] offset 1h)[10m:30s] offset 2h
) +
avg_over_time(
metric[1h + 30m] offset -1h
) *
count_over_time(
metric[2h * 0.5]
)`
expr, err := parser.ParseExpr(complexExpr)
require.NoError(t, err)
err = parser.Walk(&durationVisitor{}, expr, nil)
require.NoError(t, err)
// Verify different parts of the expression have correct durations.
// This is a binary expression at the top level.
binExpr, ok := expr.(*parser.BinaryExpr)
require.True(t, ok, "Expected binary expression at top level")
// Left side should be sum_over_time with subquery.
leftCall, ok := binExpr.LHS.(*parser.Call)
require.True(t, ok, "Expected call expression on left side")
require.Equal(t, "sum_over_time", leftCall.Func.Name)
// Extract the subquery from sum_over_time.
sumSubquery, ok := leftCall.Args[0].(*parser.SubqueryExpr)
require.True(t, ok, "Expected subquery in sum_over_time")
require.Equal(t, 10*time.Minute, sumSubquery.Range)
require.Equal(t, 30*time.Second, sumSubquery.Step)
require.Equal(t, 2*time.Hour, sumSubquery.OriginalOffset)
// Extract the rate call inside the subquery.
rateCall, ok := sumSubquery.Expr.(*parser.Call)
require.True(t, ok, "Expected rate call in subquery")
require.Equal(t, "rate", rateCall.Func.Name)
// Extract the matrix selector from rate.
rateMatrix, ok := rateCall.Args[0].(*parser.MatrixSelector)
require.True(t, ok, "Expected matrix selector in rate")
require.Equal(t, 5*time.Minute, rateMatrix.Range)
require.Equal(t, 1*time.Hour, rateMatrix.VectorSelector.(*parser.VectorSelector).OriginalOffset)
// Right side should be another binary expression (multiplication).
rightBinExpr, ok := binExpr.RHS.(*parser.BinaryExpr)
require.True(t, ok, "Expected binary expression on right side")
// Left side of multiplication should be avg_over_time.
avgCall, ok := rightBinExpr.LHS.(*parser.Call)
require.True(t, ok, "Expected call expression on left side of multiplication")
require.Equal(t, "avg_over_time", avgCall.Func.Name)
// Extract the matrix selector from avg_over_time.
avgMatrix, ok := avgCall.Args[0].(*parser.MatrixSelector)
require.True(t, ok, "Expected matrix selector in avg_over_time")
require.Equal(t, 90*time.Minute, avgMatrix.Range) // 1h + 30m
require.Equal(t, -1*time.Hour, avgMatrix.VectorSelector.(*parser.VectorSelector).OriginalOffset)
// Right side of multiplication should be count_over_time.
countCall, ok := rightBinExpr.RHS.(*parser.Call)
require.True(t, ok, "Expected call expression on right side of multiplication")
require.Equal(t, "count_over_time", countCall.Func.Name)
// Extract the matrix selector from count_over_time.
countMatrix, ok := countCall.Args[0].(*parser.MatrixSelector)
require.True(t, ok, "Expected matrix selector in count_over_time")
require.Equal(t, 1*time.Hour, countMatrix.Range) // 2h * 0.5
}
func TestCalculateDuration(t *testing.T) {
tests := []struct {
name string
expr parser.Expr
expected time.Duration
errorMessage string
allowedNegative bool
}{
{
name: "addition",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.NumberLiteral{Val: 10},
Op: parser.ADD,
},
expected: 15 * time.Second,
},
{
name: "subtraction",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 15},
RHS: &parser.NumberLiteral{Val: 5},
Op: parser.SUB,
},
expected: 10 * time.Second,
},
{
name: "subtraction with negative",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.NumberLiteral{Val: 10},
Op: parser.SUB,
},
errorMessage: "duration must be greater than 0",
},
{
name: "multiplication",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.NumberLiteral{Val: 3},
Op: parser.MUL,
},
expected: 15 * time.Second,
},
{
name: "division",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 15},
RHS: &parser.NumberLiteral{Val: 3},
Op: parser.DIV,
},
expected: 5 * time.Second,
},
{
name: "modulo with numbers",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 17},
RHS: &parser.NumberLiteral{Val: 5},
Op: parser.MOD,
},
expected: 2 * time.Second,
},
{
name: "power",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 2},
RHS: &parser.NumberLiteral{Val: 3},
Op: parser.POW,
},
expected: 8 * time.Second,
},
{
name: "complex expression",
expr: &parser.DurationExpr{
LHS: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 2},
RHS: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 3},
RHS: &parser.NumberLiteral{Val: 4},
Op: parser.ADD,
},
Op: parser.MUL,
},
RHS: &parser.NumberLiteral{Val: 1},
Op: parser.SUB,
},
expected: 13 * time.Second,
},
{
name: "unary negative",
expr: &parser.DurationExpr{
RHS: &parser.NumberLiteral{Val: 5},
Op: parser.SUB,
},
expected: -5 * time.Second,
allowedNegative: true,
},
{
name: "step",
expr: &parser.DurationExpr{
Op: parser.STEP,
},
expected: 1 * time.Second,
},
{
name: "step multiplication",
expr: &parser.DurationExpr{
LHS: &parser.DurationExpr{
Op: parser.STEP,
},
RHS: &parser.NumberLiteral{Val: 3},
Op: parser.MUL,
},
expected: 3 * time.Second,
},
{
name: "division by zero",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.NumberLiteral{Val: 5},
Op: parser.SUB,
},
Op: parser.DIV,
},
errorMessage: "division by zero",
},
{
name: "modulo by zero",
expr: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.DurationExpr{
LHS: &parser.NumberLiteral{Val: 5},
RHS: &parser.NumberLiteral{Val: 5},
Op: parser.SUB,
},
Op: parser.MOD,
},
errorMessage: "modulo by zero",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &durationVisitor{step: 1 * time.Second}
result, err := v.calculateDuration(tt.expr, tt.allowedNegative)
if tt.errorMessage != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tt.errorMessage)
return
}
require.NoError(t, err)
require.Equal(t, tt.expected, result)
})
}
}