harness.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "experimental"
  7. };
  8. const { Cc, Ci, Cu } = require("chrome");
  9. const { Loader } = require('./loader');
  10. const { serializeStack, parseStack } = require("toolkit/loader");
  11. const { setTimeout } = require('../timers');
  12. const { PlainTextConsole } = require("../console/plain-text");
  13. const { when: unload } = require("../system/unload");
  14. const { format, fromException } = require("../console/traceback");
  15. const system = require("../system");
  16. const memory = require('../deprecated/memory');
  17. const { gc: gcPromise } = require('./memory');
  18. const { defer } = require('../core/promise');
  19. // Trick manifest builder to make it think we need these modules ?
  20. const unit = require("../deprecated/unit-test");
  21. const test = require("../../test");
  22. const url = require("../url");
  23. function emptyPromise() {
  24. let { promise, resolve } = defer();
  25. resolve();
  26. return promise;
  27. }
  28. var cService = Cc['@mozilla.org/consoleservice;1'].getService()
  29. .QueryInterface(Ci.nsIConsoleService);
  30. // The console used to log messages
  31. var testConsole;
  32. // Cuddlefish loader in which we load and execute tests.
  33. var loader;
  34. // Function to call when we're done running tests.
  35. var onDone;
  36. // Function to print text to a console, w/o CR at the end.
  37. var print;
  38. // How many more times to run all tests.
  39. var iterationsLeft;
  40. // Whether to report memory profiling information.
  41. var profileMemory;
  42. // Whether we should stop as soon as a test reports a failure.
  43. var stopOnError;
  44. // Function to call to retrieve a list of tests to execute
  45. var findAndRunTests;
  46. // Combined information from all test runs.
  47. var results = {
  48. passed: 0,
  49. failed: 0,
  50. testRuns: []
  51. };
  52. // A list of the compartments and windows loaded after startup
  53. var startLeaks;
  54. // JSON serialization of last memory usage stats; we keep it stringified
  55. // so we don't actually change the memory usage stats (in terms of objects)
  56. // of the JSRuntime we're profiling.
  57. var lastMemoryUsage;
  58. function analyzeRawProfilingData(data) {
  59. var graph = data.graph;
  60. var shapes = {};
  61. // Convert keys in the graph from strings to ints.
  62. // TODO: Can we get rid of this ridiculousness?
  63. var newGraph = {};
  64. for (id in graph) {
  65. newGraph[parseInt(id)] = graph[id];
  66. }
  67. graph = newGraph;
  68. var modules = 0;
  69. var moduleIds = [];
  70. var moduleObjs = {UNKNOWN: 0};
  71. for (let name in data.namedObjects) {
  72. moduleObjs[name] = 0;
  73. moduleIds[data.namedObjects[name]] = name;
  74. modules++;
  75. }
  76. var count = 0;
  77. for (id in graph) {
  78. var parent = graph[id].parent;
  79. while (parent) {
  80. if (parent in moduleIds) {
  81. var name = moduleIds[parent];
  82. moduleObjs[name]++;
  83. break;
  84. }
  85. if (!(parent in graph)) {
  86. moduleObjs.UNKNOWN++;
  87. break;
  88. }
  89. parent = graph[parent].parent;
  90. }
  91. count++;
  92. }
  93. print("\nobject count is " + count + " in " + modules + " modules" +
  94. " (" + data.totalObjectCount + " across entire JS runtime)\n");
  95. if (lastMemoryUsage) {
  96. var last = JSON.parse(lastMemoryUsage);
  97. var diff = {
  98. moduleObjs: dictDiff(last.moduleObjs, moduleObjs),
  99. totalObjectClasses: dictDiff(last.totalObjectClasses,
  100. data.totalObjectClasses)
  101. };
  102. for (let name in diff.moduleObjs)
  103. print(" " + diff.moduleObjs[name] + " in " + name + "\n");
  104. for (let name in diff.totalObjectClasses)
  105. print(" " + diff.totalObjectClasses[name] + " instances of " +
  106. name + "\n");
  107. }
  108. lastMemoryUsage = JSON.stringify(
  109. {moduleObjs: moduleObjs,
  110. totalObjectClasses: data.totalObjectClasses}
  111. );
  112. }
  113. function dictDiff(last, curr) {
  114. var diff = {};
  115. for (let name in last) {
  116. var result = (curr[name] || 0) - last[name];
  117. if (result)
  118. diff[name] = (result > 0 ? "+" : "") + result;
  119. }
  120. for (let name in curr) {
  121. var result = curr[name] - (last[name] || 0);
  122. if (result)
  123. diff[name] = (result > 0 ? "+" : "") + result;
  124. }
  125. return diff;
  126. }
  127. function reportMemoryUsage() {
  128. if (!profileMemory) {
  129. return emptyPromise();
  130. }
  131. return gcPromise().then((function () {
  132. var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
  133. .getService(Ci.nsIMemoryReporterManager);
  134. let count = 0;
  135. function logReporter(process, path, kind, units, amount, description) {
  136. print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
  137. }
  138. mgr.getReportsForThisProcess(logReporter, null);
  139. var weakrefs = [info.weakref.get()
  140. for each (info in memory.getObjects())];
  141. weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
  142. print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n");
  143. }));
  144. }
  145. var gWeakrefInfo;
  146. function checkMemory() {
  147. return gcPromise().then(_ => {
  148. let leaks = getPotentialLeaks();
  149. let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
  150. return !(url in startLeaks.compartments);
  151. });
  152. let windowURLs = Object.keys(leaks.windows).filter(function(url) {
  153. return !(url in startLeaks.windows);
  154. });
  155. for (let url of compartmentURLs)
  156. console.warn("LEAKED", leaks.compartments[url]);
  157. for (let url of windowURLs)
  158. console.warn("LEAKED", leaks.windows[url]);
  159. }).then(showResults);
  160. }
  161. function showResults() {
  162. let { promise, resolve } = defer();
  163. if (gWeakrefInfo) {
  164. gWeakrefInfo.forEach(
  165. function(info) {
  166. var ref = info.weakref.get();
  167. if (ref !== null) {
  168. var data = ref.__url__ ? ref.__url__ : ref;
  169. var warning = data == "[object Object]"
  170. ? "[object " + data.constructor.name + "(" +
  171. [p for (p in data)].join(", ") + ")]"
  172. : data;
  173. console.warn("LEAK", warning, info.bin);
  174. }
  175. }
  176. );
  177. }
  178. onDone(results);
  179. resolve();
  180. return promise;
  181. }
  182. function cleanup() {
  183. let coverObject = {};
  184. try {
  185. for (let name in loader.modules)
  186. memory.track(loader.modules[name],
  187. "module global scope: " + name);
  188. memory.track(loader, "Cuddlefish Loader");
  189. if (profileMemory) {
  190. gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin }
  191. for each (info in memory.getObjects())];
  192. }
  193. loader.unload();
  194. if (loader.globals.console.errorsLogged && !results.failed) {
  195. results.failed++;
  196. console.error("warnings and/or errors were logged.");
  197. }
  198. if (consoleListener.errorsLogged && !results.failed) {
  199. console.warn(consoleListener.errorsLogged + " " +
  200. "warnings or errors were logged to the " +
  201. "platform's nsIConsoleService, which could " +
  202. "be of no consequence; however, they could also " +
  203. "be indicative of aberrant behavior.");
  204. }
  205. // read the code coverage object, if it exists, from CoverJS-moz
  206. if (typeof loader.globals.global == "object") {
  207. coverObject = loader.globals.global['__$coverObject'] || {};
  208. }
  209. consoleListener.errorsLogged = 0;
  210. loader = null;
  211. memory.gc();
  212. }
  213. catch (e) {
  214. results.failed++;
  215. console.error("unload.send() threw an exception.");
  216. console.exception(e);
  217. };
  218. setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1);
  219. // dump the coverobject
  220. if (Object.keys(coverObject).length){
  221. const self = require('sdk/self');
  222. const {pathFor} = require("sdk/system");
  223. let file = require('sdk/io/file');
  224. const {env} = require('sdk/system/environment');
  225. console.log("CWD:", env.PWD);
  226. let out = file.join(env.PWD,'coverstats-'+self.id+'.json');
  227. console.log('coverstats:', out);
  228. let outfh = file.open(out,'w');
  229. outfh.write(JSON.stringify(coverObject,null,2));
  230. outfh.flush();
  231. outfh.close();
  232. }
  233. }
  234. function getPotentialLeaks() {
  235. memory.gc();
  236. // Things we can assume are part of the platform and so aren't leaks
  237. let WHITELIST_BASE_URLS = [
  238. "chrome://",
  239. "resource:///",
  240. "resource://app/",
  241. "resource://gre/",
  242. "resource://gre-resources/",
  243. "resource://pdf.js/",
  244. "resource://pdf.js.components/",
  245. "resource://services-common/",
  246. "resource://services-crypto/",
  247. "resource://services-sync/"
  248. ];
  249. let ioService = Cc["@mozilla.org/network/io-service;1"].
  250. getService(Ci.nsIIOService);
  251. let uri = ioService.newURI("chrome://global/content/", "UTF-8", null);
  252. let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
  253. getService(Ci.nsIChromeRegistry);
  254. uri = chromeReg.convertChromeURL(uri);
  255. let spec = uri.spec;
  256. let pos = spec.indexOf("!/");
  257. WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2));
  258. let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)");
  259. let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/");
  260. let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$");
  261. let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active");
  262. let windowDetails = new RegExp("^(.*), id=.*$");
  263. function isPossibleLeak(item) {
  264. if (!item.location)
  265. return false;
  266. for (let whitelist of WHITELIST_BASE_URLS) {
  267. if (item.location.substring(0, whitelist.length) == whitelist)
  268. return false;
  269. }
  270. return true;
  271. }
  272. let compartments = {};
  273. let windows = {};
  274. function logReporter(process, path, kind, units, amount, description) {
  275. let matches;
  276. if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) {
  277. if (matches[1] in compartments)
  278. return;
  279. let details = compartmentDetails.exec(matches[1]);
  280. if (!details) {
  281. console.error("Unable to parse compartment detail " + matches[1]);
  282. return;
  283. }
  284. let item = {
  285. path: matches[1],
  286. principal: details[1],
  287. location: details[2] ? details[2].replace("\\", "/", "g") : undefined,
  288. source: details[3] ? details[3].split(" -> ").reverse() : undefined,
  289. toString: function() this.location
  290. };
  291. if (!isPossibleLeak(item))
  292. return;
  293. compartments[matches[1]] = item;
  294. return;
  295. }
  296. if (matches = windowRegexp.exec(path)) {
  297. if (matches[1] in windows)
  298. return;
  299. let details = windowDetails.exec(matches[1]);
  300. if (!details) {
  301. console.error("Unable to parse window detail " + matches[1]);
  302. return;
  303. }
  304. let item = {
  305. path: matches[1],
  306. location: details[1].replace("\\", "/", "g"),
  307. source: [details[1].replace("\\", "/", "g")],
  308. toString: function() this.location
  309. };
  310. if (!isPossibleLeak(item))
  311. return;
  312. windows[matches[1]] = item;
  313. }
  314. }
  315. Cc["@mozilla.org/memory-reporter-manager;1"]
  316. .getService(Ci.nsIMemoryReporterManager)
  317. .getReportsForThisProcess(logReporter, null);
  318. return { compartments: compartments, windows: windows };
  319. }
  320. function nextIteration(tests) {
  321. if (tests) {
  322. results.passed += tests.passed;
  323. results.failed += tests.failed;
  324. reportMemoryUsage().then(_ => {
  325. let testRun = [];
  326. for each (let test in tests.testRunSummary) {
  327. let testCopy = {};
  328. for (let info in test) {
  329. testCopy[info] = test[info];
  330. }
  331. testRun.push(testCopy);
  332. }
  333. results.testRuns.push(testRun);
  334. iterationsLeft--;
  335. checkForEnd();
  336. })
  337. }
  338. else {
  339. checkForEnd();
  340. }
  341. }
  342. function checkForEnd() {
  343. if (iterationsLeft && (!stopOnError || results.failed == 0)) {
  344. // Pass the loader which has a hooked console that doesn't dispatch
  345. // errors to the JS console and avoid firing false alarm in our
  346. // console listener
  347. findAndRunTests(loader, nextIteration);
  348. }
  349. else {
  350. setTimeout(cleanup, 0);
  351. }
  352. }
  353. var POINTLESS_ERRORS = [
  354. 'Invalid chrome URI:',
  355. 'OpenGL LayerManager Initialized Succesfully.',
  356. '[JavaScript Error: "TelemetryStopwatch:',
  357. 'reference to undefined property',
  358. '[JavaScript Error: "The character encoding of the HTML document was ' +
  359. 'not declared.',
  360. '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' +
  361. 'native weak map key',
  362. '[JavaScript Warning: "Duplicate resource declaration for',
  363. 'file: "chrome://browser/content/',
  364. 'file: "chrome://global/content/',
  365. '[JavaScript Warning: "The character encoding of a framed document was ' +
  366. 'not declared.'
  367. ];
  368. var consoleListener = {
  369. errorsLogged: 0,
  370. observe: function(object) {
  371. if (!(object instanceof Ci.nsIScriptError))
  372. return;
  373. this.errorsLogged++;
  374. var message = object.QueryInterface(Ci.nsIConsoleMessage).message;
  375. var pointless = [err for each (err in POINTLESS_ERRORS)
  376. if (message.indexOf(err) >= 0)];
  377. if (pointless.length == 0 && message)
  378. testConsole.log(message);
  379. }
  380. };
  381. function TestRunnerConsole(base, options) {
  382. this.__proto__ = {
  383. errorsLogged: 0,
  384. warn: function warn() {
  385. this.errorsLogged++;
  386. base.warn.apply(base, arguments);
  387. },
  388. error: function error() {
  389. this.errorsLogged++;
  390. base.error.apply(base, arguments);
  391. },
  392. info: function info(first) {
  393. if (options.verbose)
  394. base.info.apply(base, arguments);
  395. else
  396. if (first == "pass:")
  397. print(".");
  398. },
  399. __proto__: base
  400. };
  401. }
  402. function stringify(arg) {
  403. try {
  404. return String(arg);
  405. }
  406. catch(ex) {
  407. return "<toString() error>";
  408. }
  409. }
  410. function stringifyArgs(args) {
  411. return Array.map(args, stringify).join(" ");
  412. }
  413. function TestRunnerTinderboxConsole(base, options) {
  414. this.base = base;
  415. this.print = options.print;
  416. this.verbose = options.verbose;
  417. this.errorsLogged = 0;
  418. // Binding all the public methods to an instance so that they can be used
  419. // as callback / listener functions straightaway.
  420. this.log = this.log.bind(this);
  421. this.info = this.info.bind(this);
  422. this.warn = this.warn.bind(this);
  423. this.error = this.error.bind(this);
  424. this.debug = this.debug.bind(this);
  425. this.exception = this.exception.bind(this);
  426. this.trace = this.trace.bind(this);
  427. };
  428. TestRunnerTinderboxConsole.prototype = {
  429. testMessage: function testMessage(pass, expected, test, message) {
  430. let type = "TEST-";
  431. if (expected) {
  432. if (pass)
  433. type += "PASS";
  434. else
  435. type += "KNOWN-FAIL";
  436. }
  437. else {
  438. this.errorsLogged++;
  439. if (pass)
  440. type += "UNEXPECTED-PASS";
  441. else
  442. type += "UNEXPECTED-FAIL";
  443. }
  444. this.print(type + " | " + test + " | " + message + "\n");
  445. if (!expected)
  446. this.trace();
  447. },
  448. log: function log() {
  449. this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
  450. },
  451. info: function info(first) {
  452. this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
  453. },
  454. warn: function warn() {
  455. this.errorsLogged++;
  456. this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
  457. },
  458. error: function error() {
  459. this.errorsLogged++;
  460. this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n");
  461. this.base.error.apply(this.base, arguments);
  462. },
  463. debug: function debug() {
  464. this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n");
  465. },
  466. exception: function exception(e) {
  467. this.print("An exception occurred.\n" +
  468. require("../console/traceback").format(e) + "\n" + e + "\n");
  469. },
  470. trace: function trace() {
  471. var traceback = require("../console/traceback");
  472. var stack = traceback.get();
  473. stack.splice(-1, 1);
  474. this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n");
  475. }
  476. };
  477. var runTests = exports.runTests = function runTests(options) {
  478. iterationsLeft = options.iterations;
  479. profileMemory = options.profileMemory;
  480. stopOnError = options.stopOnError;
  481. onDone = options.onDone;
  482. print = options.print;
  483. findAndRunTests = options.findAndRunTests;
  484. try {
  485. cService.registerListener(consoleListener);
  486. print("Running tests on " + system.name + " " + system.version +
  487. "/Gecko " + system.platformVersion + " (" +
  488. system.id + ") under " +
  489. system.platform + "/" + system.architecture + ".\n");
  490. if (options.parseable)
  491. testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options);
  492. else
  493. testConsole = new TestRunnerConsole(new PlainTextConsole(), options);
  494. loader = Loader(module, {
  495. console: testConsole,
  496. global: {} // useful for storing things like coverage testing.
  497. });
  498. // Load these before getting initial leak stats as they will still be in
  499. // memory when we check later
  500. require("../deprecated/unit-test");
  501. require("../deprecated/unit-test-finder");
  502. startLeaks = getPotentialLeaks();
  503. nextIteration();
  504. } catch (e) {
  505. let frames = fromException(e).reverse().reduce(function(frames, frame) {
  506. if (frame.fileName.split("/").pop() === "unit-test-finder.js")
  507. frames.done = true
  508. if (!frames.done) frames.push(frame)
  509. return frames
  510. }, [])
  511. let prototype = typeof(e) === "object" ? e.constructor.prototype :
  512. Error.prototype;
  513. let stack = serializeStack(frames.reverse());
  514. let error = Object.create(prototype, {
  515. message: { value: e.message, writable: true, configurable: true },
  516. fileName: { value: e.fileName, writable: true, configurable: true },
  517. lineNumber: { value: e.lineNumber, writable: true, configurable: true },
  518. stack: { value: stack, writable: true, configurable: true },
  519. toString: { value: function() String(e), writable: true, configurable: true },
  520. });
  521. print("Error: " + error + " \n " + format(error));
  522. onDone({passed: 0, failed: 1});
  523. }
  524. };
  525. unload(function() {
  526. cService.unregisterListener(consoleListener);
  527. });