michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ michael@0: */ michael@0: /* This testcase triggers two telemetry pings. michael@0: * michael@0: * Telemetry code keeps histograms of past telemetry pings. The first michael@0: * ping populates these histograms. One of those histograms is then michael@0: * checked in the second request. michael@0: */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: Cu.import("resource://testing-common/httpd.js", this); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/LightweightThemeManager.jsm", this); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: Cu.import("resource://gre/modules/TelemetryPing.jsm", this); michael@0: Cu.import("resource://gre/modules/TelemetryFile.jsm", this); michael@0: Cu.import("resource://gre/modules/Task.jsm", this); michael@0: Cu.import("resource://gre/modules/Promise.jsm", this); michael@0: michael@0: const IGNORE_HISTOGRAM = "test::ignore_me"; michael@0: const IGNORE_HISTOGRAM_TO_CLONE = "MEMORY_HEAP_ALLOCATED"; michael@0: const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also"; michael@0: const ADDON_NAME = "Telemetry test addon"; michael@0: const ADDON_HISTOGRAM = "addon-histogram"; michael@0: // Add some unicode characters here to ensure that sending them works correctly. michael@0: const FLASH_VERSION = "\u201c1.1.1.1\u201d"; michael@0: const SHUTDOWN_TIME = 10000; michael@0: const FAILED_PROFILE_LOCK_ATTEMPTS = 2; michael@0: michael@0: // Constants from prio.h for nsIFileOutputStream.init michael@0: const PR_WRONLY = 0x2; michael@0: const PR_CREATE_FILE = 0x8; michael@0: const PR_TRUNCATE = 0x20; michael@0: const RW_OWNER = 0600; michael@0: michael@0: const NUMBER_OF_THREADS_TO_LAUNCH = 30; michael@0: let gNumberOfThreadsLaunched = 0; michael@0: michael@0: const Telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry); michael@0: michael@0: let gHttpServer = new HttpServer(); michael@0: let gServerStarted = false; michael@0: let gRequestIterator = null; michael@0: michael@0: function sendPing () { michael@0: TelemetryPing.gatherStartup(); michael@0: if (gServerStarted) { michael@0: return TelemetryPing.testPing("http://localhost:" + gHttpServer.identity.primaryPort); michael@0: } else { michael@0: return TelemetryPing.testPing("http://doesnotexist"); michael@0: } michael@0: } michael@0: michael@0: function wrapWithExceptionHandler(f) { michael@0: function wrapper(...args) { michael@0: try { michael@0: f(...args); michael@0: } catch (ex if typeof(ex) == 'object') { michael@0: dump("Caught exception: " + ex.message + "\n"); michael@0: dump(ex.stack); michael@0: do_test_finished(); michael@0: } michael@0: } michael@0: return wrapper; michael@0: } michael@0: michael@0: function registerPingHandler(handler) { michael@0: gHttpServer.registerPrefixHandler("/submit/telemetry/", michael@0: wrapWithExceptionHandler(handler)); michael@0: } michael@0: michael@0: function setupTestData() { michael@0: Telemetry.newHistogram(IGNORE_HISTOGRAM, "never", 1, 2, 3, Telemetry.HISTOGRAM_BOOLEAN); michael@0: Telemetry.histogramFrom(IGNORE_CLONED_HISTOGRAM, IGNORE_HISTOGRAM_TO_CLONE); michael@0: Services.startup.interrupted = true; michael@0: Telemetry.registerAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM, 1, 5, 6, michael@0: Telemetry.HISTOGRAM_LINEAR); michael@0: h1 = Telemetry.getAddonHistogram(ADDON_NAME, ADDON_HISTOGRAM); michael@0: h1.add(1); michael@0: } michael@0: michael@0: function getSavedHistogramsFile(basename) { michael@0: let tmpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: let histogramsFile = tmpDir.clone(); michael@0: histogramsFile.append(basename); michael@0: if (histogramsFile.exists()) { michael@0: histogramsFile.remove(true); michael@0: } michael@0: do_register_cleanup(function () { michael@0: try { michael@0: histogramsFile.remove(true); michael@0: } catch (e) { michael@0: } michael@0: }); michael@0: return histogramsFile; michael@0: } michael@0: michael@0: function decodeRequestPayload(request) { michael@0: let s = request.bodyInputStream; michael@0: let payload = null; michael@0: let decoder = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON) michael@0: michael@0: if (request.getHeader("content-encoding") == "gzip") { michael@0: let observer = { michael@0: buffer: "", michael@0: onStreamComplete: function(loader, context, status, length, result) { michael@0: this.buffer = String.fromCharCode.apply(this, result); michael@0: } michael@0: }; michael@0: michael@0: let scs = Cc["@mozilla.org/streamConverters;1"] michael@0: .getService(Ci.nsIStreamConverterService); michael@0: let listener = Cc["@mozilla.org/network/stream-loader;1"] michael@0: .createInstance(Ci.nsIStreamLoader); michael@0: listener.init(observer); michael@0: let converter = scs.asyncConvertData("gzip", "uncompressed", michael@0: listener, null); michael@0: converter.onStartRequest(null, null); michael@0: converter.onDataAvailable(null, null, s, 0, s.available()); michael@0: converter.onStopRequest(null, null, null); michael@0: let unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: unicodeConverter.charset = "UTF-8"; michael@0: let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer); michael@0: utf8string += unicodeConverter.Finish(); michael@0: payload = decoder.decode(utf8string); michael@0: } else { michael@0: payload = decoder.decodeFromStream(s, s.available()); michael@0: } michael@0: michael@0: return payload; michael@0: } michael@0: michael@0: function checkPayloadInfo(payload, reason) { michael@0: // get rid of the non-deterministic field michael@0: const expected_info = { michael@0: OS: "XPCShell", michael@0: appID: "xpcshell@tests.mozilla.org", michael@0: appVersion: "1", michael@0: appName: "XPCShell", michael@0: appBuildID: "2007010101", michael@0: platformBuildID: "2007010101", michael@0: flashVersion: FLASH_VERSION michael@0: }; michael@0: michael@0: for (let f in expected_info) { michael@0: do_check_eq(payload.info[f], expected_info[f]); michael@0: } michael@0: michael@0: do_check_eq(payload.info.reason, reason); michael@0: do_check_true("appUpdateChannel" in payload.info); michael@0: do_check_true("locale" in payload.info); michael@0: do_check_true("revision" in payload.info); michael@0: do_check_true(payload.info.revision.startsWith("http")); michael@0: michael@0: try { michael@0: // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing michael@0: // this test. michael@0: let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); michael@0: let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); michael@0: let isOSX = ("nsILocalFileMac" in Components.interfaces); michael@0: michael@0: if (isWindows || isOSX) { michael@0: do_check_true("adapterVendorID" in payload.info); michael@0: do_check_true("adapterDeviceID" in payload.info); michael@0: } michael@0: } michael@0: catch (x) { michael@0: } michael@0: } michael@0: michael@0: function checkPayload(request, reason, successfulPings) { michael@0: let payload = decodeRequestPayload(request); michael@0: // Take off ["","submit","telemetry"]. michael@0: let pathComponents = request.path.split("/").slice(3); michael@0: michael@0: checkPayloadInfo(payload, reason); michael@0: do_check_eq(reason, pathComponents[1]); michael@0: do_check_eq(request.getHeader("content-type"), "application/json; charset=UTF-8"); michael@0: do_check_true(payload.simpleMeasurements.uptime >= 0); michael@0: do_check_true(payload.simpleMeasurements.startupInterrupted === 1); michael@0: do_check_eq(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME); michael@0: do_check_eq(payload.simpleMeasurements.savedPings, 1); michael@0: do_check_true("maximalNumberOfConcurrentThreads" in payload.simpleMeasurements); michael@0: do_check_true(payload.simpleMeasurements.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); michael@0: michael@0: do_check_eq(payload.simpleMeasurements.failedProfileLockCount, michael@0: FAILED_PROFILE_LOCK_ATTEMPTS); michael@0: let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: let failedProfileLocksFile = profileDirectory.clone(); michael@0: failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt"); michael@0: do_check_true(!failedProfileLocksFile.exists()); michael@0: michael@0: michael@0: let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes); michael@0: if (isWindows) { michael@0: do_check_true(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0); michael@0: do_check_true(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0); michael@0: } michael@0: michael@0: const TELEMETRY_PING = "TELEMETRY_PING"; michael@0: const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS"; michael@0: const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG"; michael@0: const READ_SAVED_PING_SUCCESS = "READ_SAVED_PING_SUCCESS"; michael@0: do_check_true(TELEMETRY_PING in payload.histograms); michael@0: do_check_true(READ_SAVED_PING_SUCCESS in payload.histograms); michael@0: let rh = Telemetry.registeredHistograms([]); michael@0: for (let name of rh) { michael@0: if (/SQLITE/.test(name) && name in payload.histograms) { michael@0: do_check_true(("STARTUP_" + name) in payload.histograms); michael@0: } michael@0: } michael@0: do_check_false(IGNORE_HISTOGRAM in payload.histograms); michael@0: do_check_false(IGNORE_CLONED_HISTOGRAM in payload.histograms); michael@0: michael@0: // Flag histograms should automagically spring to life. michael@0: const expected_flag = { michael@0: range: [1, 2], michael@0: bucket_count: 3, michael@0: histogram_type: 3, michael@0: values: {0:1, 1:0}, michael@0: sum: 0, michael@0: sum_squares_lo: 0, michael@0: sum_squares_hi: 0 michael@0: }; michael@0: let flag = payload.histograms[TELEMETRY_TEST_FLAG]; michael@0: do_check_eq(uneval(flag), uneval(expected_flag)); michael@0: michael@0: // There should be one successful report from the previous telemetry ping. michael@0: const expected_tc = { michael@0: range: [1, 2], michael@0: bucket_count: 3, michael@0: histogram_type: 2, michael@0: values: {0:1, 1:successfulPings, 2:0}, michael@0: sum: successfulPings, michael@0: sum_squares_lo: successfulPings, michael@0: sum_squares_hi: 0 michael@0: }; michael@0: let tc = payload.histograms[TELEMETRY_SUCCESS]; michael@0: do_check_eq(uneval(tc), uneval(expected_tc)); michael@0: michael@0: let h = payload.histograms[READ_SAVED_PING_SUCCESS]; michael@0: do_check_eq(h.values[0], 1); michael@0: michael@0: // The ping should include data from memory reporters. We can't check that michael@0: // this data is correct, because we can't control the values returned by the michael@0: // memory reporters. But we can at least check that the data is there. michael@0: // michael@0: // It's important to check for the presence of reporters with a mix of units, michael@0: // because TelemetryPing has separate logic for each one. But we can't michael@0: // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because michael@0: // Telemetry doesn't touch a memory reporter with these units that's michael@0: // available on all platforms. michael@0: michael@0: do_check_true('MEMORY_JS_GC_HEAP' in payload.histograms); // UNITS_BYTES michael@0: do_check_true('MEMORY_JS_COMPARTMENTS_SYSTEM' in payload.histograms); // UNITS_COUNT michael@0: michael@0: // We should have included addon histograms. michael@0: do_check_true("addonHistograms" in payload); michael@0: do_check_true(ADDON_NAME in payload.addonHistograms); michael@0: do_check_true(ADDON_HISTOGRAM in payload.addonHistograms[ADDON_NAME]); michael@0: michael@0: do_check_true(("mainThread" in payload.slowSQL) && michael@0: ("otherThreads" in payload.slowSQL)); michael@0: } michael@0: michael@0: function dummyTheme(id) { michael@0: return { michael@0: id: id, michael@0: name: Math.random().toString(), michael@0: headerURL: "http://lwttest.invalid/a.png", michael@0: footerURL: "http://lwttest.invalid/b.png", michael@0: textcolor: Math.random().toString(), michael@0: accentcolor: Math.random().toString() michael@0: }; michael@0: } michael@0: michael@0: // A fake plugin host for testing flash version telemetry michael@0: let PluginHost = { michael@0: getPluginTags: function(countRef) { michael@0: let plugins = [{name: "Shockwave Flash", version: FLASH_VERSION}]; michael@0: countRef.value = plugins.length; michael@0: return plugins; michael@0: }, michael@0: michael@0: QueryInterface: function(iid) { michael@0: if (iid.equals(Ci.nsIPluginHost) michael@0: || iid.equals(Ci.nsISupports)) michael@0: return this; michael@0: michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: } michael@0: } michael@0: michael@0: let PluginHostFactory = { michael@0: createInstance: function (outer, iid) { michael@0: if (outer != null) michael@0: throw Components.results.NS_ERROR_NO_AGGREGATION; michael@0: return PluginHost.QueryInterface(iid); michael@0: } michael@0: }; michael@0: michael@0: const PLUGINHOST_CONTRACTID = "@mozilla.org/plugin/host;1"; michael@0: const PLUGINHOST_CID = Components.ID("{2329e6ea-1f15-4cbe-9ded-6e98e842de0e}"); michael@0: michael@0: function registerFakePluginHost() { michael@0: let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); michael@0: registrar.registerFactory(PLUGINHOST_CID, "Fake Plugin Host", michael@0: PLUGINHOST_CONTRACTID, PluginHostFactory); michael@0: } michael@0: michael@0: function writeStringToFile(file, contents) { michael@0: let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"] michael@0: .createInstance(Ci.nsIFileOutputStream); michael@0: ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, michael@0: RW_OWNER, ostream.DEFER_OPEN); michael@0: ostream.write(contents, contents.length); michael@0: ostream.QueryInterface(Ci.nsISafeOutputStream).finish(); michael@0: ostream.close(); michael@0: } michael@0: michael@0: function write_fake_shutdown_file() { michael@0: let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: let file = profileDirectory.clone(); michael@0: file.append("Telemetry.ShutdownTime.txt"); michael@0: let contents = "" + SHUTDOWN_TIME; michael@0: writeStringToFile(file, contents); michael@0: } michael@0: michael@0: function write_fake_failedprofilelocks_file() { michael@0: let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: let file = profileDirectory.clone(); michael@0: file.append("Telemetry.FailedProfileLocks.txt"); michael@0: let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS; michael@0: writeStringToFile(file, contents); michael@0: } michael@0: michael@0: function run_test() { michael@0: do_test_pending(); michael@0: try { michael@0: let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfoDebug); michael@0: gfxInfo.spoofVendorID("0xabcd"); michael@0: gfxInfo.spoofDeviceID("0x1234"); michael@0: } catch (x) { michael@0: // If we can't test gfxInfo, that's fine, we'll note it later. michael@0: } michael@0: michael@0: // Addon manager needs a profile directory michael@0: do_get_profile(); michael@0: createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2"); michael@0: michael@0: // Make it look like we've previously failed to lock a profile a couple times. michael@0: write_fake_failedprofilelocks_file(); michael@0: michael@0: // Make it look like we've shutdown before. michael@0: write_fake_shutdown_file(); michael@0: michael@0: let currentMaxNumberOfThreads = Telemetry.maximalNumberOfConcurrentThreads; michael@0: do_check_true(currentMaxNumberOfThreads > 0); michael@0: michael@0: // Try to augment the maximal number of threads currently launched michael@0: let threads = []; michael@0: try { michael@0: for (let i = 0; i < currentMaxNumberOfThreads + 10; ++i) { michael@0: threads.push(Services.tm.newThread(0)); michael@0: } michael@0: } catch (ex) { michael@0: // If memory is too low, it is possible that not all threads will be launched. michael@0: } michael@0: gNumberOfThreadsLaunched = threads.length; michael@0: michael@0: do_check_true(Telemetry.maximalNumberOfConcurrentThreads >= gNumberOfThreadsLaunched); michael@0: michael@0: do_register_cleanup(function() { michael@0: threads.forEach(function(thread) { michael@0: thread.shutdown(); michael@0: }); michael@0: }); michael@0: michael@0: Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(actualTest)); michael@0: } michael@0: michael@0: function actualTest() { michael@0: // try to make LightweightThemeManager do stuff michael@0: let gInternalManager = Cc["@mozilla.org/addons/integration;1"] michael@0: .getService(Ci.nsIObserver) michael@0: .QueryInterface(Ci.nsITimerCallback); michael@0: michael@0: gInternalManager.observe(null, "addons-startup", null); michael@0: LightweightThemeManager.currentTheme = dummyTheme("1234"); michael@0: michael@0: // fake plugin host for consistent flash version data michael@0: registerFakePluginHost(); michael@0: michael@0: run_next_test(); michael@0: } michael@0: michael@0: // Ensure that not overwriting an existing file fails silently michael@0: add_task(function* test_overwritePing() { michael@0: let ping = {slug: "foo"} michael@0: yield TelemetryFile.savePing(ping, true); michael@0: yield TelemetryFile.savePing(ping, false); michael@0: yield TelemetryFile.cleanupPingFile(ping); michael@0: }); michael@0: michael@0: // Ensures that expired histograms are not part of the payload. michael@0: add_task(function* test_expiredHistogram() { michael@0: let histogram_id = "FOOBAR"; michael@0: let dummy = Telemetry.newHistogram(histogram_id, "30", 1, 2, 3, Telemetry.HISTOGRAM_EXPONENTIAL); michael@0: michael@0: dummy.add(1); michael@0: michael@0: do_check_eq(TelemetryPing.getPayload()["histograms"][histogram_id], undefined); michael@0: do_check_eq(TelemetryPing.getPayload()["histograms"]["TELEMETRY_TEST_EXPIRED"], undefined); michael@0: }); michael@0: michael@0: // Checks that an invalid histogram file is deleted if TelemetryFile fails to parse it. michael@0: add_task(function* test_runInvalidJSON() { michael@0: let histogramsFile = getSavedHistogramsFile("invalid-histograms.dat"); michael@0: michael@0: writeStringToFile(histogramsFile, "this.is.invalid.JSON"); michael@0: do_check_true(histogramsFile.exists()); michael@0: michael@0: yield TelemetryPing.testLoadHistograms(histogramsFile); michael@0: do_check_false(histogramsFile.exists()); michael@0: }); michael@0: michael@0: // Sends a ping to a non existing server. michael@0: add_task(function* test_noServerPing() { michael@0: yield sendPing(); michael@0: }); michael@0: michael@0: // Checks that a sent ping is correctly received by a dummy http server. michael@0: add_task(function* test_simplePing() { michael@0: gHttpServer.start(-1); michael@0: gServerStarted = true; michael@0: gRequestIterator = Iterator(new Request()); michael@0: michael@0: yield sendPing(); michael@0: decodeRequestPayload(yield gRequestIterator.next()); michael@0: }); michael@0: michael@0: // Saves the current session histograms, reloads them, perfoms a ping michael@0: // and checks that the dummy http server received both the previously michael@0: // saved histograms and the new ones. michael@0: add_task(function* test_saveLoadPing() { michael@0: let histogramsFile = getSavedHistogramsFile("saved-histograms.dat"); michael@0: michael@0: setupTestData(); michael@0: yield TelemetryPing.testSaveHistograms(histogramsFile); michael@0: yield TelemetryPing.testLoadHistograms(histogramsFile); michael@0: yield sendPing(); michael@0: checkPayload((yield gRequestIterator.next()), "test-ping", 1); michael@0: checkPayload((yield gRequestIterator.next()), "saved-session", 1); michael@0: }); michael@0: michael@0: // Checks that an expired histogram file is deleted when loaded. michael@0: add_task(function* test_runOldPingFile() { michael@0: let histogramsFile = getSavedHistogramsFile("old-histograms.dat"); michael@0: michael@0: yield TelemetryPing.testSaveHistograms(histogramsFile); michael@0: do_check_true(histogramsFile.exists()); michael@0: let mtime = histogramsFile.lastModifiedTime; michael@0: histogramsFile.lastModifiedTime = mtime - (14 * 24 * 60 * 60 * 1000 + 60000); // 14 days, 1m michael@0: michael@0: yield TelemetryPing.testLoadHistograms(histogramsFile); michael@0: do_check_false(histogramsFile.exists()); michael@0: }); michael@0: michael@0: add_task(function* stopServer(){ michael@0: gHttpServer.stop(do_test_finished); michael@0: }); michael@0: michael@0: // An iterable sequence of http requests michael@0: function Request() { michael@0: let defers = []; michael@0: let current = 0; michael@0: michael@0: function RequestIterator() {} michael@0: michael@0: // Returns a promise that resolves to the next http request michael@0: RequestIterator.prototype.next = function() { michael@0: let deferred = defers[current++]; michael@0: return deferred.promise; michael@0: } michael@0: michael@0: this.__iterator__ = function(){ michael@0: return new RequestIterator(); michael@0: } michael@0: michael@0: registerPingHandler((request, response) => { michael@0: let deferred = defers[defers.length - 1]; michael@0: defers.push(Promise.defer()); michael@0: deferred.resolve(request); michael@0: }); michael@0: michael@0: defers.push(Promise.defer()); michael@0: }