query.ts (8128B)
1 import { TestParams } from '../../framework/fixture.js'; 2 import { optionWorkerMode } from '../../runtime/helper/options.js'; 3 import { assert, unreachable } from '../../util/util.js'; 4 import { Expectation } from '../logging/result.js'; 5 6 import { compareQueries, Ordering } from './compare.js'; 7 import { encodeURIComponentSelectively } from './encode_selectively.js'; 8 import { parseQuery } from './parseQuery.js'; 9 import { kBigSeparator, kPathSeparator, kWildcard } from './separators.js'; 10 import { stringifyPublicParams } from './stringify_params.js'; 11 12 /** 13 * Represents a test query of some level. 14 * 15 * TestQuery types are immutable. 16 */ 17 export type TestQuery = 18 | TestQuerySingleCase 19 | TestQueryMultiCase 20 | TestQueryMultiTest 21 | TestQueryMultiFile; 22 23 /** 24 * - 1 = MultiFile. 25 * - 2 = MultiTest. 26 * - 3 = MultiCase. 27 * - 4 = SingleCase. 28 */ 29 export type TestQueryLevel = 1 | 2 | 3 | 4; 30 31 export interface TestQueryWithExpectation { 32 query: TestQuery; 33 expectation: Expectation; 34 } 35 36 /** 37 * A multi-file test query, like `s:*` or `s:a,b,*`. 38 * 39 * Immutable (makes copies of constructor args). 40 */ 41 export class TestQueryMultiFile { 42 readonly level: TestQueryLevel = 1; 43 readonly isMultiFile: boolean = true; 44 readonly suite: string; 45 readonly filePathParts: readonly string[]; 46 47 constructor(suite: string, file: readonly string[]) { 48 this.suite = suite; 49 this.filePathParts = [...file]; 50 } 51 52 get depthInLevel() { 53 return this.filePathParts.length; 54 } 55 56 toString(): string { 57 return encodeURIComponentSelectively(this.toStringHelper().join(kBigSeparator)); 58 } 59 60 protected toStringHelper(): string[] { 61 return [this.suite, [...this.filePathParts, kWildcard].join(kPathSeparator)]; 62 } 63 } 64 65 /** 66 * A multi-test test query, like `s:f:*` or `s:f:a,b,*`. 67 * 68 * Immutable (makes copies of constructor args). 69 */ 70 export class TestQueryMultiTest extends TestQueryMultiFile { 71 override readonly level: TestQueryLevel = 2; 72 override readonly isMultiFile = false as const; 73 readonly isMultiTest: boolean = true; 74 readonly testPathParts: readonly string[]; 75 76 constructor(suite: string, file: readonly string[], test: readonly string[]) { 77 super(suite, file); 78 assert(file.length > 0, 'multi-test (or finer) query must have file-path'); 79 this.testPathParts = [...test]; 80 } 81 82 override get depthInLevel() { 83 return this.testPathParts.length; 84 } 85 86 protected override toStringHelper(): string[] { 87 return [ 88 this.suite, 89 this.filePathParts.join(kPathSeparator), 90 [...this.testPathParts, kWildcard].join(kPathSeparator), 91 ]; 92 } 93 } 94 95 /** 96 * A multi-case test query, like `s:f:t:*` or `s:f:t:a,b,*`. 97 * 98 * Immutable (makes copies of constructor args), except for param values 99 * (which aren't normally supposed to change; they're marked readonly in TestParams). 100 */ 101 export class TestQueryMultiCase extends TestQueryMultiTest { 102 override readonly level: TestQueryLevel = 3; 103 override readonly isMultiTest = false as const; 104 readonly isMultiCase: boolean = true; 105 readonly params: TestParams; 106 107 constructor(suite: string, file: readonly string[], test: readonly string[], params: TestParams) { 108 super(suite, file, test); 109 assert(test.length > 0, 'multi-case (or finer) query must have test-path'); 110 this.params = { ...params }; 111 } 112 113 override get depthInLevel() { 114 return Object.keys(this.params).length; 115 } 116 117 protected override toStringHelper(): string[] { 118 return [ 119 this.suite, 120 this.filePathParts.join(kPathSeparator), 121 this.testPathParts.join(kPathSeparator), 122 stringifyPublicParams(this.params, true), 123 ]; 124 } 125 } 126 127 /** 128 * A multi-case test query, like `s:f:t:` or `s:f:t:a=1,b=1`. 129 * 130 * Immutable (makes copies of constructor args). 131 */ 132 export class TestQuerySingleCase extends TestQueryMultiCase { 133 override readonly level: TestQueryLevel = 4; 134 override readonly isMultiCase = false as const; 135 136 override get depthInLevel() { 137 return 0; 138 } 139 140 protected override toStringHelper(): string[] { 141 return [ 142 this.suite, 143 this.filePathParts.join(kPathSeparator), 144 this.testPathParts.join(kPathSeparator), 145 stringifyPublicParams(this.params), 146 ]; 147 } 148 } 149 150 /** 151 * Parse raw expectations input into TestQueryWithExpectation[], filtering so that only 152 * expectations that are relevant for the provided query and wptURL. 153 * 154 * `rawExpectations` should be @type {{ query: string, expectation: Expectation }[]} 155 * 156 * The `rawExpectations` are parsed and validated that they are in the correct format. 157 * If `wptURL` is passed, the query string should be of the full path format such 158 * as `path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;*`. 159 * If `wptURL` is `undefined`, the query string should be only the query 160 * `suite:test_path:test_name:foo=1;bar=2;*`. 161 */ 162 export function parseExpectationsForTestQuery( 163 rawExpectations: 164 | unknown 165 | { 166 query: string; 167 expectation: Expectation; 168 }[], 169 query: TestQuery, 170 wptURL?: URL 171 ) { 172 if (!Array.isArray(rawExpectations)) { 173 unreachable('Expectations should be an array'); 174 } 175 const expectations: TestQueryWithExpectation[] = []; 176 for (const entry of rawExpectations) { 177 assert(typeof entry === 'object'); 178 const rawExpectation = entry as { query?: string; expectation?: string }; 179 assert(rawExpectation.query !== undefined, 'Expectation missing query string'); 180 assert(rawExpectation.expectation !== undefined, 'Expectation missing expectation string'); 181 182 let expectationQuery: TestQuery; 183 if (wptURL !== undefined) { 184 const expectationURL = new URL(`${wptURL.origin}/${entry.query}`); 185 if (expectationURL.pathname !== wptURL.pathname) { 186 continue; 187 } 188 assert( 189 expectationURL.pathname === wptURL.pathname, 190 `Invalid expectation path ${expectationURL.pathname} 191 Expectation should be of the form path/to/cts.https.html?debug=0&q=suite:test_path:test_name:foo=1;bar=2;... 192 ` 193 ); 194 195 const params = expectationURL.searchParams; 196 if (optionWorkerMode('worker', params) !== optionWorkerMode('worker', wptURL.searchParams)) { 197 continue; 198 } 199 200 const qs = params.getAll('q'); 201 assert(qs.length === 1, 'currently, there must be exactly one ?q= in the expectation string'); 202 expectationQuery = parseQuery(qs[0]); 203 } else { 204 expectationQuery = parseQuery(entry.query); 205 } 206 207 // Strip params from multicase expectations so that an expectation of foo=2;* 208 // is stored if the test query is bar=3;* 209 const queryForFilter = 210 expectationQuery instanceof TestQueryMultiCase 211 ? new TestQueryMultiCase( 212 expectationQuery.suite, 213 expectationQuery.filePathParts, 214 expectationQuery.testPathParts, 215 {} 216 ) 217 : expectationQuery; 218 219 if (compareQueries(query, queryForFilter) === Ordering.Unordered) { 220 continue; 221 } 222 223 switch (entry.expectation) { 224 case 'pass': 225 case 'skip': 226 case 'fail': 227 break; 228 default: 229 unreachable(`Invalid expectation ${entry.expectation}`); 230 } 231 232 expectations.push({ 233 query: expectationQuery, 234 expectation: entry.expectation, 235 }); 236 } 237 return expectations; 238 } 239 240 /** 241 * For display purposes only, produces a "relative" query string from parent to child. 242 * Used in the wpt runtime to reduce the verbosity of logs. 243 */ 244 export function relativeQueryString(parent: TestQuery, child: TestQuery): string { 245 const ordering = compareQueries(parent, child); 246 if (ordering === Ordering.Equal) { 247 return ''; 248 } else if (ordering === Ordering.StrictSuperset) { 249 const parentString = parent.toString(); 250 assert(parentString.endsWith(kWildcard)); 251 const childString = child.toString(); 252 assert( 253 childString.startsWith(parentString.substring(0, parentString.length - 2)), 254 'impossible?: childString does not start with parentString[:-2]' 255 ); 256 return childString.substring(parentString.length - 2); 257 } else { 258 unreachable( 259 `relativeQueryString arguments have invalid ordering ${ordering}:\n${parent}\n${child}` 260 ); 261 } 262 }