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 {
16  Add,
17  AggregateExpr,
18  BinaryExpr,
19  Div,
20  Eql,
21  Expr,
22  FunctionCall,
23  FunctionCallArgs,
24  FunctionCallBody,
25  Gte,
26  Gtr,
27  Lss,
28  Lte,
29  Mod,
30  Mul,
31  Neq,
32  NumberLiteral,
33  Sub,
34  VectorSelector,
35} from '../grammar/parser.terms';
36import { createEditorState } from '../test/utils.test';
37import { containsAtLeastOneChild, containsChild, retrieveAllRecursiveNodes, walkBackward, walkThrough } from './path-finder';
38import { SyntaxNode } from '@lezer/common';
39import { syntaxTree } from '@codemirror/language';
40
41describe('walkThrough test', () => {
42  const testCases = [
43    {
44      title: 'should return the node when no path is given',
45      expr: '1 > bool 2',
46      pos: 0,
47      expectedNode: 'PromQL',
48      path: [] as number[],
49      expectedDoc: '1 > bool 2',
50    },
51    {
52      title: 'should find the path',
53      expr: "100 * (1 - avg by(instance)(irate(node_cpu{mode='idle'}[5m])))",
54      pos: 11,
55      path: [Expr, NumberLiteral],
56      // for the moment the function walkThrough is not able to find the following path.
57      // That's because the function is iterating through the tree by searching the first possible node that matched
58      // the node ID path[i].
59      // So for the current expression, and the given position we are in the sub expr (1 - avg ...).
60      // Expr is matching 1 and not avg.
61      // TODO fix this issue
62      // path: [Expr, AggregateExpr, AggregateOp, Avg],
63      expectedNode: NumberLiteral,
64      expectedDoc: '1',
65    },
66    {
67      title: 'should not find the path',
68      expr: 'topk(10, count by (job)({__name__=~".+"}))',
69      pos: 12,
70      path: [Expr, BinaryExpr],
71      expectedNode: undefined,
72      expectedDoc: undefined,
73    },
74    {
75      title: 'should find a node in a recursive node definition',
76      expr: 'rate(1, 2, 3)',
77      pos: 0,
78      path: [Expr, FunctionCall, FunctionCallBody, FunctionCallArgs, FunctionCallArgs, Expr, NumberLiteral],
79      expectedNode: NumberLiteral,
80      expectedDoc: '2',
81    },
82  ];
83  testCases.forEach((value) => {
84    it(value.title, () => {
85      const state = createEditorState(value.expr);
86      const subTree = syntaxTree(state).resolve(value.pos, -1);
87      const node = walkThrough(subTree, ...value.path);
88      if (typeof value.expectedNode === 'number') {
89        chai.expect(value.expectedNode).to.equal(node?.type.id);
90      } else {
91        chai.expect(value.expectedNode).to.equal(node?.type.name);
92      }
93      if (node) {
94        chai.expect(value.expectedDoc).to.equal(state.sliceDoc(node.from, node.to));
95      }
96    });
97  });
98});
99
100describe('containsAtLeastOneChild test', () => {
101  const testCases = [
102    {
103      title: 'should not find a node if none is defined',
104      expr: '1 > 2',
105      pos: 3,
106      expectedResult: false,
107      walkThrough: [],
108      child: [],
109    },
110    {
111      title: 'should find a node in the given list',
112      expr: '1 > 2',
113      pos: 0,
114      walkThrough: [Expr, BinaryExpr],
115      child: [Eql, Neq, Lte, Lss, Gte, Gtr],
116      expectedResult: true,
117    },
118    {
119      title: 'should not find a node in the given list',
120      expr: '1 > 2',
121      pos: 0,
122      walkThrough: [Expr, BinaryExpr],
123      child: [Mul, Div, Mod, Add, Sub],
124      expectedResult: false,
125    },
126  ];
127  testCases.forEach((value) => {
128    it(value.title, () => {
129      const state = createEditorState(value.expr);
130      const subTree = syntaxTree(state).resolve(value.pos, -1);
131      const node = walkThrough(subTree, ...value.walkThrough);
132      chai.expect(node).to.not.null;
133      if (node) {
134        chai.expect(value.expectedResult).to.equal(containsAtLeastOneChild(node, ...value.child));
135      }
136    });
137  });
138});
139
140describe('containsChild test', () => {
141  const testCases = [
142    {
143      title: 'Should find all expr in a subtree',
144      expr: 'metric_name / ignor',
145      pos: 0,
146      expectedResult: true,
147      walkThrough: [Expr, BinaryExpr],
148      child: [Expr, Expr],
149    },
150    {
151      title: 'Should not find all child required',
152      expr: 'sum(ra)',
153      pos: 0,
154      expectedResult: false,
155      walkThrough: [Expr, AggregateExpr, FunctionCallBody, FunctionCallArgs],
156      child: [Expr, Expr],
157    },
158  ];
159  testCases.forEach((value) => {
160    it(value.title, () => {
161      const state = createEditorState(value.expr);
162      const subTree = syntaxTree(state).resolve(value.pos, -1);
163      const node: SyntaxNode | null = walkThrough(subTree, ...value.walkThrough);
164
165      chai.expect(node).to.not.null;
166      if (node) {
167        chai.expect(value.expectedResult).to.equal(containsChild(node, ...value.child));
168      }
169    });
170  });
171});
172
173describe('retrieveAllRecursiveNodes test', () => {
174  it('should find every occurrence', () => {
175    const state = createEditorState('rate(1,2,3)');
176    const tree = syntaxTree(state).topNode.firstChild;
177    chai.expect(tree).to.not.null;
178    if (tree) {
179      chai.expect(3).to.equal(retrieveAllRecursiveNodes(walkThrough(tree, FunctionCall, FunctionCallBody), FunctionCallArgs, Expr).length);
180    }
181  });
182});
183
184describe('walkbackward test', () => {
185  const testCases = [
186    {
187      title: 'should find the parent',
188      expr: 'metric_name{}',
189      pos: 12,
190      exit: VectorSelector,
191      expectedResult: VectorSelector,
192    },
193  ];
194  testCases.forEach((value) => {
195    it(value.title, () => {
196      const state = createEditorState(value.expr);
197      const tree = syntaxTree(state).resolve(value.pos, -1);
198      chai.expect(value.expectedResult).to.equal(walkBackward(tree, value.exit)?.type.id);
199    });
200  });
201});
202