Skip to content

Commit 33a7878

Browse files
joyeecheungcodebytere
authored andcommittedJun 7, 2020
test: refactor WPTRunner
- Print test results as soon as they are available, instead of until after all the tests are complete. This helps us printing tests whose completion callback is not called because of failures. - Run the scripts specified by `// META: script=` one by one instead of concatenating them first for better error stack traces. - Print a status summary when the test process is about to exit. This can be used as reference for updating the status file. For example the stderr output of `out/Release/node test/wpt/test-console.js` would be: ``` { 'idlharness.any.js': { fail: { expected: [ 'assert_equals: operation has wrong .length expected 1 but got 0' ] } } } Ran 4/4 tests, 0 skipped, 3 passed, 1 expected failures, 0 unexpected failures ``` PR-URL: #33297 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent fa16313 commit 33a7878

File tree

1 file changed

+208
-145
lines changed

1 file changed

+208
-145
lines changed
 

‎test/common/wpt.js

+208-145
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ class ResourceLoader {
4242
this.path = path;
4343
}
4444

45+
toRealFilePath(from, url) {
46+
// We need to patch this to load the WebIDL parser
47+
url = url.replace(
48+
'/resources/WebIDLParser.js',
49+
'/resources/webidl2/lib/webidl2.js'
50+
);
51+
const base = path.dirname(from);
52+
return url.startsWith('/') ?
53+
fixtures.path('wpt', url) :
54+
fixtures.path('wpt', base, url);
55+
}
56+
4557
/**
4658
* Load a resource in test/fixtures/wpt specified with a URL
4759
* @param {string} from the path of the file loading this resource,
@@ -51,15 +63,7 @@ class ResourceLoader {
5163
* pseudo-Response object.
5264
*/
5365
read(from, url, asFetch = true) {
54-
// We need to patch this to load the WebIDL parser
55-
url = url.replace(
56-
'/resources/WebIDLParser.js',
57-
'/resources/webidl2/lib/webidl2.js'
58-
);
59-
const base = path.dirname(from);
60-
const file = url.startsWith('/') ?
61-
fixtures.path('wpt', url) :
62-
fixtures.path('wpt', base, url);
66+
const file = this.toRealFilePath(from, url);
6367
if (asFetch) {
6468
return fsPromises.readFile(file)
6569
.then((data) => {
@@ -135,7 +139,8 @@ class StatusRuleSet {
135139
}
136140
}
137141

138-
class WPTTest {
142+
// A specification of WPT test
143+
class WPTTestSpec {
139144
/**
140145
* @param {string} mod name of the WPT module, e.g.
141146
* 'html/webappapis/microtask-queuing'
@@ -227,8 +232,8 @@ class StatusLoader {
227232
this.path = path;
228233
this.loaded = false;
229234
this.rules = new StatusRuleSet();
230-
/** @type {WPTTest[]} */
231-
this.tests = [];
235+
/** @type {WPTTestSpec[]} */
236+
this.specs = [];
232237
}
233238

234239
/**
@@ -265,15 +270,19 @@ class StatusLoader {
265270
for (const file of list) {
266271
const relativePath = path.relative(subDir, file);
267272
const match = this.rules.match(relativePath);
268-
this.tests.push(new WPTTest(this.path, relativePath, match));
273+
this.specs.push(new WPTTestSpec(this.path, relativePath, match));
269274
}
270275
this.loaded = true;
271276
}
272277
}
273278

274-
const PASSED = 1;
275-
const FAILED = 2;
276-
const SKIPPED = 3;
279+
const kPass = 'pass';
280+
const kFail = 'fail';
281+
const kSkip = 'skip';
282+
const kTimeout = 'timeout';
283+
const kIncomplete = 'incomplete';
284+
const kUncaught = 'uncaught';
285+
const NODE_UNCAUGHT = 100;
277286

278287
class WPTRunner {
279288
constructor(path) {
@@ -286,12 +295,13 @@ class WPTRunner {
286295

287296
this.status = new StatusLoader(path);
288297
this.status.load();
289-
this.tests = new Map(
290-
this.status.tests.map((item) => [item.filename, item])
298+
this.specMap = new Map(
299+
this.status.specs.map((item) => [item.filename, item])
291300
);
292301

293-
this.results = new Map();
302+
this.results = {};
294303
this.inProgress = new Set();
304+
this.unexpectedFailures = [];
295305
}
296306

297307
/**
@@ -328,39 +338,97 @@ class WPTRunner {
328338
// only `subset.any.js` will be run by the runner.
329339
if (process.argv[2]) {
330340
const filename = process.argv[2];
331-
if (!this.tests.has(filename)) {
341+
if (!this.specMap.has(filename)) {
332342
throw new Error(`${filename} not found!`);
333343
}
334-
queue.push(this.tests.get(filename));
344+
queue.push(this.specMap.get(filename));
335345
} else {
336346
queue = this.buildQueue();
337347
}
338348

339-
this.inProgress = new Set(queue.map((item) => item.filename));
340-
341-
for (const test of queue) {
342-
const filename = test.filename;
343-
const content = test.getContent();
344-
const meta = test.title = this.getMeta(content);
345-
346-
const absolutePath = test.getAbsolutePath();
347-
const context = this.generateContext(test);
348-
const relativePath = test.getRelativePath();
349-
const code = this.mergeScripts(relativePath, meta, content);
350-
try {
351-
vm.runInContext(code, context, {
352-
filename: absolutePath
353-
});
354-
} catch (err) {
355-
this.fail(filename, {
356-
name: '',
357-
message: err.message,
358-
stack: inspect(err)
359-
}, 'UNCAUGHT');
360-
this.inProgress.delete(filename);
349+
this.inProgress = new Set(queue.map((spec) => spec.filename));
350+
351+
for (const spec of queue) {
352+
const testFileName = spec.filename;
353+
const content = spec.getContent();
354+
const meta = spec.title = this.getMeta(content);
355+
356+
const absolutePath = spec.getAbsolutePath();
357+
const context = this.generateContext(spec);
358+
const relativePath = spec.getRelativePath();
359+
const scriptsToRun = [];
360+
// Scripts specified with the `// META: script=` header
361+
if (meta.script) {
362+
for (const script of meta.script) {
363+
scriptsToRun.push({
364+
filename: this.resource.toRealFilePath(relativePath, script),
365+
code: this.resource.read(relativePath, script, false)
366+
});
367+
}
368+
}
369+
// The actual test
370+
scriptsToRun.push({
371+
code: content,
372+
filename: absolutePath
373+
});
374+
375+
for (const { code, filename } of scriptsToRun) {
376+
try {
377+
vm.runInContext(code, context, { filename });
378+
} catch (err) {
379+
this.fail(
380+
testFileName,
381+
{
382+
status: NODE_UNCAUGHT,
383+
name: 'evaluation in WPTRunner.runJsTests()',
384+
message: err.message,
385+
stack: inspect(err)
386+
},
387+
kUncaught
388+
);
389+
this.inProgress.delete(filename);
390+
break;
391+
}
361392
}
362393
}
363-
this.tryFinish();
394+
395+
process.on('exit', () => {
396+
const total = this.specMap.size;
397+
if (this.inProgress.size > 0) {
398+
for (const filename of this.inProgress) {
399+
this.fail(filename, { name: 'Unknown' }, kIncomplete);
400+
}
401+
}
402+
inspect.defaultOptions.depth = Infinity;
403+
console.log(this.results);
404+
405+
const failures = [];
406+
let expectedFailures = 0;
407+
let skipped = 0;
408+
for (const key of Object.keys(this.results)) {
409+
const item = this.results[key];
410+
if (item.fail && item.fail.unexpected) {
411+
failures.push(key);
412+
}
413+
if (item.fail && item.fail.expected) {
414+
expectedFailures++;
415+
}
416+
if (item.skip) {
417+
skipped++;
418+
}
419+
}
420+
const ran = total - skipped;
421+
const passed = ran - expectedFailures - failures.length;
422+
console.log(`Ran ${ran}/${total} tests, ${skipped} skipped,`,
423+
`${passed} passed, ${expectedFailures} expected failures,`,
424+
`${failures.length} unexpected failures`);
425+
if (failures.length > 0) {
426+
const file = path.join('test', 'wpt', 'status', `${this.path}.json`);
427+
throw new Error(
428+
`Found ${failures.length} unexpected failures. ` +
429+
`Consider updating ${file} for these files:\n${failures.join('\n')}`);
430+
}
431+
});
364432
}
365433

366434
mock(testfile) {
@@ -410,115 +478,124 @@ class WPTRunner {
410478
sandbox.self = sandbox;
411479
// TODO(joyeecheung): we are not a window - work with the upstream to
412480
// add a new scope for us.
413-
414481
return context;
415482
}
416483

417-
resultCallback(filename, test) {
418-
switch (test.status) {
484+
getTestTitle(filename) {
485+
const spec = this.specMap.get(filename);
486+
const title = spec.meta && spec.meta.title;
487+
return title ? `${filename} : ${title}` : filename;
488+
}
489+
490+
// Map WPT test status to strings
491+
getTestStatus(status) {
492+
switch (status) {
419493
case 1:
420-
this.fail(filename, test, 'FAILURE');
421-
break;
494+
return kFail;
422495
case 2:
423-
this.fail(filename, test, 'TIMEOUT');
424-
break;
496+
return kTimeout;
425497
case 3:
426-
this.fail(filename, test, 'INCOMPLETE');
427-
break;
498+
return kIncomplete;
499+
case NODE_UNCAUGHT:
500+
return kUncaught;
428501
default:
429-
this.succeed(filename, test);
502+
return kPass;
430503
}
431504
}
432505

506+
/**
507+
* Report the status of each specific test case (there could be multiple
508+
* in one test file).
509+
*
510+
* @param {string} filename
511+
* @param {Test} test The Test object returned by WPT harness
512+
*/
513+
resultCallback(filename, test) {
514+
const status = this.getTestStatus(test.status);
515+
const title = this.getTestTitle(filename);
516+
console.log(`---- ${title} ----`);
517+
if (status !== kPass) {
518+
this.fail(filename, test, status);
519+
} else {
520+
this.succeed(filename, test, status);
521+
}
522+
}
523+
524+
/**
525+
* Report the status of each WPT test (one per file)
526+
*
527+
* @param {string} filename
528+
* @param {Test[]} test The Test objects returned by WPT harness
529+
*/
433530
completionCallback(filename, tests, harnessStatus) {
531+
// Treat it like a test case failure
434532
if (harnessStatus.status === 2) {
435-
assert.fail(`test harness timed out in ${filename}`);
533+
const title = this.getTestTitle(filename);
534+
console.log(`---- ${title} ----`);
535+
this.resultCallback(filename, { status: 2, name: 'Unknown' });
436536
}
437537
this.inProgress.delete(filename);
438-
this.tryFinish();
439538
}
440539

441-
tryFinish() {
442-
if (this.inProgress.size > 0) {
443-
return;
540+
addTestResult(filename, item) {
541+
let result = this.results[filename];
542+
if (!result) {
543+
result = this.results[filename] = {};
444544
}
445-
446-
this.reportResults();
447-
}
448-
449-
reportResults() {
450-
const unexpectedFailures = [];
451-
for (const [filename, items] of this.results) {
452-
const test = this.tests.get(filename);
453-
let title = test.meta && test.meta.title;
454-
title = title ? `${filename} : ${title}` : filename;
455-
console.log(`---- ${title} ----`);
456-
for (const item of items) {
457-
switch (item.type) {
458-
case FAILED: {
459-
if (test.failReasons.length) {
460-
console.log(`[EXPECTED_FAILURE] ${item.test.name}`);
461-
console.log(test.failReasons.join('; '));
462-
} else {
463-
console.log(`[UNEXPECTED_FAILURE] ${item.test.name}`);
464-
unexpectedFailures.push([title, filename, item]);
465-
}
466-
break;
467-
}
468-
case PASSED: {
469-
console.log(`[PASSED] ${item.test.name}`);
470-
break;
471-
}
472-
case SKIPPED: {
473-
console.log(`[SKIPPED] ${item.reason}`);
474-
break;
475-
}
476-
}
545+
if (item.status === kSkip) {
546+
// { filename: { skip: 'reason' } }
547+
result[kSkip] = item.reason;
548+
} else {
549+
// { filename: { fail: { expected: [ ... ],
550+
// unexpected: [ ... ] } }}
551+
if (!result[item.status]) {
552+
result[item.status] = {};
477553
}
478-
}
479-
480-
if (unexpectedFailures.length > 0) {
481-
for (const [title, filename, item] of unexpectedFailures) {
482-
console.log(`---- ${title} ----`);
483-
console.log(`[${item.reason}] ${item.test.name}`);
484-
console.log(item.test.message);
485-
console.log(item.test.stack);
486-
const command = `${process.execPath} ${process.execArgv}` +
487-
` ${require.main.filename} ${filename}`;
488-
console.log(`Command: ${command}\n`);
554+
const key = item.expected ? 'expected' : 'unexpected';
555+
if (!result[item.status][key]) {
556+
result[item.status][key] = [];
557+
}
558+
if (result[item.status][key].indexOf(item.reason) === -1) {
559+
result[item.status][key].push(item.reason);
489560
}
490-
assert.fail(`${unexpectedFailures.length} unexpected failures found`);
491-
}
492-
}
493-
494-
addResult(filename, item) {
495-
const result = this.results.get(filename);
496-
if (result) {
497-
result.push(item);
498-
} else {
499-
this.results.set(filename, [item]);
500561
}
501562
}
502563

503-
succeed(filename, test) {
504-
this.addResult(filename, {
505-
type: PASSED,
506-
test
507-
});
564+
succeed(filename, test, status) {
565+
console.log(`[${status.toUpperCase()}] ${test.name}`);
508566
}
509567

510-
fail(filename, test, reason) {
511-
this.addResult(filename, {
512-
type: FAILED,
513-
test,
514-
reason
568+
fail(filename, test, status) {
569+
const spec = this.specMap.get(filename);
570+
const expected = !!(spec.failReasons.length);
571+
if (expected) {
572+
console.log(`[EXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
573+
console.log(spec.failReasons.join('; '));
574+
} else {
575+
console.log(`[UNEXPECTED_FAILURE][${status.toUpperCase()}] ${test.name}`);
576+
}
577+
if (status === kFail || status === kUncaught) {
578+
console.log(test.message);
579+
console.log(test.stack);
580+
}
581+
const command = `${process.execPath} ${process.execArgv}` +
582+
` ${require.main.filename} ${filename}`;
583+
console.log(`Command: ${command}\n`);
584+
this.addTestResult(filename, {
585+
expected,
586+
status: kFail,
587+
reason: test.message || status
515588
});
516589
}
517590

518591
skip(filename, reasons) {
519-
this.addResult(filename, {
520-
type: SKIPPED,
521-
reason: reasons.join('; ')
592+
const title = this.getTestTitle(filename);
593+
console.log(`---- ${title} ----`);
594+
const joinedReasons = reasons.join('; ');
595+
console.log(`[SKIPPED] ${joinedReasons}`);
596+
this.addTestResult(filename, {
597+
status: kSkip,
598+
reason: joinedReasons
522599
});
523600
}
524601

@@ -546,36 +623,22 @@ class WPTRunner {
546623
}
547624
}
548625

549-
mergeScripts(base, meta, content) {
550-
if (!meta.script) {
551-
return content;
552-
}
553-
554-
// only one script
555-
let result = '';
556-
for (const script of meta.script) {
557-
result += this.resource.read(base, script, false);
558-
}
559-
560-
return result + content;
561-
}
562-
563626
buildQueue() {
564627
const queue = [];
565-
for (const test of this.tests.values()) {
566-
const filename = test.filename;
567-
if (test.skipReasons.length > 0) {
568-
this.skip(filename, test.skipReasons);
628+
for (const spec of this.specMap.values()) {
629+
const filename = spec.filename;
630+
if (spec.skipReasons.length > 0) {
631+
this.skip(filename, spec.skipReasons);
569632
continue;
570633
}
571634

572-
const lackingIntl = intlRequirements.isLacking(test.requires);
635+
const lackingIntl = intlRequirements.isLacking(spec.requires);
573636
if (lackingIntl) {
574637
this.skip(filename, [ `requires ${lackingIntl}` ]);
575638
continue;
576639
}
577640

578-
queue.push(test);
641+
queue.push(spec);
579642
}
580643
return queue;
581644
}

0 commit comments

Comments
 (0)
Please sign in to comment.