@prometheus-io/codemirror-promql
Version:
a CodeMirror mode for the PromQL language
1,627 lines (1,619 loc) • 53.7 kB
text/typescript
// Copyright 2021 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.
import { analyzeCompletion, computeStartCompletePosition, computeEndCompletePosition, ContextKind, durationWithUnitRegexp } from './hybrid';
import { createEditorState, mockedMetricsTerms, mockPrometheusServer } from '../test/utils-test';
import { Completion, CompletionContext } from '@codemirror/autocomplete';
import {
aggregateOpModifierTerms,
aggregateOpTerms,
atModifierTerms,
binOpModifierTerms,
binOpTerms,
durationTerms,
functionIdentifierTerms,
matchOpTerms,
numberTerms,
snippets,
} from './promql.terms';
import { EqlSingle, Neq } from '@prometheus-io/lezer-promql';
import { syntaxTree } from '@codemirror/language';
import { newCompleteStrategy } from './index';
import nock from 'nock';
describe('analyzeCompletion test', () => {
const testCases = [
{
title: 'empty expr',
expr: '',
pos: 0,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: '',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.Number },
],
},
{
title: 'simple metric & number autocompletion',
expr: 'go_',
pos: 3, // cursor is at the end of the expr
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: 'go_',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.Number },
],
},
{
title: 'metric/function/aggregation autocompletion',
expr: 'sum()',
pos: 4,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: '',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete AggregateOpModifier or BinOp after closing aggregation',
expr: 'sum()',
pos: 5, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion in incomplete function',
expr: 'sum(',
pos: 4,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: '',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete binOp after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion 2',
expr: 'sum(rat)',
pos: 7,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: 'rat',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'metric/function/aggregation autocompletion 3',
expr: 'sum(rate())',
pos: 9,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: '',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete AggregateOpModifier or BinOp after closing nested aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'autocomplete binOp after closing aggregation with existing modifier',
expr: 'sum by(job)(rate(foo[5m]))',
pos: 26, // cursor is after the closing bracket
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'metric/function/aggregation autocompletion 4',
expr: 'sum(rate(my_))',
pos: 12,
expectedContext: [
{
kind: ContextKind.MetricName,
metricName: 'my_',
},
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
],
},
{
title: 'autocomplete binOp modifier or metric or number',
expr: 'metric_name / ignor',
pos: 19,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'ignor' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
],
},
{
title: 'autocomplete binOp modifier or metric or number 2',
expr: 'sum(http_requests_total{method="GET"} / o)',
pos: 41,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'o' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 1',
expr: '1 > b)',
pos: 5,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 2',
expr: '1 == b)',
pos: 6,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 3',
expr: '1 != b)',
pos: 6,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 4',
expr: '1 > b)',
pos: 5,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 5',
expr: '1 >= b)',
pos: 6,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 6',
expr: '1 <= b)',
pos: 6,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'autocomplete bool/binOp modifier/metric/number 7',
expr: '1 < b)',
pos: 5,
expectedContext: [
{ kind: ContextKind.MetricName, metricName: 'b' },
{ kind: ContextKind.Function },
{ kind: ContextKind.Aggregation },
{ kind: ContextKind.BinOpModifier },
{ kind: ContextKind.Number },
{ kind: ContextKind.Bool },
],
},
{
title: 'starting to autocomplete labelName in aggregate modifier',
expr: 'sum by ()',
pos: 8, // cursor is between the bracket
expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
},
{
title: 'starting to autocomplete labelName in aggregate modifier with metric name',
expr: 'sum(up) by ()',
pos: 12, // cursor is between ()
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'up' }],
},
{
title: 'starting to autocomplete labelName in aggregate modifier with metric name in front',
expr: 'sum by ()(up)',
pos: 8, // cursor is between ()
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'up' }],
},
{
title: 'continue to autocomplete labelName in aggregate modifier',
expr: 'sum by (myL)',
pos: 11, // cursor is between the bracket after the string myL
expectedContext: [{ kind: ContextKind.LabelName }],
},
{
title: 'continue to autocomplete QuotedLabelName in aggregate modifier',
expr: 'sum by ("myL")',
pos: 12, // cursor is between the bracket after the string myL
expectedContext: [{ kind: ContextKind.LabelName }],
},
{
title: 'autocomplete labelName in a list',
expr: 'sum by (myLabel1,)',
pos: 17, // cursor is between the bracket after the string myLab
expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
},
{
title: 'autocomplete labelName in a list 2',
expr: 'sum by (myLabel1, myLab)',
pos: 23, // cursor is between the bracket after the string myLab
expectedContext: [{ kind: ContextKind.LabelName }],
},
{
title: 'autocomplete labelName in a list 2',
expr: 'sum by ("myLabel1", "myLab")',
pos: 27, // cursor is between the bracket after the string myLab
expectedContext: [{ kind: ContextKind.LabelName }],
},
{
title: 'autocomplete labelName associated to a metric',
expr: 'metric_name{}',
pos: 12, // cursor is between the bracket
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
},
{
title: 'autocomplete labelName that defined a metric',
expr: '{}',
pos: 1, // cursor is between the bracket
expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
},
{
title: 'continue to autocomplete labelName associated to a metric',
expr: 'metric_name{myL}',
pos: 15, // cursor is between the bracket after the string myL
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
},
{
title: 'continue to autocomplete labelName associated to a metric 2',
expr: 'metric_name{myLabel="labelValue",}',
pos: 33, // cursor is between the bracket after the comma
expectedContext: [{ kind: ContextKind.LabelName, metricName: 'metric_name' }],
},
{
title: 'no label suggestions after closing matcher',
expr: 'up{job="prometheus"}',
pos: 20, // cursor is right after the closing curly bracket
expectedContext: [],
},
{
title: 'continue autocomplete labelName that defined a metric',
expr: '{myL}',
pos: 4, // cursor is between the bracket after the string myL
expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
},
{
title: 'continue autocomplete labelName that defined a metric 2',
expr: '{myLabel="labelValue",}',
pos: 22, // cursor is between the bracket after the comma
expectedContext: [{ kind: ContextKind.LabelName, metricName: '' }],
},
{
title: 'continue to autocomplete quoted labelName associated to a metric',
expr: '{"metric_"}',
pos: 10, // cursor is between the bracket after the string metric_
expectedContext: [{ kind: ContextKind.MetricName, metricName: 'metric_' }],
},
{
title: 'autocomplete the labelValue with metricName + labelName',
expr: 'metric_name{labelName=""}',
pos: 23, // cursor is between the quotes
expectedContext: [
{
kind: ContextKind.LabelValue,
metricName: 'metric_name',
labelName: 'labelName',
matchers: [
{
name: 'labelName',
type: EqlSingle,
value: '',
},
],
},
],
},
{
title: 'autocomplete the labelValue with metricName + labelName 2',
expr: 'metric_name{labelName="labelValue", labelName!=""}',
pos: 48, // cursor is between the quotes
expectedContext: [
{
kind: ContextKind.LabelValue,
metricName: 'metric_name',
labelName: 'labelName',
matchers: [
{
name: 'labelName',
type: EqlSingle,
value: 'labelValue',
},
{
name: 'labelName',
type: Neq,
value: '',
},
],
},
],
},
{
title: 'autocomplete the labelValue with metricName + quoted labelName',
expr: 'metric_name{labelName="labelValue", "labelName"!=""}',
pos: 50, // cursor is between the quotes
expectedContext: [
{
kind: ContextKind.LabelValue,
metricName: 'metric_name',
labelName: 'labelName',
matchers: [
{
name: 'labelName',
type: Neq,
value: '',
},
{
name: 'labelName',
type: EqlSingle,
value: 'labelValue',
},
],
},
],
},
{
title: 'autocomplete the labelValue associated to a labelName',
expr: '{labelName=""}',
pos: 12, // cursor is between the quotes
expectedContext: [
{
kind: ContextKind.LabelValue,
metricName: '',
labelName: 'labelName',
matchers: [
{
name: 'labelName',
type: EqlSingle,
value: '',
},
],
},
],
},
{
title: 'autocomplete the labelValue associated to a labelName 2',
expr: '{labelName="labelValue", labelName!=""}',
pos: 37, // cursor is between the quotes
expectedContext: [
{
kind: ContextKind.LabelValue,
metricName: '',
labelName: 'labelName',
matchers: [
{
name: 'labelName',
type: EqlSingle,
value: 'labelValue',
},
{
name: 'labelName',
type: Neq,
value: '',
},
],
},
],
},
{
title: 'autocomplete AggregateOpModifier or BinOp',
expr: 'sum() b',
pos: 7, // cursor is after the 'b'
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'autocomplete AggregateOpModifier or BinOp 2',
expr: 'sum(rate(foo[5m])) an',
pos: 21,
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }],
},
{
title: 'autocomplete AggregateOpModifier or BinOp or Offset',
expr: 'sum b',
pos: 5, // cursor is after 'b'
expectedContext: [{ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'autocomplete binOp',
expr: 'metric_name !',
pos: 13,
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'autocomplete binOp 2',
expr: 'metric_name =',
pos: 13,
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'autocomplete matchOp',
expr: 'go{instance=""}',
pos: 12, // cursor is after the 'equal'
expectedContext: [{ kind: ContextKind.MatchOp }],
},
{
title: 'autocomplete matchOp 2',
expr: 'metric_name{labelName!}',
pos: 22, // cursor is after '!'
expectedContext: [{ kind: ContextKind.MatchOp }],
},
{
title: 'autocomplete matchOp 3',
expr: 'metric_name{"labelName"!}',
pos: 24, // cursor is after '!'
expectedContext: [{ kind: ContextKind.BinOp }],
},
{
title: 'autocomplete duration with offset',
expr: 'http_requests_total offset 5',
pos: 28,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration with offset',
expr: 'sum(http_requests_total{method="GET"} offset 4)',
pos: 46,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete offset or binOp',
expr: 'http_requests_total off',
pos: 23,
expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'autocomplete offset or binOp 2',
expr: 'metric_name unle',
pos: 16,
expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'autocomplete offset or binOp 3',
expr: 'http_requests_total{method="GET"} off',
pos: 37,
expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'autocomplete offset or binOp 4',
expr: 'rate(foo[5m]) un',
pos: 16,
expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'autocomplete offset or binop 5',
expr: 'sum(http_requests_total{method="GET"} off)',
pos: 41,
expectedContext: [{ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }],
},
{
title: 'not autocompleting duration for a matrixSelector',
expr: 'go[]',
pos: 3,
expectedContext: [],
},
{
title: 'not autocompleting duration for a matrixSelector 2',
expr: 'go{l1="l2"}[]',
pos: 12,
expectedContext: [],
},
{
title: 'autocomplete duration for a matrixSelector',
expr: 'go[5]',
pos: 4,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a matrixSelector 2',
expr: 'go[5d1]',
pos: 6,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a matrixSelector 3',
expr: 'rate(my_metric{l1="l2"}[25])',
pos: 26,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a matrixSelector 4',
expr: 'rate(my_metric{l1="l2"}[25d5])',
pos: 28,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'do not autocomplete duration when unit already present in matrixSelector',
expr: 'rate(foo[5m])',
pos: 10,
expectedContext: [],
},
{
title: 'do not autocomplete duration when multi char unit already present in matrixSelector',
expr: 'rate(foo[5ms])',
pos: 10,
expectedContext: [],
},
{
title: 'autocomplete duration for a subQuery',
expr: 'go[5d:5]',
pos: 7,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a subQuery 2',
expr: 'go[5d:5d4]',
pos: 9,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a subQuery 3',
expr: 'rate(my_metric{l1="l2"}[25d:6])',
pos: 29,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete duration for a subQuery 4',
expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
pos: 31,
expectedContext: [{ kind: ContextKind.Duration }],
},
{
title: 'autocomplete at modifiers',
expr: '1 @ s',
pos: 5,
expectedContext: [{ kind: ContextKind.AtModifiers }],
},
{
title: 'autocomplete topk params',
expr: 'topk()',
pos: 5,
expectedContext: [{ kind: ContextKind.Number }],
},
{
title: 'autocomplete topk params 2',
expr: 'topk(inf,)',
pos: 9,
expectedContext: [{ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
{
title: 'autocomplete topk params 3',
expr: 'topk(inf,r)',
pos: 10,
expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
{
title: 'autocomplete topk params 4',
expr: 'topk by(instance) ()',
pos: 19,
expectedContext: [{ kind: ContextKind.Number }],
},
{
title: 'autocomplete topk params 5',
expr: 'topk by(instance) (inf,r)',
pos: 24,
expectedContext: [{ kind: ContextKind.MetricName, metricName: 'r' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }],
},
];
testCases.forEach((value) => {
it(value.title, () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = analyzeCompletion(state, node, value.pos);
expect(result).toEqual(value.expectedContext);
});
});
});
describe('durationWithUnitRegexp test', () => {
it('should match complete durations with units', () => {
const testCases = [
{ input: '5m', expected: true },
{ input: '30s', expected: true },
{ input: '1h', expected: true },
{ input: '500ms', expected: true },
{ input: '2d', expected: true },
{ input: '1w', expected: true },
{ input: '1y', expected: true },
{ input: '1d2h', expected: true },
{ input: '2h30m', expected: true },
{ input: '1h2m3s', expected: true },
{ input: '250ms2s', expected: true },
{ input: '2h3m4s5ms', expected: true },
{ input: '5', expected: false },
{ input: '5m5', expected: false },
{ input: 'm', expected: false },
{ input: 'd', expected: false },
{ input: '', expected: false },
{ input: '1hms', expected: false },
{ input: '2x', expected: false },
];
testCases.forEach(({ input, expected }) => {
expect(durationWithUnitRegexp.test(input)).toBe(expected);
});
});
it('should not match durations without units or partial units', () => {
const testCases = ['5', '30', '100', '5m5', 'm', 'd'];
testCases.forEach((input) => {
expect(durationWithUnitRegexp.test(input)).toBe(false);
});
});
});
describe('computeStartCompletePosition test', () => {
const testCases = [
{
title: 'empty bracket',
expr: '{}',
pos: 1, // cursor is between the bracket
expectedStart: 1,
},
{
title: 'empty bracket 2',
expr: 'metricName{}',
pos: 11, // cursor is between the bracket
expectedStart: 11,
},
{
title: 'empty bracket 3',
expr: 'sum by()',
pos: 7, // cursor is between the bracket
expectedStart: 7,
},
{
title: 'empty bracket 4',
expr: 'sum by(test) ()',
pos: 14, // cursor is between the bracket
expectedStart: 14,
},
{
title: 'empty bracket 5',
expr: 'sum()',
pos: 4, // cursor is between the bracket
expectedStart: 4,
},
{
title: 'empty bracket 6',
expr: 'sum(rate())',
pos: 9, // cursor is between the bracket
expectedStart: 9,
},
{
title: 'start should be equal to the pos after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedStart: 13,
},
{
title: 'start should be equal to the pos after closing aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedStart: 18,
},
{
title: 'bracket containing a substring',
expr: '{myL}',
pos: 4, // cursor is between the bracket
expectedStart: 1,
},
{
title: 'bracket containing a substring 2',
expr: '{myLabel="LabelValue",}',
pos: 22, // cursor is after the comma
expectedStart: 22,
},
{
title: 'bracket containing a substring 2',
expr: 'metricName{myL}',
pos: 14, // cursor is between the bracket
expectedStart: 11,
},
{
title: 'bracket containing a substring 3',
expr: 'metricName{myLabel="LabelValue",}',
pos: 32, // cursor is after the comma
expectedStart: 32,
},
{
title: 'bracket containing a substring 4',
expr: 'sum by(myL)',
pos: 10, // cursor is between the bracket
expectedStart: 7,
},
{
title: 'bracket containing a substring 5',
expr: 'sum by(myLabel,)',
pos: 15, // cursor is after the comma
expectedStart: 15,
},
{
title: 'bracket containing a substring 6',
expr: 'sum(ra)',
pos: 6, // cursor is between the bracket
expectedStart: 4,
},
{
title: 'bracket containing a substring 7',
expr: 'sum(rate(my))',
pos: 11, // cursor is between the bracket
expectedStart: 9,
},
{
title: 'start should not be at the beginning of the substring',
expr: 'metric_name{labelName!}',
pos: 22,
expectedStart: 21,
},
{
title: 'start should not be at the beginning of the substring 2',
expr: 'metric_name{labelName!="labelValue"}',
pos: 22,
expectedStart: 21,
},
{
title: 'start should be equal to the pos for the duration of an offset',
expr: 'http_requests_total offset 5',
pos: 28,
expectedStart: 28,
},
{
title: 'start should be equal to the pos for the duration of an offset 2',
expr: 'http_requests_total offset 587',
pos: 30,
expectedStart: 30,
},
{
title: 'start should be equal to the pos for the duration of an offset 3',
expr: 'http_requests_total offset 587',
pos: 29,
expectedStart: 29,
},
{
title: 'start should be equal to the pos for the duration of an offset 4',
expr: 'sum(http_requests_total{method="GET"} offset 4)',
pos: 46,
expectedStart: 46,
},
{
title: 'start should not be equal to the pos for the duration in a matrix selector',
expr: 'go[]',
pos: 3,
expectedStart: 0,
},
{
title: 'start should be equal to the pos for the duration in a matrix selector',
expr: 'go[5]',
pos: 4,
expectedStart: 4,
},
{
title: 'start should be equal to the pos for the duration in a matrix selector 2',
expr: 'go[5d5]',
pos: 6,
expectedStart: 6,
},
{
title: 'start should be equal to the pos for the duration in a matrix selector 3',
expr: 'rate(my_metric{l1="l2"}[25])',
pos: 26,
expectedStart: 26,
},
{
title: 'start should be equal to the pos for the duration in a matrix selector 4',
expr: 'rate(my_metric{l1="l2"}[25d5])',
pos: 28,
expectedStart: 28,
},
{
title: 'start should be equal to the pos for the duration in a subquery selector',
expr: 'go[5d:5]',
pos: 7,
expectedStart: 7,
},
{
title: 'start should be equal to the pos for the duration in a subquery selector 2',
expr: 'go[5d:5d5]',
pos: 9,
expectedStart: 9,
},
{
title: 'start should be equal to the pos for the duration in a subquery selector 3',
expr: 'rate(my_metric{l1="l2"}[25d:6])',
pos: 29,
expectedStart: 29,
},
{
title: 'start should be equal to the pos for the duration in a subquery selector 3',
expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
pos: 31,
expectedStart: 31,
},
];
testCases.forEach((value) => {
it(value.title, () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = computeStartCompletePosition(state, node, value.pos);
expect(result).toEqual(value.expectedStart);
});
});
});
describe('computeEndCompletePosition test', () => {
const testCases = [
{
title: 'cursor at end of metric name',
expr: 'metric_name',
pos: 11, // cursor is at the end
expectedEnd: 11,
},
{
title: 'cursor in middle of metric name - should extend to end',
expr: 'coredns_cache_hits_total',
pos: 14, // cursor is after 'coredns_cache_' (before 'hits')
expectedEnd: 24, // should extend to end of 'coredns_cache_hits_total'
},
{
title: 'cursor in middle of metric name inside rate() - should extend to end',
expr: 'rate(coredns_cache_hits_total[2m])',
pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
expectedEnd: 29, // should extend to end of 'coredns_cache_hits_total'
},
{
title: 'cursor in middle of metric name inside sum(rate()) - should extend to end',
expr: 'sum(rate(coredns_cache_hits_total[2m]))',
pos: 24, // cursor is after 'coredns_cache_' (before 'hits')
expectedEnd: 33, // should extend to end of 'coredns_cache_hits_total'
},
{
title: 'cursor at beginning of metric name - should extend to end',
expr: 'metric_name',
pos: 1, // cursor after 'm'
expectedEnd: 11,
},
{
title: 'cursor in middle of incomplete function name - should extend to end',
expr: 'sum_ov',
pos: 4, // cursor after 'sum_' (before 'ov')
expectedEnd: 6, // should extend to end of 'sum_ov'
},
{
title: 'cursor in middle of incomplete function name within aggregator - should extend to end',
expr: 'sum(sum_ov(foo[5m]))',
pos: 8, // cursor after 'sum_' (before 'ov')
expectedEnd: 10, // should extend to end of 'sum_ov'
},
{
title: 'empty bracket - ends before the closing bracket',
expr: '{}',
pos: 1,
expectedEnd: 1,
},
{
title: 'cursor in label matchers - ends before the closing bracket',
expr: 'metric_name{label="value"}',
pos: 12, // cursor after '{'
expectedEnd: 25,
},
{
title: 'cursor in middle of label name in grouping clause - should extend to end',
expr: 'sum by (instance_name)',
pos: 12, // cursor after 'inst' (before 'ance')
expectedEnd: 21, // should extend to end of 'instance_name'
},
{
title: 'cursor in middle of label name in label matcher - should extend to end',
expr: 'metric{instance_name="value"}',
pos: 11, // cursor after 'inst' (before 'ance')
expectedEnd: 20, // should extend to end of 'instance_name'
},
{
title: 'cursor in middle of label name in on() modifier - should extend to end',
expr: 'a / on(instance_name) b',
pos: 11, // cursor after 'inst' (before 'ance')
expectedEnd: 20, // should extend to end of 'instance_name'
},
{
title: 'cursor in middle of label name in ignoring() modifier - should extend to end',
expr: 'a / ignoring(instance_name) b',
pos: 17, // cursor after 'inst' (before 'ance')
expectedEnd: 26, // should extend to end of 'instance_name'
},
{
title: 'cursor in middle of function name rate - should extend to end',
expr: 'rate(foo[5m])',
pos: 2, // cursor after 'ra' (before 'te')
expectedEnd: 4, // should extend to end of 'rate'
},
{
title: 'cursor in middle of function name histogram_quantile - should extend to end',
expr: 'histogram_quantile(0.9, rate(foo[5m]))',
pos: 10, // cursor after 'histogram_' (before 'quantile')
expectedEnd: 18, // should extend to end of 'histogram_quantile'
},
{
title: 'cursor in middle of aggregator sum - should extend to end',
expr: 'sum(rate(foo[5m]))',
pos: 2, // cursor after 'su' (before 'm')
expectedEnd: 3, // should extend to end of 'sum'
},
{
title: 'cursor in middle of aggregator count_values - should extend to end',
expr: 'count_values("label", foo)',
pos: 6, // cursor after 'count_' (before 'values')
expectedEnd: 12, // should extend to end of 'count_values'
},
{
title: 'cursor in middle of nested function - should extend to end',
expr: 'sum(rate(foo[5m]))',
pos: 6, // cursor after 'ra' inside rate (before 'te')
expectedEnd: 8, // should extend to end of 'rate'
},
{
title: 'cursor at beginning of aggregator - should extend to end',
expr: 'avg by (instance) (rate(foo[5m]))',
pos: 1, // cursor after 'a' (before 'vg')
expectedEnd: 3, // should extend to end of 'avg'
},
{
title: 'cursor in middle of function name with binary op - should extend to end',
expr: 'rate(foo[5m]) / irate(bar[5m])',
pos: 17, // cursor after 'ir' inside irate (before 'ate')
expectedEnd: 21, // should extend to end of 'irate'
},
{
title: 'error node - returns pos (cursor position)',
expr: 'metric_name !',
pos: 13, // cursor at '!' (error node)
expectedEnd: 13, // error node returns pos
},
{
title: 'end should be equal to the pos after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedEnd: 13,
},
{
title: 'end should be equal to the pos after closing aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedEnd: 18,
},
];
testCases.forEach((value) => {
it(value.title, () => {
const state = createEditorState(value.expr);
const node = syntaxTree(state).resolve(value.pos, -1);
const result = computeEndCompletePosition(state, node, value.pos);
expect(result).toEqual(value.expectedEnd);
});
});
});
describe('autocomplete promQL test', () => {
beforeEach(() => {
mockPrometheusServer();
});
const testCases = [
{
title: 'offline empty expr',
expr: '',
pos: 0,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
from: 0,
to: 0,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline simple function/aggregation/number autocompletion',
expr: 'go_',
pos: 3,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
from: 0,
to: 3,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation',
expr: 'sum()',
pos: 4,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 4,
to: 4,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator after closing aggregation',
expr: 'sum()',
pos: 5, // cursor is after the closing bracket
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 5,
to: 5,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete binary operator after closing function',
expr: 'rate(foo[5m])',
pos: 13, // cursor is after the closing bracket
expectedResult: {
options: binOpTerms,
from: 13,
to: 13,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation 2',
expr: 'sum(ra)',
pos: 6,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 4,
to: 6,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'cursor in middle of metric name - to should extend to end (issue #15839)',
expr: 'sum(coredns_cache_hits_total)',
pos: 18, // cursor is after 'coredns_cache_' (before 'hits')
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 4,
to: 28, // should extend to end of 'coredns_cache_hits_total'
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'cursor in middle of metric name inside rate() - to should extend to end (issue #15839)',
expr: 'rate(coredns_cache_hits_total[2m])',
pos: 19, // cursor is after 'coredns_cache_' (before 'hits')
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 5,
to: 29, // should extend to end of 'coredns_cache_hits_total'
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation 3',
expr: 'sum(rate())',
pos: 9,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 9,
to: 9,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator after closing nested aggregation',
expr: 'sum(rate(foo[5m]))',
pos: 18, // cursor is after the closing bracket
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 18,
to: 18,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline function/aggregation autocompletion in aggregation 4',
expr: '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',
pos: 33,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, snippets),
from: 25,
to: 33,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'autocomplete binOp modifier/metric/number',
expr: 'metric_name / ignor',
pos: 19,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
from: 14,
to: 19,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'autocomplete binOp modifier/metric/number 2',
expr: 'sum(http_requests_total{method="GET"} / o)',
pos: 41,
expectedResult: {
options: ([] as Completion[]).concat(functionIdentifierTerms, aggregateOpTerms, binOpModifierTerms, numberTerms, snippets),
from: 40,
to: 41,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing',
expr: 'sum by ()',
pos: 8, // cursor is between the bracket
expectedResult: {
options: [],
from: 8,
to: 8,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 2',
expr: 'sum by (myL)',
pos: 11, // cursor is between the bracket after the string myL
expectedResult: {
options: [],
from: 8,
to: 11,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 3',
expr: 'sum by (myLabel1, myLab)',
pos: 23, // cursor is between the bracket after the string myLab
expectedResult: {
options: [],
from: 18,
to: 23,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 4',
expr: 'metric_name{}',
pos: 12, // cursor is between the bracket
expectedResult: {
options: [],
from: 12,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 5',
expr: '{}',
pos: 1, // cursor is between the bracket
expectedResult: {
options: [],
from: 1,
to: 1,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 6',
expr: 'metric_name{myL}',
pos: 15, // cursor is between the bracket after the string myL
expectedResult: {
options: [],
from: 12,
to: 15,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelName return nothing 7',
expr: '{myL}',
pos: 4, // cursor is between the bracket after the string myL
expectedResult: {
options: [],
from: 1,
to: 4,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelValue return nothing',
expr: 'metric_name{labelName=""}',
pos: 23, // cursor is between the quotes
expectedResult: {
options: [],
from: 23,
to: 23,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete labelValue return nothing 2',
expr: '{labelName=""}',
pos: 12, // cursor is between the quotes
expectedResult: {
options: [],
from: 12,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator',
expr: 'sum() b',
pos: 7, // cursor is after 'b'
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 6,
to: 7,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator 2',
expr: 'sum(rate(foo[5m])) an',
pos: 21, // cursor is after the string 'an'
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms),
from: 19,
to: 21,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete aggregate operation modifier or binary operator or offset',
expr: 'sum b',
pos: 5, // cursor is after 'b'
expectedResult: {
options: ([] as Completion[]).concat(aggregateOpModifierTerms, binOpTerms, [{ label: 'offset' }]),
from: 4,
to: 5,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete binOp',
expr: 'metric_name !',
pos: 13,
expectedResult: {
options: binOpTerms,
from: 12,
to: 13,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete binOp 2',
expr: 'metric_name =',
pos: 13,
expectedResult: {
options: binOpTerms,
from: 12,
to: 13,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete matchOp',
expr: 'go{instance=""}',
pos: 12, // cursor is after the 'equal'
expectedResult: {
options: matchOpTerms,
from: 11,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete matchOp 2',
expr: 'metric_name{labelName!}',
pos: 22, // cursor is after '!'
expectedResult: {
options: matchOpTerms,
from: 21,
to: 22,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete duration with offset',
expr: 'http_requests_total offset 5',
pos: 28,
expectedResult: {
options: durationTerms,
from: 28,
to: 28,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration with offset 2',
expr: 'sum(http_requests_total{method="GET"} offset 4)',
pos: 46,
expectedResult: {
options: durationTerms,
from: 46,
to: 46,
validFor: undefined,
},
},
{
title: 'offline autocomplete offset or binOp',
expr: 'http_requests_total off',
pos: 23,
expectedResult: {
options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
from: 20,
to: 23,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete offset or binOp 2',
expr: 'metric_name unle',
pos: 16,
expectedResult: {
options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
from: 12,
to: 16,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete offset or binOp 3',
expr: 'http_requests_total{method="GET"} off',
pos: 37,
expectedResult: {
options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
from: 34,
to: 37,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete offset or binOp 4',
expr: 'rate(foo[5m]) un',
pos: 16,
expectedResult: {
options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
from: 14,
to: 16,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'autocomplete offset or binop 5',
expr: 'sum(http_requests_total{method="GET"} off)',
pos: 41,
expectedResult: {
options: ([] as Completion[]).concat(binOpTerms, [{ label: 'offset' }]),
from: 38,
to: 41,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline not autocompleting duration for a matrixSelector',
expr: 'go[]',
pos: 3,
expectedResult: {
options: [],
from: 0,
to: 3,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline not autocompleting duration for a matrixSelector 2',
expr: 'go{l1="l2"}[]',
pos: 12,
expectedResult: {
options: [],
from: 0,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete duration for a matrixSelector',
expr: 'go[5]',
pos: 4,
expectedResult: {
options: durationTerms,
from: 4,
to: 4,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a matrixSelector 2',
expr: 'go[5d1]',
pos: 6,
expectedResult: {
options: durationTerms,
from: 6,
to: 6,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a matrixSelector 3',
expr: 'rate(my_metric{l1="l2"}[25])',
pos: 26,
expectedResult: {
options: durationTerms,
from: 26,
to: 26,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a matrixSelector 4',
expr: 'rate(my_metric{l1="l2"}[25d5])',
pos: 28,
expectedResult: {
options: durationTerms,
from: 28,
to: 28,
validFor: undefined,
},
},
{
title: 'offline do not autocomplete duration when unit already present in matrixSelector',
expr: 'rate(foo[5m])',
pos: 10,
expectedResult: {
options: [],
from: 10,
to: 11,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline do not autocomplete duration when multi char unit already present in matrixSelector',
expr: 'rate(foo[5ms])',
pos: 10,
expectedResult: {
options: [],
from: 10,
to: 12,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'offline autocomplete duration for a subQuery',
expr: 'go[5d:5]',
pos: 7,
expectedResult: {
options: durationTerms,
from: 7,
to: 7,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a subQuery 2',
expr: 'go[5d:5d4]',
pos: 9,
expectedResult: {
options: durationTerms,
from: 9,
to: 9,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a subQuery 3',
expr: 'rate(my_metric{l1="l2"}[25d:6])',
pos: 29,
expectedResult: {
options: durationTerms,
from: 29,
to: 29,
validFor: undefined,
},
},
{
title: 'offline autocomplete duration for a subQuery 4',
expr: 'rate(my_metric{l1="l2"}[25d:6d5])',
pos: 31,
expectedResult: {
options: durationTerms,
from: 31,
to: 31,
validFor: undefined,
},
},
{
title: 'offline autocomplete at modifiers',
expr: '1 @ s',
pos: 5,
expectedResult: {
options: atModifierTerms,
from: 4,
to: 5,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'online autocomplete of metrics',
expr: 'alert',
pos: 5,
conf: { remote: { url: 'http://localhost:8080' } },
expectedResult: {
options: ([] as Completion[]).concat(mockedMetricsTerms, functionIdentifierTerms, aggregateOpTerms, numberTerms, snippets),
from: 0,
to: 5,
validFor: /^[a-zA-Z0-9_:]+$/,
},
},
{
title: 'online autocomplete of label name corresponding to a metric',