evaluator.spec.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import {
  2. insertImplicitAND,
  3. type ProcessedTokenResult,
  4. toFlattened,
  5. } from 'sentry/components/searchSyntax/evaluator';
  6. import {
  7. parseSearch,
  8. Token,
  9. type TokenResult,
  10. } from 'sentry/components/searchSyntax/parser';
  11. const tokensToString = (tokens: ProcessedTokenResult[]): string => {
  12. let str = '';
  13. for (const token of tokens) {
  14. let concatstr;
  15. switch (token.type) {
  16. case Token.FREE_TEXT:
  17. concatstr = token.text;
  18. break;
  19. case Token.SPACES:
  20. concatstr = 'space';
  21. break;
  22. case Token.VALUE_DURATION:
  23. case Token.VALUE_BOOLEAN:
  24. case Token.VALUE_NUMBER:
  25. case Token.VALUE_SIZE:
  26. case Token.VALUE_PERCENTAGE:
  27. case Token.VALUE_TEXT:
  28. case Token.VALUE_ISO_8601_DATE:
  29. case Token.VALUE_RELATIVE_DATE:
  30. concatstr = token.value;
  31. break;
  32. case Token.LOGIC_GROUP:
  33. case Token.LOGIC_BOOLEAN:
  34. concatstr = token.text;
  35. break;
  36. case Token.KEY_SIMPLE:
  37. concatstr = token.text + ':';
  38. break;
  39. case Token.VALUE_NUMBER_LIST:
  40. case Token.VALUE_TEXT_LIST:
  41. concatstr = token.text;
  42. break;
  43. case Token.KEY_EXPLICIT_TAG:
  44. concatstr = token.key;
  45. break;
  46. case 'L_PAREN': {
  47. concatstr = '(';
  48. break;
  49. }
  50. case 'R_PAREN': {
  51. concatstr = ')';
  52. break;
  53. }
  54. default: {
  55. concatstr = token.text;
  56. break;
  57. }
  58. }
  59. // The parsing logic text() captures leading/trailing spaces in some cases.
  60. // We'll just trim them so the tests are easier to read.
  61. str += concatstr.trim();
  62. str += concatstr && tokens.indexOf(token) !== tokens.length - 1 ? ' ' : '';
  63. }
  64. return str;
  65. };
  66. function assertTokens(
  67. tokens: TokenResult<Token>[] | null
  68. ): asserts tokens is TokenResult<Token>[] {
  69. if (tokens === null) {
  70. throw new Error('Expected tokens to be an array');
  71. }
  72. }
  73. describe('Search Syntax Evaluator', () => {
  74. describe('flatten tree', () => {
  75. it('flattens simple expressions', () => {
  76. const tokens = parseSearch('is:unresolved duration:>1h');
  77. assertTokens(tokens);
  78. const flattened = toFlattened(tokens);
  79. expect(flattened).toHaveLength(2);
  80. expect(tokensToString(flattened)).toBe('is:unresolved duration:>1h');
  81. });
  82. it('handles filters', () => {
  83. const tokens = parseSearch('has:unresolved duration:[1,2,3]');
  84. assertTokens(tokens);
  85. const flattened = toFlattened(tokens);
  86. expect(flattened).toHaveLength(2);
  87. expect(tokensToString(flattened)).toBe('has:unresolved duration:[1,2,3]');
  88. });
  89. it('handles free text', () => {
  90. const tokens = parseSearch('hello world');
  91. assertTokens(tokens);
  92. const flattened = toFlattened(tokens);
  93. expect(flattened).toHaveLength(1);
  94. expect(tokensToString(flattened)).toBe('hello world');
  95. });
  96. it('handles logical booleans', () => {
  97. const tokens = parseSearch('hello AND world');
  98. assertTokens(tokens);
  99. const flattened = toFlattened(tokens);
  100. expect(flattened).toHaveLength(3);
  101. expect(tokensToString(flattened)).toBe('hello AND world');
  102. });
  103. it('handles logical groups', () => {
  104. const tokens = parseSearch('is:unresolved AND (is:dead OR is:alive)');
  105. assertTokens(tokens);
  106. const flattened = toFlattened(tokens);
  107. expect(flattened).toHaveLength(7);
  108. expect(tokensToString(flattened)).toBe('is:unresolved AND ( is:dead OR is:alive )');
  109. });
  110. });
  111. describe('injects implicit AND', () => {
  112. describe('boolean operators', () => {
  113. it('implicit AND', () => {
  114. const tokens = toFlattened(parseSearch('is:unresolved duration:>1h')!);
  115. const withImplicitAND = insertImplicitAND(tokens);
  116. expect(tokensToString(withImplicitAND)).toBe('is:unresolved AND duration:>1h');
  117. });
  118. it('explicit AND', () => {
  119. const tokens = toFlattened(parseSearch('is:unresolved AND duration:>1h')!);
  120. const withImplicitAND = insertImplicitAND(tokens);
  121. expect(tokensToString(withImplicitAND)).toBe('is:unresolved AND duration:>1h');
  122. });
  123. it('multiple implicit AND', () => {
  124. const tokens = toFlattened(
  125. parseSearch('is:unresolved duration:>1h duration:<1m')!
  126. );
  127. const withImplicitAND = insertImplicitAND(tokens);
  128. expect(tokensToString(withImplicitAND)).toBe(
  129. 'is:unresolved AND duration:>1h AND duration:<1m'
  130. );
  131. });
  132. it('explicit OR', () => {
  133. const tokens = toFlattened(parseSearch('is:unresolved OR duration:>1h')!);
  134. const withImplicitAND = insertImplicitAND(tokens);
  135. expect(tokensToString(withImplicitAND)).toBe('is:unresolved OR duration:>1h');
  136. });
  137. it('multiple explicit OR', () => {
  138. const tokens = toFlattened(
  139. parseSearch('is:unresolved OR duration:>1h OR duration:<1h')!
  140. );
  141. const withImplicitAND = insertImplicitAND(tokens);
  142. expect(tokensToString(withImplicitAND)).toBe(
  143. 'is:unresolved OR duration:>1h OR duration:<1h'
  144. );
  145. });
  146. it('with logical groups', () => {
  147. const tokens = toFlattened(parseSearch('is:unresolved (duration:>1h)')!);
  148. const withImplicitAND = insertImplicitAND(tokens);
  149. expect(tokensToString(withImplicitAND)).toBe(
  150. 'is:unresolved AND ( duration:>1h )'
  151. );
  152. });
  153. });
  154. describe('logical groups', () => {
  155. it('explicit OR', () => {
  156. const tokens = toFlattened(parseSearch('is:unresolved OR ( duration:>1h )')!);
  157. const withImplicitAND = insertImplicitAND(tokens);
  158. expect(tokensToString(withImplicitAND)).toBe('is:unresolved OR ( duration:>1h )');
  159. });
  160. it('explicit AND', () => {
  161. const tokens = toFlattened(parseSearch('is:unresolved AND ( duration:>1h )')!);
  162. expect(tokensToString(tokens)).toBe('is:unresolved AND ( duration:>1h )');
  163. });
  164. });
  165. describe('complex expressions', () => {
  166. it('handles complex expressions', () => {
  167. const tokens = toFlattened(
  168. parseSearch('is:unresolved AND ( duration:>1h OR duration:<1h )')!
  169. );
  170. expect(tokensToString(tokens)).toBe(
  171. 'is:unresolved AND ( duration:>1h OR duration:<1h )'
  172. );
  173. });
  174. it('handles complex expressions with implicit AND', () => {
  175. const tokens = toFlattened(
  176. parseSearch('is:unresolved ( duration:>1h OR ( duration:<1h duration:1m ) )')!
  177. );
  178. const withImplicitAND = insertImplicitAND(tokens);
  179. expect(tokensToString(withImplicitAND)).toBe(
  180. 'is:unresolved AND ( duration:>1h OR ( duration:<1h AND duration:1m ) )'
  181. );
  182. });
  183. });
  184. });
  185. });