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