1// Copyright 2021 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14import chai from 'chai';
15import { analyzeCompletion, computeStartCompletePosition, ContextKind } from './hybrid';
16import { createEditorState, mockedMetricsTerms, mockPrometheusServer } from '../../test/utils';
17import { Completion, CompletionContext } from '@codemirror/autocomplete';
18import {
19  aggregateOpModifierTerms,
20  aggregateOpTerms,
21  atModifierTerms,
22  binOpModifierTerms,
23  binOpTerms,
24  durationTerms,
25  functionIdentifierTerms,
26  matchOpTerms,
27  numberTerms,
28  snippets,
29} from './promql.terms';
30import { EqlSingle, Neq } from '../grammar/parser.terms';
31import { syntaxTree } from '@codemirror/language';
32import { newCompleteStrategy } from './index';
33
34describe('analyzeCompletion test', () => {
35  const testCases = [
36    {
37      title: 'empty expr',
38      expr: '',
39      pos: 0,
40      expectedContext: [
41        {
42          kind: ContextKind.MetricName,
43          metricName: '',
44        },
45        { kind: ContextKind.Function },
46        { kind: ContextKind.Aggregation },
47        { kind: ContextKind.Number },
48      ],
49    },
50    {
51      title: 'simple metric & number autocompletion',
52      expr: 'go_',
53      pos: 3, // cursor is at the end of the expr
54      expectedContext: [
55        {
56          kind: ContextKind.MetricName,
57          metricName: 'go_',
58        },
59        { kind: ContextKind.Function },
60        { kind: ContextKind.Aggregation },
61        { kind: ContextKind.Number },
62      ],
63    },
64    {
65      title: 'metric/function/aggregation autocompletion',
66      expr: 'sum()',
67      pos: 4,
68      expectedContext: [
69        {
70          kind: ContextKind.MetricName,
71          metricName: '',
72        },
73        { kind: ContextKind.Function },
74        { kind: ContextKind.Aggregation },
75      ],
76    },
77    {
78      title: 'metric/function/aggregation autocompletion 2',
79      expr: 'sum(rat)',
80      pos: 7,
81      expectedContext: [
82        {
83          kind: ContextKind.MetricName,
84          metricName: 'rat',
85        },
86        { kind: ContextKind.Function },
87        { kind: ContextKind.Aggregation },
88      ],
89    },
90    {
91      title: 'metric/function/aggregation autocompletion 3',
92      expr: 'sum(rate())',
93      pos: 9,
94      expectedContext: [
95        {
96          kind: ContextKind.MetricName,
97          metricName: '',
98        },
99        { kind: ContextKind.Function },
100        { kind: ContextKind.Aggregation },
101      ],
102    },
103    {
104      title: 'metric/function/aggregation autocompletion 4',
105      expr: 'sum(rate(my_))',
106      pos: 12,
107      expectedContext: [
108        {
109          kind: ContextKind.MetricName,
110          metricName: 'my_',
111        },
112        { kind: ContextKind.Function },
113        { kind: ContextKind.Aggregation },
114      ],
115    },
116    {
117      title: 'autocomplete binOp modifier or metric or number',
118      expr: 'metric_name / ignor',
119      pos: 19,
120      expectedContext: [
121        { kind: ContextKind.MetricName, metricName: 'ignor' },
122        { kind: ContextKind.Function },
123        { kind: ContextKind.Aggregation },
124        { kind: ContextKind.BinOpModifier },
125        { kind: ContextKind.Number },
126      ],
127    },
128    {
129      title: 'autocomplete binOp modifier or metric or number 2',
130      expr: 'sum(http_requests_total{method="GET"} / o)',
131      pos: 41,
132      expectedContext: [
133        { kind: ContextKind.MetricName, metricName: 'o' },
134        { kind: ContextKind.Function },
135        { kind: ContextKind.Aggregation },
136        { kind: ContextKind.BinOpModifier },
137        { kind: ContextKind.Number },
138      ],
139    },
140    {
141      title: 'autocomplete bool/binOp modifier/metric/number 1',
142      expr: '1 > b)',
143      pos: 5,
144      expectedContext: [
145        { kind: ContextKind.MetricName, metricName: 'b' },
146        { kind: ContextKind.Function },
147        { kind: ContextKind.Aggregation },
148        { kind: ContextKind.BinOpModifier },
149        { kind: ContextKind.Number },
150        { kind: ContextKind.Bool },
151      ],
152    },
153    {
154      title: 'autocomplete bool/binOp modifier/metric/number 2',
155      expr: '1 == b)',
156      pos: 6,
157      expectedContext: [
158        { kind: ContextKind.MetricName, metricName: 'b' },
159        { kind: ContextKind.Function },
160        { kind: ContextKind.Aggregation },
161        { kind: ContextKind.BinOpModifier },
162        { kind: ContextKind.Number },
163        { kind: ContextKind.Bool },
164      ],
165    },
166    {
167      title: 'autocomplete bool/binOp modifier/metric/number 3',
168      expr: '1 != b)',
169      pos: 6,
170      expectedContext: [
171        { kind: ContextKind.MetricName, metricName: 'b' },
172        { kind: ContextKind.Function },
173        { kind: ContextKind.Aggregation },
174        { kind: ContextKind.BinOpModifier },
175        { kind: ContextKind.Number },
176        { kind: ContextKind.Bool },
177      ],
178    },
179    {
180      title: 'autocomplete bool/binOp modifier/metric/number 4',
181      expr: '1 > b)',
182      pos: 5,
183      expectedContext: [
184        { kind: ContextKind.MetricName, metricName: 'b' },
185        { kind: ContextKind.Function },
186        { kind: ContextKind.Aggregation },
187        { kind: ContextKind.BinOpModifier },
188        { kind: ContextKind.Number },
189        { kind: ContextKind.Bool },
190      ],
191    },
192    {
193      title: 'autocomplete bool/binOp modifier/metric/number 5',
194      expr: '1 >= b)',
195      pos: 6,
196      expectedContext: [
197        { kind: ContextKind.MetricName, metricName: 'b' },
198        { kind: ContextKind.Function },
199        { kind: ContextKind.Aggregation },
200        { kind: ContextKind.BinOpModifier },
201        { kind: ContextKind.Number },
202        { kind: ContextKind.Bool },
203      ],
204    },
205    {
206      title: 'autocomplete bool/binOp modifier/metric/number 6',
207      expr: '1 <= b)',
208      pos: 6,
209      expectedContext: [
210        { kind: ContextKind.MetricName, metricName: 'b' },
211        { kind: ContextKind.Function },
212        { kind: ContextKind.Aggregation },
213        { kind: ContextKind.BinOpModifier },
214        { kind: ContextKind.Number },
215        { kind: ContextKind.Bool },
216      ],
217    },
218    {
219      title: 'autocomplete bool/binOp modifier/metric/number 7',
220      expr: '1 < b)',
221      pos: 5,
222      expectedContext: [
223        { kind: ContextKind.MetricName, metricName: 'b' },
224        { kind: ContextKind.Function },
225        { kind: ContextKind.Aggregation },
226        { kind: ContextKind.BinOpModifier },
227        { kind: ContextKind.Number },
228        { kind: ContextKind.Bool },
229      ],
230    },
231    {
232      title: 'starting to autocomplete labelName in aggregate modifier',
233      expr: 'sum by ()',
234      pos: 8, // cursor is between the bracket
235      expectedContext: [{ kind: ContextKind.LabelName }],
236    },
237    {
238      title: 'continue to autocomplete labelName in aggregate modifier',
239      expr: 'sum by (myL)',
240      pos: 11, // cursor is between the bracket after the string myL
241      expectedContext: [{ kind: ContextKind.LabelName }],
242    },
243    {
244      title: 'autocomplete labelName in a list',
245      expr: 'sum by (myLabel1,)',
246      pos: 17, // cursor is between the bracket after the string myLab
247      expectedContext: [{ kind: ContextKind.LabelName }],
248    },
249    {
250      title: 'autocomplete labelName in a list 2',
251      expr: 'sum by (myLabel1, myLab)',
252      pos: 23, // cursor is between the bracket after the string myLab
253      expectedContext: [{ kind: ContextKind.LabelName }],
254    },
255    {
256      title: 'autocomplete labelName associated to a metric',
257      expr: 'metric_name{}',
258      pos: 12, // cursor is between the bracket
259      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
260    },
261    {
262      title: 'autocomplete labelName that defined a metric',
263      expr: '{}',
264      pos: 1, // cursor is between the bracket
265      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
266    },
267    {
268      title: 'continue to autocomplete labelName associated to a metric',
269      expr: 'metric_name{myL}',
270      pos: 15, // cursor is between the bracket after the string myL
271      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
272    },
273    {
274      title: 'continue to autocomplete labelName associated to a metric 2',
275      expr: 'metric_name{myLabel="labelValue",}',
276      pos: 33, // cursor is between the bracket after the comma
277      expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
278    },
279    {
280      title: 'continue autocomplete labelName that defined a metric',
281      expr: '{myL}',
282      pos: 4, // cursor is between the bracket after the string myL
283      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
284    },
285    {
286      title: 'continue autocomplete labelName that defined a metric 2',
287      expr: '{myLabel="labelValue",}',
288      pos: 22, // cursor is between the bracket after the comma
289      expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
290    },
291    {
292      title: 'autocomplete the labelValue with metricName + labelName',
293      expr: 'metric_name{labelName=""}',
294      pos: 23, // cursor is between the quotes
295      expectedContext: [
296        {
297          kind: ContextKind.LabelValue,
298          metricName: 'metric_name',
299          labelName: 'labelName',
300          matchers: [
301            {
302              name: 'labelName',
303              type: EqlSingle,
304              value: '',
305            },
306          ],
307        },
308      ],
309    },
310    {
311      title: 'autocomplete the labelValue with metricName + labelName 2',
312      expr: 'metric_name{labelName="labelValue", labelName!=""}',
313      pos: 48, // cursor is between the quotes
314      expectedContext: [
315        {
316          kind: ContextKind.LabelValue,
317          metricName: 'metric_name',
318          labelName: 'labelName',
319          matchers: [
320            {
321              name: 'labelName',
322              type: EqlSingle,
323              value: 'labelValue',
324            },
325            {
326              name: 'labelName',
327              type: Neq,
328              value: '',
329            },
330          ],
331        },
332      ],
333    },
334    {
335      title: 'autocomplete the labelValue associated to a labelName',
336      expr: '{labelName=""}',
337      pos: 12, // cursor is between the quotes
338      expectedContext: [
339        {
340          kind: ContextKind.LabelValue,
341          metricName: '',
342          labelName: 'labelName',
343          matchers: [
344            {
345              name: 'labelName',
346              type: EqlSingle,
347              value: '',
348            },
349          ],
350        },
351      ],
352    },
353    {
354      title: 'autocomplete the labelValue associated to a labelName 2',
355      expr: '{labelName="labelValue", labelName!=""}',
356      pos: 37, // cursor is between the quotes
357      expectedContext: [
358        {
359          kind: ContextKind.LabelValue,
360          metricName: '',
361          labelName: 'labelName',
362          matchers: [
363            {
364              name: 'labelName',
365              type: EqlSingle,
366              value: 'labelValue',
367            },
368            {
369              name: 'labelName',
370              type: Neq,
371              value: '',
372            },
373          ],
374        },
375      ],
376    },
377    {
378      title: 'autocomplete AggregateOpModifier or BinOp',
379      expr: 'sum() b',
380      pos: 7, // cursor is after the 'b'
381      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
382    },
383    {
384      title: 'autocomplete AggregateOpModifier or BinOp 2',
385      expr: 'sum(rate(foo[5m])) an',
386      pos: 21,
387      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
388    },
389    {
390      title: 'autocomplete AggregateOpModifier or BinOp or Offset',
391      expr: 'sum b',
392      pos: 5, // cursor is after 'b'
393      expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
394    },
395    {
396      title: 'autocomplete binOp',
397      expr: 'metric_name !',
398      pos: 13,
399      expectedContext: [{ kind: ContextKind.BinOp }],
400    },
401    {
402      title: 'autocomplete binOp 2',
403      expr: 'metric_name =',
404      pos: 13,
405      expectedContext: [{ kind: ContextKind.BinOp }],
406    },
407    {
408      title: 'autocomplete matchOp',
409      expr: 'go{instance=""}',
410      pos: 12, // cursor is after the 'equal'
411      expectedContext: [{ kind: ContextKind.MatchOp }],
412    },
413    {
414      title: 'autocomplete matchOp 2',
415      expr: 'metric_name{labelName!}',
416      pos: 22, // cursor is after '!'
417      expectedContext: [{ kind: ContextKind.MatchOp }],
418    },
419    {
420      title: 'autocomplete duration with offset',
421      expr: 'http_requests_total offset 5',
422      pos: 28,
423      expectedContext: [{ kind: ContextKind.Duration }],
424    },
425    {
426      title: 'autocomplete duration with offset',
427      expr: 'sum(http_requests_total{method="GET"} offset 4)',
428      pos: 46,
429      expectedContext: [{ kind: ContextKind.Duration }],
430    },
431    {
432      title: 'autocomplete offset or binOp',
433      expr: 'http_requests_total off',
434      pos: 23,
435      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
436    },
437    {
438      title: 'autocomplete offset or binOp 2',
439      expr: 'metric_name unle',
440      pos: 16,
441      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
442    },
443    {
444      title: 'autocomplete offset or binOp 3',
445      expr: 'http_requests_total{method="GET"} off',
446      pos: 37,
447      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
448    },
449    {
450      title: 'autocomplete offset or binOp 4',
451      expr: 'rate(foo[5m]) un',
452      pos: 16,
453      expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
454    },
455    {
456      title: 'not autocompleting duration for a matrixSelector',
457      expr: 'go[]',
458      pos: 3,
459      expectedContext: [],
460    },
461    {
462      title: 'not autocompleting duration for a matrixSelector 2',
463      expr: 'go{l1="l2"}[]',
464      pos: 12,
465      expectedContext: [],
466    },
467    {
468      title: 'autocomplete duration for a matrixSelector',
469      expr: 'go[5]',
470      pos: 4,
471      expectedContext: [{ kind: ContextKind.Duration }],
472    },
473    {
474      title: 'autocomplete duration for a matrixSelector 2',
475      expr: 'go[5d1]',
476      pos: 6,
477      expectedContext: [{ kind: ContextKind.Duration }],
478    },
479    {
480      title: 'autocomplete duration for a matrixSelector 3',
481      expr: 'rate(my_metric{l1="l2"}[25])',
482      pos: 26,
483      expectedContext: [{ kind: ContextKind.Duration }],
484    },
485    {
486      title: 'autocomplete duration for a matrixSelector 4',
487      expr: 'rate(my_metric{l1="l2"}[25d5])',
488      pos: 28,
489      expectedContext: [{ kind: ContextKind.Duration }],
490    },
491    {
492      title: 'autocomplete duration for a subQuery',
493      expr: 'go[5d:5]',
494      pos: 7,
495      expectedContext: [{ kind: ContextKind.Duration }],
496    },
497    {
498      title: 'autocomplete duration for a subQuery 2',
499      expr: 'go[5d:5d4]',
500      pos: 9,
501      expectedContext: [{ kind: ContextKind.Duration }],
502    },
503    {
504      title: 'autocomplete duration for a subQuery 3',
505      expr: 'rate(my_metric{l1="l2"}[25d:6])',
506      pos: 29,
507      expectedContext: [{ kind: ContextKind.Duration }],
508    },
509    {
510      title: 'autocomplete duration for a subQuery 4',
511      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
512      pos: 31,
513      expectedContext: [{ kind: ContextKind.Duration }],
514    },
515    {
516      title: 'autocomplete at modifiers',
517      expr: '1 @ s',
518      pos: 5,
519      expectedContext: [{ kind: ContextKind.AtModifiers }],
520    },
521  ];
522  testCases.forEach((value) => {
523    it(value.title, () => {
524      const state = createEditorState(value.expr);
525      const node = syntaxTree(state).resolve(value.pos, -1);
526      const result = analyzeCompletion(state, node);
527      chai.expect(value.expectedContext).to.deep.equal(result);
528    });
529  });
530});
531
532describe('computeStartCompletePosition test', () => {
533  const testCases = [
534    {
535      title: 'empty bracket',
536      expr: '{}',
537      pos: 1, // cursor is between the bracket
538      expectedStart: 1,
539    },
540    {
541      title: 'empty bracket 2',
542      expr: 'metricName{}',
543      pos: 11, // cursor is between the bracket
544      expectedStart: 11,
545    },
546    {
547      title: 'empty bracket 3',
548      expr: 'sum by()',
549      pos: 7, // cursor is between the bracket
550      expectedStart: 7,
551    },
552    {
553      title: 'empty bracket 4',
554      expr: 'sum by(test) ()',
555      pos: 14, // cursor is between the bracket
556      expectedStart: 14,
557    },
558    {
559      title: 'empty bracket 5',
560      expr: 'sum()',
561      pos: 4, // cursor is between the bracket
562      expectedStart: 4,
563    },
564    {
565      title: 'empty bracket 6',
566      expr: 'sum(rate())',
567      pos: 9, // cursor is between the bracket
568      expectedStart: 9,
569    },
570    {
571      title: 'bracket containing a substring',
572      expr: '{myL}',
573      pos: 4, // cursor is between the bracket
574      expectedStart: 1,
575    },
576    {
577      title: 'bracket containing a substring 2',
578      expr: '{myLabel="LabelValue",}',
579      pos: 22, // cursor is after the comma
580      expectedStart: 22,
581    },
582    {
583      title: 'bracket containing a substring 2',
584      expr: 'metricName{myL}',
585      pos: 14, // cursor is between the bracket
586      expectedStart: 11,
587    },
588    {
589      title: 'bracket containing a substring 3',
590      expr: 'metricName{myLabel="LabelValue",}',
591      pos: 32, // cursor is after the comma
592      expectedStart: 32,
593    },
594    {
595      title: 'bracket containing a substring 4',
596      expr: 'sum by(myL)',
597      pos: 10, // cursor is between the bracket
598      expectedStart: 7,
599    },
600    {
601      title: 'bracket containing a substring 5',
602      expr: 'sum by(myLabel,)',
603      pos: 15, // cursor is after the comma
604      expectedStart: 15,
605    },
606    {
607      title: 'bracket containing a substring 6',
608      expr: 'sum(ra)',
609      pos: 6, // cursor is between the bracket
610      expectedStart: 4,
611    },
612    {
613      title: 'bracket containing a substring 7',
614      expr: 'sum(rate(my))',
615      pos: 11, // cursor is between the bracket
616      expectedStart: 9,
617    },
618    {
619      title: 'start should not be at the beginning of the substring',
620      expr: 'metric_name{labelName!}',
621      pos: 22,
622      expectedStart: 21,
623    },
624    {
625      title: 'start should not be at the beginning of the substring 2',
626      expr: 'metric_name{labelName!="labelValue"}',
627      pos: 22,
628      expectedStart: 21,
629    },
630    {
631      title: 'start should be equal to the pos for the duration of an offset',
632      expr: 'http_requests_total offset 5',
633      pos: 28,
634      expectedStart: 28,
635    },
636    {
637      title: 'start should be equal to the pos for the duration of an offset 2',
638      expr: 'http_requests_total offset 587',
639      pos: 30,
640      expectedStart: 30,
641    },
642    {
643      title: 'start should be equal to the pos for the duration of an offset 3',
644      expr: 'http_requests_total offset 587',
645      pos: 29,
646      expectedStart: 29,
647    },
648    {
649      title: 'start should be equal to the pos for the duration of an offset 4',
650      expr: 'sum(http_requests_total{method="GET"} offset 4)',
651      pos: 46,
652      expectedStart: 46,
653    },
654    {
655      title: 'start should not be equal to the pos for the duration in a matrix selector',
656      expr: 'go[]',
657      pos: 3,
658      expectedStart: 0,
659    },
660    {
661      title: 'start should be equal to the pos for the duration in a matrix selector',
662      expr: 'go[5]',
663      pos: 4,
664      expectedStart: 4,
665    },
666    {
667      title: 'start should be equal to the pos for the duration in a matrix selector 2',
668      expr: 'go[5d5]',
669      pos: 6,
670      expectedStart: 6,
671    },
672    {
673      title: 'start should be equal to the pos for the duration in a matrix selector 3',
674      expr: 'rate(my_metric{l1="l2"}[25])',
675      pos: 26,
676      expectedStart: 26,
677    },
678    {
679      title: 'start should be equal to the pos for the duration in a matrix selector 4',
680      expr: 'rate(my_metric{l1="l2"}[25d5])',
681      pos: 28,
682      expectedStart: 28,
683    },
684    {
685      title: 'start should be equal to the pos for the duration in a subquery selector',
686      expr: 'go[5d:5]',
687      pos: 7,
688      expectedStart: 7,
689    },
690    {
691      title: 'start should be equal to the pos for the duration in a subquery selector 2',
692      expr: 'go[5d:5d5]',
693      pos: 9,
694      expectedStart: 9,
695    },
696    {
697      title: 'start should be equal to the pos for the duration in a subquery selector 3',
698      expr: 'rate(my_metric{l1="l2"}[25d:6])',
699      pos: 29,
700      expectedStart: 29,
701    },
702    {
703      title: 'start should be equal to the pos for the duration in a subquery selector 3',
704      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
705      pos: 31,
706      expectedStart: 31,
707    },
708  ];
709  testCases.forEach((value) => {
710    it(value.title, () => {
711      const state = createEditorState(value.expr);
712      const node = syntaxTree(state).resolve(value.pos, -1);
713      const result = computeStartCompletePosition(node, value.pos);
714      chai.expect(value.expectedStart).to.equal(result);
715    });
716  });
717});
718
719describe('autocomplete promQL test', () => {
720  beforeEach(() => {
721    mockPrometheusServer();
722  });
723  const testCases = [
724    {
725      title: 'offline empty expr',
726      expr: '',
727      pos: 0,
728      expectedResult: {
729        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
730        from: 0,
731        to: 0,
732        span: /^[a-zA-Z0-9_:]+$/,
733      },
734    },
735    {
736      title: 'offline simple function/aggregation/number autocompletion',
737      expr: 'go_',
738      pos: 3,
739      expectedResult: {
740        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
741        from: 0,
742        to: 3,
743        span: /^[a-zA-Z0-9_:]+$/,
744      },
745    },
746    {
747      title: 'offline function/aggregation autocompletion in aggregation',
748      expr: 'sum()',
749      pos: 4,
750      expectedResult: {
751        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
752        from: 4,
753        to: 4,
754        span: /^[a-zA-Z0-9_:]+$/,
755      },
756    },
757    {
758      title: 'offline function/aggregation autocompletion in aggregation 2',
759      expr: 'sum(ra)',
760      pos: 6,
761      expectedResult: {
762        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
763        from: 4,
764        to: 6,
765        span: /^[a-zA-Z0-9_:]+$/,
766      },
767    },
768    {
769      title: 'offline function/aggregation autocompletion in aggregation 3',
770      expr: 'sum(rate())',
771      pos: 9,
772      expectedResult: {
773        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
774        from: 9,
775        to: 9,
776        span: /^[a-zA-Z0-9_:]+$/,
777      },
778    },
779    {
780      title: 'offline function/aggregation autocompletion in aggregation 4',
781      expr:
782        'sum by (instance, job) ( sum_over(scrape_series_added[1h])) / sum by (instance, job) (sum_over_time(scrape_samples_scraped[1h])) > 0.1 and sum by(instance, job) (scrape_samples_scraped{) > 100',
783      pos: 33,
784      expectedResult: {
785        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
786        from: 25,
787        to: 33,
788        span: /^[a-zA-Z0-9_:]+$/,
789      },
790    },
791    {
792      title: 'autocomplete binOp modifier/metric/number',
793      expr: 'metric_name / ignor',
794      pos: 19,
795      expectedResult: {
796        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
797        from: 14,
798        to: 19,
799        span: /^[a-zA-Z0-9_:]+$/,
800      },
801    },
802    {
803      title: 'autocomplete binOp modifier/metric/number 2',
804      expr: 'sum(http_requests_total{method="GET"} / o)',
805      pos: 41,
806      expectedResult: {
807        options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
808        from: 40,
809        to: 41,
810        span: /^[a-zA-Z0-9_:]+$/,
811      },
812    },
813    {
814      title: 'offline autocomplete labelName return nothing',
815      expr: 'sum by ()',
816      pos: 8, // cursor is between the bracket
817      expectedResult: {
818        options: [],
819        from: 8,
820        to: 8,
821        span: /^[a-zA-Z0-9_:]+$/,
822      },
823    },
824    {
825      title: 'offline autocomplete labelName return nothing 2',
826      expr: 'sum by (myL)',
827      pos: 11, // cursor is between the bracket after the string myL
828      expectedResult: {
829        options: [],
830        from: 8,
831        to: 11,
832        span: /^[a-zA-Z0-9_:]+$/,
833      },
834    },
835    {
836      title: 'offline autocomplete labelName return nothing 3',
837      expr: 'sum by (myLabel1, myLab)',
838      pos: 23, // cursor is between the bracket after the string myLab
839      expectedResult: {
840        options: [],
841        from: 18,
842        to: 23,
843        span: /^[a-zA-Z0-9_:]+$/,
844      },
845    },
846    {
847      title: 'offline autocomplete labelName return nothing 4',
848      expr: 'metric_name{}',
849      pos: 12, // cursor is between the bracket
850      expectedResult: {
851        options: [],
852        from: 12,
853        to: 12,
854        span: /^[a-zA-Z0-9_:]+$/,
855      },
856    },
857    {
858      title: 'offline autocomplete labelName return nothing 5',
859      expr: '{}',
860      pos: 1, // cursor is between the bracket
861      expectedResult: {
862        options: [],
863        from: 1,
864        to: 1,
865        span: /^[a-zA-Z0-9_:]+$/,
866      },
867    },
868    {
869      title: 'offline autocomplete labelName return nothing 6',
870      expr: 'metric_name{myL}',
871      pos: 15, // cursor is between the bracket after the string myL
872      expectedResult: {
873        options: [],
874        from: 12,
875        to: 15,
876        span: /^[a-zA-Z0-9_:]+$/,
877      },
878    },
879    {
880      title: 'offline autocomplete labelName return nothing 7',
881      expr: '{myL}',
882      pos: 4, // cursor is between the bracket after the string myL
883      expectedResult: {
884        options: [],
885        from: 1,
886        to: 4,
887        span: /^[a-zA-Z0-9_:]+$/,
888      },
889    },
890    {
891      title: 'offline autocomplete labelValue return nothing',
892      expr: 'metric_name{labelName=""}',
893      pos: 23, // cursor is between the quotes
894      expectedResult: {
895        options: [],
896        from: 23,
897        to: 23,
898        span: /^[a-zA-Z0-9_:]+$/,
899      },
900    },
901    {
902      title: 'offline autocomplete labelValue return nothing 2',
903      expr: '{labelName=""}',
904      pos: 12, // cursor is between the quotes
905      expectedResult: {
906        options: [],
907        from: 12,
908        to: 12,
909        span: /^[a-zA-Z0-9_:]+$/,
910      },
911    },
912    {
913      title: 'offline autocomplete aggregate operation modifier or binary operator',
914      expr: 'sum() b',
915      pos: 7, // cursor is after 'b'
916      expectedResult: {
917        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
918        from: 6,
919        to: 7,
920        span: /^[a-zA-Z0-9_:]+$/,
921      },
922    },
923    {
924      title: 'offline autocomplete aggregate operation modifier or binary operator 2',
925      expr: 'sum(rate(foo[5m])) an',
926      pos: 21, // cursor is after the string 'an'
927      expectedResult: {
928        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
929        from: 19,
930        to: 21,
931        span: /^[a-zA-Z0-9_:]+$/,
932      },
933    },
934    {
935      title: 'offline autocomplete aggregate operation modifier or binary operator or offset',
936      expr: 'sum b',
937      pos: 5, // cursor is after 'b'
938      expectedResult: {
939        options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms, [{ label: 'offset' }]),
940        from: 4,
941        to: 5,
942        span: /^[a-zA-Z0-9_:]+$/,
943      },
944    },
945    {
946      title: 'offline autocomplete binOp',
947      expr: 'metric_name !',
948      pos: 13,
949      expectedResult: {
950        options: binOpTerms,
951        from: 12,
952        to: 13,
953        span: /^[a-zA-Z0-9_:]+$/,
954      },
955    },
956    {
957      title: 'offline autocomplete binOp 2',
958      expr: 'metric_name =',
959      pos: 13,
960      expectedResult: {
961        options: binOpTerms,
962        from: 12,
963        to: 13,
964        span: /^[a-zA-Z0-9_:]+$/,
965      },
966    },
967    {
968      title: 'offline autocomplete matchOp',
969      expr: 'go{instance=""}',
970      pos: 12, // cursor is after the 'equal'
971      expectedResult: {
972        options: matchOpTerms,
973        from: 11,
974        to: 12,
975        span: /^[a-zA-Z0-9_:]+$/,
976      },
977    },
978    {
979      title: 'offline autocomplete matchOp 2',
980      expr: 'metric_name{labelName!}',
981      pos: 22, // cursor is after '!'
982      expectedResult: {
983        options: matchOpTerms,
984        from: 21,
985        to: 22,
986        span: /^[a-zA-Z0-9_:]+$/,
987      },
988    },
989    {
990      title: 'offline autocomplete duration with offset',
991      expr: 'http_requests_total offset 5',
992      pos: 28,
993      expectedResult: {
994        options: durationTerms,
995        from: 28,
996        to: 28,
997        span: undefined,
998      },
999    },
1000    {
1001      title: 'offline autocomplete duration with offset 2',
1002      expr: 'sum(http_requests_total{method="GET"} offset 4)',
1003      pos: 46,
1004      expectedResult: {
1005        options: durationTerms,
1006        from: 46,
1007        to: 46,
1008        span: undefined,
1009      },
1010    },
1011    {
1012      title: 'offline autocomplete offset or binOp',
1013      expr: 'http_requests_total off',
1014      pos: 23,
1015      expectedResult: {
1016        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
1017        from: 20,
1018        to: 23,
1019        span: /^[a-zA-Z0-9_:]+$/,
1020      },
1021    },
1022    {
1023      title: 'offline autocomplete offset or binOp 2',
1024      expr: 'metric_name unle',
1025      pos: 16,
1026      expectedResult: {
1027        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
1028        from: 12,
1029        to: 16,
1030        span: /^[a-zA-Z0-9_:]+$/,
1031      },
1032    },
1033    {
1034      title: 'offline autocomplete offset or binOp 3',
1035      expr: 'http_requests_total{method="GET"} off',
1036      pos: 37,
1037      expectedResult: {
1038        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
1039        from: 34,
1040        to: 37,
1041        span: /^[a-zA-Z0-9_:]+$/,
1042      },
1043    },
1044    {
1045      title: 'offline autocomplete offset or binOp 4',
1046      expr: 'rate(foo[5m]) un',
1047      pos: 16,
1048      expectedResult: {
1049        options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
1050        from: 14,
1051        to: 16,
1052        span: /^[a-zA-Z0-9_:]+$/,
1053      },
1054    },
1055    {
1056      title: 'offline not autocompleting duration for a matrixSelector',
1057      expr: 'go[]',
1058      pos: 3,
1059      expectedResult: {
1060        options: [],
1061        from: 0,
1062        to: 3,
1063        span: /^[a-zA-Z0-9_:]+$/,
1064      },
1065    },
1066    {
1067      title: 'offline not autocompleting duration for a matrixSelector 2',
1068      expr: 'go{l1="l2"}[]',
1069      pos: 12,
1070      expectedResult: {
1071        options: [],
1072        from: 0,
1073        to: 12,
1074        span: /^[a-zA-Z0-9_:]+$/,
1075      },
1076    },
1077    {
1078      title: 'offline autocomplete duration for a matrixSelector',
1079      expr: 'go[5]',
1080      pos: 4,
1081      expectedResult: {
1082        options: durationTerms,
1083        from: 4,
1084        to: 4,
1085        span: undefined,
1086      },
1087    },
1088    {
1089      title: 'offline autocomplete duration for a matrixSelector 2',
1090      expr: 'go[5d1]',
1091      pos: 6,
1092      expectedResult: {
1093        options: durationTerms,
1094        from: 6,
1095        to: 6,
1096        span: undefined,
1097      },
1098    },
1099    {
1100      title: 'offline autocomplete duration for a matrixSelector 3',
1101      expr: 'rate(my_metric{l1="l2"}[25])',
1102      pos: 26,
1103      expectedResult: {
1104        options: durationTerms,
1105        from: 26,
1106        to: 26,
1107        span: undefined,
1108      },
1109    },
1110    {
1111      title: 'offline autocomplete duration for a matrixSelector 4',
1112      expr: 'rate(my_metric{l1="l2"}[25d5])',
1113      pos: 28,
1114      expectedResult: {
1115        options: durationTerms,
1116        from: 28,
1117        to: 28,
1118        span: undefined,
1119      },
1120    },
1121    {
1122      title: 'offline autocomplete duration for a subQuery',
1123      expr: 'go[5d:5]',
1124      pos: 7,
1125      expectedResult: {
1126        options: durationTerms,
1127        from: 7,
1128        to: 7,
1129        span: undefined,
1130      },
1131    },
1132    {
1133      title: 'offline autocomplete duration for a subQuery 2',
1134      expr: 'go[5d:5d4]',
1135      pos: 9,
1136      expectedResult: {
1137        options: durationTerms,
1138        from: 9,
1139        to: 9,
1140        span: undefined,
1141      },
1142    },
1143    {
1144      title: 'offline autocomplete duration for a subQuery 3',
1145      expr: 'rate(my_metric{l1="l2"}[25d:6])',
1146      pos: 29,
1147      expectedResult: {
1148        options: durationTerms,
1149        from: 29,
1150        to: 29,
1151        span: undefined,
1152      },
1153    },
1154    {
1155      title: 'offline autocomplete duration for a subQuery 4',
1156      expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
1157      pos: 31,
1158      expectedResult: {
1159        options: durationTerms,
1160        from: 31,
1161        to: 31,
1162        span: undefined,
1163      },
1164    },
1165    {
1166      title: 'offline autocomplete at modifiers',
1167      expr: '1 @ s',
1168      pos: 5,
1169      expectedResult: {
1170        options: atModifierTerms,
1171        from: 4,
1172        to: 5,
1173        span: /^[a-zA-Z0-9_:]+$/,
1174      },
1175    },
1176    {
1177      title: 'online autocomplete of metrics',
1178      expr: 'alert',
1179      pos: 5,
1180      conf: { remote: { url: 'http://localhost:8080' } },
1181      expectedResult: {
1182        options: ([] as Completion[]).concat(mockedMetricsTerms, functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
1183        from: 0,
1184        to: 5,
1185        span: /^[a-zA-Z0-9_:]+$/,
1186      },
1187    },
1188    {
1189      title: 'online autocomplete of label name corresponding to a metric',
1190      expr: 'alertmanager_alerts{}',
1191      pos: 20,
1192      conf: { remote: { url: 'http://localhost:8080' } },
1193      expectedResult: {
1194        options: [
1195          {
1196            label: 'env',
1197            type: 'constant',
1198          },
1199          {
1200            label: 'instance',
1201            type: 'constant',
1202          },
1203          {
1204            label: 'job',
1205            type: 'constant',
1206          },
1207          {
1208            label: 'state',
1209            type: 'constant',
1210          },
1211        ],
1212        from: 20,
1213        to: 20,
1214        span: /^[a-zA-Z0-9_:]+$/,
1215      },
1216    },
1217    {
1218      title: 'online autocomplete of label value corresponding to a metric and a label name',
1219      expr: 'alertmanager_alerts{env=""}',
1220      pos: 25,
1221      conf: { remote: { url: 'http://localhost:8080' } },
1222      expectedResult: {
1223        options: [
1224          {
1225            label: 'demo',
1226            type: 'text',
1227          },
1228        ],
1229        from: 25,
1230        to: 25,
1231        span: /^[a-zA-Z0-9_:]+$/,
1232      },
1233    },
1234  ];
1235  testCases.forEach((value) => {
1236    it(value.title, async () => {
1237      const state = createEditorState(value.expr);
1238      const context = new CompletionContext(state, value.pos, true);
1239      const completion = newCompleteStrategy(value.conf);
1240      const result = await completion.promQL(context);
1241      chai.expect(value.expectedResult).to.deep.equal(result);
1242    });
1243  });
1244});
1245