From 2a93ad0f968e09f677d4693ab55b4e7803ec4b13 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Fri, 24 Jan 2025 03:25:47 +0000 Subject: [PATCH] CLI: Fix TAP compliance for actual/expected indent and skip/todo colors Cherry-picked from adc449396ab13e2605a1be0ea39fa80bb6f1ec13 (3.0.0-dev). --- package-lock.json | 111 ++++++++++- package.json | 3 +- src/reporters/TapReporter.js | 15 +- test/cli/TapReporter-to-TapParser.js | 212 +++++++++++++++++++++ test/cli/TapReporter.js | 46 ++--- test/cli/fixtures/only-module-flat.tap.txt | 4 +- test/cli/fixtures/only-module.tap.txt | 4 +- test/cli/fixtures/test-if.tap.txt | 8 +- 8 files changed, 365 insertions(+), 38 deletions(-) create mode 100644 test/cli/TapReporter-to-TapParser.js diff --git a/package-lock.json b/package-lock.json index 123901def..5e934ffa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,8 @@ "requirejs": "^2.3.6", "rimraf": "^3.0.2", "rollup": "^2.79.1", - "semver": "^7.6.2" + "semver": "^7.6.2", + "tap-parser": "11.0.2" }, "engines": { "node": ">=10" @@ -4363,6 +4364,12 @@ "node": ">=0.4.x" } }, + "node_modules/events-to-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", + "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", + "dev": true + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -6665,6 +6672,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -8504,6 +8529,32 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/tap-parser": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-11.0.2.tgz", + "integrity": "sha512-6qGlC956rcORw+fg7Fv1iCRAY8/bU9UabUAhs3mXRH6eRmVZcNPLheSXCYaVaYeSwx5xa/1HXZb1537YSvwDZg==", + "dev": true, + "dependencies": { + "events-to-array": "^1.0.1", + "minipass": "^3.1.6", + "tap-yaml": "^1.0.0" + }, + "bin": { + "tap-parser": "bin/cmd.js" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tap-yaml": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.2.tgz", + "integrity": "sha512-GegASpuqBnRNdT1U+yuUPZ8rEU64pL35WPBpCISWwff4dErS2/438barz7WFJl4Nzh3Y05tfPidZnH+GaV1wMg==", + "dev": true, + "dependencies": { + "yaml": "^1.10.2" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -9244,6 +9295,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", @@ -12478,6 +12538,12 @@ "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", "dev": true }, + "events-to-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz", + "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", + "dev": true + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -14182,6 +14248,23 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -15605,6 +15688,26 @@ } } }, + "tap-parser": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-11.0.2.tgz", + "integrity": "sha512-6qGlC956rcORw+fg7Fv1iCRAY8/bU9UabUAhs3mXRH6eRmVZcNPLheSXCYaVaYeSwx5xa/1HXZb1537YSvwDZg==", + "dev": true, + "requires": { + "events-to-array": "^1.0.1", + "minipass": "^3.1.6", + "tap-yaml": "^1.0.0" + } + }, + "tap-yaml": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tap-yaml/-/tap-yaml-1.0.2.tgz", + "integrity": "sha512-GegASpuqBnRNdT1U+yuUPZ8rEU64pL35WPBpCISWwff4dErS2/438barz7WFJl4Nzh3Y05tfPidZnH+GaV1wMg==", + "dev": true, + "requires": { + "yaml": "^1.10.2" + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -16192,6 +16295,12 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, "yargs": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", diff --git a/package.json b/package.json index 63ff8d8c4..baa08d099 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "requirejs": "^2.3.6", "rimraf": "^3.0.2", "rollup": "^2.79.1", - "semver": "^7.6.2" + "semver": "^7.6.2", + "tap-parser": "11.0.2" }, "scripts": { "build": "rollup -c && grunt copy:src-css", diff --git a/src/reporters/TapReporter.js b/src/reporters/TapReporter.js index c3abbcbd3..56467c6f2 100644 --- a/src/reporters/TapReporter.js +++ b/src/reporters/TapReporter.js @@ -35,7 +35,7 @@ import { annotateStacktrace } from '../core/stacktrace'; * Objects with cyclical references will be stringifed as * "[Circular]" as they cannot otherwise be represented. */ -function prettyYamlValue (value, indent = 4) { +function prettyYamlValue (value, indent = 2) { if (value === undefined) { // Not supported in JSON/YAML, turn into string // and let the below output it as bare string. @@ -94,7 +94,7 @@ function prettyYamlValue (value, indent = 4) { // See also // Support IE 9-11: Avoid ES6 String#repeat - const prefix = (new Array(indent + 1)).join(' '); + const prefix = (new Array((indent * 2) + 1)).join(' '); const trailingLinebreakMatch = value.match(/\n+$/); const trailingLinebreaks = trailingLinebreakMatch @@ -126,8 +126,13 @@ function prettyYamlValue (value, indent = 4) { } } + const prefix = (new Array(indent + 1)).join(' '); + // Handle null, boolean, array, and object - return JSON.stringify(decycledShallowClone(value), null, 2); + return JSON.stringify(decycledShallowClone(value), null, 2) + .split('\n') + .map((line, i) => i === 0 ? line : prefix + line) + .join('\n'); } /** @@ -223,11 +228,11 @@ export default class TapReporter { this.log(`ok ${this.testCount} ${test.fullName.join(' > ')}`); } else if (test.status === 'skipped') { this.log( - `ok ${this.testCount} ${kleur.yellow(`# SKIP ${test.fullName.join(' > ')}`)}` + `ok ${this.testCount} ${kleur.yellow(test.fullName.join(' > '))} # SKIP` ); } else if (test.status === 'todo') { this.log( - `not ok ${this.testCount} ${kleur.cyan(`# TODO ${test.fullName.join(' > ')}`)}` + `not ok ${this.testCount} ${kleur.cyan(test.fullName.join(' > '))} # TODO` ); test.errors.forEach((error) => this.logAssertion(error, 'todo')); } else { diff --git a/test/cli/TapReporter-to-TapParser.js b/test/cli/TapReporter-to-TapParser.js new file mode 100644 index 000000000..d605ae207 --- /dev/null +++ b/test/cli/TapReporter-to-TapParser.js @@ -0,0 +1,212 @@ +const { EventEmitter } = require('events'); +const Parser = require('tap-parser'); + +QUnit.module('TapReporter-to-TapParser', hooks => { + let emitter; + let buffer = ''; + + function log (str) { + buffer += str + '\n'; + } + + async function getParseResult () { + const p = new Parser({ passes: true }); + const onComplete = new Promise((resolve) => p.on('complete', resolve)); + // console.log(buffer); // Debugging + p.write(buffer); + p.end(); + return await onComplete; + } + + hooks.beforeEach(function () { + buffer = ''; + emitter = new EventEmitter(); + QUnit.reporters.tap.init(emitter, { + log: log + }); + }); + + QUnit.test('Basic summary', async (assert) => { + emitter.emit('runStart'); + ['example', 'hello', 'world'].forEach((name) => { + emitter.emit('testEnd', { + fullName: [name], + status: 'passed', + runtime: 0, + errors: [], + assertions: [] + }); + }); + emitter.emit('runEnd', { + testCounts: { + total: 3, + passed: 3, + failed: 0, + skipped: 0, + todo: 0 + } + }); + + assert.propContains(await getParseResult(), { + ok: true, + count: 3, + pass: 3, + fail: 0, + todo: 0, + skip: 0, + plan: { + start: 1, + end: 3 + } + }); + }); + + QUnit.test('Basic test failure', async (assert) => { + emitter.emit('runStart'); + emitter.emit('testEnd', { + fullName: ['example'], + status: 'failed', + runtime: 0, + errors: [{ + message: 'equal', + passed: false, + actual: 'the moon', + expected: 'the only light we\'ll see', + stack: ' at /bla.js:3' + }] + }); + emitter.emit('runEnd', { + testCounts: { + total: 1, + passed: 0, + failed: 1, + skipped: 0, + todo: 0 + } + }); + + assert.propContains(await getParseResult(), { + ok: false, + count: 1, + pass: 0, + fail: 1, + todo: 0, + skip: 0, + plan: { + start: 1, + end: 1 + }, + failures: [ + { + ok: false, + name: '\u001b[31mexample\u001b[39m', + diag: { + message: 'equal', + severity: 'failed', + actual: 'the moon', + expected: 'the only light we\'ll see', + stack: 'at /bla.js:3\n' + } + } + ] + }); + }); + + QUnit.test('Deep equal failure', async (assert) => { + emitter.emit('runStart'); + emitter.emit('testEnd', { + fullName: ['example'], + status: 'failed', + runtime: 0, + errors: [{ + message: 'deepEqual', + passed: false, + actual: { the: 'moon' }, + expected: { the: 'only light we\'ll see' }, + stack: ' at /bla.js:3' + }] + }); + emitter.emit('runEnd', { + testCounts: { + total: 1, + passed: 0, + failed: 1, + skipped: 0, + todo: 0 + } + }); + + assert.propContains(await getParseResult(), { + ok: false, + count: 1, + pass: 0, + fail: 1, + todo: 0, + skip: 0, + plan: { + start: 1, + end: 1 + }, + failures: [ + { + ok: false, + name: '\u001b[31mexample\u001b[39m', + diag: { + message: 'deepEqual', + severity: 'failed', + actual: { the: 'moon' }, + expected: { the: 'only light we\'ll see' }, + stack: 'at /bla.js:3\n' + } + } + ] + }); + }); + + QUnit.test('Directives', async (assert) => { + emitter.emit('runStart'); + const tests = { + example: 'passed', + hello: 'skipped', + world: 'todo' + }; + for (const name in tests) { + emitter.emit('testEnd', { + fullName: [name], + status: tests[name], + runtime: 0, + errors: [], + assertions: [] + }); + } + emitter.emit('runEnd', { + testCounts: { + total: 3, + passed: 1, + failed: 0, + skipped: 1, + todo: 1 + } + }); + + assert.propContains(await getParseResult(), { + ok: true, + count: 3, + pass: 2, // tap-parser counts SKIP as both 'skip' and 'pass' + fail: 1, // tap-parser counts TODO as both 'todo' and 'fail' + todo: 1, + skip: 1, + plan: { + start: 1, + end: 3 + }, + passes: [{ + ok: true, + name: 'example' + }, { + ok: true, + name: '\u001b[33mhello\u001b[39m' + }] + }); + }); +}); diff --git a/test/cli/TapReporter.js b/test/cli/TapReporter.js index 72f277272..8a657824e 100644 --- a/test/cli/TapReporter.js +++ b/test/cli/TapReporter.js @@ -66,7 +66,7 @@ QUnit.module('TapReporter', hooks => { }); QUnit.test('output ok for a skipped test', assert => { - const expected = 'ok 1 ' + kleur.yellow('# SKIP name'); + const expected = 'ok 1 ' + kleur.yellow('name') + ' # SKIP'; emitter.emit('testEnd', { name: 'name', @@ -81,7 +81,7 @@ QUnit.module('TapReporter', hooks => { }); QUnit.test('output not ok for a todo test', assert => { - const expected = 'not ok 1 ' + kleur.cyan('# TODO name'); + const expected = 'not ok 1 ' + kleur.cyan('name') + ' # TODO'; emitter.emit('testEnd', { name: 'name', @@ -321,9 +321,9 @@ Bail out! ReferenceError: Boo is not defined message: failed severity: failed actual : { - "a": "example", - "cycle": "[Circular]" -} + "a": "example", + "cycle": "[Circular]" + } expected: expected ...` ); @@ -341,11 +341,11 @@ Bail out! ReferenceError: Boo is not defined message: failed severity: failed actual : { - "a": "example", - "sub": { - "cycle": "[Circular]" + "a": "example", + "sub": { + "cycle": "[Circular]" + } } -} expected: expected ...` ); @@ -363,12 +363,12 @@ Bail out! ReferenceError: Boo is not defined message: failed severity: failed actual : { - "sub": [ - "example", - "[Circular]", - "[Circular]" - ] -} + "sub": [ + "example", + "[Circular]", + "[Circular]" + ] + } expected: expected ...` ); @@ -392,14 +392,14 @@ Bail out! ReferenceError: Boo is not defined message: failed severity: failed actual : { - "a": { - "example": "value" - }, - "b": { - "example": "value" - }, - "c": "unique" -} + "a": { + "example": "value" + }, + "b": { + "example": "value" + }, + "c": "unique" + } expected: expected ...` ); diff --git a/test/cli/fixtures/only-module-flat.tap.txt b/test/cli/fixtures/only-module-flat.tap.txt index 572c377a0..c598fe0e5 100644 --- a/test/cli/fixtures/only-module-flat.tap.txt +++ b/test/cli/fixtures/only-module-flat.tap.txt @@ -1,7 +1,7 @@ # command: ["qunit", "only-module-flat.js"] TAP version 13 -not ok 1 # TODO module B > test B +not ok 1 module B > test B # TODO --- message: not implemented yet severity: todo @@ -10,7 +10,7 @@ not ok 1 # TODO module B > test B stack: | at /qunit/test/cli/fixtures/only-module-flat.js:8:14 ... -ok 2 # SKIP module B > test C +ok 2 module B > test C # SKIP ok 3 module B > test D 1..4 # pass 2 diff --git a/test/cli/fixtures/only-module.tap.txt b/test/cli/fixtures/only-module.tap.txt index bf5803fc7..8ad538d03 100644 --- a/test/cli/fixtures/only-module.tap.txt +++ b/test/cli/fixtures/only-module.tap.txt @@ -2,7 +2,7 @@ # command: ["qunit", "only-module.js"] TAP version 13 -not ok 1 # TODO module B > Only this module should run > a todo test +not ok 1 module B > Only this module should run > a todo test # TODO --- message: not implemented yet severity: todo @@ -11,7 +11,7 @@ not ok 1 # TODO module B > Only this module should run > a todo test stack: | at /qunit/test/cli/fixtures/only-module.js:17:18 ... -ok 2 # SKIP module B > Only this module should run > implicitly skipped test +ok 2 module B > Only this module should run > implicitly skipped test # SKIP ok 3 module B > Only this module should run > normal test ok 4 module D > test D ok 5 module E > module F > test F diff --git a/test/cli/fixtures/test-if.tap.txt b/test/cli/fixtures/test-if.tap.txt index f8e6bedf1..a18272a6b 100644 --- a/test/cli/fixtures/test-if.tap.txt +++ b/test/cli/fixtures/test-if.tap.txt @@ -2,14 +2,14 @@ # command: ["qunit", "test-if.js"] TAP version 13 -ok 1 # SKIP skip me +ok 1 skip me # SKIP ok 2 keep me ok 3 regular -ok 4 # SKIP skip dataset [a] -ok 5 # SKIP skip dataset [b] +ok 4 skip dataset [a] # SKIP +ok 5 skip dataset [b] # SKIP ok 6 keep dataset [a] ok 7 keep dataset [b] -ok 8 # SKIP skip group > skipper +ok 8 skip group > skipper # SKIP ok 9 keep group > keeper 1..9 # pass 5