Fri, 16 Jan 2015 18:13:44 +0100
Integrate suggestion from review to improve consistency with existing code.
1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/
3 */
4 /* This testcase triggers two telemetry pings.
5 *
6 * Telemetry code keeps histograms of past telemetry pings. The first
7 * ping populates these histograms. One of those histograms is then
8 * checked in the second request.
9 */
11 const Cc = Components.classes;
12 const Ci = Components.interfaces;
13 const Cu = Components.utils;
14 const Cr = Components.results;
16 Cu.import("resource://testing-common/httpd.js", this);
17 Cu.import("resource://gre/modules/Services.jsm");
18 Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this);
19 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
20 Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
21 Cu.import("resource://gre/modules/TelemetryFile.jsm", this);
22 Cu.import("resource://gre/modules/Task.jsm", this);
23 Cu.import("resource://gre/modules/Promise.jsm", this);
25 const IGNORE_HISTOGRAM = "test::ignore_me";
26 const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED";
27 const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
28 const ADDON_NAME = "Telemetry test addon";
29 const ADDON_HISTOGRAM = "addon-histogram";
30 // Add some unicode characters here to ensure that sending them works correctly.
31 const FLASH_VERSION = "\u201c1.1.1.1\u201d";
32 const SHUTDOWN_TIME = 10000;
33 const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
35 // Constants from prio.h for nsIFileOutputStream.init
36 const PR_WRONLY = 0x2;
37 const PR_CREATE_FILE = 0x8;
38 const PR_TRUNCATE = 0x20;
39 const RW_OWNER = 0600;
41 const NUMBER_OF_THREADS_TO_LAUNCH = 30;
42 let gNumberOfThreadsLaunched = 0;
44 const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
46 let gHttpServer = new HttpServer();
47 let gServerStarted = false;
48 let gRequestIterator = null;
50 function sendPing () {
51 TelemetryPing.gatherStartup();
52 if (gServerStarted) {
53 return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort);
54 } else {
55 return TelemetryPing.testPing("http://doesnotexist");
56 }
57 }
59 function wrapWithExceptionHandler(f) {
60 function wrapper(...args) {
61 try {
62 f(...args);
63 } catch (ex if typeof(ex) == 'object') {
64 dump("Caught exception: " + ex.message + "\n");
65 dump(ex.stack);
66 do_test_finished();
67 }
68 }
69 return wrapper;
70 }
72 function registerPingHandler(handler) {
73 gHttpServer.registerPrefixHandler("/submit/telemetry/",
74 wrapWithExceptionHandler(handler));
75 }
77 function setupTestData() {
78 Telemetry.newHistogram(IGNORE_HISTOGRAM, "never", 1, 2, 3, Telemetry.HISTOGRAM_BOOLEAN);
79 Telemetry.histogramFrom(IGNORE_CLONED_HISTOGRAM, IGNORE_HISTOGRAM_TO_CLONE);
80 Services.startup.interrupted = true;
81 Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM, 1, 5, 6,
82 Telemetry.HISTOGRAM_LINEAR);
83 h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM);
84 h1.add(1);
85 }
87 function getSavedHistogramsFile(basename) {
88 let tmpDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
89 let histogramsFile = tmpDir.clone();
90 histogramsFile.append(basename);
91 if (histogramsFile.exists()) {
92 histogramsFile.remove(true);
93 }
94 do_register_cleanup(function () {
95 try {
96 histogramsFile.remove(true);
97 } catch (e) {
98 }
99 });
100 return histogramsFile;
101 }
103 function decodeRequestPayload(request) {
104 let s = request.bodyInputStream;
105 let payload = null;
106 let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON)
108 if (request.getHeader("content-encoding") == "gzip") {
109 let observer = {
110 buffer: "",
111 onStreamComplete: function(loader, context, status, length, result) {
112 this.buffer = String.fromCharCode.apply(this, result);
113 }
114 };
116 let scs = Cc["@mozilla.org/streamConverters;1"]
117 .getService(Ci.nsIStreamConverterService);
118 let listener = Cc["@mozilla.org/network/stream-loader;1"]
119 .createInstance(Ci.nsIStreamLoader);
120 listener.init(observer);
121 let converter = scs.asyncConvertData("gzip", "uncompressed",
122 listener, null);
123 converter.onStartRequest(null, null);
124 converter.onDataAvailable(null, null, s, 0, s.available());
125 converter.onStopRequest(null, null, null);
126 let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
127 .createInstance(Ci.nsIScriptableUnicodeConverter);
128 unicodeConverter.charset = "UTF-8";
129 let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer);
130 utf8string += unicodeConverter.Finish();
131 payload = decoder.decode(utf8string);
132 } else {
133 payload = decoder.decodeFromStream(s, s.available());
134 }
136 return payload;
137 }
139 function checkPayloadInfo(payload, reason) {
140 // get rid of the non-deterministic field
141 const expected_info = {
142 OS: "XPCShell",
143 appID: "xpcshell@tests.mozilla.org",
144 appVersion: "1",
145 appName: "XPCShell",
146 appBuildID: "2007010101",
147 platformBuildID: "2007010101",
148 flashVersion: FLASH_VERSION
149 };
151 for (let f in expected_info) {
152 do_check_eq(payload.info[f], expected_info[f]);
153 }
155 do_check_eq(payload.info.reason, reason);
156 do_check_true("appUpdateChannel" in payload.info);
157 do_check_true("locale" in payload.info);
158 do_check_true("revision" in payload.info);
159 do_check_true(payload.info.revision.startsWith("http"));
161 try {
162 // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
163 // this test.
164 let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
165 let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
166 let isOSX = ("nsILocalFileMac" in Components.interfaces);
168 if (isWindows || isOSX) {
169 do_check_true("adapterVendorID" in payload.info);
170 do_check_true("adapterDeviceID" in payload.info);
171 }
172 }
173 catch (x) {
174 }
175 }
177 function checkPayload(request, reason, successfulPings) {
178 let payload = decodeRequestPayload(request);
179 // Take off ["","submit","telemetry"].
180 let pathComponents = request.path.split("/").slice(3);
182 checkPayloadInfo(payload, reason);
183 do_check_eq(reason, pathComponents[1]);
184 do_check_eq(request.getHeader("content-type"), "application/json; charset=UTF-8");
185 do_check_true(payload.simpleMeasurements.uptime >= 0);
186 do_check_true(payload.simpleMeasurements.startupInterrupted === 1);
187 do_check_eq(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
188 do_check_eq(payload.simpleMeasurements.savedPings, 1);
189 do_check_true("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements);
190 do_check_true(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
192 do_check_eq(payload.simpleMeasurements.failedProfileLockCount,
193 FAILED_PROFILE_LOCK_ATTEMPTS);
194 let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
195 let failedProfileLocksFile = profileDirectory.clone();
196 failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
197 do_check_true(!failedProfileLocksFile.exists());
200 let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
201 if (isWindows) {
202 do_check_true(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
203 do_check_true(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
204 }
206 const TELEMETRY_PING = "TELEMETRY_PING";
207 const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
208 const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
209 const READ_SAVED_PING_SUCCESS = "READ_SAVED_PING_SUCCESS";
210 do_check_true(TELEMETRY_PING in payload.histograms);
211 do_check_true(READ_SAVED_PING_SUCCESS in payload.histograms);
212 let rh = Telemetry.registeredHistograms([]);
213 for (let name of rh) {
214 if (/SQLITE/.test(name) && name in payload.histograms) {
215 do_check_true(("STARTUP_" + name) in payload.histograms);
216 }
217 }
218 do_check_false(IGNORE_HISTOGRAM in payload.histograms);
219 do_check_false(IGNORE_CLONED_HISTOGRAM in payload.histograms);
221 // Flag histograms should automagically spring to life.
222 const expected_flag = {
223 range: [1, 2],
224 bucket_count: 3,
225 histogram_type: 3,
226 values: {0:1, 1:0},
227 sum: 0,
228 sum_squares_lo: 0,
229 sum_squares_hi: 0
230 };
231 let flag = payload.histograms[TELEMETRY_TEST_FLAG];
232 do_check_eq(uneval(flag), uneval(expected_flag));
234 // There should be one successful report from the previous telemetry ping.
235 const expected_tc = {
236 range: [1, 2],
237 bucket_count: 3,
238 histogram_type: 2,
239 values: {0:1, 1:successfulPings, 2:0},
240 sum: successfulPings,
241 sum_squares_lo: successfulPings,
242 sum_squares_hi: 0
243 };
244 let tc = payload.histograms[TELEMETRY_SUCCESS];
245 do_check_eq(uneval(tc), uneval(expected_tc));
247 let h = payload.histograms[READ_SAVED_PING_SUCCESS];
248 do_check_eq(h.values[0], 1);
250 // The ping should include data from memory reporters. We can't check that
251 // this data is correct, because we can't control the values returned by the
252 // memory reporters. But we can at least check that the data is there.
253 //
254 // It's important to check for the presence of reporters with a mix of units,
255 // because TelemetryPing has separate logic for each one. But we can't
256 // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
257 // Telemetry doesn't touch a memory reporter with these units that's
258 // available on all platforms.
260 do_check_true('MEMORY_JS_GC_HEAP' in payload.histograms); // UNITS_BYTES
261 do_check_true('MEMORY_JS_COMPARTMENTS_SYSTEM' in payload.histograms); // UNITS_COUNT
263 // We should have included addon histograms.
264 do_check_true("addonHistograms" in payload);
265 do_check_true(ADDON_NAME in payload.addonHistograms);
266 do_check_true(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]);
268 do_check_true(("mainThread" in payload.slowSQL) &&
269 ("otherThreads" in payload.slowSQL));
270 }
272 function dummyTheme(id) {
273 return {
274 id: id,
275 name: Math.random().toString(),
276 headerURL: "http://lwttest.invalid/a.png",
277 footerURL: "http://lwttest.invalid/b.png",
278 textcolor: Math.random().toString(),
279 accentcolor: Math.random().toString()
280 };
281 }
283 // A fake plugin host for testing flash version telemetry
284 let PluginHost = {
285 getPluginTags: function(countRef) {
286 let plugins = [{name: "Shockwave Flash", version: FLASH_VERSION}];
287 countRef.value = plugins.length;
288 return plugins;
289 },
291 QueryInterface: function(iid) {
292 if (iid.equals(Ci.nsIPluginHost)
293 || iid.equals(Ci.nsISupports))
294 return this;
296 throw Components.results.NS_ERROR_NO_INTERFACE;
297 }
298 }
300 let PluginHostFactory = {
301 createInstance: function (outer, iid) {
302 if (outer != null)
303 throw Components.results.NS_ERROR_NO_AGGREGATION;
304 return PluginHost.QueryInterface(iid);
305 }
306 };
308 const PLUGINHOST_CONTRACTID = "@mozilla.org/plugin/host;1";
309 const PLUGINHOST_CID = Components.ID("{2329e6ea-1f15-4cbe-9ded-6e98e842de0e}");
311 function registerFakePluginHost() {
312 let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
313 registrar.registerFactory(PLUGINHOST_CID, "Fake Plugin Host",
314 PLUGINHOST_CONTRACTID, PluginHostFactory);
315 }
317 function writeStringToFile(file, contents) {
318 let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
319 .createInstance(Ci.nsIFileOutputStream);
320 ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
321 RW_OWNER, ostream.DEFER_OPEN);
322 ostream.write(contents, contents.length);
323 ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
324 ostream.close();
325 }
327 function write_fake_shutdown_file() {
328 let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
329 let file = profileDirectory.clone();
330 file.append("Telemetry.ShutdownTime.txt");
331 let contents = "" + SHUTDOWN_TIME;
332 writeStringToFile(file, contents);
333 }
335 function write_fake_failedprofilelocks_file() {
336 let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
337 let file = profileDirectory.clone();
338 file.append("Telemetry.FailedProfileLocks.txt");
339 let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
340 writeStringToFile(file, contents);
341 }
343 function run_test() {
344 do_test_pending();
345 try {
346 let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug);
347 gfxInfo.spoofVendorID("0xabcd");
348 gfxInfo.spoofDeviceID("0x1234");
349 } catch (x) {
350 // If we can't test gfxInfo, that's fine, we'll note it later.
351 }
353 // Addon manager needs a profile directory
354 do_get_profile();
355 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
357 // Make it look like we've previously failed to lock a profile a couple times.
358 write_fake_failedprofilelocks_file();
360 // Make it look like we've shutdown before.
361 write_fake_shutdown_file();
363 let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads;
364 do_check_true(currentMaxNumberOfThreads > 0);
366 // Try to augment the maximal number of threads currently launched
367 let threads = [];
368 try {
369 for (let i = 0; i < currentMaxNumberOfThreads + 10; ++i) {
370 threads.push(Services.tm.newThread(0));
371 }
372 } catch (ex) {
373 // If memory is too low, it is possible that not all threads will be launched.
374 }
375 gNumberOfThreadsLaunched = threads.length;
377 do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched);
379 do_register_cleanup(function() {
380 threads.forEach(function(thread) {
381 thread.shutdown();
382 });
383 });
385 Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(actualTest));
386 }
388 function actualTest() {
389 // try to make LightweightThemeManager do stuff
390 let gInternalManager = Cc["@mozilla.org/addons/integration;1"]
391 .getService(Ci.nsIObserver)
392 .QueryInterface(Ci.nsITimerCallback);
394 gInternalManager.observe(null, "addons-startup", null);
395 LightweightThemeManager.currentTheme = dummyTheme("1234");
397 // fake plugin host for consistent flash version data
398 registerFakePluginHost();
400 run_next_test();
401 }
403 // Ensure that not overwriting an existing file fails silently
404 add_task(function* test_overwritePing() {
405 let ping = {slug: "foo"}
406 yield TelemetryFile.savePing(ping, true);
407 yield TelemetryFile.savePing(ping, false);
408 yield TelemetryFile.cleanupPingFile(ping);
409 });
411 // Ensures that expired histograms are not part of the payload.
412 add_task(function* test_expiredHistogram() {
413 let histogram_id = "FOOBAR";
414 let dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL);
416 dummy.add(1);
418 do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined);
419 do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined);
420 });
422 // Checks that an invalid histogram file is deleted if TelemetryFile fails to parse it.
423 add_task(function* test_runInvalidJSON() {
424 let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat");
426 writeStringToFile(histogramsFile, "this.is.invalid.JSON");
427 do_check_true(histogramsFile.exists());
429 yield TelemetryPing.testLoadHistograms(histogramsFile);
430 do_check_false(histogramsFile.exists());
431 });
433 // Sends a ping to a non existing server.
434 add_task(function* test_noServerPing() {
435 yield sendPing();
436 });
438 // Checks that a sent ping is correctly received by a dummy http server.
439 add_task(function* test_simplePing() {
440 gHttpServer.start(-1);
441 gServerStarted = true;
442 gRequestIterator = Iterator(new Request());
444 yield sendPing();
445 decodeRequestPayload(yield gRequestIterator.next());
446 });
448 // Saves the current session histograms, reloads them, perfoms a ping
449 // and checks that the dummy http server received both the previously
450 // saved histograms and the new ones.
451 add_task(function* test_saveLoadPing() {
452 let histogramsFile = getSavedHistogramsFile("saved-histograms.dat");
454 setupTestData();
455 yield TelemetryPing.testSaveHistograms(histogramsFile);
456 yield TelemetryPing.testLoadHistograms(histogramsFile);
457 yield sendPing();
458 checkPayload((yield gRequestIterator.next()), "test-ping", 1);
459 checkPayload((yield gRequestIterator.next()), "saved-session", 1);
460 });
462 // Checks that an expired histogram file is deleted when loaded.
463 add_task(function* test_runOldPingFile() {
464 let histogramsFile = getSavedHistogramsFile("old-histograms.dat");
466 yield TelemetryPing.testSaveHistograms(histogramsFile);
467 do_check_true(histogramsFile.exists());
468 let mtime = histogramsFile.lastModifiedTime;
469 histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m
471 yield TelemetryPing.testLoadHistograms(histogramsFile);
472 do_check_false(histogramsFile.exists());
473 });
475 add_task(function* stopServer(){
476 gHttpServer.stop(do_test_finished);
477 });
479 // An iterable sequence of http requests
480 function Request() {
481 let defers = [];
482 let current = 0;
484 function RequestIterator() {}
486 // Returns a promise that resolves to the next http request
487 RequestIterator.prototype.next = function() {
488 let deferred = defers[current++];
489 return deferred.promise;
490 }
492 this.__iterator__ = function(){
493 return new RequestIterator();
494 }
496 registerPingHandler((request, response) => {
497 let deferred = defers[defers.length - 1];
498 defers.push(Promise.defer());
499 deferred.resolve(request);
500 });
502 defers.push(Promise.defer());
503 }