PanelQueryRunner.test.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import { PanelQueryRunner, QueryRunnerOptions } from './PanelQueryRunner';
  2. import { PanelData, DataQueryRequest, DataStreamObserver, DataStreamState, ScopedVars } from '@grafana/ui';
  3. import { LoadingState, MutableDataFrame } from '@grafana/data';
  4. import { dateTime } from '@grafana/data';
  5. import { SHARED_DASHBODARD_QUERY } from 'app/plugins/datasource/dashboard/SharedQueryRunner';
  6. import { DashboardQuery } from 'app/plugins/datasource/dashboard/types';
  7. import { PanelModel } from './PanelModel';
  8. import { Subject } from 'rxjs';
  9. jest.mock('app/core/services/backend_srv');
  10. // Defined within setup functions
  11. const panelsForCurrentDashboardMock: { [key: number]: PanelModel } = {};
  12. jest.mock('app/features/dashboard/services/DashboardSrv', () => ({
  13. getDashboardSrv: () => {
  14. return {
  15. getCurrent: () => {
  16. return {
  17. getPanelById: (id: number) => {
  18. return panelsForCurrentDashboardMock[id];
  19. },
  20. };
  21. },
  22. };
  23. },
  24. }));
  25. interface ScenarioContext {
  26. setup: (fn: () => void) => void;
  27. // Options used in setup
  28. maxDataPoints?: number | null;
  29. widthPixels: number;
  30. dsInterval?: string;
  31. minInterval?: string;
  32. scopedVars: ScopedVars;
  33. // Filled in by the Scenario runner
  34. events?: PanelData[];
  35. res?: PanelData;
  36. queryCalledWith?: DataQueryRequest;
  37. observer: DataStreamObserver;
  38. runner: PanelQueryRunner;
  39. }
  40. type ScenarioFn = (ctx: ScenarioContext) => void;
  41. function describeQueryRunnerScenario(description: string, scenarioFn: ScenarioFn) {
  42. describe(description, () => {
  43. let setupFn = () => {};
  44. const ctx: ScenarioContext = {
  45. widthPixels: 200,
  46. scopedVars: {
  47. server: { text: 'Server1', value: 'server-1' },
  48. },
  49. runner: new PanelQueryRunner(1),
  50. observer: (args: any) => {},
  51. setup: (fn: () => void) => {
  52. setupFn = fn;
  53. },
  54. };
  55. const response: any = {
  56. data: [{ target: 'hello', datapoints: [[1, 1000], [2, 2000]] }],
  57. };
  58. beforeEach(async () => {
  59. setupFn();
  60. const datasource: any = {
  61. name: 'TestDB',
  62. interval: ctx.dsInterval,
  63. query: (options: DataQueryRequest, observer: DataStreamObserver) => {
  64. ctx.queryCalledWith = options;
  65. ctx.observer = observer;
  66. return Promise.resolve(response);
  67. },
  68. testDatasource: jest.fn(),
  69. };
  70. const args: any = {
  71. datasource,
  72. scopedVars: ctx.scopedVars,
  73. minInterval: ctx.minInterval,
  74. widthPixels: ctx.widthPixels,
  75. maxDataPoints: ctx.maxDataPoints,
  76. timeRange: {
  77. from: dateTime().subtract(1, 'days'),
  78. to: dateTime(),
  79. raw: { from: '1h', to: 'now' },
  80. },
  81. panelId: 1,
  82. queries: [{ refId: 'A', test: 1 }],
  83. };
  84. ctx.runner = new PanelQueryRunner(1);
  85. ctx.runner.subscribe({
  86. next: (data: PanelData) => {
  87. ctx.events.push(data);
  88. },
  89. });
  90. panelsForCurrentDashboardMock[1] = {
  91. id: 1,
  92. getQueryRunner: () => {
  93. return ctx.runner;
  94. },
  95. } as PanelModel;
  96. ctx.events = [];
  97. ctx.res = await ctx.runner.run(args);
  98. });
  99. scenarioFn(ctx);
  100. });
  101. }
  102. describe('PanelQueryRunner', () => {
  103. describeQueryRunnerScenario('simple scenario', ctx => {
  104. it('should set requestId on request', async () => {
  105. expect(ctx.queryCalledWith.requestId).toBe('Q100');
  106. });
  107. it('should set datasource name on request', async () => {
  108. expect(ctx.queryCalledWith.targets[0].datasource).toBe('TestDB');
  109. });
  110. it('should pass scopedVars to datasource with interval props', async () => {
  111. expect(ctx.queryCalledWith.scopedVars.server.text).toBe('Server1');
  112. expect(ctx.queryCalledWith.scopedVars.__interval.text).toBe('5m');
  113. expect(ctx.queryCalledWith.scopedVars.__interval_ms.text).toBe('300000');
  114. });
  115. });
  116. describeQueryRunnerScenario('with no maxDataPoints or minInterval', ctx => {
  117. ctx.setup(() => {
  118. ctx.maxDataPoints = null;
  119. ctx.widthPixels = 200;
  120. });
  121. it('should return data', async () => {
  122. expect(ctx.res.error).toBeUndefined();
  123. expect(ctx.res.series.length).toBe(1);
  124. });
  125. it('should use widthPixels as maxDataPoints', async () => {
  126. expect(ctx.queryCalledWith.maxDataPoints).toBe(200);
  127. });
  128. it('should calculate interval based on width', async () => {
  129. expect(ctx.queryCalledWith.interval).toBe('5m');
  130. });
  131. it('fast query should only publish 1 data events', async () => {
  132. expect(ctx.events.length).toBe(1);
  133. });
  134. });
  135. describeQueryRunnerScenario('with no panel min interval but datasource min interval', ctx => {
  136. ctx.setup(() => {
  137. ctx.widthPixels = 20000;
  138. ctx.dsInterval = '15s';
  139. });
  140. it('should limit interval to data source min interval', async () => {
  141. expect(ctx.queryCalledWith.interval).toBe('15s');
  142. });
  143. });
  144. describeQueryRunnerScenario('with panel min interval and data source min interval', ctx => {
  145. ctx.setup(() => {
  146. ctx.widthPixels = 20000;
  147. ctx.dsInterval = '15s';
  148. ctx.minInterval = '30s';
  149. });
  150. it('should limit interval to panel min interval', async () => {
  151. expect(ctx.queryCalledWith.interval).toBe('30s');
  152. });
  153. });
  154. describeQueryRunnerScenario('with maxDataPoints', ctx => {
  155. ctx.setup(() => {
  156. ctx.maxDataPoints = 10;
  157. });
  158. it('should pass maxDataPoints if specified', async () => {
  159. expect(ctx.queryCalledWith.maxDataPoints).toBe(10);
  160. });
  161. });
  162. describeQueryRunnerScenario('when datasource is streaming data', ctx => {
  163. let streamState: DataStreamState;
  164. let isUnsubbed = false;
  165. beforeEach(() => {
  166. streamState = {
  167. state: LoadingState.Streaming,
  168. key: 'test-stream-1',
  169. data: [
  170. new MutableDataFrame({
  171. fields: [],
  172. name: 'I am a magic stream',
  173. }),
  174. ],
  175. request: {
  176. requestId: ctx.queryCalledWith.requestId,
  177. } as any,
  178. unsubscribe: () => {
  179. isUnsubbed = true;
  180. },
  181. };
  182. ctx.observer(streamState);
  183. });
  184. it('should push another update to subscriber', async () => {
  185. expect(ctx.events.length).toBe(2);
  186. });
  187. it('should set state to streaming', async () => {
  188. expect(ctx.events[1].state).toBe(LoadingState.Streaming);
  189. });
  190. it('should not unsubscribe', async () => {
  191. expect(isUnsubbed).toBe(false);
  192. });
  193. it('destroy should unsubscribe streams', async () => {
  194. ctx.runner.destroy();
  195. expect(isUnsubbed).toBe(true);
  196. });
  197. });
  198. describeQueryRunnerScenario('Shared query request', ctx => {
  199. ctx.setup(() => {});
  200. it('should get the same results as the original', async () => {
  201. // Get the results from
  202. const q: DashboardQuery = { refId: 'Z', panelId: 1 };
  203. const myPanelId = 7;
  204. const runnerWantingSharedResults = new PanelQueryRunner(myPanelId);
  205. panelsForCurrentDashboardMock[myPanelId] = {
  206. id: myPanelId,
  207. getQueryRunner: () => {
  208. return runnerWantingSharedResults;
  209. },
  210. } as PanelModel;
  211. const res = await runnerWantingSharedResults.run({
  212. datasource: SHARED_DASHBODARD_QUERY,
  213. queries: [q],
  214. // Same query setup
  215. scopedVars: ctx.scopedVars,
  216. minInterval: ctx.minInterval,
  217. widthPixels: ctx.widthPixels,
  218. maxDataPoints: ctx.maxDataPoints,
  219. timeRange: {
  220. from: dateTime().subtract(1, 'days'),
  221. to: dateTime(),
  222. raw: { from: '1h', to: 'now' },
  223. },
  224. panelId: myPanelId, // Not 1
  225. });
  226. const req = res.request;
  227. expect(req.panelId).toBe(1); // The source panel
  228. expect(req.targets[0].datasource).toBe('TestDB');
  229. expect(res.series.length).toBe(1);
  230. expect(res.series[0].length).toBe(2);
  231. // Get the private subject and check that someone is listening
  232. const subject = (ctx.runner as any).subject as Subject<PanelData>;
  233. expect(subject.observers.length).toBe(2);
  234. // Now change the query and we should stop listening
  235. try {
  236. runnerWantingSharedResults.run({
  237. datasource: 'unknown-datasource',
  238. panelId: myPanelId, // Not 1
  239. } as QueryRunnerOptions);
  240. } catch {}
  241. // runnerWantingSharedResults subject is now unsubscribed
  242. // the test listener is still subscribed
  243. expect(subject.observers.length).toBe(1);
  244. });
  245. });
  246. });