mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-25 11:30:08 +00:00
628 lines
13 KiB
TypeScript
628 lines
13 KiB
TypeScript
/* eslint-disable */
|
|
|
|
// Mock all dependencies before importing the function
|
|
// Global variable to store the current test input
|
|
let currentTestInput = '';
|
|
|
|
// Now import the function after all mocks are set up
|
|
// Import the mocked antlr4 to access CharStreams
|
|
import * as antlr4 from 'antlr4';
|
|
|
|
import {
|
|
createContext,
|
|
extractQueryPairs,
|
|
getCurrentQueryPair,
|
|
getCurrentValueIndexAtCursor,
|
|
} from '../queryContextUtils';
|
|
|
|
jest.mock('antlr4', () => ({
|
|
CharStreams: {
|
|
fromString: jest.fn().mockImplementation((input: string) => {
|
|
currentTestInput = input;
|
|
return {
|
|
inputSource: { strdata: input },
|
|
};
|
|
}),
|
|
},
|
|
CommonTokenStream: jest.fn().mockImplementation(() => {
|
|
// Use the dynamically captured input string from the current test
|
|
const input = currentTestInput;
|
|
|
|
// Generate tokens dynamically based on the input
|
|
const tokens = [];
|
|
let currentPos = 0;
|
|
let i = 0;
|
|
|
|
while (i < input.length) {
|
|
// Skip whitespace
|
|
while (i < input.length && /\s/.test(input[i])) {
|
|
i++;
|
|
currentPos++;
|
|
}
|
|
if (i >= input.length) break;
|
|
|
|
// Handle array brackets
|
|
if (input[i] === '[') {
|
|
tokens.push({
|
|
type: 3, // LBRACK
|
|
text: '[',
|
|
start: currentPos,
|
|
stop: currentPos,
|
|
channel: 0,
|
|
});
|
|
i++;
|
|
currentPos++;
|
|
continue;
|
|
}
|
|
|
|
if (input[i] === ']') {
|
|
tokens.push({
|
|
type: 4, // RBRACK
|
|
text: ']',
|
|
start: currentPos,
|
|
stop: currentPos,
|
|
channel: 0,
|
|
});
|
|
i++;
|
|
currentPos++;
|
|
continue;
|
|
}
|
|
|
|
if (input[i] === ',') {
|
|
tokens.push({
|
|
type: 5, // COMMA
|
|
text: ',',
|
|
start: currentPos,
|
|
stop: currentPos,
|
|
channel: 0,
|
|
});
|
|
i++;
|
|
currentPos++;
|
|
continue;
|
|
}
|
|
|
|
// Find the end of the current token
|
|
let tokenEnd = i;
|
|
let inQuotes = false;
|
|
let quoteChar = '';
|
|
|
|
while (tokenEnd < input.length) {
|
|
const char = input[tokenEnd];
|
|
|
|
if (
|
|
!inQuotes &&
|
|
(char === ' ' || char === '[' || char === ']' || char === ',')
|
|
) {
|
|
break;
|
|
}
|
|
|
|
if ((char === '"' || char === "'") && !inQuotes) {
|
|
inQuotes = true;
|
|
quoteChar = char;
|
|
} else if (char === quoteChar && inQuotes) {
|
|
inQuotes = false;
|
|
quoteChar = '';
|
|
}
|
|
|
|
tokenEnd++;
|
|
}
|
|
|
|
const tokenText = input.substring(i, tokenEnd);
|
|
|
|
// Determine token type
|
|
let tokenType = 28; // Default to QUOTED_TEXT
|
|
|
|
if (tokenText === 'IN') {
|
|
tokenType = 19;
|
|
} else if (tokenText === 'AND') {
|
|
tokenType = 21;
|
|
} else if (tokenText === '=') {
|
|
tokenType = 6;
|
|
} else if (tokenText === '<') {
|
|
tokenType = 9;
|
|
} else if (tokenText === '>') {
|
|
tokenType = 10;
|
|
} else if (tokenText === '!=') {
|
|
tokenType = 7;
|
|
} else if (tokenText.includes('.')) {
|
|
tokenType = 29; // KEY
|
|
} else if (/^\d+$/.test(tokenText)) {
|
|
tokenType = 27; // NUMBER
|
|
} else if (
|
|
(tokenText.startsWith("'") && tokenText.endsWith("'")) ||
|
|
(tokenText.startsWith('"') && tokenText.endsWith('"'))
|
|
) {
|
|
tokenType = 28; // QUOTED_TEXT
|
|
}
|
|
|
|
tokens.push({
|
|
type: tokenType,
|
|
text: tokenText,
|
|
start: currentPos,
|
|
stop: currentPos + tokenText.length - 1,
|
|
channel: 0,
|
|
});
|
|
|
|
currentPos += tokenText.length;
|
|
i = tokenEnd;
|
|
}
|
|
|
|
return {
|
|
fill: jest.fn(),
|
|
tokens: [
|
|
...tokens,
|
|
// EOF
|
|
{ type: -1, text: '', start: 0, stop: 0, channel: 0 },
|
|
],
|
|
};
|
|
}),
|
|
Token: {
|
|
EOF: -1,
|
|
},
|
|
}));
|
|
|
|
jest.mock('parser/FilterQueryLexer', () => ({
|
|
__esModule: true,
|
|
default: class MockFilterQueryLexer {
|
|
static readonly KEY = 29;
|
|
|
|
static readonly IN = 19;
|
|
|
|
static readonly EQUALS = 6;
|
|
|
|
static readonly LT = 9;
|
|
|
|
static readonly AND = 21;
|
|
|
|
static readonly LPAREN = 1;
|
|
|
|
static readonly RPAREN = 2;
|
|
|
|
static readonly LBRACK = 3;
|
|
|
|
static readonly RBRACK = 4;
|
|
|
|
static readonly COMMA = 5;
|
|
|
|
static readonly NOT = 20;
|
|
|
|
static readonly OR = 22;
|
|
|
|
static readonly EOF = -1;
|
|
|
|
static readonly QUOTED_TEXT = 28;
|
|
|
|
static readonly NUMBER = 27;
|
|
|
|
static readonly WS = 30;
|
|
|
|
static readonly FREETEXT = 31;
|
|
},
|
|
}));
|
|
|
|
jest.mock('parser/analyzeQuery', () => ({}));
|
|
|
|
jest.mock('../tokenUtils', () => ({
|
|
isOperatorToken: jest.fn((tokenType: number) =>
|
|
[6, 9, 19, 20].includes(tokenType),
|
|
),
|
|
isMultiValueOperator: jest.fn((operator: string) => operator === 'IN'),
|
|
isValueToken: jest.fn((tokenType: number) => [27, 28, 29].includes(tokenType)),
|
|
isConjunctionToken: jest.fn((tokenType: number) =>
|
|
[21, 22].includes(tokenType),
|
|
),
|
|
isQueryPairComplete: jest.fn((pair: any) => {
|
|
if (!pair) return false;
|
|
if (pair.operator === 'EXISTS') {
|
|
return !!pair.key && !!pair.operator;
|
|
}
|
|
return Boolean(pair.key && pair.operator && pair.value);
|
|
}),
|
|
}));
|
|
|
|
describe('extractQueryPairs', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('should extract query pairs from complex query with IN operator and multiple conditions', () => {
|
|
const input =
|
|
"service.name IN ['adservice', 'consumer-svc-1'] AND cloud.account.id = 'signoz-staging' code.lineno < 172";
|
|
|
|
const result = extractQueryPairs(input);
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
key: 'service.name',
|
|
operator: 'IN',
|
|
value: "['adservice', 'consumer-svc-1']",
|
|
valueList: ["'adservice'", "'consumer-svc-1'"],
|
|
valuesPosition: [
|
|
{
|
|
start: 17,
|
|
end: 27,
|
|
},
|
|
{
|
|
start: 30,
|
|
end: 45,
|
|
},
|
|
],
|
|
hasNegation: false,
|
|
isMultiValue: true,
|
|
position: {
|
|
keyStart: 0,
|
|
keyEnd: 11,
|
|
operatorStart: 13,
|
|
operatorEnd: 14,
|
|
valueStart: 16,
|
|
valueEnd: 46,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
{
|
|
key: 'cloud.account.id',
|
|
operator: '=',
|
|
value: "'signoz-staging'",
|
|
valueList: [],
|
|
valuesPosition: [],
|
|
hasNegation: false,
|
|
isMultiValue: false,
|
|
position: {
|
|
keyStart: 52,
|
|
keyEnd: 67,
|
|
operatorStart: 69,
|
|
operatorEnd: 69,
|
|
valueStart: 71,
|
|
valueEnd: 86,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
{
|
|
key: 'code.lineno',
|
|
operator: '<',
|
|
value: '172',
|
|
valueList: [],
|
|
valuesPosition: [],
|
|
hasNegation: false,
|
|
isMultiValue: false,
|
|
position: {
|
|
keyStart: 88,
|
|
keyEnd: 98,
|
|
operatorStart: 100,
|
|
operatorEnd: 100,
|
|
valueStart: 102,
|
|
valueEnd: 104,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should extract query pairs from complex query with IN operator without brackets', () => {
|
|
const input =
|
|
"service.name IN 'adservice' AND cloud.account.id = 'signoz-staging' code.lineno < 172";
|
|
|
|
const result = extractQueryPairs(input);
|
|
expect(result).toEqual([
|
|
{
|
|
key: 'service.name',
|
|
operator: 'IN',
|
|
value: "'adservice'",
|
|
valueList: ["'adservice'"],
|
|
valuesPosition: [
|
|
{
|
|
start: 16,
|
|
end: 26,
|
|
},
|
|
],
|
|
hasNegation: false,
|
|
isMultiValue: true,
|
|
position: {
|
|
keyStart: 0,
|
|
keyEnd: 11,
|
|
operatorStart: 13,
|
|
operatorEnd: 14,
|
|
valueStart: 16,
|
|
valueEnd: 26,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
{
|
|
key: 'cloud.account.id',
|
|
operator: '=',
|
|
value: "'signoz-staging'",
|
|
valueList: [],
|
|
valuesPosition: [],
|
|
hasNegation: false,
|
|
isMultiValue: false,
|
|
position: {
|
|
keyStart: 32,
|
|
keyEnd: 47,
|
|
operatorStart: 49,
|
|
operatorEnd: 49,
|
|
valueStart: 51,
|
|
valueEnd: 66,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
{
|
|
key: 'code.lineno',
|
|
operator: '<',
|
|
value: '172',
|
|
valueList: [],
|
|
valuesPosition: [],
|
|
hasNegation: false,
|
|
isMultiValue: false,
|
|
position: {
|
|
keyStart: 68,
|
|
keyEnd: 78,
|
|
operatorStart: 80,
|
|
operatorEnd: 80,
|
|
valueStart: 82,
|
|
valueEnd: 84,
|
|
negationStart: 0,
|
|
negationEnd: 0,
|
|
},
|
|
isComplete: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('should handle error gracefully and return empty array', () => {
|
|
// Mock console.error to suppress output during test
|
|
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
// Mock CharStreams to throw an error
|
|
jest.mocked(antlr4.CharStreams.fromString).mockImplementation(() => {
|
|
throw new Error('Mock error');
|
|
});
|
|
|
|
const input = 'some query';
|
|
const result = extractQueryPairs(input);
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
// Restore console.error
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
test('should handle recursion guard', () => {
|
|
// This test verifies the recursion protection in the function
|
|
// We'll mock the function to simulate recursion
|
|
|
|
// Mock console.warn to capture the warning
|
|
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
|
|
// Call the function multiple times to trigger recursion guard
|
|
// Note: This is a simplified test since we can't easily trigger the actual recursion
|
|
const result = extractQueryPairs('test');
|
|
|
|
// The function should still work normally
|
|
expect(Array.isArray(result)).toBe(true);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('createContext', () => {
|
|
test('should create a context object with all parameters', () => {
|
|
const mockToken = {
|
|
type: 29,
|
|
text: 'test',
|
|
start: 0,
|
|
stop: 3,
|
|
};
|
|
|
|
const result = createContext(
|
|
mockToken as any,
|
|
true, // isInKey
|
|
false, // isInNegation
|
|
false, // isInOperator
|
|
false, // isInValue
|
|
'testKey', // keyToken
|
|
'=', // operatorToken
|
|
'testValue', // valueToken
|
|
[], // queryPairs
|
|
null, // currentPair
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
tokenType: 29,
|
|
text: 'test',
|
|
start: 0,
|
|
stop: 3,
|
|
currentToken: 'test',
|
|
isInKey: true,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
keyToken: 'testKey',
|
|
operatorToken: '=',
|
|
valueToken: 'testValue',
|
|
queryPairs: [],
|
|
currentPair: null,
|
|
});
|
|
});
|
|
|
|
test('should create a context object with minimal parameters', () => {
|
|
const mockToken = {
|
|
type: 29,
|
|
text: 'test',
|
|
start: 0,
|
|
stop: 3,
|
|
};
|
|
|
|
const result = createContext(mockToken as any, false, false, false, false);
|
|
|
|
expect(result).toEqual({
|
|
tokenType: 29,
|
|
text: 'test',
|
|
start: 0,
|
|
stop: 3,
|
|
currentToken: 'test',
|
|
isInKey: false,
|
|
isInNegation: false,
|
|
isInOperator: false,
|
|
isInValue: false,
|
|
isInFunction: false,
|
|
isInConjunction: false,
|
|
isInParenthesis: false,
|
|
keyToken: undefined,
|
|
operatorToken: undefined,
|
|
valueToken: undefined,
|
|
queryPairs: [],
|
|
currentPair: undefined,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getCurrentValueIndexAtCursor', () => {
|
|
test('should return correct value index when cursor is within a value range', () => {
|
|
const valuesPosition = [
|
|
{ start: 0, end: 10 },
|
|
{ start: 15, end: 25 },
|
|
{ start: 30, end: 40 },
|
|
];
|
|
|
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 20);
|
|
|
|
expect(result).toBe(1);
|
|
});
|
|
|
|
test('should return null when cursor is not within any value range', () => {
|
|
const valuesPosition = [
|
|
{ start: 0, end: 10 },
|
|
{ start: 15, end: 25 },
|
|
];
|
|
|
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 12);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('should return correct index when cursor is at the boundary', () => {
|
|
const valuesPosition = [
|
|
{ start: 0, end: 10 },
|
|
{ start: 15, end: 25 },
|
|
];
|
|
|
|
const result = getCurrentValueIndexAtCursor(valuesPosition, 10);
|
|
|
|
expect(result).toBe(0);
|
|
});
|
|
|
|
test('should return null for empty valuesPosition array', () => {
|
|
const result = getCurrentValueIndexAtCursor([], 5);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getCurrentQueryPair', () => {
|
|
test('should return the correct query pair at cursor position', () => {
|
|
const queryPairs = [
|
|
{
|
|
key: 'a',
|
|
operator: '=',
|
|
value: '1',
|
|
position: {
|
|
keyStart: 0,
|
|
keyEnd: 0,
|
|
operatorStart: 2,
|
|
operatorEnd: 2,
|
|
valueStart: 4,
|
|
valueEnd: 4,
|
|
},
|
|
isComplete: true,
|
|
} as any,
|
|
{
|
|
key: 'b',
|
|
operator: '=',
|
|
value: '2',
|
|
position: {
|
|
keyStart: 10,
|
|
keyEnd: 10,
|
|
operatorStart: 12,
|
|
operatorEnd: 12,
|
|
valueStart: 14,
|
|
valueEnd: 14,
|
|
},
|
|
isComplete: true,
|
|
} as any,
|
|
];
|
|
|
|
const query = 'a = 1 AND b = 2';
|
|
const result = getCurrentQueryPair(queryPairs, query, 15);
|
|
|
|
expect(result).toEqual(queryPairs[1]);
|
|
});
|
|
|
|
test('should return null when no pairs match cursor position', () => {
|
|
const queryPairs = [
|
|
{
|
|
key: 'a',
|
|
operator: '=',
|
|
value: '1',
|
|
position: {
|
|
keyStart: 0,
|
|
keyEnd: 0,
|
|
operatorStart: 2,
|
|
operatorEnd: 2,
|
|
valueStart: 4,
|
|
valueEnd: 4,
|
|
},
|
|
isComplete: true,
|
|
} as any,
|
|
];
|
|
|
|
const query = 'a = 1';
|
|
// Test with cursor position that's before any pair starts
|
|
const result = getCurrentQueryPair(queryPairs, query, -1);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('should return null for empty queryPairs array', () => {
|
|
const result = getCurrentQueryPair([], 'test query', 5);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test('should return last pair when cursor is at the end', () => {
|
|
const queryPairs = [
|
|
{
|
|
key: 'a',
|
|
operator: '=',
|
|
value: '1',
|
|
position: {
|
|
keyStart: 0,
|
|
keyEnd: 0,
|
|
operatorStart: 2,
|
|
operatorEnd: 2,
|
|
valueStart: 4,
|
|
valueEnd: 4,
|
|
},
|
|
isComplete: true,
|
|
} as any,
|
|
];
|
|
|
|
const query = 'a = 1';
|
|
const result = getCurrentQueryPair(queryPairs, query, 5);
|
|
|
|
expect(result).toEqual(queryPairs[0]);
|
|
});
|
|
});
|