gecko-dev/dom/tests/browser/browser_localStorage_e10s.js
Andrew Sutherland 8e40043ab4 Bug 1350637 - Part 10: Update LocalStorage e10s tests for change to PBackground. r=janv
The e10s tests were written assuming a world where ContentTask.spawn and
LocalStorage were both PContent.  Also, that Quantum DOM labeling wasn't
something to worry about.  These assumptions no longer held, resulting in
the test intermittently failing if all changes hadn't propagated via
PBackground to the tab under test by the time the ContentTask.spawn
state retrieval call made it to the tab's main thread.

This has been corrected by using "storage" events where already in use and
polling where not in use.  Plase see the added comments for more details.
2017-08-07 04:14:17 -04:00

473 lines
19 KiB
JavaScript

const HELPER_PAGE_URL =
"http://example.com/browser/dom/tests/browser/page_localstorage_e10s.html";
const HELPER_PAGE_ORIGIN = "http://example.com/";
// Simple tab wrapper abstracting our messaging mechanism;
class KnownTab {
constructor(name, tab) {
this.name = name;
this.tab = tab;
}
cleanup() {
this.tab = null;
}
}
// Simple data structure class to help us track opened tabs and their pids.
class KnownTabs {
constructor() {
this.byPid = new Map();
this.byName = new Map();
}
cleanup() {
this.byPid = null;
this.byName = null;
}
}
/**
* Open our helper page in a tab in its own content process, asserting that it
* really is in its own process. We initially load and wait for about:blank to
* load, and only then loadURI to our actual page. This is to ensure that
* LocalStorageManager has had an opportunity to be created and populate
* mOriginsHavingData.
*
* (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of
* the unconditional call to nsGlobalWindow::PreloadLocalStorage. This will
* reliably create the StorageDBChild instance, and its corresponding
* StorageDBParent will send the set of origins when it is constructed.)
*/
async function openTestTabInOwnProcess(name, knownTabs) {
let realUrl = HELPER_PAGE_URL + '?' + encodeURIComponent(name);
// Load and wait for about:blank.
let tab = await BrowserTestUtils.openNewForegroundTab({
gBrowser, opening: 'about:blank', forceNewProcess: true
});
let pid = tab.linkedBrowser.frameLoader.tabParent.osPid;
ok(!knownTabs.byName.has(name), "tab needs its own name: " + name);
ok(!knownTabs.byPid.has(pid), "tab needs to be in its own process: " + pid);
let knownTab = new KnownTab(name, tab);
knownTabs.byPid.set(pid, knownTab);
knownTabs.byName.set(name, knownTab);
// Now trigger the actual load of our page.
tab.linkedBrowser.loadURI(realUrl);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
is(tab.linkedBrowser.frameLoader.tabParent.osPid, pid, "still same pid");
return knownTab;
}
/**
* Close all the tabs we opened.
*/
async function cleanupTabs(knownTabs) {
for (let knownTab of knownTabs.byName.values()) {
await BrowserTestUtils.removeTab(knownTab.tab);
knownTab.cleanup();
}
knownTabs.cleanup();
}
/**
* Wait for a LocalStorage flush to occur. This notification can occur as a
* result of any of:
* - The normal, hardcoded 5-second flush timer.
* - InsertDBOp seeing a preload op for an origin with outstanding changes.
* - Us generating a "domstorage-test-flush-force" observer notification.
*/
function waitForLocalStorageFlush() {
return new Promise(function(resolve) {
let observer = {
observe: function() {
SpecialPowers.removeObserver(observer, "domstorage-test-flushed");
resolve();
}
};
SpecialPowers.addObserver(observer, "domstorage-test-flushed");
});
}
/**
* Trigger and wait for a flush. This is only necessary for forcing
* mOriginsHavingData to be updated. Normal operations exposed to content know
* to automatically flush when necessary for correctness.
*
* The notification we're waiting for to verify flushing is fundamentally
* ambiguous (see waitForLocalStorageFlush), so we actually trigger the flush
* twice and wait twice. In the event there was a race, there will be 3 flush
* notifications, but correctness is guaranteed after the second notification.
*/
function triggerAndWaitForLocalStorageFlush() {
SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
// This first wait is ambiguous...
return waitForLocalStorageFlush().then(function() {
// So issue a second flush and wait for that.
SpecialPowers.notifyObservers(null, "domstorage-test-flush-force");
return waitForLocalStorageFlush();
})
}
/**
* Clear the origin's storage so that "OriginsHavingData" will return false for
* our origin. Note that this is only the case for AsyncClear() which is
* explicitly issued against a cache, or AsyncClearAll() which we can trigger
* by wiping all storage. However, the more targeted domain clearings that
* we can trigger via observer, AsyncClearMatchingOrigin and
* AsyncClearMatchingOriginAttributes will not clear the hashtable entry for
* the origin.
*
* So we explicitly access the cache here in the parent for the origin and issue
* an explicit clear. Clearing all storage might be a little easier but seems
* like asking for intermittent failures.
*/
function clearOriginStorageEnsuringNoPreload() {
let principal =
Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(
HELPER_PAGE_ORIGIN);
// We want to use createStorage to force the cache to be created so we can
// issue the clear. It's possible for getStorage to return false but for the
// origin preload hash to still have our origin in it.
let storage = Services.domStorageManager.createStorage(null, principal, "");
storage.clear();
// We also need to trigger a flush os that mOriginsHavingData gets updated.
// The inherent flush race is fine here because
return triggerAndWaitForLocalStorageFlush();
}
async function verifyTabPreload(knownTab, expectStorageExists) {
let storageExists = await ContentTask.spawn(
knownTab.tab.linkedBrowser,
HELPER_PAGE_ORIGIN,
function(origin) {
let principal =
Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(
origin);
return !!Services.domStorageManager.getStorage(null, principal);
});
is(storageExists, expectStorageExists, "Storage existence === preload");
}
/**
* Instruct the given tab to execute the given series of mutations. For
* simplicity, the mutations representation matches the expected events rep.
*/
async function mutateTabStorage(knownTab, mutations, sentinelValue) {
await ContentTask.spawn(
knownTab.tab.linkedBrowser,
{ mutations, sentinelValue },
function(args) {
return content.wrappedJSObject.mutateStorage(args);
});
}
/**
* Instruct the given tab to add a "storage" event listener and record all
* received events. verifyTabStorageEvents is the corresponding method to
* check and assert the recorded events.
*/
async function recordTabStorageEvents(knownTab, sentinelValue) {
await ContentTask.spawn(
knownTab.tab.linkedBrowser,
sentinelValue,
function(sentinelValue) {
return content.wrappedJSObject.listenForStorageEvents(sentinelValue);
});
}
/**
* Retrieve the current localStorage contents perceived by the tab and assert
* that they match the provided expected state.
*
* If maybeSentinel is non-null, it's assumed to be a string that identifies the
* value we should be waiting for the sentinel key to take on. This is
* necessary because we cannot make any assumptions about when state will be
* propagated to the given process. See the comments in
* page_localstorage_e10s.js for more context. In general, a sentinel value is
* required for correctness unless the process in question is the one where the
* writes were performed or verifyTabStorageEvents was used.
*/
async function verifyTabStorageState(knownTab, expectedState, maybeSentinel) {
let actualState = await ContentTask.spawn(
knownTab.tab.linkedBrowser,
maybeSentinel,
function(maybeSentinel) {
return content.wrappedJSObject.getStorageState(maybeSentinel);
});
for (let [expectedKey, expectedValue] of Object.entries(expectedState)) {
ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey);
is(actualState[expectedKey], expectedValue, "value correct");
}
for (let actualKey of Object.keys(actualState)) {
if (!expectedState.hasOwnProperty(actualKey)) {
ok(false, "actual state has key it shouldn't have: " + actualKey);
}
}
}
/**
* Retrieve and clear the storage events recorded by the tab and assert that
* they match the provided expected events. For simplicity, the expected events
* representation is the same as that used by mutateTabStorage.
*
* Note that by convention for test readability we are passed a 3rd argument of
* the sentinel value, but we don't actually care what it is.
*/
async function verifyTabStorageEvents(knownTab, expectedEvents) {
let actualEvents = await ContentTask.spawn(
knownTab.tab.linkedBrowser,
{},
function() {
return content.wrappedJSObject.returnAndClearStorageEvents();
});
is(actualEvents.length, expectedEvents.length, "right number of events");
for (let i = 0; i < actualEvents.length; i++) {
let [actualKey, actualNewValue, actualOldValue] = actualEvents[i];
let [expectedKey, expectedNewValue, expectedOldValue] = expectedEvents[i];
is(actualKey, expectedKey, "keys match");
is(actualNewValue, expectedNewValue, "new values match");
is(actualOldValue, expectedOldValue, "old values match");
}
}
// We spin up a ton of child processes.
requestLongerTimeout(4);
/**
* Verify the basics of our multi-e10s localStorage support. We are focused on
* whitebox testing two things. When this is being written, broadcast filtering
* is not in place, but the test is intended to attempt to verify that its
* implementation does not break things.
*
* 1) That pages see the same localStorage state in a timely fashion when
* engaging in non-conflicting operations. We are not testing races or
* conflict resolution; the spec does not cover that.
*
* 2) That there are no edge-cases related to when the Storage instance is
* created for the page or the StorageCache for the origin. (StorageCache is
* what actually backs the Storage binding exposed to the page.) This
* matters because the following reasons can exist for them to be created:
* - Preload, on the basis of knowing the origin uses localStorage. The
* interesting edge case is when we have the same origin open in different
* processes and the origin starts using localStorage when it did not
* before. Preload will not have instantiated bindings, which could impact
* correctness.
* - The page accessing localStorage for read or write purposes. This is the
* obvious, boring one.
* - The page adding a "storage" listener. This is less obvious and
* interacts with the preload edge-case mentioned above. The page needs to
* hear "storage" events even if the page has not touched localStorage
* itself and its origin had nothing stored in localStorage when the page
* was created.
*
* We use the same simple child page in all tabs that:
* - can be instructed to listen for and record "storage" events
* - can be instructed to issue a series of localStorage writes
* - can be instructed to return the current entire localStorage contents
*
* We open the 5 following tabs:
* - Open a "writer" tab that does not listen for "storage" events and will
* issue only writes.
* - Open a "listener" tab instructed to listen for "storage" events
* immediately. We expect it to capture all events.
* - Open an "reader" tab that does not listen for "storage" events and will
* only issue reads when instructed.
* - Open a "lateWriteThenListen" tab that initially does nothing. We will
* later tell it to issue a write and then listen for events to make sure it
* captures the later events.
* - Open "lateOpenSeesPreload" tab after we've done everything and ensure that
* it preloads/precaches the data without us having touched localStorage or
* added an event listener.
*/
add_task(async function() {
await SpecialPowers.pushPrefEnv({
set: [
// Stop the preallocated process manager from speculatively creating
// processes. Our test explicitly asserts on whether preload happened or
// not for each tab's process. This information is loaded and latched by
// the StorageDBParent constructor which the child process's
// LocalStorageManager() constructor causes to be created via a call to
// LocalStorageCache::StartDatabase(). Although the service is lazily
// created and should not have been created prior to our opening the tab,
// it's safest to ensure the process simply didn't exist before we ask for
// it.
//
// This is done in conjunction with our use of forceNewProcess when
// opening tabs. There would be no point if we weren't also requesting a
// new process.
["dom.ipc.processPrelaunch.enabled", false],
// Enable LocalStorage's testing API so we can explicitly trigger a flush
// when needed.
["dom.storage.testing", true],
]
});
// Ensure that there is no localstorage data or potential false positives for
// localstorage preloads by forcing the origin to be cleared prior to the
// start of our test.
await clearOriginStorageEnsuringNoPreload();
// Make sure mOriginsHavingData gets updated.
await triggerAndWaitForLocalStorageFlush();
// - Open tabs. Don't configure any of them yet.
const knownTabs = new KnownTabs();
const writerTab = await openTestTabInOwnProcess("writer", knownTabs);
const listenerTab = await openTestTabInOwnProcess("listener", knownTabs);
const readerTab = await openTestTabInOwnProcess("reader", knownTabs);
const lateWriteThenListenTab = await openTestTabInOwnProcess(
"lateWriteThenListen", knownTabs);
// Sanity check that preloading did not occur in the tabs.
await verifyTabPreload(writerTab, false);
await verifyTabPreload(listenerTab, false);
await verifyTabPreload(readerTab, false);
// - Configure the tabs.
const initialSentinel = 'initial';
const noSentinelCheck = null;
await recordTabStorageEvents(listenerTab, initialSentinel);
// - Issue the initial batch of writes and verify.
info("initial writes");
const initialWriteMutations = [
//[key (null=clear), newValue (null=delete), oldValue (verification)]
["getsCleared", "1", null],
["alsoGetsCleared", "2", null],
[null, null, null],
["stays", "3", null],
["clobbered", "pre", null],
["getsDeletedLater", "4", null],
["getsDeletedImmediately", "5", null],
["getsDeletedImmediately", null, "5"],
["alsoStays", "6", null],
["getsDeletedLater", null, "4"],
["clobbered", "post", "pre"]
];
const initialWriteState = {
stays: "3",
clobbered: "post",
alsoStays: "6"
};
await mutateTabStorage(writerTab, initialWriteMutations, initialSentinel);
// We expect the writer tab to have the correct state because it just did the
// writes. We do not perform a sentinel-check because the writes should be
// locally available and consistent.
await verifyTabStorageState(writerTab, initialWriteState, noSentinelCheck);
// We expect the listener tab to have heard all events despite preload not
// having occurred and despite not issuing any reads or writes itself. We
// intentionally check the events before the state because we're most
// interested in adding the listener having had a side-effect of subscribing
// to changes for the process.
//
// We ensure it had a chance to hear all of the events because we told
// recordTabStorageEvents to listen for the given sentinel. The state check
// then does not need to do a sentinel check.
await verifyTabStorageEvents(
listenerTab, initialWriteMutations, initialSentinel);
await verifyTabStorageState(
listenerTab, initialWriteState, noSentinelCheck);
// We expect the reader tab to retrieve the current localStorage state from
// the database. Because of the above checks, we are confident that the
// writes have hit PBackground and therefore that the (synchronous) state
// retrieval contains all the data we need. No sentinel-check is required.
await verifyTabStorageState(readerTab, initialWriteState, noSentinelCheck);
// - Issue second set of writes from lateWriteThenListen
// This tests that our new tab that begins by issuing only writes is building
// on top of the existing state (although we don't verify that until after the
// next set of mutations). We also verify that the initial "writerTab" that
// was our first tab and started with only writes sees the writes, even though
// it did not add an event listener.
info("late writes");
const lateWriteSentinel = 'lateWrite';
const lateWriteMutations = [
["lateStays", "10", null],
["lateClobbered", "latePre", null],
["lateDeleted", "11", null],
["lateClobbered", "lastPost", "latePre"],
["lateDeleted", null, "11"]
];
const lateWriteState = Object.assign({}, initialWriteState, {
lateStays: "10",
lateClobbered: "lastPost"
});
await recordTabStorageEvents(listenerTab, lateWriteSentinel);
await mutateTabStorage(
lateWriteThenListenTab, lateWriteMutations, lateWriteSentinel);
// Verify the writer tab saw the writes. It has to wait for the sentinel to
// appear before checking.
await verifyTabStorageState(writerTab, lateWriteState, lateWriteSentinel);
// Wait for the sentinel event before checking the events and then the state.
await verifyTabStorageEvents(
listenerTab, lateWriteMutations, lateWriteSentinel);
await verifyTabStorageState(listenerTab, lateWriteState, noSentinelCheck);
// We need to wait for the sentinel to show up for the reader.
await verifyTabStorageState(readerTab, lateWriteState, lateWriteSentinel);
// - Issue last set of writes from writerTab.
info("last set of writes");
const lastWriteSentinel = 'lastWrite';
const lastWriteMutations = [
["lastStays", "20", null],
["lastDeleted", "21", null],
["lastClobbered", "lastPre", null],
["lastClobbered", "lastPost", "lastPre"],
["lastDeleted", null, "21"]
];
const lastWriteState = Object.assign({}, lateWriteState, {
lastStays: "20",
lastClobbered: "lastPost"
});
await recordTabStorageEvents(listenerTab, lastWriteSentinel);
await recordTabStorageEvents(lateWriteThenListenTab, lastWriteSentinel);
await mutateTabStorage(writerTab, lastWriteMutations, lastWriteSentinel);
// The writer performed the writes, no need to wait for the sentinel.
await verifyTabStorageState(writerTab, lastWriteState, noSentinelCheck);
// Wait for the sentinel event to be received, then check.
await verifyTabStorageEvents(
listenerTab, lastWriteMutations, lastWriteSentinel);
await verifyTabStorageState(listenerTab, lastWriteState, noSentinelCheck);
// We need to wait for the sentinel to show up for the reader.
await verifyTabStorageState(readerTab, lastWriteState, lastWriteSentinel);
// Wait for the sentinel event to be received, then check.
await verifyTabStorageEvents(
lateWriteThenListenTab, lastWriteMutations, lastWriteSentinel);
await verifyTabStorageState(
lateWriteThenListenTab, lastWriteState, noSentinelCheck);
// - Force a LocalStorage DB flush so mOriginsHavingData is updated.
// mOriginsHavingData is only updated when the storage thread runs its
// accumulated operations during the flush. If we don't initiate and ensure
// that a flush has occurred before moving on to the next step,
// mOriginsHavingData may not include our origin when it's sent down to the
// child process.
info("flush to make preload check work");
await triggerAndWaitForLocalStorageFlush();
// - Open a fresh tab and make sure it sees the precache/preload
info("late open preload check");
const lateOpenSeesPreload =
await openTestTabInOwnProcess("lateOpenSeesPreload", knownTabs);
await verifyTabPreload(lateOpenSeesPreload, true);
// - Clean up.
await cleanupTabs(knownTabs);
clearOriginStorageEnsuringNoPreload();
});