summaryrefslogtreecommitdiffstats
path: root/browser/modules/test/unit/test_ProfileCounter.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/test/unit/test_ProfileCounter.js')
-rw-r--r--browser/modules/test/unit/test_ProfileCounter.js230
1 files changed, 230 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..d2febe77a9
--- /dev/null
+++ b/browser/modules/test/unit/test_ProfileCounter.js
@@ -0,0 +1,230 @@
+/* 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();
+
+ 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"
+ );
+}
+
+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);
+});