Skip to content

Commit a89bae5

Browse files
sylrclaude
andcommitted
test: add SelectHints regression test for anchored/smoothed
Assert that hints.Range passed to NewMatrixSelector always equals the original selector range, not the widened query window. Uses a spy scanner that captures hints and verifies the contract for standard, anchored, and smoothed selectors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Sylvain Rabot <sylvain@abstraction.fr>
1 parent a215c7f commit a89bae5

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

engine/extended_range_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ package engine_test
66
import (
77
"context"
88
"fmt"
9+
"sync"
910
"testing"
1011
"time"
1112

1213
"github.com/efficientgo/core/testutil"
1314
"github.com/prometheus/prometheus/promql"
1415
"github.com/prometheus/prometheus/promql/parser"
1516
"github.com/prometheus/prometheus/promql/promqltest"
17+
promstorage "github.com/prometheus/prometheus/storage"
1618

1719
"github.com/thanos-io/promql-engine/engine"
20+
"github.com/thanos-io/promql-engine/execution/model"
1821
"github.com/thanos-io/promql-engine/logicalplan"
22+
"github.com/thanos-io/promql-engine/query"
23+
engstorage "github.com/thanos-io/promql-engine/storage"
24+
promscan "github.com/thanos-io/promql-engine/storage/prometheus"
1925
)
2026

2127
func TestAnchoredSmoothedModifiers(t *testing.T) {
@@ -289,3 +295,126 @@ func TestAnchoredSmoothedWhitelist(t *testing.T) {
289295
})
290296
}
291297
}
298+
299+
// hintsSpy wraps real scanners and records the SelectHints passed to NewMatrixSelector.
300+
type hintsSpy struct {
301+
inner engstorage.Scanners
302+
mu sync.Mutex
303+
matrixHintsCap []promstorage.SelectHints
304+
}
305+
306+
func (s *hintsSpy) Close() error { return s.inner.Close() }
307+
308+
func (s *hintsSpy) NewVectorSelector(ctx context.Context, opts *query.Options, hints promstorage.SelectHints, selector logicalplan.VectorSelector) (model.VectorOperator, error) {
309+
return s.inner.NewVectorSelector(ctx, opts, hints, selector)
310+
}
311+
312+
func (s *hintsSpy) NewMatrixSelector(ctx context.Context, opts *query.Options, hints promstorage.SelectHints, selector logicalplan.MatrixSelector, call logicalplan.FunctionCall) (model.VectorOperator, error) {
313+
s.mu.Lock()
314+
s.matrixHintsCap = append(s.matrixHintsCap, hints)
315+
s.mu.Unlock()
316+
return s.inner.NewMatrixSelector(ctx, opts, hints, selector, call)
317+
}
318+
319+
func (s *hintsSpy) capturedHints() []promstorage.SelectHints {
320+
s.mu.Lock()
321+
defer s.mu.Unlock()
322+
return s.matrixHintsCap
323+
}
324+
325+
func TestAnchoredSmoothedSelectHints(t *testing.T) {
326+
t.Parallel()
327+
parser.EnableExtendedRangeSelectors = true
328+
329+
load := `load 10s
330+
metric 0 10 20 30 40 50 60 70 80 90 100`
331+
storage := promqltest.LoadedStorage(t, load)
332+
defer storage.Close()
333+
334+
lookbackDelta := 5 * time.Minute // engine default
335+
336+
cases := []struct {
337+
name string
338+
query string
339+
expectedRange int64 // hints.Range in ms — should always be original selector range
340+
}{
341+
{
342+
name: "standard rate — Range equals selector range",
343+
query: `rate(metric[30s])`,
344+
expectedRange: 30000,
345+
},
346+
{
347+
name: "anchored rate — Range equals selector range, not widened",
348+
query: `rate(metric[30s] anchored)`,
349+
expectedRange: 30000,
350+
},
351+
{
352+
name: "smoothed rate — Range equals selector range, not widened",
353+
query: `rate(metric[30s] smoothed)`,
354+
expectedRange: 30000,
355+
},
356+
{
357+
name: "anchored increase — Range equals selector range",
358+
query: `increase(metric[1m] anchored)`,
359+
expectedRange: 60000,
360+
},
361+
{
362+
name: "smoothed delta — Range equals selector range",
363+
query: `delta(metric[1m] smoothed)`,
364+
expectedRange: 60000,
365+
},
366+
}
367+
368+
for _, tc := range cases {
369+
t.Run(tc.name, func(t *testing.T) {
370+
opts := engine.Opts{
371+
EngineOpts: promql.EngineOpts{
372+
Timeout: 1 * time.Hour,
373+
MaxSamples: 1e10,
374+
},
375+
EnableExtendedRangeSelectors: true,
376+
}
377+
qOpts := &query.Options{
378+
Start: time.Unix(0, 0),
379+
End: time.Unix(100, 0),
380+
Step: 10 * time.Second,
381+
LookbackDelta: lookbackDelta,
382+
ExtLookbackDelta: 1 * time.Hour,
383+
}
384+
385+
// Parse and build the logical plan to create real scanners.
386+
parser.EnableExtendedRangeSelectors = true
387+
expr, err := parser.NewParser(tc.query).ParseExpr()
388+
testutil.Ok(t, err)
389+
390+
planOpts := logicalplan.PlanOptions{}
391+
lplan, err := logicalplan.NewFromAST(expr, qOpts, planOpts)
392+
testutil.Ok(t, err)
393+
optimizedPlan, _ := lplan.Optimize(logicalplan.AllOptimizers)
394+
395+
realScanners, err := promscan.NewPrometheusScanners(storage, qOpts, optimizedPlan)
396+
testutil.Ok(t, err)
397+
defer realScanners.Close()
398+
399+
spy := &hintsSpy{inner: realScanners}
400+
401+
eng := engine.NewWithScanners(opts, spy)
402+
q, err := eng.NewRangeQuery(context.Background(), storage, nil, tc.query, time.Unix(0, 0), time.Unix(100, 0), 10*time.Second)
403+
testutil.Ok(t, err)
404+
defer q.Close()
405+
406+
res := q.Exec(context.Background())
407+
testutil.Ok(t, res.Err)
408+
409+
hints := spy.capturedHints()
410+
if len(hints) == 0 {
411+
t.Fatal("expected at least one NewMatrixSelector call, got none")
412+
}
413+
for i, h := range hints {
414+
if h.Range != tc.expectedRange {
415+
t.Errorf("hints[%d].Range = %d, want %d (original selector range)", i, h.Range, tc.expectedRange)
416+
}
417+
}
418+
})
419+
}
420+
}

0 commit comments

Comments
 (0)