Bug 1440714 - Convert Android marionette tests to use adb.py; r=bc
authorGeoff Brown <gbrown@mozilla.com>
Thu, 26 Apr 2018 10:43:08 -0600
changeset 460796 a79cd8dc4c1c802cb7768e863584c951eb38f6d6
parent 460795 583940ed5be79dbc03ff7d148194ce28fc5eedd4
child 460797 9d1c3d342559c09b232cb43850eea2d66c7010b0
push id165
push userfmarier@mozilla.com
push dateMon, 30 Apr 2018 23:50:51 +0000
reviewersbc
bugs1440714
milestone61.0a1
Bug 1440714 - Convert Android marionette tests to use adb.py; r=bc
testing/marionette/client/marionette_driver/geckoinstance.py
testing/marionette/harness/marionette_harness/runner/base.py
testing/mozbase/mozrunner/mozrunner/application.py
testing/mozbase/mozrunner/mozrunner/base/device.py
testing/mozbase/mozrunner/mozrunner/devices/base.py
testing/mozbase/mozrunner/mozrunner/devices/emulator.py
testing/mozbase/mozrunner/mozrunner/runners.py
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -9,17 +9,16 @@ import sys
 import tempfile
 import time
 import traceback
 
 from copy import deepcopy
 
 import mozversion
 
-from mozdevice import DMError
 from mozprofile import Profile
 from mozrunner import Runner, FennecEmulatorRunner
 from six import reraise
 
 from . import errors
 
 
 # ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A MARIONETTE PEER!
@@ -436,26 +435,26 @@ class FennecInstance(GeckoInstance):
             self.runner.start()
         except Exception as e:
             exc, val, tb = sys.exc_info()
             message = "Error possibly due to runner or device args: {}"
             reraise(exc, message.format(e.message), tb)
         # gecko_log comes from logcat when running with device/emulator
         logcat_args = {
             "filterspec": "Gecko",
-            "serial": self.runner.device.dm._deviceSerial
+            "serial": self.runner.device.app_ctx.device_serial
         }
         if self.gecko_log == "-":
             logcat_args["stream"] = sys.stdout
         else:
             logcat_args["logfile"] = self.gecko_log
         self.runner.device.start_logcat(**logcat_args)
 
         # forward marionette port (localhost:2828)
-        self.runner.device.dm.forward(
+        self.runner.device.device.forward(
             local="tcp:{}".format(self.marionette_port),
             remote="tcp:{}".format(self.marionette_port))
 
     def _get_runner_args(self):
         process_args = {
             "processOutputLine": [NullOutput()],
         }
 
@@ -483,20 +482,20 @@ class FennecInstance(GeckoInstance):
         If `clean` is True and the Fennec instance is running in an
         emulator managed by mozrunner, this will stop the emulator.
 
         :param clean: If True, also perform runner cleanup.
         """
         super(FennecInstance, self).close(clean)
         if clean and self.runner and self.runner.device.connected:
             try:
-                self.runner.device.dm.remove_forward(
+                self.runner.device.device.remove_forwards(
                     "tcp:{}".format(self.marionette_port))
                 self.unresponsive_count = 0
-            except DMError:
+            except Exception:
                 self.unresponsive_count += 1
                 traceback.print_exception(*sys.exc_info())
 
 
 class DesktopInstance(GeckoInstance):
     desktop_prefs = {
         # Disable application updates
         "app.update.enabled": False,
--- a/testing/marionette/harness/marionette_harness/runner/base.py
+++ b/testing/marionette/harness/marionette_harness/runner/base.py
@@ -867,17 +867,17 @@ class BaseMarionetteTestRunner(object):
             # backwards compatibility
             self.marionette.baseurl = serve.where_is("/")
 
         self._add_tests(tests)
 
         device_info = None
         if self.marionette.instance and self.emulator:
             try:
-                device_info = self.marionette.instance.runner.device.dm.getInfo()
+                device_info = self.marionette.instance.runner.device.device.get_info()
             except Exception:
                 self.logger.warning('Could not get device info', exc_info=True)
 
         self.marionette.start_session()
         self.logger.info("e10s is {}".format("enabled" if self.is_e10s else "disabled"))
         if self.e10s != self.is_e10s:
             self.cleanup()
             raise AssertionError("BaseMarionetteTestRunner configuration (self.e10s) "
--- a/testing/mozbase/mozrunner/mozrunner/application.py
+++ b/testing/mozbase/mozrunner/mozrunner/application.py
@@ -4,17 +4,17 @@
 
 from __future__ import absolute_import
 
 from abc import ABCMeta, abstractmethod
 from distutils.spawn import find_executable
 import os
 import posixpath
 
-from mozdevice import DeviceManagerADB, DroidADB
+from mozdevice import ADBAndroid
 from mozprofile import (
     Profile,
     ChromeProfile,
     FirefoxProfile,
     ThunderbirdProfile
 )
 
 here = os.path.abspath(os.path.dirname(__file__))
@@ -34,21 +34,20 @@ def get_app_context(appname):
 
 
 class DefaultContext(object):
     profile_class = Profile
 
 
 class RemoteContext(object):
     __metaclass__ = ABCMeta
-    _dm = None
+    device = None
     _remote_profile = None
     _adb = None
     profile_class = Profile
-    dm_class = DeviceManagerADB
     _bindir = None
     remote_test_root = ''
     remote_process = None
 
     @property
     def bindir(self):
         if self._bindir is None:
             paths = [find_executable('emulator')]
@@ -69,22 +68,16 @@ class RemoteContext(object):
             if not paths:
                 raise OSError(
                     'Could not find the adb binary, make sure it is on your'
                     'path or set the $ADB_PATH environment variable.')
             self._adb = paths[0]
         return self._adb
 
     @property
-    def dm(self):
-        if not self._dm:
-            self._dm = self.dm_class(adbPath=self.adb, autoconnect=False)
-        return self._dm
-
-    @property
     def remote_profile(self):
         if not self._remote_profile:
             self._remote_profile = posixpath.join(self.remote_test_root,
                                                   'profile')
         return self._remote_profile
 
     def which(self, binary):
         paths = os.environ.get('PATH', {}).split(os.pathsep)
@@ -99,36 +92,37 @@ class RemoteContext(object):
         """ Run (device manager) command to stop application. """
         pass
 
 
 class FennecContext(RemoteContext):
     _remote_profiles_ini = None
     _remote_test_root = None
 
-    def __init__(self, app=None, adb_path=None, avd_home=None):
+    def __init__(self, app=None, adb_path=None, avd_home=None, device_serial=None):
         self._adb = adb_path
         self.avd_home = avd_home
-        self.dm_class = DroidADB
-        self.remote_process = app or self.dm._packageName
+        self.remote_process = app
+        self.device_serial = device_serial
+        self.device = ADBAndroid(adb=self.adb, device=device_serial)
 
     def stop_application(self):
-        self.dm.stopApplication(self.remote_process)
+        self.device.stop_application(self.remote_process)
 
     @property
     def remote_test_root(self):
         if not self._remote_test_root:
-            self._remote_test_root = self.dm.getDeviceRoot()
+            self._remote_test_root = self.device.test_root
         return self._remote_test_root
 
     @property
     def remote_profiles_ini(self):
         if not self._remote_profiles_ini:
             self._remote_profiles_ini = posixpath.join(
-                self.dm.getAppRoot(self.remote_process),
+                '/data', 'data', self.remote_process,
                 'files', 'mozilla', 'profiles.ini'
             )
         return self._remote_profiles_ini
 
 
 class FirefoxContext(object):
     profile_class = FirefoxProfile
 
--- a/testing/mozbase/mozrunner/mozrunner/base/device.py
+++ b/testing/mozbase/mozrunner/mozrunner/base/device.py
@@ -52,18 +52,18 @@ class DeviceRunner(BaseRunner):
         BaseRunner.__init__(self, **kwargs)
 
         device_args = device_args or {}
         self.device = device_class(**device_args)
 
     @property
     def command(self):
         cmd = [self.app_ctx.adb]
-        if self.app_ctx.dm._deviceSerial:
-            cmd.extend(['-s', self.app_ctx.dm._deviceSerial])
+        if self.app_ctx.device_serial:
+            cmd.extend(['-s', self.app_ctx.device_serial])
         cmd.append('shell')
         for k, v in self._device_env.iteritems():
             cmd.append('%s=%s' % (k, v))
         cmd.append(self.app_ctx.remote_binary)
         return cmd
 
     def start(self, *args, **kwargs):
         if isinstance(self.device, BaseEmulator) and not self.device.connected:
@@ -80,28 +80,28 @@ class DeviceRunner(BaseRunner):
             raise Exception("Network did not come up when starting device")
 
         pid = BaseRunner.start(self, *args, **kwargs)
 
         timeout = 10  # seconds
         end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout)
         while not self.is_running() and datetime.datetime.now() < end_time:
             time.sleep(.1)
-        else:
+        if not self.is_running():
             print("timed out waiting for '%s' process to start" % self.app_ctx.remote_process)
 
         if not self.device.wait_for_net():
             raise Exception("Failed to get a network connection")
         return pid
 
     def stop(self, sig=None):
         if self.is_running():
             timeout = 10
 
-            self.app_ctx.dm.killProcess(self.app_ctx.remote_process, sig=sig)
+            self.app_ctx.device.pkill(self.app_ctx.remote_process, sig=sig)
             if self.wait(timeout) is None and sig is not None:
                 print("timed out waiting for '%s' process to exit, trying "
                       "without signal {}".format(
                           self.app_ctx.remote_process, sig))
 
             # need to call adb stop otherwise the system will attempt to
             # restart the process
             self.app_ctx.stop_application()
@@ -111,20 +111,20 @@ class DeviceRunner(BaseRunner):
 
     @property
     def returncode(self):
         """The returncode of the remote process.
 
         A value of None indicates the process is still running. Otherwise 0 is
         returned, because there is no known way yet to retrieve the real exit code.
         """
-        if self.app_ctx.dm.processExist(self.app_ctx.remote_process) is None:
-            return 0
+        if self.app_ctx.device.process_exist(self.app_ctx.remote_process):
+            return None
 
-        return None
+        return 0
 
     def wait(self, timeout=None):
         """Wait for the remote process to exit.
 
         :param timeout: if not None, will return after timeout seconds.
 
         :returns: the process return code or None if timeout was reached
                   and the process is still running.
@@ -181,18 +181,18 @@ class FennecRunner(DeviceRunner):
 
     def __init__(self, cmdargs=None, **kwargs):
         super(FennecRunner, self).__init__(**kwargs)
         self.cmdargs = cmdargs or []
 
     @property
     def command(self):
         cmd = [self.app_ctx.adb]
-        if self.app_ctx.dm._deviceSerial:
-            cmd.extend(["-s", self.app_ctx.dm._deviceSerial])
+        if self.app_ctx.device_serial:
+            cmd.extend(["-s", self.app_ctx.device_serial])
         cmd.append("shell")
         app = "%s/org.mozilla.gecko.BrowserApp" % self.app_ctx.remote_process
         am_subcommand = ["am", "start", "-a", "android.activity.MAIN", "-n", app]
         app_params = ["-no-remote", "-profile", self.app_ctx.remote_profile]
         app_params.extend(self.cmdargs)
         am_subcommand.extend(["--es", "args", "'%s'" % " ".join(app_params)])
         # Append env variables in the form |--es env0 MOZ_CRASHREPORTER=1|
         for (count, (k, v)) in enumerate(self._device_env.iteritems()):
--- a/testing/mozbase/mozrunner/mozrunner/devices/base.py
+++ b/testing/mozbase/mozrunner/mozrunner/devices/base.py
@@ -9,44 +9,44 @@ import datetime
 import os
 import posixpath
 import re
 import shutil
 import subprocess
 import tempfile
 import time
 
-from mozdevice import DMError
+from mozdevice import ADBHost, ADBError
 from mozprocess import ProcessHandler
 
 
 class Device(object):
     connected = False
     logcat_proc = None
 
     def __init__(self, app_ctx, logdir=None, serial=None, restore=True):
         self.app_ctx = app_ctx
-        self.dm = self.app_ctx.dm
+        self.device = self.app_ctx.device
         self.restore = restore
         self.serial = serial
         self.logdir = os.path.abspath(os.path.expanduser(logdir))
         self.added_files = set()
         self.backup_files = set()
 
     @property
     def remote_profiles(self):
         """
         A list of remote profiles on the device.
         """
         remote_ini = self.app_ctx.remote_profiles_ini
-        if not self.dm.fileExists(remote_ini):
+        if not self.device.is_file(remote_ini):
             raise IOError("Remote file '%s' not found" % remote_ini)
 
         local_ini = tempfile.NamedTemporaryFile()
-        self.dm.getFile(remote_ini, local_ini.name)
+        self.device.pull(remote_ini, local_ini.name)
         cfg = ConfigParser()
         cfg.read(local_ini.name)
 
         profiles = []
         for section in cfg.sections():
             if cfg.has_option(section, 'Path'):
                 if cfg.has_option(section, 'IsRelative') and cfg.getint(section, 'IsRelative'):
                     profiles.append(posixpath.join(posixpath.dirname(remote_ini),
@@ -58,100 +58,102 @@ class Device(object):
     def pull_minidumps(self):
         """
         Saves any minidumps found in the remote profile on the local filesystem.
 
         :returns: Path to directory containing the dumps.
         """
         remote_dump_dir = posixpath.join(self.app_ctx.remote_profile, 'minidumps')
         local_dump_dir = tempfile.mkdtemp()
-        self.dm.getDirectory(remote_dump_dir, local_dump_dir)
+        try:
+            self.device.pull(remote_dump_dir, local_dump_dir)
+        except ADBError as e:
+            # OK if directory not present -- sometimes called before browser start
+            if 'does not exist' not in str(e):
+                raise
         if os.listdir(local_dump_dir):
-            for f in self.dm.listFiles(remote_dump_dir):
-                self.dm.removeFile(posixpath.join(remote_dump_dir, f))
+            self.device.rm(remote_dump_dir, recursive=True)
         return local_dump_dir
 
     def setup_profile(self, profile):
         """
         Copy profile to the device and update the remote profiles.ini
         to point to the new profile.
 
         :param profile: mozprofile object to copy over.
         """
-        self.dm.remount()
+        if self.device.is_dir(self.app_ctx.remote_profile):
+            self.device.rm(self.app_ctx.remote_profile, recursive=True)
 
-        if self.dm.dirExists(self.app_ctx.remote_profile):
-            self.dm.shellCheckOutput(['rm', '-r', self.app_ctx.remote_profile])
-
-        self.dm.pushDir(profile.profile, self.app_ctx.remote_profile)
+        self.device.push(profile.profile, self.app_ctx.remote_profile)
 
         timeout = 5  # seconds
         starttime = datetime.datetime.now()
         while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
-            if self.dm.fileExists(self.app_ctx.remote_profiles_ini):
+            if self.device.is_file(self.app_ctx.remote_profiles_ini):
                 break
             time.sleep(1)
-        else:
+        local_profiles_ini = tempfile.NamedTemporaryFile()
+        if not self.device.is_file(self.app_ctx.remote_profiles_ini):
+            # Unless fennec is already running, and/or remote_profiles_ini is
+            # not inside the remote_profile (deleted above), this is entirely
+            # normal.
             print("timed out waiting for profiles.ini")
-
-        local_profiles_ini = tempfile.NamedTemporaryFile()
-        self.dm.getFile(self.app_ctx.remote_profiles_ini, local_profiles_ini.name)
+        else:
+            self.device.pull(self.app_ctx.remote_profiles_ini, local_profiles_ini.name)
 
         config = ProfileConfigParser()
         config.read(local_profiles_ini.name)
         for section in config.sections():
             if 'Profile' in section:
                 config.set(section, 'IsRelative', 0)
                 config.set(section, 'Path', self.app_ctx.remote_profile)
 
         new_profiles_ini = tempfile.NamedTemporaryFile()
         config.write(open(new_profiles_ini.name, 'w'))
 
         self.backup_file(self.app_ctx.remote_profiles_ini)
-        self.dm.pushFile(new_profiles_ini.name, self.app_ctx.remote_profiles_ini)
+        self.device.push(new_profiles_ini.name, self.app_ctx.remote_profiles_ini)
 
         # Ideally all applications would read the profile the same way, but in practice
         # this isn't true. Perform application specific profile-related setup if necessary.
         if hasattr(self.app_ctx, 'setup_profile'):
             for remote_path in self.app_ctx.remote_backup_files:
                 self.backup_file(remote_path)
             self.app_ctx.setup_profile(profile)
 
     def _get_online_devices(self):
-        return [d[0] for d in self.dm.devices()
-                if d[1] != 'offline'
-                if not d[0].startswith('emulator')]
+        adbhost = ADBHost()
+        devices = adbhost.devices()
+        return [d['device_serial'] for d in devices
+                if d['state'] != 'offline'
+                if not d['device_serial'].startswith('emulator')]
 
     def connect(self):
         """
         Connects to a running device. If no serial was specified in the
         constructor, defaults to the first entry in `adb devices`.
         """
         if self.connected:
             return
 
-        if self.serial:
-            serial = self.serial
-        else:
-            online_devices = self._get_online_devices()
-            if not online_devices:
-                raise IOError("No devices connected. Ensure the device is on and "
-                              "remote debugging via adb is enabled in the settings.")
-            serial = online_devices[0]
+        online_devices = self._get_online_devices()
+        if not online_devices:
+            raise IOError("No devices connected. Ensure the device is on and "
+                          "remote debugging via adb is enabled in the settings.")
+        self.serial = online_devices[0]
 
-        self.dm._deviceSerial = serial
-        self.dm.connect()
         self.connected = True
 
         if self.logdir:
             # save logcat
-            logcat_log = os.path.join(self.logdir, '%s.log' % serial)
+            logcat_log = os.path.join(self.logdir, '%s.log' % self.serial)
             if os.path.isfile(logcat_log):
                 self._rotate_log(logcat_log)
-            self.logcat_proc = self.start_logcat(serial, logfile=logcat_log)
+            self.logcat_proc = self.start_logcat(self.serial, logfile=logcat_log)
 
     def start_logcat(self, serial, logfile=None, stream=None, filterspec=None):
         logcat_args = [self.app_ctx.adb, '-s', '%s' % serial,
                        'logcat', '-v', 'time', '-b', 'main', '-b', 'radio']
         # only log filterspec
         if filterspec:
             logcat_args.extend(['-s', filterspec])
         process_args = {}
@@ -162,35 +164,17 @@ class Device(object):
         proc = ProcessHandler(logcat_args, **process_args)
         proc.run()
         return proc
 
     def reboot(self):
         """
         Reboots the device via adb.
         """
-        self.dm.reboot(wait=True)
-
-    def install_busybox(self, busybox):
-        """
-        Installs busybox on the device.
-
-        :param busybox: Path to busybox binary to install.
-        """
-        self.dm.remount()
-        print('pushing %s' % self.app_ctx.remote_busybox)
-        self.dm.pushFile(busybox, self.app_ctx.remote_busybox, retryLimit=10)
-        # TODO for some reason using dm.shellCheckOutput doesn't work,
-        #      while calling adb shell directly does.
-        args = [self.app_ctx.adb, '-s', self.dm._deviceSerial,
-                'shell', 'cd /system/bin; chmod 555 busybox;'
-                'for x in `./busybox --list`; do ln -s ./busybox $x; done']
-        adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        adb.wait()
-        self.dm._verifyZip()
+        self.device.reboot()
 
     def wait_for_net(self):
         active = False
         time_out = 0
         while not active and time_out < 40:
             proc = subprocess.Popen([self.app_ctx.adb, 'shell', '/system/bin/netcfg'],
                                     stdout=subprocess.PIPE)
             proc.stdout.readline()  # ignore first line
@@ -203,50 +187,47 @@ class Device(object):
             time_out += 1
             time.sleep(1)
         return active
 
     def backup_file(self, remote_path):
         if not self.restore:
             return
 
-        if self.dm.fileExists(remote_path) or self.dm.dirExists(remote_path):
-            self.dm.copyTree(remote_path, '%s.orig' % remote_path)
+        if self.device.exists(remote_path):
+            self.device.cp(remote_path, '%s.orig' % remote_path, recursive=True)
             self.backup_files.add(remote_path)
         else:
             self.added_files.add(remote_path)
 
     def cleanup(self):
         """
         Cleanup the device.
         """
         if not self.restore:
             return
 
         try:
-            self.dm._verifyDevice()
-        except DMError:
-            return
+            self.device.remount()
+            # Restore the original profile
+            for added_file in self.added_files:
+                self.device.rm(added_file)
 
-        self.dm.remount()
-        # Restore the original profile
-        for added_file in self.added_files:
-            self.dm.removeFile(added_file)
+            for backup_file in self.backup_files:
+                if self.device.exists('%s.orig' % backup_file):
+                    self.device.mv('%s.orig' % backup_file, backup_file)
 
-        for backup_file in self.backup_files:
-            if self.dm.fileExists('%s.orig' % backup_file) or \
-               self.dm.dirExists('%s.orig' % backup_file):
-                self.dm.moveTree('%s.orig' % backup_file, backup_file)
+            # Perform application specific profile cleanup if necessary
+            if hasattr(self.app_ctx, 'cleanup_profile'):
+                self.app_ctx.cleanup_profile()
 
-        # Perform application specific profile cleanup if necessary
-        if hasattr(self.app_ctx, 'cleanup_profile'):
-            self.app_ctx.cleanup_profile()
-
-        # Remove the test profile
-        self.dm.removeDir(self.app_ctx.remote_profile)
+            # Remove the test profile
+            self.device.rm(self.app_ctx.remote_profile, recursive=True)
+        except Exception as e:
+            print("cleanup aborted: %s" % str(e))
 
     def _rotate_log(self, srclog, index=1):
         """
         Rotate a logfile, by recursively rotating logs further in the sequence,
         deleting the last file if necessary.
         """
         basename = os.path.basename(srclog)
         basename = basename[:-len('.log')]
--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
@@ -7,16 +7,17 @@ from __future__ import absolute_import
 from telnetlib import Telnet
 import datetime
 import os
 import shutil
 import subprocess
 import tempfile
 import time
 
+from mozdevice import ADBHost
 from mozprocess import ProcessHandler
 
 from .base import Device
 from .emulator_battery import EmulatorBattery
 from .emulator_geo import EmulatorGeo
 from .emulator_screen import EmulatorScreen
 from ..errors import TimeoutException
 
@@ -136,30 +137,30 @@ class BaseEmulator(Device):
                 raise TimeoutException(
                     'timed out waiting for emulator to start')
             devices = set(self._get_online_devices())
         devices = devices - original_devices
         self.serial = devices.pop()
         self.connect()
 
     def _get_online_devices(self):
-        return [d[0] for d in self.dm.devices() if d[1] != 'offline' if
-                d[0].startswith('emulator')]
+        adbhost = ADBHost()
+        return [d['device_serial'] for d in adbhost.devices() if d['state'] != 'offline' if
+                d['device_serial'].startswith('emulator')]
 
     def connect(self):
         """
         Connects to a running device. If no serial was specified in the
         constructor, defaults to the first entry in `adb devices`.
         """
         if self.connected:
             return
 
         super(BaseEmulator, self).connect()
-        serial = self.serial or self.dm._deviceSerial
-        self.port = int(serial[serial.rindex('-') + 1:])
+        self.port = int(self.serial[self.serial.rindex('-') + 1:])
 
     def cleanup(self):
         """
         Cleans up and kills the emulator, if it was started by mozrunner.
         """
         super(BaseEmulator, self).cleanup()
         if self.proc:
             self.proc.kill()
--- a/testing/mozbase/mozrunner/mozrunner/runners.py
+++ b/testing/mozbase/mozrunner/mozrunner/runners.py
@@ -107,21 +107,21 @@ def FennecEmulatorRunner(avd='mozemulato
     :param binary: Path to emulator binary.
         Defaults to None, which causes the device_class to guess based on PATH.
     :param app: Name of Fennec app (often org.mozilla.fennec_$USER)
         Defaults to 'org.mozilla.fennec'
     :param cmdargs: Arguments to pass into binary.
     :returns: A DeviceRunner for Android emulators.
     """
     kwargs['app_ctx'] = get_app_context('fennec')(app, adb_path=adb_path,
-                                                  avd_home=avd_home)
+                                                  avd_home=avd_home,
+                                                  device_serial=serial)
     device_args = {'app_ctx': kwargs['app_ctx'],
                    'avd': avd,
                    'binary': binary,
-                   'serial': serial,
                    'logdir': logdir}
     return FennecRunner(device_class=EmulatorAVD,
                         device_args=device_args,
                         **kwargs)
 
 
 runners = {
     'chrome': ChromeRunner,