parseQuery.ts (6520B)
1 import { assert } from '../../util/util.js'; 2 import { 3 TestParamsRW, 4 JSONWithUndefined, 5 badParamValueChars, 6 paramKeyIsPublic, 7 } from '../params_utils.js'; 8 9 import { parseParamValue } from './json_param_value.js'; 10 import { 11 TestQuery, 12 TestQueryMultiFile, 13 TestQueryMultiTest, 14 TestQueryMultiCase, 15 TestQuerySingleCase, 16 } from './query.js'; 17 import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js'; 18 import { validQueryPart } from './validQueryPart.js'; 19 20 /** 21 * converts foo/bar/src/webgpu/this/that/file.spec.ts to webgpu:this,that,file,* 22 */ 23 function convertPathToQuery(path: string) { 24 // removes .spec.ts and splits by directory separators. 25 const parts = path.substring(0, path.length - 8).split(/\/|\\/g); 26 // Gets parts only after the last `src`. Example: returns ['webgpu', 'foo', 'bar', 'test'] 27 // for ['Users', 'me', 'src', 'cts', 'src', 'webgpu', 'foo', 'bar', 'test'] 28 const partsAfterSrc = parts.slice(parts.lastIndexOf('src') + 1); 29 const suite = partsAfterSrc.shift(); 30 return `${suite}:${partsAfterSrc.join(',')},*`; 31 } 32 33 /** 34 * If a query looks like a path (ends in .spec.ts and has directory separators) 35 * then convert try to convert it to a query. 36 */ 37 function convertPathLikeToQuery(queryOrPath: string) { 38 return queryOrPath.endsWith('.spec.ts') && 39 (queryOrPath.includes('/') || queryOrPath.includes('\\')) 40 ? convertPathToQuery(queryOrPath) 41 : queryOrPath; 42 } 43 44 /** 45 * Convert long suite names (the part before the first colon) to the 46 * shortest last word 47 * foo.bar.moo:test,subtest,foo -> moo:test,subtest,foo 48 */ 49 function shortenSuiteName(query: string) { 50 const parts = query.split(':'); 51 // converts foo.bar.moo to moo 52 const suite = parts.shift()?.replace(/.*\.(\w+)$/, '$1'); 53 return [suite, ...parts].join(':'); 54 } 55 56 export function parseQuery(queryLike: string): TestQuery { 57 try { 58 const query = shortenSuiteName(convertPathLikeToQuery(queryLike)); 59 return parseQueryImpl(query); 60 } catch (ex) { 61 if (ex instanceof Error) { 62 ex.message += `\n on: ${queryLike}`; 63 } 64 throw ex; 65 } 66 } 67 68 function parseQueryImpl(s: string): TestQuery { 69 // Undo encodeURIComponentSelectively 70 s = decodeURIComponent(s); 71 72 // bigParts are: suite, file, test, params (note kBigSeparator could appear in params) 73 let suite: string; 74 let fileString: string | undefined; 75 let testString: string | undefined; 76 let paramsString: string | undefined; 77 { 78 const i1 = s.indexOf(kBigSeparator); 79 assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`); 80 suite = s.substring(0, i1); 81 const i2 = s.indexOf(kBigSeparator, i1 + 1); 82 if (i2 === -1) { 83 fileString = s.substring(i1 + 1); 84 } else { 85 fileString = s.substring(i1 + 1, i2); 86 const i3 = s.indexOf(kBigSeparator, i2 + 1); 87 if (i3 === -1) { 88 testString = s.substring(i2 + 1); 89 } else { 90 testString = s.substring(i2 + 1, i3); 91 paramsString = s.substring(i3 + 1); 92 } 93 } 94 } 95 96 const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator); 97 98 if (testString === undefined) { 99 // Query is file-level 100 assert( 101 filePathHasWildcard, 102 `File-level query without wildcard ${kWildcard}. Did you want a file-level query \ 103 (append ${kPathSeparator}${kWildcard}) or test-level query (append ${kBigSeparator}${kWildcard})?` 104 ); 105 return new TestQueryMultiFile(suite, file); 106 } 107 assert(!filePathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`); 108 109 const { parts: test, wildcard: testPathHasWildcard } = parseBigPart(testString, kPathSeparator); 110 111 if (paramsString === undefined) { 112 // Query is test-level 113 assert( 114 testPathHasWildcard, 115 `Test-level query without wildcard ${kWildcard}; did you want a test-level query \ 116 (append ${kPathSeparator}${kWildcard}) or case-level query (append ${kBigSeparator}${kWildcard})?` 117 ); 118 assert(file.length > 0, 'File part of test-level query was empty (::)'); 119 return new TestQueryMultiTest(suite, file, test); 120 } 121 122 // Query is case-level 123 assert(!testPathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`); 124 125 const { parts: paramsParts, wildcard: paramsHasWildcard } = parseBigPart( 126 paramsString, 127 kParamSeparator 128 ); 129 130 assert(test.length > 0, 'Test part of case-level query was empty (::)'); 131 132 const params: TestParamsRW = {}; 133 for (const paramPart of paramsParts) { 134 const [k, v] = parseSingleParam(paramPart); 135 assert(validQueryPart.test(k), `param key names must match ${validQueryPart}`); 136 params[k] = v; 137 } 138 if (paramsHasWildcard) { 139 return new TestQueryMultiCase(suite, file, test, params); 140 } else { 141 return new TestQuerySingleCase(suite, file, test, params); 142 } 143 } 144 145 // webgpu:a,b,* or webgpu:a,b,c:* 146 const kExampleQueries = `\ 147 webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}${kWildcard} or \ 148 webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}c${kBigSeparator}${kWildcard}`; 149 150 function parseBigPart( 151 s: string, 152 separator: typeof kParamSeparator | typeof kPathSeparator 153 ): { parts: string[]; wildcard: boolean } { 154 if (s === '') { 155 return { parts: [], wildcard: false }; 156 } 157 const parts = s.split(separator); 158 159 let endsWithWildcard = false; 160 for (const [i, part] of parts.entries()) { 161 if (i === parts.length - 1) { 162 endsWithWildcard = part === kWildcard; 163 } 164 assert( 165 part.indexOf(kWildcard) === -1 || endsWithWildcard, 166 `Wildcard ${kWildcard} must be complete last part of a path (e.g. ${kExampleQueries})` 167 ); 168 } 169 if (endsWithWildcard) { 170 // Remove the last element of the array (which is just the wildcard). 171 parts.length = parts.length - 1; 172 } 173 return { parts, wildcard: endsWithWildcard }; 174 } 175 176 function parseSingleParam(paramSubstring: string): [string, JSONWithUndefined] { 177 assert(paramSubstring !== '', 'Param in a query must not be blank (is there a trailing comma?)'); 178 const i = paramSubstring.indexOf('='); 179 assert(i !== -1, 'Param in a query must be of form key=value'); 180 const k = paramSubstring.substring(0, i); 181 assert(paramKeyIsPublic(k), 'Param in a query must not be private (start with _)'); 182 const v = paramSubstring.substring(i + 1); 183 return [k, parseSingleParamValue(v)]; 184 } 185 186 function parseSingleParamValue(s: string): JSONWithUndefined { 187 assert( 188 !badParamValueChars.test(s), 189 `param value must not match ${badParamValueChars} - was ${s}` 190 ); 191 return parseParamValue(s); 192 }