1package queryrange
2
3import (
4	"context"
5	"io/ioutil"
6	"net/http"
7	"net/http/httptest"
8	"net/url"
9	"strconv"
10	"testing"
11	"time"
12
13	"github.com/weaveworks/common/httpgrpc"
14
15	"github.com/prometheus/prometheus/promql/parser"
16	"github.com/stretchr/testify/require"
17	"github.com/weaveworks/common/middleware"
18	"github.com/weaveworks/common/user"
19	"go.uber.org/atomic"
20)
21
22const seconds = 1e3 // 1e3 milliseconds per second.
23
24func TestNextIntervalBoundary(t *testing.T) {
25	for i, tc := range []struct {
26		in, step, out int64
27		interval      time.Duration
28	}{
29		// Smallest possible period is 1 millisecond
30		{0, 1, toMs(day) - 1, day},
31		{0, 1, toMs(time.Hour) - 1, time.Hour},
32		// A more standard example
33		{0, 15 * seconds, toMs(day) - 15*seconds, day},
34		{0, 15 * seconds, toMs(time.Hour) - 15*seconds, time.Hour},
35		// Move start time forward 1 second; end time moves the same
36		{1 * seconds, 15 * seconds, toMs(day) - (15-1)*seconds, day},
37		{1 * seconds, 15 * seconds, toMs(time.Hour) - (15-1)*seconds, time.Hour},
38		// Move start time forward 14 seconds; end time moves the same
39		{14 * seconds, 15 * seconds, toMs(day) - (15-14)*seconds, day},
40		{14 * seconds, 15 * seconds, toMs(time.Hour) - (15-14)*seconds, time.Hour},
41		// Now some examples where the period does not divide evenly into a day:
42		// 1 day modulus 35 seconds = 20 seconds
43		{0, 35 * seconds, toMs(day) - 20*seconds, day},
44		// 1 hour modulus 35 sec = 30  (3600 mod 35 = 30)
45		{0, 35 * seconds, toMs(time.Hour) - 30*seconds, time.Hour},
46		// Move start time forward 1 second; end time moves the same
47		{1 * seconds, 35 * seconds, toMs(day) - (20-1)*seconds, day},
48		{1 * seconds, 35 * seconds, toMs(time.Hour) - (30-1)*seconds, time.Hour},
49		// If the end time lands exactly on midnight we stop one period before that
50		{20 * seconds, 35 * seconds, toMs(day) - 35*seconds, day},
51		{30 * seconds, 35 * seconds, toMs(time.Hour) - 35*seconds, time.Hour},
52		// This example starts 35 seconds after the 5th one ends
53		{toMs(day) + 15*seconds, 35 * seconds, 2*toMs(day) - 5*seconds, day},
54		{toMs(time.Hour) + 15*seconds, 35 * seconds, 2*toMs(time.Hour) - 15*seconds, time.Hour},
55	} {
56		t.Run(strconv.Itoa(i), func(t *testing.T) {
57			require.Equal(t, tc.out, nextIntervalBoundary(tc.in, tc.step, tc.interval))
58		})
59	}
60}
61
62func TestSplitQuery(t *testing.T) {
63	for i, tc := range []struct {
64		input    Request
65		expected []Request
66		interval time.Duration
67	}{
68		{
69			input: &PrometheusRequest{
70				Start: 0,
71				End:   60 * 60 * seconds,
72				Step:  15 * seconds,
73				Query: "foo",
74			},
75			expected: []Request{
76				&PrometheusRequest{
77					Start: 0,
78					End:   60 * 60 * seconds,
79					Step:  15 * seconds,
80					Query: "foo",
81				},
82			},
83			interval: day,
84		},
85		{
86			input: &PrometheusRequest{
87				Start: 0,
88				End:   60 * 60 * seconds,
89				Step:  15 * seconds,
90				Query: "foo",
91			},
92			expected: []Request{
93				&PrometheusRequest{
94					Start: 0,
95					End:   60 * 60 * seconds,
96					Step:  15 * seconds,
97					Query: "foo",
98				},
99			},
100			interval: 3 * time.Hour,
101		},
102		{
103			input: &PrometheusRequest{
104				Start: 0,
105				End:   24 * 3600 * seconds,
106				Step:  15 * seconds,
107				Query: "foo",
108			},
109			expected: []Request{
110				&PrometheusRequest{
111					Start: 0,
112					End:   24 * 3600 * seconds,
113					Step:  15 * seconds,
114					Query: "foo",
115				},
116			},
117			interval: day,
118		},
119		{
120			input: &PrometheusRequest{
121				Start: 0,
122				End:   3 * 3600 * seconds,
123				Step:  15 * seconds,
124				Query: "foo",
125			},
126			expected: []Request{
127				&PrometheusRequest{
128					Start: 0,
129					End:   3 * 3600 * seconds,
130					Step:  15 * seconds,
131					Query: "foo",
132				},
133			},
134			interval: 3 * time.Hour,
135		},
136		{
137			input: &PrometheusRequest{
138				Start: 0,
139				End:   2 * 24 * 3600 * seconds,
140				Step:  15 * seconds,
141				Query: "foo @ start()",
142			},
143			expected: []Request{
144				&PrometheusRequest{
145					Start: 0,
146					End:   (24 * 3600 * seconds) - (15 * seconds),
147					Step:  15 * seconds,
148					Query: "foo @ 0.000",
149				},
150				&PrometheusRequest{
151					Start: 24 * 3600 * seconds,
152					End:   2 * 24 * 3600 * seconds,
153					Step:  15 * seconds,
154					Query: "foo @ 0.000",
155				},
156			},
157			interval: day,
158		},
159		{
160			input: &PrometheusRequest{
161				Start: 0,
162				End:   2 * 3 * 3600 * seconds,
163				Step:  15 * seconds,
164				Query: "foo",
165			},
166			expected: []Request{
167				&PrometheusRequest{
168					Start: 0,
169					End:   (3 * 3600 * seconds) - (15 * seconds),
170					Step:  15 * seconds,
171					Query: "foo",
172				},
173				&PrometheusRequest{
174					Start: 3 * 3600 * seconds,
175					End:   2 * 3 * 3600 * seconds,
176					Step:  15 * seconds,
177					Query: "foo",
178				},
179			},
180			interval: 3 * time.Hour,
181		},
182		{
183			input: &PrometheusRequest{
184				Start: 3 * 3600 * seconds,
185				End:   3 * 24 * 3600 * seconds,
186				Step:  15 * seconds,
187				Query: "foo",
188			},
189			expected: []Request{
190				&PrometheusRequest{
191					Start: 3 * 3600 * seconds,
192					End:   (24 * 3600 * seconds) - (15 * seconds),
193					Step:  15 * seconds,
194					Query: "foo",
195				},
196				&PrometheusRequest{
197					Start: 24 * 3600 * seconds,
198					End:   (2 * 24 * 3600 * seconds) - (15 * seconds),
199					Step:  15 * seconds,
200					Query: "foo",
201				},
202				&PrometheusRequest{
203					Start: 2 * 24 * 3600 * seconds,
204					End:   3 * 24 * 3600 * seconds,
205					Step:  15 * seconds,
206					Query: "foo",
207				},
208			},
209			interval: day,
210		},
211		{
212			input: &PrometheusRequest{
213				Start: 2 * 3600 * seconds,
214				End:   3 * 3 * 3600 * seconds,
215				Step:  15 * seconds,
216				Query: "foo",
217			},
218			expected: []Request{
219				&PrometheusRequest{
220					Start: 2 * 3600 * seconds,
221					End:   (3 * 3600 * seconds) - (15 * seconds),
222					Step:  15 * seconds,
223					Query: "foo",
224				},
225				&PrometheusRequest{
226					Start: 3 * 3600 * seconds,
227					End:   (2 * 3 * 3600 * seconds) - (15 * seconds),
228					Step:  15 * seconds,
229					Query: "foo",
230				},
231				&PrometheusRequest{
232					Start: 2 * 3 * 3600 * seconds,
233					End:   3 * 3 * 3600 * seconds,
234					Step:  15 * seconds,
235					Query: "foo",
236				},
237			},
238			interval: 3 * time.Hour,
239		},
240	} {
241		t.Run(strconv.Itoa(i), func(t *testing.T) {
242			days, err := splitQuery(tc.input, tc.interval)
243			require.NoError(t, err)
244			require.Equal(t, tc.expected, days)
245		})
246	}
247}
248
249func TestSplitByDay(t *testing.T) {
250	mergedResponse, err := PrometheusCodec.MergeResponse(parsedResponse, parsedResponse)
251	require.NoError(t, err)
252
253	mergedHTTPResponse, err := PrometheusCodec.EncodeResponse(context.Background(), mergedResponse)
254	require.NoError(t, err)
255
256	mergedHTTPResponseBody, err := ioutil.ReadAll(mergedHTTPResponse.Body)
257	require.NoError(t, err)
258
259	for i, tc := range []struct {
260		path, expectedBody string
261		expectedQueryCount int32
262	}{
263		{query, string(mergedHTTPResponseBody), 2},
264	} {
265		t.Run(strconv.Itoa(i), func(t *testing.T) {
266			var actualCount atomic.Int32
267			s := httptest.NewServer(
268				middleware.AuthenticateUser.Wrap(
269					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
270						actualCount.Inc()
271						_, _ = w.Write([]byte(responseBody))
272					}),
273				),
274			)
275			defer s.Close()
276
277			u, err := url.Parse(s.URL)
278			require.NoError(t, err)
279
280			interval := func(_ Request) time.Duration { return 24 * time.Hour }
281			roundtripper := NewRoundTripper(singleHostRoundTripper{
282				host: u.Host,
283				next: http.DefaultTransport,
284			}, PrometheusCodec, NewLimitsMiddleware(mockLimits{}), SplitByIntervalMiddleware(interval, mockLimits{}, PrometheusCodec, nil))
285
286			req, err := http.NewRequest("GET", tc.path, http.NoBody)
287			require.NoError(t, err)
288
289			ctx := user.InjectOrgID(context.Background(), "1")
290			req = req.WithContext(ctx)
291
292			resp, err := roundtripper.RoundTrip(req)
293			require.NoError(t, err)
294			require.Equal(t, 200, resp.StatusCode)
295
296			bs, err := ioutil.ReadAll(resp.Body)
297			require.NoError(t, err)
298			require.Equal(t, tc.expectedBody, string(bs))
299			require.Equal(t, tc.expectedQueryCount, actualCount.Load())
300		})
301	}
302}
303
304func Test_evaluateAtModifier(t *testing.T) {
305	const (
306		start, end = int64(1546300800), int64(1646300800)
307	)
308	for _, tt := range []struct {
309		in, expected      string
310		expectedErrorCode int
311	}{
312		{
313			in:       "topk(5, rate(http_requests_total[1h] @ start()))",
314			expected: "topk(5, rate(http_requests_total[1h] @ 1546300.800))",
315		},
316		{
317			in:       "topk(5, rate(http_requests_total[1h] @ 0))",
318			expected: "topk(5, rate(http_requests_total[1h] @ 0.000))",
319		},
320		{
321			in:       "http_requests_total[1h] @ 10.001",
322			expected: "http_requests_total[1h] @ 10.001",
323		},
324		{
325			in: `min_over_time(
326				sum by(cluster) (
327					rate(http_requests_total[5m] @ end())
328				)[10m:]
329			)
330			or
331			max_over_time(
332				stddev_over_time(
333					deriv(
334						rate(http_requests_total[10m] @ start())
335					[5m:1m])
336				[2m:])
337			[10m:])`,
338			expected: `min_over_time(
339				sum by(cluster) (
340					rate(http_requests_total[5m] @ 1646300.800)
341				)[10m:]
342			)
343			or
344			max_over_time(
345				stddev_over_time(
346					deriv(
347						rate(http_requests_total[10m] @ 1546300.800)
348					[5m:1m])
349				[2m:])
350			[10m:])`,
351		},
352		{
353			// parse error: missing unit character in duration
354			in:                "http_requests_total[5] @ 10.001",
355			expectedErrorCode: http.StatusBadRequest,
356		},
357		{
358			// parse error: @ modifier must be preceded by an instant vector selector or range vector selector or a subquery
359			in:                "sum(http_requests_total[5m]) @ 10.001",
360			expectedErrorCode: http.StatusBadRequest,
361		},
362	} {
363		tt := tt
364		t.Run(tt.in, func(t *testing.T) {
365			t.Parallel()
366			out, err := evaluateAtModifierFunction(tt.in, start, end)
367			if tt.expectedErrorCode != 0 {
368				require.Error(t, err)
369				httpResp, ok := httpgrpc.HTTPResponseFromError(err)
370				require.True(t, ok, "returned error is not an httpgrpc response")
371				require.Equal(t, tt.expectedErrorCode, int(httpResp.Code))
372			} else {
373				require.NoError(t, err)
374				expectedExpr, err := parser.ParseExpr(tt.expected)
375				require.NoError(t, err)
376				require.Equal(t, expectedExpr.String(), out)
377			}
378		})
379	}
380}
381