/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; module.metadata = { "stability": "experimental" }; const { Cc, Ci, Cu } = require("chrome"); const { Loader } = require('./loader'); const { serializeStack, parseStack } = require("toolkit/loader"); const { setTimeout } = require('../timers'); const { PlainTextConsole } = require("../console/plain-text"); const { when: unload } = require("../system/unload"); const { format, fromException } = require("../console/traceback"); const system = require("../system"); const { gc: gcPromise } = require('./memory'); const { defer } = require('../core/promise'); const { extend } = require('../core/heritage'); // Trick manifest builder to make it think we need these modules ? const unit = require("../deprecated/unit-test"); const test = require("../../test"); const url = require("../url"); function emptyPromise() { let { promise, resolve } = defer(); resolve(); return promise; } var cService = Cc['@mozilla.org/consoleservice;1'].getService(Ci.nsIConsoleService); // The console used to log messages var testConsole; // Cuddlefish loader in which we load and execute tests. var loader; // Function to call when we're done running tests. var onDone; // Function to print text to a console, w/o CR at the end. var print; // How many more times to run all tests. var iterationsLeft; // Whether to report memory profiling information. var profileMemory; // Whether we should stop as soon as a test reports a failure. var stopOnError; // Function to call to retrieve a list of tests to execute var findAndRunTests; // Combined information from all test runs. var results; // A list of the compartments and windows loaded after startup var startLeaks; // JSON serialization of last memory usage stats; we keep it stringified // so we don't actually change the memory usage stats (in terms of objects) // of the JSRuntime we're profiling. var lastMemoryUsage; function analyzeRawProfilingData(data) { var graph = data.graph; var shapes = {}; // Convert keys in the graph from strings to ints. // TODO: Can we get rid of this ridiculousness? var newGraph = {}; for (id in graph) { newGraph[parseInt(id)] = graph[id]; } graph = newGraph; var modules = 0; var moduleIds = []; var moduleObjs = {UNKNOWN: 0}; for (let name in data.namedObjects) { moduleObjs[name] = 0; moduleIds[data.namedObjects[name]] = name; modules++; } var count = 0; for (id in graph) { var parent = graph[id].parent; while (parent) { if (parent in moduleIds) { var name = moduleIds[parent]; moduleObjs[name]++; break; } if (!(parent in graph)) { moduleObjs.UNKNOWN++; break; } parent = graph[parent].parent; } count++; } print("\nobject count is " + count + " in " + modules + " modules" + " (" + data.totalObjectCount + " across entire JS runtime)\n"); if (lastMemoryUsage) { var last = JSON.parse(lastMemoryUsage); var diff = { moduleObjs: dictDiff(last.moduleObjs, moduleObjs), totalObjectClasses: dictDiff(last.totalObjectClasses, data.totalObjectClasses) }; for (let name in diff.moduleObjs) print(" " + diff.moduleObjs[name] + " in " + name + "\n"); for (let name in diff.totalObjectClasses) print(" " + diff.totalObjectClasses[name] + " instances of " + name + "\n"); } lastMemoryUsage = JSON.stringify( {moduleObjs: moduleObjs, totalObjectClasses: data.totalObjectClasses} ); } function dictDiff(last, curr) { var diff = {}; for (let name in last) { var result = (curr[name] || 0) - last[name]; if (result) diff[name] = (result > 0 ? "+" : "") + result; } for (let name in curr) { var result = curr[name] - (last[name] || 0); if (result) diff[name] = (result > 0 ? "+" : "") + result; } return diff; } function reportMemoryUsage() { if (!profileMemory) { return emptyPromise(); } return gcPromise().then((() => { var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] .getService(Ci.nsIMemoryReporterManager); let count = 0; function logReporter(process, path, kind, units, amount, description) { print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n"); } mgr.getReportsForThisProcess(logReporter, null, /* anonymize = */ false); })); } var gWeakrefInfo; function checkMemory() { return gcPromise().then(_ => { let leaks = getPotentialLeaks(); let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { return !(url in startLeaks.compartments); }); let windowURLs = Object.keys(leaks.windows).filter(function(url) { return !(url in startLeaks.windows); }); for (let url of compartmentURLs) console.warn("LEAKED", leaks.compartments[url]); for (let url of windowURLs) console.warn("LEAKED", leaks.windows[url]); }).then(showResults); } function showResults() { let { promise, resolve } = defer(); if (gWeakrefInfo) { gWeakrefInfo.forEach( function(info) { var ref = info.weakref.get(); if (ref !== null) { var data = ref.__url__ ? ref.__url__ : ref; var warning = data == "[object Object]" ? "[object " + data.constructor.name + "(" + Object.keys(data).join(", ") + ")]" : data; console.warn("LEAK", warning, info.bin); } } ); } onDone(results); resolve(); return promise; } function cleanup() { let coverObject = {}; try { loader.unload(); if (loader.globals.console.errorsLogged && !results.failed) { results.failed++; console.error("warnings and/or errors were logged."); } if (consoleListener.errorsLogged && !results.failed) { console.warn(consoleListener.errorsLogged + " " + "warnings or errors were logged to the " + "platform's nsIConsoleService, which could " + "be of no consequence; however, they could also " + "be indicative of aberrant behavior."); } // read the code coverage object, if it exists, from CoverJS-moz if (typeof loader.globals.global == "object") { coverObject = loader.globals.global['__$coverObject'] || {}; } consoleListener.errorsLogged = 0; loader = null; consoleListener.unregister(); Cu.forceGC(); } catch (e) { results.failed++; console.error("unload.send() threw an exception."); console.exception(e); }; setTimeout(require("./options").checkMemory ? checkMemory : showResults, 1); // dump the coverobject if (Object.keys(coverObject).length){ const self = require('sdk/self'); const {pathFor} = require("sdk/system"); let file = require('sdk/io/file'); const {env} = require('sdk/system/environment'); console.log("CWD:", env.PWD); let out = file.join(env.PWD,'coverstats-'+self.id+'.json'); console.log('coverstats:', out); let outfh = file.open(out,'w'); outfh.write(JSON.stringify(coverObject,null,2)); outfh.flush(); outfh.close(); } } function getPotentialLeaks() { Cu.forceGC(); // Things we can assume are part of the platform and so aren't leaks let GOOD_BASE_URLS = [ "chrome://", "resource:///", "resource://app/", "resource://gre/", "resource://gre-resources/", "resource://pdf.js/", "resource://pdf.js.components/", "resource://services-common/", "resource://services-crypto/", "resource://services-sync/" ]; let ioService = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); let uri = ioService.newURI("chrome://global/content/", "UTF-8", null); let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. getService(Ci.nsIChromeRegistry); uri = chromeReg.convertChromeURL(uri); let spec = uri.spec; let pos = spec.indexOf("!/"); GOOD_BASE_URLS.push(spec.substring(0, pos + 2)); let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)"); let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/"); let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$"); let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active"); let windowDetails = new RegExp("^(.*), id=.*$"); function isPossibleLeak(item) { if (!item.location) return false; for (let url of GOOD_BASE_URLS) { if (item.location.substring(0, url.length) == url) { return false; } } return true; } let compartments = {}; let windows = {}; function logReporter(process, path, kind, units, amount, description) { let matches; if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) { if (matches[1] in compartments) return; let details = compartmentDetails.exec(matches[1]); if (!details) { console.error("Unable to parse compartment detail " + matches[1]); return; } let item = { path: matches[1], principal: details[1], location: details[2] ? details[2].replace(/\\/g, "/") : undefined, source: details[3] ? details[3].split(" -> ").reverse() : undefined, toString: function() { return this.location; } }; if (!isPossibleLeak(item)) return; compartments[matches[1]] = item; return; } if ((matches = windowRegexp.exec(path))) { if (matches[1] in windows) return; let details = windowDetails.exec(matches[1]); if (!details) { console.error("Unable to parse window detail " + matches[1]); return; } let item = { path: matches[1], location: details[1].replace(/\\/g, "/"), source: [details[1].replace(/\\/g, "/")], toString: function() { return this.location; } }; if (!isPossibleLeak(item)) return; windows[matches[1]] = item; } } Cc["@mozilla.org/memory-reporter-manager;1"] .getService(Ci.nsIMemoryReporterManager) .getReportsForThisProcess(logReporter, null, /* anonymize = */ false); return { compartments: compartments, windows: windows }; } function nextIteration(tests) { if (tests) { results.passed += tests.passed; results.failed += tests.failed; reportMemoryUsage().then(_ => { let testRun = []; for (let test of tests.testRunSummary) { let testCopy = {}; for (let info in test) { testCopy[info] = test[info]; } testRun.push(testCopy); } results.testRuns.push(testRun); iterationsLeft--; checkForEnd(); }) } else { checkForEnd(); } } function checkForEnd() { if (iterationsLeft && (!stopOnError || results.failed == 0)) { // Pass the loader which has a hooked console that doesn't dispatch // errors to the JS console and avoid firing false alarm in our // console listener findAndRunTests(loader, nextIteration); } else { setTimeout(cleanup, 0); } } var POINTLESS_ERRORS = [ 'Invalid chrome URI:', 'OpenGL LayerManager Initialized Succesfully.', '[JavaScript Error: "TelemetryStopwatch:', 'reference to undefined property', '[JavaScript Error: "The character encoding of the HTML document was ' + 'not declared.', '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' + 'native weak map key', '[JavaScript Warning: "Duplicate resource declaration for', 'file: "chrome://browser/content/', 'file: "chrome://global/content/', '[JavaScript Warning: "The character encoding of a framed document was ' + 'not declared.', 'file: "chrome://browser/skin/' ]; // These are messages that will cause a test to fail if logged through the // console service var IMPORTANT_ERRORS = [ 'Sending message that cannot be cloned. Are you trying to send an XPCOM object?', ]; var consoleListener = { registered: false, register: function() { if (this.registered) return; cService.registerListener(this); this.registered = true; }, unregister: function() { if (!this.registered) return; cService.unregisterListener(this); this.registered = false; }, errorsLogged: 0, observe: function(object) { if (!(object instanceof Ci.nsIScriptError)) return; this.errorsLogged++; var message = object.QueryInterface(Ci.nsIConsoleMessage).message; if (IMPORTANT_ERRORS.find(msg => message.indexOf(msg) >= 0)) { testConsole.error(message); return; } var pointless = POINTLESS_ERRORS.filter(err => message.indexOf(err) >= 0); if (pointless.length == 0 && message) testConsole.log(message); } }; function TestRunnerConsole(base, options) { let proto = extend(base, { errorsLogged: 0, warn: function warn() { this.errorsLogged++; base.warn.apply(base, arguments); }, error: function error() { this.errorsLogged++; base.error.apply(base, arguments); }, info: function info(first) { if (options.verbose) base.info.apply(base, arguments); else if (first == "pass:") print("."); }, }); return Object.create(proto); } function stringify(arg) { try { return String(arg); } catch(ex) { return ""; } } function stringifyArgs(args) { return Array.map(args, stringify).join(" "); } function TestRunnerTinderboxConsole(base, options) { this.base = base; this.print = options.print; this.verbose = options.verbose; this.errorsLogged = 0; // Binding all the public methods to an instance so that they can be used // as callback / listener functions straightaway. this.log = this.log.bind(this); this.info = this.info.bind(this); this.warn = this.warn.bind(this); this.error = this.error.bind(this); this.debug = this.debug.bind(this); this.exception = this.exception.bind(this); this.trace = this.trace.bind(this); }; TestRunnerTinderboxConsole.prototype = { testMessage: function testMessage(pass, expected, test, message) { let type = "TEST-"; if (expected) { if (pass) type += "PASS"; else type += "KNOWN-FAIL"; } else { this.errorsLogged++; if (pass) type += "UNEXPECTED-PASS"; else type += "UNEXPECTED-FAIL"; } this.print(type + " | " + test + " | " + message + "\n"); if (!expected) this.trace(); }, log: function log() { this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); }, info: function info(first) { this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); }, warn: function warn() { this.errorsLogged++; this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); }, error: function error() { this.errorsLogged++; this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); this.base.error.apply(this.base, arguments); }, debug: function debug() { this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); }, exception: function exception(e) { this.print("An exception occurred.\n" + require("../console/traceback").format(e) + "\n" + e + "\n"); }, trace: function trace() { var traceback = require("../console/traceback"); var stack = traceback.get(); stack.splice(-1, 1); this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n"); } }; var runTests = exports.runTests = function runTests(options) { iterationsLeft = options.iterations; profileMemory = options.profileMemory; stopOnError = options.stopOnError; onDone = options.onDone; print = options.print; findAndRunTests = options.findAndRunTests; results = { passed: 0, failed: 0, testRuns: [] }; try { consoleListener.register(); print("Running tests on " + system.name + " " + system.version + "/Gecko " + system.platformVersion + " (Build " + system.build + ") (" + system.id + ") under " + system.platform + "/" + system.architecture + ".\n"); if (options.parseable) testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options); else testConsole = new TestRunnerConsole(new PlainTextConsole(), options); loader = Loader(module, { console: testConsole, global: {} // useful for storing things like coverage testing. }); // Load these before getting initial leak stats as they will still be in // memory when we check later require("../deprecated/unit-test"); require("../deprecated/unit-test-finder"); if (profileMemory) startLeaks = getPotentialLeaks(); nextIteration(); } catch (e) { let frames = fromException(e).reverse().reduce(function(frames, frame) { if (frame.fileName.split("/").pop() === "unit-test-finder.js") frames.done = true if (!frames.done) frames.push(frame) return frames }, []) let prototype = typeof(e) === "object" ? e.constructor.prototype : Error.prototype; let stack = serializeStack(frames.reverse()); let error = Object.create(prototype, { message: { value: e.message, writable: true, configurable: true }, fileName: { value: e.fileName, writable: true, configurable: true }, lineNumber: { value: e.lineNumber, writable: true, configurable: true }, stack: { value: stack, writable: true, configurable: true }, toString: { value: () => String(e), writable: true, configurable: true }, }); print("Error: " + error + " \n " + format(error)); onDone({passed: 0, failed: 1}); } }; unload(_ => consoleListener.unregister());