Merge mozilla-central and fx-team

This commit is contained in:
Ed Morley 2014-04-29 18:24:19 +01:00
commit f26e4e2c73
50 changed files with 1263 additions and 735 deletions

View File

@ -639,7 +639,7 @@
<hbox id="nav-bar-customization-target" flex="1">
<toolbaritem id="urlbar-container" flex="400" persist="width"
forwarddisabled="true" title="&locationItem.title;" removable="false"
title="&locationItem.title;" removable="false"
cui-areatype="toolbar"
class="chromeclass-location" overflows="false">
<toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional"

View File

@ -9,6 +9,7 @@
const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/content/test/social/social_crash_content_helper.js";
let {getFrameWorkerHandle} = Cu.import("resource://gre/modules/FrameWorker.jsm", {});
let {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
function test() {
waitForExplicitFinish();
@ -16,6 +17,9 @@ function test() {
// We need to ensure all our workers are in the same content process.
Services.prefs.setIntPref("dom.ipc.processCount", 1);
// This test generates many uncaught promises that should not cause failures.
Promise.Debugging.clearUncaughtErrorObservers();
runSocialTestWithProvider(gProviders, function (finishcb) {
runSocialTests(tests, undefined, undefined, function() {
Services.prefs.clearUserPref("dom.ipc.processCount");

View File

@ -126,15 +126,16 @@ SessionStartup.prototype = {
return;
}
this._initialState = this._parseStateString(stateString);
this._initialState = this._parseStateString(stateString);
let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
let shouldResumeSession = shouldResumeSessionOnce ||
Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
// If this is a normal restore then throw away any previous session
if (!shouldResumeSessionOnce)
if (!shouldResumeSessionOnce && this._initialState) {
delete this._initialState.lastSessionState;
}
let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");

View File

@ -246,7 +246,7 @@ function test() {
test_existingSHEnd_noClear, test_existingSHMiddle_noClear,
test_getBrowserState_lotsOfTabsOpening,
test_getBrowserState_userTypedValue, test_userTypedClearLoadURI];
let originalState = ss.getBrowserState();
let originalState = JSON.parse(ss.getBrowserState());
let state = {
windows: [{
tabs: [{ entries: [{ url: "about:blank" }] }]
@ -256,8 +256,7 @@ function test() {
if (tests.length) {
waitForBrowserState(state, tests.shift());
} else {
ss.setBrowserState(originalState);
executeSoon(finish);
waitForBrowserState(originalState, finish);
}
}

View File

@ -2,7 +2,7 @@
* 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/. */
let stateBackup = ss.getBrowserState();
let stateBackup = JSON.parse(ss.getBrowserState());
function test() {
/** Test for Bug 600545 **/
@ -72,8 +72,7 @@ function done() {
currentWindow.close();
}
ss.setBrowserState(stateBackup);
executeSoon(finish);
waitForBrowserState(stateBackup, finish);
}
// Count up the number of tabs in the state data

View File

@ -46,7 +46,7 @@ function testBug601955_3() {
}
function done() {
gBrowser.removeTab(window.gBrowser.tabs[0]);
gBrowser.removeTab(window.gBrowser.tabs[1]);
Services.prefs.clearUserPref("browser.sessionstore.interval");

View File

@ -165,6 +165,9 @@ function waitForBrowserState(aState, aSetStateCallback) {
listening = true;
gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
// Ensure setBrowserState() doesn't remove the initial tab.
gBrowser.selectedTab = gBrowser.tabs[0];
// Finally, call setBrowserState
ss.setBrowserState(JSON.stringify(aState));
}
@ -520,6 +523,7 @@ let TestRunner = {
*/
finish: function () {
closeAllButPrimaryWindow();
gBrowser.selectedTab = gBrowser.tabs[0];
waitForBrowserState(this.backupState, finish);
}
};

View File

@ -0,0 +1,3 @@
{
"windows": // invalid json
}

View File

@ -0,0 +1,21 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test() {
let profd = do_get_profile();
let sourceSession = do_get_file("data/sessionstore_invalid.js");
sourceSession.copyTo(profd, "sessionstore.js");
let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json");
sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json");
do_test_pending();
let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsISessionStartup);
afterSessionStartupInitialization(function cb() {
do_check_eq(startup.sessionType, Ci.nsISessionStartup.NO_SESSION);
do_test_finished();
});
}

View File

@ -4,9 +4,11 @@ tail =
firefox-appdir = browser
support-files =
data/sessionCheckpoints_all.json
data/sessionstore_invalid.js
data/sessionstore_valid.js
[test_backup.js]
[test_backup_once.js]
[test_startup_nosession_async.js]
[test_startup_session_async.js]
[test_startup_invalid_session.js]

View File

@ -37,6 +37,7 @@ function test() {
Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs");
gBrowser.selectedTab = gBrowser.tabs[0];
ss.setBrowserState(stateBackup);
});

View File

@ -9,6 +9,10 @@ function test() {
registerCleanupFunction(function() {
gBrowser.unpinTab(pinnedTab);
// Don't remove the initial tab.
gBrowser.moveTabTo(gBrowser.tabs[1], 0);
while (gBrowser.tabs[1])
gBrowser.removeTab(gBrowser.tabs[1]);
hideTabView();

View File

@ -3,34 +3,26 @@
function test() {
waitForExplicitFinish();
window.addEventListener("tabviewshown", onTabViewWindowLoaded, false);
TabView.toggle();
newWindowWithTabView(onTabViewWindowLoaded);
}
function onTabViewWindowLoaded() {
window.removeEventListener("tabviewshown", onTabViewWindowLoaded, false);
let contentWindow = document.getElementById("tab-view").contentWindow;
function onTabViewWindowLoaded(win) {
let contentWindow = win.TabView.getContentWindow();
is(contentWindow.GroupItems.groupItems.length, 1,
"There is one group item on startup");
is(gBrowser.tabs.length, 1, "There is one tab on startup");
is(win.gBrowser.tabs.length, 1, "There is one tab on startup");
let groupItem = contentWindow.GroupItems.groupItems[0];
hideGroupItem(groupItem, function () {
let onTabViewHidden = function() {
window.removeEventListener("tabviewhidden", onTabViewHidden, false);
hideTabView(() => {
is(contentWindow.GroupItems.groupItems.length, 1,
"There is still one group item");
isnot(groupItem, contentWindow.GroupItems.groupItems[0],
"The initial group item is not the same as the final group item");
is(gBrowser.tabs.length, 1, "There is only one tab");
ok(!TabView.isVisible(), "Tab View is hidden");
finish();
};
window.addEventListener("tabviewhidden", onTabViewHidden, false);
TabView.hide();
is(win.gBrowser.tabs.length, 1, "There is only one tab");
ok(!win.TabView.isVisible(), "Tab View is hidden");
promiseWindowClosed(win).then(finish);
}, win);
});
}

View File

@ -40,7 +40,7 @@ function test() {
is(targetGroup.getChildren().length, 2, 'target group has now two children');
// cleanup and finish
tabItem.close();
targetGroup.getChild(0).close();
hideTabView(finishTest);
});
}

View File

@ -3,6 +3,7 @@
function test() {
let cw;
let win;
let currentTest;
let getGroupItem = function (index) {
@ -15,7 +16,7 @@ function test() {
cw.UI.setActive(groupItem);
for (let i=0; i<numTabs || 0; i++)
gBrowser.loadOneTab('about:blank', {inBackground: true});
win.gBrowser.loadOneTab('about:blank', {inBackground: true});
return groupItem;
}
@ -35,13 +36,13 @@ function test() {
}
currentTest = test.name;
showTabView(test.func);
showTabView(test.func, win);
} else
hideTabView(finish);
promiseWindowClosed(win).then(finish);
}
let assertTabViewIsHidden = function () {
ok(!TabView.isVisible(), currentTest + ': tabview is hidden');
ok(!win.TabView.isVisible(), currentTest + ': tabview is hidden');
}
let assertNumberOfGroupItems = function (num) {
@ -49,7 +50,7 @@ function test() {
}
let assertNumberOfTabs = function (num) {
is(gBrowser.tabs.length, num, currentTest + ': number of tabs is equal to ' + num);
is(win.gBrowser.tabs.length, num, currentTest + ': number of tabs is equal to ' + num);
}
let assertGroupItemRemoved = function (groupItem) {
@ -68,7 +69,7 @@ function test() {
closeGroupItem(groupItem, function () {
assertNumberOfGroupItems(1);
assertGroupItemRemoved(groupItem);
whenTabViewIsHidden(next);
whenTabViewIsHidden(next, win);
});
}
@ -82,7 +83,7 @@ function test() {
assertNumberOfGroupItems(1);
assertGroupItemRemoved(groupItem);
next();
});
}, win);
});
}
@ -97,7 +98,7 @@ function test() {
closeGroupItem(groupItem, function () {
assertNumberOfGroupItems(1);
assertGroupItemExists(newGroupItem);
hideTabView(next);
hideTabView(next, win);
});
}
@ -115,7 +116,7 @@ function test() {
assertGroupItemRemoved(groupItem);
assertGroupItemExists(newGroupItem);
next();
});
}, win);
});
}
@ -123,37 +124,37 @@ function test() {
// action: exit panorama
// expected: nothing should happen
let testPinnedTab1 = function () {
gBrowser.pinTab(gBrowser.selectedTab);
win.gBrowser.pinTab(win.gBrowser.selectedTab);
let groupItem = getGroupItem(0);
hideTabView(function () {
assertNumberOfGroupItems(1);
assertGroupItemExists(groupItem);
gBrowser.unpinTab(gBrowser.selectedTab);
win.gBrowser.unpinTab(win.gBrowser.selectedTab);
next();
});
}, win);
}
// setup: 1 pinned tab
// action: exit panorama
// expected: new blank group is created
let testPinnedTab2 = function () {
gBrowser.pinTab(gBrowser.selectedTab);
win.gBrowser.pinTab(win.gBrowser.selectedTab);
getGroupItem(0).close();
hideTabView(function () {
assertNumberOfTabs(1);
assertNumberOfGroupItems(1);
gBrowser.unpinTab(gBrowser.selectedTab);
win.gBrowser.unpinTab(win.gBrowser.selectedTab);
next();
});
}, win);
}
// setup: 1 pinned tab, 1 empty group, 1 non-empty group
// action: close non-empty group
// expected: nothing should happen
let testPinnedTab3 = function () {
gBrowser.pinTab(gBrowser.selectedTab);
win.gBrowser.pinTab(win.gBrowser.selectedTab);
let groupItem = getGroupItem(0);
let newGroupItem = createGroupItem(1);
@ -163,8 +164,8 @@ function test() {
assertNumberOfGroupItems(1);
assertGroupItemExists(groupItem);
gBrowser.unpinTab(gBrowser.selectedTab);
hideTabView(next);
win.gBrowser.unpinTab(win.gBrowser.selectedTab);
hideTabView(next, win);
});
}
@ -172,7 +173,7 @@ function test() {
// action: hide non-empty group, exit panorama
// expected: nothing should happen
let testPinnedTab4 = function () {
gBrowser.pinTab(gBrowser.selectedTab);
win.gBrowser.pinTab(win.gBrowser.selectedTab);
let groupItem = getGroupItem(0);
let newGroupItem = createGroupItem(1);
@ -183,9 +184,9 @@ function test() {
assertNumberOfGroupItems(1);
assertGroupItemExists(groupItem);
assertGroupItemRemoved(newGroupItem);
gBrowser.unpinTab(gBrowser.selectedTab);
win.gBrowser.unpinTab(win.gBrowser.selectedTab);
next();
});
}, win);
});
}
@ -200,7 +201,7 @@ function test() {
closeGroupItem(groupItem, function () {
assertNumberOfGroupItems(1);
assertGroupItemExists(newGroupItem);
whenTabViewIsHidden(next);
whenTabViewIsHidden(next, win);
});
}
@ -218,7 +219,7 @@ function test() {
assertGroupItemRemoved(groupItem);
assertGroupItemExists(newGroupItem);
next();
});
}, win);
});
}
@ -235,7 +236,7 @@ function test() {
assertNumberOfGroupItems(1);
assertGroupItemRemoved(groupItem);
assertGroupItemExists(hiddenGroupItem);
hideTabView(next);
hideTabView(next, win);
});
});
}
@ -255,7 +256,7 @@ function test() {
assertGroupItemRemoved(groupItem);
assertGroupItemRemoved(hiddenGroupItem);
next();
});
}, win);
});
});
}
@ -279,8 +280,9 @@ function test() {
waitForExplicitFinish();
showTabView(function () {
cw = TabView.getContentWindow();
newWindowWithTabView(window => {
win = window;
cw = win.TabView.getContentWindow();
next();
});
}

View File

@ -3,11 +3,12 @@
function test() {
let cw;
let win;
let groupItemId;
let prefix = 'start';
let assertTabViewIsHidden = function () {
ok(!TabView.isVisible(), prefix + ': tabview is hidden');
ok(!win.TabView.isVisible(), prefix + ': tabview is hidden');
}
let assertNumberOfGroups = function (num) {
@ -15,11 +16,11 @@ function test() {
}
let assertNumberOfTabs = function (num) {
is(gBrowser.visibleTabs.length, num, prefix + ': there should be ' + num + ' tabs');
is(win.gBrowser.visibleTabs.length, num, prefix + ': there should be ' + num + ' tabs');
}
let assertNumberOfPinnedTabs = function (num) {
is(gBrowser._numPinnedTabs, num, prefix + ': there should be ' + num + ' pinned tabs');
is(win.gBrowser._numPinnedTabs, num, prefix + ': there should be ' + num + ' pinned tabs');
}
let assertGroupItemPreserved = function () {
@ -34,7 +35,7 @@ function test() {
}
let createTab = function (url) {
return gBrowser.loadOneTab(url || 'http://mochi.test:8888/', {inBackground: true});
return win.gBrowser.loadOneTab(url || 'http://mochi.test:8888/', {inBackground: true});
}
let createBlankTab = function () {
@ -44,7 +45,7 @@ function test() {
let finishTest = function () {
prefix = 'finish';
assertValidPrerequisites();
finish();
promiseWindowClosed(win).then(finish);
}
let testUndoCloseWithSelectedBlankTab = function () {
@ -53,7 +54,7 @@ function test() {
assertNumberOfTabs(2);
afterAllTabsLoaded(function () {
gBrowser.removeTab(tab);
win.gBrowser.removeTab(tab);
assertNumberOfTabs(1);
assertNumberOfPinnedTabs(0);
@ -63,9 +64,9 @@ function test() {
assertGroupItemPreserved();
createBlankTab();
afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab);
});
});
afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab, win);
}, 0, win);
}, win);
}
let testUndoCloseWithSelectedBlankPinnedTab = function () {
@ -73,14 +74,8 @@ function test() {
assertNumberOfTabs(2);
afterAllTabsLoaded(function () {
gBrowser.removeTab(gBrowser.tabs[0]);
gBrowser.pinTab(gBrowser.tabs[0]);
registerCleanupFunction(function () {
let tab = gBrowser.tabs[0];
if (tab.pinned)
gBrowser.unpinTab(tab);
});
win.gBrowser.removeTab(win.gBrowser.tabs[0]);
win.gBrowser.pinTab(win.gBrowser.tabs[0]);
assertNumberOfTabs(1);
assertNumberOfPinnedTabs(1);
@ -91,23 +86,24 @@ function test() {
assertGroupItemPreserved();
createBlankTab();
gBrowser.removeTab(gBrowser.tabs[0]);
win.gBrowser.removeTab(win.gBrowser.tabs[0]);
afterAllTabsLoaded(finishTest);
});
});
afterAllTabsLoaded(finishTest, win);
}, 0, win);
}, win);
}
waitForExplicitFinish();
registerCleanupFunction(function () TabView.hide());
showTabView(function () {
newWindowWithTabView(window => {
win = window;
hideTabView(function () {
cw = TabView.getContentWindow();
cw = win.TabView.getContentWindow();
groupItemId = cw.GroupItems.groupItems[0].id;
assertValidPrerequisites();
testUndoCloseWithSelectedBlankTab();
});
}, win);
});
}

View File

@ -18,18 +18,18 @@ let activeGroup;
function test() {
waitForExplicitFinish();
showTabView(function () {
contentWindow = TabView.getContentWindow();
newWindowWithTabView(win => {
contentWindow = win.TabView.getContentWindow();
activeGroup = contentWindow.GroupItems.getActiveGroupItem();
gBrowser.browsers[0].loadURI("data:text/html,<p>test for bug 626455, tab1");
win.gBrowser.browsers[0].loadURI("data:text/html,<p>test for bug 626455, tab1");
let tab = gBrowser.addTab(TEST_URL);
afterAllTabsLoaded(() => testStayOnPage(tab));
let tab = win.gBrowser.addTab(TEST_URL);
afterAllTabsLoaded(() => testStayOnPage(win, tab));
});
}
function testStayOnPage(blockingTab) {
function testStayOnPage(win, blockingTab) {
let browser = blockingTab.linkedBrowser;
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
// stay on page
@ -37,12 +37,12 @@ function testStayOnPage(blockingTab) {
executeSoon(function () {
showTabView(function () {
is(gBrowser.tabs.length, 1,
is(win.gBrowser.tabs.length, 1,
"The total number of tab is 1 when staying on the page");
// The other initial tab has been closed when trying to close the tab
// group. The only tab left is the one with the onbeforeunload dialog.
let url = gBrowser.browsers[0].currentURI.spec;
let url = win.gBrowser.browsers[0].currentURI.spec;
ok(url.contains("onbeforeunload"), "The open tab is the expected one");
is(contentWindow.GroupItems.getActiveGroupItem(), activeGroup,
@ -52,32 +52,32 @@ function testStayOnPage(blockingTab) {
"Only one group is open");
// start the next test
testLeavePage(gBrowser.tabs[0]);
});
testLeavePage(win, win.gBrowser.tabs[0]);
}, win);
});
});
closeGroupItem(activeGroup);
}
function testLeavePage(blockingTab) {
function testLeavePage(win, blockingTab) {
let browser = blockingTab.linkedBrowser;
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
// Leave page
btnLeave.click();
});
whenGroupClosed(activeGroup, finishTest);
whenGroupClosed(activeGroup, () => finishTest(win));
closeGroupItem(activeGroup);
}
function finishTest() {
is(gBrowser.tabs.length, 1,
function finishTest(win) {
is(win.gBrowser.tabs.length, 1,
"The total number of tab is 1 after leaving the page");
is(contentWindow.TabItems.getItems().length, 1,
"The total number of tab items is 1 after leaving the page");
let location = gBrowser.browsers[0].currentURI.spec;
let location = win.gBrowser.browsers[0].currentURI.spec;
is(location, BROWSER_NEW_TAB_URL, "The open tab is the expected one");
isnot(contentWindow.GroupItems.getActiveGroupItem(), activeGroup,
@ -88,7 +88,7 @@ function finishTest() {
contentWindow = null;
activeGroup = null;
finish();
promiseWindowClosed(win).then(finish);
}
// ----------

View File

@ -4,10 +4,10 @@
function test() {
waitForExplicitFinish();
showTabView(function () {
is(gBrowser.tabs.length, 1, "There is only one tab");
newWindowWithTabView(win => {
is(win.gBrowser.tabs.length, 1, "There is only one tab");
let tab = gBrowser.tabs[0];
let tab = win.gBrowser.tabs[0];
let tabItem = tab._tabViewTabItem;
ok(tabItem.parent, "The tab item belongs to a group");
let groupId = tabItem.parent.id;
@ -16,16 +16,16 @@ function test() {
whenTabViewIsHidden(function() {
// a new tab with group should be opened
is(gBrowser.tabs.length, 1, "There is still one tab");
isnot(gBrowser.selectedTab, tab, "The tab is different");
is(win.gBrowser.tabs.length, 1, "There is still one tab");
isnot(win.gBrowser.selectedTab, tab, "The tab is different");
tab = gBrowser.tabs[0];
tab = win.gBrowser.tabs[0];
tabItem = tab._tabViewTabItem;
ok(tabItem.parent, "This new tab item belongs to a group");
is(tabItem.parent.id, groupId, "The group is different");
finish();
});
promiseWindowClosed(win).then(finish);
}, win);
});
}

View File

@ -4,44 +4,38 @@
function test() {
waitForExplicitFinish();
gBrowser.addTab("http://example.com/");
gBrowser.addTab("http://example.com/");
newWindowWithTabView(win => {
win.gBrowser.addTab("http://example.com/");
win.gBrowser.addTab("http://example.com/");
registerCleanupFunction(function () {
while (gBrowser.tabs.length > 1)
gBrowser.removeTab(gBrowser.tabs[1]);
hideTabView();
})
afterAllTabsLoaded(function() {
showTabView(function() {
let cw = TabView.getContentWindow();
afterAllTabsLoaded(function() {
let cw = win.TabView.getContentWindow();
let groupItemOne = cw.GroupItems.groupItems[0];
is(groupItemOne.getChildren().length, 3, "The number of tabs in group one is 3");
// create a group with a blank tab
let groupItemTwo = createGroupItemWithBlankTabs(window, 400, 400, 40, 1);
let groupItemTwo = createGroupItemWithBlankTabs(win, 400, 400, 40, 1);
is(groupItemTwo.getChildren().length, 1, "The number of tabs in group two is 1");
cw.UI.setActive(groupItemOne);
moveTabToAnotherGroup(groupItemOne.getChild(2).tab, groupItemOne, groupItemTwo, function() {
moveTabToAnotherGroup(groupItemOne.getChild(1).tab, groupItemOne, groupItemTwo, function() {
moveTabToAnotherGroup(win, groupItemOne.getChild(2).tab, groupItemOne, groupItemTwo, function() {
moveTabToAnotherGroup(win, groupItemOne.getChild(1).tab, groupItemOne, groupItemTwo, function() {
groupItemOne.close();
hideTabView(finish);
promiseWindowClosed(win).then(finish);
});
});
});
});
}
function moveTabToAnotherGroup(targetTab, groupItemOne, groupItemTwo, callback) {
function moveTabToAnotherGroup(win, targetTab, groupItemOne, groupItemTwo, callback) {
hideTabView(function() {
let tabCountInGroupItemOne = groupItemOne.getChildren().length;
let tabCountInGroupItemTwo = groupItemTwo.getChildren().length;
TabView.moveTabTo(targetTab, groupItemTwo.id);
win.TabView.moveTabTo(targetTab, groupItemTwo.id);
showTabView(function() {
is(groupItemOne.getChildren().length, --tabCountInGroupItemOne, "The number of tab items in group one is decreased");
@ -49,6 +43,6 @@ function moveTabToAnotherGroup(targetTab, groupItemOne, groupItemTwo, callback)
is(groupItemTwo.getChild(tabCountInGroupItemTwo-1).tab, targetTab, "The last tab is the moved tab");
callback();
});
});
}, win);
}, win);
}

View File

@ -509,12 +509,13 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
if (stackframeItem) {
// The container is not empty and an actual item was selected.
let depth = stackframeItem.attachment.depth;
DebuggerController.StackFrames.selectFrame(depth);
// Mirror the selected item in the classic list.
this.suppressSelectionEvents = true;
this._mirror.selectedItem = e => e.attachment.depth == depth;
this.suppressSelectionEvents = false;
DebuggerController.StackFrames.selectFrame(depth);
}
},

View File

@ -749,10 +749,11 @@ function openVarPopup(aPanel, aCoords, aWaitForFetchedProperties) {
let fetchedProperties = aWaitForFetchedProperties
? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES)
: promise.resolve(null);
let updatedFrame = waitForDebuggerEvents(aPanel, events.FETCHED_SCOPES);
let { left, top } = editor.getCoordsFromPosition(aCoords);
bubble._findIdentifier(left, top);
return promise.all([popupShown, fetchedProperties]).then(waitForTick);
return promise.all([popupShown, fetchedProperties, updatedFrame]).then(waitForTick);
}
// Simulates the mouse hovering a variable in the debugger

View File

@ -1702,13 +1702,17 @@ Experiments.ExperimentEntry.prototype = {
}
this._enabled = false;
let changes = yield this.reconcileAddonState();
let now = this._policy.now();
this._lastChangedDate = now;
this._endDate = now;
let changes = yield this.reconcileAddonState();
this._logTermination(terminationKind, terminationReason);
if (terminationKind == TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED) {
changes |= this.ADDON_CHANGE_UNINSTALL;
}
return changes;
}),

View File

@ -11,6 +11,7 @@ Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://services-sync/healthreport.jsm", this);
Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
Cu.import("resource://testing-common/AddonManagerTesting.jsm");
const PREF_EXPERIMENTS_ENABLED = "experiments.enabled";
const PREF_LOGGING_LEVEL = "experiments.logging.level";

View File

@ -25,6 +25,24 @@ let gManifestObject = null;
let gManifestHandlerURI = null;
let gTimerScheduleOffset = -1;
function uninstallExperimentAddons() {
return Task.spawn(function* () {
let addons = yield getExperimentAddons();
for (let a of addons) {
yield AddonTestUtils.uninstallAddonByID(a.id);
}
});
}
function testCleanup(experimentsInstance) {
return Task.spawn(function* () {
yield experimentsInstance.uninit();
yield removeCacheFile();
yield uninstallExperimentAddons();
restartManager();
});
}
function run_test() {
run_next_test();
}
@ -256,8 +274,7 @@ add_task(function* test_getExperiments() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that we handle the experiments addon already being
@ -337,8 +354,7 @@ add_task(function* test_addonAlreadyInstalled() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
add_task(function* test_lastActiveToday() {
@ -364,8 +380,7 @@ add_task(function* test_lastActiveToday() {
Assert.ok(lastActive, "Have a last active experiment");
Assert.equal(lastActive, e[0], "Last active object is expected.");
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test explicitly disabling experiments.
@ -450,10 +465,7 @@ add_task(function* test_disableExperiment() {
"Property " + k + " should match reference data.");
}
// Cleanup.
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
add_task(function* test_disableExperimentsFeature() {
@ -541,10 +553,7 @@ add_task(function* test_disableExperimentsFeature() {
"Property " + k + " should match reference data.");
}
// Cleanup.
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that after a failed experiment install:
@ -676,11 +685,7 @@ add_task(function* test_installFailure() {
}
}
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that after an experiment was disabled by user action,
@ -779,8 +784,7 @@ add_task(function* test_userDisabledAndUpdated() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that changing the hash for an active experiments triggers an
@ -871,8 +875,7 @@ add_task(function* test_updateActiveExperiment() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Tests that setting the disable flag for an active experiment
@ -963,8 +966,7 @@ add_task(function* test_disableActiveExperiment() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that:
@ -1044,8 +1046,7 @@ add_task(function* test_freezePendingExperiment() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that setting the frozen flag for an active experiment doesn't
@ -1125,8 +1126,7 @@ add_task(function* test_freezeActiveExperiment() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that removing an active experiment from the manifest doesn't
@ -1218,8 +1218,7 @@ add_task(function* test_removeActiveExperiment() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that we correctly handle experiment start & install failures.
@ -1274,8 +1273,7 @@ add_task(function* test_invalidUrl() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test that we handle it properly when active experiment addons are being
@ -1353,8 +1351,7 @@ add_task(function* test_unexpectedUninstall() {
// Cleanup.
Services.obs.removeObserver(observer, OBSERVER_TOPIC);
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// If the Addon Manager knows of an experiment that we don't, it should get
@ -1387,8 +1384,7 @@ add_task(function* testUnknownExperimentsUninstalled() {
addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Experiment 1 was uninstalled.");
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// If someone else installs an experiment add-on, we detect and stop that.
@ -1415,8 +1411,7 @@ add_task(function* testForeignExperimentInstall() {
addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Add-on install should have been cancelled.");
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Experiment add-ons will be disabled after Addon Manager restarts. Ensure
@ -1441,7 +1436,7 @@ add_task(function* testEnabledAfterRestart() {
};
let addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Precondition: No experimenta add-ons installed.");
Assert.equal(addons.length, 0, "Precondition: No experiment add-ons installed.");
yield experiments.updateManifest();
let fromManifest = yield experiments.getExperiments();
@ -1464,6 +1459,65 @@ add_task(function* testEnabledAfterRestart() {
yield experiments.updateManifest();
Assert.ok(addons[0].isActive, "It activates when the manifest is evaluated.");
yield experiments.uninit();
yield removeCacheFile();
yield testCleanup(experiments);
});
// Test coverage for an add-on uninstall disabling the experiment and that it stays
// disabled over restarts.
add_task(function* test_foreignUninstallAndRestart() {
let experiments = new Experiments.Experiments(gPolicy);
gManifestObject = {
"version": 1,
experiments: [
{
id: EXPERIMENT1_ID,
xpiURL: gDataRoot + EXPERIMENT1_XPI_NAME,
xpiHash: EXPERIMENT1_XPI_SHA1,
startTime: gPolicy.now().getTime() / 1000 - 60,
endTime: gPolicy.now().getTime() / 1000 + 60,
maxActiveSeconds: 10 * SEC_IN_ONE_DAY,
appName: ["XPCShell"],
channel: ["nightly"],
},
],
};
let addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Precondition: No experiment add-ons installed.");
yield experiments.updateManifest();
let experimentList = yield experiments.getExperiments();
Assert.equal(experimentList.length, 1, "A single experiment is known.");
addons = yield getExperimentAddons();
Assert.equal(addons.length, 1, "A single experiment add-on is installed.");
Assert.ok(addons[0].isActive, "That experiment is active.");
yield AddonTestUtils.uninstallAddonByID(EXPERIMENT1_ID);
yield experiments._mainTask;
let addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "Experiment add-on should have been removed.");
experimentList = yield experiments.getExperiments();
Assert.equal(experimentList.length, 1, "A single experiment is known.");
Assert.equal(experimentList[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.ok(!experimentList[0].active, "Experiment 1 should not be active anymore.");
// Fake restart behaviour.
experiments.uninit();
restartManager();
experiments = new Experiments.Experiments(gPolicy);
yield experiments.updateManifest();
let addons = yield getExperimentAddons();
Assert.equal(addons.length, 0, "No experiment add-ons installed.");
experimentList = yield experiments.getExperiments();
Assert.equal(experimentList.length, 1, "A single experiment is known.");
Assert.equal(experimentList[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
Assert.ok(!experimentList[0].active, "Experiment 1 should not be active.");
yield testCleanup(experiments);
});

View File

@ -1265,7 +1265,7 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
position: relative;
background-color: #fff;
background-clip: padding-box;
padding-left: 4px;
padding-left: 3px;
border-radius: 2.5px 0 0 2.5px;
border-width: 0 8px 0 0;
border-style: solid;
@ -1275,6 +1275,10 @@ toolbarbutton[sdk-button="true"][cui-areatype="toolbar"] > .toolbarbutton-icon {
margin-bottom: -1px;
}
@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
padding-left: 7px;
}
#notification-popup-box:not([hidden]) + #identity-box {
-moz-padding-start: 10px;
border-radius: 0;

View File

@ -3384,7 +3384,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
}
}
@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar > #notification-popup-box {
@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
padding-left: 7px;
}

View File

@ -2274,8 +2274,8 @@ toolbarbutton.bookmark-item[dragover="true"][open="true"] {
-moz-margin-end: -8px;
}
@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box {
padding-left: 5px;
@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
padding-left: 7px;
}
#notification-popup-box:-moz-locale-dir(rtl),

View File

@ -418,6 +418,31 @@ abstract public class BrowserApp extends GeckoApp
});
}
private void handleReaderFaviconRequest(final String url) {
(new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
@Override
public String doInBackground(Void... params) {
return Favicons.getFaviconURLForPageURL(url);
}
@Override
public void onPostExecute(String faviconUrl) {
JSONObject args = new JSONObject();
if (faviconUrl != null) {
try {
args.put("url", url);
args.put("faviconUrl", faviconUrl);
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
}
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
}
}).execute();
}
@Override
public void onCreate(Bundle savedInstanceState) {
mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME");
@ -497,6 +522,11 @@ abstract public class BrowserApp extends GeckoApp
registerEventListener("Accounts:Create");
registerEventListener("Accounts:Exist");
registerEventListener("Prompt:ShowTop");
registerEventListener("Reader:ListStatusRequest");
registerEventListener("Reader:Added");
registerEventListener("Reader:Removed");
registerEventListener("Reader:Share");
registerEventListener("Reader:FaviconRequest");
Distribution.init(this);
JavaAddonManager.getInstance().init(getApplicationContext());
@ -843,6 +873,11 @@ abstract public class BrowserApp extends GeckoApp
unregisterEventListener("Menu:Update");
unregisterEventListener("Accounts:Create");
unregisterEventListener("Accounts:Exist");
unregisterEventListener("Reader:ListStatusRequest");
unregisterEventListener("Reader:Added");
unregisterEventListener("Reader:Removed");
unregisterEventListener("Reader:Share");
unregisterEventListener("Reader:FaviconRequest");
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
@ -1206,6 +1241,9 @@ abstract public class BrowserApp extends GeckoApp
final String url = message.getString("url");
GeckoAppShell.openUriExternal(url, "text/plain", "", "",
Intent.ACTION_SEND, title);
} else if (event.equals("Reader:FaviconRequest")) {
final String url = message.getString("url");
handleReaderFaviconRequest(url);
} else if (event.equals("Settings:Show")) {
// null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830)
String resource = null;

View File

@ -496,31 +496,6 @@ public abstract class GeckoApp
outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
}
void handleFaviconRequest(final String url) {
(new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
@Override
public String doInBackground(Void... params) {
return Favicons.getFaviconURLForPageURL(url);
}
@Override
public void onPostExecute(String faviconUrl) {
JSONObject args = new JSONObject();
if (faviconUrl != null) {
try {
args.put("url", url);
args.put("faviconUrl", faviconUrl);
} catch (JSONException e) {
Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
}
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
}
}).execute();
}
void handleClearHistory() {
BrowserDB.clearHistory(getContentResolver());
}
@ -565,9 +540,6 @@ public abstract class GeckoApp
// generic log listener
final String msg = message.getString("msg");
Log.d(LOGTAG, "Log: " + msg);
} else if (event.equals("Reader:FaviconRequest")) {
final String url = message.getString("url");
handleFaviconRequest(url);
} else if (event.equals("Gecko:DelayedStartup")) {
ThreadUtils.postToBackgroundThread(new UninstallListener.DelayedStartupTask(this));
} else if (event.equals("Gecko:Ready")) {
@ -1573,11 +1545,6 @@ public abstract class GeckoApp
//register for events
registerEventListener("log");
registerEventListener("Reader:ListStatusRequest");
registerEventListener("Reader:Added");
registerEventListener("Reader:Removed");
registerEventListener("Reader:Share");
registerEventListener("Reader:FaviconRequest");
registerEventListener("onCameraCapture");
registerEventListener("Gecko:Ready");
registerEventListener("Gecko:DelayedStartup");
@ -2114,11 +2081,6 @@ public abstract class GeckoApp
public void onDestroy()
{
unregisterEventListener("log");
unregisterEventListener("Reader:ListStatusRequest");
unregisterEventListener("Reader:Added");
unregisterEventListener("Reader:Removed");
unregisterEventListener("Reader:Share");
unregisterEventListener("Reader:FaviconRequest");
unregisterEventListener("onCameraCapture");
unregisterEventListener("Gecko:Ready");
unregisterEventListener("Gecko:DelayedStartup");

View File

@ -29,13 +29,19 @@ public interface TelemetryContract {
// Set default panel.
public static final String PANEL_SET_DEFAULT = "setdefault.1";
// Sanitizing private data.
public static final String SANITIZE = "sanitize.1";
}
/**
* Holds event methods. Intended for use in
* Telemetry.sendUIEvent() as the "method" parameter.
*/
public interface Method {}
public interface Method {
// Action triggered from a dialog.
public static final String DIALOG = "dialog";
}
/**
* Holds session names. Intended for use with

View File

@ -7,6 +7,8 @@ package org.mozilla.gecko.preferences;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.json.JSONException;
import org.json.JSONObject;
@ -19,11 +21,8 @@ class PrivateDataPreference extends MultiChoicePreference {
private static final String LOGTAG = "GeckoPrivateDataPreference";
private static final String PREF_KEY_PREFIX = "private.data.";
private Context mContext;
public PrivateDataPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
}
@Override
@ -33,6 +32,8 @@ class PrivateDataPreference extends MultiChoicePreference {
if (!positiveResult)
return;
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.DIALOG);
CharSequence keys[] = getEntryKeys();
boolean values[] = getValues();
JSONObject json = new JSONObject();

View File

@ -118,6 +118,8 @@ skip-if = android_version == "10"
[testSharedPreferences]
[testSimpleDiscovery]
[testUITelemetry]
# disabled on 2.2, see bug 993813
skip-if = android_version == "8"
[testVideoDiscovery]
# Used for Talos, please don't use in mochitest

View File

@ -74,6 +74,10 @@ public final class HardwareUtils {
}
public static boolean isTelevision() {
if (Build.VERSION.SDK_INT < 16) {
// System feature not supported before Jelly Bean.
return false;
}
if (sIsTelevision == null) {
sIsTelevision = sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION);
}

View File

@ -29,6 +29,12 @@ var MemoryObserver = {
}
}
Telemetry.addData("FENNEC_LOWMEM_TAB_COUNT", tabs.length);
// Change some preferences temporarily for only this session
let defaults = Services.prefs.getDefaultBranch(null);
// Reduce the amount of decoded image data we keep around
defaults.setIntPref("image.mem.max_decoded_image_kb", 0);
},
zombify: function(tab) {

View File

@ -69,52 +69,9 @@ this.CommonUtils = {
return true;
},
exceptionStr: function exceptionStr(e) {
if (!e) {
return "" + e;
}
let message = e.message ? e.message : e;
return message + " " + CommonUtils.stackTrace(e);
},
stackTrace: function stackTrace(e) {
// Wrapped nsIException
if (e.location) {
let frame = e.location;
let output = [];
while (frame) {
// Works on frames or exceptions, munges file:// URIs to shorten the paths
// FIXME: filename munging is sort of hackish, might be confusing if
// there are multiple extensions with similar filenames
let str = "<file:unknown>";
let file = frame.filename || frame.fileName;
if (file){
str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
}
if (frame.lineNumber){
str += ":" + frame.lineNumber;
}
if (frame.name){
str = frame.name + "()@" + str;
}
if (str){
output.push(str);
}
frame = frame.caller;
}
return "Stack trace: " + output.join(" < ");
}
// Standard JS exception
if (e.stack){
return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
}
return "No traceback available";
},
// Import these from Log.jsm for backward compatibility
exceptionStr: Log.exceptionStr,
stackTrace: Log.stackTrace,
/**
* Encode byte string as base64URL (RFC 4648).

View File

@ -296,8 +296,9 @@ add_task(function test_category_manager_registration_error() {
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.contains("Provider error: DummyThrowOnInitProvider: " +
"Error registering provider from category manager: Dummy Error"));
do_check_true(msg.contains("Provider error: DummyThrowOnInitProvider: "
+ "Error registering provider from category manager: "
+ "Error: Dummy Error"));
yield storage.close();
});
@ -322,7 +323,7 @@ add_task(function test_pull_only_registration_error() {
let msg = yield deferred.promise;
do_check_true(msg.contains("Provider error: DummyPullOnlyThrowsOnInitProvider: " +
"Error registering pull-only provider: Dummy Error"));
"Error registering pull-only provider: Error: Dummy Error"));
yield storage.close();
});
@ -350,7 +351,7 @@ add_task(function test_error_during_shutdown() {
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.contains("Provider error: DummyThrowOnShutdownProvider: " +
"Error when shutting down provider: Dummy shutdown error"));
"Error when shutting down provider: Error: Dummy shutdown error"));
yield storage.close();
});

View File

@ -102,8 +102,13 @@ checkAndLogStatementPerformance(sqlite3_stmt *aStatement)
else
message.Append(" sort operations have ");
message.Append("occurred for the SQL statement '");
#ifdef MOZ_STORAGE_SORTWARNING_SQL_DUMP
message.Append("SQL command: ");
message.Append(sql);
#else
nsPrintfCString address("0x%p", aStatement);
message.Append(address);
#endif
message.Append("'. See https://developer.mozilla.org/En/Storage/Warnings "
"details.");
NS_WARNING(message.get());

View File

@ -6,6 +6,18 @@
"use strict";
const Debugger = require("Debugger");
const Services = require("Services");
const { Cc, Ci, Cu, components } = require("chrome");
const { ActorPool } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn } = DevToolsUtils;
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const { all, defer, resolve } = promise;
Cu.import("resource://gre/modules/NetUtil.jsm");
let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
@ -38,7 +50,7 @@ function mapURIToAddonID(uri, id) {
return addonManager.mapURIToAddonID(uri, id);
}
catch (e) {
DevtoolsUtils.reportException("mapURIToAddonID", e);
DevToolsUtils.reportException("mapURIToAddonID", e);
return false;
}
}
@ -313,6 +325,8 @@ BreakpointStore.prototype = {
},
};
exports.BreakpointStore = BreakpointStore;
/**
* Manages pushing event loops and automatically pops and exits them in the
* correct order as they are resolved.
@ -2398,6 +2412,7 @@ ThreadActor.prototype.requestTypes = {
"prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
};
exports.ThreadActor = ThreadActor;
/**
* Creates a PauseActor.
@ -3461,6 +3476,7 @@ ObjectActor.prototype.requestTypes = {
"scope": ObjectActor.prototype.onScope,
};
exports.ObjectActor = ObjectActor;
/**
* Functions for adding information to ObjectActor grips for the purpose of
@ -4236,6 +4252,7 @@ LongStringActor.prototype.requestTypes = {
"release": LongStringActor.prototype.onRelease
};
exports.LongStringActor = LongStringActor;
/**
* Creates an actor for the specified stack frame.
@ -4647,6 +4664,8 @@ EnvironmentActor.prototype.requestTypes = {
"bindings": EnvironmentActor.prototype.onBindings
};
exports.EnvironmentActor = EnvironmentActor;
/**
* Override the toString method in order to get more meaningful script output
* for debugging the debugger.
@ -4748,6 +4767,8 @@ update(ChromeDebuggerActor.prototype, {
}
});
exports.ChromeDebuggerActor = ChromeDebuggerActor;
/**
* Creates an actor for handling add-on debugging. AddonThreadActor is
* a thin wrapper over ThreadActor.
@ -4923,6 +4944,8 @@ update(AddonThreadActor.prototype.requestTypes, {
"attach": AddonThreadActor.prototype.onAttach
});
exports.AddonThreadActor = AddonThreadActor;
/**
* Manages the sources for a thread. Handles source maps, locations in the
* sources, etc for ThreadActors.
@ -5285,6 +5308,8 @@ ThreadSources.prototype = {
}
};
exports.ThreadSources = ThreadSources;
// Utility functions.
// TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
@ -5393,7 +5418,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) {
case "resource":
try {
NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
if (!Components.isSuccessCode(aStatus)) {
if (!components.isSuccessCode(aStatus)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatus
+ " after NetUtil.asyncFetch for url = "
@ -5424,7 +5449,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) {
let chunks = [];
let streamListener = {
onStartRequest: function(aRequest, aContext, aStatusCode) {
if (!Components.isSuccessCode(aStatusCode)) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStartRequest handler for url = "
@ -5435,7 +5460,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) {
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (!Components.isSuccessCode(aStatusCode)) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStopRequest handler for url = "
@ -5600,3 +5625,15 @@ function getInnerId(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
};
exports.register = function(handle) {
ThreadActor.breakpointStore = new BreakpointStore();
ThreadSources._blackBoxedSources = new Set(["self-hosted"]);
ThreadSources._prettyPrintedSources = new Map();
};
exports.unregister = function(handle) {
ThreadActor.breakpointStore = null;
ThreadSources._blackBoxedSources.clear();
ThreadSources._prettyPrintedSources.clear();
};

View File

@ -6,12 +6,13 @@
"use strict";
let {Ci,Cu} = require("chrome");
let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common");
let { Ci, Cu } = require("chrome");
let Services = require("Services");
let { createExtraActors, appendExtraActors } = require("devtools/server/actors/common");
let { AddonThreadActor, ThreadActor } = require("devtools/server/actors/script");
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");

View File

@ -6,17 +6,13 @@
"use strict";
let {Cc, Ci, Cu} = require("chrome");
const { Cc, Ci, Cu } = require("chrome");
const Debugger = require("Debugger");
const { DebuggerServer, ActorPool } = require("devtools/server/main");
const { EnvironmentActor, LongStringActor, ObjectActor, ThreadActor } = require("devtools/server/actors/script");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let { DebuggerServer, ActorPool } = require("devtools/server/main");
// Symbols from script.js
let { ThreadActor, EnvironmentActor, ObjectActor, LongStringActor } = DebuggerServer;
Cu.import("resource://gre/modules/jsdebugger.jsm");
addDebuggerToGlobal(this);
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "NetworkMonitor", () => {

View File

@ -346,7 +346,8 @@ var DebuggerServer = {
if (!restrictPrivileges) {
this.addTabActors();
this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
let { ChromeDebuggerActor } = require("devtools/server/actors/script");
this.addGlobalActor(ChromeDebuggerActor, "chromeDebugger");
this.registerModule("devtools/server/actors/preference");
}
@ -377,7 +378,7 @@ var DebuggerServer = {
* Install tab actors.
*/
addTabActors: function() {
this.addActors("resource://gre/modules/devtools/server/actors/script.js");
this.registerModule("devtools/server/actors/script");
this.registerModule("devtools/server/actors/webconsole");
this.registerModule("devtools/server/actors/inspector");
this.registerModule("devtools/server/actors/call-watcher");

View File

@ -8,11 +8,11 @@ const Cu = Components.utils;
const Cr = Components.results;
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const Services = devtools.require("Services");
const { ActorPool, createExtraActors, appendExtraActors } = devtools.require("devtools/server/actors/common");
const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const Services = devtools.require("Services");
const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
// Always log packets when running tests. runxpcshelltests.py will throw
// the output away anyway, unless you give it the --verbose flag.
Services.prefs.setBoolPref("devtools.debugger.log", true);
@ -34,6 +34,8 @@ tryImport("resource://gre/modules/devtools/dbg-client.jsm");
tryImport("resource://gre/modules/devtools/Loader.jsm");
tryImport("resource://gre/modules/devtools/Console.jsm");
let { BreakpointStore, LongStringActor, ThreadActor } = devtools.require("devtools/server/actors/script");
function testExceptionHook(ex) {
try {
do_report_unexpected_exception(ex);
@ -182,7 +184,7 @@ function attachTestTabAndResume(aClient, aTitle, aCallback) {
function initTestDebuggerServer()
{
DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
DebuggerServer.registerModule("devtools/server/actors/script");
DebuggerServer.addActors("resource://test/testactors.js");
// Allow incoming connections.
DebuggerServer.init(function () { return true; });
@ -191,7 +193,7 @@ function initTestDebuggerServer()
function initTestTracerServer()
{
DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
DebuggerServer.registerModule("devtools/server/actors/script");
DebuggerServer.addActors("resource://test/testactors.js");
DebuggerServer.registerModule("devtools/server/actors/tracer");
// Allow incoming connections.

View File

@ -8,9 +8,6 @@ function run_test()
{
Cu.import("resource://gre/modules/jsdebugger.jsm");
addDebuggerToGlobal(this);
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("resource://gre/modules/devtools/server/actors/script.js");
test_has_breakpoint();
test_bug_754251();

View File

@ -6,9 +6,6 @@ function run_test()
{
Cu.import("resource://gre/modules/jsdebugger.jsm");
addDebuggerToGlobal(this);
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("resource://gre/modules/devtools/server/actors/script.js");
test_LSA_disconnect();
test_LSA_grip();

View File

@ -1,6 +1,10 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const Cu = Components.utils;
const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const { ThreadActor } = devtools.require("devtools/server/actors/script");
var gTestGlobals = [];
DebuggerServer.addTestGlobal = function(aGlobal) {
gTestGlobals.push(aGlobal);

View File

@ -20,6 +20,16 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
/*
* Dump a message everywhere we can if we have a failure.
*/
function dumpError(text) {
dump(text + "\n");
Cu.reportError(text);
}
this.Log = {
Level: {
@ -80,6 +90,7 @@ this.Log = {
FileAppender: FileAppender,
BoundedFileAppender: BoundedFileAppender,
ParameterFormatter: ParameterFormatter,
// Logging helper:
// let logger = Log.repository.getLogger("foo");
// logger.info(Log.enumerateInterfaces(someObject).join(","));
@ -100,14 +111,13 @@ this.Log = {
// Logging helper:
// let logger = Log.repository.getLogger("foo");
// logger.info(Log.enumerateProperties(someObject).join(","));
enumerateProperties: function Log_enumerateProps(aObject,
aExcludeComplexTypes) {
enumerateProperties: function (aObject, aExcludeComplexTypes) {
let properties = [];
for (p in aObject) {
try {
if (aExcludeComplexTypes &&
(typeof aObject[p] == "object" || typeof aObject[p] == "function"))
(typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function"))
continue;
properties.push(p + " = " + aObject[p]);
}
@ -117,10 +127,81 @@ this.Log = {
}
return properties;
},
_formatError: function _formatError(e) {
let result = e.toString();
if (e.fileName) {
result += " (" + e.fileName;
if (e.lineNumber) {
result += ":" + e.lineNumber;
}
if (e.columnNumber) {
result += ":" + e.columnNumber;
}
result += ")";
}
return result + " " + Log.stackTrace(e);
},
// This is for back compatibility with services/common/utils.js; we duplicate
// some of the logic in ParameterFormatter
exceptionStr: function exceptionStr(e) {
if (!e) {
return "" + e;
}
if (e instanceof Ci.nsIException) {
return e.toString() + " " + Log.stackTrace(e);
}
else if (isError(e)) {
return Log._formatError(e);
}
// else
let message = e.message ? e.message : e;
return message + " " + Log.stackTrace(e);
},
stackTrace: function stackTrace(e) {
// Wrapped nsIException
if (e.location) {
let frame = e.location;
let output = [];
while (frame) {
// Works on frames or exceptions, munges file:// URIs to shorten the paths
// FIXME: filename munging is sort of hackish, might be confusing if
// there are multiple extensions with similar filenames
let str = "<file:unknown>";
let file = frame.filename || frame.fileName;
if (file) {
str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
}
if (frame.lineNumber) {
str += ":" + frame.lineNumber;
}
if (frame.name) {
str = frame.name + "()@" + str;
}
if (str) {
output.push(str);
}
frame = frame.caller;
}
return "Stack trace: " + output.join(" < ");
}
// Standard JS exception
if (e.stack) {
return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
}
return "No traceback available";
}
};
/*
* LogMessage
* Encapsulates a single log event's data
@ -128,8 +209,21 @@ this.Log = {
function LogMessage(loggerName, level, message, params) {
this.loggerName = loggerName;
this.level = level;
this.message = message;
this.params = params;
/*
* Special case to handle "log./level/(object)", for example logging a caught exception
* without providing text or params like: catch(e) { logger.warn(e) }
* Treating this as an empty text with the object in the 'params' field causes the
* object to be formatted properly by BasicFormatter.
*/
if (!params && message && (typeof(message) == "object") &&
(typeof(message.valueOf()) != "string")) {
this.message = null;
this.params = message;
} else {
// If the message text is empty, or a string, or a String object, normal handling
this.message = message;
this.params = params;
}
// The _structured field will correspond to whether this message is to
// be interpreted as a structured message.
@ -143,7 +237,7 @@ LogMessage.prototype = {
return "UNKNOWN";
},
toString: function LogMsg_toString(){
toString: function LogMsg_toString() {
let msg = "LogMessage [" + this.time + " " + this.level + " " +
this.message;
if (this.params) {
@ -178,7 +272,7 @@ Logger.prototype = {
return this._level;
if (this.parent)
return this.parent.level;
dump("Log warning: root logger configuration error: no level defined\n");
dumpError("Log warning: root logger configuration error: no level defined");
return Log.Level.All;
},
set level(level) {
@ -246,7 +340,8 @@ Logger.prototype = {
* (object) Parameters to be included in the message.
* If _level is included as a key and the corresponding value
* is a number or known level name, the message will be logged
* at the indicated level.
* at the indicated level. If _message is included as a key, the
* value is used as the descriptive text for the message.
*/
logStructured: function (action, params) {
if (!action) {
@ -255,13 +350,18 @@ Logger.prototype = {
if (!params) {
return this.log(this.level, undefined, {"action": action});
}
if (typeof params != "object") {
if (typeof(params) != "object") {
throw "The params argument is required to be an object.";
}
let level = params._level || this.level;
if ((typeof level == "string") && level in Log.Level.Numbers) {
level = Log.Level.Numbers[level];
let level = params._level;
if (level) {
let ulevel = level.toUpperCase();
if (ulevel in Log.Level.Numbers) {
level = Log.Level.Numbers[ulevel];
}
} else {
level = this.level;
}
params.action = action;
@ -428,17 +528,69 @@ Formatter.prototype = {
// Basic formatter that doesn't do anything fancy.
function BasicFormatter(dateFormat) {
if (dateFormat)
if (dateFormat) {
this.dateFormat = dateFormat;
}
this.parameterFormatter = new ParameterFormatter();
}
BasicFormatter.prototype = {
__proto__: Formatter.prototype,
/**
* Format the text of a message with optional parameters.
* If the text contains ${identifier}, replace that with
* the value of params[identifier]; if ${}, replace that with
* the entire params object. If no params have been substituted
* into the text, format the entire object and append that
* to the message.
*/
formatText: function (message) {
let params = message.params;
if (!params) {
return message.message || "";
}
// Defensive handling of non-object params
// We could add a special case for NSRESULT values here...
let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function');
// if we have params, try and find substitutions.
if (message.params && this.parameterFormatter) {
// have we successfully substituted any parameters into the message?
// in the log message
let subDone = false;
let regex = /\$\{(\S*)\}/g;
let textParts = [];
if (message.message) {
textParts.push(message.message.replace(regex, (_, sub) => {
// ${foo} means use the params['foo']
if (sub) {
if (pIsObject && sub in message.params) {
subDone = true;
return this.parameterFormatter.format(message.params[sub]);
}
return '${' + sub + '}';
}
// ${} means use the entire params object.
subDone = true;
return this.parameterFormatter.format(message.params);
}));
}
if (!subDone) {
// There were no substitutions in the text, so format the entire params object
let rest = this.parameterFormatter.format(message.params);
if (rest !== null && rest != "{}") {
textParts.push(rest);
}
}
return textParts.join(': ');
}
},
format: function BF_format(message) {
return message.time + "\t" +
message.loggerName + "\t" +
message.levelDesc + "\t" +
message.message + "\n";
this.formatText(message);
}
};
@ -451,7 +603,7 @@ MessageOnlyFormatter.prototype = Object.freeze({
__proto__: Formatter.prototype,
format: function (message) {
return message.message + "\n";
return message.message;
},
});
@ -485,6 +637,67 @@ StructuredFormatter.prototype = {
}
}
/**
* Test an object to see if it is a Mozilla JS Error.
*/
function isError(aObj) {
return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj &&
"fileName" in aObj && "lineNumber" in aObj && "stack" in aObj);
};
/*
* Parameter Formatters
* These massage an object used as a parameter for a LogMessage into
* a string representation of the object.
*/
function ParameterFormatter() {
this._name = "ParameterFormatter"
}
ParameterFormatter.prototype = {
format: function(ob) {
try {
if (ob === undefined) {
return "undefined";
}
if (ob === null) {
return "null";
}
// Pass through primitive types and objects that unbox to primitive types.
if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") &&
typeof(ob) != "function") {
return ob;
}
if (ob instanceof Ci.nsIException) {
return ob.toString() + " " + Log.stackTrace(ob);
}
else if (isError(ob)) {
return Log._formatError(ob);
}
// Just JSONify it. Filter out our internal fields and those the caller has
// already handled.
return JSON.stringify(ob, (key, val) => {
if (INTERNAL_FIELDS.has(key)) {
return undefined;
}
return val;
});
}
catch (e) {
dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e));
}
// Fancy formatting failed. Just toSource() it - but even this may fail!
try {
return ob.toSource();
} catch (_) { }
try {
return "" + ob;
} catch (_) {
return "[object]"
}
}
}
/*
* Appenders
* These can be attached to Loggers to log to different places
@ -504,10 +717,10 @@ Appender.prototype = {
}
},
toString: function App_toString() {
return this._name + " [level=" + this._level +
return this._name + " [level=" + this.level +
", formatter=" + this._formatter + "]";
},
doAppend: function App_doAppend(message) {}
doAppend: function App_doAppend(formatted) {}
};
/*
@ -522,8 +735,8 @@ function DumpAppender(formatter) {
DumpAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function DApp_doAppend(message) {
dump(message);
doAppend: function DApp_doAppend(formatted) {
dump(formatted + "\n");
}
};
@ -539,13 +752,21 @@ function ConsoleAppender(formatter) {
ConsoleAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function CApp_doAppend(message) {
if (message.level > Log.Level.Warn) {
Cu.reportError(message);
return;
// XXX this should be replaced with calls to the Browser Console
append: function App_append(message) {
if (message) {
let m = this._formatter.format(message);
if (message.level > Log.Level.Warn) {
Cu.reportError(m);
return;
}
this.doAppend(m);
}
},
doAppend: function CApp_doAppend(formatted) {
Cc["@mozilla.org/consoleservice;1"].
getService(Ci.nsIConsoleService).logStringMessage(message);
getService(Ci.nsIConsoleService).logStringMessage(formatted);
}
};
@ -614,19 +835,19 @@ StorageStreamAppender.prototype = {
this._ss = null;
},
doAppend: function (message) {
if (!message) {
doAppend: function (formatted) {
if (!formatted) {
return;
}
try {
this.outputStream.writeString(message);
this.outputStream.writeString(formatted + "\n");
} catch(ex) {
if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
// The underlying output stream is closed, so let's open a new one
// and try again.
this._outputStream = null;
} try {
this.outputStream.writeString(message);
this.outputStream.writeString(formatted + "\n");
} catch (ex) {
// Ah well, we tried, but something seems to be hosed permanently.
}
@ -682,8 +903,8 @@ FileAppender.prototype = {
});
},
doAppend: function (message) {
let array = this._encoder.encode(message);
doAppend: function (formatted) {
let array = this._encoder.encode(formatted + "\n");
if (this._file) {
this._lastWritePromise = this._file.write(array);
} else {
@ -709,7 +930,7 @@ FileAppender.prototype = {
* Bounded File appender
*
* Writes output to file using OS.File. After the total message size
* (as defined by message.length) exceeds maxSize, existing messages
* (as defined by formatted.length) exceeds maxSize, existing messages
* will be discarded, and subsequent writes will be appended to a new log file.
*/
function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
@ -723,17 +944,17 @@ function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
BoundedFileAppender.prototype = {
__proto__: FileAppender.prototype,
doAppend: function (message) {
doAppend: function (formatted) {
if (!this._removeFilePromise) {
if (this._size < this._maxSize) {
this._size += message.length;
return FileAppender.prototype.doAppend.call(this, message);
this._size += formatted.length;
return FileAppender.prototype.doAppend.call(this, formatted);
}
this._removeFilePromise = this.reset();
}
this._removeFilePromise.then(_ => {
this._removeFilePromise = null;
this.doAppend(message);
this.doAppend(formatted);
});
},

View File

@ -12,7 +12,7 @@ let testFormatter = {
format: function format(message) {
return message.loggerName + "\t" +
message.levelDesc + "\t" +
message.message + "\n";
message.message;
}
};
@ -32,7 +32,7 @@ function run_test() {
run_next_test();
}
add_test(function test_Logger() {
add_task(function test_Logger() {
let log = Log.repository.getLogger("test.logger");
let appender = new MockAppender(new Log.BasicFormatter());
@ -46,11 +46,9 @@ add_test(function test_Logger() {
let msgRe = /\d+\ttest.logger\t\INFO\tinfo test/;
do_check_true(msgRe.test(appender.messages[0]));
run_next_test();
});
add_test(function test_Logger_parent() {
add_task(function test_Logger_parent() {
// Check whether parenting is correct
let grandparentLog = Log.repository.getLogger("grandparent");
let childLog = Log.repository.getLogger("grandparent.parent.child");
@ -68,8 +66,6 @@ add_test(function test_Logger_parent() {
do_check_eq(gpAppender.messages.length, 1);
do_check_true(gpAppender.messages[0].indexOf("child info test") > 0);
run_next_test();
});
add_test(function test_LoggerWithMessagePrefix() {
@ -85,17 +81,19 @@ add_test(function test_LoggerWithMessagePrefix() {
Assert.equal(appender.messages.length, 2, "2 messages were logged.");
Assert.deepEqual(appender.messages, [
"no prefix\n",
"prefix: with prefix\n",
"no prefix",
"prefix: with prefix",
], "Prefix logger works.");
run_next_test();
});
// A utility method for checking object equivalence.
// Fields with a reqular expression value in expected will be tested
// against the corresponding value in actual. Otherwise objects
// are expected to have the same keys and equal values.
/*
* A utility method for checking object equivalence.
* Fields with a reqular expression value in expected will be tested
* against the corresponding value in actual. Otherwise objects
* are expected to have the same keys and equal values.
*/
function checkObjects(expected, actual) {
do_check_true(expected instanceof Object);
do_check_true(actual instanceof Object);
@ -117,7 +115,7 @@ function checkObjects(expected, actual) {
}
}
add_test(function test_StructuredLogCommands() {
add_task(function test_StructuredLogCommands() {
let appender = new MockAppender(new Log.StructuredFormatter());
let logger = Log.repository.getLogger("test.StructuredOutput");
logger.addAppender(appender);
@ -195,11 +193,9 @@ add_test(function test_StructuredLogCommands() {
checkObjects(messageOne, JSON.parse(appender.messages[0]));
checkObjects(messageTwo, JSON.parse(appender.messages[1]));
run_next_test();
});
add_test(function test_StorageStreamAppender() {
add_task(function test_StorageStreamAppender() {
let appender = new Log.StorageStreamAppender(testFormatter);
do_check_eq(appender.getInputStream(), null);
@ -227,8 +223,6 @@ add_test(function test_StorageStreamAppender() {
data = NetUtil.readInputStreamToString(inputStream,
inputStream.available());
do_check_eq(data, "test.StorageStreamAppender\tDEBUG\twut?!?\n");
run_next_test();
});
function fileContents(path) {
@ -335,3 +329,244 @@ add_task(function test_BoundedFileAppender() {
});
/*
* Test parameter formatting.
*/
add_task(function log_message_with_params() {
let formatter = new Log.BasicFormatter();
function formatMessage(text, params) {
let full = formatter.format(new Log.LogMessage("test.logger", Log.Level.Warn, text, params));
return full.split("\t")[3];
}
// Strings are substituted directly.
do_check_eq(formatMessage("String is ${foo}", {foo: "bar"}),
"String is bar");
// Numbers are substituted.
do_check_eq(formatMessage("Number is ${number}", {number: 47}),
"Number is 47")
// The entire params object is JSON-formatted and substituted.
do_check_eq(formatMessage("Object is ${}", {foo: "bar"}),
'Object is {"foo":"bar"}');
// An object nested inside params is JSON-formatted and substituted.
do_check_eq(formatMessage("Sub object is ${sub}", {sub: {foo: "bar"}}),
'Sub object is {"foo":"bar"}');
// The substitution field is missing from params. Leave the placeholder behind
// to make the mistake obvious.
do_check_eq(formatMessage("Missing object is ${missing}", {}),
'Missing object is ${missing}');
// Make sure we don't treat the parameter name 'false' as a falsey value.
do_check_eq(formatMessage("False is ${false}", {false: true}),
'False is true');
// If an object has a .toJSON method, the formatter uses it.
let ob = function() {};
ob.toJSON = function() {return {sneaky: "value"}};
do_check_eq(formatMessage("JSON is ${sub}", {sub: ob}),
'JSON is {"sneaky":"value"}');
// Fall back to .toSource() if JSON.stringify() fails on an object.
let ob = function() {};
ob.toJSON = function() {throw "oh noes JSON"};
do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
'Fail is (function () {})');
// Fall back to .toString if both .toJSON and .toSource fail.
ob.toSource = function() {throw "oh noes SOURCE"};
do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
'Fail is function () {}');
// Fall back to '[object]' if .toJSON, .toSource and .toString fail.
ob.toString = function() {throw "oh noes STRING"};
do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
'Fail is [object]');
// If params are passed but there are no substitution in the text
// we JSON format and append the entire parameters object.
do_check_eq(formatMessage("Text with no subs", {a: "b", c: "d"}),
'Text with no subs: {"a":"b","c":"d"}');
// If we substitute one parameter but not the other,
// we ignore any params that aren't substituted.
do_check_eq(formatMessage("Text with partial sub ${a}", {a: "b", c: "d"}),
'Text with partial sub b');
// We don't format internal fields stored in params.
do_check_eq(formatMessage("Params with _ ${}", {a: "b", _c: "d", _level:20, _message:"froo",
_time:123456, _namespace:"here.there"}),
'Params with _ {"a":"b","_c":"d"}');
// Don't print an empty params holder if all params are internal.
do_check_eq(formatMessage("All params internal", {_level:20, _message:"froo",
_time:123456, _namespace:"here.there"}),
'All params internal');
// Format params with null and undefined values.
do_check_eq(formatMessage("Null ${n} undefined ${u}", {n: null, u: undefined}),
'Null null undefined undefined');
// Format params with number, bool, and Object/String type.
do_check_eq(formatMessage("number ${n} boolean ${b} boxed Boolean ${bx} String ${s}",
{n: 45, b: false, bx: new Boolean(true), s: new String("whatevs")}),
'number 45 boolean false boxed Boolean true String whatevs');
/*
* Check that errors get special formatting if they're formatted directly as
* a named param or they're the only param, but not if they're a field in a
* larger structure.
*/
let err = Components.Exception("test exception", Components.results.NS_ERROR_FAILURE);
let str = formatMessage("Exception is ${}", err);
do_check_true(str.contains('Exception is [Exception... "test exception"'));
do_check_true(str.contains("(NS_ERROR_FAILURE)"));
let str = formatMessage("Exception is", err);
do_check_true(str.contains('Exception is: [Exception... "test exception"'));
let str = formatMessage("Exception is ${error}", {error: err});
do_check_true(str.contains('Exception is [Exception... "test exception"'));
let str = formatMessage("Exception is", {_error: err});
do_print(str);
// Exceptions buried inside objects are formatted badly.
do_check_true(str.contains('Exception is: {"_error":{}'));
// If the message text is null, the message contains only the formatted params object.
let str = formatMessage(null, err);
do_check_true(str.startsWith('[Exception... "test exception"'));
// If the text is null and 'params' is a String object, the message is exactly that string.
let str = formatMessage(null, new String("String in place of params"));
do_check_eq(str, "String in place of params");
// We use object.valueOf() internally; make sure a broken valueOf() method
// doesn't cause the logger to fail.
let vOf = {a: 1, valueOf: function() {throw "oh noes valueOf"}};
do_check_eq(formatMessage("Broken valueOf ${}", vOf),
'Broken valueOf ({a:1, valueOf:(function () {throw "oh noes valueOf"})})');
// Test edge cases of bad data to formatter:
// If 'params' is not an object, format it as a basic type.
do_check_eq(formatMessage("non-object no subst", 1),
'non-object no subst: 1');
do_check_eq(formatMessage("non-object all subst ${}", 2),
'non-object all subst 2');
// If 'params' is not an object, no named substitutions can succeed;
// therefore we leave the placeholder and append the formatted params.
do_check_eq(formatMessage("non-object named subst ${junk} space", 3),
'non-object named subst ${junk} space: 3');
// If there are no params, we leave behind the placeholders in the text.
do_check_eq(formatMessage("no params ${missing}", undefined),
'no params ${missing}');
// If params doesn't contain any of the tags requested in the text,
// we leave them all behind and append the formatted params.
do_check_eq(formatMessage("object missing tag ${missing} space", {mising: "not here"}),
'object missing tag ${missing} space: {"mising":"not here"}');
// If we are given null text and no params, the resulting formatted message is empty.
do_check_eq(formatMessage(null), '');
});
/*
* If we call a log function with a non-string object in place of the text
* argument, and no parameters, treat that the same as logging empty text
* with the object argument as parameters. This makes the log useful when the
* caller does "catch(err) {logger.error(err)}"
*/
add_task(function test_log_err_only() {
let log = Log.repository.getLogger("error.only");
let testFormatter = { format: msg => msg };
let appender = new MockAppender(testFormatter);
log.addAppender(appender);
/*
* Check that log.error(err) is treated the same as
* log.error(null, err) by the logMessage constructor; the formatMessage()
* tests above ensure that the combination of null text and an error object
* is formatted correctly.
*/
try {
eval("javascript syntax error");
}
catch (e) {
log.error(e);
msg = appender.messages.pop();
do_check_eq(msg.message, null);
do_check_eq(msg.params, e);
}
});
/*
* Test logStructured() messages through basic formatter.
*/
add_task(function test_structured_basic() {
let log = Log.repository.getLogger("test.logger");
let appender = new MockAppender(new Log.BasicFormatter());
log.level = Log.Level.Info;
appender.level = Log.Level.Info;
log.addAppender(appender);
// A structured entry with no _message is treated the same as log./level/(null, params)
// except the 'action' field is added to the object.
log.logStructured("action", {data: "structure"});
do_check_eq(appender.messages.length, 1);
do_check_true(appender.messages[0].contains('{"data":"structure","action":"action"}'));
// A structured entry with _message and substitution is treated the same as
// log./level/(null, params).
log.logStructured("action", {_message: "Structured sub ${data}", data: "structure"});
do_check_eq(appender.messages.length, 2);
do_print(appender.messages[1]);
do_check_true(appender.messages[1].contains('Structured sub structure'));
});
/*
* Test that all the basic logger methods pass the message and params through to the appender.
*/
add_task(function log_message_with_params() {
let log = Log.repository.getLogger("error.logger");
let testFormatter = { format: msg => msg };
let appender = new MockAppender(testFormatter);
log.addAppender(appender);
let testParams = {a:1, b:2};
log.fatal("Test fatal", testParams);
log.error("Test error", testParams);
log.warn("Test warn", testParams);
log.info("Test info", testParams);
log.config("Test config", testParams);
log.debug("Test debug", testParams);
log.trace("Test trace", testParams);
do_check_eq(appender.messages.length, 7);
for (let msg of appender.messages) {
do_check_true(msg.params === testParams);
do_check_true(msg.message.startsWith("Test "));
}
});
/*
* Check that we format JS Errors reasonably.
*/
add_task(function format_errors() {
let pFormat = new Log.ParameterFormatter();
// Test that subclasses of Error are recognized as errors.
err = new ReferenceError("Ref Error", "ERROR_FILE", 28);
str = pFormat.format(err);
do_check_true(str.contains("ReferenceError"));
do_check_true(str.contains("ERROR_FILE:28"));
do_check_true(str.contains("Ref Error"));
// Test that JS-generated Errors are recognized and formatted.
try {
eval("javascript syntax error");
}
catch (e) {
str = pFormat.format(e);
do_check_true(str.contains("SyntaxError: missing ;"));
// Make sure we identified it as an Error and formatted the error location as
// lineNumber:columnNumber.
do_check_true(str.contains(":1:11)"));
}
});

View File

@ -13,8 +13,6 @@ Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
@ -25,6 +23,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
"resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
@ -50,7 +51,6 @@ const FILE_DATABASE = "addons.json";
const DB_SCHEMA = 5;
const DB_MIN_JSON_SCHEMA = 5;
const DB_BATCH_TIMEOUT_MS = 50;
const DB_DATA_WRITTEN_TOPIC = "addon-repository-data-written"
const BLANK_DB = function() {
return {
@ -496,12 +496,6 @@ this.AddonRepository = {
// An array of callbacks pending the retrieval of add-ons from AddonDatabase
_pendingCallbacks: null,
// Whether a migration in currently in progress
_migrationInProgress: false,
// A callback to be called when migration finishes
_postMigrationCallback: null,
// Whether a search is currently in progress
_searching: false,
@ -553,7 +547,7 @@ this.AddonRepository = {
* @param aCallback
* The callback to pass the result back to
*/
getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) {
getCachedAddonByID: Task.async(function* (aId, aCallback) {
if (!aId || !this.cacheEnabled) {
aCallback(null);
return;
@ -569,20 +563,20 @@ this.AddonRepository = {
// Data has not been retrieved from the database, so retrieve it
this._pendingCallbacks = [];
this._pendingCallbacks.push(getAddon);
AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) {
let pendingCallbacks = self._pendingCallbacks;
// Check if cache was shutdown or deleted before callback was called
if (pendingCallbacks == null)
return;
let addons = yield AddonDatabase.retrieveStoredData();
let pendingCallbacks = self._pendingCallbacks;
// Callbacks may want to trigger a other caching operations that may
// affect _addons and _pendingCallbacks, so set to final values early
self._pendingCallbacks = null;
self._addons = aAddons;
// Check if cache was shutdown or deleted before callback was called
if (pendingCallbacks == null)
return;
pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
});
// Callbacks may want to trigger a other caching operations that may
// affect _addons and _pendingCallbacks, so set to final values early
self._pendingCallbacks = null;
self._addons = addons;
pendingCallbacks.forEach(function(aCallback) aCallback(addons));
return;
}
@ -594,7 +588,7 @@ this.AddonRepository = {
// Data has been retrieved, so immediately return result
getAddon(this._addons);
},
}),
/**
* Asynchronously repopulate cache so it only contains the add-ons
@ -1430,6 +1424,7 @@ this.AddonRepository = {
this._request.addEventListener("error", aEvent => this._reportFailure(), false);
this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
this._request.addEventListener("load", aEvent => {
logger.debug("Got metadata search load event");
let request = aEvent.target;
let responseXML = request.responseXML;
@ -1518,119 +1513,104 @@ this.AddonRepository = {
}
}
return null;
}
},
flush: function() {
return AddonDatabase.flush();
}
};
var AddonDatabase = {
// true if the database connection has been opened
initialized: false,
// false if there was an unrecoverable error openning the database
// false if there was an unrecoverable error opening the database
databaseOk: true,
connectionPromise: null,
// the in-memory database
DB: BLANK_DB(),
/**
* A getter to retrieve an nsIFile pointer to the DB
* A getter to retrieve the path to the DB
*/
get jsonFile() {
delete this.jsonFile;
return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
},
return OS.Path.join(OS.Constants.Path.profileDir, FILE_DATABASE);
},
/**
* Synchronously opens a new connection to the database file.
* Asynchronously opens a new connection to the database file.
*
* @return {Promise} a promise that resolves to the database.
*/
openConnection: function() {
this.DB = BLANK_DB();
this.initialized = true;
delete this.connection;
if (!this.connectionPromise) {
this.connectionPromise = Task.spawn(function*() {
this.DB = BLANK_DB();
let inputDB, fstream, cstream, schema;
let inputDB, schema;
try {
let data = "";
fstream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
.createInstance(Ci.nsIConverterInputStream);
try {
let data = yield OS.File.read(this.jsonFile, { encoding: "utf-8"})
inputDB = JSON.parse(data);
fstream.init(this.jsonFile, -1, 0, 0);
cstream.init(fstream, "UTF-8", 0, 0);
let (str = {}) {
let read = 0;
do {
read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
data += str.value;
} while (read != 0);
}
if (!inputDB.hasOwnProperty("addons") ||
!Array.isArray(inputDB.addons)) {
throw new Error("No addons array.");
}
inputDB = JSON.parse(data);
if (!inputDB.hasOwnProperty("schema")) {
throw new Error("No schema specified.");
}
if (!inputDB.hasOwnProperty("addons") ||
!Array.isArray(inputDB.addons)) {
throw new Error("No addons array.");
}
schema = parseInt(inputDB.schema, 10);
if (!inputDB.hasOwnProperty("schema")) {
throw new Error("No schema specified.");
}
if (!Number.isInteger(schema) ||
schema < DB_MIN_JSON_SCHEMA) {
throw new Error("Invalid schema value.");
}
} catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
logger.debug("No " + FILE_DATABASE + " found.");
schema = parseInt(inputDB.schema, 10);
// Create a blank addons.json file
this._saveDBToDisk();
if (!Number.isInteger(schema) ||
schema < DB_MIN_JSON_SCHEMA) {
throw new Error("Invalid schema value.");
}
let dbSchema = 0;
try {
dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
} catch (e) {}
} catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
logger.debug("No " + FILE_DATABASE + " found.");
if (dbSchema < DB_MIN_JSON_SCHEMA) {
let results = yield new Promise((resolve, reject) => {
AddonRepository_SQLiteMigrator.migrate(resolve);
});
// Create a blank addons.json file
this._saveDBToDisk();
if (results.length) {
yield this._insertAddons(results);
}
let dbSchema = 0;
try {
dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
} catch (e) {}
Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
}
if (dbSchema < DB_MIN_JSON_SCHEMA) {
this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => {
if (results.length)
this.insertAddons(results);
return this.DB;
} catch (e) {
logger.error("Malformed " + FILE_DATABASE + ": " + e);
this.databaseOk = false;
if (this._postMigrationCallback) {
this._postMigrationCallback();
this._postMigrationCallback = null;
}
return this.DB;
}
this._migrationInProgress = false;
});
Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
}
// We use _insertAddon manually instead of calling
// insertAddons to avoid the write to disk which would
// be a waste since this is the data that was just read.
for (let addon of inputDB.addons) {
this._insertAddon(addon);
}
return;
} catch (e) {
logger.error("Malformed " + FILE_DATABASE + ": " + e);
this.databaseOk = false;
return;
} finally {
cstream.close();
fstream.close();
return this.DB;
}.bind(this));
}
Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
// We use _insertAddon manually instead of calling
// insertAddons to avoid the write to disk which would
// be a waste since this is the data that was just read.
for (let addon of inputDB.addons) {
this._insertAddon(addon);
}
return this.connectionPromise;
},
/**
@ -1653,18 +1633,14 @@ var AddonDatabase = {
shutdown: function AD_shutdown(aSkipFlush) {
this.databaseOk = true;
if (!this.initialized) {
return Promise.resolve(0);
if (!this.connectionPromise) {
return Promise.resolve();
}
this.initialized = false;
this.__defineGetter__("connection", function shutdown_connectionGetter() {
return this.openConnection();
});
this.connectionPromise = null;
if (aSkipFlush) {
return Promise.resolve(0);
return Promise.resolve();
} else {
return this.Writer.flush();
}
@ -1680,13 +1656,14 @@ var AddonDatabase = {
delete: function AD_delete(aCallback) {
this.DB = BLANK_DB();
this.Writer.flush()
this._deleting = this.Writer.flush()
.then(null, () => {})
// shutdown(true) never rejects
.then(() => this.shutdown(true))
.then(() => OS.File.remove(this.jsonFile.path, {}))
.then(() => OS.File.remove(this.jsonFile, {}))
.then(null, error => logger.error("Unable to delete Addon Repository file " +
this.jsonFile.path, error))
this.jsonFile, error))
.then(() => this._deleting = null)
.then(aCallback);
},
@ -1710,13 +1687,26 @@ var AddonDatabase = {
get Writer() {
delete this.Writer;
this.Writer = new DeferredSave(
this.jsonFile.path,
this.jsonFile,
() => { return JSON.stringify(this); },
DB_BATCH_TIMEOUT_MS
);
return this.Writer;
},
/**
* Flush any pending I/O on the addons.json file
* @return: Promise{null}
* Resolves when the pending I/O (writing out or deleting
* addons.json) completes
*/
flush: function() {
if (this._deleting) {
return this._deleting;
}
return this.Writer.flush();
},
/**
* Asynchronously retrieve all add-ons from the database, and pass it
* to the specified callback
@ -1724,23 +1714,16 @@ var AddonDatabase = {
* @param aCallback
* The callback to pass the add-ons back to
*/
retrieveStoredData: function AD_retrieveStoredData(aCallback) {
if (!this.initialized)
this.openConnection();
retrieveStoredData: Task.async(function* (){
let db = yield this.openConnection();
let result = {};
let gatherResults = () => {
let result = {};
for (let [key, value] of this.DB.addons)
result[key] = value;
for (let [key, value] of db.addons) {
result[key] = value;
}
executeSoon(function() aCallback(result));
};
if (this._migrationInProgress)
this._postMigrationCallback = gatherResults;
else
gatherResults();
},
return result;
}),
/**
* Asynchronously repopulates the database so it only contains the
@ -1764,19 +1747,19 @@ var AddonDatabase = {
* @param aCallback
* An optional callback to call once complete
*/
insertAddons: function AD_insertAddons(aAddons, aCallback) {
if (!this.initialized)
this.openConnection();
insertAddons: Task.async(function* (aAddons, aCallback) {
yield this.openConnection();
yield this._insertAddons(aAddons, aCallback);
}),
_insertAddons: Task.async(function* (aAddons, aCallback) {
for (let addon of aAddons) {
this._insertAddon(addon);
}
this._saveDBToDisk();
if (aCallback)
executeSoon(aCallback);
},
yield this._saveDBToDisk();
aCallback && aCallback();
}),
/**
* Inserts an individual add-on into the database. If the add-on already
@ -1919,8 +1902,8 @@ var AddonDatabase = {
*/
_saveDBToDisk: function() {
return this.Writer.saveChanges().then(
function() Services.obs.notifyObservers(null, DB_DATA_WRITTEN_TOPIC, null),
logger.error);
null,
e => logger.error("SaveDBToDisk failed", e));
},
/**
@ -1980,7 +1963,3 @@ var AddonDatabase = {
appMaxVersion);
},
};
function executeSoon(aCallback) {
Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
}

View File

@ -398,6 +398,25 @@ function startupManager(aAppChanged) {
loadAddonsList();
}
/**
* Helper to spin the event loop until a promise resolves or rejects
*/
function loopUntilPromise(aPromise) {
let done = false;
aPromise.then(
() => done = true,
err => {
do_report_unexpected_exception(err);
done = true;
});
let thr = Services.tm.mainThread;
while (!done) {
thr.processNextEvent(true);
}
}
/**
* Restarts the add-on manager as if the host application was restarted.
*
@ -407,51 +426,58 @@ function startupManager(aAppChanged) {
* the application version has changed.
*/
function restartManager(aNewVersion) {
shutdownManager();
if (aNewVersion) {
gAppInfo.version = aNewVersion;
startupManager(true);
}
else {
startupManager(false);
}
loopUntilPromise(promiseRestartManager(aNewVersion));
}
function promiseRestartManager(aNewVersion) {
return promiseShutdownManager()
.then(null, err => do_report_unexpected_exception(err))
.then(() => {
if (aNewVersion) {
gAppInfo.version = aNewVersion;
startupManager(true);
}
else {
startupManager(false);
}
});
}
function shutdownManager() {
if (!gInternalManager)
return;
loopUntilPromise(promiseShutdownManager());
}
let shutdownDone = false;
Services.obs.notifyObservers(null, "quit-application-granted", null);
MockAsyncShutdown.hook().then(
() => shutdownDone = true,
err => shutdownDone = true);
let thr = Services.tm.mainThread;
// Wait until we observe the shutdown notifications
while (!shutdownDone) {
thr.processNextEvent(true);
function promiseShutdownManager() {
if (!gInternalManager) {
return Promise.resolve(false);
}
gInternalManager = null;
let hookErr = null;
Services.obs.notifyObservers(null, "quit-application-granted", null);
return MockAsyncShutdown.hook()
.then(null, err => hookErr = err)
.then( () => {
gInternalManager = null;
// Load the add-ons list as it was after application shutdown
loadAddonsList();
// Load the add-ons list as it was after application shutdown
loadAddonsList();
// Clear any crash report annotations
gAppInfo.annotations = {};
// Clear any crash report annotations
gAppInfo.annotations = {};
// Force the XPIProvider provider to reload to better
// simulate real-world usage.
let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
// This would be cleaner if I could get it as the rejection reason from
// the AddonManagerInternal.shutdown() promise
gXPISaveError = XPIscope.XPIProvider._shutdownError;
do_print("gXPISaveError set to: " + gXPISaveError);
AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
// Force the XPIProvider provider to reload to better
// simulate real-world usage.
let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
// This would be cleaner if I could get it as the rejection reason from
// the AddonManagerInternal.shutdown() promise
gXPISaveError = XPIscope.XPIProvider._shutdownError;
do_print("gXPISaveError set to: " + gXPISaveError);
AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
if (hookErr) {
throw hookErr;
}
});
}
function loadAddonsList() {

View File

@ -405,31 +405,25 @@ const WITH_EXTENSION_CACHE = [{
/*
* Trigger an AddonManager background update check
*
* @param aCallback
* Callback to call once the background update is complete
* @return Promise{null}
* Resolves when the background update notification is received
*/
function trigger_background_update(aCallback) {
Services.obs.addObserver({
observe: function(aSubject, aTopic, aData) {
Services.obs.removeObserver(this, "addons-background-update-complete");
do_execute_soon(aCallback);
}
}, "addons-background-update-complete", false);
function trigger_background_update() {
return new Promise((resolve, reject) => {
Services.obs.addObserver({
observe: function(aSubject, aTopic, aData) {
do_print("Observed " + aTopic);
Services.obs.removeObserver(this, "addons-background-update-complete");
resolve();
}
}, "addons-background-update-complete", false);
gInternalManager.notify(null);
gInternalManager.notify(null);
});
}
/*
* Check whether or not the add-ons database exists
*
* @param aExpectedExists
* Whether or not the database is expected to exist
*/
function check_database_exists(aExpectedExists) {
let file = gProfD.clone();
file.append(FILE_DATABASE);
do_check_eq(file.exists(), aExpectedExists);
}
let gDBFile = gProfD.clone();
gDBFile.append(FILE_DATABASE);
/*
* Check the actual add-on results against the expected add-on results
@ -471,302 +465,271 @@ function check_results(aActualAddons, aExpectedAddons, aFromRepository) {
* A boolean representing if results from the cache are expected
* immediately. Results are not immediate if the cache has not been
* initialized yet.
* @param aCallback
* A callback to call once the checks are complete
* @return Promise{null}
* Resolves once the checks are complete
*/
function check_cache(aExpectedToFind, aExpectedImmediately, aCallback) {
function check_cache(aExpectedToFind, aExpectedImmediately) {
do_check_eq(aExpectedToFind.length, REPOSITORY_ADDONS.length);
let pendingAddons = REPOSITORY_ADDONS.length;
let immediatelyFound = true;
let lookups = [];
for (let i = 0; i < REPOSITORY_ADDONS.length; i++) {
let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null;
AddonRepository.getCachedAddonByID(REPOSITORY_ADDONS[i].id, function(aAddon) {
do_check_eq(immediatelyFound, aExpectedImmediately);
if (expected == null)
do_check_eq(aAddon, null);
else
check_results([aAddon], [expected], true);
if (--pendingAddons == 0)
do_execute_soon(aCallback);
});
for (let i = 0 ; i < REPOSITORY_ADDONS.length ; i++) {
lookups.push(new Promise((resolve, reject) => {
let immediatelyFound = true;
let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null;
// can't Promise-wrap this because we're also testing whether the callback is
// sync or async
AddonRepository.getCachedAddonByID(REPOSITORY_ADDONS[i].id, function(aAddon) {
do_check_eq(immediatelyFound, aExpectedImmediately);
if (expected == null)
do_check_eq(aAddon, null);
else
check_results([aAddon], [expected], true);
resolve();
});
immediatelyFound = false;
}));
}
immediatelyFound = false;
return Promise.all(lookups);
}
/*
* Check an initialized cache by checking the cache, then restarting the
* Task to check an initialized cache by checking the cache, then restarting the
* manager, and checking the cache. This checks that the cache is consistent
* across manager restarts.
*
* @param aExpectedToFind
* An array of booleans representing which REPOSITORY_ADDONS are
* expected to be found in the cache
* @param aCallback
* A callback to call once the checks are complete
*/
function check_initialized_cache(aExpectedToFind, aCallback) {
check_cache(aExpectedToFind, true, function restart_initialized_cache() {
restartManager();
function* check_initialized_cache(aExpectedToFind) {
yield check_cache(aExpectedToFind, true);
yield promiseRestartManager();
// If cache is disabled, then expect results immediately
let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED);
check_cache(aExpectedToFind, !cacheEnabled, aCallback);
});
}
// Waits for the data to be written from the in-memory DB to the addons.json
// file that is done asynchronously through OS.File
function waitForFlushedData(aCallback) {
Services.obs.addObserver({
observe: function(aSubject, aTopic, aData) {
Services.obs.removeObserver(this, "addon-repository-data-written");
aCallback(aData == "true");
}
}, "addon-repository-data-written", false);
// If cache is disabled, then expect results immediately
let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED);
yield check_cache(aExpectedToFind, !cacheEnabled);
}
function run_test() {
run_next_test();
}
add_task(function* setup() {
// Setup for test
do_test_pending("test_AddonRepository_cache");
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
startupManager();
// Install XPI add-ons
installAllFiles(ADDON_FILES, function first_installs() {
restartManager();
yield promiseInstallAllFiles(ADDON_FILES);
yield promiseRestartManager();
gServer = new HttpServer();
gServer.registerDirectory("/data/", do_get_file("data"));
gServer.start(PORT);
do_execute_soon(run_test_1);
});
}
function end_test() {
gServer.stop(function() {do_test_finished("test_AddonRepository_cache");});
}
gServer = new HttpServer();
gServer.registerDirectory("/data/", do_get_file("data"));
gServer.start(PORT);
});
// Tests AddonRepository.cacheEnabled
function run_test_1() {
add_task(function* run_test_1() {
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
do_check_false(AddonRepository.cacheEnabled);
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
do_check_true(AddonRepository.cacheEnabled);
do_execute_soon(run_test_2);
}
});
// Tests that the cache and database begin as empty
function run_test_2() {
check_database_exists(false);
check_cache([false, false, false], false, function(){});
waitForFlushedData(run_test_3);
}
add_task(function* run_test_2() {
do_check_false(gDBFile.exists());
yield check_cache([false, false, false], false);
yield AddonRepository.flush();
});
// Tests repopulateCache when the search fails
function run_test_3() {
check_database_exists(true);
add_task(function* run_test_3() {
do_check_true(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
AddonRepository.repopulateCache(ADDON_IDS, function test_3_repopulated() {
check_initialized_cache([false, false, false], run_test_4);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.repopulateCache(ADDON_IDS, resolve));
yield check_initialized_cache([false, false, false]);
});
// Tests repopulateCache when search returns no results
function run_test_4() {
add_task(function* run_test_4() {
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
AddonRepository.repopulateCache(ADDON_IDS, function() {
check_initialized_cache([false, false, false], run_test_5);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.repopulateCache(ADDON_IDS, resolve));
yield check_initialized_cache([false, false, false]);
});
// Tests repopulateCache when search returns results
function run_test_5() {
add_task(function* run_test_5() {
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
AddonRepository.repopulateCache(ADDON_IDS, function() {
check_initialized_cache([true, true, true], run_test_5_1);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.repopulateCache(ADDON_IDS, resolve));
yield check_initialized_cache([true, true, true]);
});
// Tests repopulateCache when caching is disabled for a single add-on
function run_test_5_1() {
add_task(function* run_test_5_1() {
Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, false);
AddonRepository.repopulateCache(ADDON_IDS, function() {
// Reset pref for next test
Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true);
check_initialized_cache([false, true, true], run_test_6);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.repopulateCache(ADDON_IDS, resolve));
// Reset pref for next test
Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true);
yield check_initialized_cache([false, true, true]);
});
// Tests repopulateCache when caching is disabled
function run_test_6() {
check_database_exists(true);
add_task(function* run_test_6() {
do_check_true(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
AddonRepository.repopulateCache(ADDON_IDS, function() {
// Database should have been deleted
check_database_exists(false);
yield new Promise((resolve, reject) =>
AddonRepository.repopulateCache(ADDON_IDS, resolve));
// Database should have been deleted
do_check_false(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
check_cache([false, false, false], false, function() {});
waitForFlushedData(run_test_7);
});
}
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
yield check_cache([false, false, false], false);
yield AddonRepository.flush();
});
// Tests cacheAddons when the search fails
function run_test_7() {
check_database_exists(true);
add_task(function* run_test_7() {
do_check_true(gDBFile.exists());
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
AddonRepository.cacheAddons(ADDON_IDS, function() {
check_initialized_cache([false, false, false], run_test_8);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons(ADDON_IDS, resolve));
yield check_initialized_cache([false, false, false]);
});
// Tests cacheAddons when the search returns no results
function run_test_8() {
add_task(function* run_test_8() {
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
AddonRepository.cacheAddons(ADDON_IDS, function() {
check_initialized_cache([false, false, false], run_test_9);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons(ADDON_IDS, resolve));
yield check_initialized_cache([false, false, false]);
});
// Tests cacheAddons for a single add-on when search returns results
function run_test_9() {
add_task(function* run_test_9() {
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
AddonRepository.cacheAddons([ADDON_IDS[0]], function() {
check_initialized_cache([true, false, false], run_test_9_1);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons([ADDON_IDS[0]], resolve));
yield check_initialized_cache([true, false, false]);
});
// Tests cacheAddons when caching is disabled for a single add-on
function run_test_9_1() {
add_task(function* run_test_9_1() {
Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, false);
AddonRepository.cacheAddons(ADDON_IDS, function() {
// Reset pref for next test
Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true);
check_initialized_cache([true, false, true], run_test_10);
});
}
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons(ADDON_IDS, resolve));
// Reset pref for next test
Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true);
yield check_initialized_cache([true, false, true]);
});
// Tests cacheAddons for multiple add-ons, some already in the cache,
function run_test_10() {
AddonRepository.cacheAddons(ADDON_IDS, function() {
check_initialized_cache([true, true, true], run_test_11);
});
}
add_task(function* run_test_10() {
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons(ADDON_IDS, resolve));
yield check_initialized_cache([true, true, true]);
});
// Tests cacheAddons when caching is disabled
function run_test_11() {
check_database_exists(true);
add_task(function* run_test_11() {
do_check_true(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
AddonRepository.cacheAddons(ADDON_IDS, function() {
// Database deleted for repopulateCache, not cacheAddons
check_database_exists(true);
yield new Promise((resolve, reject) =>
AddonRepository.cacheAddons(ADDON_IDS, resolve));
do_check_true(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
check_initialized_cache([true, true, true], run_test_12);
});
}
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
yield check_initialized_cache([true, true, true]);
});
// Tests that XPI add-ons do not use any of the repository properties if
// caching is disabled, even if there are repository properties available
function run_test_12() {
check_database_exists(true);
add_task(function* run_test_12() {
do_check_true(gDBFile.exists());
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
AddonManager.getAddonsByIDs(ADDON_IDS, function test_12_check(aAddons) {
check_results(aAddons, WITHOUT_CACHE);
do_execute_soon(run_test_13);
});
}
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITHOUT_CACHE);
});
// Tests that a background update with caching disabled deletes the add-ons
// database, and that XPI add-ons still do not use any of repository properties
function run_test_13() {
check_database_exists(true);
add_task(function* run_test_13() {
do_check_true(gDBFile.exists());
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_EMPTY);
trigger_background_update(function() {
// Database should have been deleted
check_database_exists(false);
yield trigger_background_update();
// Database should have been deleted
do_check_false(gDBFile.exists());
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
check_results(aAddons, WITHOUT_CACHE);
do_execute_soon(run_test_14);
});
});
}
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITHOUT_CACHE);
});
// Tests that the XPI add-ons have the correct properties if caching is
// enabled but has no information
function run_test_14() {
add_task(function* run_test_14() {
Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
waitForFlushedData(function() {
check_database_exists(true);
yield trigger_background_update();
yield AddonRepository.flush();
do_check_true(gDBFile.exists());
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
check_results(aAddons, WITHOUT_CACHE);
do_execute_soon(run_test_15);
});
});
trigger_background_update();
}
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITHOUT_CACHE);
});
// Tests that the XPI add-ons correctly use the repository properties when
// caching is enabled and the repository information is available
function run_test_15() {
add_task(function* run_test_15() {
Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_RESULTS);
trigger_background_update(function() {
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
check_results(aAddons, WITH_CACHE);
do_execute_soon(run_test_16);
});
});
}
yield trigger_background_update();
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITH_CACHE);
});
// Tests that restarting the manager does not change the checked properties
// on the XPI add-ons (repository properties still exist and are still properly
// used)
function run_test_16() {
restartManager();
add_task(function* run_test_16() {
yield promiseRestartManager();
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
check_results(aAddons, WITH_CACHE);
do_execute_soon(run_test_17);
});
}
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITH_CACHE);
});
// Tests that setting a list of types to cache works
function run_test_17() {
add_task(function* run_test_17() {
Services.prefs.setCharPref(PREF_GETADDONS_CACHE_TYPES, "foo,bar,extension,baz");
trigger_background_update(function() {
AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
check_results(aAddons, WITH_EXTENSION_CACHE);
end_test();
});
});
}
yield trigger_background_update();
let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
check_results(aAddons, WITH_EXTENSION_CACHE);
});
add_task(function* end_test() {
yield new Promise((resolve, reject) => gServer.stop(resolve));
});