diff options
Diffstat (limited to 'browser/modules/test/unit/test_ProfileCounter.js')
-rw-r--r-- | browser/modules/test/unit/test_ProfileCounter.js | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/browser/modules/test/unit/test_ProfileCounter.js b/browser/modules/test/unit/test_ProfileCounter.js new file mode 100644 index 0000000000..e8b3179f34 --- /dev/null +++ b/browser/modules/test/unit/test_ProfileCounter.js @@ -0,0 +1,239 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const { BrowserUsageTelemetry } = ChromeUtils.import( + "resource:///modules/BrowserUsageTelemetry.jsm" +); +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const PROFILE_COUNT_SCALAR = "browser.engagement.profile_count"; +// Largest possible uint32_t value represents an error. +const SCALAR_ERROR_VALUE = 0; + +const FILE_OPEN_OPERATION = "open"; +const ERROR_FILE_NOT_FOUND = "NotFoundError"; +const ERROR_ACCESS_DENIED = "NotAllowedError"; + +// We will redirect I/O to/from the profile counter file to read/write this +// variable instead. That makes it easier for us to: +// - avoid interference from any pre-existing file +// - read and change the values in the file. +// - clean up changes made to the file +// We will translate a null value stored here to a File Not Found error. +var gFakeProfileCounterFile = null; +// We will use this to check that the profile counter code doesn't try to write +// to multiple files (since this test will malfunction in that case due to +// gFakeProfileCounterFile only being setup to accommodate a single file). +var gProfileCounterFilePath = null; + +// Storing a value here lets us test the behavior when we encounter an error +// reading or writing to the file. A null value means that no error will +// be simulated (other than possibly a NotFoundError). +var gNextReadExceptionReason = null; +var gNextWriteExceptionReason = null; + +// Nothing will actually be stored in this directory, so it's not important that +// it be valid, but the leafname should be unique to this test in order to be +// sure of preventing name conflicts with the pref: +// `browser.engagement.profileCounted.${hash}` +function getDummyUpdateDirectory() { + const testName = "test_ProfileCounter"; + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(`C:\\foo\\bar\\${testName}`); + return dir; +} + +// We aren't going to bother generating anything looking like a real client ID +// for this. The only real requirements for client ids is that they not repeat +// and that they be strings. So we'll just return an integer as a string and +// increment it when we want a new client id. +var gDummyTelemetryClientId = 0; +function getDummyTelemetryClientId() { + return gDummyTelemetryClientId.toString(); +} +function setNewDummyTelemetryClientId() { + ++gDummyTelemetryClientId; +} + +// Returns null if the (fake) profile count file hasn't been created yet. +function getProfileCount() { + // Strict equality to ensure distinguish properly between a non-existent + // file and an empty one. + if (gFakeProfileCounterFile === null) { + return null; + } + let saveData = JSON.parse(gFakeProfileCounterFile); + return saveData.profileTelemetryIds.length; +} + +// Resets the state to the original state, before the profile count file has +// even been written. +// If resetFile is specified as false, this will reset everything except for the +// file itself. This allows us to sort of pretend that another installation +// wrote the file. +function reset(resetFile = true) { + if (resetFile) { + gFakeProfileCounterFile = null; + } + gNextReadExceptionReason = null; + gNextWriteExceptionReason = null; + setNewDummyTelemetryClientId(); +} + +function setup() { + reset(); + // FOG needs a profile directory to put its data in. + do_get_profile(); + // Initialize FOG so we can test the FOG version of profile count + Services.fog.initializeFOG(); + Services.fog.testResetFOG(); + + BrowserUsageTelemetry.Policy.readProfileCountFile = async path => { + if (!gProfileCounterFilePath) { + gProfileCounterFilePath = path; + } else { + // We've only got one mock-file variable. Make sure we are always + // accessing the same file or this will cause problems. + Assert.equal( + gProfileCounterFilePath, + path, + "Only one file should be accessed" + ); + } + // Strict equality to ensure distinguish properly between null and 0. + if (gNextReadExceptionReason !== null) { + let ex = new DOMException(FILE_OPEN_OPERATION, gNextReadExceptionReason); + gNextReadExceptionReason = null; + throw ex; + } + // Strict equality to ensure distinguish properly between a non-existent + // file and an empty one. + if (gFakeProfileCounterFile === null) { + throw new DOMException(FILE_OPEN_OPERATION, ERROR_FILE_NOT_FOUND); + } + return gFakeProfileCounterFile; + }; + BrowserUsageTelemetry.Policy.writeProfileCountFile = async (path, data) => { + if (!gProfileCounterFilePath) { + gProfileCounterFilePath = path; + } else { + // We've only got one mock-file variable. Make sure we are always + // accessing the same file or this will cause problems. + Assert.equal( + gProfileCounterFilePath, + path, + "Only one file should be accessed" + ); + } + // Strict equality to ensure distinguish properly between null and 0. + if (gNextWriteExceptionReason !== null) { + let ex = new DOMException(FILE_OPEN_OPERATION, gNextWriteExceptionReason); + gNextWriteExceptionReason = null; + throw ex; + } + gFakeProfileCounterFile = data; + }; + BrowserUsageTelemetry.Policy.getUpdateDirectory = getDummyUpdateDirectory; + BrowserUsageTelemetry.Policy.getTelemetryClientId = getDummyTelemetryClientId; +} + +// Checks that the number of profiles reported is the number expected. Because +// of bucketing, the raw count may be different than the reported count. +function checkSuccess(profilesReported, rawCount = profilesReported) { + Assert.equal(rawCount, getProfileCount()); + const scalars = TelemetryTestUtils.getProcessScalars("parent"); + TelemetryTestUtils.assertScalar( + scalars, + PROFILE_COUNT_SCALAR, + profilesReported, + "The value reported to telemetry should be the expected profile count" + ); + Assert.equal( + profilesReported, + Glean.browserEngagement.profileCount.testGetValue() + ); +} + +function checkError() { + const scalars = TelemetryTestUtils.getProcessScalars("parent"); + TelemetryTestUtils.assertScalar( + scalars, + PROFILE_COUNT_SCALAR, + SCALAR_ERROR_VALUE, + "The value reported to telemetry should be the error value" + ); +} + +add_task(async function testProfileCounter() { + setup(); + + info("Testing basic functionality, single install"); + await BrowserUsageTelemetry.reportProfileCount(); + checkSuccess(1); + await BrowserUsageTelemetry.reportProfileCount(); + checkSuccess(1); + + // Fake another installation by resetting everything except for the profile + // count file. + reset(false); + + info("Testing basic functionality, faking a second install"); + await BrowserUsageTelemetry.reportProfileCount(); + checkSuccess(2); + + // Check if we properly handle the case where we cannot read from the file + // and we have already set its contents. This should report an error. + info("Testing read error after successful write"); + gNextReadExceptionReason = ERROR_ACCESS_DENIED; + await BrowserUsageTelemetry.reportProfileCount(); + checkError(); + + reset(); + + // A read error should cause an error to be reported, but should also write + // to the file in an attempt to fix it. So the next (successful) read should + // result in the correct telemetry. + info("Testing read error self-correction"); + gNextReadExceptionReason = ERROR_ACCESS_DENIED; + await BrowserUsageTelemetry.reportProfileCount(); + checkError(); + + await BrowserUsageTelemetry.reportProfileCount(); + checkSuccess(1); + + reset(); + + // If the file is malformed. We should report an error and fix it, then report + // the correct profile count next time. + info("Testing with malformed profile count file"); + gFakeProfileCounterFile = "<malformed file data>"; + await BrowserUsageTelemetry.reportProfileCount(); + checkError(); + + await BrowserUsageTelemetry.reportProfileCount(); + checkSuccess(1); + + reset(); + + // If we haven't yet written to the file, a write error should cause an error + // to be reported. + info("Testing write error before the first write"); + gNextWriteExceptionReason = ERROR_ACCESS_DENIED; + await BrowserUsageTelemetry.reportProfileCount(); + checkError(); + + reset(); + + info("Testing bucketing"); + // Fake 15 installations to drive the raw profile count up to 15. + for (let i = 0; i < 15; i++) { + reset(false); + await BrowserUsageTelemetry.reportProfileCount(); + } + // With bucketing, values from 10-99 should all be reported as 10. + checkSuccess(10, 15); +}); |