/** * Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ const { AppConstants } = ChromeUtils.importESModule( "resource://gre/modules/AppConstants.sys.mjs" ); const { TelemetryTestUtils } = ChromeUtils.importESModule( "resource://testing-common/TelemetryTestUtils.sys.mjs" ); const storageDirName = "storage"; const storageFileName = "storage.sqlite"; const indexedDBDirName = "indexedDB"; const persistentStorageDirName = "storage/persistent"; const histogramName = "QM_FIRST_INITIALIZATION_ATTEMPT"; const testcases = [ { mainKey: "Storage", async setup(expectedInitResult) { if (!expectedInitResult) { // Make the database unusable by creating it as a directory (not a // file). const storageFile = getRelativeFile(storageFileName); storageFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "TemporaryStorage", async setup(expectedInitResult) { // We need to initialize storage before populating the repositories. If // we don't do that, the storage directory created for the repositories // would trigger storage upgrades (from version 0 to current version). let request = init(); await requestFinished(request); populateRepository("temporary"); populateRepository("default"); if (!expectedInitResult) { makeRepositoryUnusable("temporary"); makeRepositoryUnusable("default"); } }, initFunction: initTemporaryStorage, getExpectedSnapshots() { const expectedSnapshotsInNightly = { initFailure: { Storage: { values: [0, 1, 0], }, TemporaryRepository: { values: [1, 0], }, DefaultRepository: { values: [1, 0], }, // mainKey TemporaryStorage: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, TemporaryRepository: { values: [1, 1, 0], }, DefaultRepository: { values: [1, 1, 0], }, // mainKey TemporaryStorage: { values: [1, 1, 0], }, }, }; const expectedSnapshotsInOthers = { initFailure: { Storage: { values: [0, 1, 0], }, TemporaryRepository: { values: [1, 0], }, // mainKey TemporaryStorage: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, TemporaryRepository: { values: [1, 1, 0], }, DefaultRepository: { values: [0, 1, 0], }, // mainKey TemporaryStorage: { values: [1, 1, 0], }, }, }; return AppConstants.NIGHTLY_BUILD ? expectedSnapshotsInNightly : expectedSnapshotsInOthers; }, }, { mainKey: "DefaultRepository", async setup(expectedInitResult) { // See the comment for "TemporaryStorage". let request = init(); await requestFinished(request); populateRepository("default"); if (!expectedInitResult) { makeRepositoryUnusable("default"); } }, initFunction: initTemporaryStorage, expectedSnapshots: { initFailure: { Storage: { values: [0, 1, 0], }, TemporaryRepository: { values: [0, 1, 0], }, // mainKey DefaultRepository: { values: [1, 0], }, TemporaryStorage: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, TemporaryRepository: { values: [0, 2, 0], }, // mainKey DefaultRepository: { values: [1, 1, 0], }, TemporaryStorage: { values: [1, 1, 0], }, }, }, }, { mainKey: "TemporaryRepository", async setup(expectedInitResult) { // See the comment for "TemporaryStorage". let request = init(); await requestFinished(request); populateRepository("temporary"); if (!expectedInitResult) { makeRepositoryUnusable("temporary"); } }, initFunction: initTemporaryStorage, getExpectedSnapshots() { const expectedSnapshotsInNightly = { initFailure: { Storage: { values: [0, 1, 0], }, // mainKey TemporaryRepository: { values: [1, 0], }, DefaultRepository: { values: [0, 1, 0], }, TemporaryStorage: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, // mainKey TemporaryRepository: { values: [1, 1, 0], }, DefaultRepository: { values: [0, 2, 0], }, TemporaryStorage: { values: [1, 1, 0], }, }, }; const expectedSnapshotsInOthers = { initFailure: { Storage: { values: [0, 1, 0], }, // mainKey TemporaryRepository: { values: [1, 0], }, TemporaryStorage: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, // mainKey TemporaryRepository: { values: [1, 1, 0], }, DefaultRepository: { values: [0, 1, 0], }, TemporaryStorage: { values: [1, 1, 0], }, }, }; return AppConstants.NIGHTLY_BUILD ? expectedSnapshotsInNightly : expectedSnapshotsInOthers; }, }, { mainKey: "UpgradeStorageFrom0_0To1_0", async setup(expectedInitResult) { // storage used prior FF 49 (storage version 0.0) installPackage("version0_0_profile"); if (!expectedInitResult) { installPackage("version0_0_make_it_unusable"); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeStorageFrom0_0To1_0: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeStorageFrom0_0To1_0: { values: [1, 1, 0], }, UpgradeStorageFrom1_0To2_0: { values: [0, 1, 0], }, UpgradeStorageFrom2_0To2_1: { values: [0, 1, 0], }, UpgradeStorageFrom2_1To2_2: { values: [0, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeStorageFrom1_0To2_0", async setup(expectedInitResult) { // storage used by FF 49-54 (storage version 1.0) installPackage("version1_0_profile"); if (!expectedInitResult) { installPackage("version1_0_make_it_unusable"); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeStorageFrom1_0To2_0: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeStorageFrom1_0To2_0: { values: [1, 1, 0], }, UpgradeStorageFrom2_0To2_1: { values: [0, 1, 0], }, UpgradeStorageFrom2_1To2_2: { values: [0, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeStorageFrom2_0To2_1", async setup(expectedInitResult) { // storage used by FF 55-56 (storage version 2.0) installPackage("version2_0_profile"); if (!expectedInitResult) { installPackage("version2_0_make_it_unusable"); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeStorageFrom2_0To2_1: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeStorageFrom2_0To2_1: { values: [1, 1, 0], }, UpgradeStorageFrom2_1To2_2: { values: [0, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeStorageFrom2_1To2_2", async setup(expectedInitResult) { // storage used by FF 57-67 (storage version 2.1) installPackage("version2_1_profile"); if (!expectedInitResult) { installPackage("version2_1_make_it_unusable"); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeStorageFrom2_1To2_2: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeStorageFrom2_1To2_2: { values: [1, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeStorageFrom2_2To2_3", async setup(expectedInitResult) { // storage used by FF 68-69 (storage version 2.2) installPackage("version2_2_profile"); if (!expectedInitResult) { installPackage( "version2_2_make_it_unusable", /* allowFileOverwrites */ true ); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeStorageFrom2_2To2_3: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeStorageFrom2_2To2_3: { values: [1, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeFromIndexedDBDirectory", async setup(expectedInitResult) { const indexedDBDir = getRelativeFile(indexedDBDirName); indexedDBDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); if (!expectedInitResult) { // "indexedDB" directory will be moved under "storage" directory and at // the same time renamed to "persistent". Create a storage file to cause // the moves to fail. const storageFile = getRelativeFile(storageDirName); storageFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeFromIndexedDBDirectory: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeFromIndexedDBDirectory: { values: [1, 1, 0], }, UpgradeFromPersistentStorageDirectory: { values: [0, 1, 0], }, UpgradeStorageFrom0_0To1_0: { values: [0, 1, 0], }, UpgradeStorageFrom1_0To2_0: { values: [0, 1, 0], }, UpgradeStorageFrom2_0To2_1: { values: [0, 1, 0], }, UpgradeStorageFrom2_1To2_2: { values: [0, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "UpgradeFromPersistentStorageDirectory", async setup(expectedInitResult) { const persistentStorageDir = getRelativeFile(persistentStorageDirName); persistentStorageDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); if (!expectedInitResult) { // Create a metadata directory to break creating or upgrading directory // metadata files. const metadataDir = getRelativeFile( "storage/persistent/https+++bad.example.com/.metadata" ); metadataDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); } }, initFunction: init, expectedSnapshots: { initFailure: { // mainKey UpgradeFromPersistentStorageDirectory: { values: [1, 0], }, Storage: { values: [1, 0], }, }, initFailureThenSuccess: { // mainKey UpgradeFromPersistentStorageDirectory: { values: [1, 1, 0], }, UpgradeStorageFrom0_0To1_0: { values: [0, 1, 0], }, UpgradeStorageFrom1_0To2_0: { values: [0, 1, 0], }, UpgradeStorageFrom2_0To2_1: { values: [0, 1, 0], }, UpgradeStorageFrom2_1To2_2: { values: [0, 1, 0], }, UpgradeStorageFrom2_2To2_3: { values: [0, 1, 0], }, Storage: { values: [1, 1, 0], }, }, }, }, { mainKey: "PersistentOrigin", async setup(expectedInitResult) { // We need to initialize storage before creating the origin files. If we // don't do that, the storage directory created for the origin files // would trigger storage upgrades (from version 0 to current version). let request = init(); await requestFinished(request); if (!expectedInitResult) { const originFiles = [ getRelativeFile("storage/permanent/https+++example.com"), getRelativeFile("storage/permanent/https+++example1.com"), getRelativeFile("storage/default/https+++example2.com"), ]; for (const originFile of originFiles) { originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); } } request = initTemporaryStorage(); await requestFinished(request); }, initFunctions: [ { name: initPersistentOrigin, args: [getPrincipal("https://example.com")], }, { name: initPersistentOrigin, args: [getPrincipal("https://example1.com")], }, { name: initTemporaryOrigin, args: ["default", getPrincipal("https://example2.com")], }, ], expectedSnapshots: { initFailure: { Storage: { values: [0, 1, 0], }, TemporaryRepository: { values: [0, 1, 0], }, DefaultRepository: { values: [0, 1, 0], }, TemporaryStorage: { values: [0, 1, 0], }, // mainKey PersistentOrigin: { values: [2, 0], }, TemporaryOrigin: { values: [1, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, TemporaryRepository: { values: [0, 2, 0], }, DefaultRepository: { values: [0, 2, 0], }, TemporaryStorage: { values: [0, 2, 0], }, // mainKey PersistentOrigin: { values: [2, 2, 0], }, TemporaryOrigin: { values: [1, 1, 0], }, }, }, }, { mainKey: "TemporaryOrigin", async setup(expectedInitResult) { // See the comment for "PersistentOrigin". let request = init(); await requestFinished(request); if (!expectedInitResult) { const originFiles = [ getRelativeFile("storage/temporary/https+++example.com"), getRelativeFile("storage/default/https+++example.com"), getRelativeFile("storage/default/https+++example1.com"), getRelativeFile("storage/permanent/https+++example2.com"), ]; for (const originFile of originFiles) { originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); } } request = initTemporaryStorage(); await requestFinished(request); }, initFunctions: [ { name: initTemporaryOrigin, args: ["temporary", getPrincipal("https://example.com")], }, { name: initTemporaryOrigin, args: ["default", getPrincipal("https://example.com")], }, { name: initTemporaryOrigin, args: ["default", getPrincipal("https://example1.com")], }, { name: initPersistentOrigin, args: [getPrincipal("https://example2.com")], }, ], // Only the first result of EnsureTemporaryOriginIsInitialized per origin // should be reported. Thus, only the results for (temporary, example.com), // and (default, example1.com) should be reported. expectedSnapshots: { initFailure: { Storage: { values: [0, 1, 0], }, TemporaryRepository: { values: [0, 1, 0], }, DefaultRepository: { values: [0, 1, 0], }, TemporaryStorage: { values: [0, 1, 0], }, PersistentOrigin: { values: [1, 0], }, // mainKey TemporaryOrigin: { values: [2, 0], }, }, initFailureThenSuccess: { Storage: { values: [0, 2, 0], }, TemporaryRepository: { values: [0, 2, 0], }, DefaultRepository: { values: [0, 2, 0], }, TemporaryStorage: { values: [0, 2, 0], }, PersistentOrigin: { values: [1, 1, 0], }, // mainKey TemporaryOrigin: { values: [2, 2, 0], }, }, }, }, ]; loadScript("dom/quota/test/xpcshell/common/utils.js"); function verifyHistogram(histogram, mainKey, expectedSnapshot) { const snapshot = histogram.snapshot(); ok( mainKey in snapshot, `The histogram ${histogram.name()} must contain the main key ${mainKey}` ); const keys = Object.keys(snapshot); is( keys.length, Object.keys(expectedSnapshot).length, `The number of keys must match the expected number of keys for ` + `${histogram.name()}` ); for (const key of keys) { ok( key in expectedSnapshot, `The key ${key} must match the expected keys for ${histogram.name()}` ); const values = Object.entries(snapshot[key].values); const expectedValues = expectedSnapshot[key].values; is( values.length, expectedValues.length, `The number of values should match the expected number of values for ` + `${histogram.name()}` ); for (let [i, val] of values) { is( val, expectedValues[i], `Expected counts should match for ${histogram.name()} at index ${i}` ); } } } async function testSteps() { let request; for (const testcase of testcases) { const mainKey = testcase.mainKey; info(`Verifying ${histogramName} histogram for the main key ${mainKey}`); const histogram = TelemetryTestUtils.getAndClearKeyedHistogram(histogramName); for (const expectedInitResult of [false, true]) { info( `Verifying the histogram when the initialization ` + `${expectedInitResult ? "failed and then succeeds" : "fails"}` ); await testcase.setup(expectedInitResult); const msg = `Should ${expectedInitResult ? "not " : ""} have thrown`; // Call the initialization function twice, so we can verify below that // only the first initialization attempt has been reported. for (let i = 0; i < 2; ++i) { let initFunctions; if (testcase.initFunctions) { initFunctions = testcase.initFunctions; } else { initFunctions = [ { name: testcase.initFunction, args: [], }, ]; } for (const initFunction of initFunctions) { request = initFunction.name(...initFunction.args); try { await requestFinished(request); ok(expectedInitResult, msg); } catch (ex) { ok(!expectedInitResult, msg); } } } const expectedSnapshots = testcase.getExpectedSnapshots ? testcase.getExpectedSnapshots() : testcase.expectedSnapshots; const expectedSnapshot = expectedInitResult ? expectedSnapshots.initFailureThenSuccess : expectedSnapshots.initFailure; verifyHistogram(histogram, mainKey, expectedSnapshot); // The first initialization attempt has been reported in the histogram // and any new attemps wouldn't be reported if we didn't reset or clear // the storage here. We need a clean profile for the next iteration // anyway. // However, the clear storage operation needs initialized storage, so // clearing can fail if the storage is unusable and it can also increase // some of the telemetry counters. Instead of calling clear, we can just // call reset and clear profile manually. request = reset(); await requestFinished(request); const indexedDBDir = getRelativeFile(indexedDBDirName); if (indexedDBDir.exists()) { indexedDBDir.remove(false); } const storageDir = getRelativeFile(storageDirName); if (storageDir.exists()) { storageDir.remove(true); } const storageFile = getRelativeFile(storageFileName); if (storageFile.exists()) { // It could be a non empty directory, so remove it recursively. storageFile.remove(true); } } } }