Bug 1525654 - Move version compatibility check to dedicated module and add unit-tests;r=ochameau
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 12 Feb 2019 18:20:40 +0000
changeset 458859 23fa2fb048b9
parent 458858 e5f3e1584cf9
child 458860 8826dd4a075a
push id35551
push usershindli@mozilla.com
push dateWed, 13 Feb 2019 21:34:09 +0000
treeherdermozilla-central@08f794a4928e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1525654
milestone67.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 1525654 - Move version compatibility check to dedicated module and add unit-tests;r=ochameau Differential Revision: https://phabricator.services.mozilla.com/D18934
devtools/client/shared/remote-debugging/moz.build
devtools/client/shared/remote-debugging/test/unit/.eslintrc.js
devtools/client/shared/remote-debugging/test/unit/test_version_checker.js
devtools/client/shared/remote-debugging/test/unit/xpcshell-head.js
devtools/client/shared/remote-debugging/test/unit/xpcshell.ini
devtools/client/shared/remote-debugging/version-checker.js
devtools/client/webide/content/webide.js
devtools/shared/client/debugger-client.js
--- a/devtools/client/shared/remote-debugging/moz.build
+++ b/devtools/client/shared/remote-debugging/moz.build
@@ -1,12 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'remote-client-manager.js',
+    'version-checker.js',
 )
 
+XPCSHELL_TESTS_MANIFESTS += [
+    'test/unit/xpcshell.ini'
+]
+
 with Files('**'):
     BUG_COMPONENT = ('DevTools', 'about:debugging')
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+  // Extend from the common devtools xpcshell eslintrc config.
+  "extends": "../../../../../.eslintrc.xpcshell.js"
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/unit/test_version_checker.js
@@ -0,0 +1,110 @@
+/* global equal */
+
+"use strict";
+
+const {
+  _compareVersionCompatibility,
+  checkVersionCompatibility,
+  COMPATIBILITY_STATUS,
+} = require("devtools/client/shared/remote-debugging/version-checker");
+
+const TEST_DATA = [
+  {
+    description: "same build date and same version number",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "60.0",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same build date and older version in range (-1)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "59.0",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same build date and older version in range (-2)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "58.0",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same build date and older version in range (-2 Nightly)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "58.0a1",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same build date and older version out of range (-3)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "57.0",
+    expected: COMPATIBILITY_STATUS.TOO_OLD,
+  },
+  {
+    description: "same build date and newer version out of range (+1)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190131000000",
+    runtimeVersion: "61.0",
+    expected: COMPATIBILITY_STATUS.TOO_RECENT,
+  },
+  {
+    description: "same major version and build date in range (-10 days)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190121000000",
+    runtimeVersion: "60.0",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same major version and build date in range (+2 days)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190202000000",
+    runtimeVersion: "60.0",
+    expected: COMPATIBILITY_STATUS.COMPATIBLE,
+  },
+  {
+    description: "same major version and build date out of range (+8 days)",
+    localBuildId: "20190131000000",
+    localVersion: "60.0",
+    runtimeBuildId: "20190208000000",
+    runtimeVersion: "60.0",
+    expected: COMPATIBILITY_STATUS.TOO_RECENT,
+  },
+];
+
+add_task(async function testVersionChecker() {
+  for (const testData of TEST_DATA) {
+    const localDescription = {
+      appbuildid: testData.localBuildId,
+      platformversion: testData.localVersion,
+    };
+
+    const runtimeDescription = {
+      appbuildid: testData.runtimeBuildId,
+      platformversion: testData.runtimeVersion,
+    };
+
+    const report = _compareVersionCompatibility(localDescription, runtimeDescription);
+    equal(report.status, testData.expected,
+      "Expected status for test: " + testData.description);
+  }
+});
+
+add_task(async function testVersionCheckWithVeryOldClient() {
+  // Use an empty object as debugger client, calling any method on it will fail.
+  const emptyClient = {};
+  const report = await checkVersionCompatibility(emptyClient);
+  equal(report.status, COMPATIBILITY_STATUS.TOO_OLD,
+      "Report status too old if debugger client is not implementing expected interface");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/unit/xpcshell-head.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/test/unit/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+head = xpcshell-head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_version_checker.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/remote-debugging/version-checker.js
@@ -0,0 +1,147 @@
+/* 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";
+
+const Services = require("Services");
+const {AppConstants} = require("resource://gre/modules/AppConstants.jsm");
+
+const MS_PER_DAY = 1000 * 60 * 60 * 24;
+
+const COMPATIBILITY_STATUS = {
+  COMPATIBLE: "compatible",
+  TOO_OLD: "too-old",
+  TOO_RECENT: "too-recent",
+};
+exports.COMPATIBILITY_STATUS = COMPATIBILITY_STATUS;
+
+function getDateFromBuildID(buildID) {
+  // Build IDs are a timestamp in the yyyyMMddHHmmss format.
+  // Extract the year, month and day information.
+  const fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
+  // Date expects 0 - 11 for months
+  const month = Number.parseInt(fields[2], 10) - 1;
+  return new Date(fields[1], month, fields[3]);
+}
+
+function getMajorVersion(platformVersion) {
+  // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
+  return Number.parseInt(platformVersion.match(/\d+/)[0], 10);
+}
+
+/**
+ * Compute the minimum and maximum supported version for remote debugging for the provided
+ * version of Firefox. Backward compatibility policy for devtools supports at most 2
+ * versions older than the current version.
+ *
+ * @param {String} localVersion
+ *        The version of the local Firefox instance, eg "67.0"
+ * @return {Object}
+ *         - minVersion {String} the minimum supported version, eg "65.0a1"
+ *         - maxVersion {String} the first unsupported version, eg "68.0a1"
+ */
+function computeMinMaxVersion(localVersion) {
+  // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
+  const localMajorVersion = getMajorVersion(localVersion);
+
+  return {
+    // Define the minimum officially supported version of Firefox when connecting to a
+    // remote runtime. (Use ".0a1" to support the very first nightly version)
+    // This matches the release channel's version when we are on nightly,
+    // or 2 versions before when we are on other channels.
+    minVersion: (localMajorVersion - 2) + ".0a1",
+    // The maximum version is the first excluded from the support range. That's why we
+    // increase the current version by 1 and use ".0a1" to point to the first Nightly.
+    // We do not support forward compatibility at all.
+    maxVersion: (localMajorVersion + 1) + ".0a1",
+  };
+}
+
+/**
+ * Tells if the remote device is using a supported version of Firefox.
+ *
+ * @param {DebuggerClient} debuggerClient
+ *        DebuggerClient instance connected to the target remote Firefox.
+ * @return Object with the following attributes:
+ *   * String status, one of COMPATIBILITY_STATUS
+ *            COMPATIBLE if the runtime is compatible,
+ *            TOO_RECENT if the runtime uses a too recent version,
+ *            TOO_OLD if the runtime uses a too old version.
+ *   * String minVersion
+ *            The minimum supported version.
+ *   * String runtimeVersion
+ *            The remote runtime version.
+ *   * String localID
+ *            Build ID of local runtime. A date with like this: YYYYMMDD.
+ *   * String deviceID
+ *            Build ID of remote runtime. A date with like this: YYYYMMDD.
+ */
+async function checkVersionCompatibility(debuggerClient) {
+  const localDescription = {
+    appbuildid: Services.appinfo.appBuildID,
+    platformversion: AppConstants.MOZ_APP_VERSION,
+  };
+
+  try {
+    const deviceFront = await debuggerClient.mainRoot.getFront("device");
+    const description = await deviceFront.getDescription();
+    return _compareVersionCompatibility(localDescription, description);
+  } catch (e) {
+    // If we failed to retrieve the device description, assume we are trying to connect to
+    // a really old version of Firefox.
+    const localVersion = localDescription.platformversion;
+    const { minVersion } = computeMinMaxVersion(localVersion);
+    return {
+      minVersion,
+      runtimeVersion: "<55",
+      status: COMPATIBILITY_STATUS.TOO_OLD,
+    };
+  }
+}
+exports.checkVersionCompatibility = checkVersionCompatibility;
+
+function _compareVersionCompatibility(localDescription, deviceDescription) {
+  const runtimeID = deviceDescription.appbuildid.substr(0, 8);
+  const localID = localDescription.appbuildid.substr(0, 8);
+
+  const runtimeDate = getDateFromBuildID(runtimeID);
+  const localDate = getDateFromBuildID(localID);
+
+  const runtimeVersion = deviceDescription.platformversion;
+  const localVersion = localDescription.platformversion;
+
+  const { minVersion, maxVersion } = computeMinMaxVersion(localVersion);
+  const isTooOld = Services.vc.compare(runtimeVersion, minVersion) < 0;
+  const isTooRecent = Services.vc.compare(runtimeVersion, maxVersion) >= 0;
+
+  const runtimeMajorVersion = getMajorVersion(runtimeVersion);
+  const localMajorVersion = getMajorVersion(localVersion);
+  const isSameMajorVersion = runtimeMajorVersion === localMajorVersion;
+
+  let status;
+  if (isTooOld) {
+    status = COMPATIBILITY_STATUS.TOO_OLD;
+  } else if (isTooRecent) {
+    status = COMPATIBILITY_STATUS.TOO_RECENT;
+  } else if (isSameMajorVersion && runtimeDate - localDate > 7 * MS_PER_DAY) {
+    // If both local and remote runtimes have the same major version, compare build dates.
+    // This check is useful for Gecko developers as we might introduce breaking changes
+    // within a Nightly cycle.
+    // Still allow devices to be newer by up to a week. This accommodates those with local
+    // device builds, since their devices will almost always be newer than the client.
+    status = COMPATIBILITY_STATUS.TOO_RECENT;
+  } else {
+    status = COMPATIBILITY_STATUS.COMPATIBLE;
+  }
+
+  return {
+    localID,
+    minVersion,
+    runtimeID,
+    runtimeVersion,
+    status,
+  };
+}
+// Exported for tests.
+exports._compareVersionCompatibility = _compareVersionCompatibility;
--- a/devtools/client/webide/content/webide.js
+++ b/devtools/client/webide/content/webide.js
@@ -15,16 +15,20 @@ const {AppProjects} = require("devtools/
 const {Connection} = require("devtools/shared/client/connection-manager");
 const {AppManager} = require("devtools/client/webide/modules/app-manager");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("promise");
 const {getJSON} = require("devtools/client/shared/getjson");
 const Telemetry = require("devtools/client/shared/telemetry");
 const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
 const {openContentLink} = require("devtools/client/shared/link");
+const {
+  checkVersionCompatibility,
+  COMPATIBILITY_STATUS,
+} = require("devtools/client/shared/remote-debugging/version-checker");
 
 loader.lazyRequireGetter(this, "adbAddon", "devtools/shared/adb/adb-addon", true);
 
 const Strings =
   Services.strings.createBundle("chrome://devtools/locale/webide.properties");
 
 const TELEMETRY_WEBIDE_IMPORT_PROJECT_COUNT = "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT";
 
@@ -732,22 +736,23 @@ var UI = {
     this.resetFocus();
     const deck = document.querySelector("#deck");
     deck.selectedPanel = null;
   },
 
   async checkRuntimeVersion() {
     if (AppManager.connected) {
       const { client } = AppManager.connection;
-      const report = await client.checkRuntimeVersion();
-      if (report.incompatible == "too-recent") {
+      const report = await checkVersionCompatibility(client);
+
+      if (report.status == COMPATIBILITY_STATUS.TOO_RECENT) {
         this.reportError("error_runtimeVersionTooRecent", report.runtimeID,
           report.localID);
       }
-      if (report.incompatible == "too-old") {
+      if (report.status == COMPATIBILITY_STATUS.TOO_OLD) {
         this.reportError("error_runtimeVersionTooOld", report.runtimeVersion,
           report.minVersion);
       }
     }
   },
 
   /** ******** TOOLBOX **********/
 
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -1,17 +1,15 @@
 /* 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";
 
-const Services = require("Services");
 const promise = require("devtools/shared/deprecated-sync-thenables");
-const {AppConstants} = require("resource://gre/modules/AppConstants.jsm");
 
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { getStack, callFunctionWithAsyncStack } = require("devtools/shared/platform/stack");
 const eventSource = require("devtools/shared/client/event-source");
 const {
   ThreadStateTypes,
   UnsolicitedNotifications,
   UnsolicitedPauses,
@@ -24,27 +22,16 @@ loader.lazyRequireGetter(this, "EventEmi
 loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
 loader.lazyRequireGetter(this, "RootFront", "devtools/shared/fronts/root", true);
 loader.lazyRequireGetter(this, "BrowsingContextTargetFront", "devtools/shared/fronts/targets/browsing-context", true);
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/object-client");
 loader.lazyRequireGetter(this, "Pool", "devtools/shared/protocol", true);
 loader.lazyRequireGetter(this, "Front", "devtools/shared/protocol", true);
 
-// Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
-const PLATFORM_MAJOR_VERSION = AppConstants.MOZ_APP_VERSION.match(/\d+/)[0];
-
-// Define the minimum officially supported version of Firefox when connecting to a remote
-// runtime. (Use ".0a1" to support the very first nightly version)
-// This matches the release channel's version when we are on nightly,
-// or 2 versions before when we are on other channels.
-const MIN_SUPPORTED_PLATFORM_VERSION = (PLATFORM_MAJOR_VERSION - 2) + ".0a1";
-
-const MS_PER_DAY = 86400000;
-
 /**
  * Creates a client for the remote debugging protocol server. This client
  * provides the means to communicate with the server and exchange the messages
  * required by the protocol in a traditional JavaScript API.
  */
 function DebuggerClient(transport) {
   this._transport = transport;
   this._transport.hooks = this;
@@ -197,90 +184,16 @@ DebuggerClient.prototype = {
       deferred.resolve([applicationType, traits]);
     });
 
     this._transport.ready();
     return deferred.promise;
   },
 
   /**
-   * Tells if the remote device is using a supported version of Firefox.
-   *
-   * @return Object with the following attributes:
-   *   * String incompatible
-   *            null if the runtime is compatible,
-   *            "too-recent" if the runtime uses a too recent version,
-   *            "too-old" if the runtime uses a too old version.
-   *   * String minVersion
-   *            The minimum supported version.
-   *   * String runtimeVersion
-   *            The remote runtime version.
-   *   * String localID
-   *            Build ID of local runtime. A date with like this: YYYYMMDD.
-   *   * String deviceID
-   *            Build ID of remote runtime. A date with like this: YYYYMMDD.
-   */
-  async checkRuntimeVersion() {
-    const localID = Services.appinfo.appBuildID.substr(0, 8);
-
-    let deviceFront;
-    try {
-      deviceFront = await this.mainRoot.getFront("device");
-    } catch (e) {
-      // On <FF55, getFront is going to call RootActor.getRoot and fail
-      // because this method doesn't exists.
-      if (e.error == "unrecognizedPacketType") {
-        return {
-          incompatible: "too-old",
-          minVersion: MIN_SUPPORTED_PLATFORM_VERSION,
-          runtimeVersion: "<55",
-          localID,
-          runtimeID: "?",
-        };
-      }
-      throw e;
-    }
-    const desc = await deviceFront.getDescription();
-    let incompatible = null;
-
-    // 1) Check for Firefox too recent on device.
-    // Compare device and firefox build IDs
-    // and only compare by day (strip hours/minutes) to prevent
-    // warning against builds of the same day.
-    const runtimeID = desc.appbuildid.substr(0, 8);
-    function buildIDToDate(buildID) {
-      const fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
-      // Date expects 0 - 11 for months
-      return new Date(fields[1], Number.parseInt(fields[2], 10) - 1, fields[3]);
-    }
-    const runtimeDate = buildIDToDate(runtimeID);
-    const localDate = buildIDToDate(localID);
-    // Allow device to be newer by up to a week.  This accommodates those with
-    // local device builds, since their devices will almost always be newer
-    // than the client.
-    if (runtimeDate - localDate > 7 * MS_PER_DAY) {
-      incompatible = "too-recent";
-    }
-
-    // 2) Check for too old Firefox on device
-    const platformversion = desc.platformversion;
-    if (Services.vc.compare(platformversion, MIN_SUPPORTED_PLATFORM_VERSION) < 0) {
-      incompatible = "too-old";
-    }
-
-    return {
-      incompatible,
-      minVersion: MIN_SUPPORTED_PLATFORM_VERSION,
-      runtimeVersion: platformversion,
-      localID,
-      runtimeID,
-    };
-  },
-
-  /**
    * Shut down communication with the debugging server.
    *
    * @param onClosed function
    *        If specified, will be called when the debugging connection
    *        has been closed. This parameter is deprecated - please use
    *        the returned Promise.
    * @return Promise
    *         Resolves after the underlying transport is closed.