@@ -6,9 +6,119 @@ const fs = require('fs');
66const fsPromises = fs . promises ;
77const path = require ( 'path' ) ;
88const events = require ( 'events' ) ;
9+ const os = require ( 'os' ) ;
910const { inspect } = require ( 'util' ) ;
1011const { Worker } = require ( 'worker_threads' ) ;
1112
13+ function getBrowserProperties ( ) {
14+ const { node : version } = process . versions ; // e.g. 18.13.0, 20.0.0-nightly202302078e6e215481
15+ const release = / ^ \d + \. \d + \. \d + $ / . test ( version ) ;
16+ const browser = {
17+ browser_channel : release ? 'stable' : 'experimental' ,
18+ browser_version : version ,
19+ } ;
20+
21+ return browser ;
22+ }
23+
24+ /**
25+ * Return one of three expected values
26+ * https://github.com/web-platform-tests/wpt/blob/1c6ff12/tools/wptrunner/wptrunner/tests/test_update.py#L953-L958
27+ */
28+ function getOs ( ) {
29+ switch ( os . type ( ) ) {
30+ case 'Linux' :
31+ return 'linux' ;
32+ case 'Darwin' :
33+ return 'mac' ;
34+ case 'Windows_NT' :
35+ return 'win' ;
36+ default :
37+ throw new Error ( 'Unsupported os.type()' ) ;
38+ }
39+ }
40+
41+ // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3705
42+ function sanitizeUnpairedSurrogates ( str ) {
43+ return str . replace (
44+ / ( [ \ud800 - \udbff ] + ) (? ! [ \udc00 - \udfff ] ) | ( ^ | [ ^ \ud800 - \udbff ] ) ( [ \udc00 - \udfff ] + ) / g,
45+ function ( _ , low , prefix , high ) {
46+ let output = prefix || '' ; // Prefix may be undefined
47+ const string = low || high ; // Only one of these alternates can match
48+ for ( let i = 0 ; i < string . length ; i ++ ) {
49+ output += codeUnitStr ( string [ i ] ) ;
50+ }
51+ return output ;
52+ } ) ;
53+ }
54+
55+ function codeUnitStr ( char ) {
56+ return 'U+' + char . charCodeAt ( 0 ) . toString ( 16 ) ;
57+ }
58+
59+ class WPTReport {
60+ constructor ( ) {
61+ this . results = [ ] ;
62+ this . time_start = Date . now ( ) ;
63+ }
64+
65+ addResult ( name , status ) {
66+ const result = {
67+ test : name ,
68+ status,
69+ subtests : [ ] ,
70+ addSubtest ( name , status , message ) {
71+ const subtest = {
72+ status,
73+ // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L3722
74+ name : sanitizeUnpairedSurrogates ( name ) ,
75+ } ;
76+ if ( message ) {
77+ // https://github.com/web-platform-tests/wpt/blob/b24eedd/resources/testharness.js#L4506
78+ subtest . message = sanitizeUnpairedSurrogates ( message ) ;
79+ }
80+ this . subtests . push ( subtest ) ;
81+ return subtest ;
82+ } ,
83+ } ;
84+ this . results . push ( result ) ;
85+ return result ;
86+ }
87+
88+ write ( ) {
89+ this . time_end = Date . now ( ) ;
90+ this . results = this . results . filter ( ( result ) => {
91+ return result . status === 'SKIP' || result . subtests . length !== 0 ;
92+ } ) . map ( ( result ) => {
93+ const url = new URL ( result . test , 'http://wpt' ) ;
94+ url . pathname = url . pathname . replace ( / \. j s $ / , '.html' ) ;
95+ result . test = url . href . slice ( url . origin . length ) ;
96+ return result ;
97+ } ) ;
98+
99+ if ( fs . existsSync ( 'out/wpt/wptreport.json' ) ) {
100+ const prev = JSON . parse ( fs . readFileSync ( 'out/wpt/wptreport.json' ) ) ;
101+ this . results = [ ...prev . results , ...this . results ] ;
102+ this . time_start = prev . time_start ;
103+ this . time_end = Math . max ( this . time_end , prev . time_end ) ;
104+ this . run_info = prev . run_info ;
105+ } else {
106+ /**
107+ * Return required and some optional properties
108+ * https://github.com/web-platform-tests/wpt.fyi/blob/60da175/api/README.md?plain=1#L331-L335
109+ */
110+ this . run_info = {
111+ product : 'node.js' ,
112+ ...getBrowserProperties ( ) ,
113+ revision : process . env . WPT_REVISION || 'unknown' ,
114+ os : getOs ( ) ,
115+ } ;
116+ }
117+
118+ fs . writeFileSync ( 'out/wpt/wptreport.json' , JSON . stringify ( this ) ) ;
119+ }
120+ }
121+
12122// https://github.com/web-platform-tests/wpt/blob/HEAD/resources/testharness.js
13123// TODO: get rid of this half-baked harness in favor of the one
14124// pulled from WPT
@@ -313,6 +423,10 @@ class WPTRunner {
313423 this . unexpectedFailures = [ ] ;
314424
315425 this . scriptsModifier = null ;
426+
427+ if ( process . env . WPT_REPORT != null ) {
428+ this . report = new WPTReport ( ) ;
429+ }
316430 }
317431
318432 /**
@@ -339,18 +453,27 @@ class WPTRunner {
339453 this . scriptsModifier = modifier ;
340454 }
341455
342- get fullInitScript ( ) {
456+ fullInitScript ( hasSubsetScript , locationSearchString ) {
457+ let { initScript } = this ;
458+ if ( hasSubsetScript || locationSearchString ) {
459+ initScript = `${ initScript } \n\n//===\nglobalThis.location ||= {};` ;
460+ }
461+
462+ if ( locationSearchString ) {
463+ initScript = `${ initScript } \n\n//===\nglobalThis.location.search = "${ locationSearchString } ";` ;
464+ }
465+
343466 if ( this . globalThisInitScripts . length === null ) {
344- return this . initScript ;
467+ return initScript ;
345468 }
346469
347470 const globalThisInitScript = this . globalThisInitScripts . join ( '\n\n//===\n' ) ;
348471
349- if ( this . initScript === null ) {
472+ if ( initScript === null ) {
350473 return globalThisInitScript ;
351474 }
352475
353- return `${ globalThisInitScript } \n\n//===\n${ this . initScript } ` ;
476+ return `${ globalThisInitScript } \n\n//===\n${ initScript } ` ;
354477 }
355478
356479 /**
@@ -455,15 +578,20 @@ class WPTRunner {
455578 for ( const spec of queue ) {
456579 const testFileName = spec . filename ;
457580 const content = spec . getContent ( ) ;
458- const meta = spec . title = this . getMeta ( content ) ;
581+ const meta = spec . meta = this . getMeta ( content ) ;
459582
460583 const absolutePath = spec . getAbsolutePath ( ) ;
461584 const relativePath = spec . getRelativePath ( ) ;
462585 const harnessPath = fixtures . path ( 'wpt' , 'resources' , 'testharness.js' ) ;
463586 const scriptsToRun = [ ] ;
587+ let hasSubsetScript = false ;
588+
464589 // Scripts specified with the `// META: script=` header
465590 if ( meta . script ) {
466591 for ( const script of meta . script ) {
592+ if ( script === '/common/subset-tests.js' || script === '/common/subset-tests-by-key.js' ) {
593+ hasSubsetScript = true ;
594+ }
467595 const obj = {
468596 filename : this . resource . toRealFilePath ( relativePath , script ) ,
469597 code : this . resource . read ( relativePath , script , false ) ,
@@ -480,54 +608,65 @@ class WPTRunner {
480608 this . scriptsModifier ?. ( obj ) ;
481609 scriptsToRun . push ( obj ) ;
482610
483- const workerPath = path . join ( __dirname , 'wpt/worker.js' ) ;
484- const worker = new Worker ( workerPath , {
485- execArgv : this . flags ,
486- workerData : {
487- testRelativePath : relativePath ,
488- wptRunner : __filename ,
489- wptPath : this . path ,
490- initScript : this . fullInitScript ,
491- harness : {
492- code : fs . readFileSync ( harnessPath , 'utf8' ) ,
493- filename : harnessPath ,
611+ /**
612+ * Example test with no META variant
613+ * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/sign_verify/hmac.https.any.js#L1-L4
614+ *
615+ * Example test with multiple META variants
616+ * https://github.com/nodejs/node/blob/03854f6/test/fixtures/wpt/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js#L1-L9
617+ */
618+ for ( const variant of meta . variant || [ '' ] ) {
619+ const workerPath = path . join ( __dirname , 'wpt/worker.js' ) ;
620+ const worker = new Worker ( workerPath , {
621+ execArgv : this . flags ,
622+ workerData : {
623+ testRelativePath : relativePath ,
624+ wptRunner : __filename ,
625+ wptPath : this . path ,
626+ initScript : this . fullInitScript ( hasSubsetScript , variant ) ,
627+ harness : {
628+ code : fs . readFileSync ( harnessPath , 'utf8' ) ,
629+ filename : harnessPath ,
630+ } ,
631+ scriptsToRun,
494632 } ,
495- scriptsToRun ,
496- } ,
497- } ) ;
498- this . workers . set ( testFileName , worker ) ;
499-
500- worker . on ( 'message' , ( message ) => {
501- switch ( message . type ) {
502- case 'result' :
503- return this . resultCallback ( testFileName , message . result ) ;
504- case 'completion' :
505- return this . completionCallback ( testFileName , message . status ) ;
506- default :
507- throw new Error ( `Unexpected message from worker: ${ message . type } ` ) ;
508- }
509- } ) ;
633+ } ) ;
634+ this . workers . set ( testFileName , worker ) ;
635+
636+ let reportResult ;
637+ worker . on ( 'message' , ( message ) => {
638+ switch ( message . type ) {
639+ case 'result' :
640+ reportResult ||= this . report ?. addResult ( `/ ${ relativePath } ${ variant } ` , 'OK' ) ;
641+ return this . resultCallback ( testFileName , message . result , reportResult ) ;
642+ case 'completion' :
643+ return this . completionCallback ( testFileName , message . status ) ;
644+ default :
645+ throw new Error ( `Unexpected message from worker: ${ message . type } ` ) ;
646+ }
647+ } ) ;
510648
511- worker . on ( 'error' , ( err ) => {
512- if ( ! this . inProgress . has ( testFileName ) ) {
513- // The test is already finished. Ignore errors that occur after it.
514- // This can happen normally, for example in timers tests.
515- return ;
516- }
517- this . fail (
518- testFileName ,
519- {
520- status : NODE_UNCAUGHT ,
521- name : 'evaluation in WPTRunner.runJsTests()' ,
522- message : err . message ,
523- stack : inspect ( err ) ,
524- } ,
525- kUncaught ,
526- ) ;
527- this . inProgress . delete ( testFileName ) ;
528- } ) ;
649+ worker . on ( 'error' , ( err ) => {
650+ if ( ! this . inProgress . has ( testFileName ) ) {
651+ // The test is already finished. Ignore errors that occur after it.
652+ // This can happen normally, for example in timers tests.
653+ return ;
654+ }
655+ this . fail (
656+ testFileName ,
657+ {
658+ status : NODE_UNCAUGHT ,
659+ name : 'evaluation in WPTRunner.runJsTests()' ,
660+ message : err . message ,
661+ stack : inspect ( err ) ,
662+ } ,
663+ kUncaught ,
664+ ) ;
665+ this . inProgress . delete ( testFileName ) ;
666+ } ) ;
529667
530- await events . once ( worker , 'exit' ) . catch ( ( ) => { } ) ;
668+ await events . once ( worker , 'exit' ) . catch ( ( ) => { } ) ;
669+ }
531670 }
532671
533672 process . on ( 'exit' , ( ) => {
@@ -587,6 +726,8 @@ class WPTRunner {
587726 }
588727 }
589728
729+ this . report ?. write ( ) ;
730+
590731 const ran = queue . length ;
591732 const total = ran + skipped ;
592733 const passed = ran - expectedFailures - failures . length ;
@@ -611,8 +752,7 @@ class WPTRunner {
611752
612753 getTestTitle ( filename ) {
613754 const spec = this . specMap . get ( filename ) ;
614- const title = spec . meta && spec . meta . title ;
615- return title ? `${ filename } : ${ title } ` : filename ;
755+ return spec . meta ?. title || filename ;
616756 }
617757
618758 // Map WPT test status to strings
@@ -638,14 +778,14 @@ class WPTRunner {
638778 * @param {string } filename
639779 * @param {Test } test The Test object returned by WPT harness
640780 */
641- resultCallback ( filename , test ) {
781+ resultCallback ( filename , test , reportResult ) {
642782 const status = this . getTestStatus ( test . status ) ;
643783 const title = this . getTestTitle ( filename ) ;
644784 console . log ( `---- ${ title } ----` ) ;
645785 if ( status !== kPass ) {
646- this . fail ( filename , test , status ) ;
786+ this . fail ( filename , test , status , reportResult ) ;
647787 } else {
648- this . succeed ( filename , test , status ) ;
788+ this . succeed ( filename , test , status , reportResult ) ;
649789 }
650790 }
651791
@@ -693,11 +833,12 @@ class WPTRunner {
693833 }
694834 }
695835
696- succeed ( filename , test , status ) {
836+ succeed ( filename , test , status , reportResult ) {
697837 console . log ( `[${ status . toUpperCase ( ) } ] ${ test . name } ` ) ;
838+ reportResult ?. addSubtest ( test . name , 'PASS' ) ;
698839 }
699840
700- fail ( filename , test , status ) {
841+ fail ( filename , test , status , reportResult ) {
701842 const spec = this . specMap . get ( filename ) ;
702843 const expected = spec . failedTests . includes ( test . name ) ;
703844 if ( expected ) {
@@ -713,6 +854,9 @@ class WPTRunner {
713854 const command = `${ process . execPath } ${ process . execArgv } ` +
714855 ` ${ require . main . filename } ${ filename } ` ;
715856 console . log ( `Command: ${ command } \n` ) ;
857+
858+ reportResult ?. addSubtest ( test . name , 'FAIL' , test . message ) ;
859+
716860 this . addTestResult ( filename , {
717861 name : test . name ,
718862 expected,
@@ -742,7 +886,7 @@ class WPTRunner {
742886 const parts = match . match ( / \/ \/ M E T A : ( [ ^ = ] + ?) = ( .+ ) / ) ;
743887 const key = parts [ 1 ] ;
744888 const value = parts [ 2 ] ;
745- if ( key === 'script' ) {
889+ if ( key === 'script' || key === 'variant' ) {
746890 if ( result [ key ] ) {
747891 result [ key ] . push ( value ) ;
748892 } else {
0 commit comments