Bug 1022460 - Add wifi tethering test cases. r=vyang
authorHenry Chang <hchang@mozilla.com>
Tue, 01 Jul 2014 18:47:13 +0800
changeset 193359 c3af6e7d15d034e368ab9591eac891a74b0bed46
parent 193358 d232b9ce6b9618a6adefe6bef7505cea85ee68b3
child 193360 bcbdd66957c4edc9745c45e6160f2dc32f8ae9f0
push id27116
push userryanvm@gmail.com
push dateThu, 10 Jul 2014 22:09:47 +0000
treeherdermozilla-central@0a0348d3f0c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvyang
bugs1022460
milestone33.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 1022460 - Add wifi tethering test cases. r=vyang
dom/wifi/test/marionette/head.js
dom/wifi/test/marionette/manifest.ini
dom/wifi/test/marionette/test_wifi_tethering_wifi_active.js
dom/wifi/test/marionette/test_wifi_tethering_wifi_disabled.js
dom/wifi/test/marionette/test_wifi_tethering_wifi_inactive.js
--- a/dom/wifi/test/marionette/head.js
+++ b/dom/wifi/test/marionette/head.js
@@ -1,19 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let Promise = SpecialPowers.Cu.import('resource://gre/modules/Promise.jsm').Promise;
 
 const STOCK_HOSTAPD_NAME = 'goldfish-hostapd';
-const HOSTAPD_CONFIG_PATH = '/data/misc/wifi/hostapd/';
+const HOSTAPD_CONFIG_PATH = '/data/misc/wifi/remote-hostapd/';
+
+const SETTINGS_RIL_DATA_ENABLED = 'ril.data.enabled';
+const SETTINGS_TETHERING_WIFI_ENABLED = 'tethering.wifi.enabled';
+const SETTINGS_TETHERING_WIFI_IP = 'tethering.wifi.ip';
+const SETTINGS_TETHERING_WIFI_SECURITY = 'tethering.wifi.security.type';
 
 const HOSTAPD_COMMON_CONFIG = {
   driver: 'test',
-  ctrl_interface: '/data/misc/wifi/hostapd',
+  ctrl_interface: '/data/misc/wifi/remote-hostapd',
   test_socket: 'DIR:/data/misc/wifi/sockets',
   hw_mode: 'b',
   channel: '2',
 };
 
 const HOSTAPD_CONFIG_LIST = [
   { ssid: 'ap0' },
 
@@ -34,16 +39,39 @@ let gTestSuite = (function() {
   let suite = {};
 
   // Private member variables of the returned object |suite|.
   let wifiManager;
   let wifiOrigEnabled;
   let pendingEmulatorShellCount = 0;
 
   /**
+   * A wrapper function of "is".
+   *
+   * Calls the marionette function "is" as well as throws an exception
+   * if the givens values are not equal.
+   *
+   * @param value1
+   *        Any type of value to compare.
+   *
+   * @param value2
+   *        Any type of value to compare.
+   *
+   * @param message
+   *        Debug message for this check.
+   *
+   */
+  function isOrThrow(value1, value2, message) {
+    is(value1, value2, message);
+    if (value1 !== value2) {
+      throw message;
+    }
+  }
+
+  /**
    * Send emulator shell command with safe guard.
    *
    * We should only call |finish()| after all emulator command transactions
    * end, so here comes with the pending counter.  Resolve when the emulator
    * gives positive response, and reject otherwise.
    *
    * Fulfill params:
    *   result -- an array of emulator response lines.
@@ -94,16 +122,44 @@ let gTestSuite = (function() {
       ok(true, "WifiManager event '" + aEventName + "' got.");
       deferred.resolve(aEvent);
     });
 
     return deferred.promise;
   }
 
   /**
+   * Wait for one named MozMobileConnection event.
+   *
+   * Resolve if that named event occurs.  Never reject.
+   *
+   * Fulfill params: the DOMEvent passed.
+   *
+   * @param aEventName
+   *        A string event name.
+   *
+   * @return A deferred promise.
+   */
+  function waitForMobileConnectionEventOnce(aEventName, aServiceId) {
+    aServiceId = aServiceId || 0;
+
+    let deferred = Promise.defer();
+    let mobileconnection = navigator.mozMobileConnections[aServiceId];
+
+    mobileconnection.addEventListener(aEventName, function onevent(aEvent) {
+      mobileconnection.removeEventListener(aEventName, onevent);
+
+      ok(true, "Mobile connection event '" + aEventName + "' got.");
+      deferred.resolve(aEvent);
+    });
+
+    return deferred.promise;
+  }
+
+  /**
    * Get the detail of currently running processes containing the given name.
    *
    * Use shell command 'ps' to get the desired process's detail. Never reject.
    *
    * Fulfill params:
    *   result -- an array of { pname, pid }
    *
    * @param aProcessName
@@ -146,17 +202,19 @@ let gTestSuite = (function() {
    * Fulfill params: (none)
    *
    * @return A deferred promise.
    */
   function addRequiredPermissions() {
     let deferred = Promise.defer();
 
     let permissions = [{ 'type': 'wifi-manage', 'allow': 1, 'context': window.document },
-                       { 'type': 'settings-write', 'allow': 1, 'context': window.document }];
+                       { 'type': 'settings-write', 'allow': 1, 'context': window.document },
+                       { 'type': 'settings-read', 'allow': 1, 'context': window.document },
+                       { 'type': 'mobileconnection', 'allow': 1, 'context': window.document }];
 
     SpecialPowers.pushPermissions(permissions, function() {
       deferred.resolve();
     });
 
     return deferred.promise;
   }
 
@@ -222,16 +280,81 @@ let gTestSuite = (function() {
   function requestWifiEnabled(aEnabled) {
     return Promise.all([
       waitForWifiManagerEventOnce(aEnabled ? 'enabled' : 'disabled'),
       setSettings({ 'wifi.enabled': aEnabled }),
     ]);
   }
 
   /**
+   * Wait for RIL data being connected.
+   *
+   * This function will check |MozMobileConnection.data.connected| on
+   * every 'datachange' event. Resolve when |MozMobileConnection.data.connected|
+   * becomes the expected state. Never reject.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aConnected
+   *        Boolean that indicates the desired data state.
+   *
+   * @param aServiceId [optional]
+   *        A numeric DSDS service id. Default: 0.
+   *
+   * @return A deferred promise.
+   */
+  function waitForRilDataConnected(aConnected, aServiceId) {
+    aServiceId = aServiceId || 0;
+    return waitForMobileConnectionEventOnce('datachange', aServiceId)
+      .then(function () {
+        let mobileconnection = navigator.mozMobileConnections[aServiceId];
+        if (mobileconnection.data.connected !== aConnected) {
+          return waitForRilDataConnected(aConnected, aServiceId);
+        }
+      });
+  }
+
+  /**
+   * Request to enable/disable wifi tethering.
+   *
+   * Enable/disable wifi tethering by changing the settings value 'tethering.wifi.enabled'.
+   * Resolve when the routing is verified to set up successfully in 20 seconds. The polling
+   * period is 1 second.
+   *
+   * Fulfill params: (none)
+   * Reject params: The error message.
+   *
+   * @param aEnabled
+   *        Boolean that indicates to enable or disable wifi tethering.
+   *
+   * @return A deferred promise.
+   */
+  function requestTetheringEnabled(aEnabled) {
+    let RETRY_INTERVAL_MS = 1000;
+    let retryCnt = 20;
+
+    return setSettings1(SETTINGS_TETHERING_WIFI_ENABLED, aEnabled)
+      .then(function waitForRoutingVerified() {
+        return verifyTetheringRouting(aEnabled)
+          .then(null, function onreject(aReason) {
+
+            log('verifyTetheringRouting rejected due to ' + aReason +
+                ' (' + retryCnt + ')');
+
+            if (!retryCnt--) {
+              throw aReason;
+            }
+
+            return waitForTimeout(RETRY_INTERVAL_MS).then(waitForRoutingVerified);
+          });
+      });
+  }
+
+  /**
    * Forget the given network.
    *
    * Resolve when we successfully forget the given network; reject when any error
    * occurs.
    *
    * Fulfill params: (none)
    * Reject params: (none)
    *
@@ -455,16 +578,71 @@ let gTestSuite = (function() {
       .then(function resolve() {
         ok(true, "setSettings(" + JSON.stringify(aSettings) + ")");
       }, function reject() {
         ok(aAllowError, "setSettings(" + JSON.stringify(aSettings) + ")");
       });
   }
 
   /**
+   * Set mozSettings value with only one key.
+   *
+   * Resolve if that mozSettings value is set successfully, reject otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aKey
+   *        A string key.
+   * @param aValue
+   *        An object value.
+   * @param aAllowError [optional]
+   *        A boolean value.  If set to true, an error response won't be treated
+   *        as test failure.  Default: false.
+   *
+   * @return A deferred promise.
+   */
+  function setSettings1(aKey, aValue, aAllowError) {
+    let settings = {};
+    settings[aKey] = aValue;
+    return setSettings(settings, aAllowError);
+  }
+
+  /**
+   * Get mozSettings value specified by @aKey.
+   *
+   * Resolve if that mozSettings value is retrieved successfully, reject
+   * otherwise.
+   *
+   * Fulfill params:
+   *   The corresponding mozSettings value of the key.
+   * Reject params: (none)
+   *
+   * @param aKey
+   *        A string.
+   * @param aAllowError [optional]
+   *        A boolean value.  If set to true, an error response won't be treated
+   *        as test failure.  Default: false.
+   *
+   * @return A deferred promise.
+   */
+  function getSettings(aKey, aAllowError) {
+    let request =
+      navigator.mozSettings.createLock().get(aKey);
+    return wrapDomRequestAsPromise(request)
+      .then(function resolve(aEvent) {
+        ok(true, "getSettings(" + aKey + ") - success");
+        return aEvent.target.result[aKey];
+      }, function reject(aEvent) {
+        ok(aAllowError, "getSettings(" + aKey + ") - error");
+      });
+  }
+
+
+  /**
    * Start hostapd processes with given configuration list.
    *
    * For starting one hostapd, we need to generate a specific config file
    * then launch a hostapd process with the confg file path passed. The
    * config file is generated by two sources: one is the common
    * part (HOSTAPD_COMMON_CONFIG) and the other is from the given |aConfigList|.
    * Resolve when all the hostpads are requested to start. It is not guaranteed
    * that all the hostapds will be up and running successfully. Never reject.
@@ -755,16 +933,169 @@ let gTestSuite = (function() {
           return;
         }
         throw 'Unexpected number of running processes:' + aProcessName +
               ', expected: ' + aExpectedNum + ', actual: ' + detail.length;
       });
   }
 
   /**
+   * Verify everything about routing when the wifi tethering is either on or off.
+   *
+   * We use two unix commands to verify the routing: 'netcfg' and 'ip route'.
+   * For now the following two things will be checked:
+   *   1) The default route interface should be 'rmnet0'.
+   *   2) wlan0 is up and its ip is set to a private subnet.
+   *
+   * We also verify iptables output as netd's NatController will execute
+   *   $ iptables -t nat -A POSTROUTING -o rmnet0 -j MASQUERADE
+   *
+   * Resolve when the verification is successful and reject otherwise.
+   *
+   * Fulfill params: (none)
+   * Reject params: String that indicates the reason of rejection.
+   *
+   * @return A deferred promise.
+   */
+  function verifyTetheringRouting(aEnabled) {
+    let netcfgResult = {};
+    let ipRouteResult = {};
+
+    // Execute 'netcfg' and parse to |netcfgResult|, each key of which is the
+    // interface name and value is { ip(string) }.
+    function exeAndParseNetcfg() {
+      return runEmulatorShellSafe(['netcfg'])
+        .then(function (aLines) {
+          // Sample output:
+          //
+          // lo       UP     127.0.0.1/8   0x00000049 00:00:00:00:00:00
+          // eth0     UP     10.0.2.15/24  0x00001043 52:54:00:12:34:56
+          // rmnet1   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:58
+          // rmnet2   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:59
+          // rmnet3   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:5a
+          // wlan0    UP     192.168.1.1/24  0x00001043 52:54:00:12:34:5b
+          // sit0     DOWN   0.0.0.0/0   0x00000080 00:00:00:00:00:00
+          // rmnet0   UP     10.0.2.100/24  0x00001043 52:54:00:12:34:57
+          //
+          aLines.forEach(function (aLine) {
+            let tokens = aLine.split(/\s+/);
+            if (tokens.length < 5) {
+              return;
+            }
+            let ifname = tokens[0];
+            let ip = (tokens[2].split('/'))[0];
+            netcfgResult[ifname] = { ip: ip };
+          });
+        });
+    }
+
+    // Execute 'ip route' and parse to |ipRouteResult|, each key of which is the
+    // interface name and value is { src(string), default(boolean) }.
+    function exeAndParseIpRoute() {
+      return runEmulatorShellSafe(['ip', 'route'])
+        .then(function (aLines) {
+          // Sample output:
+          //
+          // 10.0.2.4 via 10.0.2.2 dev rmnet0
+          // 10.0.2.3 via 10.0.2.2 dev rmnet0
+          // 192.168.1.0/24 dev wlan0  proto kernel  scope link  src 192.168.1.1
+          // 10.0.2.0/24 dev eth0  proto kernel  scope link  src 10.0.2.15
+          // 10.0.2.0/24 dev rmnet0  proto kernel  scope link  src 10.0.2.100
+          // default via 10.0.2.2 dev rmnet0
+          // default via 10.0.2.2 dev eth0  metric 2
+          //
+
+          // Parse source ip for each interface.
+          aLines.forEach(function (aLine) {
+            let tokens = aLine.trim().split(/\s+/);
+            let srcIndex = tokens.indexOf('src');
+            if (srcIndex < 0 || srcIndex + 1 >= tokens.length) {
+              return;
+            }
+            let ifname = tokens[2];
+            let src = tokens[srcIndex + 1];
+            ipRouteResult[ifname] = { src: src, default: false };
+          });
+
+          // Parse default interfaces.
+          aLines.forEach(function (aLine) {
+            let tokens = aLine.split(/\s+/);
+            if (tokens.length < 2) {
+              return;
+            }
+            if ('default' === tokens[0]) {
+              let ifnameIndex = tokens.indexOf('dev');
+              if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
+                return;
+              }
+              let ifname = tokens[ifnameIndex + 1];
+              if (ipRouteResult[ifname]) {
+                ipRouteResult[ifname].default = true;
+              }
+              return;
+            }
+          });
+
+        });
+
+    }
+
+    // Find MASQUERADE in POSTROUTING section. 'MASQUERADE' should be found
+    // when tethering is enabled. 'MASQUERADE' shouldn't be found when tethering
+    // is disabled.
+    function verifyIptables() {
+      return runEmulatorShellSafe(['iptables', '-t', 'nat', '-L', 'POSTROUTING'])
+        .then(function(aLines) {
+          // $ iptables -t nat -L POSTROUTING
+          //
+          // Sample output (tethering on):
+          //
+          // Chain POSTROUTING (policy ACCEPT)
+          // target     prot opt source               destination
+          // MASQUERADE  all  --  anywhere             anywhere
+          //
+          let found = (function find_MASQUERADE() {
+            // Skip first two lines.
+            for (let i = 2; i < aLines.length; i++) {
+              if (-1 !== aLines[i].indexOf('MASQUERADE')) {
+                return true;
+              }
+            }
+            return false;
+          })();
+
+          if ((aEnabled && !found) || (!aEnabled && found)) {
+            throw 'MASQUERADE' + (found ? '' : ' not') + ' found while tethering is ' +
+                  (aEnabled ? 'enabled' : 'disabled');
+          }
+        });
+    }
+
+    function verifyDefaultRouteAndIp(aExpectedWifiTetheringIp) {
+      log(JSON.stringify(ipRouteResult));
+      log(JSON.stringify(netcfgResult));
+
+      if (aEnabled) {
+        isOrThrow(ipRouteResult['rmnet0'].src, netcfgResult['rmnet0'].ip, 'rmnet0.ip');
+        isOrThrow(ipRouteResult['rmnet0'].default, true, 'rmnet0.default');
+
+        isOrThrow(ipRouteResult['wlan0'].src, netcfgResult['wlan0'].ip, 'wlan0.ip');
+        isOrThrow(ipRouteResult['wlan0'].src, aExpectedWifiTetheringIp, 'expected ip');
+        isOrThrow(ipRouteResult['wlan0'].default, false, 'wlan0.default');
+      }
+    }
+
+    return verifyIptables()
+      .then(exeAndParseNetcfg)
+      .then(exeAndParseIpRoute)
+      .then(() => getSettings(SETTINGS_TETHERING_WIFI_IP))
+      .then(ip => verifyDefaultRouteAndIp(ip));
+  }
+
+  /**
    * Clean up all the allocated resources and running services for the test.
    *
    * After the test no matter success or failure, we should
    * 1) Restore to the wifi original state (enabled or disabled)
    * 2) Wait until all pending emulator shell commands are done.
    *
    * |finsih| will be called in the end.
    *
@@ -824,16 +1155,18 @@ let gTestSuite = (function() {
   suite.testWifiScanWithRetry = testWifiScanWithRetry;
   suite.getFirstIndexBySsid = getFirstIndexBySsid;
   suite.testAssociate = testAssociate;
   suite.getKnownNetworks = getKnownNetworks;
   suite.requestWifiScan = requestWifiScan;
   suite.waitForConnected = waitForConnected;
   suite.forgetNetwork = forgetNetwork;
   suite.waitForTimeout = waitForTimeout;
+  suite.waitForRilDataConnected = waitForRilDataConnected;
+  suite.requestTetheringEnabled = requestTetheringEnabled;
 
   /**
    * Common test routine.
    *
    * Start a test with the given test case chain. The test environment will be
    * settled down before the test. After the test, all the affected things will
    * be restored.
    *
@@ -873,10 +1206,61 @@ let gTestSuite = (function() {
   suite.doTestWithoutStockAp = function(aTestCaseChain) {
     return suite.doTest(function() {
       return stopStockHostapd()
         .then(aTestCaseChain)
         .then(startStockHostapd);
     });
   };
 
+  /**
+   * The common test routine for wifi tethering.
+   *
+   * Similar as doTest except that it will set 'ril.data.enabled' to true
+   * before testing and restore it afterward. It will also verify 'ril.data.enabled'
+   * and 'tethering.wifi.enabled' to be false in the beginning. Note that this routine
+   * will NOT change the state of 'tethering.wifi.enabled' so the user should enable
+   * than disable on his/her own. This routine will only check if tethering is turned
+   * off after testing.
+   *
+   * Fulfill params: (none)
+   * Reject params: (none)
+   *
+   * @param aTestCaseChain
+   *        The test case entry point, which can be a function or a promise.
+   *
+   * @return A deferred promise.
+   */
+  suite.doTestTethering = function(aTestCaseChain) {
+
+    function verifyInitialState() {
+      return getSettings(SETTINGS_RIL_DATA_ENABLED)
+        .then(enabled => isOrThrow(enabled, false, SETTINGS_RIL_DATA_ENABLED))
+        .then(() => getSettings(SETTINGS_TETHERING_WIFI_ENABLED))
+        .then(enabled => isOrThrow(enabled, false, SETTINGS_TETHERING_WIFI_ENABLED));
+    }
+
+    function initTetheringTestEnvironment() {
+      // Enable ril data.
+      return Promise.all([waitForRilDataConnected(true),
+                          setSettings1(SETTINGS_RIL_DATA_ENABLED, true)])
+        .then(setSettings1(SETTINGS_TETHERING_WIFI_SECURITY, 'open'));
+    }
+
+    function restoreToInitialState() {
+      return setSettings1(SETTINGS_RIL_DATA_ENABLED, false)
+        .then(() => getSettings(SETTINGS_TETHERING_WIFI_ENABLED))
+        .then(enabled => is(enabled, false, 'Tethering should be turned off.'));
+    }
+
+    return suite.doTest(function() {
+      return verifyInitialState()
+        .then(initTetheringTestEnvironment)
+        .then(aTestCaseChain)
+        .then(restoreToInitialState, function onreject(aReason) {
+          return restoreToInitialState()
+            .then(() => { throw aReason; }); // Re-throw the orignal reject reason.
+        });
+    });
+  };
+
   return suite;
 })();
--- a/dom/wifi/test/marionette/manifest.ini
+++ b/dom/wifi/test/marionette/manifest.ini
@@ -3,8 +3,11 @@ b2g = true
 browser = false
 qemu = true
 
 [test_wifi_enable.js]
 [test_wifi_scan.js]
 [test_wifi_associate.js]
 [test_wifi_associate_wo_connect.js]
 [test_wifi_auto_connect.js]
+[test_wifi_tethering_wifi_disabled.js]
+[test_wifi_tethering_wifi_inactive.js]
+[test_wifi_tethering_wifi_active.js]
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_tethering_wifi_active.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+function connectToFirstNetwork() {
+  let firstNetwork;
+  return gTestSuite.requestWifiScan()
+    .then(function (networks) {
+      firstNetwork = networks[0];
+      return gTestSuite.testAssociate(firstNetwork);
+    })
+    .then(() => firstNetwork);
+}
+
+gTestSuite.doTestTethering(function() {
+  let firstNetwork;
+
+  return gTestSuite.ensureWifiEnabled(true)
+    .then(function () {
+      return Promise.all([
+        // 1) Set up the event listener first:
+        //    RIL data should become disconnected after connecting to wifi.
+        gTestSuite.waitForRilDataConnected(false),
+
+        // 2) Connect to the first scanned network.
+        connectToFirstNetwork()
+          .then(aFirstNetwork => firstNetwork = aFirstNetwork)
+      ]);
+    })
+    .then(function() {
+      return Promise.all([
+        // 1) Set up the event listeners first:
+        //    Wifi should be turned off and RIL data should be connected.
+        gTestSuite.waitForWifiManagerEventOnce('disabled'),
+        gTestSuite.waitForRilDataConnected(true),
+
+        // 2) Start wifi tethering.
+        gTestSuite.requestTetheringEnabled(true)
+      ]);
+    })
+    .then(function() {
+      return Promise.all([
+        // 1) Set up the event listeners first:
+        //    Wifi should be enabled, RIL data should become disconnected and
+        //    we should connect to an wifi AP.
+        gTestSuite.waitForWifiManagerEventOnce('enabled'),
+        gTestSuite.waitForRilDataConnected(false),
+        gTestSuite.waitForConnected(firstNetwork),
+
+        // 2) Stop wifi tethering.
+        gTestSuite.requestTetheringEnabled(false)
+      ]);
+    })
+    // Remove wlan0 from default route by disabling wifi. Otherwise,
+    // it will cause the subsequent test cases loading page error.
+    .then(() => gTestSuite.requestWifiEnabled(false));
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_tethering_wifi_disabled.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+gTestSuite.doTestTethering(function() {
+  return gTestSuite.ensureWifiEnabled(false)
+    .then(() => gTestSuite.requestTetheringEnabled(true))
+    .then(() => gTestSuite.requestTetheringEnabled(false))
+});
new file mode 100644
--- /dev/null
+++ b/dom/wifi/test/marionette/test_wifi_tethering_wifi_inactive.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+MARIONETTE_TIMEOUT = 60000;
+MARIONETTE_HEAD_JS = 'head.js';
+
+gTestSuite.doTestTethering(function() {
+  return gTestSuite.ensureWifiEnabled(true)
+    .then(function() {
+      return Promise.all([
+        gTestSuite.waitForWifiManagerEventOnce('disabled'),
+        gTestSuite.requestTetheringEnabled(true)
+      ]);
+    })
+    .then(function() {
+      return Promise.all([
+        gTestSuite.waitForWifiManagerEventOnce('enabled'),
+        gTestSuite.requestTetheringEnabled(false)
+      ]);
+    });
+});