diff --git a/testing/mochitest/Makefile.in b/testing/mochitest/Makefile.in
index 0e8e3c71469a..9a102df40cf0 100644
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -63,6 +63,7 @@ _SERV_FILES = \
harness.xul \
browser-test-overlay.xul \
browser-test.js \
+ cc-analyzer.js \
chrome-harness.js \
browser-harness.xul \
redirect.html \
diff --git a/testing/mochitest/browser-test-overlay.xul b/testing/mochitest/browser-test-overlay.xul
index bcc4ea606f04..932b783e833c 100644
--- a/testing/mochitest/browser-test-overlay.xul
+++ b/testing/mochitest/browser-test-overlay.xul
@@ -8,4 +8,5 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js
index 6a83754c8014..67573fd06c1b 100644
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -7,6 +7,12 @@ if (Cc === undefined) {
var Ci = Components.interfaces;
var Cu = Components.utils;
}
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
window.addEventListener("load", testOnLoad, false);
function testOnLoad() {
@@ -15,21 +21,18 @@ function testOnLoad() {
gConfig = readConfig();
if (gConfig.testRoot == "browser" || gConfig.testRoot == "webapprtChrome") {
// Make sure to launch the test harness for the first opened window only
- var prefs = Cc["@mozilla.org/preferences-service;1"].
- getService(Ci.nsIPrefBranch);
+ var prefs = Services.prefs;
if (prefs.prefHasUserValue("testing.browserTestHarness.running"))
return;
prefs.setBoolPref("testing.browserTestHarness.running", true);
- var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
- getService(Ci.nsIWindowWatcher);
var sstring = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
sstring.data = location.search;
- ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
- "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
+ Services.ww.openWindow(window, "chrome://mochikit/content/browser-harness.xul", "browserTest",
+ "chrome,centerscreen,dialog=no,resizable,titlebar,toolbar=no,width=800,height=600", sstring);
} else {
// This code allows us to redirect without requiring specialpowers for chrome and a11y tests.
function messageHandler(m) {
@@ -53,15 +56,10 @@ function Tester(aTests, aDumper, aCallback) {
this.dumper = aDumper;
this.tests = aTests;
this.callback = aCallback;
- this._cs = Cc["@mozilla.org/consoleservice;1"].
- getService(Ci.nsIConsoleService);
- this._wm = Cc["@mozilla.org/appshell/window-mediator;1"].
- getService(Ci.nsIWindowMediator);
- this._fm = Cc["@mozilla.org/focus-manager;1"].
- getService(Ci.nsIFocusManager);
+ this.openedWindows = {};
+ this.openedURLs = {};
- this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
- getService(Ci.mozIJSSubScriptLoader);
+ this._scriptLoader = Services.scriptloader;
this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils);
var simpleTestScope = {};
this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/specialpowersAPI.js", simpleTestScope);
@@ -79,6 +77,8 @@ Tester.prototype = {
checker: null,
currentTestIndex: -1,
lastStartTime: null,
+ openedWindows: null,
+
get currentTest() {
return this.tests[this.currentTestIndex];
},
@@ -92,7 +92,9 @@ Tester.prototype = {
gConfig = readConfig();
this.repeat = gConfig.repeat;
this.dumper.dump("*** Start BrowserChrome Test Results ***\n");
- this._cs.registerListener(this);
+ Services.console.registerListener(this);
+ Services.obs.addObserver(this, "chrome-document-global-created", false);
+ Services.obs.addObserver(this, "content-document-global-created", false);
this._globalProperties = Object.keys(window);
this._globalPropertyWhitelist = ["navigator", "constructor", "Application",
"__SS_tabsToRestore", "__SSi", "webConsoleCommandController",
@@ -124,7 +126,7 @@ Tester.prototype = {
}
this.dumper.dump("TEST-INFO | checking window state\n");
- let windowsEnum = this._wm.getEnumerator(null);
+ let windowsEnum = Services.wm.getEnumerator(null);
while (windowsEnum.hasMoreElements()) {
let win = windowsEnum.getNext();
if (win != window && !win.closed &&
@@ -159,7 +161,9 @@ Tester.prototype = {
this.nextTest();
}
else{
- this._cs.unregisterListener(this);
+ Services.console.unregisterListener(this);
+ Services.obs.removeObserver(this, "chrome-document-global-created");
+ Services.obs.removeObserver(this, "content-document-global-created");
this.dumper.dump("\nINFO TEST-START | Shutdown\n");
if (this.tests.length) {
@@ -186,10 +190,34 @@ Tester.prototype = {
this.callback(this.tests);
this.callback = null;
this.tests = null;
+ this.openedWindows = null;
}
},
- observe: function Tester_observe(aConsoleMessage) {
+ observe: function Tester_observe(aSubject, aTopic, aData) {
+ if (!aTopic) {
+ this.onConsoleMessage(aSubject);
+ } else if (this.currentTest) {
+ this.onDocumentCreated(aSubject);
+ }
+ },
+
+ onDocumentCreated: function Tester_onDocumentCreated(aWindow) {
+ let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let outerID = utils.outerWindowID;
+ let innerID = utils.currentInnerWindowID;
+
+ if (!(outerID in this.openedWindows)) {
+ this.openedWindows[outerID] = this.currentTest;
+ }
+ this.openedWindows[innerID] = this.currentTest;
+
+ let url = aWindow.location.href || "about:blank";
+ this.openedURLs[outerID] = this.openedURLs[innerID] = url;
+ },
+
+ onConsoleMessage: function Tester_onConsoleMessage(aConsoleMessage) {
// Ignore empty messages.
if (!aConsoleMessage.message)
return;
@@ -265,12 +293,20 @@ Tester.prototype = {
// Schedule GC and CC runs before finishing in order to detect
// DOM windows leaked by our tests or the tested code.
Cu.schedulePreciseGC((function () {
- let winutils = window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- winutils.garbageCollect();
- winutils.garbageCollect();
- winutils.garbageCollect();
- this.finish();
+ let analyzer = new CCAnalyzer();
+ analyzer.run(function () {
+ for (let obj of analyzer.find("nsGlobalWindow ")) {
+ let m = obj.name.match(/^nsGlobalWindow #(\d+)/);
+ if (m && m[1] in this.openedWindows) {
+ let test = this.openedWindows[m[1]];
+ let msg = "leaked until shutdown [" + obj.name +
+ " " + (this.openedURLs[m[1]] || "NULL") + "]";
+ test.addResult(new testResult(false, msg, "", false));
+ }
+ }
+
+ this.finish();
+ }.bind(this));
}).bind(this));
return;
}
@@ -462,9 +498,7 @@ function testScope(aTester, aTest) {
};
this.executeSoon = function test_executeSoon(func) {
- let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
-
- tm.mainThread.dispatch({
+ Services.tm.mainThread.dispatch({
run: function() {
func();
}
diff --git a/testing/mochitest/cc-analyzer.js b/testing/mochitest/cc-analyzer.js
new file mode 100644
index 000000000000..1b9988072fd0
--- /dev/null
+++ b/testing/mochitest/cc-analyzer.js
@@ -0,0 +1,126 @@
+/* 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/. */
+
+function CCAnalyzer() {
+}
+
+CCAnalyzer.prototype = {
+ clear: function () {
+ this.callback = null;
+ this.processingCount = 0;
+ this.graph = {};
+ this.roots = [];
+ this.garbage = [];
+ this.edges = [];
+ this.listener = null;
+ },
+
+ run: function (aCallback) {
+ this.clear();
+ this.callback = aCallback;
+
+ this.listener = Cc["@mozilla.org/cycle-collector-logger;1"].
+ createInstance(Ci.nsICycleCollectorListener);
+
+ this.listener.disableLog = true;
+ this.listener.wantAfterProcessing = true;
+
+ this.runCC(3);
+ },
+
+ runCC: function (aCounter) {
+ let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+
+ if (aCounter > 1) {
+ utils.garbageCollect();
+ setTimeout(this.runCC.bind(this, aCounter - 1), 0);
+ } else {
+ utils.garbageCollect(this.listener);
+ this.processLog();
+ }
+ },
+
+ processLog: function () {
+ // Process entire heap step by step in 5K chunks
+ for (let i = 0; i < 5000; i++) {
+ if (!this.listener.processNext(this)) {
+ this.callback();
+ this.clear();
+ return;
+ }
+ }
+
+ // Next chunk on timeout.
+ setTimeout(this.processLog.bind(this), 0);
+ },
+
+ noteRefCountedObject: function (aAddress, aRefCount, aObjectDescription) {
+ let o = this.ensureObject(aAddress);
+ o.address = aAddress;
+ o.refcount = aRefCount;
+ o.name = aObjectDescription;
+ },
+
+ noteGCedObject: function (aAddress, aMarked, aObjectDescription) {
+ let o = this.ensureObject(aAddress);
+ o.address = aAddress;
+ o.gcmarked = aMarked;
+ o.name = aObjectDescription;
+ },
+
+ noteEdge: function (aFromAddress, aToAddress, aEdgeName) {
+ let fromObject = this.ensureObject(aFromAddress);
+ let toObject = this.ensureObject(aToAddress);
+ fromObject.edges.push({name: aEdgeName, to: toObject});
+ toObject.owners.push({name: aEdgeName, from: fromObject});
+
+ this.edges.push({
+ name: aEdgeName,
+ from: fromObject,
+ to: toObject
+ });
+ },
+
+ describeRoot: function (aAddress, aKnownEdges) {
+ let o = this.ensureObject(aAddress);
+ o.root = true;
+ o.knownEdges = aKnownEdges;
+ this.roots.push(o);
+ },
+
+ describeGarbage: function (aAddress) {
+ let o = this.ensureObject(aAddress);
+ o.garbage = true;
+ this.garbage.push(o);
+ },
+
+ ensureObject: function (aAddress) {
+ if (!this.graph[aAddress])
+ this.graph[aAddress] = new CCObject();
+
+ return this.graph[aAddress];
+ },
+
+ find: function (aText) {
+ let result = [];
+ for each (let o in this.graph) {
+ if (!o.garbage && o.name.indexOf(aText) >= 0)
+ result.push(o);
+ }
+ return result;
+ }
+};
+
+function CCObject() {
+ this.name = "";
+ this.address = null;
+ this.refcount = 0;
+ this.gcmarked = false;
+ this.root = false;
+ this.garbage = false;
+ this.knownEdges = 0;
+ this.edges = [];
+ this.owners = [];
+}
diff --git a/testing/mochitest/jar.mn b/testing/mochitest/jar.mn
index 71b5928cfa24..f246defbe885 100644
--- a/testing/mochitest/jar.mn
+++ b/testing/mochitest/jar.mn
@@ -3,6 +3,7 @@ mochikit.jar:
content/browser-harness.xul (browser-harness.xul)
content/browser-test.js (browser-test.js)
content/browser-test-overlay.xul (browser-test-overlay.xul)
+ content/cc-analyzer.js (cc-analyzer.js)
content/chrome-harness.js (chrome-harness.js)
content/harness-overlay.xul (harness-overlay.xul)
content/harness.xul (harness.xul)