test_ProfileCounter.js (8570B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ 3 */ 4 "use strict"; 5 6 const { BrowserUsageTelemetry } = ChromeUtils.importESModule( 7 "resource:///modules/BrowserUsageTelemetry.sys.mjs" 8 ); 9 const { TelemetryTestUtils } = ChromeUtils.importESModule( 10 "resource://testing-common/TelemetryTestUtils.sys.mjs" 11 ); 12 13 const PROFILE_COUNT_SCALAR = "browser.engagement.profile_count"; 14 // Largest possible uint32_t value represents an error. 15 const SCALAR_ERROR_VALUE = 0; 16 17 const FILE_OPEN_OPERATION = "open"; 18 const ERROR_FILE_NOT_FOUND = "NotFoundError"; 19 const ERROR_ACCESS_DENIED = "NotAllowedError"; 20 21 // We will redirect I/O to/from the profile counter file to read/write this 22 // variable instead. That makes it easier for us to: 23 // - avoid interference from any pre-existing file 24 // - read and change the values in the file. 25 // - clean up changes made to the file 26 // We will translate a null value stored here to a File Not Found error. 27 var gFakeProfileCounterFile = null; 28 // We will use this to check that the profile counter code doesn't try to write 29 // to multiple files (since this test will malfunction in that case due to 30 // gFakeProfileCounterFile only being setup to accommodate a single file). 31 var gProfileCounterFilePath = null; 32 33 // Storing a value here lets us test the behavior when we encounter an error 34 // reading or writing to the file. A null value means that no error will 35 // be simulated (other than possibly a NotFoundError). 36 var gNextReadExceptionReason = null; 37 var gNextWriteExceptionReason = null; 38 39 // Nothing will actually be stored in this directory, so it's not important that 40 // it be valid, but the leafname should be unique to this test in order to be 41 // sure of preventing name conflicts with the pref: 42 // `browser.engagement.profileCounted.${hash}` 43 function getDummyUpdateDirectory() { 44 const testName = "test_ProfileCounter"; 45 let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 46 dir.initWithPath(`C:\\foo\\bar\\${testName}`); 47 return dir; 48 } 49 50 // We aren't going to bother generating anything looking like a real client ID 51 // for this. The only real requirements for client ids is that they not repeat 52 // and that they be strings. So we'll just return an integer as a string and 53 // increment it when we want a new client id. 54 var gDummyTelemetryClientId = 0; 55 function getDummyTelemetryClientId() { 56 return gDummyTelemetryClientId.toString(); 57 } 58 function setNewDummyTelemetryClientId() { 59 ++gDummyTelemetryClientId; 60 } 61 62 // Returns null if the (fake) profile count file hasn't been created yet. 63 function getProfileCount() { 64 // Strict equality to ensure distinguish properly between a non-existent 65 // file and an empty one. 66 if (gFakeProfileCounterFile === null) { 67 return null; 68 } 69 let saveData = JSON.parse(gFakeProfileCounterFile); 70 return saveData.profileTelemetryIds.length; 71 } 72 73 // Resets the state to the original state, before the profile count file has 74 // even been written. 75 // If resetFile is specified as false, this will reset everything except for the 76 // file itself. This allows us to sort of pretend that another installation 77 // wrote the file. 78 function reset(resetFile = true) { 79 if (resetFile) { 80 gFakeProfileCounterFile = null; 81 } 82 gNextReadExceptionReason = null; 83 gNextWriteExceptionReason = null; 84 setNewDummyTelemetryClientId(); 85 } 86 87 function setup() { 88 reset(); 89 // FOG needs a profile directory to put its data in. 90 do_get_profile(); 91 // Initialize FOG so we can test the FOG version of profile count 92 Services.fog.initializeFOG(); 93 Services.fog.testResetFOG(); 94 95 BrowserUsageTelemetry.Policy.readProfileCountFile = async path => { 96 if (!gProfileCounterFilePath) { 97 gProfileCounterFilePath = path; 98 } else { 99 // We've only got one mock-file variable. Make sure we are always 100 // accessing the same file or this will cause problems. 101 Assert.equal( 102 gProfileCounterFilePath, 103 path, 104 "Only one file should be accessed" 105 ); 106 } 107 // Strict equality to ensure distinguish properly between null and 0. 108 if (gNextReadExceptionReason !== null) { 109 let ex = new DOMException(FILE_OPEN_OPERATION, gNextReadExceptionReason); 110 gNextReadExceptionReason = null; 111 throw ex; 112 } 113 // Strict equality to ensure distinguish properly between a non-existent 114 // file and an empty one. 115 if (gFakeProfileCounterFile === null) { 116 throw new DOMException(FILE_OPEN_OPERATION, ERROR_FILE_NOT_FOUND); 117 } 118 return gFakeProfileCounterFile; 119 }; 120 BrowserUsageTelemetry.Policy.writeProfileCountFile = async (path, data) => { 121 if (!gProfileCounterFilePath) { 122 gProfileCounterFilePath = path; 123 } else { 124 // We've only got one mock-file variable. Make sure we are always 125 // accessing the same file or this will cause problems. 126 Assert.equal( 127 gProfileCounterFilePath, 128 path, 129 "Only one file should be accessed" 130 ); 131 } 132 // Strict equality to ensure distinguish properly between null and 0. 133 if (gNextWriteExceptionReason !== null) { 134 let ex = new DOMException(FILE_OPEN_OPERATION, gNextWriteExceptionReason); 135 gNextWriteExceptionReason = null; 136 throw ex; 137 } 138 gFakeProfileCounterFile = data; 139 }; 140 BrowserUsageTelemetry.Policy.getUpdateDirectory = getDummyUpdateDirectory; 141 BrowserUsageTelemetry.Policy.getTelemetryClientId = getDummyTelemetryClientId; 142 } 143 144 // Checks that the number of profiles reported is the number expected. Because 145 // of bucketing, the raw count may be different than the reported count. 146 function checkSuccess(profilesReported, rawCount = profilesReported) { 147 Assert.equal(rawCount, getProfileCount()); 148 const scalars = TelemetryTestUtils.getProcessScalars("parent"); 149 TelemetryTestUtils.assertScalar( 150 scalars, 151 PROFILE_COUNT_SCALAR, 152 profilesReported, 153 "The value reported to telemetry should be the expected profile count" 154 ); 155 Assert.equal( 156 profilesReported, 157 Glean.browserEngagement.profileCount.testGetValue() 158 ); 159 } 160 161 function checkError() { 162 const scalars = TelemetryTestUtils.getProcessScalars("parent"); 163 TelemetryTestUtils.assertScalar( 164 scalars, 165 PROFILE_COUNT_SCALAR, 166 SCALAR_ERROR_VALUE, 167 "The value reported to telemetry should be the error value" 168 ); 169 } 170 171 add_task(async function testProfileCounter() { 172 setup(); 173 174 info("Testing basic functionality, single install"); 175 await BrowserUsageTelemetry.reportProfileCount(); 176 checkSuccess(1); 177 await BrowserUsageTelemetry.reportProfileCount(); 178 checkSuccess(1); 179 180 // Fake another installation by resetting everything except for the profile 181 // count file. 182 reset(false); 183 184 info("Testing basic functionality, faking a second install"); 185 await BrowserUsageTelemetry.reportProfileCount(); 186 checkSuccess(2); 187 188 // Check if we properly handle the case where we cannot read from the file 189 // and we have already set its contents. This should report an error. 190 info("Testing read error after successful write"); 191 gNextReadExceptionReason = ERROR_ACCESS_DENIED; 192 await BrowserUsageTelemetry.reportProfileCount(); 193 checkError(); 194 195 reset(); 196 197 // A read error should cause an error to be reported, but should also write 198 // to the file in an attempt to fix it. So the next (successful) read should 199 // result in the correct telemetry. 200 info("Testing read error self-correction"); 201 gNextReadExceptionReason = ERROR_ACCESS_DENIED; 202 await BrowserUsageTelemetry.reportProfileCount(); 203 checkError(); 204 205 await BrowserUsageTelemetry.reportProfileCount(); 206 checkSuccess(1); 207 208 reset(); 209 210 // If the file is malformed. We should report an error and fix it, then report 211 // the correct profile count next time. 212 info("Testing with malformed profile count file"); 213 gFakeProfileCounterFile = "<malformed file data>"; 214 await BrowserUsageTelemetry.reportProfileCount(); 215 checkError(); 216 217 await BrowserUsageTelemetry.reportProfileCount(); 218 checkSuccess(1); 219 220 reset(); 221 222 // If we haven't yet written to the file, a write error should cause an error 223 // to be reported. 224 info("Testing write error before the first write"); 225 gNextWriteExceptionReason = ERROR_ACCESS_DENIED; 226 await BrowserUsageTelemetry.reportProfileCount(); 227 checkError(); 228 229 reset(); 230 231 info("Testing bucketing"); 232 // Fake 15 installations to drive the raw profile count up to 15. 233 for (let i = 0; i < 15; i++) { 234 reset(false); 235 await BrowserUsageTelemetry.reportProfileCount(); 236 } 237 // With bucketing, values from 10-99 should all be reported as 10. 238 checkSuccess(10, 15); 239 });