Bug 1403577 - Add utility for truncating strings. r?whimboo
Introduces a utility that truncates strings in potentially arbitrary
object structures. This allows JSON structures that contain long
strings to be shortened with an " ..." appendix for pretty logging
when data integrity is not a vital concern.
The maximum string length is currently set to 250 characters, which
is a number I have pulled out of a hat.
MozReview-Commit-ID: 2gauOvMzBCO
new file mode 100644
--- /dev/null
+++ b/testing/marionette/format.js
@@ -0,0 +1,85 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["truncate"];
+
+const MAX_STRING_LENGTH = 250;
+
+/**
+ * Template literal that truncates string values in arbitrary objects.
+ *
+ * Given any object, the template will walk the object and truncate
+ * any strings it comes across to a reasonable limit. This is suitable
+ * when you have arbitrary data and data integrity is not important.
+ *
+ * The strings are truncated in the middle so that the beginning and
+ * the end is preserved. This will make a long, truncated string look
+ * like "X <...> Y", where X and Y are half the number of characters
+ * of the maximum string length from either side of the string.
+ *
+ * Usage:
+ *
+ * <pre><code>
+ * truncate`Hello ${"x".repeat(260)}!`;
+ * // Hello xxx ... xxx!
+ * </code></pre>
+ *
+ * Functions named <code>toJSON</code> or <code>toString</code>
+ * on objects will be called.
+ */
+function truncate(strings, ...values) {
+ function walk(obj) {
+ const typ = Object.prototype.toString.call(obj);
+
+ switch (typ) {
+ case "[object Undefined]":
+ case "[object Null]":
+ case "[object Boolean]":
+ case "[object Number]":
+ return obj;
+
+ case "[object String]":
+ if (obj.length > MAX_STRING_LENGTH) {
+ let s1 = obj.substring(0, (MAX_STRING_LENGTH / 2));
+ let s2 = obj.substring(obj.length - (MAX_STRING_LENGTH / 2));
+ return `${s1} ... ${s2}`;
+ }
+ return obj;
+
+ case "[object Array]":
+ return obj.map(walk);
+
+ // arbitrary object
+ default:
+ if (Object.getOwnPropertyNames(obj).includes("toString") &&
+ typeof obj.toString == "function") {
+ return walk(obj.toString());
+ }
+
+ let rv = {};
+ for (let prop in obj) {
+ rv[prop] = walk(obj[prop]);
+ }
+ return rv;
+ }
+ }
+
+ let res = [];
+ for (let i = 0; i < strings.length; ++i) {
+ res.push(strings[i]);
+ if (i < values.length) {
+ let obj = walk(values[i]);
+ let t = Object.prototype.toString.call(obj);
+ if (t == "[object Array]" || t == "[object Object]") {
+ res.push(JSON.stringify(obj));
+ } else {
+ res.push(obj);
+ }
+ }
+ }
+ return res.join("");
+}
+this.truncate = truncate;
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -31,16 +31,17 @@ marionette.jar:
content/addon.js (addon.js)
content/session.js (session.js)
content/transport.js (transport.js)
content/packets.js (packets.js)
content/stream-utils.js (stream-utils.js)
content/reftest.js (reftest.js)
content/reftest.xul (reftest.xul)
content/dom.js (dom.js)
+ content/format.js (format.js)
#ifdef ENABLE_TESTS
content/test.xul (chrome/test.xul)
content/test2.xul (chrome/test2.xul)
content/test_dialog.dtd (chrome/test_dialog.dtd)
content/test_dialog.properties (chrome/test_dialog.properties)
content/test_dialog.xul (chrome/test_dialog.xul)
content/test_nested_iframe.xul (chrome/test_nested_iframe.xul)
content/test_anonymous_content.xul (chrome/test_anonymous_content.xul)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/test_format.js
@@ -0,0 +1,70 @@
+/* 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/. */
+
+const {utils: Cu} = Components;
+
+const {truncate} = Cu.import("chrome://marionette/content/format.js", {});
+
+const MAX_STRING_LENGTH = 250;
+const HALF = "x".repeat(MAX_STRING_LENGTH / 2);
+
+add_test(function test_truncate_empty() {
+ equal(truncate``, "");
+ run_next_test();
+});
+
+add_test(function test_truncate_noFields() {
+ equal(truncate`foo bar`, "foo bar");
+ run_next_test();
+});
+
+add_test(function test_truncate_multipleFields() {
+ equal(truncate`${0}`, "0");
+ equal(truncate`${1}${2}${3}`, "123");
+ equal(truncate`a${1}b${2}c${3}`, "a1b2c3");
+ run_next_test();
+});
+
+add_test(function test_truncate_primitiveFields() {
+ equal(truncate`${123}`, "123");
+ equal(truncate`${true}`, "true");
+ equal(truncate`${null}`, "");
+ equal(truncate`${undefined}`, "");
+ run_next_test();
+});
+
+add_test(function test_truncate_string() {
+ equal(truncate`${"foo"}`, "foo");
+ equal(truncate`${"x".repeat(250)}`, "x".repeat(250));
+ equal(truncate`${"x".repeat(260)}`, `${HALF} ... ${HALF}`);
+ run_next_test();
+});
+
+add_test(function test_truncate_array() {
+ equal(truncate`${["foo"]}`, JSON.stringify(["foo"]));
+ equal(truncate`${"foo"} ${["bar"]}`, `foo ${JSON.stringify(["bar"])}`);
+ equal(truncate`${["x".repeat(260)]}`, JSON.stringify([`${HALF} ... ${HALF}`]));
+
+ run_next_test();
+});
+
+add_test(function test_truncate_object() {
+ equal(truncate`${{}}`, JSON.stringify({}));
+ equal(truncate`${{foo: "bar"}}`, JSON.stringify({foo: "bar"}));
+ equal(truncate`${{foo: "x".repeat(260)}}`, JSON.stringify({foo: `${HALF} ... ${HALF}`}));
+ equal(truncate`${{foo: ["bar"]}}`, JSON.stringify({foo: ["bar"]}));
+ equal(truncate`${{foo: ["bar", {baz: 42}]}}`, JSON.stringify({foo: ["bar", {baz: 42}]}));
+
+ let complex = {
+ toString() { return "hello world"; }
+ };
+ equal(truncate`${complex}`, "hello world");
+
+ let longComplex = {
+ toString() { return "x".repeat(260); }
+ };
+ equal(truncate`${longComplex}`, `${HALF} ... ${HALF}`);
+
+ run_next_test();
+});
--- a/testing/marionette/unit.ini
+++ b/testing/marionette/unit.ini
@@ -8,12 +8,13 @@
skip-if = appname == "thunderbird"
[test_action.js]
[test_assert.js]
[test_cookie.js]
[test_dom.js]
[test_element.js]
[test_error.js]
+[test_format.js]
[test_message.js]
[test_navigate.js]
[test_session.js]
[test_sync.js]