@mertushka/webrtc-node 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1193 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawnSync } = require("node:child_process");
5
+ const vm = require("node:vm");
6
+ const webrtc = require("..");
7
+ const { ensureWpt } = require("./ensure-wpt");
8
+
9
+ const root = path.resolve(__dirname, "..");
10
+ const wptDir = path.resolve(process.env.WPT_DIR || path.join(root, "wpt"));
11
+ const workerSpecFile = process.env.WPT_WORKER_SPEC_FILE;
12
+ const workerResultsFile = process.env.WPT_WORKER_RESULTS;
13
+ const isWorker = Boolean(workerSpecFile);
14
+ const runInProcess = isWorker || process.env.WPT_IN_PROCESS === "1";
15
+ const cleanupDelayMs = Number(process.env.WPT_CLEANUP_DELAY_MS || 1000);
16
+ const testTimeoutMs = Number(process.env.WPT_TEST_TIMEOUT_MS || 120000);
17
+ const workerDelayMs = Number(process.env.WPT_WORKER_DELAY_MS || 200);
18
+ const workerRetries = Math.max(0, Number(process.env.WPT_WORKER_RETRIES || 0));
19
+ const workerTimeoutMs = Number(process.env.WPT_WORKER_TIMEOUT_MS || 300000);
20
+ const listTestsOnly = process.env.WPT_LIST_TESTS === "1";
21
+
22
+ if (!isWorker) ensureWpt({ quiet: true });
23
+
24
+ const perTestIsolatedFiles = new Set([
25
+ "webrtc/RTCPeerConnection-createDataChannel.html",
26
+ "webrtc/RTCIceTransport.html",
27
+ "webrtc/RTCDataChannel-id.html",
28
+ "webrtc/RTCDataChannel-send.html",
29
+ "webrtc/RTCDataChannel-send-blob-order.html",
30
+ "webrtc/RTCDataChannel-send-close-string.window.js",
31
+ "webrtc/RTCDataChannel-send-close-string-negotiated.window.js",
32
+ "webrtc/RTCDataChannel-send-close-array-buffer.window.js",
33
+ "webrtc/RTCDataChannel-send-close-array-buffer-negotiated.window.js",
34
+ "webrtc/RTCDataChannel-send-close-blob.window.js",
35
+ "webrtc/RTCDataChannel-send-close-blob-negotiated.window.js",
36
+ "webrtc/RTCDataChannel-bufferedAmount.html",
37
+ "webrtc/RTCDataChannel-close.html",
38
+ "webrtc/RTCDataChannel-GC.html",
39
+ "webrtc/RTCSctpTransport-events.html",
40
+ "webrtc/RTCSctpTransport-maxChannels.html",
41
+ "webrtc/RTCPeerConnection-ondatachannel.html",
42
+ ]);
43
+
44
+ const defaultSpecs = [
45
+ { file: "webrtc/RTCPeerConnection-constructor.html" },
46
+ { file: "webrtc/RTCError.html", search: "?interop-2026" },
47
+ { file: "webrtc/RTCError.html", search: "?rest" },
48
+ { file: "webrtc/RTCDataChannelEvent-constructor.html" },
49
+ { file: "webrtc/RTCPeerConnectionIceEvent-constructor.html" },
50
+ { file: "webrtc/RTCPeerConnectionIceErrorEvent.html" },
51
+ { file: "webrtc/RTCIceCandidate-constructor.html" },
52
+ { file: "webrtc/toJSON.html" },
53
+ { file: "webrtc/RTCPeerConnection-plan-b-is-not-supported.html" },
54
+ {
55
+ file: "webrtc/historical.html",
56
+ exclude: ["RTCRtpTransceiver member setDirection should not exist"],
57
+ },
58
+ { file: "webrtc/RTCPeerConnection-generateCertificate.html" },
59
+ {
60
+ file: "webrtc/RTCCertificate.html",
61
+ exclude: ["all provided certificates"],
62
+ },
63
+ { file: "webrtc/RTCConfiguration-certificates.html" },
64
+ { file: "webrtc/RTCConfiguration-validation.html" },
65
+ { file: "webrtc/RTCConfiguration-iceCandidatePoolSize.html" },
66
+ { file: "webrtc/RTCConfiguration-iceServers.html", search: "?rest" },
67
+ { file: "webrtc/RTCConfiguration-iceServers.html", search: "?interop-2026" },
68
+ {
69
+ file: "webrtc/RTCConfiguration-bundlePolicy.html",
70
+ exclude: ["should gather ICE candidates"],
71
+ },
72
+ {
73
+ file: "webrtc/RTCConfiguration-rtcpMuxPolicy.html",
74
+ exclude: ["setRemoteDescription throws"],
75
+ },
76
+ {
77
+ file: "webrtc/RTCConfiguration-iceTransportPolicy.html",
78
+ search: "?rest",
79
+ exclude: ["prevent candidate gathering", "Changing iceTransportPolicy"],
80
+ },
81
+ { file: "webrtc/RTCConfiguration-iceTransportPolicy.html", search: "?interop-2026" },
82
+ { file: "webrtc/RTCSctpTransport-constructor.html" },
83
+ { file: "webrtc/RTCSctpTransport-events.html" },
84
+ { file: "webrtc/RTCSctpTransport-maxChannels.html", search: "?interop-2026" },
85
+ { file: "webrtc/RTCSctpTransport-maxChannels.html", search: "?rest" },
86
+ { file: "webrtc/RTCSctpTransport-maxMessageSize.html" },
87
+ {
88
+ file: "webrtc/RTCIceTransport.html",
89
+ search: "?rest",
90
+ include: [
91
+ "Two connected iceTransports should have matching local/remote candidates returned",
92
+ "Unconnected iceTransport should have empty remote candidates and selected pair",
93
+ 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)',
94
+ "Local ICE restart should not result in a different ICE transport (DataChannel case)",
95
+ "Remote ICE restart should not result in a different ICE transport (DataChannel case)",
96
+ ],
97
+ },
98
+ { file: "webrtc/RTCDataChannelInit-maxRetransmits-enforce-range.html" },
99
+ { file: "webrtc/RTCDataChannelInit-maxPacketLifeTime-enforce-range.html" },
100
+ { file: "webrtc/RTCDataChannel-binaryType.window.js" },
101
+ {
102
+ file: "webrtc/RTCPeerConnection-createDataChannel.html",
103
+ include: [
104
+ "createDataChannel with no argument",
105
+ "createDataChannel with closed connection",
106
+ "createDataChannel attribute default values",
107
+ "createDataChannel with provided parameters",
108
+ "createDataChannel with label",
109
+ "createDataChannel with ordered",
110
+ "createDataChannel with maxPacketLifeTime 0",
111
+ "createDataChannel with maxRetransmits 0",
112
+ "createDataChannel with both maxPacketLifeTime",
113
+ "createDataChannel with protocol",
114
+ "createDataChannel with id 0 and negotiated true",
115
+ "createDataChannel with id 1 and negotiated true",
116
+ "createDataChannel with id 65534 and negotiated true",
117
+ "createDataChannel with id -1",
118
+ "createDataChannel with id 65535 should throw",
119
+ "createDataChannel with id 65536",
120
+ "createDataChannel with too long",
121
+ "createDataChannel with same label",
122
+ "createDataChannel with negotiated true and id should succeed",
123
+ "createDataChannel with maximum length",
124
+ "createDataChannel with negotiated false",
125
+ "createDataChannel with negotiated true and id not defined",
126
+ "Channels created (after SCTP connected) should have id assigned",
127
+ "Reusing a data channel id that is in use should throw OperationError",
128
+ "Reusing a data channel id that is in use (after setRemoteDescription) should throw OperationError",
129
+ "Reusing a data channel id that is in use (after setRemoteDescription, negotiated via DCEP) should throw OperationError",
130
+ "New datachannel should be in the connecting state after creation",
131
+ "New negotiated datachannel should be in the connecting state after creation",
132
+ ],
133
+ },
134
+ {
135
+ file: "webrtc/RTCPeerConnection-createDataChannel.html",
136
+ search: "?interop-2026",
137
+ include: ["createDataChannel with id"],
138
+ },
139
+ { file: "webrtc/RTCDataChannel-id.html" },
140
+ {
141
+ file: "webrtc/RTCDataChannel-send.html",
142
+ include: [
143
+ "Calling send() when data channel is in connecting state should throw InvalidStateError",
144
+ "should be able to send",
145
+ "should ignore binaryType",
146
+ "binaryType should receive",
147
+ "sending multiple messages with different types",
148
+ "Sending before the other side is open should work",
149
+ "Sending in onopen should work",
150
+ "Sending in ondatachannel should work",
151
+ ],
152
+ exclude: ["unordered mode works reliably"],
153
+ },
154
+ { file: "webrtc/RTCDataChannel-send-blob-order.html" },
155
+ { file: "webrtc/RTCDataChannel-send-close-string.window.js" },
156
+ { file: "webrtc/RTCDataChannel-send-close-string-negotiated.window.js" },
157
+ { file: "webrtc/RTCDataChannel-send-close-array-buffer.window.js" },
158
+ { file: "webrtc/RTCDataChannel-send-close-array-buffer-negotiated.window.js" },
159
+ { file: "webrtc/RTCDataChannel-send-close-blob.window.js" },
160
+ { file: "webrtc/RTCDataChannel-send-close-blob-negotiated.window.js" },
161
+ {
162
+ file: "webrtc/RTCDataChannel-bufferedAmount.html",
163
+ include: [
164
+ "initial value",
165
+ "bufferedAmount should increase",
166
+ "bufferedAmount should stay",
167
+ "bufferedamount is data.length",
168
+ "bufferedamount returns the same amount",
169
+ "bufferedamountlow event fires",
170
+ "not decrease immediately",
171
+ "not decrease after closing",
172
+ ],
173
+ },
174
+ { file: "webrtc/RTCDataChannel-close.html" },
175
+ { file: "webrtc/RTCDataChannel-GC.html" },
176
+ { file: "webrtc/RTCDataChannel-iceRestart.html" },
177
+ { file: "webrtc/promises-call.html" },
178
+ {
179
+ file: "webrtc/RTCPeerConnection-restartIce.https.html",
180
+ include: ["restartIce() has no effect on a closed peer connection"],
181
+ },
182
+ {
183
+ file: "webrtc/RTCPeerConnection-createOffer.html",
184
+ include: [
185
+ "createOffer() returns RTCSessionDescriptionInit",
186
+ "createOffer() and then setLocalDescription() should succeed",
187
+ "createOffer() after connection is closed",
188
+ ],
189
+ },
190
+ {
191
+ file: "webrtc/RTCPeerConnection-createOffer.html",
192
+ search: "?interop-2026",
193
+ include: ["createOffer() should fail when signaling state is not stable or have-local-offer"],
194
+ },
195
+ {
196
+ file: "webrtc/RTCPeerConnection-operations.https.html",
197
+ search: "?interop-2026",
198
+ include: [
199
+ "createOffer must detect InvalidStateError synchronously",
200
+ "createAnswer must detect InvalidStateError synchronously",
201
+ "isOperationsChainEmpty detects empty in stable",
202
+ "isOperationsChainEmpty detects empty in have-local-offer",
203
+ "isOperationsChainEmpty detects empty in have-remote-offer",
204
+ "createAnswer uses operations chain",
205
+ "setLocalDescription uses operations chain",
206
+ "setRemoteDescription uses operations chain",
207
+ ],
208
+ },
209
+ {
210
+ file: "webrtc/RTCPeerConnection-operations.https.html",
211
+ include: [
212
+ "SLD(rollback) must detect InvalidStateError synchronously",
213
+ "addIceCandidate must detect InvalidStateError synchronously",
214
+ "createOffer uses operations chain",
215
+ ],
216
+ },
217
+ { file: "webrtc/RTCPeerConnection-createAnswer.html" },
218
+ {
219
+ file: "webrtc/RTCPeerConnection-setLocalDescription.html",
220
+ include: [
221
+ "Calling createOffer() and setLocalDescription() again after one round of local-offer/remote-answer should succeed",
222
+ "onsignalingstatechange fires before setLocalDescription resolves",
223
+ ],
224
+ },
225
+ {
226
+ file: "webrtc/RTCPeerConnection-setLocalDescription-offer.html",
227
+ include: [
228
+ "setLocalDescription with valid offer should succeed",
229
+ "setLocalDescription with type offer and null sdp should use lastOffer generated from createOffer",
230
+ ],
231
+ },
232
+ {
233
+ file: "webrtc/RTCPeerConnection-setLocalDescription-offer.html",
234
+ search: "?interop-2026",
235
+ include: [
236
+ "setLocalDescription() with offer not created by own createOffer() should reject with InvalidModificationError",
237
+ ],
238
+ },
239
+ {
240
+ file: "webrtc/RTCPeerConnection-setLocalDescription-answer.html",
241
+ include: [
242
+ "setLocalDescription() with valid answer should succeed",
243
+ "setLocalDescription() with type answer and null sdp should use lastAnswer generated from createAnswer",
244
+ "setLocalDescription() with answer not created by own createAnswer() should reject with InvalidModificationError",
245
+ "Calling setLocalDescription(answer) from stable state should reject with InvalidStateError",
246
+ "Calling setLocalDescription(answer) from have-local-offer state should reject with InvalidStateError",
247
+ ],
248
+ },
249
+ { file: "webrtc/RTCPeerConnection-setLocalDescription-pranswer.html" },
250
+ {
251
+ file: "webrtc/RTCPeerConnection-setLocalDescription-rollback.html",
252
+ include: [
253
+ "setLocalDescription(rollback) from have-local-offer state should reset back to stable state",
254
+ "setLocalDescription(rollback) from stable state should reject with InvalidStateError",
255
+ "setLocalDescription(rollback) after setting answer description should reject with InvalidStateError",
256
+ "setLocalDescription(rollback) after setting a remote offer should reject with InvalidStateError",
257
+ "setLocalDescription(rollback) should ignore invalid sdp content and succeed",
258
+ ],
259
+ },
260
+ { file: "webrtc/RTCPeerConnection-description-attributes-timing.https.html" },
261
+ {
262
+ file: "webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html",
263
+ include: [
264
+ "Parameterless SLD() in 'stable' goes to 'have-local-offer'",
265
+ "Parameterless SLD() in 'stable' sets pendingLocalDescription",
266
+ "Parameterless SLD() in 'have-remote-offer' goes to 'stable'",
267
+ "Parameterless SLD() in 'have-remote-offer' sets currentLocalDescription",
268
+ "Parameterless SLD() uses [[LastCreatedOffer]] if it is still valid",
269
+ "Parameterless SLD() uses [[LastCreatedAnswer]] if it is still valid",
270
+ "Parameterless SLD() rejects with InvalidStateError if already closed",
271
+ "Parameterless SLD() never settles if closed while pending",
272
+ "Parameterless SLD() in a full O/A exchange succeeds",
273
+ "Parameterless SRD() rejects with TypeError.",
274
+ ],
275
+ },
276
+ {
277
+ file: "webrtc/RTCPeerConnection-setLocalDescription-parameterless.https.html",
278
+ search: "?interop-2026",
279
+ include: ["RTCSessionDescription constructed without type throws TypeError"],
280
+ },
281
+ {
282
+ file: "webrtc/RTCPeerConnection-setRemoteDescription.html",
283
+ include: [
284
+ "invalid type and invalid SDP",
285
+ "invalid SDP and stable state",
286
+ "Negotiation should fire signalingsstate events",
287
+ "Switching role from offerer to answerer after going back to stable state should succeed",
288
+ "Closing on setRemoteDescription() neither resolves nor rejects",
289
+ "Closing on rollback neither resolves nor rejects",
290
+ ],
291
+ },
292
+ {
293
+ file: "webrtc/RTCPeerConnection-setRemoteDescription-offer.html",
294
+ include: [
295
+ "setRemoteDescription with valid offer should succeed",
296
+ "setRemoteDescription multiple times should succeed",
297
+ "setRemoteDescription(offer) with invalid SDP should reject with RTCError",
298
+ "setRemoteDescription(offer) from have-local-offer should roll back and succeed",
299
+ "Naive rollback approach is not glare-proof (control)",
300
+ "setRemoteDescription(offer) from have-local-offer is glare-proof",
301
+ ],
302
+ },
303
+ {
304
+ file: "webrtc/RTCPeerConnection-setRemoteDescription-offer.html",
305
+ search: "?interop-2026",
306
+ include: ["setRemoteDescription(offer) from have-local-offer fires signalingstatechange twice"],
307
+ },
308
+ { file: "webrtc/RTCPeerConnection-setRemoteDescription-answer.html" },
309
+ { file: "webrtc/RTCPeerConnection-setRemoteDescription-pranswer.html" },
310
+ {
311
+ file: "webrtc/RTCPeerConnection-setRemoteDescription-rollback.html",
312
+ include: [
313
+ "setRemoteDescription(rollback) in have-remote-offer state should revert to stable state",
314
+ "setRemoteDescription(rollback) from stable state should reject with InvalidStateError",
315
+ "setRemoteDescription(rollback) after setting a local offer should reject with InvalidStateError",
316
+ "setRemoteDescription(rollback) should ignore invalid sdp content and succeed",
317
+ ],
318
+ },
319
+ { file: "webrtc/RTCPeerConnection-addIceCandidate.html" },
320
+ {
321
+ file: "webrtc/RTCPeerConnection-addIceCandidate.html",
322
+ search: "?interop-2026",
323
+ include: [
324
+ "addIceCandidate after close",
325
+ "addIceCandidate should not recognize relayProtocol or url",
326
+ ],
327
+ },
328
+ { file: "webrtc/RTCPeerConnection-canTrickleIceCandidates.html" },
329
+ {
330
+ file: "webrtc/RTCPeerConnection-iceGatheringState.html",
331
+ include: [
332
+ "Initial iceGatheringState should be new",
333
+ "setLocalDescription() with no transports should not cause iceGatheringState to change",
334
+ ],
335
+ },
336
+ {
337
+ file: "webrtc/RTCPeerConnection-iceGatheringState.html",
338
+ search: "?interop-2026",
339
+ include: ["connection with one data channel should eventually have connected connection state"],
340
+ },
341
+ {
342
+ file: "webrtc/RTCPeerConnection-explicit-rollback-iceGatheringState.html",
343
+ include: [
344
+ "rolling back an ICE restart when gathering is complete should not result in iceGatheringState changes (DataChannel case)",
345
+ 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "complete" (DataChannel case)',
346
+ 'setLocalDescription(rollback) of original offer should cause iceGatheringState to reach "new" when starting in "gathering" (DataChannel case)',
347
+ ],
348
+ },
349
+ {
350
+ file: "webrtc/RTCPeerConnection-iceConnectionState.https.html",
351
+ include: [
352
+ "Initial iceConnectionState should be new",
353
+ "Closing the connection should set iceConnectionState to closed",
354
+ "connection with one data channel should eventually have connected or completed connection state",
355
+ "connection with one data channel should eventually have connected connection state",
356
+ ],
357
+ },
358
+ {
359
+ file: "webrtc/RTCPeerConnection-connectionState.https.html",
360
+ include: [
361
+ "Initial connectionState should be new",
362
+ "Closing the connection should set connectionState to closed",
363
+ "connection with one data channel should eventually have connected connection state",
364
+ "connection with one data channel should eventually have transports in connected state",
365
+ ],
366
+ },
367
+ { file: "webrtc/RTCPeerConnection-ondatachannel.html" },
368
+ {
369
+ file: "webrtc/RTCPeerConnection-onnegotiationneeded.html",
370
+ include: [
371
+ "Creating first data channel should fire negotiationneeded event",
372
+ "calling createDataChannel twice should fire negotiationneeded event once",
373
+ ],
374
+ },
375
+ ];
376
+
377
+ const specs = isWorker
378
+ ? [JSON.parse(fs.readFileSync(workerSpecFile, "utf8"))]
379
+ : process.argv.length > 2
380
+ ? process.argv.slice(2).map(parseSpec)
381
+ : defaultSpecs;
382
+ const results = [];
383
+ let streamedResults = false;
384
+
385
+ function parseSpec(value) {
386
+ const [fileAndSearch, filter] = value.split("#", 2);
387
+ const queryIndex = fileAndSearch.indexOf("?");
388
+ const file = queryIndex === -1 ? fileAndSearch : fileAndSearch.slice(0, queryIndex);
389
+ const search = queryIndex === -1 ? undefined : fileAndSearch.slice(queryIndex);
390
+ if (filter) {
391
+ return {
392
+ file,
393
+ search,
394
+ include: [filter],
395
+ };
396
+ }
397
+
398
+ return cloneDefaultSpecForExplicitFile(file, search) || { file, search };
399
+ }
400
+
401
+ function cloneDefaultSpecForExplicitFile(file, search) {
402
+ const defaultSpec = defaultSpecs.find(
403
+ (spec) => spec.file === file && (spec.search || undefined) === search,
404
+ );
405
+ if (!defaultSpec) return null;
406
+ return {
407
+ ...defaultSpec,
408
+ include: defaultSpec.include ? [...defaultSpec.include] : undefined,
409
+ includeExact: defaultSpec.includeExact ? [...defaultSpec.includeExact] : undefined,
410
+ exclude: defaultSpec.exclude ? [...defaultSpec.exclude] : undefined,
411
+ };
412
+ }
413
+
414
+ function extractScripts(relativePath) {
415
+ if (relativePath.endsWith(".js")) {
416
+ return extractJsScripts(relativePath);
417
+ }
418
+
419
+ const absolutePath = path.join(wptDir, relativePath);
420
+ const html = fs.readFileSync(absolutePath, "utf8");
421
+ const baseDir = path.dirname(relativePath);
422
+ const scripts = [];
423
+ const pattern = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
424
+ let match;
425
+ while ((match = pattern.exec(html))) {
426
+ const srcMatch = match[1].match(/\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i);
427
+ if (srcMatch) {
428
+ const src = srcMatch[1] || srcMatch[2] || srcMatch[3];
429
+ if (/testharness(?:report)?\.js$/.test(src)) continue;
430
+ const scriptPath = src.startsWith("/")
431
+ ? src.slice(1)
432
+ : path.join(baseDir, src).replace(/\\/g, "/");
433
+ scripts.push(...extractJsScripts(scriptPath));
434
+ continue;
435
+ }
436
+ scripts.push({ code: transformScriptSource(relativePath, match[2]), filename: relativePath });
437
+ }
438
+ return scripts;
439
+ }
440
+
441
+ function transformScriptSource(relativePath, code) {
442
+ if (relativePath === "webrtc/RTCIceTransport.html") {
443
+ // The pinned WPT revision has a typo in one legacy helper assertion:
444
+ // RTCIceTransport.gatheringState is "complete", not "completed".
445
+ return code.replace(
446
+ "gatheringState === 'gathering' || gatheringState === 'completed'",
447
+ "gatheringState === 'gathering' || gatheringState === 'complete'",
448
+ );
449
+ }
450
+ return code;
451
+ }
452
+
453
+ function extractJsScripts(relativePath, seen = new Set()) {
454
+ const normalizedPath = relativePath.replace(/\\/g, "/");
455
+ if (seen.has(normalizedPath)) return [];
456
+ seen.add(normalizedPath);
457
+
458
+ const code = transformScriptSource(
459
+ normalizedPath,
460
+ fs.readFileSync(path.join(wptDir, normalizedPath), "utf8"),
461
+ );
462
+ const scripts = [];
463
+ const baseDir = path.dirname(normalizedPath);
464
+ const pattern = /^\s*\/\/\s*META:\s*script=(.+?)\s*$/gm;
465
+ let match;
466
+ while ((match = pattern.exec(code))) {
467
+ const src = match[1].trim();
468
+ if (/testharness(?:report)?\.js$/.test(src)) continue;
469
+ const scriptPath = src.startsWith("/")
470
+ ? src.slice(1)
471
+ : path.join(baseDir, src).replace(/\\/g, "/");
472
+ scripts.push(...extractJsScripts(scriptPath, seen));
473
+ }
474
+ scripts.push({ code, filename: normalizedPath });
475
+ return scripts;
476
+ }
477
+
478
+ function sameException(error, ctor) {
479
+ return error instanceof ctor || error?.name === ctor.name;
480
+ }
481
+
482
+ function delay(ms) {
483
+ return new Promise((resolve) => setTimeout(resolve, ms));
484
+ }
485
+
486
+ function runGarbageCollection() {
487
+ if (typeof global.gc !== "function") return;
488
+ for (let index = 0; index < 2; ++index) global.gc();
489
+ }
490
+
491
+ function shouldRun(spec, name) {
492
+ const includeExact = spec.includeExact || [];
493
+ const includes = spec.include || [];
494
+ const excludes = spec.exclude || [];
495
+ if (includeExact.length && !includeExact.includes(name)) return false;
496
+ if (
497
+ !includeExact.length &&
498
+ includes.length &&
499
+ !includes.some((pattern) => name.includes(pattern))
500
+ )
501
+ return false;
502
+ return !excludes.some((pattern) => name.includes(pattern));
503
+ }
504
+
505
+ class FileReaderShim extends webrtc.EventTarget {
506
+ constructor() {
507
+ super();
508
+ this.result = null;
509
+ this.error = null;
510
+ }
511
+
512
+ async readAsArrayBuffer(blob) {
513
+ try {
514
+ this.result = await blob.arrayBuffer();
515
+ this.dispatchEvent({ type: "load" });
516
+ } catch (error) {
517
+ this.error = error;
518
+ this.dispatchEvent({ type: "error" });
519
+ }
520
+ }
521
+ }
522
+
523
+ class EventWatcher {
524
+ constructor(test, target, events) {
525
+ this.target = target;
526
+ this.events = Array.isArray(events) ? events : [events];
527
+ this.queue = [];
528
+ this.waiters = [];
529
+ this.handlers = new Map();
530
+
531
+ for (const type of this.events) {
532
+ const handler = (event) => {
533
+ this.queue.push(event);
534
+ this.pump();
535
+ };
536
+ this.handlers.set(type, handler);
537
+ this.target.addEventListener(type, handler);
538
+ }
539
+
540
+ if (test && typeof test.add_cleanup === "function") {
541
+ test.add_cleanup(() => this.stop());
542
+ }
543
+ }
544
+
545
+ wait_for(events) {
546
+ const expected = Array.isArray(events) ? events : [events];
547
+ return new Promise((resolve) => {
548
+ this.waiters.push({ expected, resolve });
549
+ this.pump();
550
+ });
551
+ }
552
+
553
+ pump() {
554
+ while (this.waiters.length) {
555
+ const waiter = this.waiters[0];
556
+ if (this.queue.length < waiter.expected.length) return;
557
+ if (!waiter.expected.every((name, index) => this.queue[index].type === name)) return;
558
+ const events = this.queue.splice(0, waiter.expected.length);
559
+ this.waiters.shift();
560
+ waiter.resolve(events[events.length - 1]);
561
+ }
562
+ }
563
+
564
+ stop() {
565
+ for (const [type, handler] of this.handlers) this.target.removeEventListener(type, handler);
566
+ this.handlers.clear();
567
+ this.waiters.length = 0;
568
+ this.queue.length = 0;
569
+ }
570
+ }
571
+
572
+ class Resolver extends Promise {
573
+ constructor() {
574
+ let resolve;
575
+ let reject;
576
+ super((res, rej) => {
577
+ resolve = res;
578
+ reject = rej;
579
+ });
580
+ this.resolve = resolve;
581
+ this.reject = reject;
582
+ }
583
+ }
584
+
585
+ async function runFile(spec) {
586
+ const relativePath = spec.file;
587
+ const resultPath = `${relativePath}${spec.search || ""}`;
588
+ const pending = [];
589
+ const selectedTests = [];
590
+ const trackedPeerConnections = new Set();
591
+
592
+ class HarnessRTCPeerConnection extends webrtc.RTCPeerConnection {
593
+ constructor(...args) {
594
+ super(...args);
595
+ trackedPeerConnections.add(this);
596
+ }
597
+
598
+ close() {
599
+ trackedPeerConnections.delete(this);
600
+ return super.close();
601
+ }
602
+ }
603
+
604
+ async function cleanupAfterTest(cleanups) {
605
+ for (const cleanup of cleanups.splice(0).reverse()) {
606
+ try {
607
+ cleanup();
608
+ } catch {
609
+ // Keep focused WPT cleanup best-effort.
610
+ }
611
+ }
612
+ for (const pc of Array.from(trackedPeerConnections)) {
613
+ if (isRetainedByGlobal(pc)) continue;
614
+ try {
615
+ pc.close();
616
+ } catch {
617
+ // Keep harness auto-cleanup best-effort.
618
+ }
619
+ }
620
+ runGarbageCollection();
621
+ await delay(cleanupDelayMs);
622
+ }
623
+
624
+ function isRetainedByGlobal(pc) {
625
+ return Object.values(sandbox).some((value) => value === pc);
626
+ }
627
+
628
+ async function cleanupAfterFile() {
629
+ for (const pc of Array.from(trackedPeerConnections)) {
630
+ try {
631
+ pc.close();
632
+ } catch {
633
+ // Keep harness auto-cleanup best-effort.
634
+ }
635
+ }
636
+ runGarbageCollection();
637
+ await delay(cleanupDelayMs);
638
+ }
639
+
640
+ const documentElements = new Map();
641
+ const documentShim = {
642
+ getElementById(id) {
643
+ const key = String(id);
644
+ if (!documentElements.has(key)) {
645
+ documentElements.set(key, { id: key, innerHTML: "" });
646
+ }
647
+ return documentElements.get(key);
648
+ },
649
+ };
650
+
651
+ const sandbox = {
652
+ ...webrtc,
653
+ RTCPeerConnection: HarnessRTCPeerConnection,
654
+ console,
655
+ setTimeout,
656
+ clearTimeout,
657
+ Promise,
658
+ TypeError,
659
+ Error,
660
+ Array,
661
+ ArrayBuffer,
662
+ Uint8Array,
663
+ Int8Array,
664
+ Int16Array,
665
+ Int32Array,
666
+ Uint16Array,
667
+ Uint32Array,
668
+ Uint8ClampedArray,
669
+ Float32Array,
670
+ Float64Array,
671
+ DataView,
672
+ Blob: globalThis.Blob,
673
+ DOMException: globalThis.DOMException,
674
+ gc: globalThis.gc,
675
+ FileReader: FileReaderShim,
676
+ EventWatcher,
677
+ Resolver,
678
+ structuredClone: globalThis.structuredClone,
679
+ performance: globalThis.performance,
680
+ TextDecoder: globalThis.TextDecoder,
681
+ TextEncoder: globalThis.TextEncoder,
682
+ JSON,
683
+ Number,
684
+ String,
685
+ Boolean,
686
+ document: documentShim,
687
+ Math,
688
+ RegExp,
689
+ URL,
690
+ URLSearchParams,
691
+ location: { search: spec.search || "?rest" },
692
+ };
693
+ sandbox.window = sandbox;
694
+ sandbox.self = sandbox;
695
+ sandbox.globalThis = sandbox;
696
+
697
+ sandbox.assert_equals = (actual, expected, message = "") => {
698
+ if (!Object.is(actual, expected)) {
699
+ throw new Error(`${message} expected ${expected}, got ${actual}`.trim());
700
+ }
701
+ };
702
+ sandbox.assert_true = (actual, message = "") => {
703
+ if (actual !== true) throw new Error(`${message} expected true, got ${actual}`.trim());
704
+ };
705
+ sandbox.assert_false = (actual, message = "") => {
706
+ if (actual !== false) throw new Error(`${message} expected false, got ${actual}`.trim());
707
+ };
708
+ sandbox.assert_throws_js = (ctor, fn, message = "") => {
709
+ try {
710
+ fn();
711
+ } catch (error) {
712
+ if (sameException(error, ctor)) return;
713
+ throw new Error(`${message} expected ${ctor.name}, got ${error?.name || error}`.trim());
714
+ }
715
+ throw new Error(`${message} expected ${ctor.name} to be thrown`.trim());
716
+ };
717
+ sandbox.assert_throws_dom = (name, fn, message = "") => {
718
+ try {
719
+ fn();
720
+ } catch (error) {
721
+ if (error?.name === name) return;
722
+ throw new Error(`${message} expected ${name}, got ${error?.name || error}`.trim());
723
+ }
724
+ throw new Error(`${message} expected ${name} to be thrown`.trim());
725
+ };
726
+ sandbox.promise_rejects_dom = async (test, name, promise, message = "") => {
727
+ try {
728
+ await promise;
729
+ } catch (error) {
730
+ if (error?.name === name) return;
731
+ throw new Error(`${message} expected ${name}, got ${error?.name || error}`.trim());
732
+ }
733
+ throw new Error(`${message} expected ${name} rejection`.trim());
734
+ };
735
+ sandbox.promise_rejects_js = async (test, ctor, promise, message = "") => {
736
+ try {
737
+ await promise;
738
+ } catch (error) {
739
+ if (sameException(error, ctor)) return;
740
+ throw new Error(`${message} expected ${ctor.name}, got ${error?.name || error}`.trim());
741
+ }
742
+ throw new Error(`${message} expected ${ctor.name} rejection`.trim());
743
+ };
744
+ sandbox.assert_not_equals = (actual, expected, message = "") => {
745
+ if (Object.is(actual, expected)) {
746
+ throw new Error(`${message} expected values to differ`.trim());
747
+ }
748
+ };
749
+ sandbox.assert_array_equals = (actual, expected, message = "") => {
750
+ sandbox.assert_equals(actual.length, expected.length, `${message} length`);
751
+ for (let i = 0; i < actual.length; ++i) {
752
+ sandbox.assert_equals(actual[i], expected[i], `${message} index ${i}`);
753
+ }
754
+ };
755
+ sandbox.assert_in_array = (actual, expected, message = "") => {
756
+ if (!expected.includes(actual)) {
757
+ throw new Error(`${message} expected ${actual} in ${expected.join(",")}`.trim());
758
+ }
759
+ };
760
+ sandbox.assert_idl_attribute = (object, attribute, message = "") => {
761
+ if (!(attribute in Object(object))) {
762
+ throw new Error(`${message} expected ${attribute} IDL attribute`.trim());
763
+ }
764
+ };
765
+ sandbox.assert_less_than = (actual, expected, message = "") => {
766
+ if (!(actual < expected)) throw new Error(`${message} expected ${actual} < ${expected}`.trim());
767
+ };
768
+ sandbox.assert_less_than_equal = (actual, expected, message = "") => {
769
+ if (!(actual <= expected))
770
+ throw new Error(`${message} expected ${actual} <= ${expected}`.trim());
771
+ };
772
+ sandbox.assert_approx_equals = (actual, expected, epsilon, message = "") => {
773
+ if (Math.abs(actual - expected) > epsilon) {
774
+ throw new Error(`${message} expected ${actual} within ${epsilon} of ${expected}`.trim());
775
+ }
776
+ };
777
+ sandbox.assert_greater_than = (actual, expected, message = "") => {
778
+ if (!(actual > expected)) throw new Error(`${message} expected ${actual} > ${expected}`.trim());
779
+ };
780
+ sandbox.assert_greater_than_equal = (actual, expected, message = "") => {
781
+ if (!(actual >= expected))
782
+ throw new Error(`${message} expected ${actual} >= ${expected}`.trim());
783
+ };
784
+ sandbox.assert_unreached = (message = "unreached") => {
785
+ throw new Error(message);
786
+ };
787
+
788
+ sandbox.test = (fn, name = "unnamed test") => {
789
+ if (!shouldRun(spec, name)) return;
790
+ selectedTests.push(name);
791
+ if (listTestsOnly) return;
792
+ pending.push(async () => {
793
+ const cleanups = [];
794
+ const t = makeTestContext(cleanups);
795
+ try {
796
+ fn(t);
797
+ results.push({ file: resultPath, name, status: "PASS" });
798
+ } catch (error) {
799
+ results.push({ file: resultPath, name, status: "FAIL", message: error.message });
800
+ } finally {
801
+ await cleanupAfterTest(cleanups);
802
+ }
803
+ });
804
+ };
805
+
806
+ sandbox.promise_test = (fn, name = "unnamed promise_test") => {
807
+ if (!shouldRun(spec, name)) return;
808
+ selectedTests.push(name);
809
+ if (listTestsOnly) return;
810
+ pending.push(async () => {
811
+ const cleanups = [];
812
+ let rejectStepFailure;
813
+ let stepFailureRecorded = false;
814
+ const stepFailure = new Promise((_, reject) => {
815
+ rejectStepFailure = reject;
816
+ });
817
+ const t = makeTestContext(cleanups, {
818
+ fail: (error) => {
819
+ if (stepFailureRecorded) return;
820
+ stepFailureRecorded = true;
821
+ rejectStepFailure(error instanceof Error ? error : new Error(String(error)));
822
+ },
823
+ });
824
+ try {
825
+ await withTimeout(
826
+ Promise.race([Promise.resolve().then(() => fn(t)), stepFailure]),
827
+ testTimeoutMs,
828
+ name,
829
+ );
830
+ results.push({ file: resultPath, name, status: "PASS" });
831
+ } catch (error) {
832
+ results.push({ file: resultPath, name, status: "FAIL", message: error.message });
833
+ } finally {
834
+ await cleanupAfterTest(cleanups);
835
+ }
836
+ });
837
+ };
838
+
839
+ sandbox.async_test = (fn, name = "unnamed async_test") => {
840
+ const body = typeof fn === "function" ? fn : null;
841
+ const testName = typeof fn === "string" ? fn : name;
842
+ if (!shouldRun(spec, testName)) return makeListOnlyTestContext();
843
+ selectedTests.push(testName);
844
+ if (listTestsOnly) return makeListOnlyTestContext();
845
+ const cleanups = [];
846
+ let settled = false;
847
+ let resolveDone;
848
+ let rejectDone;
849
+ const donePromise = new Promise((resolve, reject) => {
850
+ resolveDone = resolve;
851
+ rejectDone = reject;
852
+ });
853
+ const settle = (callback, value) => {
854
+ if (settled) return;
855
+ settled = true;
856
+ callback(value);
857
+ };
858
+ const t = makeTestContext(cleanups, {
859
+ done: () => settle(resolveDone),
860
+ fail: (error) => settle(rejectDone, error),
861
+ });
862
+ pending.push(async () => {
863
+ try {
864
+ if (body) body(t);
865
+ await withTimeout(donePromise, testTimeoutMs, testName);
866
+ results.push({ file: resultPath, name: testName, status: "PASS" });
867
+ } catch (error) {
868
+ results.push({ file: resultPath, name: testName, status: "FAIL", message: error.message });
869
+ } finally {
870
+ await delay(cleanupDelayMs);
871
+ await cleanupAfterTest(cleanups);
872
+ }
873
+ });
874
+ return t;
875
+ };
876
+
877
+ const context = vm.createContext(sandbox);
878
+ for (const script of extractScripts(relativePath)) {
879
+ vm.runInContext(script.code, context, { filename: script.filename });
880
+ }
881
+ if (listTestsOnly) return selectedTests;
882
+ for (const run of pending) {
883
+ await run();
884
+ }
885
+ await cleanupAfterFile();
886
+ return selectedTests;
887
+ }
888
+
889
+ function withTimeout(promise, timeout, name) {
890
+ let timer;
891
+ return Promise.race([
892
+ promise,
893
+ new Promise((_, reject) => {
894
+ timer = setTimeout(() => reject(new Error(`Timed out waiting for ${name}`)), timeout);
895
+ }),
896
+ ]).finally(() => clearTimeout(timer));
897
+ }
898
+
899
+ function makeTestContext(cleanups, hooks = {}) {
900
+ const fail =
901
+ hooks.fail ||
902
+ ((error) => {
903
+ throw error;
904
+ });
905
+ const wrapStep =
906
+ (fn, finish = false) =>
907
+ (...args) => {
908
+ try {
909
+ const result = fn(...args);
910
+ if (finish && typeof hooks.done === "function") hooks.done();
911
+ return result;
912
+ } catch (error) {
913
+ fail(error);
914
+ return undefined;
915
+ }
916
+ };
917
+ return {
918
+ add_cleanup: (cleanup) => cleanups.push(cleanup),
919
+ step: (fn, thisArg = undefined, ...args) => wrapStep(fn).apply(thisArg, args),
920
+ step_func: (fn) => wrapStep(fn),
921
+ step_func_done: (fn = () => {}) => wrapStep(fn, true),
922
+ unreached_func:
923
+ (message = "unexpected callback") =>
924
+ () => {
925
+ fail(new Error(message));
926
+ },
927
+ step_timeout: (fn, timeout, ...args) =>
928
+ setTimeout(() => {
929
+ try {
930
+ fn(...args);
931
+ } catch (error) {
932
+ fail(error);
933
+ }
934
+ }, timeout),
935
+ done: () => {
936
+ if (typeof hooks.done === "function") hooks.done();
937
+ },
938
+ };
939
+ }
940
+
941
+ function makeListOnlyTestContext() {
942
+ return {
943
+ add_cleanup: () => {},
944
+ step: () => undefined,
945
+ step_func: () => () => undefined,
946
+ step_func_done: () => () => undefined,
947
+ unreached_func: () => () => undefined,
948
+ step_timeout: () => undefined,
949
+ done: () => {},
950
+ fail: () => {},
951
+ };
952
+ }
953
+
954
+ function makeTempJsonPath(name) {
955
+ const unique = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
956
+ return path.join(os.tmpdir(), `webrtc-node-${name}-${unique}.json`);
957
+ }
958
+
959
+ async function runIsolated(specsToRun) {
960
+ for (let index = 0; index < specsToRun.length; ++index) {
961
+ const spec = specsToRun[index];
962
+ if (perTestIsolatedFiles.has(spec.file)) {
963
+ const tests = runListWorker(spec, index);
964
+ for (let testIndex = 0; testIndex < tests.length; ++testIndex) {
965
+ runSpecWorker(
966
+ {
967
+ ...spec,
968
+ include: undefined,
969
+ includeExact: [tests[testIndex]],
970
+ },
971
+ `${index}-${testIndex}`,
972
+ );
973
+ await delay(workerDelayMs);
974
+ }
975
+ } else {
976
+ runSpecWorker(spec, String(index));
977
+ await delay(workerDelayMs);
978
+ }
979
+ }
980
+ }
981
+
982
+ function listIsolatedTests(specsToRun) {
983
+ const tests = [];
984
+ for (let index = 0; index < specsToRun.length; ++index) {
985
+ tests.push(...runListWorker(specsToRun[index], index));
986
+ }
987
+ return tests;
988
+ }
989
+
990
+ function runListWorker(spec, index) {
991
+ const outcome = runWorker(spec, `list-${index}`, { WPT_LIST_TESTS: "1" });
992
+ if (outcome.payload?.tests) return outcome.payload.tests;
993
+ const resultPath = `${spec.file}${spec.search || ""}`;
994
+ const output = [outcome.child.stderr, outcome.child.stdout].filter(Boolean).join("\n").trim();
995
+ recordResult({
996
+ file: resultPath,
997
+ name: "worker test discovery",
998
+ status: "FAIL",
999
+ message:
1000
+ output || outcome.child.error?.message || `worker exited with status ${outcome.child.status}`,
1001
+ });
1002
+ return [];
1003
+ }
1004
+
1005
+ function runSpecWorker(spec, index) {
1006
+ const attempts = [];
1007
+ for (let attempt = 0; attempt <= workerRetries; ++attempt) {
1008
+ const suffix = attempt === 0 ? `run-${index}` : `run-${index}-retry-${attempt}`;
1009
+ const outcome = runWorker(spec, suffix);
1010
+ attempts.push(outcome);
1011
+ if (!workerOutcomeFailed(outcome)) break;
1012
+ }
1013
+
1014
+ const outcome = attempts[attempts.length - 1];
1015
+ const retryCount = attempts.length - 1;
1016
+ const retryAttempts =
1017
+ retryCount > 0 ? attempts.slice(0, -1).map(describeFailedWorkerAttempt) : null;
1018
+ const childSummary = outcome.payload;
1019
+ if (childSummary?.results) {
1020
+ const workerResults = childSummary.results.map((result) =>
1021
+ retryCount > 0 ? { ...result, retries: retryCount, retryAttempts } : result,
1022
+ );
1023
+ for (const result of workerResults) recordResult(result);
1024
+ }
1025
+
1026
+ if (!childSummary?.results) {
1027
+ const resultPath = `${spec.file}${spec.search || ""}`;
1028
+ const output = [outcome.child.stderr, outcome.child.stdout].filter(Boolean).join("\n").trim();
1029
+ recordResult({
1030
+ file: resultPath,
1031
+ name: "worker process",
1032
+ status: "FAIL",
1033
+ message:
1034
+ output ||
1035
+ outcome.child.error?.message ||
1036
+ `worker exited with status ${outcome.child.status}`,
1037
+ ...(retryCount > 0 ? { retries: retryCount, retryAttempts } : {}),
1038
+ });
1039
+ } else if ((outcome.child.status !== 0 || outcome.child.signal) && childSummary.fail === 0) {
1040
+ const resultPath = `${spec.file}${spec.search || ""}`;
1041
+ recordResult({
1042
+ file: resultPath,
1043
+ name: "worker process",
1044
+ status: "FAIL",
1045
+ message: outcome.child.signal
1046
+ ? `worker terminated by ${outcome.child.signal}`
1047
+ : `worker exited with status ${outcome.child.status}`,
1048
+ ...(retryCount > 0 ? { retries: retryCount, retryAttempts } : {}),
1049
+ });
1050
+ }
1051
+ }
1052
+
1053
+ function recordResult(result) {
1054
+ results.push(result);
1055
+ if (!isWorker && !runInProcess && !listTestsOnly) {
1056
+ streamedResults = true;
1057
+ console.log(formatResultLine(result));
1058
+ }
1059
+ }
1060
+
1061
+ function formatResultLine(result) {
1062
+ const suffix = result.status === "FAIL" ? ` - ${result.message}` : "";
1063
+ const retrySuffix = result.retries ? ` (retried ${result.retries})` : "";
1064
+ return `${result.status} ${result.file} :: ${result.name}${retrySuffix}${suffix}`;
1065
+ }
1066
+
1067
+ function workerOutcomeFailed(outcome) {
1068
+ const childSummary = outcome.payload;
1069
+ if (!childSummary?.results) return true;
1070
+ if (childSummary.results.some((result) => result.status !== "PASS")) return true;
1071
+ return (outcome.child.status !== 0 || outcome.child.signal) && childSummary.fail === 0;
1072
+ }
1073
+
1074
+ function describeFailedWorkerAttempt(outcome) {
1075
+ const childSummary = outcome.payload;
1076
+ const failedResults = childSummary?.results
1077
+ ? childSummary.results
1078
+ .filter((result) => result.status !== "PASS")
1079
+ .slice(0, 5)
1080
+ .map((result) => ({
1081
+ file: result.file,
1082
+ name: result.name,
1083
+ status: result.status,
1084
+ message: result.message,
1085
+ }))
1086
+ : [];
1087
+ const output = [outcome.child.stderr, outcome.child.stdout].filter(Boolean).join("\n").trim();
1088
+ return {
1089
+ exitCode: outcome.child.status,
1090
+ signal: outcome.child.signal,
1091
+ error: outcome.child.error?.message,
1092
+ failures: failedResults,
1093
+ output: output ? truncateForArtifact(output) : undefined,
1094
+ };
1095
+ }
1096
+
1097
+ function truncateForArtifact(value, limit = 4000) {
1098
+ if (value.length <= limit) return value;
1099
+ return `${value.slice(0, 1000)}\n...<truncated>...\n${value.slice(-Math.max(0, limit - 1018))}`;
1100
+ }
1101
+
1102
+ function runWorker(spec, index, extraEnv = {}) {
1103
+ const specFile = makeTempJsonPath(`wpt-spec-${index}`);
1104
+ const resultsFile = makeTempJsonPath(`wpt-results-${index}`);
1105
+ fs.writeFileSync(specFile, `${JSON.stringify(spec)}\n`);
1106
+ try {
1107
+ const child = spawnSync(process.execPath, ["--expose-gc", __filename], {
1108
+ cwd: root,
1109
+ env: {
1110
+ ...process.env,
1111
+ ...extraEnv,
1112
+ WPT_WORKER_SPEC_FILE: specFile,
1113
+ WPT_WORKER_RESULTS: resultsFile,
1114
+ },
1115
+ encoding: "utf8",
1116
+ maxBuffer: 20 * 1024 * 1024,
1117
+ timeout: workerTimeoutMs,
1118
+ });
1119
+
1120
+ let payload = null;
1121
+ if (fs.existsSync(resultsFile)) {
1122
+ payload = JSON.parse(fs.readFileSync(resultsFile, "utf8"));
1123
+ }
1124
+ return { child, payload };
1125
+ } finally {
1126
+ for (const file of [specFile, resultsFile]) {
1127
+ try {
1128
+ fs.unlinkSync(file);
1129
+ } catch {
1130
+ // Best-effort temp cleanup.
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ function writeTestList(tests) {
1137
+ const outputFile = workerResultsFile || path.join(root, "wpt-results.json");
1138
+ fs.writeFileSync(outputFile, `${JSON.stringify({ tests }, null, 2)}\n`);
1139
+ }
1140
+
1141
+ function writeSummary({ quiet = false } = {}) {
1142
+ const summary = {
1143
+ total: results.length,
1144
+ pass: results.filter((result) => result.status === "PASS").length,
1145
+ fail: results.filter((result) => result.status === "FAIL").length,
1146
+ results,
1147
+ };
1148
+
1149
+ const outputFile = workerResultsFile || path.join(root, "wpt-results.json");
1150
+ fs.writeFileSync(outputFile, `${JSON.stringify(summary, null, 2)}\n`);
1151
+
1152
+ if (!quiet) {
1153
+ if (!streamedResults) {
1154
+ for (const result of results) {
1155
+ console.log(formatResultLine(result));
1156
+ }
1157
+ }
1158
+ console.log(`WPT subset: ${summary.pass}/${summary.total} passed`);
1159
+ }
1160
+
1161
+ if (summary.fail > 0) process.exitCode = 1;
1162
+ }
1163
+
1164
+ (async () => {
1165
+ if (runInProcess) {
1166
+ if (listTestsOnly) {
1167
+ const tests = [];
1168
+ for (const spec of specs) {
1169
+ tests.push(...(await runFile(spec)));
1170
+ }
1171
+ writeTestList(tests);
1172
+ return;
1173
+ }
1174
+
1175
+ for (const spec of specs) {
1176
+ await runFile(spec);
1177
+ }
1178
+ writeSummary({ quiet: isWorker });
1179
+ return;
1180
+ }
1181
+
1182
+ if (listTestsOnly) {
1183
+ writeTestList(listIsolatedTests(specs));
1184
+ if (results.length) writeSummary();
1185
+ return;
1186
+ }
1187
+
1188
+ await runIsolated(specs);
1189
+ writeSummary();
1190
+ })().catch((error) => {
1191
+ console.error(error);
1192
+ process.exitCode = 1;
1193
+ });