|
@@ -1,8 +1,8 @@
|
|
# HG changeset patch
|
|
# HG changeset patch
|
|
# User Ian Neal <iann_cvs@blueyonder.co.uk>
|
|
# User Ian Neal <iann_cvs@blueyonder.co.uk>
|
|
-# Date 1720364642 -3600
|
|
|
|
-# Parent 95cbce82a999b85cc352403f8de9db0d6df0a541
|
|
|
|
-Bug 1906540 - Remove mozrunner devices and mozdevice from SeaMonkey - mozdevice part. r=frg a=frg
|
|
|
|
|
|
+# Date 1720395449 -3600
|
|
|
|
+# Parent 6dbc703f582441ac9908b5136433b9add4b157a1
|
|
|
|
+Bug 1906540 - Remove mozrunner devices and mozdevice from SeaMonkey. r=frg a=frg
|
|
|
|
|
|
diff --git a/config/rules.mk b/config/rules.mk
|
|
diff --git a/config/rules.mk b/config/rules.mk
|
|
--- a/config/rules.mk
|
|
--- a/config/rules.mk
|
|
@@ -64,7 +64,60 @@ diff --git a/js/src/jit-test/jit_test.py b/js/src/jit-test/jit_test.py
|
|
diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py
|
|
diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py
|
|
--- a/js/src/tests/lib/jittests.py
|
|
--- a/js/src/tests/lib/jittests.py
|
|
+++ b/js/src/tests/lib/jittests.py
|
|
+++ b/js/src/tests/lib/jittests.py
|
|
-@@ -659,20 +659,17 @@ def process_test_results(results, num_te
|
|
|
|
|
|
+@@ -365,52 +365,16 @@ def find_tests(substring=None):
|
|
|
|
+ if filename in ('shell.js', 'browser.js'):
|
|
|
|
+ continue
|
|
|
|
+ test = os.path.join(dirpath, filename)
|
|
|
|
+ if substring is None \
|
|
|
|
+ or substring in os.path.relpath(test, TEST_DIR):
|
|
|
|
+ ans.append(test)
|
|
|
|
+ return ans
|
|
|
|
+
|
|
|
|
+-def run_test_remote(test, device, prefix, options):
|
|
|
|
+- from mozdevice import ADBDevice, ADBProcessError
|
|
|
|
+-
|
|
|
|
+- if options.test_reflect_stringify:
|
|
|
|
+- raise ValueError("can't run Reflect.stringify tests remotely")
|
|
|
|
+- cmd = test.command(prefix,
|
|
|
|
+- posixpath.join(options.remote_test_root, 'lib/'),
|
|
|
|
+- posixpath.join(options.remote_test_root, 'modules/'),
|
|
|
|
+- posixpath.join(options.remote_test_root, 'tests'))
|
|
|
|
+- if options.show_cmd:
|
|
|
|
+- print(subprocess.list2cmdline(cmd))
|
|
|
|
+-
|
|
|
|
+- env = {}
|
|
|
|
+- if test.tz_pacific:
|
|
|
|
+- env['TZ'] = 'PST8PDT'
|
|
|
|
+-
|
|
|
|
+- env['LD_LIBRARY_PATH'] = options.remote_test_root
|
|
|
|
+-
|
|
|
|
+- cmd = ADBDevice._escape_command_line(cmd)
|
|
|
|
+- start = datetime.now()
|
|
|
|
+- try:
|
|
|
|
+- out = device.shell_output(cmd, env=env,
|
|
|
|
+- cwd=options.remote_test_root,
|
|
|
|
+- timeout=int(options.timeout))
|
|
|
|
+- returncode = 0
|
|
|
|
+- except ADBProcessError as e:
|
|
|
|
+- out = e.adb_process.stdout
|
|
|
|
+- print("exception output: %s" % str(out))
|
|
|
|
+- returncode = e.adb_process.exitcode
|
|
|
|
+-
|
|
|
|
+- elapsed = (datetime.now() - start).total_seconds()
|
|
|
|
+-
|
|
|
|
+- # We can't distinguish between stdout and stderr so we pass
|
|
|
|
+- # the same buffer to both.
|
|
|
|
+- return TestOutput(test, cmd, out, out, returncode, elapsed, False)
|
|
|
|
+-
|
|
|
|
+ def check_output(out, err, rc, timed_out, test, options):
|
|
|
|
+ if timed_out:
|
|
|
|
+ if os.path.normpath(test.relpath_tests).replace(os.sep, '/') \
|
|
|
|
+ in options.ignore_timeouts:
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ # The shell sometimes hangs on shutdown on Windows 7 and Windows
|
|
|
|
+ # Server 2008. See bug 970063 comment 7 for a description of the
|
|
|
|
+@@ -659,20 +623,17 @@ def process_test_results(results, num_te
|
|
return print_test_summary(num_tests, failures, complete, doing, options)
|
|
return print_test_summary(num_tests, failures, complete, doing, options)
|
|
|
|
|
|
def run_tests(tests, num_tests, prefix, options, remote=False):
|
|
def run_tests(tests, num_tests, prefix, options, remote=False):
|
|
@@ -86,7 +139,7 @@ diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py
|
|
|
|
|
|
def run_tests_local(tests, num_tests, prefix, options, slog):
|
|
def run_tests_local(tests, num_tests, prefix, options, slog):
|
|
# The jstests tasks runner requires the following options. The names are
|
|
# The jstests tasks runner requires the following options. The names are
|
|
-@@ -688,27 +685,16 @@ def run_tests_local(tests, num_tests, pr
|
|
|
|
|
|
+@@ -688,27 +649,16 @@ def run_tests_local(tests, num_tests, pr
|
|
# The test runner wants the prefix as a static on the Test class.
|
|
# The test runner wants the prefix as a static on the Test class.
|
|
JitTest.js_cmd_prefix = prefix
|
|
JitTest.js_cmd_prefix = prefix
|
|
|
|
|
|
@@ -114,7 +167,7 @@ diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py
|
|
|
|
|
|
for file in os.listdir(options.local_lib):
|
|
for file in os.listdir(options.local_lib):
|
|
if file in required_libs:
|
|
if file in required_libs:
|
|
-@@ -716,57 +702,10 @@ def push_libs(options, device):
|
|
|
|
|
|
+@@ -716,57 +666,10 @@ def push_libs(options, device):
|
|
device.push(os.path.join(options.local_lib, file), remote_file)
|
|
device.push(os.path.join(options.local_lib, file), remote_file)
|
|
|
|
|
|
def push_progs(options, device, progs):
|
|
def push_progs(options, device, progs):
|
|
@@ -688,6 +741,670 @@ diff --git a/testing/config/mozbase_requirements.txt b/testing/config/mozbase_re
|
|
../mozbase/mozlog
|
|
../mozbase/mozlog
|
|
../mozbase/moznetwork
|
|
../mozbase/moznetwork
|
|
../mozbase/mozprocess
|
|
../mozbase/mozprocess
|
|
|
|
+diff --git a/testing/marionette/client/marionette_driver/geckoinstance.py b/testing/marionette/client/marionette_driver/geckoinstance.py
|
|
|
|
+--- a/testing/marionette/client/marionette_driver/geckoinstance.py
|
|
|
|
++++ b/testing/marionette/client/marionette_driver/geckoinstance.py
|
|
|
|
+@@ -11,31 +11,30 @@ import tempfile
|
|
|
|
+ import time
|
|
|
|
+ import traceback
|
|
|
|
+
|
|
|
|
+ from copy import deepcopy
|
|
|
|
+
|
|
|
|
+ import mozversion
|
|
|
|
+
|
|
|
|
+ from mozprofile import Profile
|
|
|
|
+-from mozrunner import Runner, FennecEmulatorRunner
|
|
|
|
++from mozrunner import Runner
|
|
|
|
+ import six
|
|
|
|
+ from six import reraise
|
|
|
|
+
|
|
|
|
+ from . import errors
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class GeckoInstance(object):
|
|
|
|
+ required_prefs = {
|
|
|
|
+ # Increase the APZ content response timeout in tests to 1 minute.
|
|
|
|
+- # This is to accommodate the fact that test environments tends to be slower
|
|
|
|
+- # than production environments (with the b2g emulator being the slowest of them
|
|
|
|
+- # all), resulting in the production timeout value sometimes being exceeded
|
|
|
|
+- # and causing false-positive test failures. See bug 1176798, bug 1177018,
|
|
|
|
+- # bug 1210465.
|
|
|
|
++ # This is to accommodate the fact that test environments tends to be
|
|
|
|
++ # slower than production environments, resulting in the production
|
|
|
|
++ # timeout value sometimes being exceeded and causing false-positive
|
|
|
|
++ # test failures. See bug 1176798, bug 1177018, bug 1210465.
|
|
|
|
+ "apz.content_response_timeout": 60000,
|
|
|
|
+
|
|
|
|
+ # Do not send Firefox health reports to the production server
|
|
|
|
+ "datareporting.healthreport.documentServerURI": "http://%(server)s/dummy/healthreport/",
|
|
|
|
+
|
|
|
|
+ # Do not show datareporting policy notifications which can interfer with tests
|
|
|
|
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
|
|
|
|
+
|
|
|
|
+@@ -324,19 +323,16 @@ class GeckoInstance(object):
|
|
|
|
+ "symbols_path": self.symbols_path,
|
|
|
|
+ "process_args": process_args
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ def close(self, clean=False):
|
|
|
|
+ """
|
|
|
|
+ Close the managed Gecko process.
|
|
|
|
+
|
|
|
|
+- Depending on self.runner_class, setting `clean` to True may also kill
|
|
|
|
+- the emulator process in which this instance is running.
|
|
|
|
+-
|
|
|
|
+ :param clean: If True, also perform runner cleanup.
|
|
|
|
+ """
|
|
|
|
+ if self.runner:
|
|
|
|
+ self.runner.stop()
|
|
|
|
+ if clean:
|
|
|
|
+ self.runner.cleanup()
|
|
|
|
+
|
|
|
|
+ if clean:
|
|
|
|
+@@ -355,144 +351,16 @@ class GeckoInstance(object):
|
|
|
|
+ self.prefs = prefs
|
|
|
|
+ else:
|
|
|
|
+ self.prefs = None
|
|
|
|
+
|
|
|
|
+ self.close(clean=clean)
|
|
|
|
+ self.start()
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-class FennecInstance(GeckoInstance):
|
|
|
|
+- fennec_prefs = {
|
|
|
|
+- # Enable output of dump()
|
|
|
|
+- "browser.dom.window.dump.enabled": True,
|
|
|
|
+-
|
|
|
|
+- # Disable Android snippets
|
|
|
|
+- "browser.snippets.enabled": False,
|
|
|
|
+- "browser.snippets.syncPromo.enabled": False,
|
|
|
|
+- "browser.snippets.firstrunHomepage.enabled": False,
|
|
|
|
+-
|
|
|
|
+- # Disable safebrowsing components
|
|
|
|
+- "browser.safebrowsing.blockedURIs.enabled": False,
|
|
|
|
+- "browser.safebrowsing.downloads.enabled": False,
|
|
|
|
+- "browser.safebrowsing.passwords.enabled": False,
|
|
|
|
+- "browser.safebrowsing.malware.enabled": False,
|
|
|
|
+- "browser.safebrowsing.phishing.enabled": False,
|
|
|
|
+-
|
|
|
|
+- # Do not restore the last open set of tabs if the browser has crashed
|
|
|
|
+- "browser.sessionstore.resume_from_crash": False,
|
|
|
|
+-
|
|
|
|
+- # Disable e10s by default
|
|
|
|
+- "browser.tabs.remote.autostart": False,
|
|
|
|
+-
|
|
|
|
+- # Do not allow background tabs to be zombified, otherwise for tests that
|
|
|
|
+- # open additional tabs, the test harness tab itself might get unloaded
|
|
|
|
+- "browser.tabs.disableBackgroundZombification": True,
|
|
|
|
+- }
|
|
|
|
+-
|
|
|
|
+- def __init__(self, emulator_binary=None, avd_home=None, avd=None,
|
|
|
|
+- adb_path=None, serial=None, connect_to_running_emulator=False,
|
|
|
|
+- package_name=None, *args, **kwargs):
|
|
|
|
+- super(FennecInstance, self).__init__(*args, **kwargs)
|
|
|
|
+- self.required_prefs.update(FennecInstance.fennec_prefs)
|
|
|
|
+-
|
|
|
|
+- self.runner_class = FennecEmulatorRunner
|
|
|
|
+- # runner args
|
|
|
|
+- self._package_name = package_name
|
|
|
|
+- 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.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:
|
|
|
|
+- 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.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.device.forward(
|
|
|
|
+- local="tcp:{}".format(self.marionette_port),
|
|
|
|
+- remote="tcp:{}".format(self.marionette_port))
|
|
|
|
+-
|
|
|
|
+- def _get_runner_args(self):
|
|
|
|
+- process_args = {
|
|
|
|
+- "processOutputLine": [NullOutput()],
|
|
|
|
+- "universal_newlines": True,
|
|
|
|
+- }
|
|
|
|
+-
|
|
|
|
+- 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, clean=False):
|
|
|
|
+- """
|
|
|
|
+- Close the managed Gecko process.
|
|
|
|
+-
|
|
|
|
+- 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.device.remove_forwards(
|
|
|
|
+- "tcp:{}".format(self.marionette_port))
|
|
|
|
+- self.unresponsive_count = 0
|
|
|
|
+- except Exception:
|
|
|
|
+- self.unresponsive_count += 1
|
|
|
|
+- traceback.print_exception(*sys.exc_info())
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ class DesktopInstance(GeckoInstance):
|
|
|
|
+ desktop_prefs = {
|
|
|
|
+ # Disable application updates
|
|
|
|
+ "app.update.enabled": False,
|
|
|
|
+
|
|
|
|
+ # Enable output of dump()
|
|
|
|
+ "browser.dom.window.dump.enabled": True,
|
|
|
|
+
|
|
|
|
+@@ -579,18 +447,16 @@ class ThunderbirdInstance(GeckoInstance)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class NullOutput(object):
|
|
|
|
+ def __call__(self, line):
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ apps = {
|
|
|
|
+- 'fennec': FennecInstance,
|
|
|
|
+ 'fxdesktop': DesktopInstance,
|
|
|
|
+ 'thunderbird': ThunderbirdInstance,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ app_ids = {
|
|
|
|
+- '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'fennec',
|
|
|
|
+ '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'fxdesktop',
|
|
|
|
+ '{3550f703-e582-4d05-9a08-453d09bdfdc6}': 'thunderbird',
|
|
|
|
+ }
|
|
|
|
+diff --git a/testing/marionette/harness/marionette_harness/runner/base.py b/testing/marionette/harness/marionette_harness/runner/base.py
|
|
|
|
+--- a/testing/marionette/harness/marionette_harness/runner/base.py
|
|
|
|
++++ b/testing/marionette/harness/marionette_harness/runner/base.py
|
|
|
|
+@@ -262,23 +262,16 @@ class BaseMarionetteArguments(ArgumentPa
|
|
|
|
+ '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('--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('--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. 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('--profile',
|
|
|
|
+@@ -377,17 +370,16 @@ class BaseMarionetteArguments(ArgumentPa
|
|
|
|
+ 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)
|
|
|
|
+@@ -429,18 +421,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 and not args.emulator:
|
|
|
|
+- self.error('You must specify --binary, or --address, or --emulator')
|
|
|
|
++ if not args.address and not args.binary:
|
|
|
|
++ self.error('You must specify --binary, or --address')
|
|
|
|
+
|
|
|
|
+ if args.repeat is not None and args.repeat < 0:
|
|
|
|
+ self.error('The value of --repeat has to be equal or greater than 0.')
|
|
|
|
+
|
|
|
|
+ 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:
|
|
|
|
+@@ -460,47 +452,16 @@ 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',
|
|
|
|
+- }],
|
|
|
|
+- [['--package'],
|
|
|
|
+- {'help': 'Name of Android package, e.g. org.mozilla.fennec',
|
|
|
|
+- 'dest': 'package_name',
|
|
|
|
+- }],
|
|
|
|
+-
|
|
|
|
+- ]
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ class Fixtures(object):
|
|
|
|
+ def where_is(self, uri, on="http"):
|
|
|
|
+ return serve.where_is(uri, on)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class BaseMarionetteTestRunner(object):
|
|
|
|
+
|
|
|
|
+ textrunnerclass = MarionetteTextTestRunner
|
|
|
|
+@@ -515,31 +476,30 @@ class BaseMarionetteTestRunner(object):
|
|
|
|
+ symbols_path=None,
|
|
|
|
+ shuffle=False, shuffle_seed=random.randint(0, sys.maxint), this_chunk=1,
|
|
|
|
+ total_chunks=1,
|
|
|
|
+ server_root=None, gecko_log=None, result_callbacks=None,
|
|
|
|
+ prefs=None, test_tags=None,
|
|
|
|
+ socket_timeout=None,
|
|
|
|
+ startup_timeout=None,
|
|
|
|
+ addons=None, workspace=None,
|
|
|
|
+- verbose=0, e10s=True, emulator=False, headless=False, **kwargs):
|
|
|
|
++ verbose=0, e10s=True, headless=False, **kwargs):
|
|
|
|
+ self._appName = None
|
|
|
|
+ self._capabilities = None
|
|
|
|
+ self._filename_pattern = None
|
|
|
|
+ self._version_info = {}
|
|
|
|
+
|
|
|
|
+ self.fixture_servers = {}
|
|
|
|
+ self.fixtures = Fixtures()
|
|
|
|
+ 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.marionette = None
|
|
|
|
+ self.logdir = logdir
|
|
|
|
+ self.repeat = repeat or 0
|
|
|
|
+ self.run_until_failure = run_until_failure or False
|
|
|
|
+ self.symbols_path = symbols_path
|
|
|
|
+@@ -721,17 +681,17 @@ class BaseMarionetteTestRunner(object):
|
|
|
|
+
|
|
|
|
+ kwargs = {
|
|
|
|
+ 'socket_timeout': self.socket_timeout,
|
|
|
|
+ 'prefs': self.prefs,
|
|
|
|
+ 'startup_timeout': self.startup_timeout,
|
|
|
|
+ 'verbose': self.verbose,
|
|
|
|
+ 'symbols_path': self.symbols_path,
|
|
|
|
+ }
|
|
|
|
+- if self.bin or self.emulator:
|
|
|
|
++ if self.bin:
|
|
|
|
+ kwargs.update({
|
|
|
|
+ 'host': 'localhost',
|
|
|
|
+ 'port': 2828,
|
|
|
|
+ 'app': self.app,
|
|
|
|
+ 'app_args': self.app_args,
|
|
|
|
+ 'profile': self.profile,
|
|
|
|
+ 'addons': self.addons,
|
|
|
|
+ 'gecko_log': self.gecko_log,
|
|
|
|
+@@ -739,36 +699,23 @@ class BaseMarionetteTestRunner(object):
|
|
|
|
+ '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'),
|
|
|
|
+- 'avd': self.extra_kwargs.get('avd'),
|
|
|
|
+- 'package_name': self.extra_kwargs.get('package_name'),
|
|
|
|
+- })
|
|
|
|
+-
|
|
|
|
+ if self.address:
|
|
|
|
+ host, port = self.address.split(':')
|
|
|
|
+ kwargs.update({
|
|
|
|
+ 'host': host,
|
|
|
|
+ 'port': int(port),
|
|
|
|
+ })
|
|
|
|
+- if self.emulator:
|
|
|
|
+- kwargs.update({
|
|
|
|
+- 'connect_to_running_emulator': True,
|
|
|
|
+- })
|
|
|
|
+- if not self.bin and not self.emulator:
|
|
|
|
++ if not self.bin:
|
|
|
|
+ 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 as e:
|
|
|
|
+ exc, val, tb = sys.exc_info()
|
|
|
|
+ msg = "Connection attempt to {0}:{1} failed with error: {2}"
|
|
|
|
+@@ -864,39 +811,31 @@ class BaseMarionetteTestRunner(object):
|
|
|
|
+ for url in serve.iter_url(self.fixture_servers):
|
|
|
|
+ self.logger.info("Fixture server listening on %s" % url)
|
|
|
|
+
|
|
|
|
+ # 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.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) "
|
|
|
|
+ "does not match browser appinfo (self.is_e10s)")
|
|
|
|
+ self.marionette.delete_session()
|
|
|
|
+
|
|
|
|
+ tests_by_group = defaultdict(list)
|
|
|
|
+ for test in self.tests:
|
|
|
|
+ tests_by_group[test['group']].append(test['filepath'])
|
|
|
|
+
|
|
|
|
+ self.logger.suite_start(tests_by_group,
|
|
|
|
+ name='marionette-test',
|
|
|
|
+- version_info=self.version_info,
|
|
|
|
+- device_info=device_info)
|
|
|
|
++ version_info=self.version_info)
|
|
|
|
+
|
|
|
|
+ self._log_skipped_tests()
|
|
|
|
+
|
|
|
|
+ interrupted = None
|
|
|
|
+ try:
|
|
|
|
+ repeat_index = 0
|
|
|
|
+ while repeat_index <= self.repeat:
|
|
|
|
+ if repeat_index > 0:
|
|
|
|
+diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py
|
|
|
|
+--- a/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py
|
|
|
|
++++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py
|
|
|
|
+@@ -24,30 +24,25 @@ def logger():
|
|
|
|
+
|
|
|
|
+ @pytest.fixture
|
|
|
|
+ def mach_parsed_kwargs(logger):
|
|
|
|
+ """
|
|
|
|
+ Parsed and verified dictionary used during simplest
|
|
|
|
+ call to mach marionette-test
|
|
|
|
+ """
|
|
|
|
+ return {
|
|
|
|
+- 'adb_path': None,
|
|
|
|
+ 'addons': None,
|
|
|
|
+ 'address': None,
|
|
|
|
+ 'app': None,
|
|
|
|
+ 'app_args': [],
|
|
|
|
+- 'avd': None,
|
|
|
|
+- 'avd_home': None,
|
|
|
|
+ 'binary': u'/path/to/firefox',
|
|
|
|
+ 'browsermob_port': None,
|
|
|
|
+ 'browsermob_script': None,
|
|
|
|
+ '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,
|
|
|
|
+diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py
|
|
|
|
+--- a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py
|
|
|
|
++++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py
|
|
|
|
+@@ -43,27 +43,10 @@ def test_parsing_optional_arguments(mach
|
|
|
|
+ result = vars(parsed_args)
|
|
|
|
+ assert result.get(arg_dest) == expected_value
|
|
|
|
+ mach_parsed_kwargs[arg_dest] = result[arg_dest]
|
|
|
|
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
|
|
|
|
+ built_kwargs = runner._build_kwargs()
|
|
|
|
+ assert built_kwargs[arg_dest] == expected_value
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-@pytest.mark.parametrize("arg_name, arg_dest, arg_value, expected_value",
|
|
|
|
+- [('adb', 'adb_path', 'samplevalue', 'samplevalue'),
|
|
|
|
+- ('avd', 'avd', 'samplevalue', 'samplevalue'),
|
|
|
|
+- ('avd-home', 'avd_home', 'samplevalue', 'samplevalue'),
|
|
|
|
+- ('package', 'package_name', 'samplevalue', 'samplevalue')])
|
|
|
|
+-def test_parse_opt_args_emulator(mach_parsed_kwargs, arg_name, arg_dest, arg_value, expected_value):
|
|
|
|
+- parser = MarionetteArguments()
|
|
|
|
+- parsed_args = parser.parse_args(['--' + arg_name, arg_value])
|
|
|
|
+- result = vars(parsed_args)
|
|
|
|
+- assert result.get(arg_dest) == expected_value
|
|
|
|
+- mach_parsed_kwargs[arg_dest] = result[arg_dest]
|
|
|
|
+- mach_parsed_kwargs["emulator"] = True
|
|
|
|
+- runner = MarionetteTestRunner(**mach_parsed_kwargs)
|
|
|
|
+- built_kwargs = runner._build_kwargs()
|
|
|
|
+- assert built_kwargs[arg_dest] == expected_value
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ if __name__ == '__main__':
|
|
|
|
+ mozunit.main('-p', 'no:terminalreporter', '--log-tbpl=-')
|
|
|
|
+diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
|
|
|
|
+--- a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
|
|
|
|
++++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
|
|
|
|
+@@ -50,19 +50,19 @@ def build_kwargs_using(mach_parsed_kwarg
|
|
|
|
+ return built_kwargs, socket
|
|
|
|
+ return built_kwargs
|
|
|
|
+ return kwarg_builder
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ @pytest.fixture
|
|
|
|
+ def expected_driver_args(runner):
|
|
|
|
+ '''Helper fixture for tests of _build_kwargs
|
|
|
|
+- with binary/emulator.
|
|
|
|
++ with binary.
|
|
|
|
+ Provides a dictionary of certain arguments
|
|
|
|
+- related to binary/emulator settings
|
|
|
|
++ related to binary settings
|
|
|
|
+ which we expect to be passed to the
|
|
|
|
+ driverclass constructor. Expected values can
|
|
|
|
+ be updated in tests as needed.
|
|
|
|
+ Provides convenience methods for comparing the
|
|
|
|
+ expected arguments to the argument dictionary
|
|
|
|
+ created by _build_kwargs. '''
|
|
|
|
+
|
|
|
|
+ class ExpectedDict(dict):
|
|
|
|
+@@ -142,73 +142,16 @@ def test_build_kwargs_basic_args(build_k
|
|
|
|
+ def test_build_kwargs_with_workspace(build_kwargs_using, workspace):
|
|
|
|
+ built_kwargs = build_kwargs_using({'workspace': workspace})
|
|
|
|
+ if workspace:
|
|
|
|
+ assert built_kwargs['workspace'] == workspace
|
|
|
|
+ else:
|
|
|
|
+ assert 'workspace' not in built_kwargs
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-@pytest.mark.parametrize('address', ['host:123', None])
|
|
|
|
+-def test_build_kwargs_with_address(build_kwargs_using, address):
|
|
|
|
+- built_kwargs, socket = build_kwargs_using(
|
|
|
|
+- {'address': address, 'binary': None, 'emulator': None},
|
|
|
|
+- return_socket=True
|
|
|
|
+- )
|
|
|
|
+- assert 'connect_to_running_emulator' not in built_kwargs
|
|
|
|
+- if address is not None:
|
|
|
|
+- host, port = address.split(":")
|
|
|
|
+- assert built_kwargs['host'] == host and built_kwargs['port'] == int(port)
|
|
|
|
+- socket.socket().connect.assert_called_with((host, int(port)))
|
|
|
|
+- assert socket.socket().close.called
|
|
|
|
+- else:
|
|
|
|
+- assert not socket.socket.called
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-@pytest.mark.parametrize('address', ['host:123', None])
|
|
|
|
+-@pytest.mark.parametrize('binary', ['path/to/bin', None])
|
|
|
|
+-def test_build_kwargs_with_binary_or_address(expected_driver_args, build_kwargs_using,
|
|
|
|
+- binary, address):
|
|
|
|
+- built_kwargs = build_kwargs_using({'binary': binary, 'address': address, 'emulator': None})
|
|
|
|
+- if binary:
|
|
|
|
+- expected_driver_args['bin'] = binary
|
|
|
|
+- if address:
|
|
|
|
+- host, port = address.split(":")
|
|
|
|
+- expected_driver_args.update({'host': host, 'port': int(port)})
|
|
|
|
+- else:
|
|
|
|
+- expected_driver_args.update({'host': 'localhost', 'port': 2828})
|
|
|
|
+- expected_driver_args.assert_matches(built_kwargs)
|
|
|
|
+- elif address is None:
|
|
|
|
+- expected_driver_args.assert_keys_not_in(built_kwargs)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-@pytest.mark.parametrize('address', ['host:123', None])
|
|
|
|
+-@pytest.mark.parametrize('emulator', [True, False, None])
|
|
|
|
+-def test_build_kwargs_with_emulator_or_address(expected_driver_args, build_kwargs_using,
|
|
|
|
+- emulator, address):
|
|
|
|
+- emulator_props = [(a, getattr(sentinel, a)) for a in ['avd_home', 'adb_path', 'emulator_bin']]
|
|
|
|
+- built_kwargs = build_kwargs_using(
|
|
|
|
+- [('emulator', emulator), ('address', address), ('binary', None)] + emulator_props
|
|
|
|
+- )
|
|
|
|
+- if emulator:
|
|
|
|
+- expected_driver_args.update(emulator_props)
|
|
|
|
+- expected_driver_args['emulator_binary'] = expected_driver_args.pop('emulator_bin')
|
|
|
|
+- expected_driver_args['bin'] = True
|
|
|
|
+- if address:
|
|
|
|
+- expected_driver_args['connect_to_running_emulator'] = True
|
|
|
|
+- host, port = address.split(":")
|
|
|
|
+- expected_driver_args.update({'host': host, 'port': int(port)})
|
|
|
|
+- else:
|
|
|
|
+- expected_driver_args.update({'host': 'localhost', 'port': 2828})
|
|
|
|
+- assert 'connect_to_running_emulator' not in built_kwargs
|
|
|
|
+- expected_driver_args.assert_matches(built_kwargs)
|
|
|
|
+- elif not address:
|
|
|
|
+- expected_driver_args.assert_keys_not_in(built_kwargs)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ def test_parsing_testvars(mach_parsed_kwargs):
|
|
|
|
+ mach_parsed_kwargs.pop('tests')
|
|
|
|
+ testvars_json_loads = [
|
|
|
|
+ {"wifi": {"ssid": "blah", "keyManagement": "WPA-PSK", "psk": "foo"}},
|
|
|
|
+ {"wifi": {"PEAP": "bar"}, "device": {"stuff": "buzz"}}
|
|
|
|
+ ]
|
|
|
|
+ expected_dict = {
|
|
|
|
+ "wifi": {
|
|
|
|
+diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py
|
|
|
|
+--- a/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py
|
|
|
|
++++ b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py
|
|
|
|
+@@ -41,27 +41,16 @@ class TestPreferences(MarionetteTestCase
|
|
|
|
+
|
|
|
|
+ for key, value in required_prefs.iteritems():
|
|
|
|
+ if key in ["browser.tabs.remote.autostart"]:
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ self.assertEqual(self.marionette.get_pref(key), value,
|
|
|
|
+ "Preference {} hasn't been set to {}".format(key, value))
|
|
|
|
+
|
|
|
|
+- @skip_if_desktop("Only runnable with Fennec")
|
|
|
|
+- def test_fennec_instance_preferences(self):
|
|
|
|
+- required_prefs = geckoinstance.FennecInstance.required_prefs
|
|
|
|
+-
|
|
|
|
+- for key, value in required_prefs.iteritems():
|
|
|
|
+- if key in ["browser.tabs.remote.autostart"]:
|
|
|
|
+- return
|
|
|
|
+-
|
|
|
|
+- self.assertEqual(self.marionette.get_pref(key), value,
|
|
|
|
+- "Preference {} hasn't been set to {}".format(key, value))
|
|
|
|
+-
|
|
|
|
+ def test_clear_pref(self):
|
|
|
|
+ self.assertIsNone(self.marionette.get_pref(self.prefs["bool"]))
|
|
|
|
+
|
|
|
|
+ self.marionette.set_pref(self.prefs["bool"], True)
|
|
|
|
+ self.assertTrue(self.marionette.get_pref(self.prefs["bool"]))
|
|
|
|
+
|
|
|
|
+ self.marionette.clear_pref(self.prefs["bool"])
|
|
|
|
+ self.assertIsNone(self.marionette.get_pref(self.prefs["bool"]))
|
|
diff --git a/testing/marionette/harness/requirements.txt b/testing/marionette/harness/requirements.txt
|
|
diff --git a/testing/marionette/harness/requirements.txt b/testing/marionette/harness/requirements.txt
|
|
--- a/testing/marionette/harness/requirements.txt
|
|
--- a/testing/marionette/harness/requirements.txt
|
|
+++ b/testing/marionette/harness/requirements.txt
|
|
+++ b/testing/marionette/harness/requirements.txt
|
|
@@ -705,6 +1422,48 @@ diff --git a/testing/marionette/harness/requirements.txt b/testing/marionette/ha
|
|
mozrunner >= 7.4.0
|
|
mozrunner >= 7.4.0
|
|
moztest >= 0.8
|
|
moztest >= 0.8
|
|
mozversion >= 2.1.0
|
|
mozversion >= 2.1.0
|
|
|
|
+diff --git a/testing/marionette/mach_commands.py b/testing/marionette/mach_commands.py
|
|
|
|
+--- a/testing/marionette/mach_commands.py
|
|
|
|
++++ b/testing/marionette/mach_commands.py
|
|
|
|
+@@ -16,17 +16,17 @@ from mach.decorators import (
|
|
|
|
+ Command,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ from mozbuild.base import (
|
|
|
|
+ MachCommandBase,
|
|
|
|
+ MachCommandConditions as conditions,
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+-SUPPORTED_APPS = ['firefox', 'android', 'thunderbird']
|
|
|
|
++SUPPORTED_APPS = ['firefox', 'thunderbird']
|
|
|
|
+
|
|
|
|
+ def create_parser_tests():
|
|
|
|
+ from marionette_harness.runtests import MarionetteArguments
|
|
|
|
+ from mozlog.structured import commandline
|
|
|
|
+ parser = MarionetteArguments()
|
|
|
|
+ commandline.add_logging_group(parser)
|
|
|
|
+ return parser
|
|
|
|
+
|
|
|
|
+@@ -80,17 +80,13 @@ class MarionetteTest(MachCommandBase):
|
|
|
|
+ if not tests:
|
|
|
|
+ if conditions.is_thunderbird(self):
|
|
|
|
+ tests = [os.path.join(self.topsrcdir,
|
|
|
|
+ "comm/testing/marionette/unit-tests.ini")]
|
|
|
|
+ else:
|
|
|
|
+ tests = [os.path.join(self.topsrcdir,
|
|
|
|
+ "testing/marionette/harness/marionette_harness/tests/unit-tests.ini")]
|
|
|
|
+
|
|
|
|
+- # Force disable e10s because it is not supported in Fennec
|
|
|
|
+- if kwargs.get("app") == "fennec":
|
|
|
|
+- kwargs["e10s"] = False
|
|
|
|
+-
|
|
|
|
+ if not kwargs.get("binary") and \
|
|
|
|
+ (conditions.is_firefox(self) or conditions.is_thunderbird(self)):
|
|
|
|
+ kwargs["binary"] = self.get_binary_path("app")
|
|
|
|
+
|
|
|
|
+- return run_marionette(tests, topsrcdir=self.topsrcdir, **kwargs)
|
|
|
|
+\ No newline at end of file
|
|
|
|
++ return run_marionette(tests, topsrcdir=self.topsrcdir, **kwargs)
|
|
diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py
|
|
diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py
|
|
--- a/testing/mochitest/mach_commands.py
|
|
--- a/testing/mochitest/mach_commands.py
|
|
+++ b/testing/mochitest/mach_commands.py
|
|
+++ b/testing/mochitest/mach_commands.py
|
|
@@ -1905,6 +2664,85 @@ deleted file mode 100644
|
|
-
|
|
-
|
|
-if __name__ == "__main__":
|
|
-if __name__ == "__main__":
|
|
- sys.exit(main())
|
|
- sys.exit(main())
|
|
|
|
+diff --git a/testing/mozbase/docs/mozrunner.rst b/testing/mozbase/docs/mozrunner.rst
|
|
|
|
+--- a/testing/mozbase/docs/mozrunner.rst
|
|
|
|
++++ b/testing/mozbase/docs/mozrunner.rst
|
|
|
|
+@@ -102,43 +102,16 @@ gecko process.
|
|
|
|
+
|
|
|
|
+ waiting = 0
|
|
|
|
+ while runner.wait(timeout=1) is None:
|
|
|
|
+ waiting += 1
|
|
|
|
+ print("Been waiting for %d seconds so far.." % waiting)
|
|
|
|
+ assert waiting <= 100
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-Using a device runner
|
|
|
|
+----------------------
|
|
|
|
+-
|
|
|
|
+-The previous examples used a GeckoRuntimeRunner. If you want to control a
|
|
|
|
+-gecko process on a remote device, you need to use a DeviceRunner. The api is
|
|
|
|
+-nearly identical except you don't pass in a binary, instead you create a device
|
|
|
|
+-object. For example to run Firefox for Android on the emulator, you might do:
|
|
|
|
+-
|
|
|
|
+-.. code-block:: python
|
|
|
|
+-
|
|
|
|
+- from mozrunner import FennecEmulatorRunner
|
|
|
|
+-
|
|
|
|
+- avd_home = 'path/to/avd'
|
|
|
|
+- runner = FennecEmulatorRunner(app='org.mozilla.fennec', avd_home=avd_home)
|
|
|
|
+- runner.start()
|
|
|
|
+- runner.wait()
|
|
|
|
+-
|
|
|
|
+-Device runners have a `device` object. Remember that the gecko process runs on
|
|
|
|
+-the device. In the case of the emulator, it is possible to start the
|
|
|
|
+-device independently of the gecko process.
|
|
|
|
+-
|
|
|
|
+-.. code-block:: python
|
|
|
|
+-
|
|
|
|
+- runner.device.start() # launches the emulator
|
|
|
|
+- runner.start() # stops the gecko process (if started), installs the profile, (re)starts the gecko process
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ Runner API Documentation
|
|
|
|
+ ------------------------
|
|
|
|
+
|
|
|
|
+ Application Runners
|
|
|
|
+ ~~~~~~~~~~~~~~~~~~~
|
|
|
|
+ .. automodule:: mozrunner.runners
|
|
|
|
+ :members:
|
|
|
|
+
|
|
|
|
+@@ -153,31 +126,8 @@ GeckoRuntimeRunner
|
|
|
|
+ :show-inheritance:
|
|
|
|
+ :members:
|
|
|
|
+
|
|
|
|
+ BlinkRuntimeRunner
|
|
|
|
+ ~~~~~~~~~~~~~~~~~~
|
|
|
|
+ .. autoclass:: mozrunner.base.BlinkRuntimeRunner
|
|
|
|
+ :show-inheritance:
|
|
|
|
+ :members:
|
|
|
|
+-
|
|
|
|
+-DeviceRunner
|
|
|
|
+-~~~~~~~~~~~~
|
|
|
|
+-.. autoclass:: mozrunner.base.DeviceRunner
|
|
|
|
+- :show-inheritance:
|
|
|
|
+- :members:
|
|
|
|
+-
|
|
|
|
+-Device API Documentation
|
|
|
|
+-------------------------
|
|
|
|
+-
|
|
|
|
+-Generally using the device classes directly shouldn't be required, but in some
|
|
|
|
+-cases it may be desirable.
|
|
|
|
+-
|
|
|
|
+-Device
|
|
|
|
+-~~~~~~
|
|
|
|
+-.. autoclass:: mozrunner.devices.Device
|
|
|
|
+- :members:
|
|
|
|
+-
|
|
|
|
+-EmulatorAVD
|
|
|
|
+-~~~~~~~~~~~
|
|
|
|
+-.. autoclass:: mozrunner.devices.EmulatorAVD
|
|
|
|
+- :show-inheritance:
|
|
|
|
+- :members:
|
|
diff --git a/testing/mozbase/moz.build b/testing/mozbase/moz.build
|
|
diff --git a/testing/mozbase/moz.build b/testing/mozbase/moz.build
|
|
--- a/testing/mozbase/moz.build
|
|
--- a/testing/mozbase/moz.build
|
|
+++ b/testing/mozbase/moz.build
|
|
+++ b/testing/mozbase/moz.build
|
|
@@ -7335,26 +8173,2853 @@ deleted file mode 100644
|
|
- dm = mozdevice.dmcli:cli
|
|
- dm = mozdevice.dmcli:cli
|
|
- """,
|
|
- """,
|
|
- )
|
|
- )
|
|
-diff --git a/testing/mozbase/mozrunner/setup.py b/testing/mozbase/mozrunner/setup.py
|
|
|
|
---- a/testing/mozbase/mozrunner/setup.py
|
|
|
|
-+++ b/testing/mozbase/mozrunner/setup.py
|
|
|
|
-@@ -7,17 +7,16 @@ from __future__ import absolute_import
|
|
|
|
- from setuptools import setup, find_packages
|
|
|
|
|
|
+diff --git a/testing/mozbase/mozlog/mozlog/formatters/html/html.py b/testing/mozbase/mozlog/mozlog/formatters/html/html.py
|
|
|
|
+--- a/testing/mozbase/mozlog/mozlog/formatters/html/html.py
|
|
|
|
++++ b/testing/mozbase/mozlog/mozlog/formatters/html/html.py
|
|
|
|
+@@ -82,22 +82,16 @@ class HTMLFormatter(base.BaseFormatter):
|
|
|
|
|
|
- PACKAGE_NAME = 'mozrunner'
|
|
|
|
- PACKAGE_VERSION = '7.4.0'
|
|
|
|
|
|
+ if version_info.get("gaia_changeset"):
|
|
|
|
+ self.env["Gaia revision"] = html.a(
|
|
|
|
+ version_info.get("gaia_changeset")[:12],
|
|
|
|
+ href="https://github.com/mozilla-b2g/gaia/commit/%s" % version_info.get(
|
|
|
|
+ "gaia_changeset"),
|
|
|
|
+ target="_blank")
|
|
|
|
|
|
- desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
|
|
|
|
|
|
+- device_info = data.get("device_info")
|
|
|
|
+- if device_info:
|
|
|
|
+- self.env["Device uptime"] = device_info.get("uptime")
|
|
|
|
+- self.env["Device memory"] = device_info.get("memtotal")
|
|
|
|
+- self.env["Device serial"] = device_info.get("id")
|
|
|
|
+-
|
|
|
|
+ def suite_end(self, data):
|
|
|
|
+ self.suite_times["end"] = data["time"]
|
|
|
|
+ return self.generate_html()
|
|
|
|
+
|
|
|
|
+ def test_start(self, data):
|
|
|
|
+ self.start_times[data["test"]] = data["time"]
|
|
|
|
+
|
|
|
|
+ def test_end(self, data):
|
|
|
|
+diff --git a/testing/mozbase/mozlog/mozlog/structuredlog.py b/testing/mozbase/mozlog/mozlog/structuredlog.py
|
|
|
|
+--- a/testing/mozbase/mozlog/mozlog/structuredlog.py
|
|
|
|
++++ b/testing/mozbase/mozlog/mozlog/structuredlog.py
|
|
|
|
+@@ -298,27 +298,25 @@ class StructuredLogger(object):
|
|
|
|
+ return False
|
|
|
|
+ self._state.suite_started = False
|
|
|
|
+ return True
|
|
|
|
+
|
|
|
|
+ @log_action(TestList("tests"),
|
|
|
|
+ Unicode("name", default=None, optional=True),
|
|
|
|
+ Dict(Any, "run_info", default=None, optional=True),
|
|
|
|
+ Dict(Any, "version_info", default=None, optional=True),
|
|
|
|
+- Dict(Any, "device_info", default=None, optional=True),
|
|
|
|
+ Dict(Any, "extra", default=None, optional=True))
|
|
|
|
+ def suite_start(self, data):
|
|
|
|
+ """Log a suite_start message
|
|
|
|
+
|
|
|
|
+ :param dict tests: Test identifiers that will be run in the suite, keyed by group name.
|
|
|
|
+ :param str name: Optional name to identify the suite.
|
|
|
|
+ :param dict run_info: Optional information typically provided by mozinfo.
|
|
|
|
+ :param dict version_info: Optional target application version information provided
|
|
|
|
+ by mozversion.
|
|
|
|
+- :param dict device_info: Optional target device information provided by mozdevice.
|
|
|
|
+ """
|
|
|
|
+ if not self._ensure_suite_state('suite_start', data):
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ self._log_data("suite_start", data)
|
|
|
|
+
|
|
|
|
+ @log_action(Dict(Any, "extra", default=None, optional=True))
|
|
|
|
+ def suite_end(self, data):
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/__init__.py b/testing/mozbase/mozrunner/mozrunner/__init__.py
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/__init__.py
|
|
|
|
++++ b/testing/mozbase/mozrunner/mozrunner/__init__.py
|
|
|
|
+@@ -5,10 +5,9 @@
|
|
|
|
+
|
|
|
|
+ from __future__ import absolute_import
|
|
|
|
+
|
|
|
|
+ from .cli import *
|
|
|
|
+ from .errors import *
|
|
|
|
+ from .runners import *
|
|
|
|
+
|
|
|
|
+ import mozrunner.base
|
|
|
|
+-import mozrunner.devices
|
|
|
|
+ import mozrunner.utils
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/application.py b/testing/mozbase/mozrunner/mozrunner/application.py
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/application.py
|
|
|
|
++++ b/testing/mozbase/mozrunner/mozrunner/application.py
|
|
|
|
+@@ -4,130 +4,42 @@
|
|
|
|
+
|
|
|
|
+ from __future__ import absolute_import
|
|
|
|
+
|
|
|
|
+ from abc import ABCMeta, abstractmethod
|
|
|
|
+ from distutils.spawn import find_executable
|
|
|
|
+ import os
|
|
|
|
+ import posixpath
|
|
|
|
+
|
|
|
|
+-from mozdevice import ADBAndroid
|
|
|
|
+ from mozprofile import (
|
|
|
|
+ Profile,
|
|
|
|
+ ChromeProfile,
|
|
|
|
+ FirefoxProfile,
|
|
|
|
+ ThunderbirdProfile
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ here = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def get_app_context(appname):
|
|
|
|
+ context_map = {
|
|
|
|
+ 'chrome': ChromeContext,
|
|
|
|
+ 'default': DefaultContext,
|
|
|
|
+- 'fennec': FennecContext,
|
|
|
|
+ 'firefox': FirefoxContext,
|
|
|
|
+ 'thunderbird': ThunderbirdContext,
|
|
|
|
+ }
|
|
|
|
+ if appname not in context_map:
|
|
|
|
+ raise KeyError("Application '%s' not supported!" % appname)
|
|
|
|
+ return context_map[appname]
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class DefaultContext(object):
|
|
|
|
+ profile_class = Profile
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-class RemoteContext(object):
|
|
|
|
+- __metaclass__ = ABCMeta
|
|
|
|
+- device = None
|
|
|
|
+- _remote_profile = None
|
|
|
|
+- _adb = None
|
|
|
|
+- profile_class = Profile
|
|
|
|
+- _bindir = None
|
|
|
|
+- remote_test_root = ''
|
|
|
|
+- remote_process = None
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def bindir(self):
|
|
|
|
+- if self._bindir is None:
|
|
|
|
+- paths = [find_executable('emulator')]
|
|
|
|
+- paths = [p for p in paths if p is not None if os.path.isfile(p)]
|
|
|
|
+- if not paths:
|
|
|
|
+- self._bindir = ''
|
|
|
|
+- else:
|
|
|
|
+- self._bindir = os.path.dirname(paths[0])
|
|
|
|
+- return self._bindir
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def adb(self):
|
|
|
|
+- if not self._adb:
|
|
|
|
+- paths = [os.environ.get('ADB'),
|
|
|
|
+- os.environ.get('ADB_PATH'),
|
|
|
|
+- self.which('adb')]
|
|
|
|
+- paths = [p for p in paths if p is not None if os.path.isfile(p)]
|
|
|
|
+- 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 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)
|
|
|
|
+- if self.bindir is not None and os.path.abspath(self.bindir) not in paths:
|
|
|
|
+- paths.insert(0, os.path.abspath(self.bindir))
|
|
|
|
+- os.environ['PATH'] = os.pathsep.join(paths)
|
|
|
|
+-
|
|
|
|
+- return find_executable(binary)
|
|
|
|
+-
|
|
|
|
+- @abstractmethod
|
|
|
|
+- def stop_application(self):
|
|
|
|
+- """ 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, device_serial=None):
|
|
|
|
+- self._adb = adb_path
|
|
|
|
+- self.avd_home = avd_home
|
|
|
|
+- self.remote_process = app
|
|
|
|
+- self.device_serial = device_serial
|
|
|
|
+- self.device = ADBAndroid(adb=self.adb, device=device_serial)
|
|
|
|
+-
|
|
|
|
+- def stop_application(self):
|
|
|
|
+- self.device.stop_application(self.remote_process)
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def remote_test_root(self):
|
|
|
|
+- if not self._remote_test_root:
|
|
|
|
+- 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(
|
|
|
|
+- '/data', 'data', self.remote_process,
|
|
|
|
+- 'files', 'mozilla', 'profiles.ini'
|
|
|
|
+- )
|
|
|
|
+- return self._remote_profiles_ini
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ class FirefoxContext(object):
|
|
|
|
+ profile_class = FirefoxProfile
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class ThunderbirdContext(object):
|
|
|
|
+ profile_class = ThunderbirdProfile
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/base/__init__.py b/testing/mozbase/mozrunner/mozrunner/base/__init__.py
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/base/__init__.py
|
|
|
|
++++ b/testing/mozbase/mozrunner/mozrunner/base/__init__.py
|
|
|
|
+@@ -1,6 +1,5 @@
|
|
|
|
+ # flake8: noqa
|
|
|
|
+ from __future__ import absolute_import
|
|
|
|
+
|
|
|
|
+ from .runner import BaseRunner
|
|
|
|
+-from .device import DeviceRunner, FennecRunner
|
|
|
|
+ from .browser import GeckoRuntimeRunner, BlinkRuntimeRunner
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/base/device.py b/testing/mozbase/mozrunner/mozrunner/base/device.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/base/device.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,190 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import, print_function
|
|
|
|
+-
|
|
|
|
+-import codecs
|
|
|
|
+-import datetime
|
|
|
|
+-import re
|
|
|
|
+-import signal
|
|
|
|
+-import six
|
|
|
|
+-import sys
|
|
|
|
+-import tempfile
|
|
|
|
+-import time
|
|
|
|
+-
|
|
|
|
+-import mozfile
|
|
|
|
+-
|
|
|
|
+-from .runner import BaseRunner
|
|
|
|
+-from ..devices import BaseEmulator
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class DeviceRunner(BaseRunner):
|
|
|
|
+- """
|
|
|
|
+- The base runner class used for running gecko on
|
|
|
|
+- remote devices (or emulators), such as B2G.
|
|
|
|
+- """
|
|
|
|
+- env = {'MOZ_CRASHREPORTER': '1',
|
|
|
|
+- 'MOZ_CRASHREPORTER_NO_REPORT': '1',
|
|
|
|
+- 'MOZ_CRASHREPORTER_SHUTDOWN': '1',
|
|
|
|
+- 'MOZ_HIDE_RESULTS_TABLE': '1',
|
|
|
|
+- 'MOZ_LOG': 'signaling:3,mtransport:4,DataChannel:4,jsep:4,MediaPipelineFactory: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):
|
|
|
|
+- cmd = [self.app_ctx.adb]
|
|
|
|
+- if self.app_ctx.dm._deviceSerial:
|
|
|
|
+- cmd.extend(['-s', self.app_ctx.dm._deviceSerial])
|
|
|
|
+- 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:
|
|
|
|
+- self.device.start()
|
|
|
|
+- self.device.connect()
|
|
|
|
+- self.device.setup_profile(self.profile)
|
|
|
|
+-
|
|
|
|
+- # TODO: this doesn't work well when the device is running but dropped
|
|
|
|
+- # wifi for some reason. It would be good to probe the state of the device
|
|
|
|
+- # to see if we have the homescreen running, or something, before waiting here
|
|
|
|
+- self.device.wait_for_net()
|
|
|
|
+-
|
|
|
|
+- if not self.device.wait_for_net():
|
|
|
|
+- raise Exception("Network did not come up when starting device")
|
|
|
|
+-
|
|
|
|
+- pid = BaseRunner.start(self, *args, **kwargs)
|
|
|
|
+-
|
|
|
|
+- timeout = 10 # seconds
|
|
|
|
+- starttime = datetime.datetime.now()
|
|
|
|
+- while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
|
|
|
|
+- if self.is_running():
|
|
|
|
+- break
|
|
|
|
+- time.sleep(1)
|
|
|
|
+- else:
|
|
|
|
+- 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):
|
|
|
|
+- def _wait_for_shutdown(pid, timeout=10):
|
|
|
|
+- start_time = datetime.datetime.now()
|
|
|
|
+- end_time = datetime.timedelta(seconds=timeout)
|
|
|
|
+- while datetime.datetime.now() - start_time < end_time:
|
|
|
|
+- if self.is_running() != pid:
|
|
|
|
+- return True
|
|
|
|
+- time.sleep(1)
|
|
|
|
+- return False
|
|
|
|
+-
|
|
|
|
+- remote_pid = self.is_running()
|
|
|
|
+- if remote_pid:
|
|
|
|
+- self.app_ctx.dm.killProcess(
|
|
|
|
+- self.app_ctx.remote_process, sig=sig)
|
|
|
|
+- if not _wait_for_shutdown(remote_pid) 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
|
|
|
|
+- remote_pid = self.is_running() or remote_pid
|
|
|
|
+- self.app_ctx.stop_application()
|
|
|
|
+- if not _wait_for_shutdown(remote_pid):
|
|
|
|
+- print("timed out waiting for '%s' process to exit".format(
|
|
|
|
+- self.app_ctx.remote_process))
|
|
|
|
+-
|
|
|
|
+- def is_running(self):
|
|
|
|
+- return self.app_ctx.dm.processExist(self.app_ctx.remote_process)
|
|
|
|
+-
|
|
|
|
+- 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 []
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def command(self):
|
|
|
|
+- cmd = [self.app_ctx.adb]
|
|
|
|
+- if self.app_ctx.dm._deviceSerial:
|
|
|
|
+- cmd.extend(["-s", self.app_ctx.dm._deviceSerial])
|
|
|
|
+- 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()):
|
|
|
|
+- am_subcommand.extend(["--es", "env%d" % count, "%s=%s" % (k, v)])
|
|
|
|
+- cmd.append("%s" % " ".join(am_subcommand))
|
|
|
|
+- return cmd
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/__init__.py b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,15 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import
|
|
|
|
+-
|
|
|
|
+-from .emulator import BaseEmulator, Emulator, EmulatorAVD
|
|
|
|
+-from .base import Device
|
|
|
|
+-
|
|
|
|
+-from mozrunner.devices import emulator_battery
|
|
|
|
+-from mozrunner.devices import emulator_geo
|
|
|
|
+-from mozrunner.devices import emulator_screen
|
|
|
|
+-
|
|
|
|
+-__all__ = ['BaseEmulator', 'Emulator', 'EmulatorAVD', 'Device',
|
|
|
|
+- 'emulator_battery', 'emulator_geo', 'emulator_screen']
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/android_device.py b/testing/mozbase/mozrunner/mozrunner/devices/android_device.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/android_device.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,810 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import, print_function
|
|
|
|
+-
|
|
|
|
+-import fileinput
|
|
|
|
+-import glob
|
|
|
|
+-import os
|
|
|
|
+-import platform
|
|
|
|
+-import psutil
|
|
|
|
+-import shutil
|
|
|
|
+-import signal
|
|
|
|
+-import sys
|
|
|
|
+-import telnetlib
|
|
|
|
+-import time
|
|
|
|
+-import urlparse
|
|
|
|
+-import urllib2
|
|
|
|
+-from distutils.spawn import find_executable
|
|
|
|
+-
|
|
|
|
+-from mozdevice import DeviceManagerADB, DMError
|
|
|
|
+-from mozprocess import ProcessHandler
|
|
|
|
+-
|
|
|
|
+-EMULATOR_HOME_DIR = os.path.join(os.path.expanduser('~'), '.mozbuild', 'android-device')
|
|
|
|
+-
|
|
|
|
+-EMULATOR_AUTH_FILE = os.path.join(os.path.expanduser('~'), '.emulator_console_auth_token')
|
|
|
|
+-
|
|
|
|
+-TOOLTOOL_URL = 'https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py'
|
|
|
|
+-
|
|
|
|
+-TRY_URL = 'https://hg.mozilla.org/try/raw-file/default'
|
|
|
|
+-
|
|
|
|
+-MANIFEST_PATH = 'testing/config/tooltool-manifests'
|
|
|
|
+-
|
|
|
|
+-verbose_logging = False
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class AvdInfo(object):
|
|
|
|
+- """
|
|
|
|
+- Simple class to contain an AVD description.
|
|
|
|
+- """
|
|
|
|
+-
|
|
|
|
+- def __init__(self, description, name, tooltool_manifest, extra_args,
|
|
|
|
+- x86):
|
|
|
|
+- self.description = description
|
|
|
|
+- self.name = name
|
|
|
|
+- self.tooltool_manifest = tooltool_manifest
|
|
|
|
+- self.extra_args = extra_args
|
|
|
|
+- self.x86 = x86
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-"""
|
|
|
|
+- A dictionary to map an AVD type to a description of that type of AVD.
|
|
|
|
+-
|
|
|
|
+- There is one entry for each type of AVD used in Mozilla automated tests
|
|
|
|
+- and the parameters for each reflect those used in mozharness.
|
|
|
|
+-"""
|
|
|
|
+-AVD_DICT = {
|
|
|
|
+- '4.3': AvdInfo('Android 4.3',
|
|
|
|
+- 'mozemulator-4.3',
|
|
|
|
+- 'testing/config/tooltool-manifests/androidarm_4_3/mach-emulator.manifest',
|
|
|
|
+- ['-show-kernel', '-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket'],
|
|
|
|
+- False),
|
|
|
|
+- '6.0': AvdInfo('Android 6.0',
|
|
|
|
+- 'mozemulator-6.0',
|
|
|
|
+- 'testing/config/tooltool-manifests/androidarm_6_0/mach-emulator.manifest',
|
|
|
|
+- ['-show-kernel', '-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket'],
|
|
|
|
+- False),
|
|
|
|
+- '7.0': AvdInfo('Android 7.0',
|
|
|
|
+- 'mozemulator-7.0',
|
|
|
|
+- 'testing/config/tooltool-manifests/androidarm_7_0/mach-emulator.manifest',
|
|
|
|
+- ['-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket',
|
|
|
|
+- '-ranchu',
|
|
|
|
+- '-qemu', '-m', '2048'],
|
|
|
|
+- False),
|
|
|
|
+- 'x86': AvdInfo('Android 4.2 x86',
|
|
|
|
+- 'mozemulator-x86',
|
|
|
|
+- 'testing/config/tooltool-manifests/androidx86/mach-emulator.manifest',
|
|
|
|
+- ['-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket',
|
|
|
|
+- '-qemu', '-m', '1024', '-enable-kvm'],
|
|
|
|
+- True),
|
|
|
|
+- 'x86-6.0': AvdInfo('Android 6.0 x86',
|
|
|
|
+- 'mozemulator-x86-6.0',
|
|
|
|
+- 'testing/config/tooltool-manifests/androidx86_6_0/mach-emulator.manifest',
|
|
|
|
+- ['-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket',
|
|
|
|
+- '-ranchu',
|
|
|
|
+- '-qemu', '-m', '2048'],
|
|
|
|
+- True)
|
|
|
|
+-}
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def verify_android_device(build_obj, install=False, xre=False, debugger=False, verbose=False):
|
|
|
|
+- """
|
|
|
|
+- Determine if any Android device is connected via adb.
|
|
|
|
+- If no device is found, prompt to start an emulator.
|
|
|
|
+- If a device is found or an emulator started and 'install' is
|
|
|
|
+- specified, also check whether Firefox is installed on the
|
|
|
|
+- device; if not, prompt to install Firefox.
|
|
|
|
+- If 'xre' is specified, also check with MOZ_HOST_BIN is set
|
|
|
|
+- to a valid xre/host-utils directory; if not, prompt to set
|
|
|
|
+- one up.
|
|
|
|
+- If 'debugger' is specified, also check that JimDB is installed;
|
|
|
|
+- if JimDB is not found, prompt to set up JimDB.
|
|
|
|
+- Returns True if the emulator was started or another device was
|
|
|
|
+- already connected.
|
|
|
|
+- """
|
|
|
|
+- device_verified = False
|
|
|
|
+- emulator = AndroidEmulator('*', substs=build_obj.substs, verbose=verbose)
|
|
|
|
+- devices = emulator.dm.devices()
|
|
|
|
+- if (len(devices) > 0) and ('device' in [d[1] for d in devices]):
|
|
|
|
+- device_verified = True
|
|
|
|
+- elif emulator.is_available():
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "No Android devices connected. Start an emulator? (Y/n) ").strip()
|
|
|
|
+- if response.lower().startswith('y') or response == '':
|
|
|
|
+- if not emulator.check_avd():
|
|
|
|
+- _log_info("Fetching AVD. This may take a while...")
|
|
|
|
+- emulator.update_avd()
|
|
|
|
+- _log_info("Starting emulator running %s..." %
|
|
|
|
+- emulator.get_avd_description())
|
|
|
|
+- emulator.start()
|
|
|
|
+- emulator.wait_for_start()
|
|
|
|
+- device_verified = True
|
|
|
|
+-
|
|
|
|
+- if device_verified and install:
|
|
|
|
+- # Determine if Firefox is installed on the device; if not,
|
|
|
|
+- # prompt to install. This feature allows a test command to
|
|
|
|
+- # launch an emulator, install Firefox, and proceed with testing
|
|
|
|
+- # in one operation. It is also a basic safeguard against other
|
|
|
|
+- # cases where testing is requested but Firefox installation has
|
|
|
|
+- # been forgotten.
|
|
|
|
+- # If Firefox is installed, there is no way to determine whether
|
|
|
|
+- # the current build is installed, and certainly no way to
|
|
|
|
+- # determine if the installed build is the desired build.
|
|
|
|
+- # Installing every time is problematic because:
|
|
|
|
+- # - it prevents testing against other builds (downloaded apk)
|
|
|
|
+- # - installation may take a couple of minutes.
|
|
|
|
+- installed = emulator.dm.shellCheckOutput(['pm', 'list',
|
|
|
|
+- 'packages', 'org.mozilla.'])
|
|
|
|
+- if 'fennec' not in installed and 'firefox' not in installed:
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "It looks like Firefox is not installed on this device.\n"
|
|
|
|
+- "Install Firefox? (Y/n) ").strip()
|
|
|
|
+- if response.lower().startswith('y') or response == '':
|
|
|
|
+- _log_info("Installing Firefox. This may take a while...")
|
|
|
|
+- build_obj._run_make(directory=".", target='install',
|
|
|
|
+- ensure_exit_code=False)
|
|
|
|
+-
|
|
|
|
+- if device_verified and xre:
|
|
|
|
+- # Check whether MOZ_HOST_BIN has been set to a valid xre; if not,
|
|
|
|
+- # prompt to install one.
|
|
|
|
+- xre_path = os.environ.get('MOZ_HOST_BIN')
|
|
|
|
+- err = None
|
|
|
|
+- if not xre_path:
|
|
|
|
+- err = "environment variable MOZ_HOST_BIN is not set to a directory" \
|
|
|
|
+- "containing host xpcshell"
|
|
|
|
+- elif not os.path.isdir(xre_path):
|
|
|
|
+- err = '$MOZ_HOST_BIN does not specify a directory'
|
|
|
|
+- elif not os.path.isfile(os.path.join(xre_path, 'xpcshell')):
|
|
|
|
+- err = '$MOZ_HOST_BIN/xpcshell does not exist'
|
|
|
|
+- if err:
|
|
|
|
+- xre_path = glob.glob(os.path.join(EMULATOR_HOME_DIR, 'host-utils*'))
|
|
|
|
+- for path in xre_path:
|
|
|
|
+- if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'xpcshell')):
|
|
|
|
+- os.environ['MOZ_HOST_BIN'] = path
|
|
|
|
+- err = None
|
|
|
|
+- break
|
|
|
|
+- if err:
|
|
|
|
+- _log_info("Host utilities not found: %s" % err)
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Download and setup your host utilities? (Y/n) ").strip()
|
|
|
|
+- if response.lower().startswith('y') or response == '':
|
|
|
|
+- _log_info("Installing host utilities. This may take a while...")
|
|
|
|
+- host_platform = _get_host_platform()
|
|
|
|
+- if host_platform:
|
|
|
|
+- path = os.path.join(MANIFEST_PATH, host_platform, 'hostutils.manifest')
|
|
|
|
+- _get_tooltool_manifest(build_obj.substs, path, EMULATOR_HOME_DIR,
|
|
|
|
+- 'releng.manifest')
|
|
|
|
+- _tooltool_fetch()
|
|
|
|
+- xre_path = glob.glob(os.path.join(EMULATOR_HOME_DIR, 'host-utils*'))
|
|
|
|
+- for path in xre_path:
|
|
|
|
+- if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'xpcshell')):
|
|
|
|
+- os.environ['MOZ_HOST_BIN'] = path
|
|
|
|
+- err = None
|
|
|
|
+- break
|
|
|
|
+- if err:
|
|
|
|
+- _log_warning("Unable to install host utilities.")
|
|
|
|
+- else:
|
|
|
|
+- _log_warning(
|
|
|
|
+- "Unable to install host utilities -- your platform is not supported!")
|
|
|
|
+-
|
|
|
|
+- if debugger:
|
|
|
|
+- # Optionally set up JimDB. See https://wiki.mozilla.org/Mobile/Fennec/Android/GDB.
|
|
|
|
+- build_platform = _get_device_platform(build_obj.substs)
|
|
|
|
+- jimdb_path = os.path.join(EMULATOR_HOME_DIR, 'jimdb-%s' % build_platform)
|
|
|
|
+- jimdb_utils_path = os.path.join(jimdb_path, 'utils')
|
|
|
|
+- gdb_path = os.path.join(jimdb_path, 'bin', 'gdb')
|
|
|
|
+- err = None
|
|
|
|
+- if not os.path.isdir(jimdb_path):
|
|
|
|
+- err = '%s does not exist' % jimdb_path
|
|
|
|
+- elif not os.path.isfile(gdb_path):
|
|
|
|
+- err = '%s not found' % gdb_path
|
|
|
|
+- if err:
|
|
|
|
+- _log_info("JimDB (%s) not found: %s" % (build_platform, err))
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Download and setup JimDB (%s)? (Y/n) " % build_platform).strip()
|
|
|
|
+- if response.lower().startswith('y') or response == '':
|
|
|
|
+- host_platform = _get_host_platform()
|
|
|
|
+- if host_platform:
|
|
|
|
+- _log_info(
|
|
|
|
+- "Installing JimDB (%s/%s). This may take a while..." % (host_platform,
|
|
|
|
+- build_platform))
|
|
|
|
+- path = os.path.join(MANIFEST_PATH, host_platform,
|
|
|
|
+- 'jimdb-%s.manifest' % build_platform)
|
|
|
|
+- _get_tooltool_manifest(build_obj.substs, path,
|
|
|
|
+- EMULATOR_HOME_DIR, 'releng.manifest')
|
|
|
|
+- _tooltool_fetch()
|
|
|
|
+- if os.path.isfile(gdb_path):
|
|
|
|
+- # Get JimDB utilities from git repository
|
|
|
|
+- proc = ProcessHandler(['git', 'pull'], cwd=jimdb_utils_path)
|
|
|
|
+- proc.run()
|
|
|
|
+- git_pull_complete = False
|
|
|
|
+- try:
|
|
|
|
+- proc.wait()
|
|
|
|
+- if proc.proc.returncode == 0:
|
|
|
|
+- git_pull_complete = True
|
|
|
|
+- except Exception:
|
|
|
|
+- if proc.poll() is None:
|
|
|
|
+- proc.kill(signal.SIGTERM)
|
|
|
|
+- if not git_pull_complete:
|
|
|
|
+- _log_warning("Unable to update JimDB utils from git -- "
|
|
|
|
+- "some JimDB features may be unavailable.")
|
|
|
|
+- else:
|
|
|
|
+- _log_warning("Unable to install JimDB -- unable to fetch from tooltool.")
|
|
|
|
+- else:
|
|
|
|
+- _log_warning("Unable to install JimDB -- your platform is not supported!")
|
|
|
|
+- if os.path.isfile(gdb_path):
|
|
|
|
+- # sync gdbinit.local with build settings
|
|
|
|
+- _update_gdbinit(build_obj.substs, os.path.join(jimdb_utils_path, "gdbinit.local"))
|
|
|
|
+- # ensure JimDB is in system path, so that mozdebug can find it
|
|
|
|
+- bin_path = os.path.join(jimdb_path, 'bin')
|
|
|
|
+- os.environ['PATH'] = "%s:%s" % (bin_path, os.environ['PATH'])
|
|
|
|
+-
|
|
|
|
+- return device_verified
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def run_firefox_for_android(build_obj, params):
|
|
|
|
+- """
|
|
|
|
+- Launch Firefox for Android on the connected device.
|
|
|
|
+- Optional 'params' allow parameters to be passed to Firefox.
|
|
|
|
+- """
|
|
|
|
+- adb_path = _find_sdk_exe(build_obj.substs, 'adb', False)
|
|
|
|
+- if not adb_path:
|
|
|
|
+- adb_path = 'adb'
|
|
|
|
+- dm = DeviceManagerADB(autoconnect=False, adbPath=adb_path, retryLimit=1)
|
|
|
|
+- try:
|
|
|
|
+- #
|
|
|
|
+- # Construct an adb command similar to:
|
|
|
|
+- #
|
|
|
|
+- # $ adb shell am start -a android.activity.MAIN \
|
|
|
|
+- # -n org.mozilla.fennec_$USER \
|
|
|
|
+- # -d <url param> \
|
|
|
|
+- # --es args "<params>"
|
|
|
|
+- #
|
|
|
|
+- app = "%s/org.mozilla.gecko.BrowserApp" % build_obj.substs['ANDROID_PACKAGE_NAME']
|
|
|
|
+- cmd = ['am', 'start', '-a', 'android.activity.MAIN', '-n', app]
|
|
|
|
+- if params:
|
|
|
|
+- for p in params:
|
|
|
|
+- if urlparse.urlparse(p).scheme != "":
|
|
|
|
+- cmd.extend(['-d', p])
|
|
|
|
+- params.remove(p)
|
|
|
|
+- break
|
|
|
|
+- if params:
|
|
|
|
+- cmd.extend(['--es', 'args', '"%s"' % ' '.join(params)])
|
|
|
|
+- _log_debug(cmd)
|
|
|
|
+- output = dm.shellCheckOutput(cmd, timeout=10)
|
|
|
|
+- _log_info(output)
|
|
|
|
+- except DMError:
|
|
|
|
+- _log_warning("unable to launch Firefox for Android")
|
|
|
|
+- return 1
|
|
|
|
+- return 0
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def grant_runtime_permissions(build_obj):
|
|
|
|
+- """
|
|
|
|
+- Grant required runtime permissions to the specified app
|
|
|
|
+- (typically org.mozilla.fennec_$USER).
|
|
|
|
+- """
|
|
|
|
+- app = build_obj.substs['ANDROID_PACKAGE_NAME']
|
|
|
|
+- adb_path = _find_sdk_exe(build_obj.substs, 'adb', False)
|
|
|
|
+- if not adb_path:
|
|
|
|
+- adb_path = 'adb'
|
|
|
|
+- dm = DeviceManagerADB(autoconnect=False, adbPath=adb_path, retryLimit=1)
|
|
|
|
+- dm.default_timeout = 10
|
|
|
|
+- try:
|
|
|
|
+- sdk_level = dm.shellCheckOutput(['getprop', 'ro.build.version.sdk'])
|
|
|
|
+- if sdk_level and int(sdk_level) >= 23:
|
|
|
|
+- _log_info("Granting important runtime permissions to %s" % app)
|
|
|
|
+- dm.shellCheckOutput(['pm', 'grant', app, 'android.permission.WRITE_EXTERNAL_STORAGE'])
|
|
|
|
+- dm.shellCheckOutput(['pm', 'grant', app, 'android.permission.READ_EXTERNAL_STORAGE'])
|
|
|
|
+- dm.shellCheckOutput(['pm', 'grant', app, 'android.permission.ACCESS_FINE_LOCATION'])
|
|
|
|
+- dm.shellCheckOutput(['pm', 'grant', app, 'android.permission.CAMERA'])
|
|
|
|
+- except DMError:
|
|
|
|
+- _log_warning("Unable to grant runtime permissions to %s" % app)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class AndroidEmulator(object):
|
|
|
|
+-
|
|
|
|
+- """
|
|
|
|
+- Support running the Android emulator with an AVD from Mozilla
|
|
|
|
+- test automation.
|
|
|
|
+-
|
|
|
|
+- Example usage:
|
|
|
|
+- emulator = AndroidEmulator()
|
|
|
|
+- if not emulator.is_running() and emulator.is_available():
|
|
|
|
+- if not emulator.check_avd():
|
|
|
|
+- warn("this may take a while...")
|
|
|
|
+- emulator.update_avd()
|
|
|
|
+- emulator.start()
|
|
|
|
+- emulator.wait_for_start()
|
|
|
|
+- emulator.wait()
|
|
|
|
+- """
|
|
|
|
+-
|
|
|
|
+- def __init__(self, avd_type='4.3', verbose=False, substs=None, device_serial=None):
|
|
|
|
+- global verbose_logging
|
|
|
|
+- self.emulator_log = None
|
|
|
|
+- self.emulator_path = 'emulator'
|
|
|
|
+- verbose_logging = verbose
|
|
|
|
+- self.substs = substs
|
|
|
|
+- self.avd_type = self._get_avd_type(avd_type)
|
|
|
|
+- self.avd_info = AVD_DICT[self.avd_type]
|
|
|
|
+- self.gpu = True
|
|
|
|
+- self.restarted = False
|
|
|
|
+- adb_path = _find_sdk_exe(substs, 'adb', False)
|
|
|
|
+- if not adb_path:
|
|
|
|
+- adb_path = 'adb'
|
|
|
|
+- self.dm = DeviceManagerADB(autoconnect=False, adbPath=adb_path, retryLimit=1,
|
|
|
|
+- deviceSerial=device_serial)
|
|
|
|
+- self.dm.default_timeout = 10
|
|
|
|
+- _log_debug("Running on %s" % platform.platform())
|
|
|
|
+- _log_debug("Emulator created with type %s" % self.avd_type)
|
|
|
|
+-
|
|
|
|
+- def __del__(self):
|
|
|
|
+- if self.emulator_log:
|
|
|
|
+- self.emulator_log.close()
|
|
|
|
+-
|
|
|
|
+- def is_running(self):
|
|
|
|
+- """
|
|
|
|
+- Returns True if the Android emulator is running.
|
|
|
|
+- """
|
|
|
|
+- for proc in psutil.process_iter():
|
|
|
|
+- name = proc.name()
|
|
|
|
+- # On some platforms, "emulator" may start an emulator with
|
|
|
|
+- # process name "emulator64-arm" or similar.
|
|
|
|
+- if name and name.startswith('emulator'):
|
|
|
|
+- return True
|
|
|
|
+- return False
|
|
|
|
+-
|
|
|
|
+- def is_available(self):
|
|
|
|
+- """
|
|
|
|
+- Returns True if an emulator executable is found.
|
|
|
|
+- """
|
|
|
|
+- found = False
|
|
|
|
+- emulator_path = _find_sdk_exe(self.substs, 'emulator', True)
|
|
|
|
+- if emulator_path:
|
|
|
|
+- self.emulator_path = emulator_path
|
|
|
|
+- found = True
|
|
|
|
+- return found
|
|
|
|
+-
|
|
|
|
+- def check_avd(self, force=False):
|
|
|
|
+- """
|
|
|
|
+- Determine if the AVD is already installed locally.
|
|
|
|
+- (This is usually used to determine if update_avd() is likely
|
|
|
|
+- to require a download; it is a convenient way of determining
|
|
|
|
+- whether a 'this may take a while' warning is warranted.)
|
|
|
|
+-
|
|
|
|
+- Returns True if the AVD is installed.
|
|
|
|
+- """
|
|
|
|
+- avd = os.path.join(
|
|
|
|
+- EMULATOR_HOME_DIR, 'avd', self.avd_info.name + '.avd')
|
|
|
|
+- if force and os.path.exists(avd):
|
|
|
|
+- shutil.rmtree(avd)
|
|
|
|
+- if os.path.exists(avd):
|
|
|
|
+- _log_debug("AVD found at %s" % avd)
|
|
|
|
+- return True
|
|
|
|
+- return False
|
|
|
|
+-
|
|
|
|
+- def update_avd(self, force=False):
|
|
|
|
+- """
|
|
|
|
+- If required, update the AVD via tooltool.
|
|
|
|
+-
|
|
|
|
+- If the AVD directory is not found, or "force" is requested,
|
|
|
|
+- download the tooltool manifest associated with the AVD and then
|
|
|
|
+- invoke tooltool.py on the manifest. tooltool.py will download the
|
|
|
|
+- required archive (unless already present in the local tooltool
|
|
|
|
+- cache) and install the AVD.
|
|
|
|
+- """
|
|
|
|
+- avd = os.path.join(
|
|
|
|
+- EMULATOR_HOME_DIR, 'avd', self.avd_info.name + '.avd')
|
|
|
|
+- ini_file = os.path.join(
|
|
|
|
+- EMULATOR_HOME_DIR, 'avd', self.avd_info.name + '.ini')
|
|
|
|
+- if force and os.path.exists(avd):
|
|
|
|
+- shutil.rmtree(avd)
|
|
|
|
+- if not os.path.exists(avd):
|
|
|
|
+- if os.path.exists(ini_file):
|
|
|
|
+- os.remove(ini_file)
|
|
|
|
+- path = self.avd_info.tooltool_manifest
|
|
|
|
+- _get_tooltool_manifest(self.substs, path, EMULATOR_HOME_DIR, 'releng.manifest')
|
|
|
|
+- _tooltool_fetch()
|
|
|
|
+- self._update_avd_paths()
|
|
|
|
+-
|
|
|
|
+- def start(self):
|
|
|
|
+- """
|
|
|
|
+- Launch the emulator.
|
|
|
|
+- """
|
|
|
|
+- if os.path.exists(EMULATOR_AUTH_FILE):
|
|
|
|
+- os.remove(EMULATOR_AUTH_FILE)
|
|
|
|
+- _log_debug("deleted %s" % EMULATOR_AUTH_FILE)
|
|
|
|
+- # create an empty auth file to disable emulator authentication
|
|
|
|
+- auth_file = open(EMULATOR_AUTH_FILE, 'w')
|
|
|
|
+- auth_file.close()
|
|
|
|
+-
|
|
|
|
+- def outputHandler(line):
|
|
|
|
+- self.emulator_log.write("<%s>\n" % line)
|
|
|
|
+- if "Invalid value for -gpu" in line or "Invalid GPU mode" in line:
|
|
|
|
+- self.gpu = False
|
|
|
|
+- env = os.environ
|
|
|
|
+- env['ANDROID_AVD_HOME'] = os.path.join(EMULATOR_HOME_DIR, "avd")
|
|
|
|
+- command = [self.emulator_path, "-avd",
|
|
|
|
+- self.avd_info.name, "-port", "5554"]
|
|
|
|
+- if self.gpu:
|
|
|
|
+- command += ['-gpu', 'swiftshader']
|
|
|
|
+- if self.avd_info.extra_args:
|
|
|
|
+- # -enable-kvm option is not valid on OSX
|
|
|
|
+- if _get_host_platform() == 'macosx64' and '-enable-kvm' in self.avd_info.extra_args:
|
|
|
|
+- self.avd_info.extra_args.remove('-enable-kvm')
|
|
|
|
+- command += self.avd_info.extra_args
|
|
|
|
+- log_path = os.path.join(EMULATOR_HOME_DIR, 'emulator.log')
|
|
|
|
+- self.emulator_log = open(log_path, 'w')
|
|
|
|
+- _log_debug("Starting the emulator with this command: %s" %
|
|
|
|
+- ' '.join(command))
|
|
|
|
+- _log_debug("Emulator output will be written to '%s'" %
|
|
|
|
+- log_path)
|
|
|
|
+- self.proc = ProcessHandler(
|
|
|
|
+- command, storeOutput=False, processOutputLine=outputHandler,
|
|
|
|
+- env=env)
|
|
|
|
+- self.proc.run()
|
|
|
|
+- _log_debug("Emulator started with pid %d" %
|
|
|
|
+- int(self.proc.proc.pid))
|
|
|
|
+-
|
|
|
|
+- def wait_for_start(self):
|
|
|
|
+- """
|
|
|
|
+- Verify that the emulator is running, the emulator device is visible
|
|
|
|
+- to adb, and Android has booted.
|
|
|
|
+- """
|
|
|
|
+- if not self.proc:
|
|
|
|
+- _log_warning("Emulator not started!")
|
|
|
|
+- return False
|
|
|
|
+- if self.check_completed():
|
|
|
|
+- return False
|
|
|
|
+- _log_debug("Waiting for device status...")
|
|
|
|
+- while(('emulator-5554', 'device') not in self.dm.devices()):
|
|
|
|
+- time.sleep(10)
|
|
|
|
+- if self.check_completed():
|
|
|
|
+- return False
|
|
|
|
+- _log_debug("Device status verified.")
|
|
|
|
+-
|
|
|
|
+- _log_debug("Checking that Android has booted...")
|
|
|
|
+- complete = False
|
|
|
|
+- while(not complete):
|
|
|
|
+- output = ''
|
|
|
|
+- try:
|
|
|
|
+- output = self.dm.shellCheckOutput(
|
|
|
|
+- ['getprop', 'sys.boot_completed'], timeout=5)
|
|
|
|
+- except DMError:
|
|
|
|
+- # adb not yet responding...keep trying
|
|
|
|
+- pass
|
|
|
|
+- if output.strip() == '1':
|
|
|
|
+- complete = True
|
|
|
|
+- else:
|
|
|
|
+- time.sleep(10)
|
|
|
|
+- if self.check_completed():
|
|
|
|
+- return False
|
|
|
|
+- _log_debug("Android boot status verified.")
|
|
|
|
+-
|
|
|
|
+- if not self._verify_emulator():
|
|
|
|
+- return False
|
|
|
|
+- if self.avd_info.x86:
|
|
|
|
+- _log_info("Running the x86 emulator; be sure to install an x86 APK!")
|
|
|
|
+- else:
|
|
|
|
+- _log_info("Running the arm emulator; be sure to install an arm APK!")
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def check_completed(self):
|
|
|
|
+- if self.proc.proc.poll() is not None:
|
|
|
|
+- if not self.gpu and not self.restarted:
|
|
|
|
+- _log_warning("Emulator failed to start. Your emulator may be out of date.")
|
|
|
|
+- _log_warning("Trying to restart the emulator without -gpu argument.")
|
|
|
|
+- self.restarted = True
|
|
|
|
+- self.start()
|
|
|
|
+- return False
|
|
|
|
+- _log_warning("Emulator has already completed!")
|
|
|
|
+- log_path = os.path.join(EMULATOR_HOME_DIR, 'emulator.log')
|
|
|
|
+- _log_warning("See log at %s and/or use --verbose for more information." % log_path)
|
|
|
|
+- return True
|
|
|
|
+- return False
|
|
|
|
+-
|
|
|
|
+- def wait(self):
|
|
|
|
+- """
|
|
|
|
+- Wait for the emulator to close. If interrupted, close the emulator.
|
|
|
|
+- """
|
|
|
|
+- try:
|
|
|
|
+- self.proc.wait()
|
|
|
|
+- except Exception:
|
|
|
|
+- if self.proc.poll() is None:
|
|
|
|
+- self.cleanup()
|
|
|
|
+- return self.proc.poll()
|
|
|
|
+-
|
|
|
|
+- def cleanup(self):
|
|
|
|
+- """
|
|
|
|
+- Close the emulator.
|
|
|
|
+- """
|
|
|
|
+- self.proc.kill(signal.SIGTERM)
|
|
|
|
+-
|
|
|
|
+- def get_avd_description(self):
|
|
|
|
+- """
|
|
|
|
+- Return the human-friendly description of this AVD.
|
|
|
|
+- """
|
|
|
|
+- return self.avd_info.description
|
|
|
|
+-
|
|
|
|
+- def _update_avd_paths(self):
|
|
|
|
+- avd_path = os.path.join(EMULATOR_HOME_DIR, "avd")
|
|
|
|
+- ini_file = os.path.join(avd_path, "test-1.ini")
|
|
|
|
+- ini_file_new = os.path.join(avd_path, self.avd_info.name + ".ini")
|
|
|
|
+- os.rename(ini_file, ini_file_new)
|
|
|
|
+- avd_dir = os.path.join(avd_path, "test-1.avd")
|
|
|
|
+- avd_dir_new = os.path.join(avd_path, self.avd_info.name + ".avd")
|
|
|
|
+- os.rename(avd_dir, avd_dir_new)
|
|
|
|
+- self._replace_ini_contents(ini_file_new)
|
|
|
|
+-
|
|
|
|
+- def _replace_ini_contents(self, path):
|
|
|
|
+- with open(path, "r") as f:
|
|
|
|
+- lines = f.readlines()
|
|
|
|
+- with open(path, "w") as f:
|
|
|
|
+- for line in lines:
|
|
|
|
+- if line.startswith('path='):
|
|
|
|
+- avd_path = os.path.join(EMULATOR_HOME_DIR, "avd")
|
|
|
|
+- f.write('path=%s/%s.avd\n' %
|
|
|
|
+- (avd_path, self.avd_info.name))
|
|
|
|
+- elif line.startswith('path.rel='):
|
|
|
|
+- f.write('path.rel=avd/%s.avd\n' % self.avd_info.name)
|
|
|
|
+- else:
|
|
|
|
+- f.write(line)
|
|
|
|
+-
|
|
|
|
+- def _telnet_cmd(self, telnet, command):
|
|
|
|
+- _log_debug(">>> " + command)
|
|
|
|
+- telnet.write('%s\n' % command)
|
|
|
|
+- result = telnet.read_until('OK', 10)
|
|
|
|
+- _log_debug("<<< " + result)
|
|
|
|
+- return result
|
|
|
|
+-
|
|
|
|
+- def _verify_emulator(self):
|
|
|
|
+- telnet_ok = False
|
|
|
|
+- tn = None
|
|
|
|
+- while(not telnet_ok):
|
|
|
|
+- try:
|
|
|
|
+- tn = telnetlib.Telnet('localhost', 5554, 10)
|
|
|
|
+- if tn is not None:
|
|
|
|
+- tn.read_until('OK', 10)
|
|
|
|
+- self._telnet_cmd(tn, 'avd status')
|
|
|
|
+- self._telnet_cmd(tn, 'redir list')
|
|
|
|
+- self._telnet_cmd(tn, 'network status')
|
|
|
|
+- tn.write('quit\n')
|
|
|
|
+- tn.read_all()
|
|
|
|
+- telnet_ok = True
|
|
|
|
+- else:
|
|
|
|
+- _log_warning("Unable to connect to port 5554")
|
|
|
|
+- except Exception:
|
|
|
|
+- _log_warning("Trying again after unexpected exception")
|
|
|
|
+- finally:
|
|
|
|
+- if tn is not None:
|
|
|
|
+- tn.close()
|
|
|
|
+- if not telnet_ok:
|
|
|
|
+- time.sleep(10)
|
|
|
|
+- if self.proc.proc.poll() is not None:
|
|
|
|
+- _log_warning("Emulator has already completed!")
|
|
|
|
+- return False
|
|
|
|
+- return telnet_ok
|
|
|
|
+-
|
|
|
|
+- def _get_avd_type(self, requested):
|
|
|
|
+- if requested in AVD_DICT.keys():
|
|
|
|
+- return requested
|
|
|
|
+- if self.substs:
|
|
|
|
+- if not self.substs['TARGET_CPU'].startswith('arm'):
|
|
|
|
+- return 'x86'
|
|
|
|
+- return '4.3'
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _find_sdk_exe(substs, exe, tools):
|
|
|
|
+- if tools:
|
|
|
|
+- subdir = 'tools'
|
|
|
|
+- else:
|
|
|
|
+- subdir = 'platform-tools'
|
|
|
|
+-
|
|
|
|
+- found = False
|
|
|
|
+- if not found and substs:
|
|
|
|
+- # It's best to use the tool specified by the build, rather
|
|
|
|
+- # than something we find on the PATH or crawl for.
|
|
|
|
+- try:
|
|
|
|
+- exe_path = substs[exe.upper()]
|
|
|
|
+- if os.path.exists(exe_path):
|
|
|
|
+- found = True
|
|
|
|
+- else:
|
|
|
|
+- _log_debug(
|
|
|
|
+- "Unable to find executable at %s" % exe_path)
|
|
|
|
+- except KeyError:
|
|
|
|
+- _log_debug("%s not set" % exe.upper())
|
|
|
|
+-
|
|
|
|
+- # Append '.exe' to the name on Windows if it's not present,
|
|
|
|
+- # so that the executable can be found.
|
|
|
|
+- if (os.name == 'nt' and not exe.lower().endswith('.exe')):
|
|
|
|
+- exe += '.exe'
|
|
|
|
+-
|
|
|
|
+- if not found:
|
|
|
|
+- # Can exe be found in the Android SDK?
|
|
|
|
+- try:
|
|
|
|
+- android_sdk_root = os.environ['ANDROID_SDK_ROOT']
|
|
|
|
+- exe_path = os.path.join(
|
|
|
|
+- android_sdk_root, subdir, exe)
|
|
|
|
+- if os.path.exists(exe_path):
|
|
|
|
+- found = True
|
|
|
|
+- else:
|
|
|
|
+- _log_debug(
|
|
|
|
+- "Unable to find executable at %s" % exe_path)
|
|
|
|
+- except KeyError:
|
|
|
|
+- _log_debug("ANDROID_SDK_ROOT not set")
|
|
|
|
+-
|
|
|
|
+- if not found:
|
|
|
|
+- # Can exe be found in the default bootstrap location?
|
|
|
|
+- mozbuild_path = os.environ.get('MOZBUILD_STATE_PATH',
|
|
|
|
+- os.path.expanduser(os.path.join('~', '.mozbuild')))
|
|
|
|
+- exe_path = os.path.join(
|
|
|
|
+- mozbuild_path, 'android-sdk-linux', subdir, exe)
|
|
|
|
+- if os.path.exists(exe_path):
|
|
|
|
+- found = True
|
|
|
|
+- else:
|
|
|
|
+- _log_debug(
|
|
|
|
+- "Unable to find executable at %s" % exe_path)
|
|
|
|
+-
|
|
|
|
+- if not found:
|
|
|
|
+- # Is exe on PATH?
|
|
|
|
+- exe_path = find_executable(exe)
|
|
|
|
+- if exe_path:
|
|
|
|
+- found = True
|
|
|
|
+- else:
|
|
|
|
+- _log_debug("Unable to find executable on PATH")
|
|
|
|
+-
|
|
|
|
+- if found:
|
|
|
|
+- _log_debug("%s found at %s" % (exe, exe_path))
|
|
|
|
+- try:
|
|
|
|
+- creation_time = os.path.getctime(exe_path)
|
|
|
|
+- _log_debug(" ...with creation time %s" % time.ctime(creation_time))
|
|
|
|
+- except Exception:
|
|
|
|
+- _log_warning("Could not get creation time for %s" % exe_path)
|
|
|
|
+-
|
|
|
|
+- prop_path = os.path.join(os.path.dirname(exe_path), "source.properties")
|
|
|
|
+- if os.path.exists(prop_path):
|
|
|
|
+- with open(prop_path, 'r') as f:
|
|
|
|
+- for line in f.readlines():
|
|
|
|
+- if line.startswith("Pkg.Revision"):
|
|
|
|
+- line = line.strip()
|
|
|
|
+- _log_debug(" ...with SDK version in %s: %s" % (prop_path, line))
|
|
|
|
+- break
|
|
|
|
+- else:
|
|
|
|
+- exe_path = None
|
|
|
|
+- return exe_path
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _log_debug(text):
|
|
|
|
+- if verbose_logging:
|
|
|
|
+- print("DEBUG: %s" % text)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _log_warning(text):
|
|
|
|
+- print("WARNING: %s" % text)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _log_info(text):
|
|
|
|
+- print("%s" % text)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _download_file(url, filename, path):
|
|
|
|
+- _log_debug("Download %s to %s/%s..." % (url, path, filename))
|
|
|
|
+- f = urllib2.urlopen(url)
|
|
|
|
+- if not os.path.isdir(path):
|
|
|
|
+- try:
|
|
|
|
+- os.makedirs(path)
|
|
|
|
+- except Exception as e:
|
|
|
|
+- _log_warning(str(e))
|
|
|
|
+- return False
|
|
|
|
+- local_file = open(os.path.join(path, filename), 'wb')
|
|
|
|
+- local_file.write(f.read())
|
|
|
|
+- local_file.close()
|
|
|
|
+- _log_debug("Downloaded %s to %s/%s" % (url, path, filename))
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _get_tooltool_manifest(substs, src_path, dst_path, filename):
|
|
|
|
+- if not os.path.isdir(dst_path):
|
|
|
|
+- try:
|
|
|
|
+- os.makedirs(dst_path)
|
|
|
|
+- except Exception as e:
|
|
|
|
+- _log_warning(str(e))
|
|
|
|
+- copied = False
|
|
|
|
+- if substs and 'top_srcdir' in substs:
|
|
|
|
+- src = os.path.join(substs['top_srcdir'], src_path)
|
|
|
|
+- if os.path.exists(src):
|
|
|
|
+- dst = os.path.join(dst_path, filename)
|
|
|
|
+- shutil.copy(src, dst)
|
|
|
|
+- copied = True
|
|
|
|
+- _log_debug("Copied tooltool manifest %s to %s" % (src, dst))
|
|
|
|
+- if not copied:
|
|
|
|
+- url = os.path.join(TRY_URL, src_path)
|
|
|
|
+- _download_file(url, filename, dst_path)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _tooltool_fetch():
|
|
|
|
+- def outputHandler(line):
|
|
|
|
+- _log_debug(line)
|
|
|
|
+- _download_file(TOOLTOOL_URL, 'tooltool.py', EMULATOR_HOME_DIR)
|
|
|
|
+- command = [sys.executable, 'tooltool.py',
|
|
|
|
+- 'fetch', '-o', '-m', 'releng.manifest']
|
|
|
|
+- proc = ProcessHandler(
|
|
|
|
+- command, processOutputLine=outputHandler, storeOutput=False,
|
|
|
|
+- cwd=EMULATOR_HOME_DIR)
|
|
|
|
+- proc.run()
|
|
|
|
+- try:
|
|
|
|
+- proc.wait()
|
|
|
|
+- except Exception:
|
|
|
|
+- if proc.poll() is None:
|
|
|
|
+- proc.kill(signal.SIGTERM)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _get_host_platform():
|
|
|
|
+- plat = None
|
|
|
|
+- if 'darwin' in str(sys.platform).lower():
|
|
|
|
+- plat = 'macosx64'
|
|
|
|
+- elif 'linux' in str(sys.platform).lower():
|
|
|
|
+- if '64' in platform.architecture()[0]:
|
|
|
|
+- plat = 'linux64'
|
|
|
|
+- else:
|
|
|
|
+- plat = 'linux32'
|
|
|
|
+- return plat
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _get_device_platform(substs):
|
|
|
|
+- # PIE executables are required when SDK level >= 21 - important for gdbserver
|
|
|
|
+- adb_path = _find_sdk_exe(substs, 'adb', False)
|
|
|
|
+- if not adb_path:
|
|
|
|
+- adb_path = 'adb'
|
|
|
|
+- dm = DeviceManagerADB(autoconnect=False, adbPath=adb_path, retryLimit=1)
|
|
|
|
+- sdk_level = None
|
|
|
|
+- try:
|
|
|
|
+- cmd = ['getprop', 'ro.build.version.sdk']
|
|
|
|
+- _log_debug(cmd)
|
|
|
|
+- output = dm.shellCheckOutput(cmd, timeout=10)
|
|
|
|
+- if output:
|
|
|
|
+- sdk_level = int(output)
|
|
|
|
+- except Exception:
|
|
|
|
+- _log_warning("unable to determine Android sdk level")
|
|
|
|
+- pie = ''
|
|
|
|
+- if sdk_level and sdk_level >= 21:
|
|
|
|
+- pie = '-pie'
|
|
|
|
+- if substs['TARGET_CPU'].startswith('arm'):
|
|
|
|
+- return 'arm%s' % pie
|
|
|
|
+- return 'x86%s' % pie
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-def _update_gdbinit(substs, path):
|
|
|
|
+- if os.path.exists(path):
|
|
|
|
+- obj_replaced = False
|
|
|
|
+- src_replaced = False
|
|
|
|
+- # update existing objdir/srcroot in place
|
|
|
|
+- for line in fileinput.input(path, inplace=True):
|
|
|
|
+- if "feninit.default.objdir" in line and substs and 'MOZ_BUILD_ROOT' in substs:
|
|
|
|
+- print("python feninit.default.objdir = '%s'" % substs['MOZ_BUILD_ROOT'])
|
|
|
|
+- obj_replaced = True
|
|
|
|
+- elif "feninit.default.srcroot" in line and substs and 'top_srcdir' in substs:
|
|
|
|
+- print("python feninit.default.srcroot = '%s'" % substs['top_srcdir'])
|
|
|
|
+- src_replaced = True
|
|
|
|
+- else:
|
|
|
|
+- print(line.strip())
|
|
|
|
+- # append objdir/srcroot if not updated
|
|
|
|
+- if (not obj_replaced) and substs and 'MOZ_BUILD_ROOT' in substs:
|
|
|
|
+- with open(path, "a") as f:
|
|
|
|
+- f.write("\npython feninit.default.objdir = '%s'\n" % substs['MOZ_BUILD_ROOT'])
|
|
|
|
+- if (not src_replaced) and substs and 'top_srcdir' in substs:
|
|
|
|
+- with open(path, "a") as f:
|
|
|
|
+- f.write("python feninit.default.srcroot = '%s'\n" % substs['top_srcdir'])
|
|
|
|
+- else:
|
|
|
|
+- # write objdir/srcroot to new gdbinit file
|
|
|
|
+- with open(path, "w") as f:
|
|
|
|
+- if substs and 'MOZ_BUILD_ROOT' in substs:
|
|
|
|
+- f.write("python feninit.default.objdir = '%s'\n" % substs['MOZ_BUILD_ROOT'])
|
|
|
|
+- if substs and 'top_srcdir' in substs:
|
|
|
|
+- f.write("python feninit.default.srcroot = '%s'\n" % substs['top_srcdir'])
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/autophone.py b/testing/mozbase/mozrunner/mozrunner/devices/autophone.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/autophone.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,653 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import, print_function
|
|
|
|
+-
|
|
|
|
+-import glob
|
|
|
|
+-import json
|
|
|
|
+-import logging
|
|
|
|
+-import os
|
|
|
|
+-import shutil
|
|
|
|
+-import signal
|
|
|
|
+-import socket
|
|
|
|
+-import sys
|
|
|
|
+-import threading
|
|
|
|
+-import time
|
|
|
|
+-import which
|
|
|
|
+-import BaseHTTPServer
|
|
|
|
+-import SimpleHTTPServer
|
|
|
|
+-
|
|
|
|
+-from mozbuild.virtualenv import VirtualenvManager
|
|
|
|
+-from mozdevice import DeviceManagerADB
|
|
|
|
+-from mozprocess import ProcessHandler
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class AutophoneRunner(object):
|
|
|
|
+- """
|
|
|
|
+- Supporting the mach 'autophone' command: configure, run autophone.
|
|
|
|
+- """
|
|
|
|
+- config = {'base-dir': None,
|
|
|
|
+- 'requirements-installed': False,
|
|
|
|
+- 'devices-configured': False,
|
|
|
|
+- 'test-manifest': None}
|
|
|
|
+- CONFIG_FILE = os.path.join(os.path.expanduser('~'), '.mozbuild', 'autophone.json')
|
|
|
|
+-
|
|
|
|
+- def __init__(self, build_obj, verbose):
|
|
|
|
+- self.build_obj = build_obj
|
|
|
|
+- self.verbose = verbose
|
|
|
|
+- self.autophone_options = []
|
|
|
|
+- self.httpd = None
|
|
|
|
+- self.webserver_required = False
|
|
|
|
+-
|
|
|
|
+- def reset_to_clean(self):
|
|
|
|
+- """
|
|
|
|
+- If confirmed, remove the autophone directory and configuration.
|
|
|
|
+- """
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- if dir and os.path.exists(dir) and os.path.exists(self.CONFIG_FILE):
|
|
|
|
+- self.build_obj.log(logging.WARN, "autophone", {},
|
|
|
|
+- "*** This will delete %s and reset your "
|
|
|
|
+- "'mach autophone' configuration! ***" % dir)
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Proceed with deletion? (y/N) ").strip()
|
|
|
|
+- if response.lower().startswith('y'):
|
|
|
|
+- os.remove(self.CONFIG_FILE)
|
|
|
|
+- shutil.rmtree(dir)
|
|
|
|
+- else:
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Already clean -- nothing to do!")
|
|
|
|
+-
|
|
|
|
+- def save_config(self):
|
|
|
|
+- """
|
|
|
|
+- Persist self.config to a file.
|
|
|
|
+- """
|
|
|
|
+- try:
|
|
|
|
+- with open(self.CONFIG_FILE, 'w') as f:
|
|
|
|
+- json.dump(self.config, f)
|
|
|
|
+- if self.verbose:
|
|
|
|
+- print("saved configuration: %s" % self.config)
|
|
|
|
+- except Exception:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "unable to save 'mach autophone' "
|
|
|
|
+- "configuration to %s" % self.CONFIG_FILE)
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+-
|
|
|
|
+- def load_config(self):
|
|
|
|
+- """
|
|
|
|
+- Import the configuration info saved by save_config().
|
|
|
|
+- """
|
|
|
|
+- if os.path.exists(self.CONFIG_FILE):
|
|
|
|
+- try:
|
|
|
|
+- with open(self.CONFIG_FILE, 'r') as f:
|
|
|
|
+- self.config = json.load(f)
|
|
|
|
+- if self.verbose:
|
|
|
|
+- print("loaded configuration: %s" % self.config)
|
|
|
|
+- except Exception:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "unable to load 'mach autophone' "
|
|
|
|
+- "configuration from %s" % self.CONFIG_FILE)
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+-
|
|
|
|
+- def setup_directory(self):
|
|
|
|
+- """
|
|
|
|
+- Find the autophone source code location, or download if necessary.
|
|
|
|
+- """
|
|
|
|
+- keep_going = True
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- if not dir:
|
|
|
|
+- dir = os.path.join(os.path.expanduser('~'), 'mach-autophone')
|
|
|
|
+- if os.path.exists(os.path.join(dir, '.git')):
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Run autophone from existing directory, %s (Y/n) " % dir).strip()
|
|
|
|
+- if 'n' not in response.lower():
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Configuring and running autophone at %s" % dir)
|
|
|
|
+- return keep_going
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Unable to find an existing autophone directory. "
|
|
|
|
+- "Let's setup a new one...")
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Enter location of new autophone directory: [%s] " % dir).strip()
|
|
|
|
+- if response != '':
|
|
|
|
+- dir = response
|
|
|
|
+- self.config['base-dir'] = dir
|
|
|
|
+- if not os.path.exists(os.path.join(dir, '.git')):
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Cloning autophone repository to '%s'..." % dir)
|
|
|
|
+- self.config['requirements-installed'] = False
|
|
|
|
+- self.config['devices-configured'] = False
|
|
|
|
+- self.run_process(['git', 'clone', 'https://github.com/mozilla/autophone', dir])
|
|
|
|
+- self.run_process(['git', 'submodule', 'update', '--init', '--remote'], cwd=dir)
|
|
|
|
+- if not os.path.exists(os.path.join(dir, '.git')):
|
|
|
|
+- # git not installed? File permission problem? github not available?
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "Unable to clone autophone directory.")
|
|
|
|
+- if not self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "Try re-running this command with --verbose to get more info.")
|
|
|
|
+- keep_going = False
|
|
|
|
+- return keep_going
|
|
|
|
+-
|
|
|
|
+- def install_requirements(self):
|
|
|
|
+- """
|
|
|
|
+- Install required python modules in a virtualenv rooted at <autophone>/_virtualenv.
|
|
|
|
+- """
|
|
|
|
+- keep_going = True
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- vdir = os.path.join(dir, '_virtualenv')
|
|
|
|
+- self.auto_virtualenv_manager = VirtualenvManager(self.build_obj.topsrcdir,
|
|
|
|
+- self.build_obj.topobjdir,
|
|
|
|
+- vdir, sys.stdout,
|
|
|
|
+- os.path.join(self.build_obj.topsrcdir,
|
|
|
|
+- 'build',
|
|
|
|
+- 'virtualenv_packages.txt'))
|
|
|
|
+- if not self.config['requirements-installed'] or not os.path.exists(vdir):
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Installing required modules in a virtualenv...")
|
|
|
|
+- self.auto_virtualenv_manager.build()
|
|
|
|
+- self.auto_virtualenv_manager._run_pip(['install', '-r',
|
|
|
|
+- os.path.join(dir, 'requirements.txt')])
|
|
|
|
+- self.config['requirements-installed'] = True
|
|
|
|
+- return keep_going
|
|
|
|
+-
|
|
|
|
+- def configure_devices(self):
|
|
|
|
+- """
|
|
|
|
+- Ensure devices.ini is set up.
|
|
|
|
+- """
|
|
|
|
+- keep_going = True
|
|
|
|
+- device_ini = os.path.join(self.config['base-dir'], 'devices.ini')
|
|
|
|
+- if os.path.exists(device_ini):
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Use existing device configuration at %s? (Y/n) " % device_ini).strip()
|
|
|
|
+- if 'n' not in response.lower():
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Using device configuration at %s" % device_ini)
|
|
|
|
+- return keep_going
|
|
|
|
+- keep_going = False
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "You must configure at least one Android device "
|
|
|
|
+- "before running autophone.")
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Configure devices now? (Y/n) ").strip()
|
|
|
|
+- if response.lower().startswith('y') or response == '':
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Connect your rooted Android test device(s) with usb and press Enter ")
|
|
|
|
+- adb_path = 'adb'
|
|
|
|
+- try:
|
|
|
|
+- if os.path.exists(self.build_obj.substs["ADB"]):
|
|
|
|
+- adb_path = self.build_obj.substs["ADB"]
|
|
|
|
+- except Exception:
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+- # No build environment?
|
|
|
|
+- try:
|
|
|
|
+- adb_path = which.which('adb')
|
|
|
|
+- except which.WhichError:
|
|
|
|
+- adb_path = raw_input(
|
|
|
|
+- "adb not found. Enter path to adb: ").strip()
|
|
|
|
+- if self.verbose:
|
|
|
|
+- print("Using adb at %s" % adb_path)
|
|
|
|
+- dm = DeviceManagerADB(autoconnect=False, adbPath=adb_path, retryLimit=1)
|
|
|
|
+- device_index = 1
|
|
|
|
+- try:
|
|
|
|
+- with open(os.path.join(self.config['base-dir'], 'devices.ini'), 'w') as f:
|
|
|
|
+- for device in dm.devices():
|
|
|
|
+- serial = device[0]
|
|
|
|
+- if self.verify_device(adb_path, serial):
|
|
|
|
+- f.write("[device-%d]\nserialno=%s\n" % (device_index, serial))
|
|
|
|
+- device_index += 1
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Added '%s' to device configuration." % serial)
|
|
|
|
+- keep_going = True
|
|
|
|
+- else:
|
|
|
|
+- self.build_obj.log(logging.WARNING, "autophone", {},
|
|
|
|
+- "Device '%s' is not rooted - skipping" % serial)
|
|
|
|
+- except Exception:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "Failed to get list of connected Android devices.")
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+- keep_going = False
|
|
|
|
+- if device_index <= 1:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "No devices configured! (Can you see your rooted test device(s)"
|
|
|
|
+- " in 'adb devices'?")
|
|
|
|
+- keep_going = False
|
|
|
|
+- if keep_going:
|
|
|
|
+- self.config['devices-configured'] = True
|
|
|
|
+- return keep_going
|
|
|
|
+-
|
|
|
|
+- def configure_tests(self):
|
|
|
|
+- """
|
|
|
|
+- Determine the required autophone --test-path option.
|
|
|
|
+- """
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Autophone must be started with a 'test manifest' "
|
|
|
|
+- "describing the type(s) of test(s) to run.")
|
|
|
|
+- test_options = []
|
|
|
|
+- for ini in glob.glob(os.path.join(dir, 'tests', '*.ini')):
|
|
|
|
+- with open(ini, 'r') as f:
|
|
|
|
+- content = f.readlines()
|
|
|
|
+- for line in content:
|
|
|
|
+- if line.startswith('# @mach@ '):
|
|
|
|
+- webserver = False
|
|
|
|
+- if '@webserver@' in line:
|
|
|
|
+- webserver = True
|
|
|
|
+- line = line.replace('@webserver@', '')
|
|
|
|
+- test_options.append((line[9:].strip(), ini, webserver))
|
|
|
|
+- break
|
|
|
|
+- if len(test_options) >= 1:
|
|
|
|
+- test_options.sort()
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "These test manifests are available:")
|
|
|
|
+- index = 1
|
|
|
|
+- for option in test_options:
|
|
|
|
+- print("%d. %s" % (index, option[0]))
|
|
|
|
+- index += 1
|
|
|
|
+- highest = index - 1
|
|
|
|
+- path = None
|
|
|
|
+- while not path:
|
|
|
|
+- path = None
|
|
|
|
+- self.webserver_required = False
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Select test manifest (1-%d, or path to test manifest) " % highest).strip()
|
|
|
|
+- if os.path.isfile(response):
|
|
|
|
+- path = response
|
|
|
|
+- self.config['test-manifest'] = path
|
|
|
|
+- # Assume a webserver is required; if it isn't, user can provide a dummy url.
|
|
|
|
+- self.webserver_required = True
|
|
|
|
+- else:
|
|
|
|
+- try:
|
|
|
|
+- choice = int(response)
|
|
|
|
+- if choice >= 1 and choice <= highest:
|
|
|
|
+- path = test_options[choice - 1][1]
|
|
|
|
+- if test_options[choice - 1][2]:
|
|
|
|
+- self.webserver_required = True
|
|
|
|
+- else:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "'%s' invalid: Enter a number between "
|
|
|
|
+- "1 and %d!" % (response, highest))
|
|
|
|
+- except ValueError:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "'%s' unrecognized: Enter a number between "
|
|
|
|
+- "1 and %d!" % (response, highest))
|
|
|
|
+- self.autophone_options.extend(['--test-path', path])
|
|
|
|
+- else:
|
|
|
|
+- # Provide a simple backup for the unusual case where test manifests
|
|
|
|
+- # cannot be found.
|
|
|
|
+- response = ""
|
|
|
|
+- default = self.config['test-manifest'] or ""
|
|
|
|
+- while not os.path.isfile(response):
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Enter path to a test manifest: [%s] " % default).strip()
|
|
|
|
+- if response == "":
|
|
|
|
+- response = default
|
|
|
|
+- self.autophone_options.extend(['--test-path', response])
|
|
|
|
+- self.config['test-manifest'] = response
|
|
|
|
+- # Assume a webserver is required; if it isn't, user can provide a dummy url.
|
|
|
|
+- self.webserver_required = True
|
|
|
|
+-
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def write_unittest_defaults(self, defaults_path, xre_path):
|
|
|
|
+- """
|
|
|
|
+- Write unittest-defaults.ini.
|
|
|
|
+- """
|
|
|
|
+- try:
|
|
|
|
+- # This should be similar to unittest-defaults.ini.example
|
|
|
|
+- with open(defaults_path, 'w') as f:
|
|
|
|
+- f.write("""\
|
|
|
|
+-# Created by 'mach autophone'
|
|
|
|
+-[runtests]
|
|
|
|
+-xre_path = %s
|
|
|
|
+-utility_path = %s
|
|
|
|
+-console_level = DEBUG
|
|
|
|
+-log_level = DEBUG
|
|
|
|
+-time_out = 300""" % (xre_path, xre_path))
|
|
|
|
+- if self.verbose:
|
|
|
|
+- print("Created %s with host utilities path %s" % (defaults_path, xre_path))
|
|
|
|
+- except Exception:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "Unable to create %s" % defaults_path)
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+-
|
|
|
|
+- def configure_unittests(self):
|
|
|
|
+- """
|
|
|
|
+- Ensure unittest-defaults.ini is set up.
|
|
|
|
+- """
|
|
|
|
+- defaults_path = os.path.join(self.config['base-dir'], 'configs', 'unittest-defaults.ini')
|
|
|
|
+- if os.path.isfile(defaults_path):
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Use existing unit test configuration at %s? (Y/n) " % defaults_path).strip()
|
|
|
|
+- if 'n' in response.lower():
|
|
|
|
+- os.remove(defaults_path)
|
|
|
|
+- if not os.path.isfile(defaults_path):
|
|
|
|
+- xre_path = os.environ.get('MOZ_HOST_BIN')
|
|
|
|
+- if not xre_path or not os.path.isdir(xre_path):
|
|
|
|
+- emulator_path = os.path.join(os.path.expanduser('~'), '.mozbuild',
|
|
|
|
+- 'android-device')
|
|
|
|
+- xre_paths = glob.glob(os.path.join(emulator_path, 'host-utils*'))
|
|
|
|
+- for xre_path in xre_paths:
|
|
|
|
+- if os.path.isdir(xre_path):
|
|
|
|
+- break
|
|
|
|
+- if not xre_path or not os.path.isdir(xre_path) or \
|
|
|
|
+- not os.path.isfile(os.path.join(xre_path, 'xpcshell')):
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Some tests require access to 'host utilities' "
|
|
|
|
+- "such as xpcshell.")
|
|
|
|
+- xre_path = raw_input(
|
|
|
|
+- "Enter path to host utilities directory: ").strip()
|
|
|
|
+- if not xre_path or not os.path.isdir(xre_path) or \
|
|
|
|
+- not os.path.isfile(os.path.join(xre_path, 'xpcshell')):
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.ERROR, "autophone", {},
|
|
|
|
+- "Unable to configure unit tests - no path to host utilities.")
|
|
|
|
+- return False
|
|
|
|
+- self.write_unittest_defaults(defaults_path, xre_path)
|
|
|
|
+- if os.path.isfile(defaults_path):
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Using unit test configuration at %s" % defaults_path)
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def configure_ip(self):
|
|
|
|
+- """
|
|
|
|
+- Determine what IP should be used for the autophone --ipaddr option.
|
|
|
|
+- """
|
|
|
|
+- # Take a guess at the IP to suggest. This won't always get the "right" IP,
|
|
|
|
+- # but will save some typing, sometimes.
|
|
|
|
+- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
|
|
+- s.connect(('8.8.8.8', 0))
|
|
|
|
+- ip = s.getsockname()[0]
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "IP address of interface to use for phone callbacks [%s] " % ip).strip()
|
|
|
|
+- if response == "":
|
|
|
|
+- response = ip
|
|
|
|
+- self.autophone_options.extend(['--ipaddr', response])
|
|
|
|
+- self.ipaddr = response
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def configure_webserver(self):
|
|
|
|
+- """
|
|
|
|
+- Determine the autophone --webserver-url option.
|
|
|
|
+- """
|
|
|
|
+- if self.webserver_required:
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Some of your selected tests require a webserver.")
|
|
|
|
+- response = raw_input("Start a webserver now? [Y/n] ").strip()
|
|
|
|
+- parts = []
|
|
|
|
+- while len(parts) != 2:
|
|
|
|
+- response2 = raw_input(
|
|
|
|
+- "Webserver address? [%s:8100] " % self.ipaddr).strip()
|
|
|
|
+- if response2 == "":
|
|
|
|
+- parts = [self.ipaddr, "8100"]
|
|
|
|
+- else:
|
|
|
|
+- parts = response2.split(":")
|
|
|
|
+- if len(parts) == 2:
|
|
|
|
+- addr = parts[0]
|
|
|
|
+- try:
|
|
|
|
+- port = int(parts[1])
|
|
|
|
+- if port <= 0:
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.ERROR, "autophone", {},
|
|
|
|
+- "Port must be > 0. "
|
|
|
|
+- "Enter webserver address in the format <ip>:<port>")
|
|
|
|
+- parts = []
|
|
|
|
+- except ValueError:
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.ERROR, "autophone", {},
|
|
|
|
+- "Port must be a number. "
|
|
|
|
+- "Enter webserver address in the format <ip>:<port>")
|
|
|
|
+- parts = []
|
|
|
|
+- else:
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.ERROR, "autophone", {},
|
|
|
|
+- "Enter webserver address in the format <ip>:<port>")
|
|
|
|
+- if not ('n' in response.lower()):
|
|
|
|
+- self.launch_webserver(addr, port)
|
|
|
|
+- self.autophone_options.extend(['--webserver-url',
|
|
|
|
+- 'http://%s:%d' % (addr, port)])
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def configure_other(self):
|
|
|
|
+- """
|
|
|
|
+- Advanced users may set up additional options in autophone.ini.
|
|
|
|
+- Find and handle that case silently.
|
|
|
|
+- """
|
|
|
|
+- path = os.path.join(self.config['base-dir'], 'autophone.ini')
|
|
|
|
+- if os.path.isfile(path):
|
|
|
|
+- self.autophone_options.extend(['--config', path])
|
|
|
|
+- return True
|
|
|
|
+-
|
|
|
|
+- def configure(self):
|
|
|
|
+- """
|
|
|
|
+- Ensure all configuration files are set up and determine autophone options.
|
|
|
|
+- """
|
|
|
|
+- return self.configure_devices() and \
|
|
|
|
+- self.configure_unittests() and \
|
|
|
|
+- self.configure_tests() and \
|
|
|
|
+- self.configure_ip() and \
|
|
|
|
+- self.configure_webserver() and \
|
|
|
|
+- self.configure_other()
|
|
|
|
+-
|
|
|
|
+- def verify_device(self, adb_path, device):
|
|
|
|
+- """
|
|
|
|
+- Check that the specified device is available and rooted.
|
|
|
|
+- """
|
|
|
|
+- try:
|
|
|
|
+- dm = DeviceManagerADB(adbPath=adb_path, retryLimit=1, deviceSerial=device)
|
|
|
|
+- if dm._haveSu or dm._haveRootShell:
|
|
|
|
+- return True
|
|
|
|
+- except Exception:
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.WARN, "autophone", {},
|
|
|
|
+- "Unable to verify root on device.")
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- str(sys.exc_info()[0]))
|
|
|
|
+- return False
|
|
|
|
+-
|
|
|
|
+- def launch_autophone(self):
|
|
|
|
+- """
|
|
|
|
+- Launch autophone in its own thread and wait for autophone startup.
|
|
|
|
+- """
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Launching autophone...")
|
|
|
|
+- self.thread = threading.Thread(target=self.run_autophone)
|
|
|
|
+- self.thread.start()
|
|
|
|
+- # Wait for startup, so that autophone startup messages do not get mixed
|
|
|
|
+- # in with our interactive command prompts.
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- started = False
|
|
|
|
+- for seconds in [5, 5, 3, 3, 1, 1, 1, 1]:
|
|
|
|
+- time.sleep(seconds)
|
|
|
|
+- if self.run_process(['./ap.sh', 'autophone-status'], cwd=dir, dump=False):
|
|
|
|
+- started = True
|
|
|
|
+- break
|
|
|
|
+- time.sleep(1)
|
|
|
|
+- if not started:
|
|
|
|
+- self.build_obj.log(logging.WARN, "autophone", {},
|
|
|
|
+- "Autophone is taking longer than expected to start.")
|
|
|
|
+-
|
|
|
|
+- def run_autophone(self):
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- cmd = [self.auto_virtualenv_manager.python_path, 'autophone.py']
|
|
|
|
+- cmd.extend(self.autophone_options)
|
|
|
|
+- self.run_process(cmd, cwd=dir, dump=True)
|
|
|
|
+-
|
|
|
|
+- def command_prompts(self):
|
|
|
|
+- """
|
|
|
|
+- Interactive command prompts: Provide access to ap.sh and trigger_runs.py.
|
|
|
|
+- """
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- if self.thread.isAlive():
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "Use 'trigger' to select builds to test using the current test manifest.")
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "Type 'trigger', 'help', 'quit', or an autophone command.")
|
|
|
|
+- quitting = False
|
|
|
|
+- while self.thread.isAlive() and not quitting:
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "autophone command? ").strip().lower()
|
|
|
|
+- if response == "help":
|
|
|
|
+- self.run_process(['./ap.sh', 'autophone-help'], cwd=dir, dump=True)
|
|
|
|
+- print("""\
|
|
|
|
+-
|
|
|
|
+-Additional commands available in this interactive shell:
|
|
|
|
+-
|
|
|
|
+-trigger
|
|
|
|
+- Initiate autophone test runs. You will be prompted for a set of builds
|
|
|
|
+- to run tests against. (To run a different type of test, quit, run this
|
|
|
|
+- mach command again, and select a new test manifest.)
|
|
|
|
+-
|
|
|
|
+-quit
|
|
|
|
+- Shutdown autophone and exit this shell (short-cut to 'autophone-shutdown')
|
|
|
|
+-
|
|
|
|
+- """)
|
|
|
|
+- continue
|
|
|
|
+- if response == "trigger":
|
|
|
|
+- self.trigger_prompts()
|
|
|
|
+- continue
|
|
|
|
+- if response == "quit":
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Quitting...")
|
|
|
|
+- response = "autophone-shutdown"
|
|
|
|
+- if response == "autophone-shutdown":
|
|
|
|
+- quitting = True
|
|
|
|
+- self.run_process(['./ap.sh', response], cwd=dir, dump=True)
|
|
|
|
+- if self.httpd:
|
|
|
|
+- self.httpd.shutdown()
|
|
|
|
+- self.thread.join()
|
|
|
|
+-
|
|
|
|
+- def trigger_prompts(self):
|
|
|
|
+- """
|
|
|
|
+- Sub-prompts for the "trigger" command.
|
|
|
|
+- """
|
|
|
|
+- dir = self.config['base-dir']
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "Tests will be run against a build or collection of builds, selected by:")
|
|
|
|
+- print("""\
|
|
|
|
+-1. The latest build
|
|
|
|
+-2. Build URL
|
|
|
|
+-3. Build ID
|
|
|
|
+-4. Date/date-time range\
|
|
|
|
+- """)
|
|
|
|
+- highest = 4
|
|
|
|
+- choice = 0
|
|
|
|
+- while (choice < 1 or choice > highest):
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Build selection type? (1-%d) " % highest).strip()
|
|
|
|
+- try:
|
|
|
|
+- choice = int(response)
|
|
|
|
+- except ValueError:
|
|
|
|
+- self.build_obj.log(logging.ERROR, "autophone", {},
|
|
|
|
+- "Enter a number between 1 and %d" % highest)
|
|
|
|
+- choice = 0
|
|
|
|
+- if choice == 1:
|
|
|
|
+- options = ["latest"]
|
|
|
|
+- elif choice == 2:
|
|
|
|
+- url = raw_input(
|
|
|
|
+- "Enter url of build to test; may be an http or file schema ").strip()
|
|
|
|
+- options = ["--build-url=%s" % url]
|
|
|
|
+- elif choice == 3:
|
|
|
|
+- response = raw_input(
|
|
|
|
+- "Enter Build ID, eg 20120403063158 ").strip()
|
|
|
|
+- options = [response]
|
|
|
|
+- elif choice == 4:
|
|
|
|
+- start = raw_input(
|
|
|
|
+- "Enter start build date or date-time, "
|
|
|
|
+- "e.g. 2012-04-03 or 2012-04-03T06:31:58 ").strip()
|
|
|
|
+- end = raw_input(
|
|
|
|
+- "Enter end build date or date-time, "
|
|
|
|
+- "e.g. 2012-04-03 or 2012-04-03T06:31:58 ").strip()
|
|
|
|
+- options = [start, end]
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "You may optionally specify a repository name like 'mozilla-inbound' or 'try'.")
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "If not specified, 'mozilla-central' is assumed.")
|
|
|
|
+- repo = raw_input(
|
|
|
|
+- "Enter repository name: ").strip()
|
|
|
|
+- if len(repo) > 0:
|
|
|
|
+- options.extend(["--repo=%s" % repo])
|
|
|
|
+- if repo == "mozilla-central" or repo == "mozilla-aurora" or len(repo) < 1:
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "You may optionally specify the build location, like 'nightly' or 'tinderbox'.")
|
|
|
|
+- location = raw_input(
|
|
|
|
+- "Enter build location: ").strip()
|
|
|
|
+- if len(location) > 0:
|
|
|
|
+- options.extend(["--build-location=%s" % location])
|
|
|
|
+- else:
|
|
|
|
+- options.extend(["--build-location=tinderbox"])
|
|
|
|
+- cmd = [self.auto_virtualenv_manager.python_path, "trigger_runs.py"]
|
|
|
|
+- cmd.extend(options)
|
|
|
|
+- self.build_obj.log(
|
|
|
|
+- logging.INFO, "autophone", {},
|
|
|
|
+- "Triggering...Tests will run once builds have been downloaded.")
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Use 'autophone-status' to check progress.")
|
|
|
|
+- self.run_process(cmd, cwd=dir, dump=True)
|
|
|
|
+-
|
|
|
|
+- def launch_webserver(self, addr, port):
|
|
|
|
+- """
|
|
|
|
+- Launch the webserver (in a separate thread).
|
|
|
|
+- """
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Launching webserver...")
|
|
|
|
+- self.webserver_addr = addr
|
|
|
|
+- self.webserver_port = port
|
|
|
|
+- self.threadweb = threading.Thread(target=self.run_webserver)
|
|
|
|
+- self.threadweb.start()
|
|
|
|
+-
|
|
|
|
+- def run_webserver(self):
|
|
|
|
+- class AutoHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
|
|
|
+- # A simple request handler with logging suppressed.
|
|
|
|
+-
|
|
|
|
+- def log_message(self, format, *args):
|
|
|
|
+- pass
|
|
|
|
+-
|
|
|
|
+- os.chdir(self.config['base-dir'])
|
|
|
|
+- address = (self.webserver_addr, self.webserver_port)
|
|
|
|
+- self.httpd = BaseHTTPServer.HTTPServer(address, AutoHTTPRequestHandler)
|
|
|
|
+- try:
|
|
|
|
+- self.httpd.serve_forever()
|
|
|
|
+- except KeyboardInterrupt:
|
|
|
|
+- print("Web server interrupted.")
|
|
|
|
+-
|
|
|
|
+- def run_process(self, cmd, cwd=None, dump=False):
|
|
|
|
+- def _processOutput(line):
|
|
|
|
+- if self.verbose or dump:
|
|
|
|
+- print(line)
|
|
|
|
+-
|
|
|
|
+- if self.verbose:
|
|
|
|
+- self.build_obj.log(logging.INFO, "autophone", {},
|
|
|
|
+- "Running '%s' in '%s'" % (cmd, cwd))
|
|
|
|
+- proc = ProcessHandler(cmd, cwd=cwd, processOutputLine=_processOutput,
|
|
|
|
+- processStderrLine=_processOutput)
|
|
|
|
+- proc.run()
|
|
|
|
+- proc_complete = False
|
|
|
|
+- try:
|
|
|
|
+- proc.wait()
|
|
|
|
+- if proc.proc.returncode == 0:
|
|
|
|
+- proc_complete = True
|
|
|
|
+- except Exception:
|
|
|
|
+- if proc.poll() is None:
|
|
|
|
+- proc.kill(signal.SIGTERM)
|
|
|
|
+- if not proc_complete:
|
|
|
|
+- if not self.verbose:
|
|
|
|
+- print(proc.output)
|
|
|
|
+- return proc_complete
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/base.py b/testing/mozbase/mozrunner/mozrunner/devices/base.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/base.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,292 +0,0 @@
|
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-from ConfigParser import (
|
|
|
|
+- ConfigParser,
|
|
|
|
+- RawConfigParser
|
|
|
|
+-)
|
|
|
|
+-import datetime
|
|
|
|
+-import os
|
|
|
|
+-import posixpath
|
|
|
|
+-import re
|
|
|
|
+-import shutil
|
|
|
|
+-import subprocess
|
|
|
|
+-import tempfile
|
|
|
|
+-import time
|
|
|
|
+-
|
|
|
|
+-from mozdevice import DMError
|
|
|
|
+-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.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):
|
|
|
|
+- raise IOError("Remote file '%s' not found" % remote_ini)
|
|
|
|
+-
|
|
|
|
+- local_ini = tempfile.NamedTemporaryFile()
|
|
|
|
+- self.dm.getFile(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),
|
|
|
|
+- cfg.get(section, 'Path')))
|
|
|
|
+- else:
|
|
|
|
+- profiles.append(cfg.get(section, 'Path'))
|
|
|
|
+- return profiles
|
|
|
|
+-
|
|
|
|
+- 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)
|
|
|
|
+- if os.listdir(local_dump_dir):
|
|
|
|
+- for f in self.dm.listFiles(remote_dump_dir):
|
|
|
|
+- self.dm.removeFile(posixpath.join(remote_dump_dir, f))
|
|
|
|
+- 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.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)
|
|
|
|
+-
|
|
|
|
+- 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):
|
|
|
|
+- break
|
|
|
|
+- time.sleep(1)
|
|
|
|
+- else:
|
|
|
|
+- 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)
|
|
|
|
+-
|
|
|
|
+- 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)
|
|
|
|
+-
|
|
|
|
+- # 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')]
|
|
|
|
+-
|
|
|
|
+- 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]
|
|
|
|
+-
|
|
|
|
+- 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)
|
|
|
|
+- if os.path.isfile(logcat_log):
|
|
|
|
+- self._rotate_log(logcat_log)
|
|
|
|
+- self.logcat_proc = self.start_logcat(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 = {}
|
|
|
|
+- if logfile:
|
|
|
|
+- process_args['logfile'] = logfile
|
|
|
|
+- elif stream:
|
|
|
|
+- process_args['stream'] = stream
|
|
|
|
+- 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()
|
|
|
|
+-
|
|
|
|
+- 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
|
|
|
|
+- line = proc.stdout.readline()
|
|
|
|
+- while line != "":
|
|
|
|
+- if (re.search(r'UP\s+[1-9]\d{0,2}\.\d{1,3}\.\d{1,3}\.\d{1,3}', line)):
|
|
|
|
+- active = True
|
|
|
|
+- break
|
|
|
|
+- line = proc.stdout.readline()
|
|
|
|
+- 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)
|
|
|
|
+- 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.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.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()
|
|
|
|
+-
|
|
|
|
+- # Remove the test profile
|
|
|
|
+- self.dm.removeDir(self.app_ctx.remote_profile)
|
|
|
|
+-
|
|
|
|
+- 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')]
|
|
|
|
+- if index > 1:
|
|
|
|
+- basename = basename[:-len('.1')]
|
|
|
|
+- basename = '%s.%d.log' % (basename, index)
|
|
|
|
+-
|
|
|
|
+- destlog = os.path.join(self.logdir, basename)
|
|
|
|
+- if os.path.isfile(destlog):
|
|
|
|
+- if index == 3:
|
|
|
|
+- os.remove(destlog)
|
|
|
|
+- else:
|
|
|
|
+- self._rotate_log(destlog, index + 1)
|
|
|
|
+- shutil.move(srclog, destlog)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class ProfileConfigParser(RawConfigParser):
|
|
|
|
+- """
|
|
|
|
+- Class to create profiles.ini config files
|
|
|
|
+-
|
|
|
|
+- Subclass of RawConfigParser that outputs .ini files in the exact
|
|
|
|
+- format expected for profiles.ini, which is slightly different
|
|
|
|
+- than the default format.
|
|
|
|
+- """
|
|
|
|
+-
|
|
|
|
+- def optionxform(self, optionstr):
|
|
|
|
+- return optionstr
|
|
|
|
+-
|
|
|
|
+- def write(self, fp):
|
|
|
|
+- if self._defaults:
|
|
|
|
+- fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
|
|
|
|
+- for (key, value) in self._defaults.items():
|
|
|
|
+- fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
|
|
|
|
+- fp.write("\n")
|
|
|
|
+- for section in self._sections:
|
|
|
|
+- fp.write("[%s]\n" % section)
|
|
|
|
+- for (key, value) in self._sections[section].items():
|
|
|
|
+- if key == "__name__":
|
|
|
|
+- continue
|
|
|
|
+- if (value is not None) or (self._optcre == self.OPTCRE):
|
|
|
|
+- key = "=".join((key, str(value).replace('\n', '\n\t')))
|
|
|
|
+- fp.write("%s\n" % (key))
|
|
|
|
+- fp.write("\n")
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,291 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import
|
|
|
|
+-
|
|
|
|
+-from telnetlib import Telnet
|
|
|
|
+-import datetime
|
|
|
|
+-import os
|
|
|
|
+-import shutil
|
|
|
|
+-import subprocess
|
|
|
|
+-import tempfile
|
|
|
|
+-import time
|
|
|
|
+-
|
|
|
|
+-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
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class ArchContext(object):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, arch, context, binary=None, avd=None, extra_args=None):
|
|
|
|
+- homedir = getattr(context, 'homedir', '')
|
|
|
|
+- kernel = os.path.join(homedir, 'prebuilts', 'qemu-kernel', '%s', '%s')
|
|
|
|
+- sysdir = os.path.join(homedir, 'out', 'target', 'product', '%s')
|
|
|
|
+- self.extra_args = []
|
|
|
|
+- self.binary = os.path.join(context.bindir or '', 'emulator')
|
|
|
|
+- if arch == 'x86':
|
|
|
|
+- self.binary = os.path.join(context.bindir or '', 'emulator-x86')
|
|
|
|
+- self.kernel = kernel % ('x86', 'kernel-qemu')
|
|
|
|
+- self.sysdir = sysdir % 'generic_x86'
|
|
|
|
+- elif avd:
|
|
|
|
+- self.avd = avd
|
|
|
|
+- self.extra_args = [
|
|
|
|
+- '-show-kernel', '-debug',
|
|
|
|
+- 'init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket'
|
|
|
|
+- ]
|
|
|
|
+- else:
|
|
|
|
+- self.kernel = kernel % ('arm', 'kernel-qemu-armv7')
|
|
|
|
+- self.sysdir = sysdir % 'generic'
|
|
|
|
+- self.extra_args = ['-cpu', 'cortex-a8']
|
|
|
|
+-
|
|
|
|
+- if binary:
|
|
|
|
+- self.binary = binary
|
|
|
|
+-
|
|
|
|
+- if extra_args:
|
|
|
|
+- self.extra_args.extend(extra_args)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class SDCard(object):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, emulator, size):
|
|
|
|
+- self.emulator = emulator
|
|
|
|
+- self.path = self.create_sdcard(size)
|
|
|
|
+-
|
|
|
|
+- def create_sdcard(self, sdcard_size):
|
|
|
|
+- """
|
|
|
|
+- Creates an sdcard partition in the emulator.
|
|
|
|
+-
|
|
|
|
+- :param sdcard_size: Size of partition to create, e.g '10MB'.
|
|
|
|
+- """
|
|
|
|
+- mksdcard = self.emulator.app_ctx.which('mksdcard')
|
|
|
|
+- path = tempfile.mktemp(prefix='sdcard', dir=self.emulator.tmpdir)
|
|
|
|
+- sdargs = [mksdcard, '-l', 'mySdCard', sdcard_size, path]
|
|
|
|
+- sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE,
|
|
|
|
+- stderr=subprocess.STDOUT)
|
|
|
|
+- retcode = sd.wait()
|
|
|
|
+- if retcode:
|
|
|
|
+- raise Exception('unable to create sdcard: exit code %d: %s'
|
|
|
|
+- % (retcode, sd.stdout.read()))
|
|
|
|
+- return path
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class BaseEmulator(Device):
|
|
|
|
+- port = None
|
|
|
|
+- proc = None
|
|
|
|
+- telnet = None
|
|
|
|
+-
|
|
|
|
+- def __init__(self, app_ctx, **kwargs):
|
|
|
|
+- self.arch = ArchContext(kwargs.pop('arch', 'arm'), app_ctx,
|
|
|
|
+- binary=kwargs.pop('binary', None),
|
|
|
|
+- avd=kwargs.pop('avd', None))
|
|
|
|
+- super(BaseEmulator, self).__init__(app_ctx, **kwargs)
|
|
|
|
+- self.tmpdir = tempfile.mkdtemp()
|
|
|
|
+- # These rely on telnet
|
|
|
|
+- self.battery = EmulatorBattery(self)
|
|
|
|
+- self.geo = EmulatorGeo(self)
|
|
|
|
+- self.screen = EmulatorScreen(self)
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def args(self):
|
|
|
|
+- """
|
|
|
|
+- Arguments to pass into the emulator binary.
|
|
|
|
+- """
|
|
|
|
+- return [self.arch.binary]
|
|
|
|
+-
|
|
|
|
+- def start(self):
|
|
|
|
+- """
|
|
|
|
+- Starts a new emulator.
|
|
|
|
+- """
|
|
|
|
+- if self.proc:
|
|
|
|
+- return
|
|
|
|
+-
|
|
|
|
+- original_devices = set(self._get_online_devices())
|
|
|
|
+-
|
|
|
|
+- # QEMU relies on atexit() to remove temporary files, which does not
|
|
|
|
+- # work since mozprocess uses SIGKILL to kill the emulator process.
|
|
|
|
+- # Use a customized temporary directory so we can clean it up.
|
|
|
|
+- os.environ['ANDROID_TMP'] = self.tmpdir
|
|
|
|
+-
|
|
|
|
+- qemu_log = None
|
|
|
|
+- qemu_proc_args = {}
|
|
|
|
+- if self.logdir:
|
|
|
|
+- # save output from qemu to logfile
|
|
|
|
+- qemu_log = os.path.join(self.logdir, 'qemu.log')
|
|
|
|
+- if os.path.isfile(qemu_log):
|
|
|
|
+- self._rotate_log(qemu_log)
|
|
|
|
+- qemu_proc_args['logfile'] = qemu_log
|
|
|
|
+- else:
|
|
|
|
+- qemu_proc_args['processOutputLine'] = lambda line: None
|
|
|
|
+- self.proc = ProcessHandler(self.args, **qemu_proc_args)
|
|
|
|
+- self.proc.run()
|
|
|
|
+-
|
|
|
|
+- devices = set(self._get_online_devices())
|
|
|
|
+- now = datetime.datetime.now()
|
|
|
|
+- while (devices - original_devices) == set([]):
|
|
|
|
+- time.sleep(1)
|
|
|
|
+- # Sometimes it takes more than 60s to launch emulator, so we
|
|
|
|
+- # increase timeout value to 180s. Please see bug 1143380.
|
|
|
|
+- if datetime.datetime.now() - now > datetime.timedelta(
|
|
|
|
+- seconds=180):
|
|
|
|
+- 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')]
|
|
|
|
+-
|
|
|
|
+- 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:])
|
|
|
|
+-
|
|
|
|
+- 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()
|
|
|
|
+- self.proc = None
|
|
|
|
+- self.connected = False
|
|
|
|
+-
|
|
|
|
+- # Remove temporary files
|
|
|
|
+- if os.path.isdir(self.tmpdir):
|
|
|
|
+- shutil.rmtree(self.tmpdir)
|
|
|
|
+-
|
|
|
|
+- def _get_telnet_response(self, command=None):
|
|
|
|
+- output = []
|
|
|
|
+- assert self.telnet
|
|
|
|
+- if command is not None:
|
|
|
|
+- self.telnet.write('%s\n' % command)
|
|
|
|
+- while True:
|
|
|
|
+- line = self.telnet.read_until('\n')
|
|
|
|
+- output.append(line.rstrip())
|
|
|
|
+- if line.startswith('OK'):
|
|
|
|
+- return output
|
|
|
|
+- elif line.startswith('KO:'):
|
|
|
|
+- raise Exception('bad telnet response: %s' % line)
|
|
|
|
+-
|
|
|
|
+- def _run_telnet(self, command):
|
|
|
|
+- if not self.telnet:
|
|
|
|
+- self.telnet = Telnet('localhost', self.port)
|
|
|
|
+- self._get_telnet_response()
|
|
|
|
+- return self._get_telnet_response(command)
|
|
|
|
+-
|
|
|
|
+- def __del__(self):
|
|
|
|
+- if self.telnet:
|
|
|
|
+- self.telnet.write('exit\n')
|
|
|
|
+- self.telnet.read_all()
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class Emulator(BaseEmulator):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, app_ctx, arch, resolution=None, sdcard=None, userdata=None,
|
|
|
|
+- no_window=None, binary=None, **kwargs):
|
|
|
|
+- super(Emulator, self).__init__(app_ctx, arch=arch, binary=binary, **kwargs)
|
|
|
|
+-
|
|
|
|
+- # emulator args
|
|
|
|
+- self.resolution = resolution or '320x480'
|
|
|
|
+- self._sdcard_size = sdcard
|
|
|
|
+- self._sdcard = None
|
|
|
|
+- self.userdata = tempfile.NamedTemporaryFile(prefix='userdata-qemu', dir=self.tmpdir)
|
|
|
|
+- self.initdata = userdata if userdata else os.path.join(self.arch.sysdir, 'userdata.img')
|
|
|
|
+- self.no_window = no_window
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def sdcard(self):
|
|
|
|
+- if self._sdcard_size and not self._sdcard:
|
|
|
|
+- self._sdcard = SDCard(self, self._sdcard_size).path
|
|
|
|
+- else:
|
|
|
|
+- return self._sdcard
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def args(self):
|
|
|
|
+- """
|
|
|
|
+- Arguments to pass into the emulator binary.
|
|
|
|
+- """
|
|
|
|
+- qemu_args = super(Emulator, self).args
|
|
|
|
+- qemu_args.extend([
|
|
|
|
+- '-kernel', self.arch.kernel,
|
|
|
|
+- '-sysdir', self.arch.sysdir,
|
|
|
|
+- '-data', self.userdata.name,
|
|
|
|
+- '-initdata', self.initdata,
|
|
|
|
+- '-wipe-data'])
|
|
|
|
+- if self.no_window:
|
|
|
|
+- qemu_args.append('-no-window')
|
|
|
|
+- if self.sdcard:
|
|
|
|
+- qemu_args.extend(['-sdcard', self.sdcard])
|
|
|
|
+- qemu_args.extend(['-memory', '512',
|
|
|
|
+- '-partition-size', '512',
|
|
|
|
+- '-verbose',
|
|
|
|
+- '-skin', self.resolution,
|
|
|
|
+- '-gpu', 'on',
|
|
|
|
+- '-qemu'] + self.arch.extra_args)
|
|
|
|
+- return qemu_args
|
|
|
|
+-
|
|
|
|
+- 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(Emulator, self).connect()
|
|
|
|
+- self.geo.set_default_location()
|
|
|
|
+- self.screen.initialize()
|
|
|
|
+-
|
|
|
|
+- # setup DNS fix for networking
|
|
|
|
+- self.app_ctx.dm.shellCheckOutput(['setprop', 'net.dns1', '10.0.2.3'])
|
|
|
|
+-
|
|
|
|
+- def cleanup(self):
|
|
|
|
+- """
|
|
|
|
+- Cleans up and kills the emulator, if it was started by mozrunner.
|
|
|
|
+- """
|
|
|
|
+- super(Emulator, self).cleanup()
|
|
|
|
+- # Remove temporary files
|
|
|
|
+- self.userdata.close()
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class EmulatorAVD(BaseEmulator):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, app_ctx, binary, avd, port=5554, **kwargs):
|
|
|
|
+- super(EmulatorAVD, self).__init__(app_ctx, binary=binary, avd=avd, **kwargs)
|
|
|
|
+- self.port = port
|
|
|
|
+-
|
|
|
|
+- @property
|
|
|
|
+- def args(self):
|
|
|
|
+- """
|
|
|
|
+- Arguments to pass into the emulator binary.
|
|
|
|
+- """
|
|
|
|
+- qemu_args = super(EmulatorAVD, self).args
|
|
|
|
+- qemu_args.extend(['-avd', self.arch.avd,
|
|
|
|
+- '-port', str(self.port)])
|
|
|
|
+- qemu_args.extend(self.arch.extra_args)
|
|
|
|
+- return qemu_args
|
|
|
|
+-
|
|
|
|
+- def start(self):
|
|
|
|
+- if self.proc:
|
|
|
|
+- return
|
|
|
|
+-
|
|
|
|
+- env = os.environ
|
|
|
|
+- env['ANDROID_AVD_HOME'] = self.app_ctx.avd_home
|
|
|
|
+-
|
|
|
|
+- super(EmulatorAVD, self).start()
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,55 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class EmulatorBattery(object):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, emulator):
|
|
|
|
+- self.emulator = emulator
|
|
|
|
+-
|
|
|
|
+- def get_state(self):
|
|
|
|
+- status = {}
|
|
|
|
+- state = {}
|
|
|
|
+-
|
|
|
|
+- response = self.emulator._run_telnet('power display')
|
|
|
|
+- for line in response:
|
|
|
|
+- if ':' in line:
|
|
|
|
+- field, value = line.split(':')
|
|
|
|
+- value = value.strip()
|
|
|
|
+- if value == 'true':
|
|
|
|
+- value = True
|
|
|
|
+- elif value == 'false':
|
|
|
|
+- value = False
|
|
|
|
+- elif field == 'capacity':
|
|
|
|
+- value = float(value)
|
|
|
|
+- status[field] = value
|
|
|
|
+-
|
|
|
|
+- state['level'] = status.get('capacity', 0.0) / 100
|
|
|
|
+- if status.get('AC') == 'online':
|
|
|
|
+- state['charging'] = True
|
|
|
|
+- else:
|
|
|
|
+- state['charging'] = False
|
|
|
|
+-
|
|
|
|
+- return state
|
|
|
|
+-
|
|
|
|
+- def get_charging(self):
|
|
|
|
+- return self.get_state()['charging']
|
|
|
|
+-
|
|
|
|
+- def get_level(self):
|
|
|
|
+- return self.get_state()['level']
|
|
|
|
+-
|
|
|
|
+- def set_level(self, level):
|
|
|
|
+- self.emulator._run_telnet('power capacity %d' % (level * 100))
|
|
|
|
+-
|
|
|
|
+- def set_charging(self, charging):
|
|
|
|
+- if charging:
|
|
|
|
+- cmd = 'power ac on'
|
|
|
|
+- else:
|
|
|
|
+- cmd = 'power ac off'
|
|
|
|
+- self.emulator._run_telnet(cmd)
|
|
|
|
+-
|
|
|
|
+- charging = property(get_charging, set_charging)
|
|
|
|
+- level = property(get_level, set_level)
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,19 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class EmulatorGeo(object):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, emulator):
|
|
|
|
+- self.emulator = emulator
|
|
|
|
+-
|
|
|
|
+- def set_default_location(self):
|
|
|
|
+- self.lon = -122.08769
|
|
|
|
+- self.lat = 37.41857
|
|
|
|
+- self.set_location(self.lon, self.lat)
|
|
|
|
+-
|
|
|
|
+- def set_location(self, lon, lat):
|
|
|
|
+- self.emulator._run_telnet('geo fix %0.5f %0.5f' % (self.lon, self.lat))
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py b/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py
|
|
|
|
+deleted file mode 100644
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py
|
|
|
|
++++ /dev/null
|
|
|
|
+@@ -1,91 +0,0 @@
|
|
|
|
+-# 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 __future__ import absolute_import
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-class EmulatorScreen(object):
|
|
|
|
+- """Class for screen related emulator commands."""
|
|
|
|
+-
|
|
|
|
+- SO_PORTRAIT_PRIMARY = 'portrait-primary'
|
|
|
|
+- SO_PORTRAIT_SECONDARY = 'portrait-secondary'
|
|
|
|
+- SO_LANDSCAPE_PRIMARY = 'landscape-primary'
|
|
|
|
+- SO_LANDSCAPE_SECONDARY = 'landscape-secondary'
|
|
|
|
+-
|
|
|
|
+- def __init__(self, emulator):
|
|
|
|
+- self.emulator = emulator
|
|
|
|
+-
|
|
|
|
+- def initialize(self):
|
|
|
|
+- self.orientation = self.SO_PORTRAIT_PRIMARY
|
|
|
|
+-
|
|
|
|
+- def _get_raw_orientation(self):
|
|
|
|
+- """Get the raw value of the current device orientation."""
|
|
|
|
+- response = self.emulator._run_telnet('sensor get orientation')
|
|
|
|
+-
|
|
|
|
+- return response[0].split('=')[1].strip()
|
|
|
|
+-
|
|
|
|
+- def _set_raw_orientation(self, data):
|
|
|
|
+- """Set the raw value of the specified device orientation."""
|
|
|
|
+- self.emulator._run_telnet('sensor set orientation %s' % data)
|
|
|
|
+-
|
|
|
|
+- def get_orientation(self):
|
|
|
|
+- """Get the current device orientation.
|
|
|
|
+-
|
|
|
|
+- Returns;
|
|
|
|
+- orientation -- Orientation of the device. One of:
|
|
|
|
+- SO_PORTRAIT_PRIMARY - system buttons at the bottom
|
|
|
|
+- SO_PORTRIAT_SECONDARY - system buttons at the top
|
|
|
|
+- SO_LANDSCAPE_PRIMARY - system buttons at the right
|
|
|
|
+- SO_LANDSCAPE_SECONDARY - system buttons at the left
|
|
|
|
+-
|
|
|
|
+- """
|
|
|
|
+- data = self._get_raw_orientation()
|
|
|
|
+-
|
|
|
|
+- if data == '0:-90:0':
|
|
|
|
+- orientation = self.SO_PORTRAIT_PRIMARY
|
|
|
|
+- elif data == '0:90:0':
|
|
|
|
+- orientation = self.SO_PORTRAIT_SECONDARY
|
|
|
|
+- elif data == '0:0:90':
|
|
|
|
+- orientation = self.SO_LANDSCAPE_PRIMARY
|
|
|
|
+- elif data == '0:0:-90':
|
|
|
|
+- orientation = self.SO_LANDSCAPE_SECONDARY
|
|
|
|
+- else:
|
|
|
|
+- raise ValueError('Unknown orientation sensor value: %s.' % data)
|
|
|
|
+-
|
|
|
|
+- return orientation
|
|
|
|
+-
|
|
|
|
+- def set_orientation(self, orientation):
|
|
|
|
+- """Set the specified device orientation.
|
|
|
|
+-
|
|
|
|
+- Args
|
|
|
|
+- orientation -- Orientation of the device. One of:
|
|
|
|
+- SO_PORTRAIT_PRIMARY - system buttons at the bottom
|
|
|
|
+- SO_PORTRIAT_SECONDARY - system buttons at the top
|
|
|
|
+- SO_LANDSCAPE_PRIMARY - system buttons at the right
|
|
|
|
+- SO_LANDSCAPE_SECONDARY - system buttons at the left
|
|
|
|
+- """
|
|
|
|
+- orientation = SCREEN_ORIENTATIONS[orientation]
|
|
|
|
+-
|
|
|
|
+- if orientation == self.SO_PORTRAIT_PRIMARY:
|
|
|
|
+- data = '0:-90:0'
|
|
|
|
+- elif orientation == self.SO_PORTRAIT_SECONDARY:
|
|
|
|
+- data = '0:90:0'
|
|
|
|
+- elif orientation == self.SO_LANDSCAPE_PRIMARY:
|
|
|
|
+- data = '0:0:90'
|
|
|
|
+- elif orientation == self.SO_LANDSCAPE_SECONDARY:
|
|
|
|
+- data = '0:0:-90'
|
|
|
|
+- else:
|
|
|
|
+- raise ValueError('Invalid orientation: %s' % orientation)
|
|
|
|
+-
|
|
|
|
+- self._set_raw_orientation(data)
|
|
|
|
+-
|
|
|
|
+- orientation = property(get_orientation, set_orientation)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+-SCREEN_ORIENTATIONS = {"portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY,
|
|
|
|
+- "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
|
|
|
|
+- "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY,
|
|
|
|
+- "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
|
|
|
|
+- "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY,
|
|
|
|
+- "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY}
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/mozrunner/runners.py b/testing/mozbase/mozrunner/mozrunner/runners.py
|
|
|
|
+--- a/testing/mozbase/mozrunner/mozrunner/runners.py
|
|
|
|
++++ b/testing/mozbase/mozrunner/mozrunner/runners.py
|
|
|
|
+@@ -6,18 +6,17 @@
|
|
|
|
+ """
|
|
|
|
+ This module contains a set of shortcut methods that create runners for commonly
|
|
|
|
+ used Mozilla applications, such as Firefox, Firefox for Android or Thunderbird.
|
|
|
|
+ """
|
|
|
|
+
|
|
|
|
+ from __future__ import absolute_import
|
|
|
|
+
|
|
|
|
+ from .application import get_app_context
|
|
|
|
+-from .base import GeckoRuntimeRunner, FennecRunner, BlinkRuntimeRunner
|
|
|
|
+-from .devices import EmulatorAVD
|
|
|
|
++from .base import GeckoRuntimeRunner, BlinkRuntimeRunner
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def Runner(*args, **kwargs):
|
|
|
|
+ """
|
|
|
|
+ Create a generic GeckoRuntime runner.
|
|
|
|
+
|
|
|
|
+ :param binary: Path to binary.
|
|
|
|
+ :param cmdargs: Arguments to pass into binary.
|
|
|
|
+@@ -80,53 +79,14 @@ def ChromeRunner(*args, **kwargs):
|
|
|
|
+
|
|
|
|
+ :param binary: Path to Chrome binary.
|
|
|
|
+ :param cmdargs: Arguments to pass into the binary.
|
|
|
|
+ """
|
|
|
|
+ kwargs['app_ctx'] = get_app_context('chrome')()
|
|
|
|
+ return BlinkRuntimeRunner(*args, **kwargs)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-def FennecEmulatorRunner(avd='mozemulator-4.3',
|
|
|
|
+- adb_path=None,
|
|
|
|
+- avd_home=None,
|
|
|
|
+- logdir=None,
|
|
|
|
+- serial=None,
|
|
|
|
+- binary=None,
|
|
|
|
+- app='org.mozilla.fennec',
|
|
|
|
+- **kwargs):
|
|
|
|
+- """
|
|
|
|
+- Create a Fennec emulator runner. This can either start a new emulator
|
|
|
|
+- (which will use an avd), or connect to an already-running emulator.
|
|
|
|
+-
|
|
|
|
+- :param avd: name of an AVD available in your environment.
|
|
|
|
+- Typically obtained via tooltool: either 'mozemulator-4.3' or 'mozemulator-x86'.
|
|
|
|
+- Defaults to 'mozemulator-4.3'
|
|
|
|
+- :param avd_home: Path to avd parent directory
|
|
|
|
+- :param logdir: Path to save logfiles such as logcat and qemu output.
|
|
|
|
+- :param serial: Serial of emulator to connect to as seen in `adb devices`.
|
|
|
|
+- Defaults to the first entry in `adb devices`.
|
|
|
|
+- :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,
|
|
|
|
+- device_serial=serial)
|
|
|
|
+- device_args = {'app_ctx': kwargs['app_ctx'],
|
|
|
|
+- 'avd': avd,
|
|
|
|
+- 'binary': binary,
|
|
|
|
+- 'logdir': logdir}
|
|
|
|
+- return FennecRunner(device_class=EmulatorAVD,
|
|
|
|
+- device_args=device_args,
|
|
|
|
+- **kwargs)
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ runners = {
|
|
|
|
+ 'chrome': ChromeRunner,
|
|
|
|
+ 'default': Runner,
|
|
|
|
+ 'firefox': FirefoxRunner,
|
|
|
|
+- 'fennec': FennecEmulatorRunner,
|
|
|
|
+ 'thunderbird': ThunderbirdRunner,
|
|
|
|
+ }
|
|
|
|
+diff --git a/testing/mozbase/mozrunner/setup.py b/testing/mozbase/mozrunner/setup.py
|
|
|
|
+--- a/testing/mozbase/mozrunner/setup.py
|
|
|
|
++++ b/testing/mozbase/mozrunner/setup.py
|
|
|
|
+@@ -7,17 +7,16 @@ from __future__ import absolute_import
|
|
|
|
+ from setuptools import setup, find_packages
|
|
|
|
+
|
|
|
|
+ PACKAGE_NAME = 'mozrunner'
|
|
|
|
+ PACKAGE_VERSION = '7.4.0'
|
|
|
|
+
|
|
|
|
+ desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
|
|
|
|
+
|
|
|
|
+ deps = [
|
|
|
|
+- 'mozdevice>=1.1.6',
|
|
|
|
+ 'mozfile>=1.2',
|
|
|
|
+ 'mozinfo>=0.7,<2',
|
|
|
|
+ 'mozlog~=4.1.1',
|
|
|
|
+ 'mozprocess>=0.23,<2',
|
|
|
|
+ 'mozprofile~=2.1',
|
|
|
|
+ 'six>=1.10.0,<2',
|
|
|
|
+ ]
|
|
|
|
+
|
|
|
|
+diff --git a/testing/mozbase/mozversion/mozversion/mozversion.py b/testing/mozbase/mozversion/mozversion/mozversion.py
|
|
|
|
+--- a/testing/mozbase/mozversion/mozversion/mozversion.py
|
|
|
|
++++ b/testing/mozbase/mozversion/mozversion/mozversion.py
|
|
|
|
+@@ -47,38 +47,16 @@ class Version(object):
|
|
|
|
+ self._info['%s_%s' % (type, name)] = config.has_option(
|
|
|
|
+ section, key) and config.get(section, key) or None
|
|
|
|
+
|
|
|
|
+ if not self._info.get('application_display_name'):
|
|
|
|
+ self._info['application_display_name'] = \
|
|
|
|
+ self._info.get('application_name')
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+-class LocalFennecVersion(Version):
|
|
|
|
+-
|
|
|
|
+- def __init__(self, path, **kwargs):
|
|
|
|
+- Version.__init__(self, **kwargs)
|
|
|
|
+- self.get_gecko_info(path)
|
|
|
|
+-
|
|
|
|
+- def get_gecko_info(self, path):
|
|
|
|
+- archive = zipfile.ZipFile(path, 'r')
|
|
|
|
+- archive_list = archive.namelist()
|
|
|
|
+- for type, section in INI_DATA_MAPPING:
|
|
|
|
+- filename = "%s.ini" % type
|
|
|
|
+- if filename in archive_list:
|
|
|
|
+- fp = io.TextIOWrapper(archive.open(filename))
|
|
|
|
+- self._parse_ini_file(fp, type, section)
|
|
|
|
+- else:
|
|
|
|
+- self._logger.warning('Unable to find %s' % filename)
|
|
|
|
+-
|
|
|
|
+- if "package-name.txt" in archive_list:
|
|
|
|
+- fp = io.TextIOWrapper(archive.open("package-name.txt"))
|
|
|
|
+- self._info["package_name"] = fp.readlines()[0].strip()
|
|
|
|
+-
|
|
|
|
+-
|
|
|
|
+ class LocalVersion(Version):
|
|
|
|
+
|
|
|
|
+ def __init__(self, binary, **kwargs):
|
|
|
|
+ Version.__init__(self, **kwargs)
|
|
|
|
+
|
|
|
|
+ if binary:
|
|
|
|
+ # on Windows, the binary may be specified with or without the
|
|
|
|
+ # .exe extension
|
|
|
|
+@@ -106,28 +84,23 @@ class LocalVersion(Version):
|
|
|
|
+ def check_location(self, path):
|
|
|
|
+ return (os.path.exists(os.path.join(path, 'application.ini'))
|
|
|
|
+ and os.path.exists(os.path.join(path, 'platform.ini')))
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ def get_version(binary=None):
|
|
|
|
+ """
|
|
|
|
+ Returns the application version information as a dict. You can specify
|
|
|
|
+- a path to the binary of the application or an Android APK file (to get
|
|
|
|
+- version information for Firefox for Android). If this is omitted then the
|
|
|
|
++ a path to the binary of the application. If this is omitted then the
|
|
|
|
+ current directory is checked for the existance of an application.ini
|
|
|
|
+ file.
|
|
|
|
+
|
|
|
|
+- :param binary: Path to the binary for the application or Android APK file
|
|
|
|
++ :param binary: Path to the binary for the application
|
|
|
|
+ """
|
|
|
|
+- if binary and zipfile.is_zipfile(binary) and 'AndroidManifest.xml' in \
|
|
|
|
+- zipfile.ZipFile(binary, 'r').namelist():
|
|
|
|
+- version = LocalFennecVersion(binary)
|
|
|
|
+- else:
|
|
|
|
+- version = LocalVersion(binary)
|
|
|
|
++ version = LocalVersion(binary)
|
|
|
|
+
|
|
|
|
+ for (key, value) in sorted(version._info.items()):
|
|
|
|
+ if value:
|
|
|
|
+ version._logger.info('%s: %s' % (key, value))
|
|
|
|
+
|
|
|
|
+ return version._info
|
|
|
|
|
|
- deps = [
|
|
|
|
-- 'mozdevice>=1.1.6',
|
|
|
|
- 'mozfile>=1.2',
|
|
|
|
- 'mozinfo>=0.7,<2',
|
|
|
|
- 'mozlog~=4.1.1',
|
|
|
|
- 'mozprocess>=0.23,<2',
|
|
|
|
- 'mozprofile~=2.1',
|
|
|
|
- 'six>=1.10.0,<2',
|
|
|
|
- ]
|
|
|
|
|
|
|
|
diff --git a/testing/mozbase/packages.txt b/testing/mozbase/packages.txt
|
|
diff --git a/testing/mozbase/packages.txt b/testing/mozbase/packages.txt
|
|
--- a/testing/mozbase/packages.txt
|
|
--- a/testing/mozbase/packages.txt
|