239 lines
8.4 KiB
JavaScript
239 lines
8.4 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/
|
|
*/
|
|
"use strict";
|
|
|
|
const { BrowserUsageTelemetry } = ChromeUtils.importESModule(
|
|
"resource:///modules/BrowserUsageTelemetry.sys.mjs"
|
|
);
|
|
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);
|
|
});
|