diff --git a/src/core.js b/src/core.js index 12e7a72a9..5146df044 100644 --- a/src/core.js +++ b/src/core.js @@ -31,6 +31,8 @@ const QUnit = {}; // rather than partly in config.js and partly here. config.currentModule.suiteReport = runSuite; +config.pq = new ProcessingQueue(test); + let globalStartCalled = false; let runStarted = false; @@ -157,7 +159,7 @@ function scheduleBegin () { function unblockAndAdvanceQueue () { config.blocking = false; - ProcessingQueue.advance(); + config.pq.advance(); } export function begin () { diff --git a/src/core/config.js b/src/core/config.js index 1a303585d..e601211af 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -99,6 +99,9 @@ const config = { // Ref https://github.com/qunitjs/qunit/pull/1598 globalHooks: {}, + // Internal: ProcessingQueue singleton, created in /src/core.js + pq: null, + // Internal state blocking: true, callbacks: {}, diff --git a/src/core/processing-queue.js b/src/core/processing-queue.js index 4e15ba16c..6306ee8f4 100644 --- a/src/core/processing-queue.js +++ b/src/core/processing-queue.js @@ -3,219 +3,222 @@ import { extend, generateHash, performance } from './utilities'; import { runLoggingCallbacks } from './logging'; import Promise from '../promise'; -import { test } from '../test'; import { runSuite } from '../module'; import { emit } from '../events'; import { setTimeout } from '../globals'; -let priorityCount = 0; -let unitSampler; - -// This is a queue of functions that are tasks within a single test. -// After tests are dequeued from config.queue they are expanded into -// a set of tasks in this queue. -const taskQueue = []; - /** - * Advances the taskQueue to the next task. If the taskQueue is empty, - * process the testQueue + * Creates a seeded "sample" generator which is used for randomizing tests. */ -function advance () { - advanceTaskQueue(); +function unitSamplerGenerator (seed) { + // 32-bit xorshift, requires only a nonzero seed + // https://excamera.com/sphinx/article-xorshift.html + let sample = parseInt(generateHash(seed), 16) || -1; + return function () { + sample ^= sample << 13; + sample ^= sample >>> 17; + sample ^= sample << 5; - if (!taskQueue.length && !config.blocking && !config.current) { - advanceTestQueue(); - } -} + // ECMAScript has no unsigned number type + if (sample < 0) { + sample += 0x100000000; + } -/** - * Advances the taskQueue with an increased depth - */ -function advanceTaskQueue () { - const start = performance.now(); - config.depth = (config.depth || 0) + 1; + return sample / 0x100000000; + }; +} - processTaskQueue(start); +class ProcessingQueue { + /** + * @param {Function} test Reference to the QUnit.test() method + */ + constructor (test) { + this.test = test; + this.priorityCount = 0; + this.unitSampler = null; + + // This is a queue of functions that are tasks within a single test. + // After tests are dequeued from config.queue they are expanded into + // a set of tasks in this queue. + this.taskQueue = []; + + this.finished = false; + } - config.depth--; -} + /** + * Advances the taskQueue to the next task. If the taskQueue is empty, + * process the testQueue + */ + advance () { + this.advanceTaskQueue(); -/** - * Process the first task on the taskQueue as a promise. - * Each task is a function added by Test#queue() in /src/test.js - */ -function processTaskQueue (start) { - if (taskQueue.length && !config.blocking) { - const elapsedTime = performance.now() - start; - - // The updateRate ensures that a user interface (HTML Reporter) can be updated - // at least once every second. This can also prevent browsers from prompting - // a warning about long running scripts. - if (!setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) { - const task = taskQueue.shift(); - Promise.resolve(task()).then(function () { - if (!taskQueue.length) { - advance(); - } else { - processTaskQueue(start); - } - }); - } else { - setTimeout(advance); + if (!this.taskQueue.length && !config.blocking && !config.current) { + this.advanceTestQueue(); } } -} -/** - * Advance the testQueue to the next test to process. Call done() if testQueue completes. - */ -function advanceTestQueue () { - if (!config.blocking && !config.queue.length && config.depth === 0) { - done(); - return; - } + /** + * Advances the taskQueue with an increased depth + */ + advanceTaskQueue () { + const start = performance.now(); + config.depth = (config.depth || 0) + 1; - const testTasks = config.queue.shift(); - addToTaskQueue(testTasks()); + this.processTaskQueue(start); - if (priorityCount > 0) { - priorityCount--; + config.depth--; } - advance(); -} + /** + * Process the first task on the taskQueue as a promise. + * Each task is a function added by Test#queue() in /src/test.js + */ + processTaskQueue (start) { + if (this.taskQueue.length && !config.blocking) { + const elapsedTime = performance.now() - start; + + // The updateRate ensures that a user interface (HTML Reporter) can be updated + // at least once every second. This can also prevent browsers from prompting + // a warning about long running scripts. + if (!setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) { + const task = this.taskQueue.shift(); + Promise.resolve(task()).then(() => { + if (!this.taskQueue.length) { + this.advance(); + } else { + this.processTaskQueue(start); + } + }); + } else { + setTimeout(() => { + this.advance(); + }); + } + } + } -/** - * Enqueue the tasks for a test into the task queue. - * @param {Array} tasksArray - */ -function addToTaskQueue (tasksArray) { - taskQueue.push(...tasksArray); -} + /** + * Advance the testQueue to the next test to process. Call done() if testQueue completes. + */ + advanceTestQueue () { + if (!config.blocking && !config.queue.length && config.depth === 0) { + this.done(); + return; + } -/** - * Return the number of tasks remaining in the task queue to be processed. - * @return {number} - */ -function taskQueueLength () { - return taskQueue.length; -} + const testTasks = config.queue.shift(); + this.addToTaskQueue(testTasks()); -/** - * Adds a test to the TestQueue for execution. - * @param {Function} testTasksFunc - * @param {boolean} prioritize - * @param {string} seed - */ -function addToTestQueue (testTasksFunc, prioritize, seed) { - if (prioritize) { - config.queue.splice(priorityCount++, 0, testTasksFunc); - } else if (seed) { - if (!unitSampler) { - unitSampler = unitSamplerGenerator(seed); + if (this.priorityCount > 0) { + this.priorityCount--; } - // Insert into a random position after all prioritized items - const index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); - config.queue.splice(priorityCount + index, 0, testTasksFunc); - } else { - config.queue.push(testTasksFunc); + this.advance(); } -} -/** - * Creates a seeded "sample" generator which is used for randomizing tests. - */ -function unitSamplerGenerator (seed) { - // 32-bit xorshift, requires only a nonzero seed - // https://excamera.com/sphinx/article-xorshift.html - let sample = parseInt(generateHash(seed), 16) || -1; - return function () { - sample ^= sample << 13; - sample ^= sample >>> 17; - sample ^= sample << 5; + /** + * Enqueue the tasks for a test into the task queue. + * @param {Array} tasksArray + */ + addToTaskQueue (tasksArray) { + this.taskQueue.push(...tasksArray); + } - // ECMAScript has no unsigned number type - if (sample < 0) { - sample += 0x100000000; - } + /** + * Return the number of tasks remaining in the task queue to be processed. + * @return {number} + */ + taskCount () { + return this.taskQueue.length; + } - return sample / 0x100000000; - }; -} + /** + * Adds a test to the TestQueue for execution. + * @param {Function} testTasksFunc + * @param {boolean} prioritize + */ + add (testTasksFunc, prioritize) { + if (prioritize) { + config.queue.splice(this.priorityCount++, 0, testTasksFunc); + } else if (config.seed) { + if (!this.unitSampler) { + this.unitSampler = unitSamplerGenerator(config.seed); + } -/** - * This function is called when the ProcessingQueue is done processing all - * items. It handles emitting the final run events. - */ -function done () { - // We have reached the end of the processing queue and are about to emit the - // "runEnd" event after which reporters typically stop listening and exit - // the process. First, check if we need to emit one final test. - if (config.stats.testCount === 0 && config.failOnZeroTests === true) { - let error; - if (config.filter && config.filter.length) { - error = new Error(`No tests matched the filter "${config.filter}".`); - } else if (config.module && config.module.length) { - error = new Error(`No tests matched the module "${config.module}".`); - } else if (config.moduleId && config.moduleId.length) { - error = new Error(`No tests matched the moduleId "${config.moduleId}".`); - } else if (config.testId && config.testId.length) { - error = new Error(`No tests matched the testId "${config.testId}".`); + // Insert into a random position after all prioritized items + const index = Math.floor(this.unitSampler() * (config.queue.length - this.priorityCount + 1)); + config.queue.splice(this.priorityCount + index, 0, testTasksFunc); } else { - error = new Error('No tests were run.'); + config.queue.push(testTasksFunc); } - - test('global failure', extend(function (assert) { - assert.pushResult({ - result: false, - message: error.message, - source: error.stack - }); - }, { validTest: true })); - - // We do need to call `advance()` in order to resume the processing queue. - // Once this new test is finished processing, we'll reach `done` again, and - // that time the above condition will evaluate to false. - advance(); - return; } - const storage = config.storage; - - const runtime = Math.round(performance.now() - config.started); - const passed = config.stats.all - config.stats.bad; - - ProcessingQueue.finished = true; - - emit('runEnd', runSuite.end(true)); - runLoggingCallbacks('done', { - // @deprecated since 2.19.0 Use done() without `details` parameter, - // or use `QUnit.on('runEnd')` instead. Parameter to be replaced in - // QUnit 3.0 with test counts. - passed, - failed: config.stats.bad, - total: config.stats.all, - runtime - }).then(() => { - // Clear own storage items if all tests passed - if (storage && config.stats.bad === 0) { - for (let i = storage.length - 1; i >= 0; i--) { - const key = storage.key(i); - - if (key.indexOf('qunit-test-') === 0) { - storage.removeItem(key); - } + /** + * This function is called when the ProcessingQueue is done processing all + * items. It handles emitting the final run events. + */ + done () { + // We have reached the end of the processing queue and are about to emit the + // "runEnd" event after which reporters typically stop listening and exit + // the process. First, check if we need to emit one final test. + if (config.stats.testCount === 0 && config.failOnZeroTests === true) { + let error; + if (config.filter && config.filter.length) { + error = new Error(`No tests matched the filter "${config.filter}".`); + } else if (config.module && config.module.length) { + error = new Error(`No tests matched the module "${config.module}".`); + } else if (config.moduleId && config.moduleId.length) { + error = new Error(`No tests matched the moduleId "${config.moduleId}".`); + } else if (config.testId && config.testId.length) { + error = new Error(`No tests matched the testId "${config.testId}".`); + } else { + error = new Error('No tests were run.'); } + + this.test('global failure', extend(function (assert) { + assert.pushResult({ + result: false, + message: error.message, + source: error.stack + }); + }, { validTest: true })); + + // We do need to call `advance()` in order to resume the processing queue. + // Once this new test is finished processing, we'll reach `done` again, and + // that time the above condition will evaluate to false. + this.advance(); + return; } - }); -} -const ProcessingQueue = { - finished: false, - add: addToTestQueue, - advance, - taskCount: taskQueueLength -}; + const storage = config.storage; + + const runtime = Math.round(performance.now() - config.started); + const passed = config.stats.all - config.stats.bad; + + this.finished = true; + + emit('runEnd', runSuite.end(true)); + runLoggingCallbacks('done', { + // @deprecated since 2.19.0 Use done() without `details` parameter, + // or use `QUnit.on('runEnd')` instead. Parameter to be replaced in + // QUnit 3.0 with test counts. + passed, + failed: config.stats.bad, + total: config.stats.all, + runtime + }).then(() => { + // Clear own storage items if all tests passed + if (storage && config.stats.bad === 0) { + for (let i = storage.length - 1; i >= 0; i--) { + const key = storage.key(i); + + if (key.indexOf('qunit-test-') === 0) { + storage.removeItem(key); + } + } + } + }); + } +} export default ProcessingQueue; diff --git a/src/test.js b/src/test.js index 5f8ebc105..e75444849 100644 --- a/src/test.js +++ b/src/test.js @@ -16,7 +16,6 @@ import { } from './core/utilities'; import { runLoggingCallbacks } from './core/logging'; import { extractStacktrace, sourceFromStacktrace } from './core/stacktrace'; -import ProcessingQueue from './core/processing-queue'; import TestReport from './reports/test'; @@ -60,7 +59,7 @@ export default function Test (settings) { // Queuing a late test after the run has ended is not allowed. // This was once supported for internal use by QUnit.onError(). // Ref https://github.com/qunitjs/qunit/issues/1377 - if (ProcessingQueue.finished) { + if (config.pq.finished) { // Using this for anything other than onError(), such as testing in QUnit.done(), // is unstable and will likely result in the added tests being ignored by CI. // (Meaning the CI passes irregardless of the added tests). @@ -273,7 +272,7 @@ Test.prototype = { // when the 'after' and 'finish' tasks are the only tasks left to process if (hookName === 'after' && !lastTestWithinModuleExecuted(hookOwner) && - (config.queue.length > 0 || ProcessingQueue.taskCount() > 2)) { + (config.queue.length > 0 || config.pq.taskCount() > 2)) { return; } @@ -544,7 +543,7 @@ Test.prototype = { this.previousFailure = !!previousFailCount; - ProcessingQueue.add(runTest, prioritize, config.seed); + config.pq.add(runTest, prioritize, config.seed); }, pushResult: function (resultInfo) { @@ -1057,11 +1056,11 @@ function internalStart (test) { config.timeout = null; config.blocking = false; - ProcessingQueue.advance(); + config.pq.advance(); }); } else { config.blocking = false; - ProcessingQueue.advance(); + config.pq.advance(); } }