Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 1 | // Copyright 2022 The Dawn & Tint Authors |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 2 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 3 | // Redistribution and use in source and binary forms, with or without |
| 4 | // modification, are permitted provided that the following conditions are met: |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 5 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 6 | // 1. Redistributions of source code must retain the above copyright notice, this |
| 7 | // list of conditions and the following disclaimer. |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 8 | // |
Austin Eng | cc2516a | 2023-10-17 20:57:54 +0000 | [diff] [blame] | 9 | // 2. Redistributions in binary form must reproduce the above copyright notice, |
| 10 | // this list of conditions and the following disclaimer in the documentation |
| 11 | // and/or other materials provided with the distribution. |
| 12 | // |
| 13 | // 3. Neither the name of the copyright holder nor the names of its |
| 14 | // contributors may be used to endorse or promote products derived from |
| 15 | // this software without specific prior written permission. |
| 16 | // |
| 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| 18 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 19 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| 20 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| 21 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| 22 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| 23 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 24 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| 25 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 26 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 27 | |
Austin Eng | b00c50e | 2022-08-26 22:34:27 +0000 | [diff] [blame] | 28 | import { globalTestConfig } from '../third_party/webgpu-cts/src/common/framework/test_config.js'; |
Austin Eng | 92b32e8 | 2022-11-21 15:16:51 +0000 | [diff] [blame] | 29 | import { dataCache } from '../third_party/webgpu-cts/src/common/framework/data_cache.js'; |
Ben Clayton | 2880e5d | 2023-11-16 17:16:57 +0000 | [diff] [blame] | 30 | import { getResourcePath } from '../third_party/webgpu-cts/src/common/framework/resources.js'; |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 31 | import { DefaultTestFileLoader } from '../third_party/webgpu-cts/src/common/internal/file_loader.js'; |
| 32 | import { prettyPrintLog } from '../third_party/webgpu-cts/src/common/internal/logging/log_message.js'; |
| 33 | import { Logger } from '../third_party/webgpu-cts/src/common/internal/logging/logger.js'; |
| 34 | import { parseQuery } from '../third_party/webgpu-cts/src/common/internal/query/parseQuery.js'; |
Gregg Tavares | e1d87cd | 2023-06-29 23:36:25 +0000 | [diff] [blame] | 35 | import { parseSearchParamLikeWithCTSOptions } from '../third_party/webgpu-cts/src/common/runtime/helper/options.js'; |
| 36 | import { setDefaultRequestAdapterOptions } from '../third_party/webgpu-cts/src/common/util/navigator_gpu.js'; |
Kai Ninomiya | ca296e1 | 2024-04-08 21:20:13 +0000 | [diff] [blame] | 37 | import { unreachable } from '../third_party/webgpu-cts/src/common/util/util.js'; |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 38 | |
Kai Ninomiya | ca296e1 | 2024-04-08 21:20:13 +0000 | [diff] [blame] | 39 | import { TestDedicatedWorker, TestSharedWorker, TestServiceWorker } from '../third_party/webgpu-cts/src/common/runtime/helper/test_worker.js'; |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 40 | |
Brian Sheedy | 17b1a45 | 2022-04-14 17:19:11 +0000 | [diff] [blame] | 41 | // The Python-side websockets library has a max payload size of 72638. Set the |
| 42 | // max allowable logs size in a single payload to a bit less than that. |
| 43 | const LOGS_MAX_BYTES = 72000; |
| 44 | |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 45 | var socket; |
| 46 | |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 47 | // Returns a wrapper around `fn` which gets called at most once every `intervalMs`. |
| 48 | // If the wrapper is called when `fn` was called too recently, `fn` is scheduled to |
| 49 | // be called later in the future after the interval passes. |
| 50 | // Returns [ wrappedFn, {start, stop}] where wrappedFn is the rate-limited function, |
| 51 | // and start/stop control whether or not the function is enabled. If it is stopped, calls |
| 52 | // to the fn will no-op. If it is started, calls will be rate-limited, starting from |
| 53 | // the time `start` is called. |
| 54 | function rateLimited(fn, intervalMs) { |
| 55 | let last = undefined; |
| 56 | let timer = undefined; |
| 57 | const wrappedFn = (...args) => { |
Austin Eng | 26ffcd1 | 2022-09-13 18:28:31 +0000 | [diff] [blame] | 58 | if (last === undefined) { |
| 59 | // If the function is not enabled, return. |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 60 | return; |
| 61 | } |
| 62 | // Get the current time as a number. |
| 63 | const now = +new Date(); |
| 64 | const diff = now - last; |
| 65 | if (diff >= intervalMs) { |
| 66 | // Clear the timer, if there was one. This could happen if a timer |
| 67 | // is scheduled, but it never runs due to long-running synchronous |
| 68 | // code. |
| 69 | if (timer) { |
| 70 | clearTimeout(timer); |
| 71 | timer = undefined; |
| 72 | } |
| 73 | |
| 74 | // Call the function. |
| 75 | last = now; |
| 76 | fn(...args); |
| 77 | } else if (timer === undefined) { |
| 78 | // Otherwise, we have called `fn` too recently. |
| 79 | // Schedule a future call. |
| 80 | timer = setTimeout(() => { |
| 81 | // Clear the timer to indicate nothing is scheduled. |
| 82 | timer = undefined; |
| 83 | last = +new Date(); |
| 84 | fn(...args); |
| 85 | }, intervalMs - diff + 1); |
| 86 | } |
| 87 | }; |
| 88 | return [ |
| 89 | wrappedFn, |
| 90 | { |
| 91 | start: () => { |
| 92 | last = +new Date(); |
| 93 | }, |
| 94 | stop: () => { |
| 95 | last = undefined; |
| 96 | if (timer) { |
| 97 | clearTimeout(timer); |
| 98 | timer = undefined; |
| 99 | } |
| 100 | }, |
| 101 | } |
| 102 | ]; |
| 103 | } |
| 104 | |
Brian Sheedy | 17b1a45 | 2022-04-14 17:19:11 +0000 | [diff] [blame] | 105 | function byteSize(s) { |
| 106 | return new Blob([s]).size; |
| 107 | } |
| 108 | |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 109 | async function setupWebsocket(port) { |
| 110 | socket = new WebSocket('ws://127.0.0.1:' + port) |
Austin Eng | 2046163 | 2023-05-03 23:36:12 +0000 | [diff] [blame] | 111 | socket.addEventListener('open', () => { |
| 112 | socket.send('{"type":"CONNECTION_ACK"}'); |
| 113 | }); |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 114 | socket.addEventListener('message', runCtsTestViaSocket); |
| 115 | } |
| 116 | |
| 117 | async function runCtsTestViaSocket(event) { |
| 118 | let input = JSON.parse(event.data); |
Kai Ninomiya | ca296e1 | 2024-04-08 21:20:13 +0000 | [diff] [blame] | 119 | runCtsTest(input['q']); |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 120 | } |
| 121 | |
Austin Eng | 92b32e8 | 2022-11-21 15:16:51 +0000 | [diff] [blame] | 122 | dataCache.setStore({ |
| 123 | load: async (path) => { |
Ben Clayton | 2880e5d | 2023-11-16 17:16:57 +0000 | [diff] [blame] | 124 | const fullPath = getResourcePath(`cache/${path}`); |
| 125 | const response = await fetch(fullPath); |
| 126 | if (!response.ok) { |
Ben Clayton | 248bad4 | 2023-11-20 22:40:34 +0000 | [diff] [blame] | 127 | return Promise.reject(`failed to load cache file: '${fullPath}' |
| 128 | reason: ${response.statusText}`); |
Ben Clayton | b8b17ce | 2023-10-25 23:00:07 +0000 | [diff] [blame] | 129 | } |
Ben Clayton | 2880e5d | 2023-11-16 17:16:57 +0000 | [diff] [blame] | 130 | return new Uint8Array(await response.arrayBuffer()); |
Austin Eng | 92b32e8 | 2022-11-21 15:16:51 +0000 | [diff] [blame] | 131 | } |
| 132 | }); |
| 133 | |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 134 | // Make a rate-limited version `sendMessageTestHeartbeat` that executes |
| 135 | // at most once every 500 ms. |
| 136 | const [sendHeartbeat, { |
| 137 | start: beginHeartbeatScope, |
| 138 | stop: endHeartbeatScope |
| 139 | }] = rateLimited(sendMessageTestHeartbeat, 500); |
| 140 | |
| 141 | function wrapPromiseWithHeartbeat(prototype, key) { |
| 142 | const old = prototype[key]; |
| 143 | prototype[key] = function (...args) { |
| 144 | return new Promise((resolve, reject) => { |
| 145 | // Send the heartbeat both before and after resolve/reject |
| 146 | // so that the heartbeat is sent ahead of any potentially |
| 147 | // long-running synchronous code awaiting the Promise. |
| 148 | old.call(this, ...args) |
| 149 | .then(val => { sendHeartbeat(); resolve(val) }) |
| 150 | .catch(err => { sendHeartbeat(); reject(err) }) |
| 151 | .finally(sendHeartbeat); |
| 152 | }); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | wrapPromiseWithHeartbeat(GPU.prototype, 'requestAdapter'); |
| 157 | wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestAdapterInfo'); |
| 158 | wrapPromiseWithHeartbeat(GPUAdapter.prototype, 'requestDevice'); |
| 159 | wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createRenderPipelineAsync'); |
| 160 | wrapPromiseWithHeartbeat(GPUDevice.prototype, 'createComputePipelineAsync'); |
| 161 | wrapPromiseWithHeartbeat(GPUDevice.prototype, 'popErrorScope'); |
| 162 | wrapPromiseWithHeartbeat(GPUQueue.prototype, 'onSubmittedWorkDone'); |
| 163 | wrapPromiseWithHeartbeat(GPUBuffer.prototype, 'mapAsync'); |
James Price | 55509fa | 2023-03-10 11:06:36 +0000 | [diff] [blame] | 164 | wrapPromiseWithHeartbeat(GPUShaderModule.prototype, 'getCompilationInfo'); |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 165 | |
Austin Eng | b00c50e | 2022-08-26 22:34:27 +0000 | [diff] [blame] | 166 | globalTestConfig.testHeartbeatCallback = sendHeartbeat; |
Austin Eng | e0cbb0c | 2022-09-13 22:16:42 +0000 | [diff] [blame] | 167 | globalTestConfig.noRaceWithRejectOnTimeout = true; |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 168 | |
Ben Clayton | 33bfc988 | 2023-01-05 21:44:37 +0000 | [diff] [blame] | 169 | // FXC is very slow to compile unrolled const-eval loops, where the metal shader |
| 170 | // compiler (Intel GPU) is very slow to compile rolled loops. Intel drivers for |
| 171 | // linux may also suffer the same performance issues, so unroll const-eval loops |
| 172 | // if we're not running on Windows. |
Antonio Maiorano | 8a15bb1 | 2023-09-05 21:47:13 +0000 | [diff] [blame] | 173 | const isWindows = navigator.userAgent.includes("Windows"); |
| 174 | if (!isWindows) { |
Ben Clayton | 33bfc988 | 2023-01-05 21:44:37 +0000 | [diff] [blame] | 175 | globalTestConfig.unrollConstEvalLoops = true; |
| 176 | } |
| 177 | |
Kai Ninomiya | 9392c04 | 2024-04-09 00:06:52 +0000 | [diff] [blame] | 178 | let lastOptionsKey, testWorker; |
| 179 | |
Kai Ninomiya | ca296e1 | 2024-04-08 21:20:13 +0000 | [diff] [blame] | 180 | async function runCtsTest(queryString) { |
Gregg Tavares | e1d87cd | 2023-06-29 23:36:25 +0000 | [diff] [blame] | 181 | const { queries, options } = parseSearchParamLikeWithCTSOptions(queryString); |
Kai Ninomiya | 9392c04 | 2024-04-09 00:06:52 +0000 | [diff] [blame] | 182 | |
| 183 | // Set up a worker with the options passed into the test, avoiding creating |
| 184 | // a new worker if one was already set up for the last test. |
| 185 | // In practice, the options probably won't change between tests in a single |
| 186 | // invocation of run_gpu_integration_test.py, but this handles if they do. |
| 187 | const currentOptionsKey = JSON.stringify(options); |
| 188 | if (currentOptionsKey !== lastOptionsKey) { |
| 189 | lastOptionsKey = currentOptionsKey; |
| 190 | testWorker = |
| 191 | options.worker === null ? null : |
| 192 | options.worker === 'dedicated' ? new TestDedicatedWorker(options) : |
| 193 | options.worker === 'shared' ? new TestSharedWorker(options) : |
| 194 | options.worker === 'service' ? new TestServiceWorker(options) : |
| 195 | unreachable(); |
| 196 | } |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 197 | |
| 198 | const loader = new DefaultTestFileLoader(); |
Gregg Tavares | e1d87cd | 2023-06-29 23:36:25 +0000 | [diff] [blame] | 199 | const filterQuery = parseQuery(queries[0]); |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 200 | const testcases = Array.from(await loader.loadCases(filterQuery)); |
| 201 | |
| 202 | if (testcases.length === 0) { |
| 203 | sendMessageInfraFailure('Did not find matching test'); |
| 204 | return; |
| 205 | } |
| 206 | if (testcases.length !== 1) { |
| 207 | sendMessageInfraFailure('Found more than 1 test for given query'); |
| 208 | return; |
| 209 | } |
| 210 | const testcase = testcases[0]; |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 211 | |
Gregg Tavares | e1d87cd | 2023-06-29 23:36:25 +0000 | [diff] [blame] | 212 | const { compatibility, powerPreference } = options; |
| 213 | globalTestConfig.compatibility = compatibility; |
| 214 | if (powerPreference || compatibility) { |
| 215 | setDefaultRequestAdapterOptions({ |
| 216 | ...(powerPreference && { powerPreference }), |
| 217 | // MAINTENANCE_TODO(gman): Change this to whatever the option ends up being |
| 218 | ...(compatibility && { compatibilityMode: true }), |
| 219 | }); |
| 220 | } |
| 221 | |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 222 | const expectations = []; |
| 223 | |
| 224 | const log = new Logger(); |
| 225 | |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 226 | const name = testcase.query.toString(); |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 227 | |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 228 | const wpt_fn = async () => { |
| 229 | sendMessageTestStarted(); |
| 230 | const [rec, res] = log.record(name); |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 231 | |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 232 | beginHeartbeatScope(); |
Kai Ninomiya | ca296e1 | 2024-04-08 21:20:13 +0000 | [diff] [blame] | 233 | if (testWorker) { |
| 234 | await testWorker.run(rec, name, expectations); |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 235 | } else { |
| 236 | await testcase.run(rec, expectations); |
| 237 | } |
| 238 | endHeartbeatScope(); |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 239 | |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 240 | sendMessageTestStatus(res.status, res.timems); |
Austin Eng | 3c0186a | 2024-06-19 04:26:46 +0000 | [diff] [blame] | 241 | if (res.status === 'pass') { |
| 242 | // Send an "OK" log. Passing tests don't report logs to Telemetry. |
| 243 | sendMessageTestLogOK(); |
| 244 | } else { |
Kai Ninomiya | df5c89a | 2024-06-29 01:14:29 +0000 | [diff] [blame] | 245 | sendMessageTestLog(res.logs ?? []); |
Austin Eng | 3c0186a | 2024-06-19 04:26:46 +0000 | [diff] [blame] | 246 | } |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 247 | sendMessageTestFinished(); |
| 248 | }; |
| 249 | await wpt_fn(); |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 250 | } |
| 251 | |
Brian Sheedy | 0653501 | 2022-08-11 14:39:51 +0000 | [diff] [blame] | 252 | function splitLogsForPayload(fullLogs) { |
| 253 | let logPieces = [fullLogs] |
| 254 | // Split the log pieces until they all are guaranteed to fit into a |
| 255 | // websocket payload. |
| 256 | while (true) { |
| 257 | let tempLogPieces = [] |
| 258 | for (const piece of logPieces) { |
| 259 | if (byteSize(piece) > LOGS_MAX_BYTES) { |
| 260 | let midpoint = Math.floor(piece.length / 2); |
| 261 | tempLogPieces.push(piece.substring(0, midpoint)); |
| 262 | tempLogPieces.push(piece.substring(midpoint)); |
| 263 | } else { |
| 264 | tempLogPieces.push(piece) |
| 265 | } |
| 266 | } |
| 267 | // Didn't make any changes - all pieces are under the size limit. |
| 268 | if (logPieces.every((value, index) => value == tempLogPieces[index])) { |
| 269 | break; |
| 270 | } |
| 271 | logPieces = tempLogPieces; |
| 272 | } |
| 273 | return logPieces |
| 274 | } |
| 275 | |
| 276 | function sendMessageTestStarted() { |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 277 | socket.send('{"type":"TEST_STARTED"}'); |
| 278 | } |
| 279 | |
| 280 | function sendMessageTestHeartbeat() { |
| 281 | socket.send('{"type":"TEST_HEARTBEAT"}'); |
Brian Sheedy | 0653501 | 2022-08-11 14:39:51 +0000 | [diff] [blame] | 282 | } |
| 283 | |
| 284 | function sendMessageTestStatus(status, jsDurationMs) { |
Ben Clayton | 33bfc988 | 2023-01-05 21:44:37 +0000 | [diff] [blame] | 285 | socket.send(JSON.stringify({ |
| 286 | 'type': 'TEST_STATUS', |
| 287 | 'status': status, |
| 288 | 'js_duration_ms': jsDurationMs |
| 289 | })); |
Brian Sheedy | 0653501 | 2022-08-11 14:39:51 +0000 | [diff] [blame] | 290 | } |
| 291 | |
Austin Eng | 3c0186a | 2024-06-19 04:26:46 +0000 | [diff] [blame] | 292 | function sendMessageTestLogOK() { |
| 293 | socket.send('{"type":"TEST_LOG","log":"OK"}'); |
| 294 | } |
| 295 | |
Kai Ninomiya | df5c89a | 2024-06-29 01:14:29 +0000 | [diff] [blame] | 296 | // Logs look like: " - EXPECTATION FAILED: subcase: foobar=2;foo=a;bar=2\n...." |
| 297 | // or "EXCEPTION: Name!: Message!\nsubcase: fail = true\n..." |
| 298 | const kSubcaseLogPrefixRegex = /\bsubcase: .*$\n/m; |
| 299 | |
| 300 | /** Send logs with the most important log line on the first line as a "summary". */ |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 301 | function sendMessageTestLog(logs) { |
Kai Ninomiya | df5c89a | 2024-06-29 01:14:29 +0000 | [diff] [blame] | 302 | // Find the first log that is max-severity (doesn't have its stack hidden) |
| 303 | let summary = ''; |
| 304 | let details = []; |
| 305 | for (const log of logs) { |
| 306 | let detail = prettyPrintLog(log); |
| 307 | if (summary === '' && log.stackHiddenMessage === undefined) { |
| 308 | // First top-severity message (because its stack is not hidden). |
| 309 | // Clean up this message and pick it as the summary: |
| 310 | summary = ' ' + log.toJSON() |
| 311 | .replace(kSubcaseLogPrefixRegex, '') |
| 312 | .split('\n')[0] + '\n'; |
| 313 | // Replace ' - ' with '--> ': |
| 314 | detail = '--> ' + detail.slice(4); |
| 315 | } |
| 316 | details.push(detail); |
| 317 | } |
| 318 | |
| 319 | const outLogsString = summary + details.join('\n'); |
| 320 | splitLogsForPayload(outLogsString) |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 321 | .forEach((piece) => { |
| 322 | socket.send(JSON.stringify({ |
| 323 | 'type': 'TEST_LOG', |
| 324 | 'log': piece |
| 325 | })); |
| 326 | }); |
Brian Sheedy | 0653501 | 2022-08-11 14:39:51 +0000 | [diff] [blame] | 327 | } |
| 328 | |
| 329 | function sendMessageTestFinished() { |
Austin Eng | caa9bae | 2022-08-18 22:49:10 +0000 | [diff] [blame] | 330 | socket.send('{"type":"TEST_FINISHED"}'); |
Brian Sheedy | 0653501 | 2022-08-11 14:39:51 +0000 | [diff] [blame] | 331 | } |
| 332 | |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 333 | function sendMessageInfraFailure(message) { |
| 334 | socket.send(JSON.stringify({ |
Ben Clayton | b8b17ce | 2023-10-25 23:00:07 +0000 | [diff] [blame] | 335 | 'type': 'INFRA_FAILURE', |
| 336 | 'message': message, |
Brian Sheedy | bbcdd3e | 2023-09-29 23:00:21 +0000 | [diff] [blame] | 337 | })); |
| 338 | } |
| 339 | |
Austin Eng | 1cdea90 | 2022-03-24 00:21:55 +0000 | [diff] [blame] | 340 | window.setupWebsocket = setupWebsocket |