diff --git a/bin/qunit.js b/bin/qunit.js index 9b6b40ee8..500594fcb 100755 --- a/bin/qunit.js +++ b/bin/qunit.js @@ -39,8 +39,7 @@ program const opts = program.opts(); if (opts.reporter === true) { - const requireQUnit = require('../src/cli/require-qunit'); - displayAvailableReporters(requireQUnit().reporters); + displayAvailableReporters(); } const options = { diff --git a/src/cli/find-reporter.js b/src/cli/find-reporter.js index a76c9f2bf..b0e0da3f9 100644 --- a/src/cli/find-reporter.js +++ b/src/cli/find-reporter.js @@ -25,17 +25,20 @@ function findReporter (reporterName, builtin) { } // If we didn't find a reporter, display the available reporters and exit - displayAvailableReporters(builtin, reporterName); + displayAvailableReporters(reporterName); } -function displayAvailableReporters (builtin, inputReporterName) { +function displayAvailableReporters (inputReporterName) { const message = []; if (inputReporterName) { message.push(`No reporter found matching "${inputReporterName}".`); } - const jsReporters = Object.keys(builtin).sort(); + const jsReporters = [ + 'console', + 'tap' + ].sort(); message.push(`Built-in reporters: ${jsReporters.join(', ')}`); const npmReporters = getReportersFromDependencies(); diff --git a/src/core/utilities.js b/src/core/utilities.js index 9cd8e78b1..a1bbe16b6 100644 --- a/src/core/utilities.js +++ b/src/core/utilities.js @@ -1,45 +1,12 @@ import { window } from '../globals'; -import Logger from '../logger'; export const toString = Object.prototype.toString; export const hasOwn = Object.prototype.hasOwnProperty; export const slice = Array.prototype.slice; -const nativePerf = getNativePerf(); - -// TODO: Consider using globalThis instead so that perf marks work -// in Node.js as well. As they can have overhead, we should also -// have a way to disable these, and/or make them an opt-in reporter -// in QUnit 3 and then support globalThis. -// For example: `QUnit.addReporter(QUnit.reporters.perf)`. -function getNativePerf () { - if (window && - typeof window.performance !== 'undefined' && - typeof window.performance.mark === 'function' && - typeof window.performance.measure === 'function' - ) { - return window.performance; - } else { - return undefined; - } -} - export const performance = { - now: nativePerf - ? nativePerf.now.bind(nativePerf) - : Date.now, - measure: nativePerf - ? function (comment, startMark, endMark) { - // `performance.measure` may fail if the mark could not be found. - // reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()` - try { - nativePerf.measure(comment, startMark, endMark); - } catch (ex) { - Logger.warn('performance.measure could not be executed because of ', ex.message); - } - } - : function () {}, - mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {} + // eslint-disable-next-line compat/compat -- Checked + now: window && window.performance && window.performance.now ? window.performance.now.bind(window.performance) : Date.now }; // Returns a new Array with the elements that are in a but not in b diff --git a/src/html-reporter/html.js b/src/html-reporter/html.js index dd097a34f..bad16362f 100644 --- a/src/html-reporter/html.js +++ b/src/html-reporter/html.js @@ -39,6 +39,8 @@ export function escapeText (str) { return; } + QUnit.reporters.perf.init(QUnit); + const config = QUnit.config; const hiddenTests = []; let collapseNext = false; diff --git a/src/reporters.js b/src/reporters.js index 207c43473..862172d5b 100644 --- a/src/reporters.js +++ b/src/reporters.js @@ -1,7 +1,9 @@ import ConsoleReporter from './reporters/ConsoleReporter.js'; +import PerfReporter from './reporters/PerfReporter.js'; import TapReporter from './reporters/TapReporter.js'; export default { console: ConsoleReporter, + perf: PerfReporter, tap: TapReporter }; diff --git a/src/reporters/PerfReporter.js b/src/reporters/PerfReporter.js new file mode 100644 index 000000000..9a49c7dc0 --- /dev/null +++ b/src/reporters/PerfReporter.js @@ -0,0 +1,92 @@ +import { window } from '../globals'; +import Logger from '../logger'; + +// TODO: Consider using globalThis instead of window, so that the reporter +// works for Node.js as well. As this can add overhead, we should make +// this opt-in before we enable it for CLI. +// +// QUnit 3 will switch from `window` to `globalThis` and then make it +// no longer an implicit feature of the HTML Reporter, but rather let +// it be opt-in via `QUnit.config.reporters = ['perf']` or something +// like that. +const nativePerf = ( + window && + typeof window.performance !== 'undefined' && + // eslint-disable-next-line compat/compat -- Checked + typeof window.performance.mark === 'function' && + // eslint-disable-next-line compat/compat -- Checked + typeof window.performance.measure === 'function' +) + ? window.performance + : undefined; + +const perf = { + measure: nativePerf + ? function (comment, startMark, endMark) { + // `performance.measure` may fail if the mark could not be found. + // reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()` + try { + nativePerf.measure(comment, startMark, endMark); + } catch (ex) { + Logger.warn('performance.measure could not be executed because of ', ex.message); + } + } + : function () {}, + mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {} +}; + +export default class PerfReporter { + constructor (runner, options = {}) { + this.perf = options.perf || perf; + + runner.on('runStart', this.onRunStart.bind(this)); + runner.on('runEnd', this.onRunEnd.bind(this)); + runner.on('suiteStart', this.onSuiteStart.bind(this)); + runner.on('suiteEnd', this.onSuiteEnd.bind(this)); + runner.on('testStart', this.onTestStart.bind(this)); + runner.on('testEnd', this.onTestEnd.bind(this)); + } + + static init (runner, options) { + return new PerfReporter(runner, options); + } + + onRunStart () { + this.perf.mark('qunit_suite_0_start'); + } + + onSuiteStart (suiteStart) { + const suiteLevel = suiteStart.fullName.length; + this.perf.mark(`qunit_suite_${suiteLevel}_start`); + } + + onSuiteEnd (suiteEnd) { + const suiteLevel = suiteEnd.fullName.length; + const suiteName = suiteEnd.fullName.join(' – '); + + this.perf.mark(`qunit_suite_${suiteLevel}_end`); + this.perf.measure(`QUnit Test Suite: ${suiteName}`, + `qunit_suite_${suiteLevel}_start`, + `qunit_suite_${suiteLevel}_end` + ); + } + + onTestStart () { + this.perf.mark('qunit_test_start'); + } + + onTestEnd (testEnd) { + this.perf.mark('qunit_test_end'); + const testName = testEnd.fullName.join(' – '); + + this.perf.measure(`QUnit Test: ${testName}`, + 'qunit_test_start', + 'qunit_test_end' + ); + } + + onRunEnd () { + this.perf.mark('qunit_suite_0_end'); + this.perf.measure('QUnit Test Run', 'qunit_suite_0_start', 'qunit_suite_0_end'); + } +} diff --git a/src/reports/suite.js b/src/reports/suite.js index a2b72289f..dae864303 100644 --- a/src/reports/suite.js +++ b/src/reports/suite.js @@ -21,9 +21,6 @@ export default class SuiteReport { start (recordTime) { if (recordTime) { this._startTime = performance.now(); - - const suiteLevel = this.fullName.length; - performance.mark(`qunit_suite_${suiteLevel}_start`); } return { @@ -40,16 +37,6 @@ export default class SuiteReport { end (recordTime) { if (recordTime) { this._endTime = performance.now(); - - const suiteLevel = this.fullName.length; - const suiteName = this.fullName.join(' – '); - - performance.mark(`qunit_suite_${suiteLevel}_end`); - performance.measure( - suiteLevel === 0 ? 'QUnit Test Run' : `QUnit Test Suite: ${suiteName}`, - `qunit_suite_${suiteLevel}_start`, - `qunit_suite_${suiteLevel}_end` - ); } return { diff --git a/src/reports/test.js b/src/reports/test.js index 980207dd6..b58daf0be 100644 --- a/src/reports/test.js +++ b/src/reports/test.js @@ -22,7 +22,6 @@ export default class TestReport { start (recordTime) { if (recordTime) { this._startTime = performance.now(); - performance.mark('qunit_test_start'); } return { @@ -35,17 +34,6 @@ export default class TestReport { end (recordTime) { if (recordTime) { this._endTime = performance.now(); - if (performance) { - performance.mark('qunit_test_end'); - - const testName = this.fullName.join(' – '); - - performance.measure( - `QUnit Test: ${testName}`, - 'qunit_test_start', - 'qunit_test_end' - ); - } } return extend(this.start(), { diff --git a/test/cli/PerfReporter.js b/test/cli/PerfReporter.js new file mode 100644 index 000000000..c41567eca --- /dev/null +++ b/test/cli/PerfReporter.js @@ -0,0 +1,118 @@ +const { EventEmitter } = require('events'); + +class MockPerf { + constructor () { + this.marks = new Map(); + this.measures = []; + this.clock = 1; + } + + mark (name) { + this.clock++; + this.marks.set(name, this.clock); + } + + measure (name, startMark, endMark) { + const startTime = this.marks.get(startMark); + const endTime = this.marks.get(endMark); + this.measures.push({ name, startTime, endTime }); + this.measures.sort((a, b) => a.startTime - b.startTime); + } +} + +QUnit.module('PerfReporter', hooks => { + let emitter; + let perf; + + hooks.beforeEach(function () { + emitter = new EventEmitter(); + perf = new MockPerf(); + QUnit.reporters.perf.init(emitter, { + perf + }); + }); + + QUnit.test('Flat suites', assert => { + emitter.emit('runStart', {}); + emitter.emit('suiteStart', { fullName: ['Foo'] }); + emitter.emit('testStart', { fullName: ['Foo', 'example'] }); + emitter.emit('testEnd', { fullName: ['Foo', 'example'] }); + emitter.emit('suiteEnd', { fullName: ['Foo'] }); + emitter.emit('suiteStart', { fullName: ['Bar'] }); + emitter.emit('testStart', { fullName: ['Bar', 'example'] }); + emitter.emit('testEnd', { fullName: ['Bar', 'example'] }); + emitter.emit('suiteEnd', { fullName: ['Bar'] }); + emitter.emit('runEnd', {}); + + assert.deepEqual( + perf.measures, + [{ + name: 'QUnit Test Run', + startTime: 2, + endTime: 11 + }, + { + name: 'QUnit Test Suite: Foo', + startTime: 3, + endTime: 6 + }, + { + name: 'QUnit Test: Foo – example', + startTime: 4, + endTime: 5 + }, + { + name: 'QUnit Test Suite: Bar', + startTime: 7, + endTime: 10 + }, + { + name: 'QUnit Test: Bar – example', + startTime: 8, + endTime: 9 + }] + ); + }); + + QUnit.test('Nested suites', assert => { + emitter.emit('runStart', {}); + emitter.emit('suiteStart', { fullName: ['Foo'] }); + emitter.emit('testStart', { fullName: ['Foo', 'one'] }); + emitter.emit('testEnd', { fullName: ['Foo', 'one'] }); + emitter.emit('suiteStart', { fullName: ['Foo', 'Bar'] }); + emitter.emit('testStart', { fullName: ['Foo', 'Bar', 'two'] }); + emitter.emit('testEnd', { fullName: ['Foo', 'Bar', 'two'] }); + emitter.emit('suiteEnd', { fullName: ['Foo', 'Bar'] }); + emitter.emit('suiteEnd', { fullName: ['Fo'] }); + emitter.emit('runEnd', {}); + + assert.deepEqual( + perf.measures, + [{ + name: 'QUnit Test Run', + startTime: 2, + endTime: 11 + }, + { + name: 'QUnit Test Suite: Fo', + startTime: 3, + endTime: 10 + }, + { + name: 'QUnit Test: Foo – one', + startTime: 4, + endTime: 5 + }, + { + name: 'QUnit Test Suite: Foo – Bar', + startTime: 6, + endTime: 9 + }, + { + name: 'QUnit Test: Foo – Bar – two', + startTime: 7, + endTime: 8 + }] + ); + }); +});