Source code
Revision control
Copy as Markdown
Other Tools
import { TestParams } from '../../framework/fixture.js';
import { optionWorkerMode } from '../../runtime/helper/options.js';
import { assert, unreachable } from '../../util/util.js';
import { Expectation } from '../logging/result.js';
import { compareQueries, Ordering } from './compare.js';
import { encodeURIComponentSelectively } from './encode_selectively.js';
import { parseQuery } from './parseQuery.js';
import { kBigSeparator, kPathSeparator, kWildcard } from './separators.js';
import { stringifyPublicParams } from './stringify_params.js';
/**
* Represents a test query of some level.
*
* TestQuery types are immutable.
*/
export type TestQuery =
| TestQuerySingleCase
| TestQueryMultiCase
| TestQueryMultiTest
| TestQueryMultiFile;
/**
* - 1 = MultiFile.
* - 2 = MultiTest.
* - 3 = MultiCase.
* - 4 = SingleCase.
*/
export type TestQueryLevel = 1 | 2 | 3 | 4;
export interface TestQueryWithExpectation {
query: TestQuery;
expectation: Expectation;
}
/**
* A multi-file test query, like `s:*` or `s:a,b,*`.
*
* Immutable (makes copies of constructor args).
*/
export class TestQueryMultiFile {
readonly level: TestQueryLevel = 1;
readonly isMultiFile: boolean = true;
readonly suite: string;
readonly filePathParts: readonly string[];
constructor(suite: string, file: readonly string[]) {
this.suite = suite;
this.filePathParts = [...file];
}
get depthInLevel() {
return this.filePathParts.length;
}
toString(): string {
return encodeURIComponentSelectively(this.toStringHelper().join(kBigSeparator));
}
protected toStringHelper(): string[] {
return [this.suite, [...this.filePathParts, kWildcard].join(kPathSeparator)];
}
}
/**
* A multi-test test query, like `s:f:*` or `s:f:a,b,*`.
*
* Immutable (makes copies of constructor args).
*/
export class TestQueryMultiTest extends TestQueryMultiFile {
override readonly level: TestQueryLevel = 2;
override readonly isMultiFile = false as const;
readonly isMultiTest: boolean = true;
readonly testPathParts: readonly string[];
constructor(suite: string, file: readonly string[], test: readonly string[]) {
super(suite, file);
assert(file.length > 0, 'multi-test (or finer) query must have file-path');
this.testPathParts = [...test];
}
override get depthInLevel() {
return this.testPathParts.length;
}
protected override toStringHelper(): string[] {
return [
this.suite,
this.filePathParts.join(kPathSeparator),
[...this.testPathParts, kWildcard].join(kPathSeparator),
];
}
}
/**
* A multi-case test query, like `s:f:t:*` or `s:f:t:a,b,*`.
*
* Immutable (makes copies of constructor args), except for param values
* (which aren't normally supposed to change; they're marked readonly in TestParams).
*/
export class TestQueryMultiCase extends TestQueryMultiTest {
override readonly level: TestQueryLevel = 3;
override readonly isMultiTest = false as const;
readonly isMultiCase: boolean = true;
readonly params: TestParams;
constructor(suite: string, file: readonly string[], test: readonly string[], params: TestParams) {
super(suite, file, test);
assert(test.length > 0, 'multi-case (or finer) query must have test-path');
this.params = { ...params };
}
override get depthInLevel() {
return Object.keys(this.params).length;
}
protected override toStringHelper(): string[] {
return [
this.suite,
this.filePathParts.join(kPathSeparator),
this.testPathParts.join(kPathSeparator),
stringifyPublicParams(this.params, true),
];
}
}
/**
* A multi-case test query, like `s:f:t:` or `s:f:t:a=1,b=1`.
*
* Immutable (makes copies of constructor args).
*/
export class TestQuerySingleCase extends TestQueryMultiCase {
override readonly level: TestQueryLevel = 4;
override readonly isMultiCase = false as const;
override get depthInLevel() {
return 0;
}
protected override toStringHelper(): string[] {
return [
this.suite,
this.filePathParts.join(kPathSeparator),
this.testPathParts.join(kPathSeparator),
stringifyPublicParams(this.params),
];
}
}
/**
* Parse raw expectations input into TestQueryWithExpectation[], filtering so that only
* expectations that are relevant for the provided query and wptURL.
*
* `rawExpectations` should be @type {{ query: string, expectation: Expectation }[]}
*
* The `rawExpectations` are parsed and validated that they are in the correct format.
* If `wptURL` is passed, the query string should be of the full path format such
* as `path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;*`.
* If `wptURL` is `undefined`, the query string should be only the query
* `suite:test_path:test_name:foo=1;bar=2;*`.
*/
export function parseExpectationsForTestQuery(
rawExpectations:
| unknown
| {
query: string;
expectation: Expectation;
}[],
query: TestQuery,
wptURL?: URL
) {
if (!Array.isArray(rawExpectations)) {
unreachable('Expectations should be an array');
}
const expectations: TestQueryWithExpectation[] = [];
for (const entry of rawExpectations) {
assert(typeof entry === 'object');
const rawExpectation = entry as { query?: string; expectation?: string };
assert(rawExpectation.query !== undefined, 'Expectation missing query string');
assert(rawExpectation.expectation !== undefined, 'Expectation missing expectation string');
let expectationQuery: TestQuery;
if (wptURL !== undefined) {
const expectationURL = new URL(`${wptURL.origin}/${entry.query}`);
if (expectationURL.pathname !== wptURL.pathname) {
continue;
}
assert(
expectationURL.pathname === wptURL.pathname,
`Invalid expectation path ${expectationURL.pathname}
Expectation should be of the form path/to/cts.https.html?debug=0&q=suite:test_path:test_name:foo=1;bar=2;...
`
);
const params = expectationURL.searchParams;
if (optionWorkerMode('worker', params) !== optionWorkerMode('worker', wptURL.searchParams)) {
continue;
}
const qs = params.getAll('q');
assert(qs.length === 1, 'currently, there must be exactly one ?q= in the expectation string');
expectationQuery = parseQuery(qs[0]);
} else {
expectationQuery = parseQuery(entry.query);
}
// Strip params from multicase expectations so that an expectation of foo=2;*
// is stored if the test query is bar=3;*
const queryForFilter =
expectationQuery instanceof TestQueryMultiCase
? new TestQueryMultiCase(
expectationQuery.suite,
expectationQuery.filePathParts,
expectationQuery.testPathParts,
{}
)
: expectationQuery;
if (compareQueries(query, queryForFilter) === Ordering.Unordered) {
continue;
}
switch (entry.expectation) {
case 'pass':
case 'skip':
case 'fail':
break;
default:
unreachable(`Invalid expectation ${entry.expectation}`);
}
expectations.push({
query: expectationQuery,
expectation: entry.expectation,
});
}
return expectations;
}
/**
* For display purposes only, produces a "relative" query string from parent to child.
* Used in the wpt runtime to reduce the verbosity of logs.
*/
export function relativeQueryString(parent: TestQuery, child: TestQuery): string {
const ordering = compareQueries(parent, child);
if (ordering === Ordering.Equal) {
return '';
} else if (ordering === Ordering.StrictSuperset) {
const parentString = parent.toString();
assert(parentString.endsWith(kWildcard));
const childString = child.toString();
assert(
childString.startsWith(parentString.substring(0, parentString.length - 2)),
'impossible?: childString does not start with parentString[:-2]'
);
return childString.substring(parentString.length - 2);
} else {
unreachable(
`relativeQueryString arguments have invalid ordering ${ordering}:\n${parent}\n${child}`
);
}
}