summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozrunner/mozrunner/base/device.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozrunner/mozrunner/base/device.py')
-rw-r--r--testing/mozbase/mozrunner/mozrunner/base/device.py199
1 files changed, 199 insertions, 0 deletions
diff --git a/testing/mozbase/mozrunner/mozrunner/base/device.py b/testing/mozbase/mozrunner/mozrunner/base/device.py
new file mode 100644
index 0000000000..bf3c5965ff
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base/device.py
@@ -0,0 +1,199 @@
+# 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/.
+
+import codecs
+import datetime
+import re
+import signal
+import sys
+import tempfile
+import time
+
+import mozfile
+import six
+
+from ..devices import BaseEmulator
+from .runner import BaseRunner
+
+
+class DeviceRunner(BaseRunner):
+ """
+ The base runner class used for running gecko on
+ remote devices (or emulators).
+ """
+
+ env = {
+ "MOZ_CRASHREPORTER": "1",
+ "MOZ_CRASHREPORTER_NO_REPORT": "1",
+ "MOZ_CRASHREPORTER_SHUTDOWN": "1",
+ "MOZ_HIDE_RESULTS_TABLE": "1",
+ "MOZ_IN_AUTOMATION": "1",
+ "MOZ_LOG": "signaling:3,mtransport:4,DataChannel:4,jsep:4",
+ "R_LOG_LEVEL": "6",
+ "R_LOG_DESTINATION": "stderr",
+ "R_LOG_VERBOSE": "1",
+ }
+
+ def __init__(self, device_class, device_args=None, **kwargs):
+ process_log = tempfile.NamedTemporaryFile(suffix="pidlog")
+ # the env will be passed to the device, it is not a *real* env
+ self._device_env = dict(DeviceRunner.env)
+ self._device_env["MOZ_PROCESS_LOG"] = process_log.name
+ # be sure we do not pass env to the parent class ctor
+ env = kwargs.pop("env", None)
+ if env:
+ self._device_env.update(env)
+
+ if six.PY2:
+ stdout = codecs.getwriter("utf-8")(sys.stdout)
+ else:
+ stdout = codecs.getwriter("utf-8")(sys.stdout.buffer)
+ process_args = {
+ "stream": stdout,
+ "processOutputLine": self.on_output,
+ "onFinish": self.on_finish,
+ "onTimeout": self.on_timeout,
+ }
+ process_args.update(kwargs.get("process_args") or {})
+
+ kwargs["process_args"] = process_args
+ BaseRunner.__init__(self, **kwargs)
+
+ device_args = device_args or {}
+ self.device = device_class(**device_args)
+
+ @property
+ def command(self):
+ # command built by mozdevice -- see start() below
+ return None
+
+ def start(self, *args, **kwargs):
+ if isinstance(self.device, BaseEmulator) and not self.device.connected:
+ self.device.start()
+ self.device.connect()
+ self.device.setup_profile(self.profile)
+
+ app = self.app_ctx.remote_process
+ self.device.run_as_package = app
+ args = ["-no-remote", "-profile", self.app_ctx.remote_profile]
+ args.extend(self.cmdargs)
+ env = self._device_env
+ url = None
+ if "geckoview" in app:
+ activity = "TestRunnerActivity"
+ self.app_ctx.device.launch_activity(
+ app, activity, e10s=True, moz_env=env, extra_args=args, url=url
+ )
+ else:
+ self.app_ctx.device.launch_fennec(
+ app, moz_env=env, extra_args=args, url=url
+ )
+
+ 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(0.5)
+ if not self.is_running():
+ print(
+ "timed out waiting for '%s' process to start"
+ % self.app_ctx.remote_process
+ )
+
+ def stop(self, sig=None):
+ if not sig and self.is_running():
+ self.app_ctx.stop_application()
+
+ if self.is_running():
+ timeout = 10
+
+ 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 '{}' 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()
+ if self.wait(timeout) is None:
+ print(
+ "timed out waiting for '{}' process to exit".format(
+ self.app_ctx.remote_process
+ )
+ )
+
+ @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.device.process_exist(self.app_ctx.remote_process):
+ 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.
+ """
+ end_time = None
+ if timeout is not None:
+ end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout)
+
+ while self.is_running():
+ if end_time is not None and datetime.datetime.now() > end_time:
+ break
+ time.sleep(0.5)
+
+ return self.returncode
+
+ def on_output(self, line):
+ match = re.findall(r"TEST-START \| ([^\s]*)", line)
+ if match:
+ self.last_test = match[-1]
+
+ def on_timeout(self):
+ self.stop(sig=signal.SIGABRT)
+ msg = "DeviceRunner TEST-UNEXPECTED-FAIL | %s | application timed out after %s seconds"
+ if self.timeout:
+ timeout = self.timeout
+ else:
+ timeout = self.output_timeout
+ msg = "%s with no output" % msg
+
+ print(msg % (self.last_test, timeout))
+ self.check_for_crashes()
+
+ def on_finish(self):
+ self.check_for_crashes()
+
+ def check_for_crashes(self, dump_save_path=None, test_name=None, **kwargs):
+ test_name = test_name or self.last_test
+ dump_dir = self.device.pull_minidumps()
+ crashed = BaseRunner.check_for_crashes(
+ self,
+ dump_directory=dump_dir,
+ dump_save_path=dump_save_path,
+ test_name=test_name,
+ **kwargs
+ )
+ mozfile.remove(dump_dir)
+ return crashed
+
+ def cleanup(self, *args, **kwargs):
+ BaseRunner.cleanup(self, *args, **kwargs)
+ self.device.cleanup()
+
+
+class FennecRunner(DeviceRunner):
+ def __init__(self, cmdargs=None, **kwargs):
+ super(FennecRunner, self).__init__(**kwargs)
+ self.cmdargs = cmdargs or []