Bug 1403577 - Add utility for truncating strings. r?whimboo draft
authorAndreas Tolfsen <ato@sny.no>
Sat, 30 Sep 2017 17:06:29 +0100
changeset 679266 0377b12cb9681e000fa88387aaf9773585fe9a80
parent 679173 bc7a5be76b723cf6aac1a919156e74997c5f4902
child 679267 d10932084feadeb1207ec688f82596e8ac37bb2c
push id84168
push userbmo:ato@sny.no
push dateThu, 12 Oct 2017 12:57:30 +0000
reviewerswhimboo
bugs1403577
milestone58.0a1
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
testing/marionette/format.js
testing/marionette/jar.mn
testing/marionette/test_format.js
testing/marionette/unit.ini
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]