Skip to content

Commit

Permalink
Reporters: Copy TapReporter and ConsoleReporter from js-reporters 2.0.0
Browse files Browse the repository at this point in the history
The js-reporters project is slowly coming to an and per
qunitjs/js-reporters#133. I expect
going forward it will continue to be maintained for some time to
provide TAP streams from older versions of testing frameworks.

However, all major testing frameworks now have built-in or mature
plugins for TAP output. As such, I'd prefer to maintain TapReporter
directly in the QUnit repository.

Co-authored-by: Florentin Simion <[email protected]>
Co-authored-by: Franziska Carstens <[email protected]>
Co-authored-by: Martin Olsson <[email protected]>
Co-authored-by: Robert Jackson <[email protected]>
Co-authored-by: Timo Tijhof <[email protected]>
Co-authored-by: Trent Willis <[email protected]>
Co-authored-by: Zachary Mulgrew <[email protected]>
Co-authored-by: jeberger <[email protected]>
  • Loading branch information
8 people committed May 1, 2021
1 parent bb95acf commit c52eb91
Show file tree
Hide file tree
Showing 9 changed files with 725 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[test/cli/TapReporter.js]
trim_trailing_whitespace = false

[*.{yml,md}]
indent_style = space
indent_size = 2
8 changes: 7 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"grunt-eslint": "^23.0.0",
"grunt-git-authors": "^3.2.0",
"grunt-search": "^0.1.8",
"kleur": "4.1.4",
"npm-reporter": "file:./test/cli/fixtures/npm-reporter",
"nyc": "^15.1.0",
"proxyquire": "^1.8.0",
Expand Down
2 changes: 2 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Assert from "./assert";
import Logger from "./logger";
import Test, { test, pushFailure } from "./test";
import exportQUnit from "./export";
import reporters from "./reporters";

import config from "./core/config";
import { extend, objectType, is, now } from "./core/utilities";
Expand Down Expand Up @@ -42,6 +43,7 @@ extend( QUnit, {

dump,
equiv,
reporters,
is,
objectType,
on,
Expand Down
7 changes: 7 additions & 0 deletions src/reporters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ConsoleReporter from "./reporters/ConsoleReporter.js";
import TapReporter from "./reporters/TapReporter.js";

export default {
console: ConsoleReporter,
tap: TapReporter
};
36 changes: 36 additions & 0 deletions src/reporters/ConsoleReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { console } from "../globals";

export default class ConsoleReporter {
constructor( runner, options = {} ) {

// Cache references to console methods to ensure we can report failures
// from tests tests that mock the console object itself.
// https://github.com/js-reporters/js-reporters/issues/125
this.log = options.log || console.log.bind( console );

runner.on( "runStart", this.onRunStart.bind( this ) );
runner.on( "testStart", this.onTestStart.bind( this ) );
runner.on( "testEnd", this.onTestEnd.bind( this ) );
runner.on( "runEnd", this.onRunEnd.bind( this ) );
}

static init( runner, options ) {
return new ConsoleReporter( runner, options );
}

onRunStart( runStart ) {
this.log( "runStart", runStart );
}

onTestStart( test ) {
this.log( "testStart", test );
}

onTestEnd( test ) {
this.log( "testEnd", test );
}

onRunEnd( runEnd ) {
this.log( "runEnd", runEnd );
}
}
254 changes: 254 additions & 0 deletions src/reporters/TapReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import kleur from "kleur";
import { console } from "../globals";
const hasOwn = Object.hasOwnProperty;

/**
* Format a given value into YAML.
*
* YAML is a superset of JSON that supports all the same data
* types and syntax, and more. As such, it is always possible
* to fallback to JSON.stringfify, but we generally avoid
* that to make output easier to read for humans.
*
* Supported data types:
*
* - null
* - boolean
* - number
* - string
* - array
* - object
*
* Anything else (including NaN, Infinity, and undefined)
* must be described in strings, for display purposes.
*
* Note that quotes are optional in YAML strings if the
* strings are "simple", and as such we generally prefer
* that for improved readability. We output strings in
* one of three ways:
*
* - bare unquoted text, for simple one-line strings.
* - JSON (quoted text), for complex one-line strings.
* - YAML Block, for complex multi-line strings.
*
* Objects with cyclical references will be stringifed as
* "[Circular]" as they cannot otherwise be represented.
*/
function prettyYamlValue( value, indent = 4 ) {
if ( value === undefined ) {

// Not supported in JSON/YAML, turn into string
// and let the below output it as bare string.
value = String( value );
}

// Support IE 9-11: Use isFinite instead of ES6 Number.isFinite
if ( typeof value === "number" && !isFinite( value ) ) {

// Turn NaN and Infinity into simple strings.
// Paranoia: Don't return directly just in case there's
// a way to add special characters here.
value = String( value );
}

if ( typeof value === "number" ) {

// Simple numbers
return JSON.stringify( value );
}

if ( typeof value === "string" ) {

// If any of these match, then we can't output it
// as bare unquoted text, because that would either
// cause data loss or invalid YAML syntax.
//
// - Quotes, escapes, line breaks, or JSON-like stuff.
const rSpecialJson = /['"\\/[{}\]\r\n]/;

// - Characters that are special at the start of a YAML value
const rSpecialYaml = /[-?:,[\]{}#&*!|=>'"%@`]/;

// - Leading or trailing whitespace.
const rUntrimmed = /(^\s|\s$)/;

// - Ambiguous as YAML number, e.g. '2', '-1.2', '.2', or '2_000'
const rNumerical = /^[\d._-]+$/;

// - Ambiguous as YAML bool.
// Use case-insensitive match, although technically only
// fully-lower, fully-upper, or uppercase-first would be ambiguous.
// e.g. true/True/TRUE, but not tRUe.
const rBool = /^(true|false|y|n|yes|no|on|off)$/i;

// Is this a complex string?
if (
value === "" ||
rSpecialJson.test( value ) ||
rSpecialYaml.test( value[ 0 ] ) ||
rUntrimmed.test( value ) ||
rNumerical.test( value ) ||
rBool.test( value )
) {
if ( !/\n/.test( value ) ) {

// Complex one-line string, use JSON (quoted string)
return JSON.stringify( value );
}

// See also <https://yaml-multiline.info/>
// Support IE 9-11: Avoid ES6 String#repeat
const prefix = ( new Array( indent + 1 ) ).join( " " );

const trailingLinebreakMatch = value.match( /\n+$/ );
const trailingLinebreaks = trailingLinebreakMatch ?
trailingLinebreakMatch[ 0 ].length : 0;

if ( trailingLinebreaks === 1 ) {

// Use the most straight-forward "Block" string in YAML
// without any "Chomping" indicators.
const lines = value

// Ignore the last new line, since we'll get that one for free
// with the straight-forward Block syntax.
.replace( /\n$/, "" )
.split( "\n" )
.map( line => prefix + line );
return "|\n" + lines.join( "\n" );
} else {

// This has either no trailing new lines, or more than 1.
// Use |+ so that YAML parsers will preserve it exactly.
const lines = value
.split( "\n" )
.map( line => prefix + line );
return "|+\n" + lines.join( "\n" );
}
} else {

// Simple string, use bare unquoted text
return value;
}
}

// Handle null, boolean, array, and object
return JSON.stringify( decycledShallowClone( value ), null, 2 );
}

/**
* Creates a shallow clone of an object where cycles have
* been replaced with "[Circular]".
*/
function decycledShallowClone( object, ancestors = [] ) {
if ( ancestors.indexOf( object ) !== -1 ) {
return "[Circular]";
}

let clone;

const type = Object.prototype.toString
.call( object )
.replace( /^\[.+\s(.+?)]$/, "$1" )
.toLowerCase();

switch ( type ) {
case "array":
ancestors.push( object );
clone = object.map( function( element ) {
return decycledShallowClone( element, ancestors );
} );
ancestors.pop();
break;
case "object":
ancestors.push( object );
clone = {};
Object.keys( object ).forEach( function( key ) {
clone[ key ] = decycledShallowClone( object[ key ], ancestors );
} );
ancestors.pop();
break;
default:
clone = object;
}

return clone;
}

export default class TapReporter {
constructor( runner, options = {} ) {

// Cache references to console methods to ensure we can report failures
// from tests tests that mock the console object itself.
// https://github.com/js-reporters/js-reporters/issues/125
this.log = options.log || console.log.bind( console );

this.testCount = 0;

runner.on( "runStart", this.onRunStart.bind( this ) );
runner.on( "testEnd", this.onTestEnd.bind( this ) );
runner.on( "runEnd", this.onRunEnd.bind( this ) );
}

static init( runner, options ) {
return new TapReporter( runner, options );
}

onRunStart( _globalSuite ) {
this.log( "TAP version 13" );
}

onTestEnd( test ) {
this.testCount = this.testCount + 1;

if ( test.status === "passed" ) {
this.log( `ok ${this.testCount} ${test.fullName.join( " > " )}` );
} else if ( test.status === "skipped" ) {
this.log(
kleur.yellow( `ok ${this.testCount} # SKIP ${test.fullName.join( " > " )}` )
);
} else if ( test.status === "todo" ) {
this.log(
kleur.cyan( `not ok ${this.testCount} # TODO ${test.fullName.join( " > " )}` )
);
test.errors.forEach( ( error ) => this.logError( error, "todo" ) );
} else {
this.log(
kleur.red( `not ok ${this.testCount} ${test.fullName.join( " > " )}` )
);
test.errors.forEach( ( error ) => this.logError( error ) );
}
}

onRunEnd( globalSuite ) {
this.log( `1..${globalSuite.testCounts.total}` );
this.log( `# pass ${globalSuite.testCounts.passed}` );
this.log( kleur.yellow( `# skip ${globalSuite.testCounts.skipped}` ) );
this.log( kleur.cyan( `# todo ${globalSuite.testCounts.todo}` ) );
this.log( kleur.red( `# fail ${globalSuite.testCounts.failed}` ) );
}

logError( error, severity ) {
let out = " ---";
out += `\n message: ${prettyYamlValue( error.message || "failed" )}`;
out += `\n severity: ${prettyYamlValue( severity || "failed" )}`;

if ( hasOwn.call( error, "actual" ) ) {
out += `\n actual : ${prettyYamlValue( error.actual )}`;
}

if ( hasOwn.call( error, "expected" ) ) {
out += `\n expected: ${prettyYamlValue( error.expected )}`;
}

if ( error.stack ) {

// Since stacks aren't user generated, take a bit of liberty by
// adding a trailing new line to allow a straight-forward YAML Blocks.
out += `\n stack: ${prettyYamlValue( error.stack + "\n" )}`;
}

out += "\n ...";
this.log( out );
}
}
37 changes: 37 additions & 0 deletions test/cli/ConsoleReporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const { EventEmitter } = require( "events" );

QUnit.module( "ConsoleReporter", hooks => {
let emitter;
let callCount;

hooks.beforeEach( function() {
emitter = new EventEmitter();
callCount = 0;
const con = {
log: () => {
callCount++;
}
};
QUnit.reporters.console.init( emitter, con );
} );

QUnit.test( "Event \"runStart\"", assert => {
emitter.emit( "runStart", {} );
assert.equal( callCount, 1 );
} );

QUnit.test( "Event \"runEnd\"", assert => {
emitter.emit( "runEnd", {} );
assert.equal( callCount, 1 );
} );

QUnit.test( "Event \"testStart\"", assert => {
emitter.emit( "testStart", {} );
assert.equal( callCount, 1 );
} );

QUnit.test( "Event \"testEnd\"", assert => {
emitter.emit( "testEnd", {} );
assert.equal( callCount, 1 );
} );
} );
Loading

0 comments on commit c52eb91

Please sign in to comment.