Source code
Revision control
Copy as Markdown
Other Tools
import { assert } from '../../util/util.js';
import {
TestParamsRW,
JSONWithUndefined,
badParamValueChars,
paramKeyIsPublic,
} from '../params_utils.js';
import { parseParamValue } from './json_param_value.js';
import {
TestQuery,
TestQueryMultiFile,
TestQueryMultiTest,
TestQueryMultiCase,
TestQuerySingleCase,
} from './query.js';
import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js';
import { validQueryPart } from './validQueryPart.js';
/**
* converts foo/bar/src/webgpu/this/that/file.spec.ts to webgpu:this,that,file,*
*/
function convertPathToQuery(path: string) {
// removes .spec.ts and splits by directory separators.
const parts = path.substring(0, path.length - 8).split(/\/|\\/g);
// Gets parts only after the last `src`. Example: returns ['webgpu', 'foo', 'bar', 'test']
// for ['Users', 'me', 'src', 'cts', 'src', 'webgpu', 'foo', 'bar', 'test']
const partsAfterSrc = parts.slice(parts.lastIndexOf('src') + 1);
const suite = partsAfterSrc.shift();
return `${suite}:${partsAfterSrc.join(',')},*`;
}
/**
* If a query looks like a path (ends in .spec.ts and has directory separators)
* then convert try to convert it to a query.
*/
function convertPathLikeToQuery(queryOrPath: string) {
return queryOrPath.endsWith('.spec.ts') &&
(queryOrPath.includes('/') || queryOrPath.includes('\\'))
? convertPathToQuery(queryOrPath)
: queryOrPath;
}
/**
* Convert long suite names (the part before the first colon) to the
* shortest last word
* foo.bar.moo:test,subtest,foo -> moo:test,subtest,foo
*/
function shortenSuiteName(query: string) {
const parts = query.split(':');
// converts foo.bar.moo to moo
const suite = parts.shift()?.replace(/.*\.(\w+)$/, '$1');
return [suite, ...parts].join(':');
}
export function parseQuery(queryLike: string): TestQuery {
try {
const query = shortenSuiteName(convertPathLikeToQuery(queryLike));
return parseQueryImpl(query);
} catch (ex) {
if (ex instanceof Error) {
ex.message += `\n on: ${queryLike}`;
}
throw ex;
}
}
function parseQueryImpl(s: string): TestQuery {
// Undo encodeURIComponentSelectively
s = decodeURIComponent(s);
// bigParts are: suite, file, test, params (note kBigSeparator could appear in params)
let suite: string;
let fileString: string | undefined;
let testString: string | undefined;
let paramsString: string | undefined;
{
const i1 = s.indexOf(kBigSeparator);
assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`);
suite = s.substring(0, i1);
const i2 = s.indexOf(kBigSeparator, i1 + 1);
if (i2 === -1) {
fileString = s.substring(i1 + 1);
} else {
fileString = s.substring(i1 + 1, i2);
const i3 = s.indexOf(kBigSeparator, i2 + 1);
if (i3 === -1) {
testString = s.substring(i2 + 1);
} else {
testString = s.substring(i2 + 1, i3);
paramsString = s.substring(i3 + 1);
}
}
}
const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator);
if (testString === undefined) {
// Query is file-level
assert(
filePathHasWildcard,
`File-level query without wildcard ${kWildcard}. Did you want a file-level query \
(append ${kPathSeparator}${kWildcard}) or test-level query (append ${kBigSeparator}${kWildcard})?`
);
return new TestQueryMultiFile(suite, file);
}
assert(!filePathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
const { parts: test, wildcard: testPathHasWildcard } = parseBigPart(testString, kPathSeparator);
if (paramsString === undefined) {
// Query is test-level
assert(
testPathHasWildcard,
`Test-level query without wildcard ${kWildcard}; did you want a test-level query \
(append ${kPathSeparator}${kWildcard}) or case-level query (append ${kBigSeparator}${kWildcard})?`
);
assert(file.length > 0, 'File part of test-level query was empty (::)');
return new TestQueryMultiTest(suite, file, test);
}
// Query is case-level
assert(!testPathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
const { parts: paramsParts, wildcard: paramsHasWildcard } = parseBigPart(
paramsString,
kParamSeparator
);
assert(test.length > 0, 'Test part of case-level query was empty (::)');
const params: TestParamsRW = {};
for (const paramPart of paramsParts) {
const [k, v] = parseSingleParam(paramPart);
assert(validQueryPart.test(k), `param key names must match ${validQueryPart}`);
params[k] = v;
}
if (paramsHasWildcard) {
return new TestQueryMultiCase(suite, file, test, params);
} else {
return new TestQuerySingleCase(suite, file, test, params);
}
}
// webgpu:a,b,* or webgpu:a,b,c:*
const kExampleQueries = `\
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}${kWildcard} or \
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}c${kBigSeparator}${kWildcard}`;
function parseBigPart(
s: string,
separator: typeof kParamSeparator | typeof kPathSeparator
): { parts: string[]; wildcard: boolean } {
if (s === '') {
return { parts: [], wildcard: false };
}
const parts = s.split(separator);
let endsWithWildcard = false;
for (const [i, part] of parts.entries()) {
if (i === parts.length - 1) {
endsWithWildcard = part === kWildcard;
}
assert(
part.indexOf(kWildcard) === -1 || endsWithWildcard,
`Wildcard ${kWildcard} must be complete last part of a path (e.g. ${kExampleQueries})`
);
}
if (endsWithWildcard) {
// Remove the last element of the array (which is just the wildcard).
parts.length = parts.length - 1;
}
return { parts, wildcard: endsWithWildcard };
}
function parseSingleParam(paramSubstring: string): [string, JSONWithUndefined] {
assert(paramSubstring !== '', 'Param in a query must not be blank (is there a trailing comma?)');
const i = paramSubstring.indexOf('=');
assert(i !== -1, 'Param in a query must be of form key=value');
const k = paramSubstring.substring(0, i);
assert(paramKeyIsPublic(k), 'Param in a query must not be private (start with _)');
const v = paramSubstring.substring(i + 1);
return [k, parseSingleParamValue(v)];
}
function parseSingleParamValue(s: string): JSONWithUndefined {
assert(
!badParamValueChars.test(s),
`param value must not match ${badParamValueChars} - was ${s}`
);
return parseParamValue(s);
}