|
|
|
@ -3,8 +3,8 @@ import { config } from '@grafana/runtime'; |
|
|
|
|
import { SUGGESTIONS_LIMIT } from '../../../language_provider'; |
|
|
|
|
import { FUNCTIONS } from '../../../promql'; |
|
|
|
|
|
|
|
|
|
import { getCompletions } from './completions'; |
|
|
|
|
import { DataProvider, DataProviderParams } from './data_provider'; |
|
|
|
|
import { filterMetricNames, getCompletions } from './completions'; |
|
|
|
|
import { DataProvider, type DataProviderParams } from './data_provider'; |
|
|
|
|
import type { Situation } from './situation'; |
|
|
|
|
|
|
|
|
|
const history: string[] = ['previous_metric_name_1', 'previous_metric_name_2', 'previous_metric_name_3']; |
|
|
|
@ -41,6 +41,133 @@ afterEach(() => { |
|
|
|
|
jest.restoreAllMocks(); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('filterMetricNames', () => { |
|
|
|
|
const sampleMetrics = [ |
|
|
|
|
'http_requests_total', |
|
|
|
|
'http_requests_failed', |
|
|
|
|
'node_cpu_seconds_total', |
|
|
|
|
'node_memory_usage_bytes', |
|
|
|
|
'very_long_metric_name_with_many_underscores_and_detailed_description', |
|
|
|
|
'metric_name_1_with_extra_terms_included', |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
describe('empty input', () => { |
|
|
|
|
it('should return all metrics up to limit when input is empty', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: '', |
|
|
|
|
limit: 3, |
|
|
|
|
}); |
|
|
|
|
expect(result).toEqual(sampleMetrics.slice(0, 3)); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should return all metrics when input is whitespace', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: ' ', |
|
|
|
|
limit: 3, |
|
|
|
|
}); |
|
|
|
|
expect(result).toEqual(sampleMetrics.slice(0, 3)); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('simple searches (≤ 4 terms)', () => { |
|
|
|
|
it('should match exact strings', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'http_requests_total', |
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should match with single character errors', () => { |
|
|
|
|
// substitution
|
|
|
|
|
let result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'http_requezts_total', // 's' replaced with 'z'
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
|
|
|
|
|
// ransposition
|
|
|
|
|
result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'http_reqeust_total', // 'ue' swapped
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
|
|
|
|
|
// deletion
|
|
|
|
|
result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'http_reqests_total', // missing 'u'
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
|
|
|
|
|
// insertion
|
|
|
|
|
result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'http_reqquests_total', // extra 'q'
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should match partial strings', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'requests', // partial match
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('http_requests_total'); |
|
|
|
|
expect(result).toContainEqual('http_requests_failed'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should not match with multiple errors', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'htp_reqests_total', // two errors: missing 't' and missing 'u'
|
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).not.toContainEqual('http_requests_total'); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
describe('complex searches (> 4 terms)', () => { |
|
|
|
|
it('should use substring matching for each term', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'metric name 1 with extra terms', |
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toContainEqual('metric_name_1_with_extra_terms_included'); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should return empty array when no metrics match all terms', () => { |
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: sampleMetrics, |
|
|
|
|
inputText: 'metric name 1 with nonexistent terms', |
|
|
|
|
limit: 10, |
|
|
|
|
}); |
|
|
|
|
expect(result).toHaveLength(0); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should stop searching after limit is reached', () => { |
|
|
|
|
const manyMetrics = Array.from({ length: 10 }, (_, i) => `metric_name_${i}_with_terms`); |
|
|
|
|
|
|
|
|
|
const result = filterMetricNames({ |
|
|
|
|
metricNames: manyMetrics, |
|
|
|
|
inputText: 'metric name with terms other words', // > 4 terms
|
|
|
|
|
limit: 3, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
expect(result.length).toBeLessThanOrEqual(3); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
type MetricNameSituation = Extract<Situation['type'], 'AT_ROOT' | 'EMPTY' | 'IN_FUNCTION'>; |
|
|
|
|
const metricNameCompletionSituations = ['AT_ROOT', 'IN_FUNCTION', 'EMPTY'] as MetricNameSituation[]; |
|
|
|
|
|
|
|
|
@ -74,22 +201,22 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat |
|
|
|
|
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should limit completions for metric names when the number of metric names is greater than the limit', async () => { |
|
|
|
|
it('should limit completions for metric names when the number exceeds the limit', async () => { |
|
|
|
|
const situation: Situation = { |
|
|
|
|
type: situationType, |
|
|
|
|
}; |
|
|
|
|
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length); |
|
|
|
|
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit); |
|
|
|
|
|
|
|
|
|
// No text input
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange(''); |
|
|
|
|
// Complex query
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange('metric name one two three four five'); |
|
|
|
|
let completions = await getCompletions(situation, dataProvider); |
|
|
|
|
expect(completions).toHaveLength(expectedCompletionsCount); |
|
|
|
|
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); |
|
|
|
|
|
|
|
|
|
// With text input (use fuzzy search)
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange('name_1'); |
|
|
|
|
// Simple query with fuzzy match
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange('metric_name_'); |
|
|
|
|
completions = await getCompletions(situation, dataProvider); |
|
|
|
|
expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount); |
|
|
|
|
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should enable autocomplete suggestions update when the number of metric names is greater than the limit', async () => { |
|
|
|
@ -115,4 +242,35 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat |
|
|
|
|
await getCompletions(situation, dataProvider); |
|
|
|
|
expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should handle complex queries efficiently', async () => { |
|
|
|
|
const situation: Situation = { |
|
|
|
|
type: situationType, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
const testMetrics = ['metric_name_1', 'metric_name_2', 'metric_name_1_with_extra_terms', 'unrelated_metric']; |
|
|
|
|
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(testMetrics); |
|
|
|
|
|
|
|
|
|
// Test with a complex query (> 4 terms)
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more'); |
|
|
|
|
const completions = await getCompletions(situation, dataProvider); |
|
|
|
|
|
|
|
|
|
const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME'); |
|
|
|
|
expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
it('should handle multiple term queries efficiently', async () => { |
|
|
|
|
const situation: Situation = { |
|
|
|
|
type: situationType, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.beyondLimit); |
|
|
|
|
|
|
|
|
|
// Test with multiple terms
|
|
|
|
|
dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5'); |
|
|
|
|
const completions = await getCompletions(situation, dataProvider); |
|
|
|
|
|
|
|
|
|
const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length); |
|
|
|
|
expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|