testing/xpcshell/mach_commands.py

Wed, 31 Dec 2014 06:55:46 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:46 +0100
changeset 1
ca08bd8f51b2
permissions
-rw-r--r--

Added tag TORBROWSER_REPLICA for changeset 6474c204b198

michael@0 1 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 # Integrates the xpcshell test runner with mach.
michael@0 6
michael@0 7 from __future__ import unicode_literals, print_function
michael@0 8
michael@0 9 import mozpack.path
michael@0 10 import logging
michael@0 11 import os
michael@0 12 import shutil
michael@0 13 import sys
michael@0 14 import urllib2
michael@0 15
michael@0 16 from StringIO import StringIO
michael@0 17
michael@0 18 from mozbuild.base import (
michael@0 19 MachCommandBase,
michael@0 20 MozbuildObject,
michael@0 21 MachCommandConditions as conditions,
michael@0 22 )
michael@0 23
michael@0 24 from mach.decorators import (
michael@0 25 CommandArgument,
michael@0 26 CommandProvider,
michael@0 27 Command,
michael@0 28 )
michael@0 29
michael@0 30 ADB_NOT_FOUND = '''
michael@0 31 The %s command requires the adb binary to be on your path.
michael@0 32
michael@0 33 If you have a B2G build, this can be found in
michael@0 34 '%s/out/host/<platform>/bin'.
michael@0 35 '''.lstrip()
michael@0 36
michael@0 37 BUSYBOX_URL = 'http://www.busybox.net/downloads/binaries/latest/busybox-armv7l'
michael@0 38
michael@0 39
michael@0 40 if sys.version_info[0] < 3:
michael@0 41 unicode_type = unicode
michael@0 42 else:
michael@0 43 unicode_type = str
michael@0 44
michael@0 45 # Simple filter to omit the message emitted as a test file begins.
michael@0 46 class TestStartFilter(logging.Filter):
michael@0 47 def filter(self, record):
michael@0 48 return not record.params['msg'].endswith("running test ...")
michael@0 49
michael@0 50 # This should probably be consolidated with similar classes in other test
michael@0 51 # runners.
michael@0 52 class InvalidTestPathError(Exception):
michael@0 53 """Exception raised when the test path is not valid."""
michael@0 54
michael@0 55
michael@0 56 class XPCShellRunner(MozbuildObject):
michael@0 57 """Run xpcshell tests."""
michael@0 58 def run_suite(self, **kwargs):
michael@0 59 from manifestparser import TestManifest
michael@0 60 manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
michael@0 61 '_tests', 'xpcshell', 'xpcshell.ini')])
michael@0 62
michael@0 63 return self._run_xpcshell_harness(manifest=manifest, **kwargs)
michael@0 64
michael@0 65 def run_test(self, test_paths, interactive=False,
michael@0 66 keep_going=False, sequential=False, shuffle=False,
michael@0 67 debugger=None, debuggerArgs=None, debuggerInteractive=None,
michael@0 68 rerun_failures=False,
michael@0 69 # ignore parameters from other platforms' options
michael@0 70 **kwargs):
michael@0 71 """Runs an individual xpcshell test."""
michael@0 72 from mozbuild.testing import TestResolver
michael@0 73 from manifestparser import TestManifest
michael@0 74
michael@0 75 # TODO Bug 794506 remove once mach integrates with virtualenv.
michael@0 76 build_path = os.path.join(self.topobjdir, 'build')
michael@0 77 if build_path not in sys.path:
michael@0 78 sys.path.append(build_path)
michael@0 79
michael@0 80 if test_paths == ['all']:
michael@0 81 self.run_suite(interactive=interactive,
michael@0 82 keep_going=keep_going, shuffle=shuffle, sequential=sequential,
michael@0 83 debugger=debugger, debuggerArgs=debuggerArgs,
michael@0 84 debuggerInteractive=debuggerInteractive,
michael@0 85 rerun_failures=rerun_failures)
michael@0 86 return
michael@0 87
michael@0 88 resolver = self._spawn(TestResolver)
michael@0 89 tests = list(resolver.resolve_tests(paths=test_paths, flavor='xpcshell',
michael@0 90 cwd=self.cwd))
michael@0 91
michael@0 92 if not tests:
michael@0 93 raise InvalidTestPathError('We could not find an xpcshell test '
michael@0 94 'for the passed test path. Please select a path that is '
michael@0 95 'a test file or is a directory containing xpcshell tests.')
michael@0 96
michael@0 97 # Dynamically write out a manifest holding all the discovered tests.
michael@0 98 manifest = TestManifest()
michael@0 99 manifest.tests.extend(tests)
michael@0 100
michael@0 101 args = {
michael@0 102 'interactive': interactive,
michael@0 103 'keep_going': keep_going,
michael@0 104 'shuffle': shuffle,
michael@0 105 'sequential': sequential,
michael@0 106 'debugger': debugger,
michael@0 107 'debuggerArgs': debuggerArgs,
michael@0 108 'debuggerInteractive': debuggerInteractive,
michael@0 109 'rerun_failures': rerun_failures,
michael@0 110 'manifest': manifest,
michael@0 111 }
michael@0 112
michael@0 113 return self._run_xpcshell_harness(**args)
michael@0 114
michael@0 115 def _run_xpcshell_harness(self, manifest,
michael@0 116 test_path=None, shuffle=False, interactive=False,
michael@0 117 keep_going=False, sequential=False,
michael@0 118 debugger=None, debuggerArgs=None, debuggerInteractive=None,
michael@0 119 rerun_failures=False):
michael@0 120
michael@0 121 # Obtain a reference to the xpcshell test runner.
michael@0 122 import runxpcshelltests
michael@0 123
michael@0 124 dummy_log = StringIO()
michael@0 125 xpcshell = runxpcshelltests.XPCShellTests(log=dummy_log)
michael@0 126 self.log_manager.enable_unstructured()
michael@0 127
michael@0 128 xpcshell_filter = TestStartFilter()
michael@0 129 self.log_manager.terminal_handler.addFilter(xpcshell_filter)
michael@0 130
michael@0 131 tests_dir = os.path.join(self.topobjdir, '_tests', 'xpcshell')
michael@0 132 modules_dir = os.path.join(self.topobjdir, '_tests', 'modules')
michael@0 133 # We want output from the test to be written immediately if we are only
michael@0 134 # running a single test.
michael@0 135 verbose_output = test_path is not None or (manifest and len(manifest.test_paths())==1)
michael@0 136
michael@0 137 args = {
michael@0 138 'manifest': manifest,
michael@0 139 'xpcshell': os.path.join(self.bindir, 'xpcshell'),
michael@0 140 'mozInfo': os.path.join(self.topobjdir, 'mozinfo.json'),
michael@0 141 'symbolsPath': os.path.join(self.distdir, 'crashreporter-symbols'),
michael@0 142 'interactive': interactive,
michael@0 143 'keepGoing': keep_going,
michael@0 144 'logfiles': False,
michael@0 145 'sequential': sequential,
michael@0 146 'shuffle': shuffle,
michael@0 147 'testsRootDir': tests_dir,
michael@0 148 'testingModulesDir': modules_dir,
michael@0 149 'profileName': 'firefox',
michael@0 150 'verbose': test_path is not None,
michael@0 151 'xunitFilename': os.path.join(self.statedir, 'xpchsell.xunit.xml'),
michael@0 152 'xunitName': 'xpcshell',
michael@0 153 'pluginsPath': os.path.join(self.distdir, 'plugins'),
michael@0 154 'debugger': debugger,
michael@0 155 'debuggerArgs': debuggerArgs,
michael@0 156 'debuggerInteractive': debuggerInteractive,
michael@0 157 'on_message': (lambda obj, msg: xpcshell.log.info(msg.decode('utf-8', 'replace'))) \
michael@0 158 if verbose_output else None,
michael@0 159 }
michael@0 160
michael@0 161 if test_path is not None:
michael@0 162 args['testPath'] = test_path
michael@0 163
michael@0 164 # A failure manifest is written by default. If --rerun-failures is
michael@0 165 # specified and a prior failure manifest is found, the prior manifest
michael@0 166 # will be run. A new failure manifest is always written over any
michael@0 167 # prior failure manifest.
michael@0 168 failure_manifest_path = os.path.join(self.statedir, 'xpcshell.failures.ini')
michael@0 169 rerun_manifest_path = os.path.join(self.statedir, 'xpcshell.rerun.ini')
michael@0 170 if os.path.exists(failure_manifest_path) and rerun_failures:
michael@0 171 shutil.move(failure_manifest_path, rerun_manifest_path)
michael@0 172 args['manifest'] = rerun_manifest_path
michael@0 173 elif os.path.exists(failure_manifest_path):
michael@0 174 os.remove(failure_manifest_path)
michael@0 175 elif rerun_failures:
michael@0 176 print("No failures were found to re-run.")
michael@0 177 return 0
michael@0 178 args['failureManifest'] = failure_manifest_path
michael@0 179
michael@0 180 # Python through 2.7.2 has issues with unicode in some of the
michael@0 181 # arguments. Work around that.
michael@0 182 filtered_args = {}
michael@0 183 for k, v in args.items():
michael@0 184 if isinstance(v, unicode_type):
michael@0 185 v = v.encode('utf-8')
michael@0 186
michael@0 187 if isinstance(k, unicode_type):
michael@0 188 k = k.encode('utf-8')
michael@0 189
michael@0 190 filtered_args[k] = v
michael@0 191
michael@0 192 result = xpcshell.runTests(**filtered_args)
michael@0 193
michael@0 194 self.log_manager.terminal_handler.removeFilter(xpcshell_filter)
michael@0 195 self.log_manager.disable_unstructured()
michael@0 196
michael@0 197 if not result and not xpcshell.sequential:
michael@0 198 print("Tests were run in parallel. Try running with --sequential "
michael@0 199 "to make sure the failures were not caused by this.")
michael@0 200 return int(not result)
michael@0 201
michael@0 202 class AndroidXPCShellRunner(MozbuildObject):
michael@0 203 """Get specified DeviceManager"""
michael@0 204 def get_devicemanager(self, devicemanager, ip, port, remote_test_root):
michael@0 205 from mozdevice import devicemanagerADB, devicemanagerSUT
michael@0 206 dm = None
michael@0 207 if devicemanager == "adb":
michael@0 208 if ip:
michael@0 209 dm = devicemanagerADB.DeviceManagerADB(ip, port, packageName=None, deviceRoot=remote_test_root)
michael@0 210 else:
michael@0 211 dm = devicemanagerADB.DeviceManagerADB(packageName=None, deviceRoot=remote_test_root)
michael@0 212 else:
michael@0 213 if ip:
michael@0 214 dm = devicemanagerSUT.DeviceManagerSUT(ip, port, deviceRoot=remote_test_root)
michael@0 215 else:
michael@0 216 raise Exception("You must provide a device IP to connect to via the --ip option")
michael@0 217 return dm
michael@0 218
michael@0 219 """Run Android xpcshell tests."""
michael@0 220 def run_test(self,
michael@0 221 test_paths, keep_going,
michael@0 222 devicemanager, ip, port, remote_test_root, no_setup, local_apk,
michael@0 223 # ignore parameters from other platforms' options
michael@0 224 **kwargs):
michael@0 225 # TODO Bug 794506 remove once mach integrates with virtualenv.
michael@0 226 build_path = os.path.join(self.topobjdir, 'build')
michael@0 227 if build_path not in sys.path:
michael@0 228 sys.path.append(build_path)
michael@0 229
michael@0 230 import remotexpcshelltests
michael@0 231
michael@0 232 dm = self.get_devicemanager(devicemanager, ip, port, remote_test_root)
michael@0 233
michael@0 234 options = remotexpcshelltests.RemoteXPCShellOptions()
michael@0 235 options.shuffle = False
michael@0 236 options.sequential = True
michael@0 237 options.interactive = False
michael@0 238 options.debugger = None
michael@0 239 options.debuggerArgs = None
michael@0 240 options.setup = not no_setup
michael@0 241 options.keepGoing = keep_going
michael@0 242 options.objdir = self.topobjdir
michael@0 243 options.localLib = os.path.join(self.topobjdir, 'dist/fennec')
michael@0 244 options.localBin = os.path.join(self.topobjdir, 'dist/bin')
michael@0 245 options.testingModulesDir = os.path.join(self.topobjdir, '_tests/modules')
michael@0 246 options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json')
michael@0 247 options.manifest = os.path.join(self.topobjdir, '_tests/xpcshell/xpcshell_android.ini')
michael@0 248 options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
michael@0 249 if local_apk:
michael@0 250 options.localAPK = local_apk
michael@0 251 else:
michael@0 252 for file in os.listdir(os.path.join(options.objdir, "dist")):
michael@0 253 if file.endswith(".apk") and file.startswith("fennec"):
michael@0 254 options.localAPK = os.path.join(options.objdir, "dist")
michael@0 255 options.localAPK = os.path.join(options.localAPK, file)
michael@0 256 print ("using APK: " + options.localAPK)
michael@0 257 break
michael@0 258 else:
michael@0 259 raise Exception("You must specify an APK")
michael@0 260
michael@0 261 if test_paths == ['all']:
michael@0 262 testdirs = []
michael@0 263 options.testPath = None
michael@0 264 options.verbose = False
michael@0 265 else:
michael@0 266 if len(test_paths) > 1:
michael@0 267 print('Warning: only the first test path argument will be used.')
michael@0 268 testdirs = test_paths[0]
michael@0 269 options.testPath = test_paths[0]
michael@0 270 options.verbose = True
michael@0 271 dummy_log = StringIO()
michael@0 272 xpcshell = remotexpcshelltests.XPCShellRemote(dm, options, args=testdirs, log=dummy_log)
michael@0 273 self.log_manager.enable_unstructured()
michael@0 274
michael@0 275 xpcshell_filter = TestStartFilter()
michael@0 276 self.log_manager.terminal_handler.addFilter(xpcshell_filter)
michael@0 277
michael@0 278 result = xpcshell.runTests(xpcshell='xpcshell',
michael@0 279 testClass=remotexpcshelltests.RemoteXPCShellTestThread,
michael@0 280 testdirs=testdirs,
michael@0 281 mobileArgs=xpcshell.mobileArgs,
michael@0 282 **options.__dict__)
michael@0 283
michael@0 284 self.log_manager.terminal_handler.removeFilter(xpcshell_filter)
michael@0 285 self.log_manager.disable_unstructured()
michael@0 286
michael@0 287 return int(not result)
michael@0 288
michael@0 289 class B2GXPCShellRunner(MozbuildObject):
michael@0 290 def __init__(self, *args, **kwargs):
michael@0 291 MozbuildObject.__init__(self, *args, **kwargs)
michael@0 292
michael@0 293 # TODO Bug 794506 remove once mach integrates with virtualenv.
michael@0 294 build_path = os.path.join(self.topobjdir, 'build')
michael@0 295 if build_path not in sys.path:
michael@0 296 sys.path.append(build_path)
michael@0 297
michael@0 298 build_path = os.path.join(self.topsrcdir, 'build')
michael@0 299 if build_path not in sys.path:
michael@0 300 sys.path.append(build_path)
michael@0 301
michael@0 302 self.tests_dir = os.path.join(self.topobjdir, '_tests')
michael@0 303 self.xpcshell_dir = os.path.join(self.tests_dir, 'xpcshell')
michael@0 304 self.bin_dir = os.path.join(self.distdir, 'bin')
michael@0 305
michael@0 306 def _download_busybox(self, b2g_home):
michael@0 307 system_bin = os.path.join(b2g_home, 'out', 'target', 'product', 'generic', 'system', 'bin')
michael@0 308 busybox_path = os.path.join(system_bin, 'busybox')
michael@0 309
michael@0 310 if os.path.isfile(busybox_path):
michael@0 311 return busybox_path
michael@0 312
michael@0 313 if not os.path.isdir(system_bin):
michael@0 314 os.makedirs(system_bin)
michael@0 315
michael@0 316 try:
michael@0 317 data = urllib2.urlopen(BUSYBOX_URL)
michael@0 318 except urllib2.URLError:
michael@0 319 print('There was a problem downloading busybox. Proceeding without it,' \
michael@0 320 'initial setup will be slow.')
michael@0 321 return
michael@0 322
michael@0 323 with open(busybox_path, 'wb') as f:
michael@0 324 f.write(data.read())
michael@0 325 return busybox_path
michael@0 326
michael@0 327 def run_test(self, test_paths, b2g_home=None, busybox=None,
michael@0 328 # ignore parameters from other platforms' options
michael@0 329 **kwargs):
michael@0 330 try:
michael@0 331 import which
michael@0 332 which.which('adb')
michael@0 333 except which.WhichError:
michael@0 334 # TODO Find adb automatically if it isn't on the path
michael@0 335 print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home))
michael@0 336 sys.exit(1)
michael@0 337
michael@0 338 test_path = None
michael@0 339 if test_paths:
michael@0 340 if len(test_paths) > 1:
michael@0 341 print('Warning: Only the first test path will be used.')
michael@0 342
michael@0 343 test_path = self._wrap_path_argument(test_paths[0]).relpath()
michael@0 344
michael@0 345 import runtestsb2g
michael@0 346 parser = runtestsb2g.B2GOptions()
michael@0 347 options, args = parser.parse_args([])
michael@0 348
michael@0 349 options.b2g_path = b2g_home
michael@0 350 options.busybox = busybox or os.environ.get('BUSYBOX')
michael@0 351 options.emulator = 'arm'
michael@0 352 options.localLib = self.bin_dir
michael@0 353 options.localBin = self.bin_dir
michael@0 354 options.logcat_dir = self.xpcshell_dir
michael@0 355 options.manifest = os.path.join(self.xpcshell_dir, 'xpcshell_b2g.ini')
michael@0 356 options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json')
michael@0 357 options.objdir = self.topobjdir
michael@0 358 options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols'),
michael@0 359 options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
michael@0 360 options.testsRootDir = self.xpcshell_dir
michael@0 361 options.testPath = test_path
michael@0 362 options.use_device_libs = True
michael@0 363
michael@0 364 if not options.busybox:
michael@0 365 options.busybox = self._download_busybox(b2g_home)
michael@0 366
michael@0 367 return runtestsb2g.run_remote_xpcshell(parser, options, args)
michael@0 368
michael@0 369 def is_platform_supported(cls):
michael@0 370 """Must have a Firefox, Android or B2G build."""
michael@0 371 return conditions.is_android(cls) or \
michael@0 372 conditions.is_b2g(cls) or \
michael@0 373 conditions.is_firefox(cls)
michael@0 374
michael@0 375 @CommandProvider
michael@0 376 class MachCommands(MachCommandBase):
michael@0 377 def __init__(self, context):
michael@0 378 MachCommandBase.__init__(self, context)
michael@0 379
michael@0 380 for attr in ('b2g_home', 'device_name'):
michael@0 381 setattr(self, attr, getattr(context, attr, None))
michael@0 382
michael@0 383 @Command('xpcshell-test', category='testing',
michael@0 384 conditions=[is_platform_supported],
michael@0 385 description='Run XPCOM Shell tests.')
michael@0 386 @CommandArgument('test_paths', default='all', nargs='*', metavar='TEST',
michael@0 387 help='Test to run. Can be specified as a single JS file, a directory, '
michael@0 388 'or omitted. If omitted, the entire test suite is executed.')
michael@0 389 @CommandArgument("--debugger", default=None, metavar='DEBUGGER',
michael@0 390 help = "Run xpcshell under the given debugger.")
michael@0 391 @CommandArgument("--debugger-args", default=None, metavar='ARGS', type=str,
michael@0 392 dest = "debuggerArgs",
michael@0 393 help = "pass the given args to the debugger _before_ "
michael@0 394 "the application on the command line")
michael@0 395 @CommandArgument("--debugger-interactive", action = "store_true",
michael@0 396 dest = "debuggerInteractive",
michael@0 397 help = "prevents the test harness from redirecting "
michael@0 398 "stdout and stderr for interactive debuggers")
michael@0 399 @CommandArgument('--interactive', '-i', action='store_true',
michael@0 400 help='Open an xpcshell prompt before running tests.')
michael@0 401 @CommandArgument('--keep-going', '-k', action='store_true',
michael@0 402 help='Continue running tests after a SIGINT is received.')
michael@0 403 @CommandArgument('--sequential', action='store_true',
michael@0 404 help='Run the tests sequentially.')
michael@0 405 @CommandArgument('--shuffle', '-s', action='store_true',
michael@0 406 help='Randomize the execution order of tests.')
michael@0 407 @CommandArgument('--rerun-failures', action='store_true',
michael@0 408 help='Reruns failures from last time.')
michael@0 409 @CommandArgument('--devicemanager', default='adb', type=str,
michael@0 410 help='(Android) Type of devicemanager to use for communication: adb or sut')
michael@0 411 @CommandArgument('--ip', type=str, default=None,
michael@0 412 help='(Android) IP address of device')
michael@0 413 @CommandArgument('--port', type=int, default=20701,
michael@0 414 help='(Android) Port of device')
michael@0 415 @CommandArgument('--remote_test_root', type=str, default=None,
michael@0 416 help='(Android) Remote test root such as /mnt/sdcard or /data/local')
michael@0 417 @CommandArgument('--no-setup', action='store_true',
michael@0 418 help='(Android) Do not copy files to device')
michael@0 419 @CommandArgument('--local-apk', type=str, default=None,
michael@0 420 help='(Android) Use specified Fennec APK')
michael@0 421 @CommandArgument('--busybox', type=str, default=None,
michael@0 422 help='(B2G) Path to busybox binary (speeds up installation of tests).')
michael@0 423 def run_xpcshell_test(self, **params):
michael@0 424 from mozbuild.controller.building import BuildDriver
michael@0 425
michael@0 426 # We should probably have a utility function to ensure the tree is
michael@0 427 # ready to run tests. Until then, we just create the state dir (in
michael@0 428 # case the tree wasn't built with mach).
michael@0 429 self._ensure_state_subdir_exists('.')
michael@0 430
michael@0 431 driver = self._spawn(BuildDriver)
michael@0 432 driver.install_tests(remove=False)
michael@0 433
michael@0 434 if conditions.is_android(self):
michael@0 435 xpcshell = self._spawn(AndroidXPCShellRunner)
michael@0 436 elif conditions.is_b2g(self):
michael@0 437 xpcshell = self._spawn(B2GXPCShellRunner)
michael@0 438 params['b2g_home'] = self.b2g_home
michael@0 439 else:
michael@0 440 xpcshell = self._spawn(XPCShellRunner)
michael@0 441 xpcshell.cwd = self._mach_context.cwd
michael@0 442
michael@0 443 try:
michael@0 444 return xpcshell.run_test(**params)
michael@0 445 except InvalidTestPathError as e:
michael@0 446 print(e.message)
michael@0 447 return 1

mercurial