toolkit/components/telemetry/tests/unit/test_TelemetryPing.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 }

mercurial