From 8f97970e9cfa2bd19558bed212306ce007eeaef9 Mon Sep 17 00:00:00 2001 From: David Rajchenbach-Teller Date: Tue, 4 Jun 2013 15:35:43 +0200 Subject: [PATCH] Bug 872229 - Add an add_task API for mochitest. r=ted --- .../test/browser_833286_atomic_backup.js | 3 +- testing/mochitest/browser-test.js | 83 ++++++++++++++++++- testing/mochitest/tests/browser/Makefile.in | 2 + .../tests/browser/browser_add_task.js | 31 +++++++ .../tests/browser/browser_fail_add_task.js | 57 +++++++++++++ 5 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 testing/mochitest/tests/browser/browser_add_task.js create mode 100644 testing/mochitest/tests/browser/browser_fail_add_task.js diff --git a/browser/components/sessionstore/test/browser_833286_atomic_backup.js b/browser/components/sessionstore/test/browser_833286_atomic_backup.js index 925368c124c5..63c4ff6fe047 100644 --- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js +++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js @@ -7,10 +7,9 @@ let tmp = {}; Cu.import("resource://gre/modules/osfile.jsm", tmp); -Cu.import("resource://gre/modules/Task.jsm", tmp); Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp); -const {OS, Task, _SessionFile} = tmp; +const {OS, _SessionFile} = tmp; const PREF_SS_INTERVAL = "browser.sessionstore.interval"; // Full paths for sessionstore.js and sessionstore.bak. diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 4495ddc56c2c..17d6ff4521c4 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -73,10 +73,14 @@ function Tester(aTests, aDumper, aCallback) { this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/SimpleTest.js", simpleTestScope); this._scriptLoader.loadSubScript("chrome://mochikit/content/chrome-harness.js", simpleTestScope); this.SimpleTest = simpleTestScope.SimpleTest; + this.Task = Components.utils.import("resource://gre/modules/Task.jsm", null).Task; + this.Promise = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", null).Promise; } Tester.prototype = { EventUtils: {}, SimpleTest: {}, + Task: null, + Promise: null, repeat: 0, runUntilFailure: false, @@ -398,6 +402,8 @@ Tester.prototype = { this.currentTest.scope.EventUtils = this.EventUtils; this.currentTest.scope.SimpleTest = this.SimpleTest; this.currentTest.scope.gTestPath = this.currentTest.path; + this.currentTest.scope.Task = this.Task; + this.currentTest.scope.Promise = this.Promise; // Override SimpleTest methods with ours. ["ok", "is", "isnot", "ise", "todo", "todo_is", "todo_isnot", "info"].forEach(function(m) { @@ -431,9 +437,34 @@ Tester.prototype = { // Run the test this.lastStartTime = Date.now(); - if ("generatorTest" in this.currentTest.scope) { - if ("test" in this.currentTest.scope) + if (this.currentTest.scope.__tasks) { + // This test consists of tasks, added via the `add_task()` API. + if ("test" in this.currentTest.scope) { + throw "Cannot run both a add_task test and a normal test at the same time."; + } + let testScope = this.currentTest.scope; + let currentTest = this.currentTest; + this.Task.spawn(function() { + let task; + while ((task = this.__tasks.shift())) { + this.SimpleTest.info("Entering test " + task.name); + try { + yield task(); + } catch (ex) { + let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); + let stack = (typeof ex == "object" && "stack" in ex)?ex.stack:null; + let name = "Uncaught exception"; + let result = new testResult(isExpected, name, ex, false, stack); + currentTest.addResult(result); + } + this.SimpleTest.info("Leaving test " + task.name); + } + this.finish(); + }.bind(testScope)); + } else if ("generatorTest" in this.currentTest.scope) { + if ("test" in this.currentTest.scope) { throw "Cannot run both a generator test and a normal test at the same time."; + } // This test is a generator. It will not finish immediately. this.currentTest.scope.waitForExplicitFinish(); @@ -444,7 +475,7 @@ Tester.prototype = { this.currentTest.scope.test(); } } catch (ex) { - var isExpected = !!this.SimpleTest.isExpectingUncaughtException(); + let isExpected = !!this.SimpleTest.isExpectingUncaughtException(); if (!this.SimpleTest.isIgnoringAllUncaughtExceptions()) { this.currentTest.addResult(new testResult(isExpected, "Exception thrown", ex, false)); this.SimpleTest.expectUncaughtException(false); @@ -668,12 +699,58 @@ function testScope(aTester, aTest) { testScope.prototype = { __done: true, __generator: null, + __tasks: null, __waitTimer: null, __cleanupFunctions: [], __timeoutFactor: 1, EventUtils: {}, SimpleTest: {}, + Task: null, + Promise: null, + + /** + * Add a test function which is a Task function. + * + * Task functions are functions fed into Task.jsm's Task.spawn(). They are + * generators that emit promises. + * + * If an exception is thrown, an assertion fails, or if a rejected + * promise is yielded, the test function aborts immediately and the test is + * reported as a failure. Execution continues with the next test function. + * + * To trigger premature (but successful) termination of the function, simply + * return or throw a Task.Result instance. + * + * Example usage: + * + * add_task(function test() { + * let result = yield Promise.resolve(true); + * + * ok(result); + * + * let secondary = yield someFunctionThatReturnsAPromise(result); + * is(secondary, "expected value"); + * }); + * + * add_task(function test_early_return() { + * let result = yield somethingThatReturnsAPromise(); + * + * if (!result) { + * // Test is ended immediately, with success. + * return; + * } + * + * is(result, "foo"); + * }); + */ + add_task: function(aFunction) { + if (!this.__tasks) { + this.waitForExplicitFinish(); + this.__tasks = []; + } + this.__tasks.push(aFunction.bind(this)); + }, destroy: function test_destroy() { for (let prop in this) diff --git a/testing/mochitest/tests/browser/Makefile.in b/testing/mochitest/tests/browser/Makefile.in index 9749d49623d4..f17a1a79076d 100644 --- a/testing/mochitest/tests/browser/Makefile.in +++ b/testing/mochitest/tests/browser/Makefile.in @@ -14,6 +14,7 @@ MOCHITEST_BROWSER_FILES = \ head.js \ browser_head.js \ browser_pass.js \ + browser_add_task.js \ browser_async.js \ browser_privileges.js \ browser_popupNode.js \ @@ -25,6 +26,7 @@ MOCHITEST_BROWSER_FILES = \ # Disabled, these are only good for testing the harness' failure reporting # browser_zz_fail_openwindow.js \ # browser_fail.js \ +# browser_fail_add_task.js \ # browser_fail_async_throw.js \ # browser_fail_fp.js \ # browser_fail_pf.js \ diff --git a/testing/mochitest/tests/browser/browser_add_task.js b/testing/mochitest/tests/browser/browser_add_task.js new file mode 100644 index 000000000000..b8c92b79d4e0 --- /dev/null +++ b/testing/mochitest/tests/browser/browser_add_task.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let test1Complete = false; +let test2Complete = false; + +function executeWithTimeout() { + let deferred = Promise.defer(); + executeSoon(function() { + ok(true, "we get here after a timeout"); + deferred.resolve(); + }); + return deferred.promise; +} + +add_task(function asyncTest_no1() { + yield executeWithTimeout(); + test1Complete = true; +}); + +add_task(function asyncTest_no2() { + yield executeWithTimeout(); + test2Complete = true; +}); + +add_task(function() { + ok(test1Complete, "We have been through test 1"); + ok(test2Complete, "We have been through test 2"); +}); diff --git a/testing/mochitest/tests/browser/browser_fail_add_task.js b/testing/mochitest/tests/browser/browser_fail_add_task.js new file mode 100644 index 000000000000..bcc1be8ed0fd --- /dev/null +++ b/testing/mochitest/tests/browser/browser_fail_add_task.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test is designed to fail. +// It ensures that throwing an asynchronous error from add_task will +// fail the test. + +let passedTests = 0; + +function rejectWithTimeout(error = undefined) { + let deferred = Promise.defer(); + executeSoon(function() { + ok(true, "we get here after a timeout"); + deferred.reject(error); + }); + return deferred.promise; +} + +add_task(function failWithoutError() { + try { + yield rejectWithTimeout(); + } finally { + ++passedTests; + } +}); + +add_task(function failWithString() { + try { + yield rejectWithTimeout("Meaningless error"); + } finally { + ++passedTests; + } +}); + +add_task(function failWithoutInt() { + try { + yield rejectWithTimeout(42); + } finally { + ++passedTests; + } +}); + + +// This one should display a stack trace +add_task(function failWithError() { + try { + yield rejectWithTimeout(new Error("This is an error")); + } finally { + ++passedTests; + } +}); + +add_task(function done() { + is(passedTests, 4, "Passed all tests"); +});