Bug 1403577 - Add utility for truncating strings. r=whimboo
authorAndreas Tolfsen <ato@sny.no>
Sat, 30 Sep 2017 17:06:29 +0100
changeset 385909 df3e0cc4a9bfc95f84d241eb3b2cef9da332de87
parent 385908 ee470df77e3fbb980c953051470c2c074a0c6065
child 385910 2fe618b2b66030c44ce1e69eb2b84dc36e5211fb
push id32672
push userarchaeopteryx@coole-files.de
push dateFri, 13 Oct 2017 09:00:05 +0000
treeherdermozilla-central@3efcb26e5f37 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswhimboo
bugs1403577
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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]