DashboardQueryRunner.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import { throwError } from 'rxjs';
  2. import { delay, first } from 'rxjs/operators';
  3. import { AlertState, AlertStateInfo } from '@grafana/data';
  4. import { setDataSourceSrv } from '@grafana/runtime';
  5. import { silenceConsoleOutput } from '../../../../../test/core/utils/silenceConsoleOutput';
  6. import { backendSrv } from '../../../../core/services/backend_srv';
  7. import * as annotationsSrv from '../../../annotations/executeAnnotationQuery';
  8. import { createDashboardQueryRunner } from './DashboardQueryRunner';
  9. import { getDefaultOptions, LEGACY_DS_NAME, NEXT_GEN_DS_NAME, toAsyncOfResult } from './testHelpers';
  10. import { DashboardQueryRunner, DashboardQueryRunnerResult } from './types';
  11. jest.mock('@grafana/runtime', () => ({
  12. ...(jest.requireActual('@grafana/runtime') as unknown as object),
  13. getBackendSrv: () => backendSrv,
  14. }));
  15. function getTestContext() {
  16. jest.clearAllMocks();
  17. const timeSrvMock: any = { timeRange: jest.fn() };
  18. const options = getDefaultOptions();
  19. // These tests are setup so all the workers and runners are invoked once, this wouldn't be the case in real life
  20. const runner = createDashboardQueryRunner({ dashboard: options.dashboard, timeSrv: timeSrvMock });
  21. const getResults: AlertStateInfo[] = [
  22. { id: 1, state: AlertState.Alerting, dashboardId: 1, panelId: 1 },
  23. { id: 2, state: AlertState.Alerting, dashboardId: 1, panelId: 2 },
  24. ];
  25. const getMock = jest.spyOn(backendSrv, 'get').mockResolvedValue(getResults);
  26. const executeAnnotationQueryMock = jest
  27. .spyOn(annotationsSrv, 'executeAnnotationQuery')
  28. .mockReturnValue(toAsyncOfResult({ events: [{ id: 'NextGen' }] }));
  29. const annotationQueryMock = jest.fn().mockResolvedValue([{ id: 'Legacy' }]);
  30. const dataSourceSrvMock: any = {
  31. get: async (name: string) => {
  32. if (name === LEGACY_DS_NAME) {
  33. return {
  34. annotationQuery: annotationQueryMock,
  35. };
  36. }
  37. if (name === NEXT_GEN_DS_NAME) {
  38. return {
  39. annotations: {},
  40. };
  41. }
  42. return {};
  43. },
  44. };
  45. setDataSourceSrv(dataSourceSrvMock);
  46. return { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock };
  47. }
  48. function expectOnResults(args: {
  49. runner: DashboardQueryRunner;
  50. panelId: number;
  51. done: jest.DoneCallback;
  52. expect: (results: DashboardQueryRunnerResult) => void;
  53. }) {
  54. const { runner, done, panelId, expect: expectCallback } = args;
  55. runner
  56. .getResult(panelId)
  57. .pipe(first())
  58. .subscribe({
  59. next: (value) => {
  60. try {
  61. expectCallback(value);
  62. done();
  63. } catch (err) {
  64. done(err);
  65. }
  66. },
  67. });
  68. }
  69. describe('DashboardQueryRunnerImpl', () => {
  70. describe('when calling run and all workers succeed', () => {
  71. it('then it should return the correct results', (done) => {
  72. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  73. expectOnResults({
  74. runner,
  75. panelId: 1,
  76. done,
  77. expect: (results) => {
  78. // should have one alert state, one snapshot, one legacy and one next gen result
  79. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  80. expect(results).toEqual(getExpectedForAllResult());
  81. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  82. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  83. expect(getMock).toHaveBeenCalledTimes(1);
  84. },
  85. });
  86. runner.run(options);
  87. });
  88. });
  89. describe('when calling run and all workers succeed but take longer than 200ms', () => {
  90. it('then it should return the empty results', (done) => {
  91. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  92. const wait = 201;
  93. executeAnnotationQueryMock.mockReturnValue(toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(wait)));
  94. expectOnResults({
  95. runner,
  96. panelId: 1,
  97. done,
  98. expect: (results) => {
  99. // should have one alert state, one snapshot, one legacy and one next gen result
  100. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  101. expect(results).toEqual({ annotations: [] });
  102. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  103. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  104. expect(getMock).toHaveBeenCalledTimes(1);
  105. },
  106. });
  107. runner.run(options);
  108. });
  109. });
  110. describe('when calling run and all workers succeed but the subscriber subscribes after the run', () => {
  111. it('then it should return the last results', (done) => {
  112. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  113. runner.run(options);
  114. setTimeout(
  115. () =>
  116. expectOnResults({
  117. runner,
  118. panelId: 1,
  119. done,
  120. expect: (results) => {
  121. // should have one alert state, one snapshot, one legacy and one next gen result
  122. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  123. expect(results).toEqual(getExpectedForAllResult());
  124. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  125. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  126. expect(getMock).toHaveBeenCalledTimes(1);
  127. },
  128. }),
  129. 200
  130. ); // faking a late subscriber to make sure we get the latest results
  131. });
  132. });
  133. describe('when calling run and all workers fail', () => {
  134. silenceConsoleOutput();
  135. it('then it should return the correct results', (done) => {
  136. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  137. getMock.mockRejectedValue({ message: 'Get error' });
  138. annotationQueryMock.mockRejectedValue({ message: 'Legacy error' });
  139. executeAnnotationQueryMock.mockReturnValue(throwError({ message: 'NextGen error' }));
  140. expectOnResults({
  141. runner,
  142. panelId: 1,
  143. done,
  144. expect: (results) => {
  145. // should have one alert state, one snapshot, one legacy and one next gen result
  146. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  147. const expected = { alertState: undefined, annotations: [getExpectedForAllResult().annotations[2]] };
  148. expect(results).toEqual(expected);
  149. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  150. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  151. expect(getMock).toHaveBeenCalledTimes(1);
  152. },
  153. });
  154. runner.run(options);
  155. });
  156. });
  157. describe('when calling run and AlertStatesWorker fails', () => {
  158. silenceConsoleOutput();
  159. it('then it should return the correct results', (done) => {
  160. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  161. getMock.mockRejectedValue({ message: 'Get error' });
  162. expectOnResults({
  163. runner,
  164. panelId: 1,
  165. done,
  166. expect: (results) => {
  167. // should have one alert state, one snapshot, one legacy and one next gen result
  168. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  169. const { annotations } = getExpectedForAllResult();
  170. const expected = { alertState: undefined, annotations };
  171. expect(results).toEqual(expected);
  172. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  173. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  174. expect(getMock).toHaveBeenCalledTimes(1);
  175. },
  176. });
  177. runner.run(options);
  178. });
  179. describe('when calling run and AnnotationsWorker fails', () => {
  180. silenceConsoleOutput();
  181. it('then it should return the correct results', (done) => {
  182. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  183. annotationQueryMock.mockRejectedValue({ message: 'Legacy error' });
  184. executeAnnotationQueryMock.mockReturnValue(throwError({ message: 'NextGen error' }));
  185. expectOnResults({
  186. runner,
  187. panelId: 1,
  188. done,
  189. expect: (results) => {
  190. // should have one alert state, one snapshot, one legacy and one next gen result
  191. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  192. const { alertState, annotations } = getExpectedForAllResult();
  193. const expected = { alertState, annotations: [annotations[2]] };
  194. expect(results).toEqual(expected);
  195. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  196. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  197. expect(getMock).toHaveBeenCalledTimes(1);
  198. },
  199. });
  200. runner.run(options);
  201. });
  202. });
  203. });
  204. describe('when calling run twice', () => {
  205. it('then it should cancel previous run', (done) => {
  206. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  207. executeAnnotationQueryMock.mockReturnValueOnce(
  208. toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(10000))
  209. );
  210. expectOnResults({
  211. runner,
  212. panelId: 1,
  213. done,
  214. expect: (results) => {
  215. // should have one alert state, one snapshot, one legacy and one next gen result
  216. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  217. const { alertState, annotations } = getExpectedForAllResult();
  218. const expected = { alertState, annotations };
  219. expect(results).toEqual(expected);
  220. expect(annotationQueryMock).toHaveBeenCalledTimes(2);
  221. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(2);
  222. expect(getMock).toHaveBeenCalledTimes(2);
  223. },
  224. });
  225. runner.run(options);
  226. runner.run(options);
  227. });
  228. });
  229. describe('when calling cancel', () => {
  230. it('then it should cancel matching workers', (done) => {
  231. const { runner, options, annotationQueryMock, executeAnnotationQueryMock, getMock } = getTestContext();
  232. executeAnnotationQueryMock.mockReturnValueOnce(
  233. toAsyncOfResult({ events: [{ id: 'NextGen' }] }).pipe(delay(10000))
  234. );
  235. expectOnResults({
  236. runner,
  237. panelId: 1,
  238. done,
  239. expect: (results) => {
  240. // should have one alert state, one snapshot, one legacy and one next gen result
  241. // having both snapshot and legacy/next gen is a imaginary example for testing purposes and doesn't exist for real
  242. const { alertState, annotations } = getExpectedForAllResult();
  243. expect(results).toEqual({ alertState, annotations: [annotations[0], annotations[2]] });
  244. expect(annotationQueryMock).toHaveBeenCalledTimes(1);
  245. expect(executeAnnotationQueryMock).toHaveBeenCalledTimes(1);
  246. expect(getMock).toHaveBeenCalledTimes(1);
  247. },
  248. });
  249. runner.run(options);
  250. setTimeout(() => {
  251. // call to async needs to be async or the cancellation will be called before any of the workers have started
  252. runner.cancel(options.dashboard.annotations.list[1]);
  253. }, 100);
  254. });
  255. });
  256. });
  257. function getExpectedForAllResult(): DashboardQueryRunnerResult {
  258. return {
  259. alertState: {
  260. dashboardId: 1,
  261. id: 1,
  262. panelId: 1,
  263. state: AlertState.Alerting,
  264. },
  265. annotations: [
  266. {
  267. color: '#ffc0cb',
  268. id: 'Legacy',
  269. isRegion: false,
  270. source: {
  271. datasource: 'Legacy',
  272. enable: true,
  273. hide: false,
  274. iconColor: 'pink',
  275. id: undefined,
  276. name: 'Test',
  277. snapshotData: undefined,
  278. },
  279. type: 'Test',
  280. },
  281. {
  282. color: '#ffc0cb',
  283. id: 'NextGen',
  284. isRegion: false,
  285. source: {
  286. datasource: 'NextGen',
  287. enable: true,
  288. hide: false,
  289. iconColor: 'pink',
  290. id: undefined,
  291. name: 'Test',
  292. snapshotData: undefined,
  293. },
  294. type: 'Test',
  295. },
  296. {
  297. annotation: {
  298. datasource: 'Legacy',
  299. enable: true,
  300. hide: false,
  301. iconColor: 'pink',
  302. id: 'Snapshotted',
  303. name: 'Test',
  304. },
  305. color: '#ffc0cb',
  306. isRegion: true,
  307. source: {
  308. datasource: 'Legacy',
  309. enable: true,
  310. hide: false,
  311. iconColor: 'pink',
  312. id: 'Snapshotted',
  313. name: 'Test',
  314. },
  315. time: 1,
  316. timeEnd: 2,
  317. type: 'Test',
  318. },
  319. ],
  320. };
  321. }