language_provider.test.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import Prism, { Token } from 'prismjs';
  2. import { Value } from 'slate';
  3. import { TypeaheadOutput } from '@grafana/ui';
  4. import { CloudWatchDatasource } from './datasource';
  5. import { CloudWatchLanguageProvider } from './language_provider';
  6. import {
  7. AGGREGATION_FUNCTIONS_STATS,
  8. BOOLEAN_FUNCTIONS,
  9. DATETIME_FUNCTIONS,
  10. IP_FUNCTIONS,
  11. NUMERIC_OPERATORS,
  12. QUERY_COMMANDS,
  13. STRING_FUNCTIONS,
  14. FIELD_AND_FILTER_FUNCTIONS,
  15. } from './syntax';
  16. import { GetLogGroupFieldsResponse } from './types';
  17. const fields = ['field1', '@message'];
  18. describe('CloudWatchLanguageProvider', () => {
  19. it('should suggest ', async () => {
  20. await runSuggestionTest('stats count(\\)', [fields]);
  21. // Make sure having a field prefix does not brake anything
  22. await runSuggestionTest('stats count(@mess\\)', [fields]);
  23. });
  24. it('should suggest query commands on start of query', async () => {
  25. await runSuggestionTest('\\', [QUERY_COMMANDS.map((v) => v.label)]);
  26. });
  27. it('should suggest query commands after pipe', async () => {
  28. await runSuggestionTest('fields f | \\', [QUERY_COMMANDS.map((v) => v.label)]);
  29. });
  30. it('should suggest fields and functions after field command', async () => {
  31. await runSuggestionTest('fields \\', [fields, FIELD_AND_FILTER_FUNCTIONS.map((v) => v.label)]);
  32. });
  33. it('should suggest fields and functions after comma', async () => {
  34. await runSuggestionTest('fields field1, \\', [fields, FIELD_AND_FILTER_FUNCTIONS.map((v) => v.label)]);
  35. });
  36. it('should suggest fields and functions after comma with prefix', async () => {
  37. await runSuggestionTest('fields field1, @mess\\', [fields, FIELD_AND_FILTER_FUNCTIONS.map((v) => v.label)]);
  38. });
  39. it('should suggest fields and functions after display command', async () => {
  40. await runSuggestionTest('display \\', [fields, FIELD_AND_FILTER_FUNCTIONS.map((v) => v.label)]);
  41. });
  42. it('should suggest functions after stats command', async () => {
  43. await runSuggestionTest('stats \\', [AGGREGATION_FUNCTIONS_STATS.map((v) => v.label)]);
  44. });
  45. it('should suggest fields and some functions after `by` command', async () => {
  46. await runSuggestionTest('stats count(something) by \\', [
  47. fields,
  48. STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS).map((v) => v.label),
  49. ]);
  50. });
  51. it('should suggest fields and some functions after comparison operator', async () => {
  52. await runSuggestionTest('filter field1 >= \\', [
  53. fields,
  54. [...NUMERIC_OPERATORS.map((v) => v.label), ...BOOLEAN_FUNCTIONS.map((v) => v.label)],
  55. ]);
  56. });
  57. it('should suggest fields directly after sort', async () => {
  58. await runSuggestionTest('sort \\', [fields]);
  59. });
  60. it('should suggest fields directly after sort after a pipe', async () => {
  61. await runSuggestionTest('fields field1 | sort \\', [fields]);
  62. });
  63. it('should suggest sort order after sort command and field', async () => {
  64. await runSuggestionTest('sort field1 \\', [['asc', 'desc']]);
  65. });
  66. it('should suggest fields directly after parse', async () => {
  67. await runSuggestionTest('parse \\', [fields]);
  68. });
  69. it('should suggest fields and bool functions after filter', async () => {
  70. await runSuggestionTest('filter \\', [fields, BOOLEAN_FUNCTIONS.map((v) => v.label)]);
  71. });
  72. it('should suggest fields and functions after filter bin() function', async () => {
  73. await runSuggestionTest('stats count(@message) by bin(30m), \\', [
  74. fields,
  75. STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS).map((v) => v.label),
  76. ]);
  77. });
  78. it('should not suggest anything if not after comma in by expression', async () => {
  79. await runSuggestionTest('stats count(@message) by bin(30m) \\', []);
  80. });
  81. });
  82. async function runSuggestionTest(query: string, expectedItems: string[][]) {
  83. const result = await getProvideCompletionItems(query);
  84. expectedItems.forEach((items, index) => {
  85. expect(result.suggestions[index].items.map((item) => item.label)).toEqual(items);
  86. });
  87. }
  88. function makeDatasource(): CloudWatchDatasource {
  89. return {
  90. getLogGroupFields(): Promise<GetLogGroupFieldsResponse> {
  91. return Promise.resolve({ logGroupFields: [{ name: 'field1' }, { name: '@message' }] });
  92. },
  93. } as any;
  94. }
  95. /**
  96. * Get suggestion items based on query. Use `\\` to mark position of the cursor.
  97. */
  98. function getProvideCompletionItems(query: string): Promise<TypeaheadOutput> {
  99. const provider = new CloudWatchLanguageProvider(makeDatasource());
  100. const cursorOffset = query.indexOf('\\');
  101. const queryWithoutCursor = query.replace('\\', '');
  102. let tokens: Token[] = Prism.tokenize(queryWithoutCursor, provider.getSyntax()) as any;
  103. tokens = addTokenMetadata(tokens);
  104. const value = new ValueMock(tokens, cursorOffset);
  105. return provider.provideCompletionItems(
  106. {
  107. value,
  108. } as any,
  109. { logGroupNames: ['logGroup1'], region: 'custom' }
  110. );
  111. }
  112. class ValueMock {
  113. selection: Value['selection'];
  114. data: Value['data'];
  115. constructor(tokens: Array<string | Token>, cursorOffset: number) {
  116. this.selection = {
  117. start: {
  118. offset: cursorOffset,
  119. },
  120. } as any;
  121. this.data = {
  122. get() {
  123. return tokens;
  124. },
  125. } as any;
  126. }
  127. }
  128. /**
  129. * Adds some Slate specific metadata
  130. * @param tokens
  131. */
  132. function addTokenMetadata(tokens: Array<string | Token>): Token[] {
  133. let prev = undefined as any;
  134. let offset = 0;
  135. return tokens.reduce((acc, token) => {
  136. let newToken: any;
  137. if (typeof token === 'string') {
  138. newToken = {
  139. content: token,
  140. // Not sure what else could it be here, probably if we do not match something
  141. types: ['whitespace'],
  142. };
  143. } else {
  144. newToken = { ...token };
  145. newToken.types = [token.type];
  146. }
  147. newToken.prev = prev;
  148. if (newToken.prev) {
  149. newToken.prev.next = newToken;
  150. }
  151. const end = offset + token.length;
  152. newToken.offsets = {
  153. start: offset,
  154. end,
  155. };
  156. prev = newToken;
  157. offset = end;
  158. return [...acc, newToken];
  159. }, [] as Token[]);
  160. }