From ac46df8daea09899ce30dc8fd70986e258c746bf Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 9 Feb 2018 06:46:43 -0500 Subject: Move Add-on SDK source to toolkit/jetpack --- toolkit/jetpack/sdk/test/assert.js | 366 ++++++++++++++++++++ toolkit/jetpack/sdk/test/harness.js | 645 ++++++++++++++++++++++++++++++++++++ toolkit/jetpack/sdk/test/httpd.js | 6 + toolkit/jetpack/sdk/test/loader.js | 123 +++++++ toolkit/jetpack/sdk/test/memory.js | 11 + toolkit/jetpack/sdk/test/options.js | 23 ++ toolkit/jetpack/sdk/test/runner.js | 131 ++++++++ toolkit/jetpack/sdk/test/utils.js | 199 +++++++++++ 8 files changed, 1504 insertions(+) create mode 100644 toolkit/jetpack/sdk/test/assert.js create mode 100644 toolkit/jetpack/sdk/test/harness.js create mode 100644 toolkit/jetpack/sdk/test/httpd.js create mode 100644 toolkit/jetpack/sdk/test/loader.js create mode 100644 toolkit/jetpack/sdk/test/memory.js create mode 100644 toolkit/jetpack/sdk/test/options.js create mode 100644 toolkit/jetpack/sdk/test/runner.js create mode 100644 toolkit/jetpack/sdk/test/utils.js (limited to 'toolkit/jetpack/sdk/test') diff --git a/toolkit/jetpack/sdk/test/assert.js b/toolkit/jetpack/sdk/test/assert.js new file mode 100644 index 000000000..8478c8414 --- /dev/null +++ b/toolkit/jetpack/sdk/test/assert.js @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { isFunction, isNull, isObject, isString, + isRegExp, isArray, isDate, isPrimitive, + isUndefined, instanceOf, source } = require("../lang/type"); + +/** + * The `AssertionError` is defined in assert. + * @extends Error + * @example + * new assert.AssertionError({ + * message: message, + * actual: actual, + * expected: expected + * }) + */ +function AssertionError(options) { + let assertionError = Object.create(AssertionError.prototype); + + if (isString(options)) + options = { message: options }; + if ("actual" in options) + assertionError.actual = options.actual; + if ("expected" in options) + assertionError.expected = options.expected; + if ("operator" in options) + assertionError.operator = options.operator; + + assertionError.message = options.message; + assertionError.stack = new Error().stack; + return assertionError; +} +AssertionError.prototype = Object.create(Error.prototype, { + constructor: { value: AssertionError }, + name: { value: "AssertionError", enumerable: true }, + toString: { value: function toString() { + let value; + if (this.message) { + value = this.name + " : " + this.message; + } + else { + value = [ + this.name + " : ", + source(this.expected), + this.operator, + source(this.actual) + ].join(" "); + } + return value; + }} +}); +exports.AssertionError = AssertionError; + +function Assert(logger) { + let assert = Object.create(Assert.prototype, { _log: { value: logger }}); + + assert.fail = assert.fail.bind(assert); + assert.pass = assert.pass.bind(assert); + + return assert; +} + +Assert.prototype = { + fail: function fail(e) { + if (!e || typeof(e) !== 'object') { + this._log.fail(e); + return; + } + let message = e.message; + try { + if ('operator' in e) { + message += [ + " -", + source(e.actual), + e.operator, + source(e.expected) + ].join(" "); + } + } + catch(e) {} + this._log.fail(message); + }, + pass: function pass(message) { + this._log.pass(message); + return true; + }, + error: function error(e) { + this._log.exception(e); + }, + ok: function ok(value, message) { + if (!!!value) { + this.fail({ + actual: value, + expected: true, + message: message, + operator: "==" + }); + return false; + } + + this.pass(message); + return true; + }, + + /** + * The equality assertion tests shallow, coercive equality with `==`. + * @example + * assert.equal(1, 1, "one is one"); + */ + equal: function equal(actual, expected, message) { + if (actual == expected) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "==" + }); + return false; + }, + + /** + * The non-equality assertion tests for whether two objects are not equal + * with `!=`. + * @example + * assert.notEqual(1, 2, "one is not two"); + */ + notEqual: function notEqual(actual, expected, message) { + if (actual != expected) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!=", + }); + return false; + }, + + /** + * The equivalence assertion tests a deep (with `===`) equality relation. + * @example + * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects") + */ + deepEqual: function deepEqual(actual, expected, message) { + if (isDeepEqual(actual, expected)) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "deepEqual" + }); + return false; + }, + + /** + * The non-equivalence assertion tests for any deep (with `===`) inequality. + * @example + * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }), + * "object's inherit from different prototypes"); + */ + notDeepEqual: function notDeepEqual(actual, expected, message) { + if (!isDeepEqual(actual, expected)) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "notDeepEqual" + }); + return false; + }, + + /** + * The strict equality assertion tests strict equality, as determined by + * `===`. + * @example + * assert.strictEqual(null, null, "`null` is `null`") + */ + strictEqual: function strictEqual(actual, expected, message) { + if (actual === expected) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "===" + }); + return false; + }, + + /** + * The strict non-equality assertion tests for strict inequality, as + * determined by `!==`. + * @example + * assert.notStrictEqual(null, undefined, "`null` is not `undefined`"); + */ + notStrictEqual: function notStrictEqual(actual, expected, message) { + if (actual !== expected) { + this.pass(message); + return true; + } + + this.fail({ + actual: actual, + expected: expected, + message: message, + operator: "!==" + }); + return false; + }, + + /** + * The assertion whether or not given `block` throws an exception. If optional + * `Error` argument is provided and it's type of function thrown error is + * asserted to be an instance of it, if type of `Error` is string then message + * of throw exception is asserted to contain it. + * @param {Function} block + * Function that is expected to throw. + * @param {Error|RegExp} [Error] + * Error constructor that is expected to be thrown or a string that + * must be contained by a message of the thrown exception, or a RegExp + * matching a message of the thrown exception. + * @param {String} message + * Description message + * + * @examples + * + * assert.throws(function block() { + * doSomething(4) + * }, "Object is expected", "Incorrect argument is passed"); + * + * assert.throws(function block() { + * Object.create(5) + * }, TypeError, "TypeError is thrown"); + */ + throws: function throws(block, Error, message) { + let threw = false; + let exception = null; + + // If third argument is not provided and second argument is a string it + // means that optional `Error` argument was not passed, so we shift + // arguments. + if (isString(Error) && isUndefined(message)) { + message = Error; + Error = undefined; + } + + // Executing given `block`. + try { + block(); + } + catch (e) { + threw = true; + exception = e; + } + + // If exception was thrown and `Error` argument was not passed assert is + // passed. + if (threw && (isUndefined(Error) || + // If passed `Error` is RegExp using it's test method to + // assert thrown exception message. + (isRegExp(Error) && (Error.test(exception.message) || Error.test(exception.toString()))) || + // If passed `Error` is a constructor function testing if + // thrown exception is an instance of it. + (isFunction(Error) && instanceOf(exception, Error)))) + { + this.pass(message); + return true; + } + + // Otherwise we report assertion failure. + let failure = { + message: message, + operator: "matches" + }; + + if (exception) { + failure.actual = exception.message || exception.toString(); + } + + if (Error) { + failure.expected = Error.toString(); + } + + this.fail(failure); + return false; + } +}; +exports.Assert = Assert; + +function isDeepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + else if (isDate(actual) && isDate(expected)) { + return actual.getTime() === expected.getTime(); + } + + // XXX specification bug: this should be specified + else if (isPrimitive(actual) || isPrimitive(expected)) { + return expected === actual; + } + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + else if (!isObject(actual) && !isObject(expected)) { + return actual == expected; + } + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + else { + return actual.prototype === expected.prototype && + isEquivalent(actual, expected); + } +} + +function isEquivalent(a, b, stack) { + let aKeys = Object.keys(a); + let bKeys = Object.keys(b); + + return aKeys.length === bKeys.length && + isArrayEquivalent(aKeys.sort(), bKeys.sort()) && + aKeys.every(function(key) { + return isDeepEqual(a[key], b[key], stack) + }); +} + +function isArrayEquivalent(a, b, stack) { + return isArray(a) && isArray(b) && + a.every(function(value, index) { + return isDeepEqual(value, b[index]); + }); +} diff --git a/toolkit/jetpack/sdk/test/harness.js b/toolkit/jetpack/sdk/test/harness.js new file mode 100644 index 000000000..1b31a1c79 --- /dev/null +++ b/toolkit/jetpack/sdk/test/harness.js @@ -0,0 +1,645 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +const { Cc, Ci, Cu } = require("chrome"); +const { Loader } = require('./loader'); +const { serializeStack, parseStack } = require("toolkit/loader"); +const { setTimeout } = require('../timers'); +const { PlainTextConsole } = require("../console/plain-text"); +const { when: unload } = require("../system/unload"); +const { format, fromException } = require("../console/traceback"); +const system = require("../system"); +const { gc: gcPromise } = require('./memory'); +const { defer } = require('../core/promise'); +const { extend } = require('../core/heritage'); + +// Trick manifest builder to make it think we need these modules ? +const unit = require("../deprecated/unit-test"); +const test = require("../../test"); +const url = require("../url"); + +function emptyPromise() { + let { promise, resolve } = defer(); + resolve(); + return promise; +} + +var cService = Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService); + +// The console used to log messages +var testConsole; + +// Cuddlefish loader in which we load and execute tests. +var loader; + +// Function to call when we're done running tests. +var onDone; + +// Function to print text to a console, w/o CR at the end. +var print; + +// How many more times to run all tests. +var iterationsLeft; + +// Whether to report memory profiling information. +var profileMemory; + +// Whether we should stop as soon as a test reports a failure. +var stopOnError; + +// Function to call to retrieve a list of tests to execute +var findAndRunTests; + +// Combined information from all test runs. +var results; + +// A list of the compartments and windows loaded after startup +var startLeaks; + +// JSON serialization of last memory usage stats; we keep it stringified +// so we don't actually change the memory usage stats (in terms of objects) +// of the JSRuntime we're profiling. +var lastMemoryUsage; + +function analyzeRawProfilingData(data) { + var graph = data.graph; + var shapes = {}; + + // Convert keys in the graph from strings to ints. + // TODO: Can we get rid of this ridiculousness? + var newGraph = {}; + for (id in graph) { + newGraph[parseInt(id)] = graph[id]; + } + graph = newGraph; + + var modules = 0; + var moduleIds = []; + var moduleObjs = {UNKNOWN: 0}; + for (let name in data.namedObjects) { + moduleObjs[name] = 0; + moduleIds[data.namedObjects[name]] = name; + modules++; + } + + var count = 0; + for (id in graph) { + var parent = graph[id].parent; + while (parent) { + if (parent in moduleIds) { + var name = moduleIds[parent]; + moduleObjs[name]++; + break; + } + if (!(parent in graph)) { + moduleObjs.UNKNOWN++; + break; + } + parent = graph[parent].parent; + } + count++; + } + + print("\nobject count is " + count + " in " + modules + " modules" + + " (" + data.totalObjectCount + " across entire JS runtime)\n"); + if (lastMemoryUsage) { + var last = JSON.parse(lastMemoryUsage); + var diff = { + moduleObjs: dictDiff(last.moduleObjs, moduleObjs), + totalObjectClasses: dictDiff(last.totalObjectClasses, + data.totalObjectClasses) + }; + + for (let name in diff.moduleObjs) + print(" " + diff.moduleObjs[name] + " in " + name + "\n"); + for (let name in diff.totalObjectClasses) + print(" " + diff.totalObjectClasses[name] + " instances of " + + name + "\n"); + } + lastMemoryUsage = JSON.stringify( + {moduleObjs: moduleObjs, + totalObjectClasses: data.totalObjectClasses} + ); +} + +function dictDiff(last, curr) { + var diff = {}; + + for (let name in last) { + var result = (curr[name] || 0) - last[name]; + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + for (let name in curr) { + var result = curr[name] - (last[name] || 0); + if (result) + diff[name] = (result > 0 ? "+" : "") + result; + } + return diff; +} + +function reportMemoryUsage() { + if (!profileMemory) { + return emptyPromise(); + } + + return gcPromise().then((() => { + var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager); + let count = 0; + function logReporter(process, path, kind, units, amount, description) { + print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n"); + } + mgr.getReportsForThisProcess(logReporter, null, /* anonymize = */ false); + })); +} + +var gWeakrefInfo; + +function checkMemory() { + return gcPromise().then(_ => { + let leaks = getPotentialLeaks(); + + let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { + return !(url in startLeaks.compartments); + }); + + let windowURLs = Object.keys(leaks.windows).filter(function(url) { + return !(url in startLeaks.windows); + }); + + for (let url of compartmentURLs) + console.warn("LEAKED", leaks.compartments[url]); + + for (let url of windowURLs) + console.warn("LEAKED", leaks.windows[url]); + }).then(showResults); +} + +function showResults() { + let { promise, resolve } = defer(); + + if (gWeakrefInfo) { + gWeakrefInfo.forEach( + function(info) { + var ref = info.weakref.get(); + if (ref !== null) { + var data = ref.__url__ ? ref.__url__ : ref; + var warning = data == "[object Object]" + ? "[object " + data.constructor.name + "(" + + Object.keys(data).join(", ") + ")]" + : data; + console.warn("LEAK", warning, info.bin); + } + } + ); + } + + onDone(results); + + resolve(); + return promise; +} + +function cleanup() { + let coverObject = {}; + try { + loader.unload(); + + if (loader.globals.console.errorsLogged && !results.failed) { + results.failed++; + console.error("warnings and/or errors were logged."); + } + + if (consoleListener.errorsLogged && !results.failed) { + console.warn(consoleListener.errorsLogged + " " + + "warnings or errors were logged to the " + + "platform's nsIConsoleService, which could " + + "be of no consequence; however, they could also " + + "be indicative of aberrant behavior."); + } + + // read the code coverage object, if it exists, from CoverJS-moz + if (typeof loader.globals.global == "object") { + coverObject = loader.globals.global['__$coverObject'] || {}; + } + + consoleListener.errorsLogged = 0; + loader = null; + + consoleListener.unregister(); + + Cu.forceGC(); + } + catch (e) { + results.failed++; + console.error("unload.send() threw an exception."); + console.exception(e); + }; + + setTimeout(require("./options").checkMemory ? checkMemory : showResults, 1); + + // dump the coverobject + if (Object.keys(coverObject).length){ + const self = require('sdk/self'); + const {pathFor} = require("sdk/system"); + let file = require('sdk/io/file'); + const {env} = require('sdk/system/environment'); + console.log("CWD:", env.PWD); + let out = file.join(env.PWD,'coverstats-'+self.id+'.json'); + console.log('coverstats:', out); + let outfh = file.open(out,'w'); + outfh.write(JSON.stringify(coverObject,null,2)); + outfh.flush(); + outfh.close(); + } +} + +function getPotentialLeaks() { + Cu.forceGC(); + + // Things we can assume are part of the platform and so aren't leaks + let GOOD_BASE_URLS = [ + "chrome://", + "resource:///", + "resource://app/", + "resource://gre/", + "resource://gre-resources/", + "resource://pdf.js/", + "resource://pdf.js.components/", + "resource://services-common/", + "resource://services-crypto/", + "resource://services-sync/" + ]; + + let ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + let uri = ioService.newURI("chrome://global/content/", "UTF-8", null); + let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIChromeRegistry); + uri = chromeReg.convertChromeURL(uri); + let spec = uri.spec; + let pos = spec.indexOf("!/"); + GOOD_BASE_URLS.push(spec.substring(0, pos + 2)); + + let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)"); + let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/"); + let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$"); + let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active"); + let windowDetails = new RegExp("^(.*), id=.*$"); + + function isPossibleLeak(item) { + if (!item.location) + return false; + + for (let url of GOOD_BASE_URLS) { + if (item.location.substring(0, url.length) == url) { + return false; + } + } + + return true; + } + + let compartments = {}; + let windows = {}; + function logReporter(process, path, kind, units, amount, description) { + let matches; + + if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) { + if (matches[1] in compartments) + return; + + let details = compartmentDetails.exec(matches[1]); + if (!details) { + console.error("Unable to parse compartment detail " + matches[1]); + return; + } + + let item = { + path: matches[1], + principal: details[1], + location: details[2] ? details[2].replace(/\\/g, "/") : undefined, + source: details[3] ? details[3].split(" -> ").reverse() : undefined, + toString: function() { + return this.location; + } + }; + + if (!isPossibleLeak(item)) + return; + + compartments[matches[1]] = item; + return; + } + + if ((matches = windowRegexp.exec(path))) { + if (matches[1] in windows) + return; + + let details = windowDetails.exec(matches[1]); + if (!details) { + console.error("Unable to parse window detail " + matches[1]); + return; + } + + let item = { + path: matches[1], + location: details[1].replace(/\\/g, "/"), + source: [details[1].replace(/\\/g, "/")], + toString: function() { + return this.location; + } + }; + + if (!isPossibleLeak(item)) + return; + + windows[matches[1]] = item; + } + } + + Cc["@mozilla.org/memory-reporter-manager;1"] + .getService(Ci.nsIMemoryReporterManager) + .getReportsForThisProcess(logReporter, null, /* anonymize = */ false); + + return { compartments: compartments, windows: windows }; +} + +function nextIteration(tests) { + if (tests) { + results.passed += tests.passed; + results.failed += tests.failed; + + reportMemoryUsage().then(_ => { + let testRun = []; + for (let test of tests.testRunSummary) { + let testCopy = {}; + for (let info in test) { + testCopy[info] = test[info]; + } + testRun.push(testCopy); + } + + results.testRuns.push(testRun); + iterationsLeft--; + + checkForEnd(); + }) + } + else { + checkForEnd(); + } +} + +function checkForEnd() { + if (iterationsLeft && (!stopOnError || results.failed == 0)) { + // Pass the loader which has a hooked console that doesn't dispatch + // errors to the JS console and avoid firing false alarm in our + // console listener + findAndRunTests(loader, nextIteration); + } + else { + setTimeout(cleanup, 0); + } +} + +var POINTLESS_ERRORS = [ + 'Invalid chrome URI:', + 'OpenGL LayerManager Initialized Succesfully.', + '[JavaScript Error: "TelemetryStopwatch:', + 'reference to undefined property', + '[JavaScript Error: "The character encoding of the HTML document was ' + + 'not declared.', + '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' + + 'native weak map key', + '[JavaScript Warning: "Duplicate resource declaration for', + 'file: "chrome://browser/content/', + 'file: "chrome://global/content/', + '[JavaScript Warning: "The character encoding of a framed document was ' + + 'not declared.', + 'file: "chrome://browser/skin/' +]; + +// These are messages that will cause a test to fail if logged through the +// console service +var IMPORTANT_ERRORS = [ + 'Sending message that cannot be cloned. Are you trying to send an XPCOM object?', +]; + +var consoleListener = { + registered: false, + + register: function() { + if (this.registered) + return; + cService.registerListener(this); + this.registered = true; + }, + + unregister: function() { + if (!this.registered) + return; + cService.unregisterListener(this); + this.registered = false; + }, + + errorsLogged: 0, + + observe: function(object) { + if (!(object instanceof Ci.nsIScriptError)) + return; + this.errorsLogged++; + var message = object.QueryInterface(Ci.nsIConsoleMessage).message; + if (IMPORTANT_ERRORS.find(msg => message.indexOf(msg) >= 0)) { + testConsole.error(message); + return; + } + var pointless = POINTLESS_ERRORS.filter(err => message.indexOf(err) >= 0); + if (pointless.length == 0 && message) + testConsole.log(message); + } +}; + +function TestRunnerConsole(base, options) { + let proto = extend(base, { + errorsLogged: 0, + warn: function warn() { + this.errorsLogged++; + base.warn.apply(base, arguments); + }, + error: function error() { + this.errorsLogged++; + base.error.apply(base, arguments); + }, + info: function info(first) { + if (options.verbose) + base.info.apply(base, arguments); + else + if (first == "pass:") + print("."); + }, + }); + return Object.create(proto); +} + +function stringify(arg) { + try { + return String(arg); + } + catch(ex) { + return ""; + } +} + +function stringifyArgs(args) { + return Array.map(args, stringify).join(" "); +} + +function TestRunnerTinderboxConsole(base, options) { + this.base = base; + this.print = options.print; + this.verbose = options.verbose; + this.errorsLogged = 0; + + // Binding all the public methods to an instance so that they can be used + // as callback / listener functions straightaway. + this.log = this.log.bind(this); + this.info = this.info.bind(this); + this.warn = this.warn.bind(this); + this.error = this.error.bind(this); + this.debug = this.debug.bind(this); + this.exception = this.exception.bind(this); + this.trace = this.trace.bind(this); +}; + +TestRunnerTinderboxConsole.prototype = { + testMessage: function testMessage(pass, expected, test, message) { + let type = "TEST-"; + if (expected) { + if (pass) + type += "PASS"; + else + type += "KNOWN-FAIL"; + } + else { + this.errorsLogged++; + if (pass) + type += "UNEXPECTED-PASS"; + else + type += "UNEXPECTED-FAIL"; + } + + this.print(type + " | " + test + " | " + message + "\n"); + if (!expected) + this.trace(); + }, + + log: function log() { + this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); + }, + + info: function info(first) { + this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); + }, + + warn: function warn() { + this.errorsLogged++; + this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); + }, + + error: function error() { + this.errorsLogged++; + this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); + this.base.error.apply(this.base, arguments); + }, + + debug: function debug() { + this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); + }, + + exception: function exception(e) { + this.print("An exception occurred.\n" + + require("../console/traceback").format(e) + "\n" + e + "\n"); + }, + + trace: function trace() { + var traceback = require("../console/traceback"); + var stack = traceback.get(); + stack.splice(-1, 1); + this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n"); + } +}; + +var runTests = exports.runTests = function runTests(options) { + iterationsLeft = options.iterations; + profileMemory = options.profileMemory; + stopOnError = options.stopOnError; + onDone = options.onDone; + print = options.print; + findAndRunTests = options.findAndRunTests; + + results = { + passed: 0, + failed: 0, + testRuns: [] + }; + + try { + consoleListener.register(); + print("Running tests on " + system.name + " " + system.version + + "/Gecko " + system.platformVersion + " (Build " + + system.build + ") (" + system.id + ") under " + + system.platform + "/" + system.architecture + ".\n"); + + if (options.parseable) + testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options); + else + testConsole = new TestRunnerConsole(new PlainTextConsole(), options); + + loader = Loader(module, { + console: testConsole, + global: {} // useful for storing things like coverage testing. + }); + + // Load these before getting initial leak stats as they will still be in + // memory when we check later + require("../deprecated/unit-test"); + require("../deprecated/unit-test-finder"); + if (profileMemory) + startLeaks = getPotentialLeaks(); + + nextIteration(); + } catch (e) { + let frames = fromException(e).reverse().reduce(function(frames, frame) { + if (frame.fileName.split("/").pop() === "unit-test-finder.js") + frames.done = true + if (!frames.done) frames.push(frame) + + return frames + }, []) + + let prototype = typeof(e) === "object" ? e.constructor.prototype : + Error.prototype; + let stack = serializeStack(frames.reverse()); + + let error = Object.create(prototype, { + message: { value: e.message, writable: true, configurable: true }, + fileName: { value: e.fileName, writable: true, configurable: true }, + lineNumber: { value: e.lineNumber, writable: true, configurable: true }, + stack: { value: stack, writable: true, configurable: true }, + toString: { value: () => String(e), writable: true, configurable: true }, + }); + + print("Error: " + error + " \n " + format(error)); + onDone({passed: 0, failed: 1}); + } +}; + +unload(_ => consoleListener.unregister()); diff --git a/toolkit/jetpack/sdk/test/httpd.js b/toolkit/jetpack/sdk/test/httpd.js new file mode 100644 index 000000000..218493924 --- /dev/null +++ b/toolkit/jetpack/sdk/test/httpd.js @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +throw new Error(`This file was removed. A copy can be obtained from: + https://github.com/mozilla/addon-sdk/blob/master/test/lib/httpd.js`); diff --git a/toolkit/jetpack/sdk/test/loader.js b/toolkit/jetpack/sdk/test/loader.js new file mode 100644 index 000000000..33ba2ca5a --- /dev/null +++ b/toolkit/jetpack/sdk/test/loader.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const { resolveURI, Require, + unload, override, descriptor } = require('../../toolkit/loader'); +const { ensure } = require('../system/unload'); +const addonWindow = require('../addon/window'); +const { PlainTextConsole } = require('sdk/console/plain-text'); + +var defaultGlobals = override(require('../system/globals'), { + console: console +}); + +function CustomLoader(module, globals, packaging, overrides={}) { + let options = packaging || require("@loader/options"); + options = override(options, { + id: overrides.id || options.id, + globals: override(defaultGlobals, globals || {}), + modules: override(override(options.modules || {}, overrides.modules || {}), { + 'sdk/addon/window': addonWindow + }) + }); + + let loaderModule = options.isNative ? '../../toolkit/loader' : '../loader/cuddlefish'; + let { Loader } = require(loaderModule); + let loader = Loader(options); + let wrapper = Object.create(loader, descriptor({ + require: Require(loader, module), + sandbox: function(id) { + let requirement = loader.resolve(id, module.id); + if (!requirement) + requirement = id; + let uri = resolveURI(requirement, loader.mapping); + return loader.sandboxes[uri]; + }, + unload: function(reason) { + unload(loader, reason); + } + })); + ensure(wrapper); + return wrapper; +}; +exports.Loader = CustomLoader; + +function HookedPlainTextConsole(hook, print, innerID) { + this.log = hook.bind(null, "log", innerID); + this.info = hook.bind(null, "info", innerID); + this.warn = hook.bind(null, "warn", innerID); + this.error = hook.bind(null, "error", innerID); + this.debug = hook.bind(null, "debug", innerID); + this.exception = hook.bind(null, "exception", innerID); + this.time = hook.bind(null, "time", innerID); + this.timeEnd = hook.bind(null, "timeEnd", innerID); + + this.__exposedProps__ = { + log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw", + exception: "rw", time: "rw", timeEnd: "rw" + }; +} + +// Creates a custom loader instance whose console module is hooked in order +// to avoid printing messages to the console, and instead, expose them in the +// returned `messages` array attribute +exports.LoaderWithHookedConsole = function (module, callback) { + let messages = []; + function hook(type, innerID, msg) { + messages.push({ type: type, msg: msg, innerID: innerID }); + if (callback) + callback(type, msg, innerID); + } + + return { + loader: CustomLoader(module, { + console: new HookedPlainTextConsole(hook, null, null) + }, null, { + modules: { + 'sdk/console/plain-text': { + PlainTextConsole: HookedPlainTextConsole.bind(null, hook) + } + } + }), + messages: messages + }; +} + +// Same than LoaderWithHookedConsole with lower level, instead we get what is +// actually printed to the command line console +exports.LoaderWithHookedConsole2 = function (module, callback) { + let messages = []; + return { + loader: CustomLoader(module, { + console: new PlainTextConsole(function (msg) { + messages.push(msg); + if (callback) + callback(msg); + }) + }), + messages: messages + }; +} + +// Creates a custom loader with a filtered console. The callback is passed every +// console message type and message and if it returns false the message will +// not be logged normally +exports.LoaderWithFilteredConsole = function (module, callback) { + function hook(type, innerID, msg) { + if (callback && callback(type, msg, innerID) == false) + return; + console[type](msg); + } + + return CustomLoader(module, { + console: new HookedPlainTextConsole(hook, null, null) + }, null, { + modules: { + 'sdk/console/plain-text': { + PlainTextConsole: HookedPlainTextConsole.bind(null, hook) + } + } + }); +} diff --git a/toolkit/jetpack/sdk/test/memory.js b/toolkit/jetpack/sdk/test/memory.js new file mode 100644 index 000000000..bd1198bfe --- /dev/null +++ b/toolkit/jetpack/sdk/test/memory.js @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +const { Cu } = require("chrome"); + +function gc() { + return new Promise(resolve => Cu.schedulePreciseGC(resolve)); +} +exports.gc = gc; diff --git a/toolkit/jetpack/sdk/test/options.js b/toolkit/jetpack/sdk/test/options.js new file mode 100644 index 000000000..9bc611ca5 --- /dev/null +++ b/toolkit/jetpack/sdk/test/options.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const options = require("@test/options"); +const { id } = require("../self"); +const { get } = require("../preferences/service"); + +const readPref = (key) => get("extensions." + id + ".sdk." + key); + +exports.iterations = readPref("test.iterations") || options.iterations; +exports.filter = readPref("test.filter") || options.filter; +exports.profileMemory = readPref("profile.memory") || options.profileMemory; +exports.stopOnError = readPref("test.stop") || options.stopOnError; +exports.keepOpen = readPref("test.keepOpen") || false; +exports.verbose = (readPref("output.logLevel") == "verbose") || options.verbose; +exports.parseable = (readPref("output.format") == "tbpl") || options.parseable; +exports.checkMemory = readPref("profile.leaks") || options.check_memory; diff --git a/toolkit/jetpack/sdk/test/runner.js b/toolkit/jetpack/sdk/test/runner.js new file mode 100644 index 000000000..ea37ac84f --- /dev/null +++ b/toolkit/jetpack/sdk/test/runner.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +module.metadata = { + "stability": "experimental" +}; + +var { exit, stdout } = require("../system"); +var cfxArgs = require("../test/options"); +var events = require("../system/events"); +const { resolve } = require("../core/promise"); + +function runTests(findAndRunTests) { + var harness = require("./harness"); + + function onDone(tests) { + stdout.write("\n"); + var total = tests.passed + tests.failed; + stdout.write(tests.passed + " of " + total + " tests passed.\n"); + + events.emit("sdk:test:results", { data: JSON.stringify(tests) }); + + if (tests.failed == 0) { + if (tests.passed === 0) + stdout.write("No tests were run\n"); + if (!cfxArgs.keepOpen) + exit(0); + } else { + if (cfxArgs.verbose || cfxArgs.parseable) + printFailedTests(tests, stdout.write); + if (!cfxArgs.keepOpen) + exit(1); + } + }; + + // We may have to run test on next cycle, otherwise XPCOM components + // are not correctly updated. + // For ex: nsIFocusManager.getFocusedElementForWindow may throw + // NS_ERROR_ILLEGAL_VALUE exception. + require("../timers").setTimeout(_ => harness.runTests({ + findAndRunTests: findAndRunTests, + iterations: cfxArgs.iterations || 1, + filter: cfxArgs.filter, + profileMemory: cfxArgs.profileMemory, + stopOnError: cfxArgs.stopOnError, + verbose: cfxArgs.verbose, + parseable: cfxArgs.parseable, + print: stdout.write, + onDone: onDone + })); +} + +function printFailedTests(tests, print) { + let iterationNumber = 0; + let singleIteration = (tests.testRuns || []).length == 1; + let padding = singleIteration ? "" : " "; + + print("\nThe following tests failed:\n"); + + for (let testRun of tests.testRuns) { + iterationNumber++; + + if (!singleIteration) + print(" Iteration " + iterationNumber + ":\n"); + + for (let test of testRun) { + if (test.failed > 0) { + print(padding + " " + test.name + ": " + test.errors +"\n"); + } + } + print("\n"); + } +} + +function main() { + var testsStarted = false; + + if (!testsStarted) { + testsStarted = true; + runTests(function findAndRunTests(loader, nextIteration) { + loader.require("../deprecated/unit-test").findAndRunTests({ + testOutOfProcess: false, + testInProcess: true, + stopOnError: cfxArgs.stopOnError, + filter: cfxArgs.filter, + onDone: nextIteration + }); + }); + } +}; + +if (require.main === module) + main(); + +exports.runTestsFromModule = function runTestsFromModule(module) { + let id = module.id; + // Make a copy of exports as it may already be frozen by module loader + let exports = {}; + Object.keys(module.exports).forEach(key => { + exports[key] = module.exports[key]; + }); + + runTests(function findAndRunTests(loader, nextIteration) { + // Consider that all these tests are CommonJS ones + loader.require('../../test').run(exports); + + // Reproduce what is done in sdk/deprecated/unit-test-finder.findTests() + let tests = []; + for (let name of Object.keys(exports).sort()) { + tests.push({ + setup: exports.setup, + teardown: exports.teardown, + testFunction: exports[name], + name: id + "." + name + }); + } + + // Reproduce what is done by unit-test.findAndRunTests() + var { TestRunner } = loader.require("../deprecated/unit-test"); + var runner = new TestRunner(); + runner.startMany({ + tests: { + getNext: () => resolve(tests.shift()) + }, + stopOnError: cfxArgs.stopOnError, + onDone: nextIteration + }); + }); +} diff --git a/toolkit/jetpack/sdk/test/utils.js b/toolkit/jetpack/sdk/test/utils.js new file mode 100644 index 000000000..b01df67d4 --- /dev/null +++ b/toolkit/jetpack/sdk/test/utils.js @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.metadata = { + 'stability': 'unstable' +}; + +const { defer } = require('../core/promise'); +const { setInterval, clearInterval } = require('../timers'); +const { getTabs, closeTab } = require("../tabs/utils"); +const { windows: getWindows } = require("../window/utils"); +const { close: closeWindow } = require("../window/helpers"); +const { isGenerator } = require("../lang/type"); +const { env } = require("../system/environment"); +const { Task } = require("resource://gre/modules/Task.jsm"); + +const getTestNames = (exports) => + Object.keys(exports).filter(name => /^test/.test(name)); + +const isTestAsync = ({length}) => length > 1; +const isHelperAsync = ({length}) => length > 2; + +/* + * Takes an `exports` object of a test file and a function `beforeFn` + * to be run before each test. `beforeFn` is called with a `name` string + * as the first argument of the test name, and may specify a second + * argument function `done` to indicate that this function should + * resolve asynchronously + */ +function before (exports, beforeFn) { + getTestNames(exports).map(name => { + let testFn = exports[name]; + + // GENERATOR TESTS + if (isGenerator(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); + yield Task.spawn(testFn.bind(null, assert)); + } + } + else if (isGenerator(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function*(assert) { + beforeFn(name, assert); + yield Task.spawn(testFn.bind(null, assert)); + } + } + else if (isGenerator(testFn) && isHelperAsync(beforeFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => beforeFn(name, assert, resolve)); + yield Task.spawn(testFn.bind(null, assert)); + } + } + // SYNC TESTS + else if (!isTestAsync(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); + testFn(assert); + }; + } + else if (!isTestAsync(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function (assert) { + beforeFn(name, assert); + testFn(assert); + }; + } + else if (!isTestAsync(testFn) && isHelperAsync(beforeFn)) { + exports[name] = function (assert, done) { + beforeFn(name, assert, () => { + testFn(assert); + done(); + }); + }; + } + // ASYNC TESTS + else if (isTestAsync(testFn) && isGenerator(beforeFn)) { + exports[name] = function*(assert) { + yield Task.spawn(beforeFn.bind(null, name, assert)); + yield new Promise(resolve => testFn(assert, resolve)); + }; + } + else if (isTestAsync(testFn) && !isHelperAsync(beforeFn)) { + exports[name] = function (assert, done) { + beforeFn(name, assert); + testFn(assert, done); + }; + } + else if (isTestAsync(testFn) && isHelperAsync(beforeFn)) { + exports[name] = function (assert, done) { + beforeFn(name, assert, () => { + testFn(assert, done); + }); + }; + } + }); +} +exports.before = before; + +/* + * Takes an `exports` object of a test file and a function `afterFn` + * to be run after each test. `afterFn` is called with a `name` string + * as the first argument of the test name, and may specify a second + * argument function `done` to indicate that this function should + * resolve asynchronously + */ +function after (exports, afterFn) { + getTestNames(exports).map(name => { + let testFn = exports[name]; + + // GENERATOR TESTS + if (isGenerator(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + yield Task.spawn(afterFn.bind(null, name, assert)); + } + } + else if (isGenerator(testFn) && !isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + afterFn(name, assert); + } + } + else if (isGenerator(testFn) && isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield Task.spawn(testFn.bind(null, assert)); + yield new Promise(resolve => afterFn(name, assert, resolve)); + } + } + // SYNC TESTS + else if (!isTestAsync(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + testFn(assert); + yield Task.spawn(afterFn.bind(null, name, assert)); + }; + } + else if (!isTestAsync(testFn) && !isHelperAsync(afterFn)) { + exports[name] = function (assert) { + testFn(assert); + afterFn(name, assert); + }; + } + else if (!isTestAsync(testFn) && isHelperAsync(afterFn)) { + exports[name] = function (assert, done) { + testFn(assert); + afterFn(name, assert, done); + }; + } + // ASYNC TESTS + else if (isTestAsync(testFn) && isGenerator(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + yield Task.spawn(afterFn.bind(null, name, assert)); + }; + } + else if (isTestAsync(testFn) && !isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + afterFn(name, assert); + }; + } + else if (isTestAsync(testFn) && isHelperAsync(afterFn)) { + exports[name] = function*(assert) { + yield new Promise(resolve => testFn(assert, resolve)); + yield new Promise(resolve => afterFn(name, assert, resolve)); + }; + } + }); +} +exports.after = after; + +function waitUntil (predicate, delay) { + let { promise, resolve } = defer(); + let interval = setInterval(() => { + if (!predicate()) return; + clearInterval(interval); + resolve(); + }, delay || 10); + return promise; +} +exports.waitUntil = waitUntil; + +var cleanUI = function cleanUI() { + let { promise, resolve } = defer(); + + let windows = getWindows(null, { includePrivate: true }); + if (windows.length > 1) { + return closeWindow(windows[1]).then(cleanUI); + } + + getTabs(windows[0]).slice(1).forEach(closeTab); + + resolve(); + + return promise; +} +exports.cleanUI = cleanUI; + +exports.isTravisCI = ("TRAVIS" in env && "CI" in env); -- cgit v1.2.3