summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/logHelper.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/resources/logHelper.js')
-rw-r--r--comm/mailnews/test/resources/logHelper.js567
1 files changed, 567 insertions, 0 deletions
diff --git a/comm/mailnews/test/resources/logHelper.js b/comm/mailnews/test/resources/logHelper.js
new file mode 100644
index 0000000000..88a9616904
--- /dev/null
+++ b/comm/mailnews/test/resources/logHelper.js
@@ -0,0 +1,567 @@
+/* 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/. */
+
+/*
+ * Makes everything awesome if you are Andrew. Some day it will make everything
+ * awesome if you are not awesome too.
+ *
+ * Right now the most meaningful thing to know is that if XPCOM failures happen
+ * (and get reported to the error console), this will induce a unit test
+ * failure. You should think this is awesome no matter whether you are Andrew
+ * or not.
+ */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["Element", "Node"]);
+
+var _mailnewsTestLogger;
+var _xpcshellLogger;
+var _testLoggerContexts = [];
+var _testLoggerContextId = 0;
+var _testLoggerActiveContext;
+
+var _logHelperInterestedListeners = false;
+
+/**
+ * Let test code extend the list of allowed XPCOM errors.
+ */
+var logHelperAllowedErrors = ["NS_ERROR_FAILURE"];
+var logHelperAllowedWarnings = [/Quirks Mode/];
+
+/**
+ * Let other test helping code decide whether to register for potentially
+ * expensive notifications based on whether anyone can even hear those
+ * results.
+ */
+function logHelperHasInterestedListeners() {
+ return _logHelperInterestedListeners;
+}
+
+/**
+ * Tunnel nsIScriptErrors that show up on the error console to ConsoleInstance.
+ * We could send everything but I think only script errors are likely of much
+ * concern. Also, this nicely avoids infinite recursions no matter what you do
+ * since what we publish is not going to end up as an nsIScriptError.
+ *
+ * This is based on my (asuth') exmmad extension.
+ */
+var _errorConsoleTunnel = {
+ initialize() {
+ Services.console.registerListener(this);
+
+ // we need to unregister our listener at shutdown if we don't want explosions
+ Services.obs.addObserver(this, "quit-application");
+ },
+
+ shutdown() {
+ Services.console.unregisterListener(this);
+ Services.obs.removeObserver(this, "quit-application");
+ },
+
+ observe(aMessage, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this.shutdown();
+ return;
+ }
+
+ try {
+ if (
+ aMessage instanceof Ci.nsIScriptError &&
+ !aMessage.errorMessage.includes("Error console says")
+ ) {
+ // Unfortunately changes to mozilla-central are throwing lots
+ // of console errors during testing, so disable (we hope temporarily)
+ // failing on XPCOM console errors (see bug 1014350).
+ // An XPCOM error aMessage looks like this:
+ // [JavaScript Error: "uncaught exception: 2147500037"]
+ // Capture the number, and allow known XPCOM results.
+ let matches = /JavaScript Error: "(\w+)/.exec(aMessage);
+ let XPCOMresult = null;
+ if (matches) {
+ for (let result in Cr) {
+ if (matches[1] == Cr[result]) {
+ XPCOMresult = result;
+ break;
+ }
+ }
+ let message = XPCOMresult || aMessage;
+ if (logHelperAllowedErrors.some(e => e == matches[1])) {
+ if (XPCOMresult) {
+ info("Ignoring XPCOM error: " + message);
+ }
+ return;
+ }
+ info("Found XPCOM error: " + message);
+ }
+ // Ignore warnings that match a white-listed pattern.
+ if (
+ /JavaScript Warning:/.test(aMessage) &&
+ logHelperAllowedWarnings.some(w => w.test(aMessage))
+ ) {
+ return;
+ }
+ dump(`Error console says: ${aMessage}`);
+ }
+ } catch (ex) {
+ // This is to avoid pathological error loops. we definitely do not
+ // want to propagate an error here.
+ }
+ },
+};
+
+// This defaults to undefined and is for use by test-folder-display-helpers
+// so that it can pre-initialize the value so that when we are evaluated in
+// its subscript loader we see a value of 'true'.
+var _do_not_wrap_xpcshell;
+
+/**
+ * Initialize logging. The idea is to:
+ *
+ * - Always create a dump appender on 'test'.
+ * - Check if there's a desire to use a logsploder style network connection
+ * based on the presence of an appropriate file in 'tmp'. This should be
+ * harmless in cases where there is not such a file.
+ *
+ * We will wrap the interesting xpcshell functions if we believe there is an
+ * endpoint that cares about these things (such as logsploder).
+ */
+function _init_log_helper() {
+ // - dump on test
+ _mailnewsTestLogger = console.createInstance({
+ prefix: "test.test",
+ });
+
+ // - silent category for xpcshell stuff that already gets dump()ed
+ _xpcshellLogger = console.createInstance({
+ prefix: "xpcshell",
+ });
+
+ // Create a console listener reporting thinger in all cases. Since XPCOM
+ // failures will show up via the error console, this allows our test to fail
+ // in more situations where we might otherwise silently be cool with bad
+ // things happening.
+ _errorConsoleTunnel.initialize();
+
+ if (_logHelperInterestedListeners) {
+ if (!_do_not_wrap_xpcshell) {
+ _wrap_xpcshell_functions();
+ }
+
+ // Send a message telling the listeners about the test file being run.
+ _xpcshellLogger.info({
+ _jsonMe: true,
+ _isContext: true,
+ _specialContext: "lifecycle",
+ _id: "start",
+ testFile: _TEST_FILE,
+ });
+ }
+}
+_init_log_helper();
+
+/**
+ * Mark the start of a test. This creates nice console output as well as
+ * setting up logging contexts so that use of other helpers in here
+ * get associated with the context.
+ *
+ * This will likely only be used by the test driver framework, such as
+ * asyncTestUtils.js. However, |mark_sub_test_start| is for user test code.
+ */
+function mark_test_start(aName, aParameter, aDepth) {
+ if (aDepth == null) {
+ aDepth = 0;
+ }
+
+ // clear out any existing contexts
+ mark_test_end(aDepth);
+
+ let term = aDepth == 0 ? "test" : "subtest";
+ _testLoggerActiveContext = {
+ type: term,
+ name: aName,
+ parameter: aParameter,
+ _id: ++_testLoggerContextId,
+ };
+ if (_testLoggerContexts.length) {
+ _testLoggerActiveContext._contextDepth = _testLoggerContexts.length;
+ _testLoggerActiveContext._contextParentId =
+ _testLoggerContexts[_testLoggerContexts.length - 1]._id;
+ }
+ _testLoggerContexts.push(_testLoggerActiveContext);
+
+ _mailnewsTestLogger.info(
+ _testLoggerActiveContext._id,
+ "Starting " + term + ": " + aName + (aParameter ? ", " + aParameter : "")
+ );
+}
+
+/**
+ * Mark the end of a test started by |mark_test_start|.
+ */
+function mark_test_end(aPopTo) {
+ if (aPopTo === undefined) {
+ aPopTo = 0;
+ }
+ // clear out any existing contexts
+ while (_testLoggerContexts.length > aPopTo) {
+ let context = _testLoggerContexts.pop();
+ _mailnewsTestLogger.info(
+ context._id,
+ "Finished " +
+ context.type +
+ ": " +
+ context.name +
+ (context.parameter ? ", " + context.parameter : "")
+ );
+ }
+}
+
+/**
+ * For user test code and test support code to mark sub-regions of tests.
+ *
+ * @param {string} aName The name of the (sub) test.
+ * @param {string} [aParameter=null] The parameter if the test is being parameterized.
+ * @param {boolean} [aNest=false] Should this nest inside other sub-tests?
+ * If you omit orpass false, we will close out any existing sub-tests.
+ * If you pass true, we nest inside the previous test/sub-test and rely on
+ * you to call |mark_sub_test_end|.
+ * Sub tests can lost no longer than their parent.
+ * You should strongly consider using the aNest parameter if you are test
+ * support code.
+ */
+function mark_sub_test_start(aName, aParameter, aNest) {
+ let depth = aNest ? _testLoggerContexts.length : 1;
+ mark_test_start(aName, aParameter, depth);
+}
+
+/**
+ * Mark the end of a sub-test. Because sub-tests can't outlive their parents,
+ * there is no ambiguity about what sub-test we are closing out.
+ */
+function mark_sub_test_end() {
+ if (_testLoggerContexts.length <= 1) {
+ return;
+ }
+ mark_test_end(_testLoggerContexts.length - 1);
+}
+
+/**
+ * Express that all tests were run to completion. This helps the listener
+ * distinguish between successful termination and abort-style termination where
+ * the process just keeled over and on one told us.
+ *
+ * This also tells us to clean up.
+ */
+function mark_all_tests_run() {
+ // make sure all tests get closed out
+ mark_test_end();
+
+ _xpcshellLogger.info("All finished");
+}
+
+function _explode_flags(aFlagWord, aFlagDefs) {
+ let flagList = [];
+
+ for (let flagName in aFlagDefs) {
+ let flagVal = aFlagDefs[flagName];
+ if (flagVal & aFlagWord) {
+ flagList.push(flagName);
+ }
+ }
+
+ return flagList;
+}
+
+var _registered_json_normalizers = [];
+
+/**
+ * Copy natives or objects, deferring to _normalize_for_json for objects.
+ */
+function __value_copy(aObj, aDepthAllowed) {
+ if (aObj == null || typeof aObj != "object") {
+ return aObj;
+ }
+ return _normalize_for_json(aObj, aDepthAllowed, true);
+}
+
+/**
+ * Simple object copier to limit accidentally JSON-ing a ridiculously complex
+ * object graph or getting tripped up by prototypes.
+ *
+ * @param {object} aObj - Input object.
+ * @param {integer} aDepthAllowed - How many times we are allowed to recursively
+ * call ourselves.
+ */
+function __simple_obj_copy(aObj, aDepthAllowed) {
+ let oot = {};
+ let nextDepth = aDepthAllowed - 1;
+ for (let key in aObj) {
+ // avoid triggering getters
+ if (aObj.__lookupGetter__(key)) {
+ oot[key] = "*getter*";
+ continue;
+ }
+ let value = aObj[key];
+
+ if (value == null) {
+ oot[key] = null;
+ } else if (typeof value != "object") {
+ oot[key] = value;
+ } else if (!aDepthAllowed) {
+ // steal control flow if no more depth is allowed
+ oot[key] = "truncated, string rep: " + value.toString();
+ } else if (Array.isArray(value)) {
+ // array? (not directly counted, but we will terminate because the
+ // child copying occurs using nextDepth...)
+ oot[key] = value.map(v => __value_copy(v, nextDepth));
+ } else {
+ // it's another object! woo!
+ oot[key] = _normalize_for_json(value, nextDepth, true);
+ }
+ }
+
+ // let's take advantage of the object's native toString now
+ oot._stringRep = aObj.toString();
+
+ return oot;
+}
+
+var _INTERESTING_MESSAGE_HEADER_PROPERTIES = {
+ "gloda-id": 0,
+ "gloda-dirty": 0,
+ junkscore: "",
+ junkscoreorigin: "",
+ msgOffset: 0,
+ offlineMsgSize: 0,
+};
+
+/**
+ * Given an object, attempt to normalize it into an interesting JSON
+ * representation.
+ *
+ * We transform generally interesting mail objects like:
+ * - nsIMsgFolder
+ * - nsIMsgDBHdr
+ */
+function _normalize_for_json(aObj, aDepthAllowed, aJsonMeNotNeeded) {
+ if (aDepthAllowed === undefined) {
+ aDepthAllowed = 2;
+ }
+
+ // if it's a simple type just return it direct
+ if (typeof aObj != "object") {
+ return aObj;
+ } else if (aObj == null) {
+ return aObj;
+ }
+
+ // recursively transform arrays outright
+ if (Array.isArray(aObj)) {
+ return aObj.map(v => __value_copy(v, aDepthAllowed - 1));
+ }
+
+ // === Mail Specific ===
+ // (but common and few enough to not split out)
+ if (aObj instanceof Ci.nsIMsgFolder) {
+ return {
+ type: "folder",
+ name: aObj.prettyName,
+ uri: aObj.URI,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgFolderFlags),
+ };
+ } else if (aObj instanceof Ci.nsIMsgDBHdr) {
+ let properties = {};
+ for (let name in _INTERESTING_MESSAGE_HEADER_PROPERTIES) {
+ let propType = _INTERESTING_MESSAGE_HEADER_PROPERTIES[name];
+ if (propType === 0) {
+ properties[name] =
+ aObj.getStringProperty(name) != ""
+ ? aObj.getUint32Property(name)
+ : null;
+ } else {
+ properties[name] = aObj.getStringProperty(name);
+ }
+ }
+ return {
+ type: "msgHdr",
+ name: aObj.folder.URI + "#" + aObj.messageKey,
+ subject: aObj.mime2DecodedSubject,
+ from: aObj.mime2DecodedAuthor,
+ to: aObj.mime2DecodedRecipients,
+ messageKey: aObj.messageKey,
+ messageId: aObj.messageId,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgMessageFlags),
+ interestingProperties: properties,
+ };
+ } else if (Node.isInstance(aObj)) {
+ // === Generic ===
+ // DOM nodes, including elements
+ let name = aObj.nodeName;
+ let objAttrs = {};
+
+ if (Element.isInstance(aObj)) {
+ name += "#" + aObj.getAttribute("id");
+ }
+
+ if ("attributes" in aObj) {
+ let nodeAttrs = aObj.attributes;
+ for (let iAttr = 0; iAttr < nodeAttrs.length; iAttr++) {
+ objAttrs[nodeAttrs[iAttr].name] = nodeAttrs[iAttr].value;
+ }
+ }
+
+ let bounds = { left: null, top: null, width: null, height: null };
+ if ("getBoundingClientRect" in aObj) {
+ bounds = aObj.getBoundingClientRect();
+ }
+
+ return {
+ type: "domNode",
+ name,
+ value: aObj.nodeValue,
+ namespace: aObj.namespaceURI,
+ boundingClientRect: bounds,
+ attrs: objAttrs,
+ };
+ } else if (aObj instanceof Ci.nsIDOMWindow) {
+ let winId, title;
+ if (aObj.document && aObj.document.documentElement) {
+ title = aObj.document.title;
+ winId =
+ aObj.document.documentElement.getAttribute("windowtype") ||
+ aObj.document.documentElement.getAttribute("id") ||
+ "unnamed";
+ } else {
+ winId = "n/a";
+ title = "no document";
+ }
+ return {
+ type: "domWindow",
+ id: winId,
+ title,
+ location: "" + aObj.location,
+ coords: { x: aObj.screenX, y: aObj.screenY },
+ dims: { width: aObj.outerWidth, height: aObj.outerHeight },
+ };
+ } else if (aObj instanceof Error) {
+ // Although straight JS exceptions should serialize pretty well, we can
+ // improve things by making "stack" more friendly.
+ return {
+ type: "error",
+ message: aObj.message,
+ fileName: aObj.fileName,
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ stack: aObj.stack ? aObj.stack.split(/\n\r?/g) : null,
+ _stringRep: aObj.message,
+ };
+ } else if (aObj instanceof Ci.nsIException) {
+ return {
+ type: "error",
+ message: "nsIException: " + aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ result: aObj.result,
+ stack: null,
+ };
+ } else if (aObj instanceof Ci.nsIStackFrame) {
+ return {
+ type: "stackFrame",
+ name: aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ };
+ } else if (aObj instanceof Ci.nsIScriptError) {
+ return {
+ type: "stackFrame",
+ name: aObj.errorMessage,
+ category: aObj.category,
+ fileName: aObj.sourceName,
+ lineNumber: aObj.lineNumber,
+ };
+ }
+
+ for (let [checkType, handler] of _registered_json_normalizers) {
+ if (aObj instanceof checkType) {
+ return handler(aObj);
+ }
+ }
+
+ // Do not fall into simple object walking if this is an XPCOM interface.
+ // We might run across getters and that leads to nothing good.
+ if (aObj instanceof Ci.nsISupports) {
+ return {
+ type: "XPCOM",
+ name: aObj.toString(),
+ };
+ }
+
+ let simple_obj = __simple_obj_copy(aObj, aDepthAllowed);
+ if (!aJsonMeNotNeeded) {
+ simple_obj._jsonMe = true;
+ }
+ return simple_obj;
+}
+
+function register_json_normalizer(aType, aHandler) {
+ _registered_json_normalizers.push([aType, aHandler]);
+}
+
+/*
+ * Wrap the xpcshell test functions that do interesting things. The idea is
+ * that we clobber these only if we're going to value-add; that decision
+ * gets made up top in the initialization function.
+ *
+ * Since eq/neq fall-through to do_throw in the explosion case, we don't handle
+ * that since the scoping means that we're going to see the resulting
+ * do_throw.
+ */
+
+var _orig_do_throw;
+var _orig_do_check_neq;
+var _orig_do_check_eq;
+// do_check_true is implemented in terms of do_check_eq
+// do_check_false is implemented in terms of do_check_eq
+
+function _CheckAction(aSuccess, aLeft, aRight, aStack) {
+ this.type = "check";
+ this.success = aSuccess;
+ this.left = _normalize_for_json(aLeft);
+ this.right = _normalize_for_json(aRight);
+ this.stack = _normalize_for_json(aStack);
+}
+_CheckAction.prototype = {
+ _jsonMe: true,
+ // we don't need a toString because we should not go out to the console
+};
+
+/**
+ * Representation of a failure from do_throw.
+ */
+function _Failure(aText, aStack) {
+ this.type = "failure";
+ this.text = aText;
+ this.stack = _normalize_for_json(aStack);
+}
+_Failure.prototype = {
+ _jsonMe: true,
+};
+
+function _wrapped_do_throw(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ // We need to use an info because otherwise explosion loggers can get angry
+ // and they may be indiscriminate about what they subscribe to.
+ _xpcshellLogger.info(_testLoggerActiveContext, new _Failure(text, stack));
+
+ return _orig_do_throw(text, stack);
+}
+
+function _wrap_xpcshell_functions() {
+ _orig_do_throw = do_throw;
+ do_throw = _wrapped_do_throw; // eslint-disable-line no-global-assign
+}