Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'android'
- Manifest: browser/modules/test/unit/xpcshell.toml
/* Any copyright is dedicated to the Public Domain.
*/
"use strict";
const { BrowserUsageTelemetry } = ChromeUtils.importESModule(
"resource:///modules/BrowserUsageTelemetry.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
);
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);
});