/* 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/. */ var EXPORTED_SYMBOLS = ['Collector','Runner','events', 'runTestFile', 'log', 'timers', 'persisted', 'shutdownApplication']; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; const TIMEOUT_SHUTDOWN_HTTPD = 15000; Cu.import("resource://gre/modules/Services.jsm"); Cu.import('resource://mozmill/stdlib/httpd.js'); var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker); var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions); var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors); var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os); var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings); var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays); var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs); var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); var securableModule = {}; Cu.import('resource://mozmill/stdlib/securable-module.js', securableModule); var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); var httpd = null; var persisted = {}; var assert = new assertions.Assert(); var expect = new assertions.Expect(); var mozmill = undefined; var mozelement = undefined; var modules = undefined; var timers = []; /** * Shutdown or restart the application * * @param {boolean} [aFlags=undefined] * Additional flags how to handle the shutdown or restart. The attributes * eRestarti386 and eRestartx86_64 have not been documented yet. * @see https://developer.mozilla.org/nsIAppStartup#Attributes */ function shutdownApplication(aFlags) { var flags = Ci.nsIAppStartup.eForceQuit; if (aFlags) { flags |= aFlags; } // Send a request to shutdown the application. That will allow us and other // components to finish up with any shutdown code. Please note that we don't // care if other components or add-ons want to prevent this via cancelQuit, // we really force the shutdown. let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]. createInstance(Components.interfaces.nsISupportsPRBool); Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null); // Use a timer to trigger the application restart, which will allow us to // send an ACK packet via jsbridge if the method has been called via Python. var event = { notify: function(timer) { Services.startup.quit(flags); } } var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback(event, 100, Ci.nsITimer.TYPE_ONE_SHOT); } function stateChangeBase(possibilties, restrictions, target, cmeta, v) { if (possibilties) { if (!arrays.inArray(possibilties, v)) { // TODO Error value not in this.poss return; } } if (restrictions) { for (var i in restrictions) { var r = restrictions[i]; if (!r(v)) { // TODO error value did not pass restriction return; } } } // Fire jsbridge notification, logging notification, listener notifications events[target] = v; events.fireEvent(cmeta, target); } var events = { appQuit : false, currentModule : null, currentState : null, currentTest : null, shutdownRequested : false, userShutdown : null, userShutdownTimer : null, listeners : {}, globalListeners : [] } events.setState = function (v) { return stateChangeBase(['dependencies', 'setupModule', 'teardownModule', 'test', 'setupTest', 'teardownTest', 'collection'], null, 'currentState', 'setState', v); } events.toggleUserShutdown = function (obj){ if (!this.userShutdown) { this.userShutdown = obj; var event = { notify: function(timer) { events.toggleUserShutdown(obj); } } this.userShutdownTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); this.userShutdownTimer.initWithCallback(event, obj.timeout, Ci.nsITimer.TYPE_ONE_SHOT); } else { this.userShutdownTimer.cancel(); // If the application is not going to shutdown, the user shutdown failed and // we have to force a shutdown. if (!events.appQuit) { this.fail({'function':'events.toggleUserShutdown', 'message':'Shutdown expected but none detected before timeout', 'userShutdown': obj}); var flags = Ci.nsIAppStartup.eAttemptQuit; if (events.isRestartShutdown()) { flags |= Ci.nsIAppStartup.eRestart; } shutdownApplication(flags); } } } events.isUserShutdown = function () { return this.userShutdown ? this.userShutdown["user"] : false; } events.isRestartShutdown = function () { return this.userShutdown.restart; } events.startShutdown = function (obj) { events.fireEvent('shutdown', obj); if (obj["user"]) { events.toggleUserShutdown(obj); } else { shutdownApplication(obj.flags); } } events.setTest = function (test) { test.__start__ = Date.now(); test.__passes__ = []; test.__fails__ = []; events.currentTest = test; var obj = {'filename': events.currentModule.__file__, 'name': test.__name__} events.fireEvent('setTest', obj); } events.endTest = function (test) { // use the current test unless specified if (test === undefined) { test = events.currentTest; } // If no test is set it has already been reported. Beside that we don't want // to report it a second time. if (!test || test.status === 'done') return; // report the end of a test test.__end__ = Date.now(); test.status = 'done'; var obj = {'filename': events.currentModule.__file__, 'passed': test.__passes__.length, 'failed': test.__fails__.length, 'passes': test.__passes__, 'fails' : test.__fails__, 'name' : test.__name__, 'time_start': test.__start__, 'time_end': test.__end__} if (test.skipped) { obj['skipped'] = true; obj.skipped_reason = test.skipped_reason; } if (test.meta) { obj.meta = test.meta; } // Report the test result only if the test is a true test or if it is failing if (withs.startsWith(test.__name__, "test") || test.__fails__.length > 0) { events.fireEvent('endTest', obj); } } events.setModule = function (aModule) { aModule.__start__ = Date.now(); aModule.__status__ = 'running'; var result = stateChangeBase(null, [function (aModule) {return (aModule.__file__ != undefined)}], 'currentModule', 'setModule', aModule); return result; } events.endModule = function (aModule) { // It should only reported once, so check if it already has been done if (aModule.__status__ === 'done') return; aModule.__end__ = Date.now(); aModule.__status__ = 'done'; var obj = { 'filename': aModule.__file__, 'time_start': aModule.__start__, 'time_end': aModule.__end__ } events.fireEvent('endModule', obj); } events.pass = function (obj) { // a low level event, such as a keystroke, succeeds if (events.currentTest) { events.currentTest.__passes__.push(obj); } for each (var timer in timers) { timer.actions.push( {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__, "obj": obj, "result": "pass"} ); } events.fireEvent('pass', obj); } events.fail = function (obj) { var error = obj.exception; if (error) { // Error objects aren't enumerable https://bugzilla.mozilla.org/show_bug.cgi?id=637207 obj.exception = { name: error.name, message: error.message, lineNumber: error.lineNumber, fileName: error.fileName, stack: error.stack }; } // a low level event, such as a keystroke, fails if (events.currentTest) { events.currentTest.__fails__.push(obj); } for each (var time in timers) { timer.actions.push( {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__, "obj": obj, "result": "fail"} ); } events.fireEvent('fail', obj); } events.skip = function (reason) { // this is used to report skips associated with setupModule and nothing else events.currentTest.skipped = true; events.currentTest.skipped_reason = reason; for (var timer of timers) { timer.actions.push( {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__, "obj": reason, "result": "skip"} ); } events.fireEvent('skip', reason); } events.fireEvent = function (name, obj) { if (events.appQuit) { // dump('* Event discarded: ' + name + ' ' + JSON.stringify(obj) + '\n'); return; } if (this.listeners[name]) { for (var i in this.listeners[name]) { this.listeners[name][i](obj); } } for each(var listener in this.globalListeners) { listener(name, obj); } } events.addListener = function (name, listener) { if (this.listeners[name]) { this.listeners[name].push(listener); } else if (name == '') { this.globalListeners.push(listener) } else { this.listeners[name] = [listener]; } } events.removeListener = function (listener) { for (var listenerIndex in this.listeners) { var e = this.listeners[listenerIndex]; for (var i in e){ if (e[i] == listener) { this.listeners[listenerIndex] = arrays.remove(e, i); } } } for (var i in this.globalListeners) { if (this.globalListeners[i] == listener) { this.globalListeners = arrays.remove(this.globalListeners, i); } } } events.persist = function () { try { events.fireEvent('persist', persisted); } catch (e) { events.fireEvent('error', "persist serialization failed.") } } events.firePythonCallback = function (obj) { obj['test'] = events.currentModule.__file__; events.fireEvent('firePythonCallback', obj); } events.screenshot = function (obj) { // Find the name of the test function for (var attr in events.currentModule) { if (events.currentModule[attr] == events.currentTest) { var testName = attr; break; } } obj['test_file'] = events.currentModule.__file__; obj['test_name'] = testName; events.fireEvent('screenshot', obj); } var log = function (obj) { events.fireEvent('log', obj); } // Register the listeners broker.addObject({'endTest': events.endTest, 'fail': events.fail, 'firePythonCallback': events.firePythonCallback, 'log': log, 'pass': events.pass, 'persist': events.persist, 'screenshot': events.screenshot, 'shutdown': events.startShutdown, }); try { Cu.import('resource://jsbridge/modules/Events.jsm'); events.addListener('', function (name, obj) { Events.fireEvent('mozmill.' + name, obj); }); } catch (e) { Services.console.logStringMessage("Event module of JSBridge not available."); } /** * Observer for notifications when the application is going to shutdown */ function AppQuitObserver() { this.runner = null; Services.obs.addObserver(this, "quit-application-requested", false); } AppQuitObserver.prototype = { observe: function (aSubject, aTopic, aData) { switch (aTopic) { case "quit-application-requested": Services.obs.removeObserver(this, "quit-application-requested"); // If we observe a quit notification make sure to send the // results of the current test. In those cases we don't reach // the equivalent code in runTestModule() events.pass({'message': 'AppQuitObserver: ' + JSON.stringify(aData), 'userShutdown': events.userShutdown}); if (this.runner) { this.runner.end(); } if (httpd) { httpd.stop(); } events.appQuit = true; break; } } } var appQuitObserver = new AppQuitObserver(); /** * The collector handles HTTPd.js and initilizing the module */ function Collector() { this.test_modules_by_filename = {}; this.testing = []; } Collector.prototype.addHttpResource = function (aDirectory, aPath) { var fp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); fp.initWithPath(os.abspath(aDirectory, this.current_file)); return httpd.addHttpResource(fp, aPath); } Collector.prototype.initTestModule = function (filename, testname) { var test_module = this.loadFile(filename, this); var has_restarted = !(testname == null); test_module.__tests__ = []; for (var i in test_module) { if (typeof(test_module[i]) == "function") { test_module[i].__name__ = i; // Only run setupModule if we are a single test OR if we are the first // test of a restart chain (don't run it prior to members in a restart // chain) if (i == "setupModule" && !has_restarted) { test_module.__setupModule__ = test_module[i]; } else if (i == "setupTest") { test_module.__setupTest__ = test_module[i]; } else if (i == "teardownTest") { test_module.__teardownTest__ = test_module[i]; } else if (i == "teardownModule") { test_module.__teardownModule__ = test_module[i]; } else if (withs.startsWith(i, "test")) { if (testname && (i != testname)) { continue; } testname = null; test_module.__tests__.push(test_module[i]); } } } test_module.collector = this; test_module.status = 'loaded'; this.test_modules_by_filename[filename] = test_module; return test_module; } Collector.prototype.loadFile = function (path, collector) { var moduleLoader = new securableModule.Loader({ rootPaths: ["resource://mozmill/modules/"], defaultPrincipal: "system", globals : { Cc: Cc, Ci: Ci, Cu: Cu, Cr: Components.results} }); // load a test module from a file and add some candy var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); file.initWithPath(path); var uri = Services.io.newFileURI(file).spec; this.loadTestResources(); var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); var module = new Components.utils.Sandbox(systemPrincipal); module.assert = assert; module.Cc = Cc; module.Ci = Ci; module.Cr = Components.results; module.Cu = Cu; module.collector = collector; module.driver = moduleLoader.require("driver"); module.elementslib = mozelement; module.errors = errors; module.expect = expect; module.findElement = mozelement; module.log = log; module.mozmill = mozmill; module.persisted = persisted; module.require = function (mod) { var loader = new securableModule.Loader({ rootPaths: [Services.io.newFileURI(file.parent).spec, "resource://mozmill/modules/"], defaultPrincipal: "system", globals : { assert: assert, expect: expect, mozmill: mozmill, elementslib: mozelement, // This a quick hack to maintain backwards compatibility with 1.5.x findElement: mozelement, persisted: persisted, Cc: Cc, Ci: Ci, Cu: Cu, log: log } }); if (modules != undefined) { loader.modules = modules; } var retval = loader.require(mod); modules = loader.modules; return retval; } if (collector != undefined) { collector.current_file = file; collector.current_path = path; } try { Services.scriptloader.loadSubScript(uri, module, "UTF-8"); } catch (e) { var obj = { 'filename': path, 'passed': 0, 'failed': 1, 'passes': [], 'fails' : [{'exception' : { message: e.message, filename: e.filename, lineNumber: e.lineNumber}}], 'name' :'' }; events.fail({'exception': e}); events.fireEvent('endTest', obj); } module.__file__ = path; module.__uri__ = uri; return module; } Collector.prototype.loadTestResources = function () { // load resources we want in our tests if (mozmill === undefined) { mozmill = {}; Cu.import("resource://mozmill/driver/mozmill.js", mozmill); } if (mozelement === undefined) { mozelement = {}; Cu.import("resource://mozmill/driver/mozelement.js", mozelement); } } /** * */ function Httpd(aPort) { this.http_port = aPort; while (true) { try { var srv = new HttpServer(); srv.registerContentType("sjs", "sjs"); srv.identity.setPrimary("http", "localhost", this.http_port); srv.start(this.http_port); this._httpd = srv; break; } catch (e) { // Failure most likely due to port conflict this.http_port++; } } } Httpd.prototype.addHttpResource = function (aDir, aPath) { var path = aPath ? ("/" + aPath + "/") : "/"; try { this._httpd.registerDirectory(path, aDir); return 'http://localhost:' + this.http_port + path; } catch (e) { throw Error("Failure to register directory: " + aDir.path); } }; Httpd.prototype.stop = function () { if (!this._httpd) { return; } var shutdown = false; this._httpd.stop(function () { shutdown = true; }); assert.waitFor(function () { return shutdown; }, "Local HTTP server has been stopped", TIMEOUT_SHUTDOWN_HTTPD); this._httpd = null; }; function startHTTPd() { if (!httpd) { // Ensure that we start the HTTP server only once during a session httpd = new Httpd(43336); } } function Runner() { this.collector = new Collector(); this.ended = false; var m = {}; Cu.import('resource://mozmill/driver/mozmill.js', m); this.platform = m.platform; events.fireEvent('startRunner', true); } Runner.prototype.end = function () { if (!this.ended) { this.ended = true; appQuitObserver.runner = null; events.endTest(); events.endModule(events.currentModule); events.fireEvent('endRunner', true); events.persist(); } }; Runner.prototype.runTestFile = function (filename, name) { var module = this.collector.initTestModule(filename, name); this.runTestModule(module); }; Runner.prototype.runTestModule = function (module) { appQuitObserver.runner = this; events.setModule(module); // If setupModule passes, run all the tests. Otherwise mark them as skipped. if (this.execFunction(module.__setupModule__, module)) { for (var test of module.__tests__) { if (events.shutdownRequested) { break; } // If setupTest passes, run the test. Otherwise mark it as skipped. if (this.execFunction(module.__setupTest__, module)) { this.execFunction(test); } else { this.skipFunction(test, module.__setupTest__.__name__ + " failed"); } this.execFunction(module.__teardownTest__, module); } } else { for (var test of module.__tests__) { this.skipFunction(test, module.__setupModule__.__name__ + " failed"); } } this.execFunction(module.__teardownModule__, module); events.endModule(module); }; Runner.prototype.execFunction = function (func, arg) { if (typeof func !== "function" || events.shutdownRequested) { return true; } var isTest = withs.startsWith(func.__name__, "test"); events.setState(isTest ? "test" : func.__name); events.setTest(func); // skip excluded platforms if (func.EXCLUDED_PLATFORMS != undefined) { if (arrays.inArray(func.EXCLUDED_PLATFORMS, this.platform)) { events.skip("Platform exclusion"); events.endTest(func); return false; } } // skip function if requested if (func.__force_skip__ != undefined) { events.skip(func.__force_skip__); events.endTest(func); return false; } // execute the test function try { func(arg); } catch (e) { if (e instanceof errors.ApplicationQuitError) { events.shutdownRequested = true; } else { events.fail({'exception': e, 'test': func}) } } // If a user shutdown has been requested and the function already returned, // we can assume that a shutdown will not happen anymore. We should force a // shutdown then, to prevent the next test from being executed. if (events.isUserShutdown()) { events.shutdownRequested = true; events.toggleUserShutdown(events.userShutdown); } events.endTest(func); return events.currentTest.__fails__.length == 0; }; function runTestFile(filename, name) { var runner = new Runner(); runner.runTestFile(filename, name); runner.end(); return true; } Runner.prototype.skipFunction = function (func, message) { events.setTest(func); events.skip(message); events.endTest(func); };