/* 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 = ""; 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); });