"use strict"; const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { XPCShellContentUtils } = ChromeUtils.importESModule( "resource://testing-common/XPCShellContentUtils.sys.mjs" ); const PROCESS_COUNT_PREF = "dom.ipc.processCount"; const remote = AppConstants.platform !== "android"; XPCShellContentUtils.init(this); let contentPage; async function readBlob(key, sharedData = Services.cpmm.sharedData) { const { ExtensionUtils } = ChromeUtils.importESModule( "resource://gre/modules/ExtensionUtils.sys.mjs" ); let reader = new FileReader(); reader.readAsText(sharedData.get(key)); await ExtensionUtils.promiseEvent(reader, "loadend"); return reader.result; } function getKey(key, sharedData = Services.cpmm.sharedData) { return sharedData.get(key); } function hasKey(key, sharedData = Services.cpmm.sharedData) { return sharedData.has(key); } function getContents(sharedMap = Services.cpmm.sharedData) { return { keys: Array.from(sharedMap.keys()), values: Array.from(sharedMap.values()), entries: Array.from(sharedMap.entries()), getValues: Array.from(sharedMap.keys(), key => sharedMap.get(key)), }; } function checkMap(contents, expected) { expected = Array.from(expected); equal(contents.keys.length, expected.length, "Got correct number of keys"); equal( contents.values.length, expected.length, "Got correct number of values" ); equal( contents.entries.length, expected.length, "Got correct number of entries" ); for (let [i, [key, val]] of contents.entries.entries()) { equal(key, contents.keys[i], `keys()[${i}] matches entries()[${i}]`); deepEqual( val, contents.values[i], `values()[${i}] matches entries()[${i}]` ); } expected.sort(([a], [b]) => a.localeCompare(b)); contents.entries.sort(([a], [b]) => a.localeCompare(b)); for (let [i, [key, val]] of contents.entries.entries()) { equal( key, expected[i][0], `expected[${i}].key matches entries()[${i}].key` ); deepEqual( val, expected[i][1], `expected[${i}].value matches entries()[${i}].value` ); } } function checkParentMap(expected) { info("Checking parent map"); checkMap(getContents(Services.ppmm.sharedData), expected); } async function checkContentMaps(expected, parentOnly = false) { info("Checking in-process content map"); checkMap(getContents(Services.cpmm.sharedData), expected); if (!parentOnly) { info("Checking out-of-process content map"); let contents = await contentPage.spawn([], getContents); checkMap(contents, expected); } } async function loadContentPage() { let page = await XPCShellContentUtils.loadContentPage("data:text/html,", { remote, }); registerCleanupFunction(() => page.close()); return page; } add_setup(async function () { // Start with one content process so that we can increase the number // later and test the behavior of a fresh content process. Services.prefs.setIntPref(PROCESS_COUNT_PREF, 1); contentPage = await loadContentPage(); }); add_task(async function test_sharedMap() { let { sharedData } = Services.ppmm; info("Check that parent and child maps are both initially empty"); checkParentMap([]); await checkContentMaps([]); let expected = [ ["foo-a", { foo: "a" }], ["foo-b", { foo: "b" }], ["bar-c", null], ["bar-d", 42], ]; function setKey(key, val) { sharedData.set(key, val); expected = expected.filter(([k]) => k != key); expected.push([key, val]); } function deleteKey(key) { sharedData.delete(key); expected = expected.filter(([k]) => k != key); } for (let [key, val] of expected) { sharedData.set(key, val); } info( "Add some entries, test that they are initially only available in the parent" ); checkParentMap(expected); await checkContentMaps([]); info("Flush. Check that changes are visible in both parent and children"); sharedData.flush(); checkParentMap(expected); await checkContentMaps(expected); info( "Add another entry. Check that it is initially only available in the parent" ); let oldExpected = Array.from(expected); setKey("baz-a", { meh: "meh" }); // When we do several checks in a row, we can't check the values in // the content process, since the async checks may allow the idle // flush task to run, and update it before we're ready. checkParentMap(expected); checkContentMaps(oldExpected, true); info( "Add another entry. Check that both new entries are only available in the parent" ); setKey("baz-a", { meh: 12 }); checkParentMap(expected); checkContentMaps(oldExpected, true); info( "Delete an entry. Check that all changes are only visible in the parent" ); deleteKey("foo-b"); checkParentMap(expected); checkContentMaps(oldExpected, true); info( "Flush. Check that all entries are available in both parent and children" ); sharedData.flush(); checkParentMap(expected); await checkContentMaps(expected); info("Test that entries are automatically flushed on idle:"); info( "Add a new entry. Check that it is initially only available in the parent" ); // Test the idle flush task. oldExpected = Array.from(expected); setKey("thing", "stuff"); checkParentMap(expected); checkContentMaps(oldExpected, true); info( "Wait for an idle timeout. Check that changes are now visible in all children" ); await new Promise(resolve => ChromeUtils.idleDispatch(resolve)); checkParentMap(expected); await checkContentMaps(expected); // Test that has() rebuilds map after a flush. sharedData.set("grick", true); sharedData.flush(); equal( await contentPage.spawn(["grick"], hasKey), true, "has() should see key after flush" ); sharedData.set("grack", true); sharedData.flush(); equal( await contentPage.spawn(["gruck"], hasKey), false, "has() should return false for nonexistent key" ); }); add_task(async function test_blobs() { let { sharedData } = Services.ppmm; let text = [ "The quick brown fox jumps over the lazy dog", "Lorem ipsum dolor sit amet, consectetur adipiscing elit", "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", ]; let blobs = text.map(str => new Blob([str])); let data = { foo: { bar: "baz" } }; sharedData.set("blob0", blobs[0]); sharedData.set("blob1", blobs[1]); sharedData.set("data", data); equal( await readBlob("blob0", sharedData), text[0], "Expected text for blob0 in parent ppmm" ); sharedData.flush(); equal( await readBlob("blob0", sharedData), text[0], "Expected text for blob0 in parent ppmm" ); equal( await readBlob("blob1", sharedData), text[1], "Expected text for blob1 in parent ppmm" ); equal( await readBlob("blob0"), text[0], "Expected text for blob0 in parent cpmm" ); equal( await readBlob("blob1"), text[1], "Expected text for blob1 in parent cpmm" ); equal( await contentPage.spawn(["blob0"], readBlob), text[0], "Expected text for blob0 in child 1 cpmm" ); equal( await contentPage.spawn(["blob1"], readBlob), text[1], "Expected text for blob1 in child 1 cpmm" ); // Start a second child process Services.prefs.setIntPref(PROCESS_COUNT_PREF, 2); let page2 = await loadContentPage(); equal( await page2.spawn(["blob0"], readBlob), text[0], "Expected text for blob0 in child 2 cpmm" ); equal( await page2.spawn(["blob1"], readBlob), text[1], "Expected text for blob1 in child 2 cpmm" ); sharedData.set("blob0", blobs[2]); equal( await readBlob("blob0", sharedData), text[2], "Expected text for blob0 in parent ppmm" ); sharedData.flush(); equal( await readBlob("blob0", sharedData), text[2], "Expected text for blob0 in parent ppmm" ); equal( await readBlob("blob1", sharedData), text[1], "Expected text for blob1 in parent ppmm" ); equal( await readBlob("blob0"), text[2], "Expected text for blob0 in parent cpmm" ); equal( await readBlob("blob1"), text[1], "Expected text for blob1 in parent cpmm" ); equal( await contentPage.spawn(["blob0"], readBlob), text[2], "Expected text for blob0 in child 1 cpmm" ); equal( await contentPage.spawn(["blob1"], readBlob), text[1], "Expected text for blob1 in child 1 cpmm" ); equal( await page2.spawn(["blob0"], readBlob), text[2], "Expected text for blob0 in child 2 cpmm" ); equal( await page2.spawn(["blob1"], readBlob), text[1], "Expected text for blob1 in child 2 cpmm" ); deepEqual( await page2.spawn(["data"], getKey), data, "Expected data for data key in child 2 cpmm" ); });