options.ts (6922B)
1 import { unreachable } from '../../util/util.js'; 2 3 let windowURL: URL | undefined = undefined; 4 function getWindowURL() { 5 if (windowURL === undefined) { 6 windowURL = new URL(window.location.toString()); 7 } 8 return windowURL; 9 } 10 11 /** Parse a runner option that is always boolean-typed. False if missing or '0'. */ 12 export function optionEnabled( 13 opt: string, 14 searchParams: URLSearchParams = getWindowURL().searchParams 15 ): boolean { 16 const val = searchParams.get(opt); 17 return val !== null && val !== '0'; 18 } 19 20 /** Parse a runner option that is string-typed. If the option is missing, returns `null`. */ 21 export function optionString( 22 opt: string, 23 searchParams: URLSearchParams = getWindowURL().searchParams 24 ): string | null { 25 return searchParams.get(opt); 26 } 27 28 /** Runtime modes for running tests in different types of workers. */ 29 export type WorkerMode = 'dedicated' | 'service' | 'shared'; 30 /** Parse a runner option for different worker modes (as in `?worker=shared`). Null if no worker. */ 31 export function optionWorkerMode( 32 opt: string, 33 searchParams: URLSearchParams = getWindowURL().searchParams 34 ): WorkerMode | null { 35 const value = searchParams.get(opt); 36 if (value === null || value === '0') { 37 return null; 38 } else if (value === 'service') { 39 return 'service'; 40 } else if (value === 'shared') { 41 return 'shared'; 42 } else if (value === '' || value === '1' || value === 'dedicated') { 43 return 'dedicated'; 44 } 45 unreachable('invalid worker= option value'); 46 } 47 48 /** 49 * The possible options for the tests. 50 */ 51 export interface CTSOptions { 52 worker: WorkerMode | null; 53 debug: boolean; 54 compatibility: boolean; 55 forceFallbackAdapter: boolean; 56 enforceDefaultLimits: boolean; 57 blockAllFeatures: boolean; 58 unrollConstEvalLoops: boolean; 59 powerPreference: GPUPowerPreference | null; 60 subcasesBetweenAttemptingGC: string; 61 casesBetweenReplacingDevice: string; 62 logToWebSocket: boolean; 63 } 64 65 export const kDefaultCTSOptions: Readonly<CTSOptions> = { 66 worker: null, 67 debug: false, 68 compatibility: false, 69 forceFallbackAdapter: false, 70 enforceDefaultLimits: false, 71 blockAllFeatures: false, 72 unrollConstEvalLoops: false, 73 powerPreference: null, 74 subcasesBetweenAttemptingGC: '5000', 75 casesBetweenReplacingDevice: 'Infinity', 76 logToWebSocket: false, 77 }; 78 79 /** 80 * Extra per option info. 81 */ 82 export interface OptionInfo { 83 description: string; 84 parser?: (key: string, searchParams?: URLSearchParams) => boolean | string | null; 85 selectValueDescriptions?: { value: string | null; description: string }[]; 86 } 87 88 /** 89 * Type for info for every option. This definition means adding an option 90 * will generate a compile time error if no extra info is provided. 91 */ 92 export type OptionsInfos<Type> = Record<keyof Type, OptionInfo>; 93 94 /** 95 * Options to the CTS. 96 */ 97 export const kCTSOptionsInfo: OptionsInfos<CTSOptions> = { 98 worker: { 99 description: 'run in a worker', 100 parser: optionWorkerMode, 101 selectValueDescriptions: [ 102 { value: null, description: 'no worker' }, 103 { value: 'dedicated', description: 'dedicated worker' }, 104 { value: 'shared', description: 'shared worker' }, 105 { value: 'service', description: 'service worker' }, 106 ], 107 }, 108 debug: { description: 'show more info' }, 109 compatibility: { description: 'request adapters with featureLevel: "compatibility"' }, 110 forceFallbackAdapter: { description: 'pass forceFallbackAdapter: true to requestAdapter' }, 111 enforceDefaultLimits: { 112 description: `force the adapter limits to the default limits. 113 Note: May fail on tests for low-power/high-performance`, 114 }, 115 blockAllFeatures: { 116 description: `block all features on adapter - except 'core-features-and-limits'. 117 Note: The spec requires bc or etc2+astc which means tests checking that one or other must exist will fail. 118 `, 119 }, 120 unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' }, 121 powerPreference: { 122 description: 'set default powerPreference for some tests', 123 parser: optionString, 124 selectValueDescriptions: [ 125 { value: null, description: 'default' }, 126 { value: 'low-power', description: 'low-power' }, 127 { value: 'high-performance', description: 'high-performance' }, 128 ], 129 }, 130 subcasesBetweenAttemptingGC: { 131 description: 132 'After this many subcases, run attemptGarbageCollection(). (For custom values, edit the URL.)', 133 parser: optionString, 134 selectValueDescriptions: [ 135 { value: null, description: 'default' }, 136 { value: 'Infinity', description: 'Infinity' }, 137 { value: '5000', description: '5000' }, 138 { value: '50', description: '50' }, 139 { value: '1', description: '1' }, 140 ], 141 }, 142 casesBetweenReplacingDevice: { 143 description: 144 'After this many cases use a device, destroy and replace it to free GPU resources. (For custom values, edit the URL.)', 145 parser: optionString, 146 selectValueDescriptions: [ 147 { value: null, description: 'default' }, 148 { value: 'Infinity', description: 'Infinity' }, 149 { value: '5000', description: '5000' }, 150 { value: '50', description: '50' }, 151 { value: '1', description: '1' }, 152 ], 153 }, 154 logToWebSocket: { description: 'send some logs to ws://localhost:59497/' }, 155 }; 156 157 /** 158 * Converts camel case to snake case. 159 * Examples: 160 * fooBar -> foo_bar 161 * parseHTMLFile -> parse_html_file 162 */ 163 export function camelCaseToSnakeCase(id: string) { 164 return id 165 .replace(/(.)([A-Z][a-z]+)/g, '$1_$2') 166 .replace(/([a-z0-9])([A-Z])/g, '$1_$2') 167 .toLowerCase(); 168 } 169 170 /** 171 * Creates a Options from search parameters. 172 */ 173 function getOptionsInfoFromSearchString<Type extends CTSOptions>( 174 optionsInfos: OptionsInfos<Type>, 175 searchString: string 176 ): Type { 177 const searchParams = new URLSearchParams(searchString); 178 const optionValues: Record<string, boolean | string | null> = {}; 179 for (const [optionName, info] of Object.entries(optionsInfos)) { 180 const parser = info.parser || optionEnabled; 181 optionValues[optionName] = parser(camelCaseToSnakeCase(optionName), searchParams); 182 } 183 return optionValues as unknown as Type; 184 } 185 186 /** 187 * Given a test query string in the form of `suite:foo,bar,moo&opt1=val1&opt2=val2 188 * returns the query and the options. 189 */ 190 export function parseSearchParamLikeWithOptions<Type extends CTSOptions>( 191 optionsInfos: OptionsInfos<Type>, 192 query: string 193 ): { 194 queries: string[]; 195 options: Type; 196 } { 197 const searchString = query.includes('q=') || query.startsWith('?') ? query : `q=${query}`; 198 const queries = new URLSearchParams(searchString).getAll('q'); 199 const options = getOptionsInfoFromSearchString(optionsInfos, searchString); 200 return { queries, options }; 201 } 202 203 /** 204 * Given a test query string in the form of `suite:foo,bar,moo&opt1=val1&opt2=val2 205 * returns the query and the common options. 206 */ 207 export function parseSearchParamLikeWithCTSOptions(query: string) { 208 return parseSearchParamLikeWithOptions(kCTSOptionsInfo, query); 209 }