Bug 787203 - Add emulator and Fennec support to Marionette harness and client; r?AutomatedTester draft
authorMaja Frydrychowicz <mjzffr@gmail.com>
Tue, 05 Jul 2016 14:09:48 -0400
changeset 384165 8a46ec066ec3223ec3d9aa60ddcadb00772180e9
parent 384164 35b9e271b1a5c7161d0981d0aa61fb7607f56d94
child 384166 e85c5bd226497e542a8009dd6979ffe540c95b25
push id22185
push usermjzffr@gmail.com
push dateTue, 05 Jul 2016 18:12:09 +0000
reviewersAutomatedTester
bugs787203
milestone50.0a1
Bug 787203 - Add emulator and Fennec support to Marionette harness and client; r?AutomatedTester Add FennecInstance. Refactor GeckoInstance as well as other related clean-up. Marionette runner can now launch both Android emulator and Fennec, or connect to a running emulator and then launch Fennec. (The runner can also connect to an already running Fennec instance provided that Marionette and adb port forwarding have previously been enabled.) MozReview-Commit-ID: BrjFjld86xq
testing/marionette/client/marionette_driver/geckoinstance.py
testing/marionette/client/marionette_driver/marionette.py
testing/marionette/harness/marionette/runner/base.py
testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
--- a/testing/marionette/client/marionette_driver/geckoinstance.py
+++ b/testing/marionette/client/marionette_driver/geckoinstance.py
@@ -5,17 +5,17 @@
 import os
 import sys
 import tempfile
 import time
 
 from copy import deepcopy
 
 from mozprofile import Profile
-from mozrunner import Runner
+from mozrunner import Runner, FennecEmulatorRunner
 
 
 class GeckoInstance(object):
     required_prefs = {
         "browser.sessionstore.resume_from_crash": False,
         "browser.shell.checkDefaultBrowser": False,
         "browser.startup.page": 0,
         "browser.tabs.remote.autostart.1": False,
@@ -42,51 +42,63 @@ class GeckoInstance(object):
         # Until Bug 1238095 is fixed, we have to enable CPOWs in order
         # for Marionette tests to work properly.
         "dom.ipc.cpows.forbid-unsafe-from-browser": False,
     }
 
     def __init__(self, host, port, bin, profile=None, addons=None,
                  app_args=None, symbols_path=None, gecko_log=None, prefs=None,
                  workspace=None, verbose=0):
+        self.runner_class = Runner
+        self.app_args = app_args or []
+        self.runner = None
+        self.symbols_path = symbols_path
+        self.binary = bin
+
         self.marionette_host = host
         self.marionette_port = port
-        self.bin = bin
         # Alternative to default temporary directory
         self.workspace = workspace
+        self.addons = addons
         # Check if it is a Profile object or a path to profile
         self.profile = None
-        self.addons = addons
         if isinstance(profile, Profile):
             self.profile = profile
         else:
             self.profile_path = profile
         self.prefs = prefs
-        self.required_prefs = deepcopy(GeckoInstance.required_prefs)
+        self.required_prefs = deepcopy(self.required_prefs)
         if prefs:
             self.required_prefs.update(prefs)
-        self.app_args = app_args or []
-        self.runner = None
-        self.symbols_path = symbols_path
 
-        if gecko_log != '-':
-            if gecko_log is None:
-                gecko_log = 'gecko.log'
-            elif os.path.isdir(gecko_log):
-                fname = 'gecko-%d.log' % time.time()
-                gecko_log = os.path.join(gecko_log, fname)
-
-            gecko_log = os.path.realpath(gecko_log)
-            if os.access(gecko_log, os.F_OK):
-                os.remove(gecko_log)
-
-        self.gecko_log = gecko_log
+        self._gecko_log_option = gecko_log
+        self._gecko_log = None
         self.verbose = verbose
 
-    def start(self):
+    @property
+    def gecko_log(self):
+        if self._gecko_log:
+            return self._gecko_log
+
+        path = self._gecko_log_option
+        if path != '-':
+            if path is None:
+                path = 'gecko.log'
+            elif os.path.isdir(path):
+                fname = 'gecko-%d.log' % time.time()
+                path = os.path.join(path, fname)
+
+            path = os.path.realpath(path)
+            if os.access(path, os.F_OK):
+                os.remove(path)
+
+        self._gecko_log = path
+        return self._gecko_log
+
+    def _update_profile(self):
         profile_args = {"preferences": deepcopy(self.required_prefs)}
         profile_args["preferences"]["marionette.defaultPrefs.port"] = self.marionette_port
         if self.prefs:
             profile_args["preferences"].update(self.prefs)
         if self.verbose:
             level = "TRACE" if self.verbose >= 2 else "DEBUG"
             profile_args["preferences"]["marionette.logging"] = level
         if '-jsdebugger' in self.app_args:
@@ -113,39 +125,46 @@ class GeckoInstance(object):
                     os.path.basename(self.profile_path),
                     time.time()
                 )
                 if self.workspace:
                     profile_args["path_to"] = os.path.join(self.workspace,
                                                            profile_name)
                 self.profile = Profile.clone(**profile_args)
 
+    def start(self):
+        self._update_profile()
+        self.runner = self.runner_class(**self._get_runner_args())
+        self.runner.start()
+
+    def _get_runner_args(self):
         process_args = {
             'processOutputLine': [NullOutput()],
         }
 
         if self.gecko_log == '-':
             process_args['stream'] = sys.stdout
         else:
             process_args['logfile'] = self.gecko_log
 
         env = os.environ.copy()
 
         # environment variables needed for crashreporting
         # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
         env.update({'MOZ_CRASHREPORTER': '1',
                     'MOZ_CRASHREPORTER_NO_REPORT': '1'})
-        self.runner = Runner(
-            binary=self.bin,
-            profile=self.profile,
-            cmdargs=['-no-remote', '-marionette'] + self.app_args,
-            env=env,
-            symbols_path=self.symbols_path,
-            process_args=process_args)
-        self.runner.start()
+
+        return {
+            'binary': self.binary,
+            'profile': self.profile,
+            'cmdargs': ['-no-remote', '-marionette'] + self.app_args,
+            'env': env,
+            'symbols_path': self.symbols_path,
+            'process_args': process_args
+        }
 
     def close(self, restart=False):
         if not restart:
             self.profile = None
 
         if self.runner:
             self.runner.stop()
             self.runner.cleanup()
@@ -159,16 +178,103 @@ class GeckoInstance(object):
 
         if prefs:
             self.prefs = prefs
         else:
             self.prefs = None
         self.start()
 
 
+class FennecInstance(GeckoInstance):
+    def __init__(self, emulator_binary=None, avd_home=None, avd=None,
+                 adb_path=None, serial=None, connect_to_running_emulator=False,
+                 *args, **kwargs):
+        super(FennecInstance, self).__init__(*args, **kwargs)
+        self.runner_class = FennecEmulatorRunner
+        # runner args
+        self._package_name = None
+        self.emulator_binary = emulator_binary
+        self.avd_home = avd_home
+        self.adb_path = adb_path
+        self.avd = avd
+        self.serial = serial
+        self.connect_to_running_emulator = connect_to_running_emulator
+
+    @property
+    def package_name(self):
+        """
+        Name of app to run on emulator.
+
+        Note that FennecInstance does not use self.binary
+        """
+        if self._package_name is None:
+            self._package_name = 'org.mozilla.fennec'
+            user = os.getenv('USER')
+            if user:
+                self._package_name += '_' + user
+        return self._package_name
+
+    def start(self):
+        self._update_profile()
+        self.runner = self.runner_class(**self._get_runner_args())
+        try:
+            if self.connect_to_running_emulator:
+                self.runner.device.connect()
+            self.runner.start()
+        except Exception as e:
+            message = 'Error possibly due to runner or device args.'
+            e.args += (message,)
+            if hasattr(e, 'strerror') and e.strerror:
+                e.strerror = ', '.join([e.strerror, message])
+            raise e
+        # gecko_log comes from logcat when running with device/emulator
+        logcat_args = {
+            'filterspec': 'Gecko',
+            'serial': self.runner.device.dm._deviceSerial
+        }
+        if self.gecko_log == '-':
+            logcat_args['stream'] = sys.stdout
+        else:
+            logcat_args['logfile'] = self.gecko_log
+        self.runner.device.start_logcat(**logcat_args)
+        self.runner.device.setup_port_forwarding(
+            local_port=self.marionette_port,
+            remote_port=self.marionette_port,
+        )
+
+    def _get_runner_args(self):
+        process_args = {
+            'processOutputLine': [NullOutput()],
+        }
+
+        runner_args = {
+            'app': self.package_name,
+            'avd_home': self.avd_home,
+            'adb_path': self.adb_path,
+            'binary': self.emulator_binary,
+            'profile': self.profile,
+            'cmdargs': ['-marionette'] + self.app_args,
+            'symbols_path': self.symbols_path,
+            'process_args': process_args,
+            'logdir': self.workspace or os.getcwd(),
+            'serial': self.serial,
+        }
+        if self.avd:
+            runner_args['avd'] = self.avd
+
+        return runner_args
+
+    def close(self, restart=False):
+        super(FennecInstance, self).close(restart)
+        if self.runner and self.runner.device.connected:
+            self.runner.device.dm.remove_forward(
+                'tcp:%d' % int(self.marionette_port)
+            )
+
+
 class B2GDesktopInstance(GeckoInstance):
     def __init__(self, host, port, bin, **kwargs):
         # Pass a profile and change the binary to -bin so that
         # the built-in gaia profile doesn't get touched.
         if kwargs.get('profile', None) is None:
             # GeckoInstance.start will clone the profile.
             kwargs['profile'] = os.path.join(os.path.dirname(bin),
                                              'gaia',
@@ -229,9 +335,10 @@ class NullOutput(object):
     def __call__(self, line):
         pass
 
 
 apps = {
     'b2g': B2GDesktopInstance,
     'b2gdesktop': B2GDesktopInstance,
     'fxdesktop': DesktopInstance,
+    'fennec': FennecInstance,
 }
--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -538,73 +538,79 @@ class Marionette(object):
     CONTEXT_CHROME = 'chrome'  # non-browser content: windows, dialogs, etc.
     CONTEXT_CONTENT = 'content'  # browser content: iframes, divs, etc.
     TIMEOUT_SEARCH = 'implicit'
     TIMEOUT_SCRIPT = 'script'
     TIMEOUT_PAGE = 'page load'
     DEFAULT_SOCKET_TIMEOUT = 360
     DEFAULT_STARTUP_TIMEOUT = 120
 
-    def __init__(self, host='localhost', port=2828, app=None, app_args=None,
-                 bin=None, profile=None, addons=None,
-                 gecko_log=None, baseurl=None,
-                 symbols_path=None, timeout=None,
-                 socket_timeout=DEFAULT_SOCKET_TIMEOUT,
-                 process_args=None, prefs=None,
-                 startup_timeout=None, workspace=None, verbose=0):
+    def __init__(self, host='localhost', port=2828, app=None, bin=None,
+                 baseurl=None, timeout=None, socket_timeout=DEFAULT_SOCKET_TIMEOUT,
+                 startup_timeout=None, **instance_args):
+        """
+        :param host: address for Marionette connection
+        :param port: integer port for Marionette connection
+        :param baseurl: where to look for files served from Marionette's www directory
+        :param startup_timeout: seconds to wait for a connection with binary
+        :param timeout: time to wait for page load, scripts, search
+        :param socket_timeout: timeout for Marionette socket operations
+        :param bin: path to app binary; if any truthy value is given this will
+            attempt to start a gecko instance with the specified `app`
+        :param app: type of instance_class to use for managing app instance.
+            See marionette_driver.geckoinstance
+        :param instance_args: args to pass to instance_class
+        """
         self.host = host
-        self.port = self.local_port = port
+        self.port = self.local_port = int(port)
         self.bin = bin
-        self.profile = profile
-        self.addons = addons
         self.instance = None
         self.session = None
         self.session_id = None
         self.window = None
         self.chrome_window = None
         self.baseurl = baseurl
         self._test_name = None
         self.timeout = timeout
         self.socket_timeout = socket_timeout
 
         startup_timeout = startup_timeout or self.DEFAULT_STARTUP_TIMEOUT
-
-        if bin:
-            port = int(self.port)
-            if not Marionette.is_port_available(port, host=self.host):
-                ex_msg = "%s:%d is unavailable." % (self.host, port)
-                raise errors.MarionetteException(message=ex_msg)
-            if app:
-                # select instance class for the given app
-                try:
-                    instance_class = geckoinstance.apps[app]
-                except KeyError:
-                    msg = 'Application "%s" unknown (should be one of %s)'
-                    raise NotImplementedError(msg % (app, geckoinstance.apps.keys()))
-            else:
-                try:
-                    config = ConfigParser.RawConfigParser()
-                    config.read(os.path.join(os.path.dirname(bin), 'application.ini'))
-                    app = config.get('App', 'Name')
-                    instance_class = geckoinstance.apps[app.lower()]
-                except (ConfigParser.NoOptionError,
-                        ConfigParser.NoSectionError,
-                        KeyError):
-                    instance_class = geckoinstance.GeckoInstance
-            self.instance = instance_class(host=self.host, port=self.port,
-                                           bin=self.bin, profile=self.profile,
-                                           app_args=app_args,
-                                           symbols_path=symbols_path,
-                                           gecko_log=gecko_log, prefs=prefs,
-                                           addons=self.addons,
-                                           workspace=workspace,
-                                           verbose=verbose)
+        if self.bin:
+            self.instance = self._create_instance(app, instance_args)
             self.instance.start()
             self.raise_for_port(self.wait_for_port(timeout=startup_timeout))
 
+    def _create_instance(self, app, instance_args):
+        if not Marionette.is_port_available(self.port, host=self.host):
+            ex_msg = "%s:%d is unavailable." % (self.host, self.port)
+            raise errors.MarionetteException(message=ex_msg)
+        if app:
+            # select instance class for the given app
+            try:
+                instance_class = geckoinstance.apps[app]
+            except KeyError:
+                msg = 'Application "%s" unknown (should be one of %s)'
+                raise NotImplementedError(
+                    msg % (app, geckoinstance.apps.keys()))
+        else:
+            try:
+                if not isinstance(self.bin, basestring):
+                    raise TypeError("bin must be a string if app is not specified")
+                config = ConfigParser.RawConfigParser()
+                config.read(os.path.join(os.path.dirname(self.bin),
+                                         'application.ini'))
+                app = config.get('App', 'Name')
+                instance_class = geckoinstance.apps[app.lower()]
+            except (ConfigParser.NoOptionError,
+                    ConfigParser.NoSectionError,
+                    KeyError):
+                instance_class = geckoinstance.GeckoInstance
+        return instance_class(host=self.host, port=self.port, bin=self.bin,
+                              **instance_args)
+
     @property
     def profile_path(self):
         if self.instance and self.instance.profile:
             return self.instance.profile.profile
 
     def cleanup(self):
         if self.session:
             try:
@@ -1016,17 +1022,16 @@ class Marionette(object):
                        profile.
         : param in_app: If True, marionette will cause a restart from within the
                         browser. Otherwise the browser will be restarted immediately
                         by killing the process.
         """
         if not self.instance:
             raise errors.MarionetteException("restart can only be called "
                                              "on gecko instances launched by Marionette")
-
         if in_app:
             if clean:
                 raise ValueError
             # Values here correspond to constants in nsIAppStartup.
             # See http://mzl.la/1X0JZsC
             restart_flags = [
                 "eForceQuit",
                 "eRestart",
--- a/testing/marionette/harness/marionette/runner/base.py
+++ b/testing/marionette/harness/marionette/runner/base.py
@@ -1,14 +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/.
 
 from argparse import ArgumentParser
 
+from copy import deepcopy
 import json
 import mozinfo
 import moznetwork
 import os
 import random
 import re
 import socket
 import sys
@@ -253,36 +254,34 @@ class BaseMarionetteArguments(ArgumentPa
         self.add_argument('tests',
                           nargs='*',
                           default=[],
                           help='Tests to run. '
                                'One or more paths to test files (Python or JS), '
                                'manifest files (.ini) or directories. '
                                'When a directory is specified, '
                                'all test files in the directory will be run.')
-        self.add_argument('-v', '--verbose',
-                        action='count',
-                        help='Increase verbosity to include debug messages with -v, '
-                            'and trace messages with -vv.')
+        self.add_argument('--binary',
+                        help='path to gecko executable to launch before running the test')
         self.add_argument('--address',
                         help='host:port of running Gecko instance to connect to')
-        self.add_argument('--device',
-                        dest='device_serial',
-                        help='serial ID of a device to use for adb / fastboot')
+        self.add_argument('--emulator',
+                        action='store_true',
+                        help='If no --address is given, then the harness will launch an emulator. (See Remote options group.) '
+                             'If --address is given, then the harness assumes you are running an '
+                             'emulator already, and will launch gecko app on  that emulator.')
         self.add_argument('--app',
-                        help='application to use')
+                        help='application to use. see marionette_driver.geckoinstance')
         self.add_argument('--app-arg',
                         dest='app_args',
                         action='append',
                         default=[],
                         help='specify a command line argument to be passed onto the application')
-        self.add_argument('--binary',
-                        help='gecko executable to launch before running the test')
         self.add_argument('--profile',
-                        help='profile to use when launching the gecko process. if not passed, then a profile will be '
+                        help='profile to use when launching the gecko process. If not passed, then a profile will be '
                              'constructed and used',
                         type=dir_path)
         self.add_argument('--pref',
                         action='append',
                         dest='prefs_args',
                         help=(" A preference to set. Must be a key-value pair"
                               " separated by a ':'."))
         self.add_argument('--preferences',
@@ -361,16 +360,21 @@ class BaseMarionetteArguments(ArgumentPa
                              "used multiple times in which case the test must contain "
                              "at least one of the given tags.")
         self.add_argument('--workspace',
                           action='store',
                           default=None,
                           help="Path to directory for Marionette output. "
                                "(Default: .) (Default profile dest: TMP)",
                           type=dir_path)
+        self.add_argument('-v', '--verbose',
+                        action='count',
+                        help='Increase verbosity to include debug messages with -v, '
+                            'and trace messages with -vv.')
+        self.register_argument_container(RemoteMarionetteArguments())
 
     def register_argument_container(self, container):
         group = self.add_argument_group(container.name)
 
         for cli, kwargs in container.args:
             group.add_argument(*cli, **kwargs)
 
         self.argument_containers.append(container)
@@ -414,18 +418,18 @@ class BaseMarionetteArguments(ArgumentPa
     def verify_usage(self, args):
         if not args.tests:
             self.error('You must specify one or more test files, manifests, or directories.')
 
         missing_tests = [path for path in args.tests if not os.path.exists(path)]
         if missing_tests:
             self.error("Test file(s) not found: " + " ".join([path for path in missing_tests]))
 
-        if not args.address and not args.binary:
-            self.error('You must specify --binary, or --address')
+        if not args.address and not args.binary and not args.emulator:
+            self.error('You must specify --binary, or --address, or --emulator')
 
         if args.total_chunks is not None and args.this_chunk is None:
             self.error('You must specify which chunk to run.')
 
         if args.this_chunk is not None and args.total_chunks is None:
             self.error('You must specify how many chunks to split the tests into.')
 
         if args.total_chunks is not None:
@@ -448,55 +452,79 @@ class BaseMarionetteArguments(ArgumentPa
             })
 
         for container in self.argument_containers:
             if hasattr(container, 'verify_usage_handler'):
                 container.verify_usage_handler(args)
 
         return args
 
+class RemoteMarionetteArguments(object):
+    name = 'Remote (Emulator/Device)'
+    args = [
+        [['--emulator-binary'],
+         {'help': 'Path to emulator binary. By default mozrunner uses `which emulator`',
+          'dest': 'emulator_bin',
+          }],
+        [['--adb'],
+         {'help': 'Path to the adb. By default mozrunner uses `which adb`',
+          'dest': 'adb_path'
+          }],
+        [['--avd'],
+         {'help': ('Name of an AVD available in your environment.'
+                   'See mozrunner.FennecEmulatorRunner'),
+          }],
+        [['--avd-home'],
+         {'help': 'Path to avd parent directory',
+          }],
+        [['--device'],
+         {'help': ('Serial ID to connect to as seen in `adb devices`,'
+                   'e.g emulator-5444'),
+          'dest': 'device_serial',
+          }],
+    ]
 
 class BaseMarionetteTestRunner(object):
 
     textrunnerclass = MarionetteTextTestRunner
     driverclass = Marionette
 
     def __init__(self, address=None,
                  app=None, app_args=None, binary=None, profile=None,
                  logger=None, logdir=None,
                  repeat=0, testvars=None,
                  symbols_path=None, timeout=None,
-                 shuffle=False, shuffle_seed=random.randint(0, sys.maxint),
-                 sdcard=None, this_chunk=1, total_chunks=1, sources=None,
+                 shuffle=False, shuffle_seed=random.randint(0, sys.maxint), this_chunk=1, total_chunks=1, sources=None,
                  server_root=None, gecko_log=None, result_callbacks=None,
                  prefs=None, test_tags=None,
                  socket_timeout=BaseMarionetteArguments.socket_timeout_default,
                  startup_timeout=None, addons=None, workspace=None,
-                 verbose=0, e10s=True, **kwargs):
+                 verbose=0, e10s=True, emulator=False, **kwargs):
+        self.extra_kwargs = kwargs
+        self.test_kwargs = deepcopy(kwargs)
         self.address = address
         self.app = app
         self.app_args = app_args or []
         self.bin = binary
+        self.emulator = emulator
         self.profile = profile
         self.addons = addons
         self.logger = logger
         self.httpd = None
         self.marionette = None
         self.logdir = logdir
         self.repeat = repeat
-        self.test_kwargs = kwargs
         self.symbols_path = symbols_path
         self.timeout = timeout
         self.socket_timeout = socket_timeout
         self._capabilities = None
         self._appinfo = None
         self._appName = None
         self.shuffle = shuffle
         self.shuffle_seed = shuffle_seed
-        self.sdcard = sdcard
         self.sources = sources
         self.server_root = server_root
         self.this_chunk = this_chunk
         self.total_chunks = total_chunks
         self.mixin_run_tests = []
         self.manifest_skipped_tests = []
         self.tests = []
         self.result_callbacks = result_callbacks or []
@@ -653,36 +681,52 @@ class BaseMarionetteTestRunner(object):
 
         kwargs = {
             'timeout': self.timeout,
             'socket_timeout': self.socket_timeout,
             'prefs': self.prefs,
             'startup_timeout': self.startup_timeout,
             'verbose': self.verbose,
         }
-        if self.bin:
+        if self.bin or self.emulator:
             kwargs.update({
                 'host': 'localhost',
                 'port': 2828,
                 'app': self.app,
                 'app_args': self.app_args,
-                'bin': self.bin,
                 'profile': self.profile,
                 'addons': self.addons,
                 'gecko_log': self.gecko_log,
+                # ensure Marionette class takes care of starting gecko instance
+                'bin': True,
+            })
+
+        if self.bin:
+            kwargs.update({
+                'bin': self.bin,
+        })
+
+        if self.emulator:
+            kwargs.update({
+                'avd_home': self.extra_kwargs.get('avd_home'),
+                'adb_path': self.extra_kwargs.get('adb_path'),
+                'emulator_binary': self.extra_kwargs.get('emulator_bin'),
             })
 
         if self.address:
             host, port = self.address.split(':')
             kwargs.update({
                 'host': host,
                 'port': int(port),
             })
-
-            if not self.bin:
+            if self.emulator:
+                kwargs.update({
+                    'connect_to_running_emulator': True,
+                })
+            if not self.bin and not self.emulator:
                 try:
                     #establish a socket connection so we can vertify the data come back
                     connection = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                     connection.connect((host,int(port)))
                     connection.close()
                 except Exception, e:
                     raise Exception("Connection attempt to %s:%s failed with error: %s" %(host,port,e))
         if self.workspace:
@@ -802,24 +846,34 @@ setReq.onerror = function() {
         start_time = time.time()
         self._initialize_test_run(tests)
 
         need_external_ip = self._start_marionette()
         self._set_baseurl(need_external_ip)
 
         self._add_tests(tests)
 
+        device_info = None
+        if self.marionette.instance and self.emulator:
+            try:
+                device_info = self.marionette.instance.runner.device.dm.getInfo()
+            except Exception:
+                self.logger.warning('Could not get device info.')
+
+		#TODO Get version_info in Fennec case
         version_info = None
         if self.bin:
             version_info = mozversion.get_version(binary=self.bin,
                                                   sources=self.sources)
 
         self.logger.info("running with e10s: {}".format(self.e10s))
 
-        self.logger.suite_start(self.tests, version_info=version_info,)
+        self.logger.suite_start(self.tests,
+                                version_info=version_info,
+                                device_info=device_info)
 
         self._log_skipped_tests()
 
         interrupted = None
         try:
             counter = self.repeat
             while counter >=0:
                 round_num = self.repeat - counter
--- a/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
@@ -62,67 +62,71 @@ def logger():
 
 @pytest.fixture()
 def mach_parsed_kwargs(logger):
     """
     Parsed and verified dictionary used during simplest
     call to mach marionette-test
     """
     return {
-         'addon': None,
-         'address': None,
-         'app': None,
-         'app_args': [],
-         'binary': u'/path/to/firefox',
-         'device_serial': None,
-         'e10s': False,
-         'gecko_log': None,
-         'homedir': None,
-         'jsdebugger': False,
-         'log_errorsummary': None,
-         'log_html': None,
-         'log_mach': None,
-         'log_mach_buffer': None,
-         'log_mach_level': None,
-         'log_mach_verbose': None,
-         'log_raw': None,
-         'log_raw_level': None,
-         'log_tbpl': None,
-         'log_tbpl_buffer': None,
-         'log_tbpl_level': None,
-         'log_unittest': None,
-         'log_xunit': None,
-         'logdir': None,
-         'logger_name': 'Marionette-based Tests',
-         'no_window': False,
-         'prefs': {},
-         'prefs_args': None,
-         'prefs_files': None,
-         'profile': None,
-         'pydebugger': None,
-         'repeat': 0,
-         'sdcard': None,
-         'server_root': None,
-         'shuffle': False,
-         'shuffle_seed': 2276870381009474531,
-         'socket_timeout': 360.0,
-         'sources': None,
-         'startup_timeout': 60,
-         'symbols_path': None,
-         'test_tags': None,
-         'tests': [u'/path/to/unit-tests.ini'],
-         'testvars': None,
-         'this_chunk': None,
-         'timeout': None,
-         'total_chunks': None,
-         'tree': 'b2g',
-         'type': None,
-         'verbose': None,
-         'workspace': None,
-         'logger': logger,
+        'adb_path': None,
+        'addon': None,
+        'address': None,
+        'app': None,
+        'app_args': [],
+        'avd': None,
+        'avd_home': None,
+        'binary': u'/path/to/firefox',
+        'device_serial': None,
+        'e10s': True,
+        'emulator': False,
+        'emulator_bin': None,
+        'gecko_log': None,
+        'jsdebugger': False,
+        'log_errorsummary': None,
+        'log_html': None,
+        'log_mach': None,
+        'log_mach_buffer': None,
+        'log_mach_level': None,
+        'log_mach_verbose': None,
+        'log_raw': None,
+        'log_raw_level': None,
+        'log_tbpl': None,
+        'log_tbpl_buffer': None,
+        'log_tbpl_compact': None,
+        'log_tbpl_level': None,
+        'log_unittest': None,
+        'log_xunit': None,
+        'logger_name': 'Marionette-based Tests',
+        'prefs': {
+            'browser.tabs.remote.autostart': True,
+            'browser.tabs.remote.force-enable': True,
+            'extensions.e10sBlocksEnabling': False,
+        },
+        'prefs_args': None,
+        'prefs_files': None,
+        'profile': None,
+        'pydebugger': None,
+        'repeat': 0,
+        'server_root': None,
+        'shuffle': False,
+        'shuffle_seed': 2276870381009474531,
+        'socket_timeout': 360.0,
+        'sources': None,
+        'startup_timeout': 60,
+        'symbols_path': None,
+        'test_tags': None,
+        'tests': [u'/path/to/unit-tests.ini'],
+        'testvars': None,
+        'this_chunk': None,
+        'timeout': None,
+        'total_chunks': None,
+        'verbose': None,
+        'workspace': None,
+        'logger': logger,
     }
 
 
 @pytest.fixture()
 def runner(mach_parsed_kwargs):
     """
     MarionetteTestRunner instance initialized with default options.
     """