AlertingQueryRunner.test.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import { Observable, of, throwError } from 'rxjs';
  2. import { delay, take } from 'rxjs/operators';
  3. import { createFetchResponse } from 'test/helpers/createFetchResponse';
  4. import {
  5. ArrayVector,
  6. DataFrame,
  7. DataFrameJSON,
  8. DataSourceApi,
  9. Field,
  10. FieldType,
  11. getDefaultRelativeTimeRange,
  12. LoadingState,
  13. rangeUtil,
  14. } from '@grafana/data';
  15. import { DataSourceSrv, FetchResponse } from '@grafana/runtime';
  16. import { BackendSrv } from 'app/core/services/backend_srv';
  17. import { AlertQuery } from 'app/types/unified-alerting-dto';
  18. import { AlertingQueryResponse, AlertingQueryRunner } from './AlertingQueryRunner';
  19. describe('AlertingQueryRunner', () => {
  20. it('should successfully map response and return panel data by refId', async () => {
  21. const response = createFetchResponse<AlertingQueryResponse>({
  22. results: {
  23. A: { frames: [createDataFrameJSON([1, 2, 3])] },
  24. B: { frames: [createDataFrameJSON([5, 6])] },
  25. },
  26. });
  27. const runner = new AlertingQueryRunner(
  28. mockBackendSrv({
  29. fetch: () => of(response),
  30. }),
  31. mockDataSourceSrv()
  32. );
  33. const data = runner.get();
  34. runner.run([createQuery('A'), createQuery('B')]);
  35. await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
  36. const [data] = values;
  37. expect(data).toEqual({
  38. A: {
  39. annotations: [],
  40. state: LoadingState.Done,
  41. series: [
  42. expectDataFrameWithValues({
  43. time: [1620051612238, 1620051622238, 1620051632238],
  44. values: [1, 2, 3],
  45. }),
  46. ],
  47. structureRev: 1,
  48. timeRange: expect.anything(),
  49. timings: {
  50. dataProcessingTime: expect.any(Number),
  51. },
  52. },
  53. B: {
  54. annotations: [],
  55. state: LoadingState.Done,
  56. series: [
  57. expectDataFrameWithValues({
  58. time: [1620051612238, 1620051622238],
  59. values: [5, 6],
  60. }),
  61. ],
  62. structureRev: 1,
  63. timeRange: expect.anything(),
  64. timings: {
  65. dataProcessingTime: expect.any(Number),
  66. },
  67. },
  68. });
  69. });
  70. });
  71. it('should successfully map response with sliding relative time range', async () => {
  72. const response = createFetchResponse<AlertingQueryResponse>({
  73. results: {
  74. A: { frames: [createDataFrameJSON([1, 2, 3])] },
  75. B: { frames: [createDataFrameJSON([5, 6])] },
  76. },
  77. });
  78. const runner = new AlertingQueryRunner(
  79. mockBackendSrv({
  80. fetch: () => of(response),
  81. }),
  82. mockDataSourceSrv()
  83. );
  84. const data = runner.get();
  85. runner.run([createQuery('A'), createQuery('B')]);
  86. await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
  87. const [data] = values;
  88. // these test are flakey since the absolute computed "timeRange" can differ from the relative "defaultRelativeTimeRange"
  89. // so instead we will check if the size of the timeranges match
  90. const relativeA = rangeUtil.timeRangeToRelative(data.A.timeRange);
  91. const relativeB = rangeUtil.timeRangeToRelative(data.B.timeRange);
  92. const defaultRange = getDefaultRelativeTimeRange();
  93. expect(relativeA.from - defaultRange.from).toEqual(relativeA.to - defaultRange.to);
  94. expect(relativeB.from - defaultRange.from).toEqual(relativeB.to - defaultRange.to);
  95. });
  96. });
  97. it('should emit loading state if response is slower then 200ms', async () => {
  98. const response = createFetchResponse<AlertingQueryResponse>({
  99. results: {
  100. A: { frames: [createDataFrameJSON([1, 2, 3])] },
  101. B: { frames: [createDataFrameJSON([5, 6])] },
  102. },
  103. });
  104. const runner = new AlertingQueryRunner(
  105. mockBackendSrv({
  106. fetch: () => of(response).pipe(delay(210)),
  107. }),
  108. mockDataSourceSrv()
  109. );
  110. const data = runner.get();
  111. runner.run([createQuery('A'), createQuery('B')]);
  112. await expect(data.pipe(take(2))).toEmitValuesWith((values) => {
  113. const [loading, data] = values;
  114. expect(loading.A.state).toEqual(LoadingState.Loading);
  115. expect(loading.B.state).toEqual(LoadingState.Loading);
  116. expect(data).toEqual({
  117. A: {
  118. annotations: [],
  119. state: LoadingState.Done,
  120. series: [
  121. expectDataFrameWithValues({
  122. time: [1620051612238, 1620051622238, 1620051632238],
  123. values: [1, 2, 3],
  124. }),
  125. ],
  126. structureRev: 2,
  127. timeRange: expect.anything(),
  128. timings: {
  129. dataProcessingTime: expect.any(Number),
  130. },
  131. },
  132. B: {
  133. annotations: [],
  134. state: LoadingState.Done,
  135. series: [
  136. expectDataFrameWithValues({
  137. time: [1620051612238, 1620051622238],
  138. values: [5, 6],
  139. }),
  140. ],
  141. structureRev: 2,
  142. timeRange: expect.anything(),
  143. timings: {
  144. dataProcessingTime: expect.any(Number),
  145. },
  146. },
  147. });
  148. });
  149. });
  150. it('should emit error state if fetch request fails', async () => {
  151. const error = new Error('could not query data');
  152. const runner = new AlertingQueryRunner(
  153. mockBackendSrv({
  154. fetch: () => throwError(error),
  155. }),
  156. mockDataSourceSrv()
  157. );
  158. const data = runner.get();
  159. runner.run([createQuery('A'), createQuery('B')]);
  160. await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
  161. const [data] = values;
  162. expect(data.A.state).toEqual(LoadingState.Error);
  163. expect(data.A.error).toEqual(error);
  164. expect(data.B.state).toEqual(LoadingState.Error);
  165. expect(data.B.error).toEqual(error);
  166. });
  167. });
  168. it('should not execute if a query fails filterQuery check', async () => {
  169. const runner = new AlertingQueryRunner(
  170. mockBackendSrv({
  171. fetch: () => throwError(new Error("shouldn't happen")),
  172. }),
  173. mockDataSourceSrv({ filterQuery: () => false })
  174. );
  175. const data = runner.get();
  176. runner.run([createQuery('A'), createQuery('B')]);
  177. await expect(data.pipe(take(1))).toEmitValuesWith((values) => {
  178. const [data] = values;
  179. expect(data.A.state).toEqual(LoadingState.Done);
  180. expect(data.A.series).toHaveLength(0);
  181. expect(data.B.state).toEqual(LoadingState.Done);
  182. expect(data.B.series).toHaveLength(0);
  183. });
  184. });
  185. });
  186. type MockBackendSrvConfig = {
  187. fetch: () => Observable<FetchResponse<AlertingQueryResponse>>;
  188. };
  189. const mockBackendSrv = ({ fetch }: MockBackendSrvConfig): BackendSrv => {
  190. return {
  191. fetch,
  192. resolveCancelerIfExists: jest.fn(),
  193. } as unknown as BackendSrv;
  194. };
  195. const mockDataSourceSrv = (dsApi?: Partial<DataSourceApi>) => {
  196. return {
  197. get: () => Promise.resolve(dsApi ?? {}),
  198. } as unknown as DataSourceSrv;
  199. };
  200. const expectDataFrameWithValues = ({ time, values }: { time: number[]; values: number[] }): DataFrame => {
  201. return {
  202. fields: [
  203. {
  204. config: {},
  205. entities: {},
  206. name: 'time',
  207. state: null,
  208. type: FieldType.time,
  209. values: new ArrayVector(time),
  210. } as Field,
  211. {
  212. config: {},
  213. entities: {},
  214. name: 'value',
  215. state: null,
  216. type: FieldType.number,
  217. values: new ArrayVector(values),
  218. } as Field,
  219. ],
  220. length: values.length,
  221. };
  222. };
  223. const createDataFrameJSON = (values: number[]): DataFrameJSON => {
  224. const startTime = 1620051602238;
  225. const timeValues = values.map((_, index) => startTime + (index + 1) * 10000);
  226. return {
  227. schema: {
  228. fields: [
  229. { name: 'time', type: FieldType.time },
  230. { name: 'value', type: FieldType.number },
  231. ],
  232. },
  233. data: {
  234. values: [timeValues, values],
  235. },
  236. };
  237. };
  238. const createQuery = (refId: string): AlertQuery => {
  239. return {
  240. refId,
  241. queryType: '',
  242. datasourceUid: '',
  243. model: { refId },
  244. relativeTimeRange: getDefaultRelativeTimeRange(),
  245. };
  246. };