diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul index e632a4fe830..7e736781772 100644 --- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -639,7 +639,7 @@ { 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); }); } diff --git a/browser/components/tabview/test/browser_tabview_bug608405.js b/browser/components/tabview/test/browser_tabview_bug608405.js index e374e7c5169..cb97e0afcfe 100644 --- a/browser/components/tabview/test/browser_tabview_bug608405.js +++ b/browser/components/tabview/test/browser_tabview_bug608405.js @@ -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); }); } diff --git a/browser/components/tabview/test/browser_tabview_bug613541.js b/browser/components/tabview/test/browser_tabview_bug613541.js index 6b75936bc6f..c218925e047 100644 --- a/browser/components/tabview/test/browser_tabview_bug613541.js +++ b/browser/components/tabview/test/browser_tabview_bug613541.js @@ -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 { + win = window; + cw = win.TabView.getContentWindow(); next(); }); } diff --git a/browser/components/tabview/test/browser_tabview_bug624847.js b/browser/components/tabview/test/browser_tabview_bug624847.js index bc5cac4a0d4..323dc4d81a8 100644 --- a/browser/components/tabview/test/browser_tabview_bug624847.js +++ b/browser/components/tabview/test/browser_tabview_bug624847.js @@ -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); }); } diff --git a/browser/components/tabview/test/browser_tabview_bug626455.js b/browser/components/tabview/test/browser_tabview_bug626455.js index beabf825925..d75d9a368d6 100644 --- a/browser/components/tabview/test/browser_tabview_bug626455.js +++ b/browser/components/tabview/test/browser_tabview_bug626455.js @@ -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,

test for bug 626455, tab1"); + win.gBrowser.browsers[0].loadURI("data:text/html,

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); } // ---------- diff --git a/browser/components/tabview/test/browser_tabview_bug633788.js b/browser/components/tabview/test/browser_tabview_bug633788.js index f69e32baf6a..74974caf21f 100644 --- a/browser/components/tabview/test/browser_tabview_bug633788.js +++ b/browser/components/tabview/test/browser_tabview_bug633788.js @@ -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); }); } diff --git a/browser/components/tabview/test/browser_tabview_bug685692.js b/browser/components/tabview/test/browser_tabview_bug685692.js index db4e83a8f42..096b7fe62fc 100644 --- a/browser/components/tabview/test/browser_tabview_bug685692.js +++ b/browser/components/tabview/test/browser_tabview_bug685692.js @@ -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); } diff --git a/browser/devtools/debugger/debugger-toolbar.js b/browser/devtools/debugger/debugger-toolbar.js index 2c9b56a6710..f88a09db111 100644 --- a/browser/devtools/debugger/debugger-toolbar.js +++ b/browser/devtools/debugger/debugger-toolbar.js @@ -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); } }, diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index ce3fd654e60..3accc456b3e 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -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 diff --git a/browser/experiments/Experiments.jsm b/browser/experiments/Experiments.jsm index 5de730079d2..069b805ffa3 100644 --- a/browser/experiments/Experiments.jsm +++ b/browser/experiments/Experiments.jsm @@ -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; }), diff --git a/browser/experiments/test/xpcshell/head.js b/browser/experiments/test/xpcshell/head.js index 4bbaee22fa2..a45020303ee 100644 --- a/browser/experiments/test/xpcshell/head.js +++ b/browser/experiments/test/xpcshell/head.js @@ -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"; diff --git a/browser/experiments/test/xpcshell/test_api.js b/browser/experiments/test/xpcshell/test_api.js index b38d2d69a22..0c7ba5ea7dd 100644 --- a/browser/experiments/test/xpcshell/test_api.js +++ b/browser/experiments/test/xpcshell/test_api.js @@ -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); }); diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css index 5d7a99bf23d..627d98eb33d 100644 --- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -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; diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 4cdad1a0538..ca5dd21022e 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -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; } diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index ddc97b44225..a5d373b52dc 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -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), diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index f86cd9a7f04..a92a36f8656 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -418,6 +418,31 @@ abstract public class BrowserApp extends GeckoApp }); } + private void handleReaderFaviconRequest(final String url) { + (new UiAsyncTask(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; diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 0eb30057c76..b417996b52c 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -496,31 +496,6 @@ public abstract class GeckoApp outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession); } - void handleFaviconRequest(final String url) { - (new UiAsyncTask(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"); diff --git a/mobile/android/base/TelemetryContract.java b/mobile/android/base/TelemetryContract.java index 1f084205bb1..b9f79ecb960 100644 --- a/mobile/android/base/TelemetryContract.java +++ b/mobile/android/base/TelemetryContract.java @@ -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 diff --git a/mobile/android/base/preferences/PrivateDataPreference.java b/mobile/android/base/preferences/PrivateDataPreference.java index 38d1a459265..1aa52ec8b03 100644 --- a/mobile/android/base/preferences/PrivateDataPreference.java +++ b/mobile/android/base/preferences/PrivateDataPreference.java @@ -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(); diff --git a/mobile/android/base/tests/robocop.ini b/mobile/android/base/tests/robocop.ini index 48a9d5a9fb5..98dc1ccff78 100644 --- a/mobile/android/base/tests/robocop.ini +++ b/mobile/android/base/tests/robocop.ini @@ -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 diff --git a/mobile/android/base/util/HardwareUtils.java b/mobile/android/base/util/HardwareUtils.java index d80d8786435..632e5c6dcd2 100644 --- a/mobile/android/base/util/HardwareUtils.java +++ b/mobile/android/base/util/HardwareUtils.java @@ -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); } diff --git a/mobile/android/chrome/content/MemoryObserver.js b/mobile/android/chrome/content/MemoryObserver.js index e2362a5e445..3531b0837dc 100644 --- a/mobile/android/chrome/content/MemoryObserver.js +++ b/mobile/android/chrome/content/MemoryObserver.js @@ -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) { diff --git a/services/common/utils.js b/services/common/utils.js index ddc3ceafea9..61c050d0212 100644 --- a/services/common/utils.js +++ b/services/common/utils.js @@ -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 = ""; - - 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). diff --git a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js index 82f0328a391..f7b2bdf7d84 100644 --- a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js +++ b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js @@ -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(); }); diff --git a/storage/src/mozStoragePrivateHelpers.cpp b/storage/src/mozStoragePrivateHelpers.cpp index 3e97b6f2bb5..085bb4ff206 100644 --- a/storage/src/mozStoragePrivateHelpers.cpp +++ b/storage/src/mozStoragePrivateHelpers.cpp @@ -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()); diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index bdb4933ea8a..694c64380aa 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -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(); +}; diff --git a/toolkit/devtools/server/actors/webbrowser.js b/toolkit/devtools/server/actors/webbrowser.js index e3f473e74b1..4a617b3e638 100644 --- a/toolkit/devtools/server/actors/webbrowser.js +++ b/toolkit/devtools/server/actors/webbrowser.js @@ -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"); diff --git a/toolkit/devtools/server/actors/webconsole.js b/toolkit/devtools/server/actors/webconsole.js index ce6308db39b..b65d0433ae5 100644 --- a/toolkit/devtools/server/actors/webconsole.js +++ b/toolkit/devtools/server/actors/webconsole.js @@ -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", () => { diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 27e1518ee4f..3a764159695 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -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"); diff --git a/toolkit/devtools/server/tests/unit/head_dbg.js b/toolkit/devtools/server/tests/unit/head_dbg.js index 2bc959d28ed..fa127f91350 100644 --- a/toolkit/devtools/server/tests/unit/head_dbg.js +++ b/toolkit/devtools/server/tests/unit/head_dbg.js @@ -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. diff --git a/toolkit/devtools/server/tests/unit/test_breakpointstore.js b/toolkit/devtools/server/tests/unit/test_breakpointstore.js index 5ca4c12064a..b49e94772c5 100644 --- a/toolkit/devtools/server/tests/unit/test_breakpointstore.js +++ b/toolkit/devtools/server/tests/unit/test_breakpointstore.js @@ -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(); diff --git a/toolkit/devtools/server/tests/unit/test_longstringactor.js b/toolkit/devtools/server/tests/unit/test_longstringactor.js index 94a07cc7fb6..3cbdb6d7f5c 100644 --- a/toolkit/devtools/server/tests/unit/test_longstringactor.js +++ b/toolkit/devtools/server/tests/unit/test_longstringactor.js @@ -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(); diff --git a/toolkit/devtools/server/tests/unit/testactors.js b/toolkit/devtools/server/tests/unit/testactors.js index 3737589fb44..3c1eb4b0f31 100644 --- a/toolkit/devtools/server/tests/unit/testactors.js +++ b/toolkit/devtools/server/tests/unit/testactors.js @@ -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); diff --git a/toolkit/modules/Log.jsm b/toolkit/modules/Log.jsm index cddd3103cde..a38a941bf32 100644 --- a/toolkit/modules/Log.jsm +++ b/toolkit/modules/Log.jsm @@ -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 = ""; + + 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); }); }, diff --git a/toolkit/modules/tests/xpcshell/test_Log.js b/toolkit/modules/tests/xpcshell/test_Log.js index 0e4f2690946..93ac2d8db92 100644 --- a/toolkit/modules/tests/xpcshell/test_Log.js +++ b/toolkit/modules/tests/xpcshell/test_Log.js @@ -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)")); + } +}); diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm index 6aa2b7ef683..6f5e917f109 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm @@ -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); -} diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js index 7df61186fcd..3fc241efacd 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -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() { diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js index 1a6380e60ca..15df2559b6e 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js @@ -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)); +});