runRequest.test.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { Observable, Subscriber, Subscription } from 'rxjs';
  2. import {
  3. DataFrame,
  4. DataQueryRequest,
  5. DataQueryResponse,
  6. DataSourceApi,
  7. DataTopic,
  8. dateTime,
  9. LoadingState,
  10. PanelData,
  11. } from '@grafana/data';
  12. import { setEchoSrv } from '@grafana/runtime';
  13. import { deepFreeze } from '../../../../test/core/redux/reducerTester';
  14. import { Echo } from '../../../core/services/echo/Echo';
  15. import { DashboardModel } from '../../dashboard/state/DashboardModel';
  16. import { runRequest } from './runRequest';
  17. jest.mock('app/core/services/backend_srv');
  18. const dashboardModel = new DashboardModel({
  19. panels: [{ id: 1, type: 'graph' }],
  20. });
  21. jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
  22. getDashboardSrv: () => {
  23. return {
  24. getCurrent: () => dashboardModel,
  25. };
  26. },
  27. }));
  28. class ScenarioCtx {
  29. ds!: DataSourceApi;
  30. request!: DataQueryRequest;
  31. subscriber!: Subscriber<DataQueryResponse>;
  32. isUnsubbed = false;
  33. setupFn: () => void = () => {};
  34. results!: PanelData[];
  35. subscription!: Subscription;
  36. wasStarted = false;
  37. error: Error | null = null;
  38. toStartTime = dateTime();
  39. fromStartTime = dateTime();
  40. reset() {
  41. this.wasStarted = false;
  42. this.isUnsubbed = false;
  43. this.results = [];
  44. this.request = {
  45. range: {
  46. from: this.fromStartTime,
  47. to: this.toStartTime,
  48. raw: { from: '1h', to: 'now' },
  49. },
  50. targets: [
  51. {
  52. refId: 'A',
  53. },
  54. ],
  55. } as DataQueryRequest;
  56. this.ds = {
  57. query: (request: DataQueryRequest) => {
  58. return new Observable<DataQueryResponse>((subscriber) => {
  59. this.subscriber = subscriber;
  60. this.wasStarted = true;
  61. if (this.error) {
  62. throw this.error;
  63. }
  64. return () => {
  65. this.isUnsubbed = true;
  66. };
  67. });
  68. },
  69. } as DataSourceApi;
  70. }
  71. start() {
  72. this.subscription = runRequest(this.ds, this.request).subscribe({
  73. next: (data: PanelData) => {
  74. this.results.push(data);
  75. },
  76. });
  77. }
  78. emitPacket(packet: DataQueryResponse) {
  79. this.subscriber.next(packet);
  80. }
  81. setup(fn: () => void) {
  82. this.setupFn = fn;
  83. }
  84. }
  85. function runRequestScenario(desc: string, fn: (ctx: ScenarioCtx) => void) {
  86. describe(desc, () => {
  87. const ctx = new ScenarioCtx();
  88. beforeEach(() => {
  89. setEchoSrv(new Echo());
  90. ctx.reset();
  91. return ctx.setupFn();
  92. });
  93. fn(ctx);
  94. });
  95. }
  96. function runRequestScenarioThatThrows(desc: string, fn: (ctx: ScenarioCtx) => void) {
  97. describe(desc, () => {
  98. const ctx = new ScenarioCtx();
  99. let consoleSpy: jest.SpyInstance<any>;
  100. beforeEach(() => {
  101. consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
  102. setEchoSrv(new Echo());
  103. ctx.reset();
  104. return ctx.setupFn();
  105. });
  106. afterEach(() => {
  107. consoleSpy.mockRestore();
  108. });
  109. fn(ctx);
  110. });
  111. }
  112. describe('runRequest', () => {
  113. runRequestScenario('with no queries', (ctx) => {
  114. ctx.setup(() => {
  115. ctx.request.targets = [];
  116. ctx.start();
  117. });
  118. it('should emit empty result with loading state done', () => {
  119. expect(ctx.wasStarted).toBe(false);
  120. expect(ctx.results[0].state).toBe(LoadingState.Done);
  121. });
  122. });
  123. runRequestScenario('After first response', (ctx) => {
  124. ctx.setup(() => {
  125. ctx.start();
  126. ctx.emitPacket({
  127. data: [{ name: 'Data' } as DataFrame],
  128. });
  129. });
  130. it('should emit single result with loading state done', () => {
  131. expect(ctx.wasStarted).toBe(true);
  132. expect(ctx.results.length).toBe(1);
  133. });
  134. });
  135. runRequestScenario('After three responses, 2 with different keys', (ctx) => {
  136. ctx.setup(() => {
  137. ctx.start();
  138. ctx.emitPacket({
  139. data: [{ name: 'DataA-1' } as DataFrame],
  140. key: 'A',
  141. });
  142. ctx.emitPacket({
  143. data: [{ name: 'DataA-2' } as DataFrame],
  144. key: 'A',
  145. });
  146. ctx.emitPacket({
  147. data: [{ name: 'DataB-1' } as DataFrame],
  148. key: 'B',
  149. });
  150. });
  151. it('should emit 3 separate results', () => {
  152. expect(ctx.results.length).toBe(3);
  153. });
  154. it('should combine results and return latest data for key A', () => {
  155. expect(ctx.results[2].series).toEqual([{ name: 'DataA-2' }, { name: 'DataB-1' }]);
  156. });
  157. it('should have loading state Done', () => {
  158. expect(ctx.results[2].state).toEqual(LoadingState.Done);
  159. });
  160. });
  161. runRequestScenario('When the key is defined in refId', (ctx) => {
  162. ctx.setup(() => {
  163. ctx.start();
  164. ctx.emitPacket({
  165. data: [{ name: 'DataX-1', refId: 'X' } as DataFrame],
  166. });
  167. ctx.emitPacket({
  168. data: [{ name: 'DataY-1', refId: 'Y' } as DataFrame],
  169. });
  170. ctx.emitPacket({
  171. data: [{ name: 'DataY-2', refId: 'Y' } as DataFrame],
  172. });
  173. });
  174. it('should emit 3 separate results', () => {
  175. expect(ctx.results.length).toBe(3);
  176. });
  177. it('should keep data for X and Y', () => {
  178. expect(ctx.results[2].series).toMatchInlineSnapshot(`
  179. Array [
  180. Object {
  181. "name": "DataX-1",
  182. "refId": "X",
  183. },
  184. Object {
  185. "name": "DataY-2",
  186. "refId": "Y",
  187. },
  188. ]
  189. `);
  190. });
  191. });
  192. runRequestScenario('After response with state Streaming', (ctx) => {
  193. ctx.setup(() => {
  194. ctx.start();
  195. ctx.emitPacket({
  196. data: [{ name: 'DataA-1' } as DataFrame],
  197. key: 'A',
  198. });
  199. ctx.emitPacket({
  200. data: [{ name: 'DataA-2' } as DataFrame],
  201. key: 'A',
  202. state: LoadingState.Streaming,
  203. });
  204. });
  205. it('should have loading state Streaming', () => {
  206. expect(ctx.results[1].state).toEqual(LoadingState.Streaming);
  207. });
  208. });
  209. runRequestScenario('If no response after 250ms', (ctx) => {
  210. ctx.setup(async () => {
  211. ctx.start();
  212. await sleep(250);
  213. });
  214. it('should emit 1 result with loading state', () => {
  215. expect(ctx.results.length).toBe(1);
  216. expect(ctx.results[0].state).toBe(LoadingState.Loading);
  217. });
  218. });
  219. runRequestScenarioThatThrows('on thrown error', (ctx) => {
  220. ctx.setup(() => {
  221. ctx.error = new Error('Ohh no');
  222. ctx.start();
  223. });
  224. it('should emit 1 error result', () => {
  225. expect(ctx.results[0].error?.message).toBe('Ohh no');
  226. expect(ctx.results[0].state).toBe(LoadingState.Error);
  227. });
  228. });
  229. runRequestScenario('If time range is relative', (ctx) => {
  230. ctx.setup(async () => {
  231. // any changes to ctx.request.range will throw and state would become LoadingState.Error
  232. deepFreeze(ctx.request.range);
  233. ctx.start();
  234. // wait a bit
  235. await sleep(20);
  236. ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame], state: LoadingState.Streaming });
  237. });
  238. it('should add the correct timeRange property and the request range should not be mutated', () => {
  239. expect(ctx.results[0].timeRange.to.valueOf()).toBeDefined();
  240. expect(ctx.results[0].timeRange.to.valueOf()).not.toBe(ctx.toStartTime.valueOf());
  241. expect(ctx.results[0].timeRange.to.valueOf()).not.toBe(ctx.results[0].request?.range?.to.valueOf());
  242. expectThatRangeHasNotMutated(ctx);
  243. });
  244. });
  245. runRequestScenario('If time range is not relative', (ctx) => {
  246. ctx.setup(async () => {
  247. ctx.request.range!.raw.from = ctx.fromStartTime;
  248. ctx.request.range!.raw.to = ctx.toStartTime;
  249. // any changes to ctx.request.range will throw and state would become LoadingState.Error
  250. deepFreeze(ctx.request.range);
  251. ctx.start();
  252. // wait a bit
  253. await sleep(20);
  254. ctx.emitPacket({ data: [{ name: 'DataB-1' } as DataFrame] });
  255. });
  256. it('should add the correct timeRange property and the request range should not be mutated', () => {
  257. expect(ctx.results[0].timeRange).toBeDefined();
  258. expect(ctx.results[0].timeRange.to.valueOf()).toBe(ctx.toStartTime.valueOf());
  259. expect(ctx.results[0].timeRange.to.valueOf()).toBe(ctx.results[0].request?.range?.to.valueOf());
  260. expectThatRangeHasNotMutated(ctx);
  261. });
  262. });
  263. runRequestScenario('With annotations dataTopic', (ctx) => {
  264. ctx.setup(() => {
  265. ctx.start();
  266. ctx.emitPacket({
  267. data: [{ name: 'DataA-1' } as DataFrame],
  268. key: 'A',
  269. });
  270. ctx.emitPacket({
  271. data: [{ name: 'DataA-2', meta: { dataTopic: DataTopic.Annotations } } as DataFrame],
  272. key: 'B',
  273. });
  274. });
  275. it('should separate annotations results', () => {
  276. expect(ctx.results[1].annotations?.length).toBe(1);
  277. expect(ctx.results[1].series.length).toBe(1);
  278. });
  279. });
  280. });
  281. const expectThatRangeHasNotMutated = (ctx: ScenarioCtx) => {
  282. // Make sure that the range for request is not changed and that deepfreeze hasn't thrown
  283. expect(ctx.results[0].request?.range?.to.valueOf()).toBe(ctx.toStartTime.valueOf());
  284. expect(ctx.results[0].error).not.toBeDefined();
  285. };
  286. async function sleep(ms: number) {
  287. return new Promise((resolve) => {
  288. setTimeout(resolve, ms);
  289. });
  290. }