diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 3d4d2d55c173..a296253da99a 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -980,6 +980,89 @@ Tester.prototype = { }); }, + async handleTask(task, currentTest, PromiseTestUtils, isSetup = false) { + let currentScope = currentTest.scope; + let desc = isSetup ? "setup" : "test"; + currentScope.SimpleTest.info(`Entering ${desc} ${task.name}`); + let startTimestamp = performance.now(); + try { + let result = await task(); + if (isGenerator(result)) { + currentScope.SimpleTest.ok(false, "Task returned a generator"); + } + } catch (ex) { + if (currentTest.timedOut) { + currentTest.addResult( + new testResult({ + name: `Uncaught exception received from previously timed out ${desc}`, + pass: false, + ex, + stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, + allowFailure: currentTest.allowFailure, + }) + ); + // We timed out, so we've already cleaned up for this test, just get outta here. + return; + } + currentTest.addResult( + new testResult({ + name: `Uncaught exception in ${desc}`, + pass: currentScope.SimpleTest.isExpectingUncaughtException(), + ex, + stack: typeof ex == "object" && "stack" in ex ? ex.stack : null, + allowFailure: currentTest.allowFailure, + }) + ); + } + PromiseTestUtils.assertNoUncaughtRejections(); + ChromeUtils.addProfilerMarker( + isSetup ? "setup-task" : "task", + { category: "Test", startTime: startTimestamp }, + task.name.replace(/^bound /, "") || undefined + ); + currentScope.SimpleTest.info(`Leaving ${desc} ${task.name}`); + }, + + async _runTaskBasedTest(currentTest) { + let currentScope = currentTest.scope; + + // First run all the setups: + let setupFn; + while ((setupFn = currentScope.__setups.shift())) { + await this.handleTask( + setupFn, + currentTest, + this.PromiseTestUtils, + true /* is setup task */ + ); + } + + // Allow for a task to be skipped; we need only use the structured logger + // for this, whilst deactivating log buffering to ensure that messages + // are always printed to stdout. + let skipTask = task => { + let logger = this.structuredLogger; + logger.deactivateBuffering(); + logger.testStatus(this.currentTest.path, task.name, "SKIP"); + logger.warning("Skipping test " + task.name); + logger.activateBuffering(); + }; + + let task; + while ((task = currentScope.__tasks.shift())) { + if ( + task.__skipMe || + (currentScope.__runOnlyThisTask && + task != currentScope.__runOnlyThisTask) + ) { + skipTask(task); + continue; + } + await this.handleTask(task, currentTest, this.PromiseTestUtils); + } + currentScope.finish(); + }, + execTest: function Tester_execTest() { this.structuredLogger.testStart(this.currentTest.path); @@ -1101,73 +1184,9 @@ Tester.prototype = { "Cannot run both a add_task test and a normal test at the same time." ); } - let PromiseTestUtils = this.PromiseTestUtils; - - // Allow for a task to be skipped; we need only use the structured logger - // for this, whilst deactivating log buffering to ensure that messages - // are always printed to stdout. - let skipTask = task => { - let logger = this.structuredLogger; - logger.deactivateBuffering(); - logger.testStatus(this.currentTest.path, task.name, "SKIP"); - logger.warning("Skipping test " + task.name); - logger.activateBuffering(); - }; - - (async function() { - let task; - while ((task = this.__tasks.shift())) { - if ( - task.__skipMe || - (this.__runOnlyThisTask && task != this.__runOnlyThisTask) - ) { - skipTask(task); - continue; - } - this.SimpleTest.info("Entering test " + task.name); - let startTimestamp = performance.now(); - try { - let result = await task(); - if (isGenerator(result)) { - this.SimpleTest.ok(false, "Task returned a generator"); - } - } catch (ex) { - if (currentTest.timedOut) { - currentTest.addResult( - new testResult({ - name: - "Uncaught exception received from previously timed out test", - pass: false, - ex, - stack: - typeof ex == "object" && "stack" in ex ? ex.stack : null, - allowFailure: currentTest.allowFailure, - }) - ); - // We timed out, so we've already cleaned up for this test, just get outta here. - return; - } - currentTest.addResult( - new testResult({ - name: "Uncaught exception", - pass: this.SimpleTest.isExpectingUncaughtException(), - ex, - stack: - typeof ex == "object" && "stack" in ex ? ex.stack : null, - allowFailure: currentTest.allowFailure, - }) - ); - } - PromiseTestUtils.assertNoUncaughtRejections(); - ChromeUtils.addProfilerMarker( - "task", - { category: "Test", startTime: startTimestamp }, - task.name.replace(/^bound /, "") || undefined - ); - this.SimpleTest.info("Leaving test " + task.name); - } - this.finish(); - }.call(currentScope)); + // Spin off the async work without waiting for it to complete. + // It'll call finish() when it's done. + this._runTaskBasedTest(this.currentTest); } else if (typeof scope.test == "function") { scope.test(); } else { @@ -1617,6 +1636,7 @@ function decorateTaskFn(fn) { testScope.prototype = { __done: true, __tasks: null, + __setups: [], __runOnlyThisTask: null, __waitTimer: null, __cleanupFunctions: [], @@ -1674,6 +1694,15 @@ testScope.prototype = { return bound; }, + add_setup(aFunction) { + if (!this.__setups.length) { + this.waitForExplicitFinish(); + } + let bound = aFunction.bind(this); + this.__setups.push(bound); + return bound; + }, + destroy: function test_destroy() { for (let prop in this) { delete this[prop]; diff --git a/testing/mochitest/tests/browser/browser.ini b/testing/mochitest/tests/browser/browser.ini index 370f4395b0e9..76ab1ac01840 100644 --- a/testing/mochitest/tests/browser/browser.ini +++ b/testing/mochitest/tests/browser/browser.ini @@ -44,6 +44,8 @@ support-files = skip-if = true # Disabled beacuse it takes too long (bug 1178959) [browser_sanityException.js] [browser_sanityException2.js] +[browser_setup_runs_first.js] +[browser_setup_runs_for_only_tests.js] [browser_tasks_skip.js] [browser_tasks_skipall.js] [browser_uncaught_rejection_expected.js] diff --git a/testing/mochitest/tests/browser/browser_setup_runs_first.js b/testing/mochitest/tests/browser/browser_setup_runs_first.js new file mode 100644 index 000000000000..6b9ce145dab6 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_setup_runs_first.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let someVar = 1; + +add_task(() => { + is(someVar, 2, "Should get updated by setup which runs first."); +}); + +add_setup(() => { + someVar = 2; +}); diff --git a/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js b/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js new file mode 100644 index 000000000000..d1b811b2d185 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_setup_runs_for_only_tests.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let someVar = 1; + +add_setup(() => { + someVar = 2; +}); + +/* eslint-disable mozilla/reject-addtask-only */ +add_task(() => { + is(someVar, 2, "Setup should have run, even though this is the only test."); +}).only(); diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js index d62b18f39045..1413e4b56c33 100644 --- a/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/configs/browser-test.js @@ -23,6 +23,7 @@ module.exports = { TestUtils: false, XPCNativeWrapper: false, addLoadEvent: false, + add_setup: false, add_task: false, content: false, executeSoon: false,