summaryrefslogtreecommitdiffstats
path: root/browser/modules/test/unit/test_ProfileCounter.js
blob: 2a22d849fffbbd2cd226ab85784c704c8081ef8b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/* 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);
});