Wed, 31 Dec 2014 07:53:36 +0100
Correct small whitespace inconsistency, lost while renaming variables.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
5 var EXPORTED_SYMBOLS = ['Collector','Runner','events', 'runTestFile', 'log',
6 'timers', 'persisted', 'shutdownApplication'];
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10 const Cu = Components.utils;
12 const TIMEOUT_SHUTDOWN_HTTPD = 15000;
14 Cu.import("resource://gre/modules/Services.jsm");
16 Cu.import('resource://mozmill/stdlib/httpd.js');
18 var broker = {}; Cu.import('resource://mozmill/driver/msgbroker.js', broker);
19 var assertions = {}; Cu.import('resource://mozmill/modules/assertions.js', assertions);
20 var errors = {}; Cu.import('resource://mozmill/modules/errors.js', errors);
21 var os = {}; Cu.import('resource://mozmill/stdlib/os.js', os);
22 var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings);
23 var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays);
24 var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs);
25 var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils);
27 var securableModule = {};
28 Cu.import('resource://mozmill/stdlib/securable-module.js', securableModule);
30 var uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
32 var httpd = null;
33 var persisted = {};
35 var assert = new assertions.Assert();
37 var mozmill = undefined;
38 var mozelement = undefined;
39 var modules = undefined;
41 var timers = [];
44 /**
45 * Shutdown or restart the application
46 *
47 * @param {boolean} [aFlags=undefined]
48 * Additional flags how to handle the shutdown or restart. The attributes
49 * eRestarti386 and eRestartx86_64 have not been documented yet.
50 * @see https://developer.mozilla.org/nsIAppStartup#Attributes
51 */
52 function shutdownApplication(aFlags) {
53 var flags = Ci.nsIAppStartup.eForceQuit;
55 if (aFlags) {
56 flags |= aFlags;
57 }
59 // Send a request to shutdown the application. That will allow us and other
60 // components to finish up with any shutdown code. Please note that we don't
61 // care if other components or add-ons want to prevent this via cancelQuit,
62 // we really force the shutdown.
63 let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
64 createInstance(Components.interfaces.nsISupportsPRBool);
65 Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
67 // Use a timer to trigger the application restart, which will allow us to
68 // send an ACK packet via jsbridge if the method has been called via Python.
69 var event = {
70 notify: function(timer) {
71 Services.startup.quit(flags);
72 }
73 }
75 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
76 timer.initWithCallback(event, 100, Ci.nsITimer.TYPE_ONE_SHOT);
77 }
79 function stateChangeBase(possibilties, restrictions, target, cmeta, v) {
80 if (possibilties) {
81 if (!arrays.inArray(possibilties, v)) {
82 // TODO Error value not in this.poss
83 return;
84 }
85 }
87 if (restrictions) {
88 for (var i in restrictions) {
89 var r = restrictions[i];
90 if (!r(v)) {
91 // TODO error value did not pass restriction
92 return;
93 }
94 }
95 }
97 // Fire jsbridge notification, logging notification, listener notifications
98 events[target] = v;
99 events.fireEvent(cmeta, target);
100 }
103 var events = {
104 appQuit : false,
105 currentModule : null,
106 currentState : null,
107 currentTest : null,
108 shutdownRequested : false,
109 userShutdown : null,
110 userShutdownTimer : null,
112 listeners : {},
113 globalListeners : []
114 }
116 events.setState = function (v) {
117 return stateChangeBase(['dependencies', 'setupModule', 'teardownModule',
118 'test', 'setupTest', 'teardownTest', 'collection'],
119 null, 'currentState', 'setState', v);
120 }
122 events.toggleUserShutdown = function (obj){
123 if (!this.userShutdown) {
124 this.userShutdown = obj;
126 var event = {
127 notify: function(timer) {
128 events.toggleUserShutdown(obj);
129 }
130 }
132 this.userShutdownTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
133 this.userShutdownTimer.initWithCallback(event, obj.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
135 } else {
136 this.userShutdownTimer.cancel();
138 // If the application is not going to shutdown, the user shutdown failed and
139 // we have to force a shutdown.
140 if (!events.appQuit) {
141 this.fail({'function':'events.toggleUserShutdown',
142 'message':'Shutdown expected but none detected before timeout',
143 'userShutdown': obj});
145 var flags = Ci.nsIAppStartup.eAttemptQuit;
146 if (events.isRestartShutdown()) {
147 flags |= Ci.nsIAppStartup.eRestart;
148 }
150 shutdownApplication(flags);
151 }
152 }
153 }
155 events.isUserShutdown = function () {
156 return this.userShutdown ? this.userShutdown["user"] : false;
157 }
159 events.isRestartShutdown = function () {
160 return this.userShutdown.restart;
161 }
163 events.startShutdown = function (obj) {
164 events.fireEvent('shutdown', obj);
166 if (obj["user"]) {
167 events.toggleUserShutdown(obj);
168 } else {
169 shutdownApplication(obj.flags);
170 }
171 }
173 events.setTest = function (test) {
174 test.__start__ = Date.now();
175 test.__passes__ = [];
176 test.__fails__ = [];
178 events.currentTest = test;
180 var obj = {'filename': events.currentModule.__file__,
181 'name': test.__name__}
182 events.fireEvent('setTest', obj);
183 }
185 events.endTest = function (test) {
186 // use the current test unless specified
187 if (test === undefined) {
188 test = events.currentTest;
189 }
191 // If no test is set it has already been reported. Beside that we don't want
192 // to report it a second time.
193 if (!test || test.status === 'done')
194 return;
196 // report the end of a test
197 test.__end__ = Date.now();
198 test.status = 'done';
200 var obj = {'filename': events.currentModule.__file__,
201 'passed': test.__passes__.length,
202 'failed': test.__fails__.length,
203 'passes': test.__passes__,
204 'fails' : test.__fails__,
205 'name' : test.__name__,
206 'time_start': test.__start__,
207 'time_end': test.__end__}
209 if (test.skipped) {
210 obj['skipped'] = true;
211 obj.skipped_reason = test.skipped_reason;
212 }
214 if (test.meta) {
215 obj.meta = test.meta;
216 }
218 // Report the test result only if the test is a true test or if it is failing
219 if (withs.startsWith(test.__name__, "test") || test.__fails__.length > 0) {
220 events.fireEvent('endTest', obj);
221 }
222 }
224 events.setModule = function (aModule) {
225 aModule.__start__ = Date.now();
226 aModule.__status__ = 'running';
228 var result = stateChangeBase(null,
229 [function (aModule) {return (aModule.__file__ != undefined)}],
230 'currentModule', 'setModule', aModule);
232 return result;
233 }
235 events.endModule = function (aModule) {
236 // It should only reported once, so check if it already has been done
237 if (aModule.__status__ === 'done')
238 return;
240 aModule.__end__ = Date.now();
241 aModule.__status__ = 'done';
243 var obj = {
244 'filename': aModule.__file__,
245 'time_start': aModule.__start__,
246 'time_end': aModule.__end__
247 }
249 events.fireEvent('endModule', obj);
250 }
252 events.pass = function (obj) {
253 // a low level event, such as a keystroke, succeeds
254 if (events.currentTest) {
255 events.currentTest.__passes__.push(obj);
256 }
258 for each (var timer in timers) {
259 timer.actions.push(
260 {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
261 "obj": obj,
262 "result": "pass"}
263 );
264 }
266 events.fireEvent('pass', obj);
267 }
269 events.fail = function (obj) {
270 var error = obj.exception;
272 if (error) {
273 // Error objects aren't enumerable https://bugzilla.mozilla.org/show_bug.cgi?id=637207
274 obj.exception = {
275 name: error.name,
276 message: error.message,
277 lineNumber: error.lineNumber,
278 fileName: error.fileName,
279 stack: error.stack
280 };
281 }
283 // a low level event, such as a keystroke, fails
284 if (events.currentTest) {
285 events.currentTest.__fails__.push(obj);
286 }
288 for each (var time in timers) {
289 timer.actions.push(
290 {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
291 "obj": obj,
292 "result": "fail"}
293 );
294 }
296 events.fireEvent('fail', obj);
297 }
299 events.skip = function (reason) {
300 // this is used to report skips associated with setupModule and nothing else
301 events.currentTest.skipped = true;
302 events.currentTest.skipped_reason = reason;
304 for (var timer of timers) {
305 timer.actions.push(
306 {"currentTest": events.currentModule.__file__ + "::" + events.currentTest.__name__,
307 "obj": reason,
308 "result": "skip"}
309 );
310 }
312 events.fireEvent('skip', reason);
313 }
315 events.fireEvent = function (name, obj) {
316 if (events.appQuit) {
317 // dump('* Event discarded: ' + name + ' ' + JSON.stringify(obj) + '\n');
318 return;
319 }
321 if (this.listeners[name]) {
322 for (var i in this.listeners[name]) {
323 this.listeners[name][i](obj);
324 }
325 }
327 for each(var listener in this.globalListeners) {
328 listener(name, obj);
329 }
330 }
332 events.addListener = function (name, listener) {
333 if (this.listeners[name]) {
334 this.listeners[name].push(listener);
335 } else if (name == '') {
336 this.globalListeners.push(listener)
337 } else {
338 this.listeners[name] = [listener];
339 }
340 }
342 events.removeListener = function (listener) {
343 for (var listenerIndex in this.listeners) {
344 var e = this.listeners[listenerIndex];
346 for (var i in e){
347 if (e[i] == listener) {
348 this.listeners[listenerIndex] = arrays.remove(e, i);
349 }
350 }
351 }
353 for (var i in this.globalListeners) {
354 if (this.globalListeners[i] == listener) {
355 this.globalListeners = arrays.remove(this.globalListeners, i);
356 }
357 }
358 }
360 events.persist = function () {
361 try {
362 events.fireEvent('persist', persisted);
363 } catch (e) {
364 events.fireEvent('error', "persist serialization failed.")
365 }
366 }
368 events.firePythonCallback = function (obj) {
369 obj['test'] = events.currentModule.__file__;
370 events.fireEvent('firePythonCallback', obj);
371 }
373 events.screenshot = function (obj) {
374 // Find the name of the test function
375 for (var attr in events.currentModule) {
376 if (events.currentModule[attr] == events.currentTest) {
377 var testName = attr;
378 break;
379 }
380 }
382 obj['test_file'] = events.currentModule.__file__;
383 obj['test_name'] = testName;
384 events.fireEvent('screenshot', obj);
385 }
387 var log = function (obj) {
388 events.fireEvent('log', obj);
389 }
391 // Register the listeners
392 broker.addObject({'endTest': events.endTest,
393 'fail': events.fail,
394 'firePythonCallback': events.firePythonCallback,
395 'log': log,
396 'pass': events.pass,
397 'persist': events.persist,
398 'screenshot': events.screenshot,
399 'shutdown': events.startShutdown,
400 });
402 try {
403 Cu.import('resource://jsbridge/modules/Events.jsm');
405 events.addListener('', function (name, obj) {
406 Events.fireEvent('mozmill.' + name, obj);
407 });
408 } catch (e) {
409 Services.console.logStringMessage("Event module of JSBridge not available.");
410 }
413 /**
414 * Observer for notifications when the application is going to shutdown
415 */
416 function AppQuitObserver() {
417 this.runner = null;
419 Services.obs.addObserver(this, "quit-application-requested", false);
420 }
422 AppQuitObserver.prototype = {
423 observe: function (aSubject, aTopic, aData) {
424 switch (aTopic) {
425 case "quit-application-requested":
426 Services.obs.removeObserver(this, "quit-application-requested");
428 // If we observe a quit notification make sure to send the
429 // results of the current test. In those cases we don't reach
430 // the equivalent code in runTestModule()
431 events.pass({'message': 'AppQuitObserver: ' + JSON.stringify(aData),
432 'userShutdown': events.userShutdown});
434 if (this.runner) {
435 this.runner.end();
436 }
438 if (httpd) {
439 httpd.stop();
440 }
442 events.appQuit = true;
444 break;
445 }
446 }
447 }
449 var appQuitObserver = new AppQuitObserver();
451 /**
452 * The collector handles HTTPd.js and initilizing the module
453 */
454 function Collector() {
455 this.test_modules_by_filename = {};
456 this.testing = [];
457 }
459 Collector.prototype.addHttpResource = function (aDirectory, aPath) {
460 var fp = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
461 fp.initWithPath(os.abspath(aDirectory, this.current_file));
463 return httpd.addHttpResource(fp, aPath);
464 }
466 Collector.prototype.initTestModule = function (filename, testname) {
467 var test_module = this.loadFile(filename, this);
468 var has_restarted = !(testname == null);
469 test_module.__tests__ = [];
471 for (var i in test_module) {
472 if (typeof(test_module[i]) == "function") {
473 test_module[i].__name__ = i;
475 // Only run setupModule if we are a single test OR if we are the first
476 // test of a restart chain (don't run it prior to members in a restart
477 // chain)
478 if (i == "setupModule" && !has_restarted) {
479 test_module.__setupModule__ = test_module[i];
480 } else if (i == "setupTest") {
481 test_module.__setupTest__ = test_module[i];
482 } else if (i == "teardownTest") {
483 test_module.__teardownTest__ = test_module[i];
484 } else if (i == "teardownModule") {
485 test_module.__teardownModule__ = test_module[i];
486 } else if (withs.startsWith(i, "test")) {
487 if (testname && (i != testname)) {
488 continue;
489 }
491 testname = null;
492 test_module.__tests__.push(test_module[i]);
493 }
494 }
495 }
497 test_module.collector = this;
498 test_module.status = 'loaded';
500 this.test_modules_by_filename[filename] = test_module;
502 return test_module;
503 }
505 Collector.prototype.loadFile = function (path, collector) {
506 var moduleLoader = new securableModule.Loader({
507 rootPaths: ["resource://mozmill/modules/"],
508 defaultPrincipal: "system",
509 globals : { Cc: Cc,
510 Ci: Ci,
511 Cu: Cu,
512 Cr: Components.results}
513 });
515 // load a test module from a file and add some candy
516 var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
517 file.initWithPath(path);
518 var uri = Services.io.newFileURI(file).spec;
520 this.loadTestResources();
522 var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
523 var module = new Components.utils.Sandbox(systemPrincipal);
524 module.assert = new assertions.Assert();
525 module.Cc = Cc;
526 module.Ci = Ci;
527 module.Cr = Components.results;
528 module.Cu = Cu;
529 module.collector = collector;
530 module.driver = moduleLoader.require("driver");
531 module.elementslib = mozelement;
532 module.errors = errors;
533 module.expect = new assertions.Expect();
534 module.findElement = mozelement;
535 module.log = log;
536 module.mozmill = mozmill;
537 module.persisted = persisted;
539 module.require = function (mod) {
540 var loader = new securableModule.Loader({
541 rootPaths: [Services.io.newFileURI(file.parent).spec,
542 "resource://mozmill/modules/"],
543 defaultPrincipal: "system",
544 globals : { mozmill: mozmill,
545 elementslib: mozelement, // This a quick hack to maintain backwards compatibility with 1.5.x
546 findElement: mozelement,
547 persisted: persisted,
548 Cc: Cc,
549 Ci: Ci,
550 Cu: Cu,
551 log: log }
552 });
554 if (modules != undefined) {
555 loader.modules = modules;
556 }
558 var retval = loader.require(mod);
559 modules = loader.modules;
561 return retval;
562 }
564 if (collector != undefined) {
565 collector.current_file = file;
566 collector.current_path = path;
567 }
569 try {
570 Services.scriptloader.loadSubScript(uri, module, "UTF-8");
571 } catch (e) {
572 var obj = {
573 'filename': path,
574 'passed': 0,
575 'failed': 1,
576 'passes': [],
577 'fails' : [{'exception' : {
578 message: e.message,
579 filename: e.filename,
580 lineNumber: e.lineNumber}}],
581 'name' :'<TOP_LEVEL>'
582 };
584 events.fail({'exception': e});
585 events.fireEvent('endTest', obj);
586 }
588 module.__file__ = path;
589 module.__uri__ = uri;
591 return module;
592 }
594 Collector.prototype.loadTestResources = function () {
595 // load resources we want in our tests
596 if (mozmill === undefined) {
597 mozmill = {};
598 Cu.import("resource://mozmill/driver/mozmill.js", mozmill);
599 }
600 if (mozelement === undefined) {
601 mozelement = {};
602 Cu.import("resource://mozmill/driver/mozelement.js", mozelement);
603 }
604 }
607 /**
608 *
609 */
610 function Httpd(aPort) {
611 this.http_port = aPort;
613 while (true) {
614 try {
615 var srv = new HttpServer();
616 srv.registerContentType("sjs", "sjs");
617 srv.identity.setPrimary("http", "localhost", this.http_port);
618 srv.start(this.http_port);
620 this._httpd = srv;
621 break;
622 }
623 catch (e) {
624 // Failure most likely due to port conflict
625 this.http_port++;
626 }
627 }
628 }
630 Httpd.prototype.addHttpResource = function (aDir, aPath) {
631 var path = aPath ? ("/" + aPath + "/") : "/";
633 try {
634 this._httpd.registerDirectory(path, aDir);
635 return 'http://localhost:' + this.http_port + path;
636 }
637 catch (e) {
638 throw Error("Failure to register directory: " + aDir.path);
639 }
640 };
642 Httpd.prototype.stop = function () {
643 if (!this._httpd) {
644 return;
645 }
647 var shutdown = false;
648 this._httpd.stop(function () { shutdown = true; });
650 assert.waitFor(function () {
651 return shutdown;
652 }, "Local HTTP server has been stopped", TIMEOUT_SHUTDOWN_HTTPD);
654 this._httpd = null;
655 };
657 function startHTTPd() {
658 if (!httpd) {
659 // Ensure that we start the HTTP server only once during a session
660 httpd = new Httpd(43336);
661 }
662 }
665 function Runner() {
666 this.collector = new Collector();
667 this.ended = false;
669 var m = {}; Cu.import('resource://mozmill/driver/mozmill.js', m);
670 this.platform = m.platform;
672 events.fireEvent('startRunner', true);
673 }
675 Runner.prototype.end = function () {
676 if (!this.ended) {
677 this.ended = true;
679 appQuitObserver.runner = null;
681 events.endTest();
682 events.endModule(events.currentModule);
683 events.fireEvent('endRunner', true);
684 events.persist();
685 }
686 };
688 Runner.prototype.runTestFile = function (filename, name) {
689 var module = this.collector.initTestModule(filename, name);
690 this.runTestModule(module);
691 };
693 Runner.prototype.runTestModule = function (module) {
694 appQuitObserver.runner = this;
695 events.setModule(module);
697 // If setupModule passes, run all the tests. Otherwise mark them as skipped.
698 if (this.execFunction(module.__setupModule__, module)) {
699 for (var test of module.__tests__) {
700 if (events.shutdownRequested) {
701 break;
702 }
704 // If setupTest passes, run the test. Otherwise mark it as skipped.
705 if (this.execFunction(module.__setupTest__, module)) {
706 this.execFunction(test);
707 } else {
708 this.skipFunction(test, module.__setupTest__.__name__ + " failed");
709 }
711 this.execFunction(module.__teardownTest__, module);
712 }
714 } else {
715 for (var test of module.__tests__) {
716 this.skipFunction(test, module.__setupModule__.__name__ + " failed");
717 }
718 }
720 this.execFunction(module.__teardownModule__, module);
721 events.endModule(module);
722 };
724 Runner.prototype.execFunction = function (func, arg) {
725 if (typeof func !== "function" || events.shutdownRequested) {
726 return true;
727 }
729 var isTest = withs.startsWith(func.__name__, "test");
731 events.setState(isTest ? "test" : func.__name);
732 events.setTest(func);
734 // skip excluded platforms
735 if (func.EXCLUDED_PLATFORMS != undefined) {
736 if (arrays.inArray(func.EXCLUDED_PLATFORMS, this.platform)) {
737 events.skip("Platform exclusion");
738 events.endTest(func);
739 return false;
740 }
741 }
743 // skip function if requested
744 if (func.__force_skip__ != undefined) {
745 events.skip(func.__force_skip__);
746 events.endTest(func);
747 return false;
748 }
750 // execute the test function
751 try {
752 func(arg);
753 } catch (e) {
754 if (e instanceof errors.ApplicationQuitError) {
755 events.shutdownRequested = true;
756 } else {
757 events.fail({'exception': e, 'test': func})
758 }
759 }
761 // If a user shutdown has been requested and the function already returned,
762 // we can assume that a shutdown will not happen anymore. We should force a
763 // shutdown then, to prevent the next test from being executed.
764 if (events.isUserShutdown()) {
765 events.shutdownRequested = true;
766 events.toggleUserShutdown(events.userShutdown);
767 }
769 events.endTest(func);
770 return events.currentTest.__fails__.length == 0;
771 };
773 function runTestFile(filename, name) {
774 var runner = new Runner();
775 runner.runTestFile(filename, name);
776 runner.end();
778 return true;
779 }
781 Runner.prototype.skipFunction = function (func, message) {
782 events.setTest(func);
783 events.skip(message);
784 events.endTest(func);
785 };