Merge mozilla-central to mozilla-inbound

This commit is contained in:
Ed Morley 2012-08-03 23:34:31 +01:00
commit d89c4cc96e
45 changed files with 1360 additions and 863 deletions

View File

@ -908,6 +908,11 @@ SourceScripts.prototype = {
this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
// Select the script if it's the preferred one.
if (aPacket.url === DebuggerView.Scripts.preferredScriptUrl) {
DebuggerView.Scripts.selectScript(aPacket.url);
}
// If there are any stored breakpoints for this script, display them again,
// both in the editor and the pane.
for each (let breakpoint in DebuggerController.Breakpoints.store) {
@ -926,6 +931,14 @@ SourceScripts.prototype = {
}
DebuggerView.Scripts.commitScripts();
DebuggerController.Breakpoints.updatePaneBreakpoints();
// Select the preferred script if one exists, the first entry otherwise.
let preferredScriptUrl = DebuggerView.Scripts.preferredScriptUrl;
if (preferredScriptUrl) {
DebuggerView.Scripts.selectScript(preferredScriptUrl);
} else {
DebuggerView.Scripts.selectIndex(0);
}
},
/**

View File

@ -239,6 +239,16 @@ ScriptsView.prototype = {
return false;
},
/**
* Selects the script with the specified index from the list.
*
* @param number aIndex
* The script index.
*/
selectIndex: function DVS_selectIndex(aIndex) {
this._scripts.selectedIndex = aIndex;
},
/**
* Selects the script with the specified URL from the list.
*
@ -277,6 +287,13 @@ ScriptsView.prototype = {
this._scripts.selectedItem.value : null;
},
/**
* Gets the most recently selected script url.
* @return string | null
*/
get preferredScriptUrl()
this._preferredScriptUrl ? this._preferredScriptUrl : null,
/**
* Returns the list of labels in the scripts container.
* @return array
@ -345,7 +362,7 @@ ScriptsView.prototype = {
}
}
// The script is alphabetically the last one.
this._createScriptElement(aLabel, aScript, -1, true);
this._createScriptElement(aLabel, aScript, -1);
},
/**
@ -365,7 +382,7 @@ ScriptsView.prototype = {
for (let i = 0, l = newScripts.length; i < l; i++) {
let item = newScripts[i];
this._createScriptElement(item.label, item.script, -1, true);
this._createScriptElement(item.label, item.script, -1);
}
},
@ -380,12 +397,8 @@ ScriptsView.prototype = {
* @param number aIndex
* The index where to insert to new script in the container.
* Pass -1 to append the script at the end.
* @param boolean aSelectIfEmptyFlag
* True to set the newly created script as the currently selected item
* if there are no other existing scripts in the container.
*/
_createScriptElement: function DVS__createScriptElement(
aLabel, aScript, aIndex, aSelectIfEmptyFlag)
_createScriptElement: function DVS__createScriptElement(aLabel, aScript, aIndex)
{
// Make sure we don't duplicate anything.
if (aLabel == "null" || this.containsLabel(aLabel) || this.contains(aScript.url)) {
@ -398,10 +411,6 @@ ScriptsView.prototype = {
scriptItem.setAttribute("tooltiptext", aScript.url);
scriptItem.setUserData("sourceScript", aScript, null);
if (this._scripts.itemCount == 1 && aSelectIfEmptyFlag) {
this._scripts.selectedItem = scriptItem;
}
},
/**
@ -437,6 +446,7 @@ ScriptsView.prototype = {
}
this._preferredScript = selectedItem;
this._preferredScriptUrl = selectedItem.value;
this._scripts.setAttribute("tooltiptext", selectedItem.value);
DebuggerController.SourceScripts.showScript(selectedItem.getUserData("sourceScript"));
},

View File

@ -35,6 +35,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_propertyview-09.js \
browser_dbg_propertyview-10.js \
browser_dbg_propertyview-edit.js \
browser_dbg_reload-same-script.js \
browser_dbg_panesize.js \
browser_dbg_panesize-inner.js \
browser_dbg_stack-01.js \

View File

@ -0,0 +1,126 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the same script is shown after a page is reloaded.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_script-switching.html";
let gPane = null;
let gTab = null;
let gDebuggee = null;
let gDebugger = null;
let gView = null;
function test()
{
let step = 0;
let scriptShown = false;
let scriptShownUrl = null;
let resumed = false;
let testStarted = false;
debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gDebuggee = aDebuggee;
gPane = aPane;
gDebugger = gPane.contentWindow;
gView = gDebugger.DebuggerView;
resumed = true;
executeSoon(startTest);
});
function onScriptShown(aEvent)
{
scriptShown = aEvent.detail.url.indexOf("-01.js") != -1;
scriptShownUrl = aEvent.detail.url;
executeSoon(startTest);
}
function onUlteriorScriptShown(aEvent)
{
scriptShownUrl = aEvent.detail.url;
executeSoon(testScriptShown);
}
window.addEventListener("Debugger:ScriptShown", onScriptShown);
function startTest()
{
if (scriptShown && resumed && !testStarted) {
window.removeEventListener("Debugger:ScriptShown", onScriptShown);
window.addEventListener("Debugger:ScriptShown", onUlteriorScriptShown);
testStarted = true;
Services.tm.currentThread.dispatch({ run: performTest }, 0);
}
}
function finishTest()
{
if (scriptShown && resumed && testStarted) {
window.removeEventListener("Debugger:ScriptShown", onUlteriorScriptShown);
closeDebuggerAndFinish();
}
}
function performTest()
{
testCurrentScript("-01.js", step);
step = 1;
reloadPage();
}
function testScriptShown()
{
if (step === 1) {
testCurrentScript("-01.js", step);
step = 2;
reloadPage();
}
else if (step === 2) {
testCurrentScript("-01.js", step);
step = 3;
gView.Scripts.selectScript(gView.Scripts.scriptLocations[1]);
}
else if (step === 3) {
testCurrentScript("-02.js", step);
step = 4;
reloadPage();
}
else if (step === 4) {
testCurrentScript("-02.js", step);
finishTest();
}
}
function testCurrentScript(part, step)
{
info("Currently preferred script: " + gView.Scripts.preferredScriptUrl);
info("Currently selected script: " + gView.Scripts.selected);
isnot(gView.Scripts.preferredScriptUrl.indexOf(part), -1,
"The preferred script url wasn't set correctly. (" + step + ")");
isnot(gView.Scripts.selected.indexOf(part), -1,
"The selected script isn't the correct one. (" + step + ")");
is(gView.Scripts.selected, scriptShownUrl,
"The shown script is not the the correct one. (" + step + ")");
}
function reloadPage()
{
executeSoon(function() {
gDebuggee.location.reload();
});
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebuggee = null;
gDebugger = null;
gView = null;
});
}

View File

@ -519,7 +519,11 @@ TreePanel.prototype = {
*/
closeEditor: function TP_closeEditor()
{
if (!this.treeBrowserDocument) // already closed, bug 706092
return;
let editor = this.treeBrowserDocument.getElementById("attribute-editor");
let editorInput =
this.treeBrowserDocument.getElementById("attribute-editor-input");

View File

@ -11,7 +11,11 @@ let div;
let editorTestSteps;
function doNextStep() {
editorTestSteps.next();
try {
editorTestSteps.next();
} catch(exception) {
info("caught:", exception);
}
}
function setupEditorTests()
@ -214,16 +218,33 @@ function doEditorTestSteps()
yield; // End of Step 7
// Step 8: validate that the editor was closed and that the editing was not saved
ok(!treePanel.editingContext, "Step 8: editor session ended");
editorVisible = editor.classList.contains("editing");
ok(!editorVisible, "editor popup hidden");
is(div.getAttribute("id"), "Hello World", "`id` attribute-value *not* updated");
is(attrValNode_id.innerHTML, "Hello World", "attribute-value node in HTML panel *not* updated");
executeSoon(doNextStep);
// End of Step 8
executeSoon(finishUp);
yield; // End of Step 8
// Step 9: Open the Editor and verify that closing the tree panel does not make the
// Inspector go cray-cray.
executeSoon(function() {
// firing 2 clicks right in a row to simulate a double-click
EventUtils.synthesizeMouse(attrValNode_id, 2, 2, {clickCount: 2}, attrValNode_id.ownerDocument.defaultView);
doNextStep();
});
yield; // End of Step 9
ok(treePanel.editingContext, "Step 9: editor session started");
editorVisible = editor.classList.contains("editing");
ok(editorVisible, "editor popup is visible");
executeSoon(function() {
InspectorUI.toggleHTMLPanel();
finishUp();
});
}
function finishUp() {

View File

@ -491,9 +491,24 @@ var Scratchpad = {
*/
writeAsErrorComment: function SP_writeAsErrorComment(aError)
{
let stack = aError.stack || aError.fileName + ":" + aError.lineNumber;
let newComment = "Exception: " + aError.message + "\n" + stack.replace(/\n$/, "");
let stack = "";
if (aError.stack) {
stack = aError.stack;
}
else if (aError.fileName) {
if (aError.lineNumber) {
stack = "@" + aError.fileName + ":" + aError.lineNumber;
}
else {
stack = "@" + aError.fileName;
}
}
else if (aError.lineNumber) {
stack = "@" + aError.lineNumber;
}
let newComment = "Exception: " + ( aError.message || aError) + ( stack == "" ? stack : "\n" + stack.replace(/\n$/, "") );
this.writeAsComment(newComment);
},

View File

@ -32,6 +32,7 @@ MOCHITEST_BROWSER_FILES = \
browser_scratchpad_bug714942_goto_line_ui.js \
browser_scratchpad_bug_650760_help_key.js \
browser_scratchpad_bug_651942_recent_files.js \
browser_scratchpad_bug756681_display_non_error_exceptions.js \
head.js \
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,104 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
openScratchpad(runTests, {"state":{"text":""}});
}, true);
content.location = "data:text/html, test that exceptions are output as " +
"comments correctly in Scratchpad";
}
function runTests()
{
var scratchpad = gScratchpadWindow.Scratchpad;
var message = "\"Hello World!\""
var openComment = "\n/*\n";
var closeComment = "\n*/";
var error1 = "throw new Error(\"Ouch!\")";
var error2 = "throw \"A thrown string\"";
var error3 = "throw {}";
var error4 = "document.body.appendChild(document.body)";
let messageArray = {};
let count = {};
// Display message
scratchpad.setText(message);
scratchpad.display();
is(scratchpad.getText(),
message + openComment + "Hello World!" + closeComment,
"message display output");
// Display error1, throw new Error("Ouch")
scratchpad.setText(error1);
scratchpad.display();
is(scratchpad.getText(),
error1 + openComment + "Exception: Ouch!\n@Scratchpad:1" + closeComment,
"error display output");
// Display error2, throw "A thrown string"
scratchpad.setText(error2);
scratchpad.display();
is(scratchpad.getText(),
error2 + openComment + "Exception: A thrown string" + closeComment,
"thrown string display output");
// Display error3, throw {}
scratchpad.setText(error3);
scratchpad.display();
is(scratchpad.getText(),
error3 + openComment + "Exception: [object Object]" + closeComment,
"thrown object display output");
// Display error4, document.body.appendChild(document.body)
scratchpad.setText(error4);
scratchpad.display();
is(scratchpad.getText(),
error4 + openComment + "Exception: Node cannot be inserted " +
"at the specified point in the hierarchy\n@1" + closeComment,
"Alternative format error display output");
// Run message
scratchpad.setText(message);
scratchpad.run();
is(scratchpad.getText(), message, "message run output");
// Run error1, throw new Error("Ouch")
scratchpad.setText(error1);
scratchpad.run();
is(scratchpad.getText(),
error1 + openComment + "Exception: Ouch!\n@Scratchpad:1" + closeComment,
"error run output");
// Run error2, throw "A thrown string"
scratchpad.setText(error2);
scratchpad.run();
is(scratchpad.getText(),
error2 + openComment + "Exception: A thrown string" + closeComment,
"thrown string run output");
// Run error3, throw {}
scratchpad.setText(error3);
scratchpad.run();
is(scratchpad.getText(),
error3 + openComment + "Exception: [object Object]" + closeComment,
"thrown object run output");
// Run error4, document.body.appendChild(document.body)
scratchpad.setText(error4);
scratchpad.run();
is(scratchpad.getText(),
error4 + openComment + "Exception: Node cannot be inserted " +
"at the specified point in the hierarchy\n@1" + closeComment,
"Alternative format error run output");
finish();
}

View File

@ -523,11 +523,11 @@ function JSTermHelper(aJSTerm)
* @param string aId
* The ID of the element you want.
* @return nsIDOMNode or null
* The result of calling document.getElementById(aId).
* The result of calling document.querySelector(aSelector).
*/
aJSTerm.sandbox.$ = function JSTH_$(aId)
aJSTerm.sandbox.$ = function JSTH_$(aSelector)
{
return aJSTerm.window.document.getElementById(aId);
return aJSTerm.window.document.querySelector(aSelector);
};
/**

View File

@ -611,7 +611,6 @@ WebConsole.prototype = {
let position = Services.prefs.getCharPref("devtools.webconsole.position");
this.positionConsole(position);
this._currentUIPosition = position;
},
/**
@ -622,8 +621,10 @@ WebConsole.prototype = {
{
this.iframe.removeEventListener("load", this._onIframeLoad, true);
let position = Services.prefs.getCharPref("devtools.webconsole.position");
this.iframeWindow = this.iframe.contentWindow.wrappedJSObject;
this.ui = new this.iframeWindow.WebConsoleFrame(this, this._currentUIPosition);
this.ui = new this.iframeWindow.WebConsoleFrame(this, position);
this._setupMessageManager();
},
@ -695,8 +696,6 @@ WebConsole.prototype = {
this.iframe.flex = 1;
panel.setAttribute("height", height);
this._afterPositionConsole("window", lastIndex);
}).bind(this);
panel.addEventListener("popupshown", onPopupShown,false);
@ -736,6 +735,9 @@ WebConsole.prototype = {
if (this.splitter.parentNode) {
this.splitter.parentNode.removeChild(this.splitter);
}
this._beforePositionConsole("window", lastIndex);
panel.appendChild(this.iframe);
let space = this.chromeDocument.createElement("spacer");
@ -822,6 +824,8 @@ WebConsole.prototype = {
this.splitter.parentNode.removeChild(this.splitter);
}
this._beforePositionConsole(aPosition, lastIndex);
if (aPosition == "below") {
nBox.appendChild(this.splitter);
nBox.appendChild(this.iframe);
@ -841,12 +845,10 @@ WebConsole.prototype = {
this.iframe.removeAttribute("height");
this.iframe.style.height = height + "px";
}
this._afterPositionConsole(aPosition, lastIndex);
},
/**
* Common code that needs to execute after the Web Console is repositioned.
* Common code that needs to execute before the Web Console is repositioned.
* @private
* @param string aPosition
* The new position: "above", "below" or "window".
@ -854,8 +856,8 @@ WebConsole.prototype = {
* The last visible message in the console output before repositioning
* occurred.
*/
_afterPositionConsole:
function WC__afterPositionConsole(aPosition, aLastIndex)
_beforePositionConsole:
function WC__beforePositionConsole(aPosition, aLastIndex)
{
if (!this.ui) {
return;

View File

@ -52,7 +52,7 @@ function testJSTerm(hud)
jsterm = hud.jsterm;
jsterm.clearOutput();
jsterm.execute("'id=' + $('header').getAttribute('id')");
jsterm.execute("'id=' + $('#header').getAttribute('id')");
checkResult('"id=header"', "$() worked", 1);
yield;

View File

@ -37,7 +37,7 @@ function waitForPosition(aPosition, aCallback) {
{
return hudRef._currentUIPosition == aPosition;
},
successFn: aCallback,
successFn: executeSoon.bind(null, aCallback),
failureFn: finishTest,
});
}
@ -55,9 +55,10 @@ function consoleOpened(aHudRef) {
"position menu checkbox is below");
is(Services.prefs.getCharPref(POSITION_PREF), "below", "pref is below");
hudRef.positionConsole("above");
waitForPosition("above", onPositionAbove);
executeSoon(function() {
hudRef.positionConsole("above");
waitForPosition("above", onPositionAbove);
});
}
function onPositionAbove() {
@ -81,8 +82,10 @@ function onPositionAbove() {
Services.prefs.setIntPref(TOP_PREF, 50);
Services.prefs.setIntPref(LEFT_PREF, 51);
hudRef.positionConsole("window");
waitForPosition("window", onPositionWindow);
executeSoon(function() {
hudRef.positionConsole("window");
waitForPosition("window", onPositionWindow);
});
}
function onPositionWindow() {

View File

@ -819,7 +819,7 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
self.log.info("Can't trigger Breakpad, just killing process")
proc.kill()
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger):
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
""" Look for timeout or crashes and return the status after the process terminates """
stackFixerProcess = None
stackFixerFunction = None
@ -856,8 +856,6 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
if stackFixerFunction:
line = stackFixerFunction(line)
self.log.info(line.rstrip().decode("UTF-8", "ignore"))
if logger:
logger.log(line)
if "TEST-START" in line and "|" in line:
self.lastTestSeen = line.split("|")[1].strip()
if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
@ -947,7 +945,7 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
def runApp(self, testURL, env, app, profileDir, extraArgs,
runSSLTunnel = False, utilityPath = None,
xrePath = None, certPath = None, logger = None,
xrePath = None, certPath = None,
debuggerInfo = None, symbolsPath = None,
timeout = -1, maxTime = None):
"""
@ -1006,7 +1004,7 @@ user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless t
stderr = subprocess.STDOUT)
self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger)
status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
# Do a final check for zombie child processes.

View File

@ -7,7 +7,6 @@ from __future__ import with_statement
import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
import re
from urlparse import urlparse
from operator import itemgetter
__all__ = [
"ZipFileReader",
@ -20,7 +19,6 @@ __all__ = [
"DEBUGGER_INFO",
"replaceBackSlashes",
"wrapCommand",
"ShutdownLeakLogger"
]
# Map of debugging programs to information about them, like default arguments
@ -419,115 +417,3 @@ def wrapCommand(cmd):
return ["arch", "-arch", "i386"] + cmd
# otherwise just execute the command normally
return cmd
class ShutdownLeakLogger(object):
"""
Parses the mochitest run log when running a debug build, assigns all leaked
DOM windows (that are still around after test suite shutdown, despite running
the GC) to the tests that created them and prints leak statistics.
"""
MAX_LEAK_COUNT = 5
def __init__(self, logger):
self.logger = logger
self.tests = []
self.leakedWindows = {}
self.leakedDocShells = set()
self.currentTest = None
self.seenShutdown = False
def log(self, line):
if line[2:11] == "DOMWINDOW":
self._logWindow(line)
elif line[2:10] == "DOCSHELL":
self._logDocShell(line)
elif line.startswith("TEST-START"):
fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "")
self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()}
elif line.startswith("INFO TEST-END"):
# don't track a test if no windows or docShells leaked
if self.currentTest and (self.currentTest["windows"] or self.currentTest["docShells"]):
self.tests.append(self.currentTest)
self.currentTest = None
elif line.startswith("INFO TEST-START | Shutdown"):
self.seenShutdown = True
def parse(self):
leakingTests = self._parseLeakingTests()
if leakingTests:
totalWindows = sum(len(test["leakedWindows"]) for test in leakingTests)
totalDocShells = sum(len(test["leakedDocShells"]) for test in leakingTests)
msgType = "TEST-INFO" if totalWindows + totalDocShells <= self.MAX_LEAK_COUNT else "TEST-UNEXPECTED-FAIL"
self.logger.info("%s | ShutdownLeaks | leaked %d DOMWindow(s) and %d DocShell(s) until shutdown", msgType, totalWindows, totalDocShells)
for test in leakingTests:
for url, count in self._zipLeakedWindows(test["leakedWindows"]):
self.logger.info("%s | %s | leaked %d window(s) until shutdown [url = %s]", msgType, test["fileName"], count, url)
if test["leakedDocShells"]:
self.logger.info("%s | %s | leaked %d docShell(s) until shutdown", msgType, test["fileName"], len(test["leakedDocShells"]))
def _logWindow(self, line):
created = line[:2] == "++"
id = self._parseValue(line, "serial")
# log line has invalid format
if not id:
return
if self.currentTest:
windows = self.currentTest["windows"]
if created:
windows.add(id)
else:
windows.discard(id)
elif self.seenShutdown and not created:
self.leakedWindows[id] = self._parseValue(line, "url")
def _logDocShell(self, line):
created = line[:2] == "++"
id = self._parseValue(line, "id")
# log line has invalid format
if not id:
return
if self.currentTest:
docShells = self.currentTest["docShells"]
if created:
docShells.add(id)
else:
docShells.discard(id)
elif self.seenShutdown and not created:
self.leakedDocShells.add(id)
def _parseValue(self, line, name):
match = re.search("\[%s = (.+?)\]" % name, line)
if match:
return match.group(1)
return None
def _parseLeakingTests(self):
leakingTests = []
for test in self.tests:
test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows]
test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells]
test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"])
if test["leakCount"]:
leakingTests.append(test)
return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True)
def _zipLeakedWindows(self, leakedWindows):
counts = []
counted = set()
for url in leakedWindows:
if not url in counted:
counts.append((url, leakedWindows.count(url)))
counted.add(url)
return sorted(counts, key=itemgetter(1), reverse=True)

View File

@ -62,7 +62,7 @@ class RemoteAutomation(Automation):
return env
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir, logger):
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir):
# maxTime is used to override the default timeout, we should honor that
status = proc.wait(timeout = maxTime)

View File

@ -1246,6 +1246,12 @@ NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsGlobalWindow)
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsGlobalWindow)
if (NS_UNLIKELY(cb.WantDebugInfo())) {
char name[512];
PR_snprintf(name, sizeof(name), "nsGlobalWindow #%ld", tmp->mWindowID);
cb.DescribeRefCountedNode(tmp->mRefCnt.get(), sizeof(nsGlobalWindow), name);
}
if (!cb.WantAllTraces() && tmp->IsBlackForCC()) {
return NS_SUCCESS_INTERRUPTED_TRAVERSE;
}

View File

@ -14,7 +14,7 @@
* function in the loaded actors in order to initialize properly.
*/
function createRootActor(aConnection) {
return new FennecRootActor(aConnection);
return new DeviceRootActor(aConnection);
}
/**
@ -26,18 +26,18 @@ function createRootActor(aConnection) {
* @param aConnection DebuggerServerConnection
* The conection to the client.
*/
function FennecRootActor(aConnection) {
function DeviceRootActor(aConnection) {
BrowserRootActor.call(this, aConnection);
}
FennecRootActor.prototype = new BrowserRootActor();
DeviceRootActor.prototype = new BrowserRootActor();
/**
* Handles the listTabs request. Builds a list of actors
* for the tabs running in the process. The actors will survive
* until at least the next listTabs request.
*/
FennecRootActor.prototype.onListTabs = function FRA_onListTabs() {
DeviceRootActor.prototype.onListTabs = function DRA_onListTabs() {
// Get actors for all the currently-running tabs (reusing
// existing actors where applicable), and store them in
// an ActorPool.
@ -92,7 +92,7 @@ FennecRootActor.prototype.onListTabs = function FRA_onListTabs() {
/**
* Return the tab container for the specified window.
*/
FennecRootActor.prototype.getTabContainer = function FRA_getTabContainer(aWindow) {
DeviceRootActor.prototype.getTabContainer = function DRA_getTabContainer(aWindow) {
return aWindow.document.getElementById("browsers");
};
@ -100,12 +100,12 @@ FennecRootActor.prototype.getTabContainer = function FRA_getTabContainer(aWindow
* When a tab is closed, exit its tab actor. The actor
* will be dropped at the next listTabs request.
*/
FennecRootActor.prototype.onTabClosed = function FRA_onTabClosed(aEvent) {
DeviceRootActor.prototype.onTabClosed = function DRA_onTabClosed(aEvent) {
this.exitTabActor(aEvent.target.browser);
};
// nsIWindowMediatorListener
FennecRootActor.prototype.onCloseWindow = function FRA_onCloseWindow(aWindow) {
DeviceRootActor.prototype.onCloseWindow = function DRA_onCloseWindow(aWindow) {
if (aWindow.BrowserApp) {
this.unwatchWindow(aWindow);
}
@ -114,6 +114,6 @@ FennecRootActor.prototype.onCloseWindow = function FRA_onCloseWindow(aWindow) {
/**
* The request types this actor can handle.
*/
FennecRootActor.prototype.requestTypes = {
"listTabs": FennecRootActor.prototype.onListTabs
DeviceRootActor.prototype.requestTypes = {
"listTabs": DeviceRootActor.prototype.onListTabs
};

View File

@ -32,7 +32,14 @@ libs::
TEST_DIRS += tests
TESTING_JS_MODULES := aitcserver.js storageserver.js
testing_modules := \
aitcserver.js \
storageserver.js \
utils.js \
$(NULL)
TESTING_JS_MODULES := $(foreach file,$(testing_modules),modules-testing/$(file))
TESTING_JS_MODULE_DIR := services-common
# What follows is a helper to launch a standalone storage server instance.

View File

@ -0,0 +1,42 @@
/* 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 EXPORTED_SYMBOLS = [
"TestingUtils",
];
let TestingUtils = {
/**
* Perform a deep copy of an Array or Object.
*/
deepCopy: function deepCopy(thing, noSort) {
if (typeof(thing) != "object" || thing == null) {
return thing;
}
if (Array.isArray(thing)) {
let ret = [];
for (let element of thing) {
ret.push(this.deepCopy(element, noSort));
}
return ret;
}
let ret = {};
let props = [p for (p in thing)];
if (!noSort) {
props = props.sort();
}
for (let prop of props) {
ret[prop] = this.deepCopy(thing[prop], noSort);
}
return ret;
},
};

View File

@ -1,8 +1,11 @@
Cu.import("resource://services-sync/util.js");
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://testing-common/services-common/utils.js");
function run_test() {
let thing = {o: {foo: "foo", bar: ["bar"]}, a: ["foo", {bar: "bar"}]};
let ret = deepCopy(thing);
let ret = TestingUtils.deepCopy(thing);
do_check_neq(ret, thing)
do_check_neq(ret.o, thing.o);
do_check_neq(ret.o.bar, thing.o.bar);

View File

@ -6,6 +6,7 @@ tail =
[test_load_modules.js]
[test_utils_atob.js]
[test_utils_deepCopy.js]
[test_utils_encodeBase32.js]
[test_utils_encodeBase64URL.js]
[test_utils_json.js]

View File

@ -0,0 +1,474 @@
/* 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 EXPORTED_SYMBOLS = ["AddonUtils"];
const {interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/log4moz.js");
Cu.import("resource://services-sync/util.js");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
"resource://gre/modules/AddonRepository.jsm");
function AddonUtilsInternal() {
this._log = Log4Moz.repository.getLogger("Sync.AddonUtils");
this._log.Level = Log4Moz.Level[Svc.Prefs.get("log.logger.addonutils")];
}
AddonUtilsInternal.prototype = {
/**
* Obtain an AddonInstall object from an AddonSearchResult instance.
*
* The callback will be invoked with the result of the operation. The
* callback receives 2 arguments, error and result. Error will be falsy
* on success or some kind of error value otherwise. The result argument
* will be an AddonInstall on success or null on failure. It is possible
* for the error to be falsy but result to be null. This could happen if
* an install was not found.
*
* @param addon
* AddonSearchResult to obtain install from.
* @param cb
* Function to be called with result of operation.
*/
getInstallFromSearchResult:
function getInstallFromSearchResult(addon, cb, requireSecureURI=true) {
this._log.debug("Obtaining install for " + addon.id);
// Verify that the source URI uses TLS. We don't allow installs from
// insecure sources for security reasons. The Addon Manager ensures that
// cert validation, etc is performed.
if (requireSecureURI) {
let scheme = addon.sourceURI.scheme;
if (scheme != "https") {
cb(new Error("Insecure source URI scheme: " + scheme), addon.install);
return;
}
}
// We should theoretically be able to obtain (and use) addon.install if
// it is available. However, the addon.sourceURI rewriting won't be
// reflected in the AddonInstall, so we can't use it. If we ever get rid
// of sourceURI rewriting, we can avoid having to reconstruct the
// AddonInstall.
AddonManager.getInstallForURL(
addon.sourceURI.spec,
function handleInstall(install) {
cb(null, install);
},
"application/x-xpinstall",
undefined,
addon.name,
addon.iconURL,
addon.version
);
},
/**
* Installs an add-on from an AddonSearchResult instance.
*
* The options argument defines extra options to control the install.
* Recognized keys in this map are:
*
* syncGUID - Sync GUID to use for the new add-on.
* enabled - Boolean indicating whether the add-on should be enabled upon
* install.
* requireSecureURI - Boolean indicating whether to require a secure
* URI to install from. This defaults to true.
*
* When complete it calls a callback with 2 arguments, error and result.
*
* If error is falsy, result is an object. If error is truthy, result is
* null.
*
* The result object has the following keys:
*
* id ID of add-on that was installed.
* install AddonInstall that was installed.
* addon Addon that was installed.
*
* @param addon
* AddonSearchResult to install add-on from.
* @param options
* Object with additional metadata describing how to install add-on.
* @param cb
* Function to be invoked with result of operation.
*/
installAddonFromSearchResult:
function installAddonFromSearchResult(addon, options, cb) {
this._log.info("Trying to install add-on from search result: " + addon.id);
if (options.requireSecureURI === undefined) {
options.requireSecureURI = true;
}
this.getInstallFromSearchResult(addon, function onResult(error, install) {
if (error) {
cb(error, null);
return;
}
if (!install) {
cb(new Error("AddonInstall not available: " + addon.id), null);
return;
}
try {
this._log.info("Installing " + addon.id);
let log = this._log;
let listener = {
onInstallStarted: function onInstallStarted(install) {
if (!options) {
return;
}
if (options.syncGUID) {
log.info("Setting syncGUID of " + install.name +": " +
options.syncGUID);
install.addon.syncGUID = options.syncGUID;
}
// We only need to change userDisabled if it is disabled because
// enabled is the default.
if ("enabled" in options && !options.enabled) {
log.info("Marking add-on as disabled for install: " +
install.name);
install.addon.userDisabled = true;
}
},
onInstallEnded: function(install, addon) {
install.removeListener(listener);
cb(null, {id: addon.id, install: install, addon: addon});
},
onInstallFailed: function(install) {
install.removeListener(listener);
cb(new Error("Install failed: " + install.error), null);
},
onDownloadFailed: function(install) {
install.removeListener(listener);
cb(new Error("Download failed: " + install.error), null);
}
};
install.addListener(listener);
install.install();
}
catch (ex) {
this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
cb(ex, null);
}
}.bind(this), options.requireSecureURI);
},
/**
* Uninstalls the Addon instance and invoke a callback when it is done.
*
* @param addon
* Addon instance to uninstall.
* @param cb
* Function to be invoked when uninstall has finished. It receives a
* truthy value signifying error and the add-on which was uninstalled.
*/
uninstallAddon: function uninstallAddon(addon, cb) {
let listener = {
onUninstalling: function(uninstalling, needsRestart) {
if (addon.id != uninstalling.id) {
return;
}
// We assume restartless add-ons will send the onUninstalled event
// soon.
if (!needsRestart) {
return;
}
// For non-restartless add-ons, we issue the callback on uninstalling
// because we will likely never see the uninstalled event.
AddonManager.removeAddonListener(listener);
cb(null, addon);
},
onUninstalled: function(uninstalled) {
if (addon.id != uninstalled.id) {
return;
}
AddonManager.removeAddonListener(listener);
cb(null, addon);
}
};
AddonManager.addAddonListener(listener);
addon.uninstall();
},
/**
* Installs multiple add-ons specified by metadata.
*
* The first argument is an array of objects. Each object must have the
* following keys:
*
* id - public ID of the add-on to install.
* syncGUID - syncGUID for new add-on.
* enabled - boolean indicating whether the add-on should be enabled.
* requireSecureURI - Boolean indicating whether to require a secure
* URI when installing from a remote location. This defaults to
* true.
*
* The callback will be called when activity on all add-ons is complete. The
* callback receives 2 arguments, error and result.
*
* If error is truthy, it contains a string describing the overall error.
*
* The 2nd argument to the callback is always an object with details on the
* overall execution state. It contains the following keys:
*
* installedIDs Array of add-on IDs that were installed.
* installs Array of AddonInstall instances that were installed.
* addons Array of Addon instances that were installed.
* errors Array of errors encountered. Only has elements if error is
* truthy.
*
* @param installs
* Array of objects describing add-ons to install.
* @param cb
* Function to be called when all actions are complete.
*/
installAddons: function installAddons(installs, cb) {
if (!cb) {
throw new Error("Invalid argument: cb is not defined.");
}
let ids = [];
for each (let addon in installs) {
ids.push(addon.id);
}
AddonRepository.getAddonsByIDs(ids, {
searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
this._log.info("Found " + addonsLength + "/" + ids.length +
" add-ons during repository search.");
let ourResult = {
installedIDs: [],
installs: [],
addons: [],
errors: []
};
if (!addonsLength) {
cb(null, ourResult);
return;
}
let expectedInstallCount = 0;
let finishedCount = 0;
let installCallback = function installCallback(error, result) {
finishedCount++;
if (error) {
ourResult.errors.push(error);
} else {
ourResult.installedIDs.push(result.id);
ourResult.installs.push(result.install);
ourResult.addons.push(result.addon);
}
if (finishedCount >= expectedInstallCount) {
if (ourResult.errors.length > 0) {
cb(new Error("1 or more add-ons failed to install"), ourResult);
} else {
cb(null, ourResult);
}
}
}.bind(this);
let toInstall = [];
// Rewrite the "src" query string parameter of the source URI to note
// that the add-on was installed by Sync and not something else so
// server-side metrics aren't skewed (bug 708134). The server should
// ideally send proper URLs, but this solution was deemed too
// complicated at the time the functionality was implemented.
for each (let addon in addons) {
// sourceURI presence isn't enforced by AddonRepository. So, we skip
// add-ons without a sourceURI.
if (!addon.sourceURI) {
this._log.info("Skipping install of add-on because missing " +
"sourceURI: " + addon.id);
continue;
}
toInstall.push(addon);
// We should always be able to QI the nsIURI to nsIURL. If not, we
// still try to install the add-on, but we don't rewrite the URL,
// potentially skewing metrics.
try {
addon.sourceURI.QueryInterface(Ci.nsIURL);
} catch (ex) {
this._log.warn("Unable to QI sourceURI to nsIURL: " +
addon.sourceURI.spec);
continue;
}
let params = addon.sourceURI.query.split("&").map(
function rewrite(param) {
if (param.indexOf("src=") == 0) {
return "src=sync";
} else {
return param;
}
});
addon.sourceURI.query = params.join("&");
}
expectedInstallCount = toInstall.length;
if (!expectedInstallCount) {
cb(null, ourResult);
return;
}
// Start all the installs asynchronously. They will report back to us
// as they finish, eventually triggering the global callback.
for each (let addon in toInstall) {
let options = {};
for each (let install in installs) {
if (install.id == addon.id) {
options = install;
break;
}
}
this.installAddonFromSearchResult(addon, options, installCallback);
}
}.bind(this),
searchFailed: function searchFailed() {
cb(new Error("AddonRepository search failed"), null);
},
});
},
/**
* Update the user disabled flag for an add-on.
*
* The supplied callback will ba called when the operation is
* complete. If the new flag matches the existing or if the add-on
* isn't currently active, the function will fire the callback
* immediately. Else, the callback is invoked when the AddonManager
* reports the change has taken effect or has been registered.
*
* The callback receives as arguments:
*
* (Error) Encountered error during operation or null on success.
* (Addon) The add-on instance being operated on.
*
* @param addon
* (Addon) Add-on instance to operate on.
* @param value
* (bool) New value for add-on's userDisabled property.
* @param cb
* (function) Callback to be invoked on completion.
*/
updateUserDisabled: function updateUserDisabled(addon, value, cb) {
if (addon.userDisabled == value) {
cb(null, addon);
return;
}
let listener = {
onEnabling: function onEnabling(wrapper, needsRestart) {
this._log.debug("onEnabling: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
// We ignore the restartless case because we'll get onEnabled shortly.
if (!needsRestart) {
return;
}
AddonManager.removeAddonListener(listener);
cb(null, wrapper);
}.bind(this),
onEnabled: function onEnabled(wrapper) {
this._log.debug("onEnabled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
cb(null, wrapper);
}.bind(this),
onDisabling: function onDisabling(wrapper, needsRestart) {
this._log.debug("onDisabling: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
if (!needsRestart) {
return;
}
AddonManager.removeAddonListener(listener);
cb(null, wrapper);
}.bind(this),
onDisabled: function onDisabled(wrapper) {
this._log.debug("onDisabled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
cb(null, wrapper);
}.bind(this),
onOperationCancelled: function onOperationCancelled(wrapper) {
this._log.debug("onOperationCancelled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
cb(new Error("Operation cancelled"), wrapper);
}.bind(this)
};
// The add-on listeners are only fired if the add-on is active. If not, the
// change is silently updated and made active when/if the add-on is active.
if (!addon.appDisabled) {
AddonManager.addAddonListener(listener);
}
this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
addon.userDisabled = !!value;
if (!addon.appDisabled) {
cb(null, addon);
return;
}
// Else the listener will handle invoking the callback.
},
};
XPCOMUtils.defineLazyGetter(this, "AddonUtils", function() {
return new AddonUtilsInternal();
});

View File

@ -35,6 +35,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/addonsreconciler.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
@ -291,10 +292,11 @@ AddonsStore.prototype = {
*/
create: function create(record) {
let cb = Async.makeSpinningCallback();
this.installAddons([{
id: record.addonID,
syncGUID: record.id,
enabled: record.enabled
AddonUtils.installAddons([{
id: record.addonID,
syncGUID: record.id,
enabled: record.enabled,
requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false),
}], cb);
// This will throw if there was an error. This will get caught by the sync
@ -332,7 +334,7 @@ AddonsStore.prototype = {
this._log.info("Uninstalling add-on: " + addon.id);
let cb = Async.makeSpinningCallback();
this.uninstallAddon(addon, cb);
AddonUtils.uninstallAddon(addon, cb);
cb.wait();
},
@ -607,185 +609,6 @@ AddonsStore.prototype = {
return true;
},
/**
* Obtain an AddonInstall object from an AddonSearchResult instance.
*
* The callback will be invoked with the result of the operation. The
* callback receives 2 arguments, error and result. Error will be falsy
* on success or some kind of error value otherwise. The result argument
* will be an AddonInstall on success or null on failure. It is possible
* for the error to be falsy but result to be null. This could happen if
* an install was not found.
*
* @param addon
* AddonSearchResult to obtain install from.
* @param cb
* Function to be called with result of operation.
*/
getInstallFromSearchResult: function getInstallFromSearchResult(addon, cb) {
// We should theoretically be able to obtain (and use) addon.install if
// it is available. However, the addon.sourceURI rewriting won't be
// reflected in the AddonInstall, so we can't use it. If we ever get rid
// of sourceURI rewriting, we can avoid having to reconstruct the
// AddonInstall.
this._log.debug("Obtaining install for " + addon.id);
// Verify that the source URI uses TLS. We don't allow installs from
// insecure sources for security reasons. The Addon Manager ensures that
// cert validation, etc is performed.
if (!Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
let scheme = addon.sourceURI.scheme;
if (scheme != "https") {
cb(new Error("Insecure source URI scheme: " + scheme), addon.install);
}
}
AddonManager.getInstallForURL(
addon.sourceURI.spec,
function handleInstall(install) {
cb(null, install);
},
"application/x-xpinstall",
undefined,
addon.name,
addon.iconURL,
addon.version
);
},
/**
* Installs an add-on from an AddonSearchResult instance.
*
* The options argument defines extra options to control the install.
* Recognized keys in this map are:
*
* syncGUID - Sync GUID to use for the new add-on.
* enabled - Boolean indicating whether the add-on should be enabled upon
* install.
*
* When complete it calls a callback with 2 arguments, error and result.
*
* If error is falsy, result is an object. If error is truthy, result is
* null.
*
* The result object has the following keys:
*
* id ID of add-on that was installed.
* install AddonInstall that was installed.
* addon Addon that was installed.
*
* @param addon
* AddonSearchResult to install add-on from.
* @param options
* Object with additional metadata describing how to install add-on.
* @param cb
* Function to be invoked with result of operation.
*/
installAddonFromSearchResult:
function installAddonFromSearchResult(addon, options, cb) {
this._log.info("Trying to install add-on from search result: " + addon.id);
this.getInstallFromSearchResult(addon, function(error, install) {
if (error) {
cb(error, null);
return;
}
if (!install) {
cb(new Error("AddonInstall not available: " + addon.id), null);
return;
}
try {
this._log.info("Installing " + addon.id);
let log = this._log;
let listener = {
onInstallStarted: function(install) {
if (!options) {
return;
}
if (options.syncGUID) {
log.info("Setting syncGUID of " + install.name +": " +
options.syncGUID);
install.addon.syncGUID = options.syncGUID;
}
// We only need to change userDisabled if it is disabled because
// enabled is the default.
if ("enabled" in options && !options.enabled) {
log.info("Marking add-on as disabled for install: " +
install.name);
install.addon.userDisabled = true;
}
},
onInstallEnded: function(install, addon) {
install.removeListener(listener);
cb(null, {id: addon.id, install: install, addon: addon});
},
onInstallFailed: function(install) {
install.removeListener(listener);
cb(new Error("Install failed: " + install.error), null);
},
onDownloadFailed: function(install) {
install.removeListener(listener);
cb(new Error("Download failed: " + install.error), null);
}
};
install.addListener(listener);
install.install();
}
catch (ex) {
this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
cb(ex, null);
}
}.bind(this));
},
/**
* Uninstalls the Addon instance and invoke a callback when it is done.
*
* @param addon
* Addon instance to uninstall.
* @param callback
* Function to be invoked when uninstall has finished. It receives a
* truthy value signifying error and the add-on which was uninstalled.
*/
uninstallAddon: function uninstallAddon(addon, callback) {
let listener = {
onUninstalling: function(uninstalling, needsRestart) {
if (addon.id != uninstalling.id) {
return;
}
// We assume restartless add-ons will send the onUninstalled event
// soon.
if (!needsRestart) {
return;
}
// For non-restartless add-ons, we issue the callback on uninstalling
// because we will likely never see the uninstalled event.
AddonManager.removeAddonListener(listener);
callback(null, addon);
},
onUninstalled: function(uninstalled) {
if (addon.id != uninstalled.id) {
return;
}
AddonManager.removeAddonListener(listener);
callback(null, addon);
}
};
AddonManager.addAddonListener(listener);
addon.uninstall();
},
/**
* Update the userDisabled flag on an add-on.
*
@ -816,232 +639,8 @@ AddonsStore.prototype = {
return;
}
let listener = {
onEnabling: function onEnabling(wrapper, needsRestart) {
this._log.debug("onEnabling: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
// We ignore the restartless case because we'll get onEnabled shortly.
if (!needsRestart) {
return;
}
AddonManager.removeAddonListener(listener);
callback(null, wrapper);
}.bind(this),
onEnabled: function onEnabled(wrapper) {
this._log.debug("onEnabled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
callback(null, wrapper);
}.bind(this),
onDisabling: function onDisabling(wrapper, needsRestart) {
this._log.debug("onDisabling: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
if (!needsRestart) {
return;
}
AddonManager.removeAddonListener(listener);
callback(null, wrapper);
}.bind(this),
onDisabled: function onDisabled(wrapper) {
this._log.debug("onDisabled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
callback(null, wrapper);
}.bind(this),
onOperationCancelled: function onOperationCancelled(wrapper) {
this._log.debug("onOperationCancelled: " + wrapper.id);
if (wrapper.id != addon.id) {
return;
}
AddonManager.removeAddonListener(listener);
callback(new Error("Operation cancelled"), wrapper);
}.bind(this)
};
// The add-on listeners are only fired if the add-on is active. If not, the
// change is silently updated and made active when/if the add-on is active.
if (!addon.appDisabled) {
AddonManager.addAddonListener(listener);
}
this._log.info("Updating userDisabled flag: " + addon.id + " -> " + value);
addon.userDisabled = !!value;
if (!addon.appDisabled) {
callback(null, addon);
return;
}
// Else the listener will handle invoking the callback.
AddonUtils.updateUserDisabled(addon, value, callback);
},
/**
* Installs multiple add-ons specified by metadata.
*
* The first argument is an array of objects. Each object must have the
* following keys:
*
* id - public ID of the add-on to install.
* syncGUID - syncGUID for new add-on.
* enabled - boolean indicating whether the add-on should be enabled.
*
* The callback will be called when activity on all add-ons is complete. The
* callback receives 2 arguments, error and result.
*
* If error is truthy, it contains a string describing the overall error.
*
* The 2nd argument to the callback is always an object with details on the
* overall execution state. It contains the following keys:
*
* installedIDs Array of add-on IDs that were installed.
* installs Array of AddonInstall instances that were installed.
* addons Array of Addon instances that were installed.
* errors Array of errors encountered. Only has elements if error is
* truthy.
*
* @param installs
* Array of objects describing add-ons to install.
* @param cb
* Function to be called when all actions are complete.
*/
installAddons: function installAddons(installs, cb) {
if (!cb) {
throw new Error("Invalid argument: cb is not defined.");
}
let ids = [];
for each (let addon in installs) {
ids.push(addon.id);
}
AddonRepository.getAddonsByIDs(ids, {
searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
this._log.info("Found " + addonsLength + "/" + ids.length +
" add-ons during repository search.");
let ourResult = {
installedIDs: [],
installs: [],
addons: [],
errors: []
};
if (!addonsLength) {
cb(null, ourResult);
return;
}
let expectedInstallCount = 0;
let finishedCount = 0;
let installCallback = function installCallback(error, result) {
finishedCount++;
if (error) {
ourResult.errors.push(error);
} else {
ourResult.installedIDs.push(result.id);
ourResult.installs.push(result.install);
ourResult.addons.push(result.addon);
}
if (finishedCount >= expectedInstallCount) {
if (ourResult.errors.length > 0) {
cb(new Error("1 or more add-ons failed to install"), ourResult);
} else {
cb(null, ourResult);
}
}
}.bind(this);
let toInstall = [];
// Rewrite the "src" query string parameter of the source URI to note
// that the add-on was installed by Sync and not something else so
// server-side metrics aren't skewed (bug 708134). The server should
// ideally send proper URLs, but this solution was deemed too
// complicated at the time the functionality was implemented.
for each (let addon in addons) {
// sourceURI presence isn't enforced by AddonRepository. So, we skip
// add-ons without a sourceURI.
if (!addon.sourceURI) {
this._log.info("Skipping install of add-on because missing " +
"sourceURI: " + addon.id);
continue;
}
toInstall.push(addon);
// We should always be able to QI the nsIURI to nsIURL. If not, we
// still try to install the add-on, but we don't rewrite the URL,
// potentially skewing metrics.
try {
addon.sourceURI.QueryInterface(Ci.nsIURL);
} catch (ex) {
this._log.warn("Unable to QI sourceURI to nsIURL: " +
addon.sourceURI.spec);
continue;
}
let params = addon.sourceURI.query.split("&").map(
function rewrite(param) {
if (param.indexOf("src=") == 0) {
return "src=sync";
} else {
return param;
}
});
addon.sourceURI.query = params.join("&");
}
expectedInstallCount = toInstall.length;
if (!expectedInstallCount) {
cb(null, ourResult);
return;
}
// Start all the installs asynchronously. They will report back to us
// as they finish, eventually triggering the global callback.
for each (let addon in toInstall) {
let options = {};
for each (let install in installs) {
if (install.id == addon.id) {
options = install;
break;
}
}
this.installAddonFromSearchResult(addon, options, installCallback);
}
}.bind(this),
searchFailed: function searchFailed() {
cb(new Error("AddonRepository search failed"), null);
}.bind(this)
});
}
};
/**

View File

@ -193,6 +193,7 @@ let SyncScheduler = {
}
break;
case "weave:service:setup-complete":
Services.prefs.savePrefFile(null);
Svc.Idle.addIdleObserver(this, Svc.Prefs.get("scheduler.idleTime"));
break;
case "weave:service:start-over":

View File

@ -54,6 +54,7 @@ pref("services.sync.log.appender.file.logOnError", true);
pref("services.sync.log.appender.file.logOnSuccess", false);
pref("services.sync.log.appender.file.maxErrorAge", 864000); // 10 days
pref("services.sync.log.rootLogger", "Debug");
pref("services.sync.log.logger.addonutils", "Debug");
pref("services.sync.log.logger.service.main", "Debug");
pref("services.sync.log.logger.status", "Debug");
pref("services.sync.log.logger.authenticator", "Debug");

View File

@ -380,26 +380,3 @@ RotaryEngine.prototype = {
}
}
};
deepCopy: function deepCopy(thing, noSort) {
if (typeof(thing) != "object" || thing == null){
return thing;
}
let ret;
if (Array.isArray(thing)) {
ret = [];
for (let i = 0; i < thing.length; i++){
ret.push(deepCopy(thing[i], noSort));
}
} else {
ret = {};
let props = [p for (p in thing)];
if (!noSort){
props = props.sort();
}
props.forEach(function(k) ret[k] = deepCopy(thing[k], noSort));
}
return ret;
};

View File

@ -0,0 +1,161 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-common/preferences.js");
const HTTP_PORT = 8888;
const SERVER_ADDRESS = "http://127.0.0.1:8888";
let prefs = new Preferences();
prefs.set("extensions.getAddons.get.url",
SERVER_ADDRESS + "/search/guid:%IDS%");
loadAddonTestFunctions();
startupManager();
function createAndStartHTTPServer(port=HTTP_PORT) {
try {
let server = new HttpServer();
let bootstrap1XPI = ExtensionsTestPath("/addons/test_bootstrap1_1.xpi");
server.registerFile("/search/guid:missing-sourceuri%40tests.mozilla.org",
do_get_file("missing-sourceuri.xml"));
server.registerFile("/search/guid:rewrite%40tests.mozilla.org",
do_get_file("rewrite-search.xml"));
server.start(port);
return server;
} catch (ex) {
_("Got exception starting HTTP server on port " + port);
_("Error: " + Utils.exceptionStr(ex));
do_throw(ex);
}
}
function run_test() {
initTestLogging("Trace");
run_next_test();
}
add_test(function test_handle_empty_source_uri() {
_("Ensure that search results without a sourceURI are properly ignored.");
let server = createAndStartHTTPServer();
const ID = "missing-sourceuri@tests.mozilla.org";
let cb = Async.makeSpinningCallback();
AddonUtils.installAddons([{id: ID, requireSecureURI: false}], cb);
let result = cb.wait();
do_check_true("installedIDs" in result);
do_check_eq(0, result.installedIDs.length);
server.stop(run_next_test);
});
add_test(function test_ignore_untrusted_source_uris() {
_("Ensures that source URIs from insecure schemes are rejected.");
let ioService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
const bad = ["http://example.com/foo.xpi",
"ftp://example.com/foo.xpi",
"silly://example.com/foo.xpi"];
const good = ["https://example.com/foo.xpi"];
for (let s of bad) {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "bad", id: "bad"};
try {
let cb = Async.makeSpinningCallback();
AddonUtils.getInstallFromSearchResult(addon, cb, true);
cb.wait();
} catch (ex) {
do_check_neq(null, ex);
do_check_eq(0, ex.message.indexOf("Insecure source URI"));
continue;
}
// We should never get here if an exception is thrown.
do_check_true(false);
}
let count = 0;
for (let s of good) {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "good", id: "good"};
// Despite what you might think, we don't get an error in the callback.
// The install won't work because the underlying Addon instance wasn't
// proper. But, that just results in an AddonInstall that is missing
// certain values. We really just care that the callback is being invoked
// anyway.
let callback = function onInstall(error, install) {
do_check_null(error);
do_check_neq(null, install);
do_check_eq(sourceURI.spec, install.sourceURI.spec);
count += 1;
if (count >= good.length) {
run_next_test();
}
};
AddonUtils.getInstallFromSearchResult(addon, callback, true);
}
});
add_test(function test_source_uri_rewrite() {
_("Ensure that a 'src=api' query string is rewritten to 'src=sync'");
// This tests for conformance with bug 708134 so server-side metrics aren't
// skewed.
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
// We resort to monkeypatching because of the API design.
let oldFunction = AddonUtils.__proto__.installAddonFromSearchResult;
let installCalled = false;
AddonUtils.__proto__.installAddonFromSearchResult =
function testInstallAddon(addon, metadata, cb) {
do_check_eq(SERVER_ADDRESS + "/require.xpi?src=sync",
addon.sourceURI.spec);
installCalled = true;
AddonUtils.getInstallFromSearchResult(addon, function (error, install) {
do_check_null(error);
do_check_eq(SERVER_ADDRESS + "/require.xpi?src=sync",
install.sourceURI.spec);
cb(null, {id: addon.id, addon: addon, install: install});
}, false);
};
let server = createAndStartHTTPServer();
let installCallback = Async.makeSpinningCallback();
AddonUtils.installAddons([{id: "rewrite@tests.mozilla.org"}], installCallback);
installCallback.wait();
do_check_true(installCalled);
AddonUtils.__proto__.installAddonFromSearchResult = oldFunction;
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});

View File

@ -3,8 +3,9 @@
"use strict";
Cu.import("resource://services-sync/engines/addons.js");
Cu.import("resource://services-common/preferences.js");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/engines/addons.js");
const HTTP_PORT = 8888;
@ -53,12 +54,6 @@ function createAndStartHTTPServer(port) {
server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
do_get_file("missing-xpi-search.xml"));
server.registerFile("/search/guid:rewrite%40tests.mozilla.org",
do_get_file("rewrite-search.xml"));
server.registerFile("/search/guid:missing-sourceuri%40tests.mozilla.org",
do_get_file("missing-sourceuri.xml"));
server.start(port);
return server;
@ -409,64 +404,6 @@ add_test(function test_ignore_hotfixes() {
run_next_test();
});
add_test(function test_ignore_untrusted_source_uris() {
_("Ensures that source URIs from insecure schemes are rejected.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", false);
let ioService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService);
const bad = ["http://example.com/foo.xpi",
"ftp://example.com/foo.xpi",
"silly://example.com/foo.xpi"];
const good = ["https://example.com/foo.xpi"];
for each (let s in bad) {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "foo"};
try {
let cb = Async.makeSpinningCallback();
store.getInstallFromSearchResult(addon, cb);
cb.wait();
} catch (ex) {
do_check_neq(null, ex);
do_check_eq(0, ex.message.indexOf("Insecure source URI"));
continue;
}
// We should never get here if an exception is thrown.
do_check_true(false);
}
let count = 0;
for each (let s in good) {
let sourceURI = ioService.newURI(s, null, null);
let addon = {sourceURI: sourceURI, name: "foo", id: "foo"};
// Despite what you might think, we don't get an error in the callback.
// The install won't work because the underlying Addon instance wasn't
// proper. But, that just results in an AddonInstall that is missing
// certain values. We really just care that the callback is being invoked
// anyway.
let callback = function(error, install) {
do_check_eq(null, error);
do_check_neq(null, install);
do_check_eq(sourceURI.spec, install.sourceURI.spec);
count += 1;
if (count >= good.length) {
run_next_test();
}
};
store.getInstallFromSearchResult(addon, callback);
}
});
add_test(function test_wipe() {
_("Ensures that wiping causes add-ons to be uninstalled.");
@ -482,64 +419,3 @@ add_test(function test_wipe() {
run_next_test();
});
add_test(function test_source_uri_rewrite() {
_("Ensure that a 'src=api' query string is rewritten to 'src=sync'");
// This tests for conformance with bug 708134 so server-side metrics aren't
// skewed.
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
// We resort to monkeypatching because of the API design.
let oldFunction = store.__proto__.installAddonFromSearchResult;
let installCalled = false;
store.__proto__.installAddonFromSearchResult =
function testInstallAddon(addon, metadata, cb) {
do_check_eq("http://127.0.0.1:8888/require.xpi?src=sync",
addon.sourceURI.spec);
installCalled = true;
store.getInstallFromSearchResult(addon, function (error, install) {
do_check_eq("http://127.0.0.1:8888/require.xpi?src=sync",
install.sourceURI.spec);
cb(null, {id: addon.id, addon: addon, install: install});
});
};
let server = createAndStartHTTPServer(HTTP_PORT);
let installCallback = Async.makeSpinningCallback();
store.installAddons([{id: "rewrite@tests.mozilla.org"}], installCallback);
installCallback.wait();
do_check_true(installCalled);
store.__proto__.installAddonFromSearchResult = oldFunction;
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});
add_test(function test_handle_empty_source_uri() {
_("Ensure that search results without a sourceURI are properly ignored.");
Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
let server = createAndStartHTTPServer(HTTP_PORT);
const ID = "missing-sourceuri@tests.mozilla.org";
let cb = Async.makeSpinningCallback();
store.installAddons([{id: ID}], cb);
let result = cb.wait();
do_check_true("installedIDs" in result);
do_check_eq(0, result.installedIDs.length);
Svc.Prefs.reset("addons.ignoreRepositoryChecking");
server.stop(run_next_test);
});

View File

@ -1,11 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-common/log4moz.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://testing-common/services-common/utils.js");
const DESCRIPTION_ANNO = "bookmarkProperties/description";
@ -56,7 +59,7 @@ store.wipe();
function makeLivemark(p, mintGUID) {
let b = new Livemark("bookmarks", p.id);
// Copy here, because tests mutate the contents.
b.cleartext = deepCopy(p);
b.cleartext = TestingUtils.deepCopy(p);
if (mintGUID)
b.id = Utils.makeGUID();

View File

@ -1,27 +1,28 @@
const modules = [
"addonsreconciler.js",
"constants.js",
"engines/addons.js",
"engines/bookmarks.js",
"engines/clients.js",
"engines/forms.js",
"engines/history.js",
"engines/passwords.js",
"engines/prefs.js",
"engines/tabs.js",
"engines.js",
"identity.js",
"jpakeclient.js",
"keys.js",
"main.js",
"notifications.js",
"policies.js",
"record.js",
"resource.js",
"rest.js",
"service.js",
"status.js",
"util.js",
"addonutils.js",
"addonsreconciler.js",
"constants.js",
"engines/addons.js",
"engines/bookmarks.js",
"engines/clients.js",
"engines/forms.js",
"engines/history.js",
"engines/passwords.js",
"engines/prefs.js",
"engines/tabs.js",
"engines.js",
"identity.js",
"jpakeclient.js",
"keys.js",
"main.js",
"notifications.js",
"policies.js",
"record.js",
"resource.js",
"rest.js",
"service.js",
"status.js",
"util.js",
];
function run_test() {

View File

@ -155,12 +155,77 @@ let quotaValue;
Observers.add("weave:service:quota:remaining",
function (subject) { quotaValue = subject; });
let server;
function run_test() {
logger = Log4Moz.repository.getLogger('Test');
Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
Svc.Prefs.set("network.numRetries", 1); // speed up test
run_next_test();
}
// This apparently has to come first in order for our PAC URL to be hit.
// Don't put any other HTTP requests earlier in the file!
add_test(function test_proxy_auth_redirect() {
_("Ensure that a proxy auth redirect (which switches out our channel) " +
"doesn't break AsyncResource.");
let server = httpd_setup({
"/open": server_open,
"/pac2": server_pac
});
PACSystemSettings.PACURI = "http://localhost:8080/pac2";
installFakePAC();
let res = new AsyncResource("http://localhost:8080/open");
res.get(function (error, result) {
do_check_true(!error);
do_check_true(pacFetched);
do_check_true(fetched);
do_check_eq("This path exists", result);
pacFetched = fetched = false;
uninstallFakePAC();
server.stop(run_next_test);
});
});
add_test(function test_new_channel() {
_("Ensure a redirect to a new channel is handled properly.");
let resourceRequested = false;
function resourceHandler(metadata, response) {
resourceRequested = true;
let body = "Test";
response.setHeader("Content-Type", "text/plain");
response.bodyOutputStream.write(body, body.length);
}
function redirectHandler(metadata, response) {
let body = "Redirecting";
response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
response.setHeader("Location", "http://localhost:8080/resource");
response.bodyOutputStream.write(body, body.length);
}
let server = httpd_setup({"/resource": resourceHandler,
"/redirect": redirectHandler},
8080);
let request = new AsyncResource("http://localhost:8080/redirect");
request.get(function onRequest(error, content) {
do_check_null(error);
do_check_true(resourceRequested);
do_check_eq(200, content.status);
do_check_true("content-type" in content.headers);
do_check_eq("text/plain", content.headers["content-type"]);
server.stop(run_next_test);
});
});
let server;
add_test(function setup() {
server = httpd_setup({
"/open": server_open,
"/protected": server_protected,
@ -176,27 +241,7 @@ function run_test() {
"/quota-error": server_quota_error
});
Svc.Prefs.set("network.numRetries", 1); // speed up test
run_next_test();
}
// This apparently has to come first in order for our PAC URL to be hit.
// Don't put any other HTTP requests earlier in the file!
add_test(function test_proxy_auth_redirect() {
_("Ensure that a proxy auth redirect (which switches out our channel) " +
"doesn't break AsyncResource.");
PACSystemSettings.PACURI = "http://localhost:8080/pac2";
installFakePAC();
let res = new AsyncResource("http://localhost:8080/open");
res.get(function (error, result) {
do_check_true(!error);
do_check_true(pacFetched);
do_check_true(fetched);
do_check_eq("This path exists", result);
pacFetched = fetched = false;
uninstallFakePAC();
run_next_test();
});
});
add_test(function test_members() {
@ -662,38 +707,3 @@ add_test(function test_uri_construction() {
add_test(function eliminate_server() {
server.stop(run_next_test);
});
add_test(function test_new_channel() {
_("Ensure a redirect to a new channel is handled properly.");
let resourceRequested = false;
function resourceHandler(metadata, response) {
resourceRequested = true;
let body = "Test";
response.setHeader("Content-Type", "text/plain");
response.bodyOutputStream.write(body, body.length);
}
function redirectHandler(metadata, response) {
let body = "Redirecting";
response.setStatusLine(metadata.httpVersion, 307, "TEMPORARY REDIRECT");
response.setHeader("Location", "http://localhost:8080/resource");
response.bodyOutputStream.write(body, body.length);
}
let server = httpd_setup({"/resource": resourceHandler,
"/redirect": redirectHandler},
8080);
let request = new AsyncResource("http://localhost:8080/redirect");
request.get(function onRequest(error, content) {
do_check_null(error);
do_check_true(resourceRequested);
do_check_eq(200, content.status);
do_check_true("content-type" in content.headers);
do_check_eq("text/plain", content.headers["content-type"]);
server.stop(run_next_test);
});
});

View File

@ -1,5 +1,9 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://services-sync/engines/tabs.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services-common/utils.js");
function test_lastUsed() {
let store = new TabEngine()._store;
@ -80,7 +84,7 @@ function fakeSessionSvc(url, numtabs) {
if (numtabs) {
let tabs = obj.windows[0].tabs;
for (let i = 0; i < numtabs-1; i++)
tabs.push(deepCopy(tabs[0]));
tabs.push(TestingUtils.deepCopy(tabs[0]));
}
return JSON.stringify(obj);
}

View File

@ -11,7 +11,6 @@ tail =
# util contains a bunch of functionality used throughout.
[test_utils_catch.js]
[test_utils_deepCopy.js]
[test_utils_deepEquals.js]
[test_utils_deferGetSet.js]
[test_utils_deriveKey.js]
@ -25,6 +24,7 @@ tail =
[test_utils_passphrase.js]
# We have a number of other libraries that are pretty much standalone.
[test_addon_utils.js]
[test_httpd_sync_server.js]
[test_jpakeclient.js]
# Bug 618233: this test produces random failures on Windows 7.

View File

@ -11,7 +11,7 @@ Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/AddonRepository.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/addonutils.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://tps/logger.jsm");
@ -59,8 +59,7 @@ Addon.prototype = {
Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
cb = Async.makeSpinningCallback();
let store = Engines.get("addons")._store;
store.uninstallAddon(addon, cb);
AddonUtils.uninstallAddon(addon, cb);
cb.wait();
},
@ -97,11 +96,7 @@ Addon.prototype = {
// for the addon's install .xml; we'll read the actual id from the .xml.
let cb = Async.makeSpinningCallback();
// We call the store's APIs for installation because it is simpler. If that
// API is broken, it should ideally be caught by an xpcshell test. But, if
// TPS tests fail, it's all the same: a genuite reported error.
let store = Engines.get("addons")._store;
store.installAddons([{id: this.id}], cb);
AddonUtils.installAddons([{id: this.id, requireSecureURI: false}], cb);
let result = cb.wait();
Logger.AssertEqual(1, result.installedIDs.length, "Exactly 1 add-on was installed.");
@ -121,9 +116,8 @@ Addon.prototype = {
throw new Error("Unknown flag to setEnabled: " + flag);
}
let store = Engines.get("addons")._store;
let cb = Async.makeSpinningCallback();
store.updateUserDisabled(this.addon, userDisabled, cb);
AddonUtils.updateUserDisabled(this.addon, userDisabled, cb);
cb.wait();
return true;

View File

@ -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 \

View File

@ -8,4 +8,5 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
<script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/>
<script type="application/javascript" src="chrome://mochikit/content/cc-analyzer.js"/>
</overlay>

View File

@ -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();
}

View File

@ -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 = [];
}

View File

@ -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)

View File

@ -679,14 +679,6 @@ class Mochitest(object):
else:
timeout = 330.0 # default JS harness timeout is 300 seconds
# it's a debug build, we can parse leaked DOMWindows and docShells
# but skip for WebappRT chrome tests, where DOMWindow "leaks" aren't
# meaningful. See https://bugzilla.mozilla.org/show_bug.cgi?id=733631#c46
if Automation.IS_DEBUG_BUILD and not options.webapprtChrome:
logger = ShutdownLeakLogger(self.automation.log)
else:
logger = None
if options.vmwareRecording:
self.startVMwareRecording(options);
@ -700,7 +692,6 @@ class Mochitest(object):
certPath=options.certPath,
debuggerInfo=debuggerInfo,
symbolsPath=options.symbolsPath,
logger = logger,
timeout = timeout)
except KeyboardInterrupt:
self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n");
@ -716,9 +707,6 @@ class Mochitest(object):
self.stopWebSocketServer(options)
processLeakLog(self.leak_report_file, options.leakThreshold)
if logger:
logger.parse()
self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
if manifest is not None:

View File

@ -308,7 +308,9 @@ BrowserTabActor.prototype = {
// Watch for globals being created in this tab.
this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
this.browser.addEventListener("pageshow", this._onWindowCreated, true);
this._progressListener = new DebuggerProgressListener(this);
if (this._tabbrowser) {
this._progressListener = new DebuggerProgressListener(this);
}
this._attached = true;
},

View File

@ -2031,9 +2031,9 @@ var XPIProvider = {
// We'll be replacing a currently active bootstrapped add-on so
// call its uninstall method
let oldVersion = aManifests[aLocation.name][id].version;
let newVersion = oldBootstrap.version;
let uninstallReason = Services.vc.compare(newVersion, oldVersion) < 0 ?
let newVersion = aManifests[aLocation.name][id].version;
let oldVersion = oldBootstrap.version;
let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
BOOTSTRAP_REASONS.ADDON_UPGRADE :
BOOTSTRAP_REASONS.ADDON_DOWNGRADE;