From 66be589d867ce746229891e35273c70eaf9c4f05 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:32:09 +0200 Subject: [PATCH] Loki: Fix tests for test decoupling (#108678) * Loki: Fix tests for test decoupling * Update * Add test and fix implementarion * Remove empty line * prettier --- .../datasource/loki/LanguageProvider.test.ts | 28 ++++++++----------- .../datasource/loki/LanguageProvider.ts | 2 +- .../loki/LogContextProvider.test.ts | 9 +++--- .../datasource/loki/datasource.test.ts | 14 ++++++++++ .../app/plugins/datasource/loki/datasource.ts | 22 ++++++++++++--- .../datasource/loki/querySplitting.test.ts | 18 +++--------- .../components/UnwrapParamEditor.tsx | 24 +++++++--------- 7 files changed, 64 insertions(+), 53 deletions(-) diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index eb449333632..b00b54393c0 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -1,4 +1,4 @@ -import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange, ScopedVars } from '@grafana/data'; +import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, ScopedVars } from '@grafana/data'; import { config } from '@grafana/runtime'; import LanguageProvider from './LanguageProvider'; @@ -25,18 +25,6 @@ const mockTimeRange = { }, }; -jest.mock('@grafana/data', () => ({ - ...jest.requireActual('@grafana/data'), - getDefaultTimeRange: jest.fn().mockImplementation(() => ({ - from: dateTime(0), - to: dateTime(1), - raw: { - from: dateTime(0), - to: dateTime(1), - }, - })), -})); - describe('Language completion provider', () => { describe('start', () => { const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] }); @@ -208,8 +196,16 @@ describe('Language completion provider', () => { .mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() })); const languageProvider = new LanguageProvider(datasource); languageProvider.request = jest.fn().mockResolvedValue([]); + jest.spyOn(languageProvider, 'getDefaultTimeRange').mockImplementation(() => ({ + from: dateTime(0), + to: dateTime(1), + raw: { + from: dateTime(0), + to: dateTime(1), + }, + })); languageProvider.fetchLabelValues('testKey'); - expect(getDefaultTimeRange).toHaveBeenCalled(); + expect(languageProvider.getDefaultTimeRange).toHaveBeenCalled(); expect(languageProvider.request).toHaveBeenCalled(); expect(languageProvider.request).toHaveBeenCalledWith('label/testKey/values', { end: 1, @@ -765,7 +761,7 @@ describe('Query imports', () => { maxLines: DEFAULT_MAX_LINES_SAMPLE, refId: 'data-samples', }, - getDefaultTimeRange() + languageProvider.getDefaultTimeRange() ); }); @@ -786,7 +782,7 @@ describe('Query imports', () => { maxLines: 5, refId: 'data-samples', }, - getDefaultTimeRange() + languageProvider.getDefaultTimeRange() ); }); diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 5607980b557..17cbe513eaa 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -511,7 +511,7 @@ export default class LokiLanguageProvider extends LanguageProvider { * * @returns {TimeRange} The default time range */ - private getDefaultTimeRange(): TimeRange { + getDefaultTimeRange(): TimeRange { return getDefaultTimeRange(); } } diff --git a/public/app/plugins/datasource/loki/LogContextProvider.test.ts b/public/app/plugins/datasource/loki/LogContextProvider.test.ts index 109003ff15a..e4d3e3a142a 100644 --- a/public/app/plugins/datasource/loki/LogContextProvider.test.ts +++ b/public/app/plugins/datasource/loki/LogContextProvider.test.ts @@ -1,5 +1,4 @@ import { of } from 'rxjs'; -import { initTemplateSrv } from 'test/helpers/initTemplateSrv'; import { DataQueryResponse, @@ -9,7 +8,7 @@ import { createDataFrame, dateTime, } from '@grafana/data'; -import { setTemplateSrv } from '@grafana/runtime'; +import { setTemplateSrv, TemplateSrv } from '@grafana/runtime'; import LokiLanguageProvider from './LanguageProvider'; import { @@ -66,8 +65,10 @@ const frameWithoutTypes = { describe('LogContextProvider', () => { let logContextProvider: LogContextProvider; beforeEach(() => { - const templateSrv = initTemplateSrv('key', [{ type: 'query', name: 'foo', current: { value: 'baz' } }]); - setTemplateSrv(templateSrv); + const templateSrv = { + replace: jest.fn((str: string) => str?.replace('$test', 'baz')), + }; + setTemplateSrv(templateSrv as unknown as TemplateSrv); const defaultDatasourceMock = createLokiDatasource(templateSrv); defaultDatasourceMock.query = jest.fn(() => of({ data: [] } as DataQueryResponse)); defaultDatasourceMock.languageProvider = defaultLanguageProviderMock; diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 8d49fe6f27b..359e1716055 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1772,6 +1772,20 @@ describe('LokiDatasource', () => { }) ); }); + it('calls query if convertMetricQueryToLogQuery is true for a metric query', async () => { + const spy = jest.spyOn(ds, 'query').mockImplementation(() => of({} as DataQueryResponse)); + await ds.getDataSamples({ expr: 'rate({a="b"}[1m])', refId: 'A' }, mockTimeRange, { + convertMetricQueryToLogQuery: true, + }); + expect(spy).toHaveBeenCalled(); + }); + it('does not call query if convertMetricQueryToLogQuery is false for a metric query', async () => { + const spy = jest.spyOn(ds, 'query'); + await ds.getDataSamples({ expr: 'rate({a="b"}[1m])', refId: 'A' }, mockTimeRange, { + convertMetricQueryToLogQuery: false, + }); + expect(spy).not.toHaveBeenCalled(); + }); }); describe('Query splitting', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 468b52b005e..7bdfcf2e1ee 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -739,14 +739,28 @@ export class LokiDatasource * Currently, it works for logs data only. * @returns A Promise that resolves to an array of DataFrames containing data samples. */ - async getDataSamples(query: LokiQuery, timeRange: TimeRange): Promise { - // Currently works only for logs sample - if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) { + async getDataSamples( + query: LokiQuery, + timeRange: TimeRange, + options?: { convertMetricQueryToLogQuery?: boolean } + ): Promise { + let queryExpr = query.expr; + if (isQueryWithError(this.interpolateString(queryExpr, placeHolderScopedVars))) { return []; } + if (!isLogsQuery(queryExpr)) { + // If it is not a logs query, we need to check if we need to convert it to a logs query + if (options?.convertMetricQueryToLogQuery) { + queryExpr = getLogQueryFromMetricsQuery(queryExpr); + } else { + // Otherwise, we return an empty array, as data samples are only supported for logs queries + return []; + } + } + const lokiLogsQuery: LokiQuery = { - expr: query.expr, + expr: queryExpr, queryType: LokiQueryType.Range, refId: REF_ID_DATA_SAMPLES, maxLines: query.maxLines || DEFAULT_MAX_LINES_SAMPLE, diff --git a/public/app/plugins/datasource/loki/querySplitting.test.ts b/public/app/plugins/datasource/loki/querySplitting.test.ts index 82a585739aa..26abb0423b8 100644 --- a/public/app/plugins/datasource/loki/querySplitting.test.ts +++ b/public/app/plugins/datasource/loki/querySplitting.test.ts @@ -4,8 +4,6 @@ import { DataQueryRequest, dateTime, LoadingState } from '@grafana/data'; import { config } from '@grafana/runtime'; import { LokiDatasource } from './datasource'; -import * as logsTimeSplit from './logsTimeSplitting'; -import * as metricTimeSplit from './metricTimeSplitting'; import { createLokiDatasource } from './mocks/datasource'; import { getMockFrames } from './mocks/frames'; import { runSplitQuery } from './querySplitting'; @@ -282,18 +280,12 @@ describe('runSplitQuery()', () => { describe('Hidden and empty queries', () => { beforeAll(() => { - jest.spyOn(logsTimeSplit, 'splitTimeRange').mockReturnValue([]); - jest.spyOn(metricTimeSplit, 'splitTimeRange').mockReturnValue([]); jest.useFakeTimers().setSystemTime(new Date('Wed May 17 2023 17:20:12 GMT+0200')); }); beforeEach(() => { - jest.mocked(logsTimeSplit.splitTimeRange).mockClear(); - jest.mocked(logsTimeSplit.splitTimeRange).mockClear(); jest.mocked(trackGroupedQueries).mockClear(); }); afterAll(() => { - jest.mocked(logsTimeSplit.splitTimeRange).mockRestore(); - jest.mocked(metricTimeSplit.splitTimeRange).mockRestore(); jest.useRealTimers(); }); test('Ignores hidden queries', async () => { @@ -302,8 +294,6 @@ describe('runSplitQuery()', () => { { expr: '{a="b"}', refId: 'B' }, ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { - expect(logsTimeSplit.splitTimeRange).toHaveBeenCalled(); - expect(metricTimeSplit.splitTimeRange).not.toHaveBeenCalled(); expect(trackGroupedQueries).toHaveBeenCalledTimes(1); expect(trackGroupedQueries).toHaveBeenCalledWith( { @@ -313,9 +303,10 @@ describe('runSplitQuery()', () => { }, [ { - partition: [], + partition: expect.any(Array), request: { ...request, + // Only includes queries that are not hidden targets: request.targets.filter((query) => !query.hide), }, }, @@ -331,8 +322,6 @@ describe('runSplitQuery()', () => { { expr: '', refId: 'B' }, ]); await expect(runSplitQuery(datasource, request)).toEmitValuesWith(() => { - expect(logsTimeSplit.splitTimeRange).not.toHaveBeenCalled(); - expect(metricTimeSplit.splitTimeRange).toHaveBeenCalled(); expect(trackGroupedQueries).toHaveBeenCalledTimes(1); expect(trackGroupedQueries).toHaveBeenCalledWith( { @@ -342,9 +331,10 @@ describe('runSplitQuery()', () => { }, [ { - partition: [], + partition: expect.any(Array), request: { ...request, + // Only includes queries with an expression targets: request.targets.filter((query) => query.expr), }, }, diff --git a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx index bfb8306b6f0..6a6e7d85e82 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/UnwrapParamEditor.tsx @@ -1,12 +1,9 @@ import { useState } from 'react'; -import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data'; +import { DataSourceApi, SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data'; import { QueryBuilderOperationParamEditorProps, VisualQueryModeller } from '@grafana/plugin-ui'; import { Select } from '@grafana/ui'; -import { placeHolderScopedVars } from '../../components/monaco-query-field/monaco-completion-provider/validation'; -import { LokiDatasource } from '../../datasource'; -import { getLogQueryFromMetricsQuery, isQueryWithError } from '../../queryUtils'; import { extractUnwrapLabelKeysFromDataFrame } from '../../responseUtils'; import { getOperationParamId } from '../operationUtils'; import { LokiVisualQuery } from '../types'; @@ -31,11 +28,9 @@ export function UnwrapParamEditor({ inputId={getOperationParamId(operationId, index)} onOpenMenu={async () => { // This check is always true, we do it to make typescript happy - if (datasource instanceof LokiDatasource) { - setState({ isLoading: true }); - const options = await loadUnwrapOptions(query, datasource, queryModeller, timeRange); - setState({ options, isLoading: undefined }); - } + setState({ isLoading: true }); + const options = await loadUnwrapOptions(query, datasource, queryModeller, timeRange); + setState({ options, isLoading: undefined }); }} isLoading={state.isLoading} allowCustomValue @@ -54,17 +49,18 @@ export function UnwrapParamEditor({ async function loadUnwrapOptions( query: LokiVisualQuery, - datasource: LokiDatasource, + datasource: DataSourceApi, queryModeller: VisualQueryModeller, timeRange = getDefaultTimeRange() ): Promise>> { const queryExpr = queryModeller.renderQuery(query); - const logExpr = getLogQueryFromMetricsQuery(queryExpr); - if (isQueryWithError(datasource.interpolateString(logExpr, placeHolderScopedVars))) { + if (!('getDataSamples' in datasource) || typeof datasource.getDataSamples !== 'function') { return []; } - - const samples = await datasource.getDataSamples({ expr: logExpr, refId: 'unwrap_samples' }, timeRange); + // the query is a metric query, we need to set metricQueryToLogConversion to true to getSamples use the log query + const samples = await datasource.getDataSamples({ expr: queryExpr, refId: 'unwrap_samples' }, timeRange, { + convertMetricQueryToLogQuery: true, + }); const unwrapLabels = extractUnwrapLabelKeysFromDataFrame(samples[0]); const labelOptions = unwrapLabels.map((label) => ({