mach_commands.py (20398B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import functools 6 import logging 7 import os 8 import sys 9 import warnings 10 from argparse import Namespace 11 from collections import defaultdict 12 13 from mach.decorators import Command, CommandArgument 14 from mozbuild.base import MachCommandConditions as conditions 15 from mozbuild.base import MozbuildObject 16 from mozfile import load_source 17 18 here = os.path.abspath(os.path.dirname(__file__)) 19 20 21 ENG_BUILD_REQUIRED = """ 22 The mochitest command requires an engineering build. It may be the case that 23 VARIANT=user or PRODUCTION=1 were set. Try re-building with VARIANT=eng: 24 25 $ VARIANT=eng ./build.sh 26 27 There should be an app called 'test-container.gaiamobile.org' located in 28 {}. 29 """.lstrip() 30 31 SUPPORTED_TESTS_NOT_FOUND = """ 32 The mochitest command could not find any supported tests to run! The 33 following flavors and subsuites were found, but are either not supported on 34 {} builds, or were excluded on the command line: 35 36 {} 37 38 Double check the command line you used, and make sure you are running in 39 context of the proper build. To switch build contexts, either run |mach| 40 from the appropriate objdir, or export the correct mozconfig: 41 42 $ export MOZCONFIG=path/to/mozconfig 43 """.lstrip() 44 45 TESTS_NOT_FOUND = """ 46 The mochitest command could not find any mochitests under the following 47 test path(s): 48 49 {} 50 51 Please check spelling and make sure there are mochitests living there. 52 """.lstrip() 53 54 SUPPORTED_APPS = ["firefox", "android", "thunderbird", "ios"] 55 56 parser = None 57 58 59 class MochitestRunner(MozbuildObject): 60 """Easily run mochitests. 61 62 This currently contains just the basics for running mochitests. We may want 63 to hook up result parsing, etc. 64 """ 65 66 def __init__(self, *args, **kwargs): 67 MozbuildObject.__init__(self, *args, **kwargs) 68 69 # TODO Bug 794506 remove once mach integrates with virtualenv. 70 build_path = os.path.join(self.topobjdir, "build") 71 if build_path not in sys.path: 72 sys.path.append(build_path) 73 74 self.tests_dir = os.path.join(self.topobjdir, "_tests") 75 self.mochitest_dir = os.path.join(self.tests_dir, "testing", "mochitest") 76 self.bin_dir = os.path.join(self.topobjdir, "dist", "bin") 77 78 def resolve_tests(self, test_paths, test_objects=None, cwd=None): 79 if test_objects: 80 return test_objects 81 82 from moztest.resolve import TestResolver 83 84 resolver = self._spawn(TestResolver) 85 tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd)) 86 return tests 87 88 def run_desktop_test(self, command_context, tests=None, **kwargs): 89 """Runs a mochitest.""" 90 # runtests.py is ambiguous, so we load the file/module manually. 91 if "mochitest" not in sys.modules: 92 path = os.path.join(self.mochitest_dir, "runtests.py") 93 load_source("mochitest", path) 94 95 import mochitest 96 97 # This is required to make other components happy. Sad, isn't it? 98 os.chdir(self.topobjdir) 99 100 # Automation installs its own stream handler to stdout. Since we want 101 # all logging to go through us, we just remove their handler. 102 remove_handlers = [ 103 l 104 for l in logging.getLogger().handlers 105 if isinstance(l, logging.StreamHandler) 106 ] 107 for handler in remove_handlers: 108 logging.getLogger().removeHandler(handler) 109 110 options = Namespace(**kwargs) 111 options.topsrcdir = self.topsrcdir 112 options.topobjdir = self.topobjdir 113 114 from manifestparser import TestManifest 115 116 if tests and not options.manifestFile: 117 manifest = TestManifest() 118 manifest.tests.extend(tests) 119 options.manifestFile = manifest 120 121 # When developing mochitest-plain tests, it's often useful to be able to 122 # refresh the page to pick up modifications. Therefore leave the browser 123 # open if only running a single mochitest-plain test. This behaviour can 124 # be overridden by passing in --keep-open=false. 125 if ( 126 len(tests) == 1 127 and options.keep_open is None 128 and not options.headless 129 and getattr(options, "flavor", "plain") == "plain" 130 ): 131 options.keep_open = True 132 133 # We need this to enable colorization of output. 134 self.log_manager.enable_unstructured() 135 result = mochitest.run_test_harness(parser, options) 136 self.log_manager.disable_unstructured() 137 return result 138 139 def run_android_test(self, command_context, tests, **kwargs): 140 host_ret = verify_host_bin() 141 if host_ret != 0: 142 return host_ret 143 144 path = os.path.join(self.mochitest_dir, "runtestsremote.py") 145 load_source("runtestsremote", path) 146 147 import runtestsremote 148 149 options = Namespace(**kwargs) 150 151 from manifestparser import TestManifest 152 153 if tests and not options.manifestFile: 154 manifest = TestManifest() 155 manifest.tests.extend(tests) 156 options.manifestFile = manifest 157 158 return runtestsremote.run_test_harness(parser, options) 159 160 def run_ios_test(self, command_context, tests, **kwargs): 161 host_ret = verify_host_bin() 162 if host_ret != 0: 163 return host_ret 164 165 path = os.path.join(self.mochitest_dir, "runtestsremoteios.py") 166 load_source("runtestsremoteios", path) 167 168 import runtestsremoteios 169 170 options = Namespace(**kwargs) 171 172 from manifestparser import TestManifest 173 174 if tests and not options.manifestFile: 175 manifest = TestManifest() 176 manifest.tests.extend(tests) 177 options.manifestFile = manifest 178 179 return runtestsremoteios.run_test_harness(parser, options) 180 181 def run_geckoview_junit_test(self, context, **kwargs): 182 host_ret = verify_host_bin() 183 if host_ret != 0: 184 return host_ret 185 186 import runjunit 187 188 options = Namespace(**kwargs) 189 190 return runjunit.run_test_harness(parser, options) 191 192 193 # parser 194 195 196 def setup_argument_parser(): 197 build_obj = MozbuildObject.from_environment(cwd=here) 198 199 build_path = os.path.join(build_obj.topobjdir, "build") 200 if build_path not in sys.path: 201 sys.path.append(build_path) 202 203 mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest") 204 205 with warnings.catch_warnings(): 206 warnings.simplefilter("ignore") 207 208 path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py") 209 if not os.path.exists(path): 210 path = os.path.join(here, "runtests.py") 211 212 load_source("mochitest", path) 213 214 from mochitest_options import MochitestArgumentParser 215 216 if conditions.is_android(build_obj): 217 # On Android, check for a connected device (and offer to start an 218 # emulator if appropriate) before running tests. This check must 219 # be done in this admittedly awkward place because 220 # MochitestArgumentParser initialization fails if no device is found. 221 from mozrunner.devices.android_device import ( 222 InstallIntent, 223 verify_android_device, 224 ) 225 226 # verify device and xre 227 verify_android_device(build_obj, install=InstallIntent.NO, xre=True) 228 229 if conditions.is_ios(build_obj): 230 # On iOS, check for a connected device before running tests. 231 from mozrunner.devices.ios_device import verify_ios_device 232 233 # verify device and xre 234 verify_ios_device(build_obj, install=False, xre=True) 235 236 global parser 237 parser = MochitestArgumentParser() 238 return parser 239 240 241 def setup_junit_argument_parser(): 242 build_obj = MozbuildObject.from_environment(cwd=here) 243 244 build_path = os.path.join(build_obj.topobjdir, "build") 245 if build_path not in sys.path: 246 sys.path.append(build_path) 247 248 mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest") 249 250 with warnings.catch_warnings(): 251 warnings.simplefilter("ignore") 252 253 # runtests.py contains MochitestDesktop, required by runjunit 254 path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py") 255 if not os.path.exists(path): 256 path = os.path.join(here, "runtests.py") 257 258 load_source("mochitest", path) 259 260 import runjunit 261 from mozrunner.devices.android_device import ( 262 InstallIntent, 263 verify_android_device, 264 ) 265 266 verify_android_device( 267 build_obj, install=InstallIntent.NO, xre=True, network=True 268 ) 269 270 global parser 271 parser = runjunit.JunitArgumentParser() 272 return parser 273 274 275 def verify_host_bin(): 276 # validate MOZ_HOST_BIN environment variables for Android tests 277 xpcshell_binary = "xpcshell" 278 if os.name == "nt": 279 xpcshell_binary = "xpcshell.exe" 280 MOZ_HOST_BIN = os.environ.get("MOZ_HOST_BIN") 281 if not MOZ_HOST_BIN: 282 print( 283 "environment variable MOZ_HOST_BIN must be set to a directory containing host " 284 "%s" % xpcshell_binary 285 ) 286 return 1 287 elif not os.path.isdir(MOZ_HOST_BIN): 288 print("$MOZ_HOST_BIN does not specify a directory") 289 return 1 290 elif not os.path.isfile(os.path.join(MOZ_HOST_BIN, xpcshell_binary)): 291 print("$MOZ_HOST_BIN/%s does not exist" % xpcshell_binary) 292 return 1 293 return 0 294 295 296 @Command( 297 "mochitest", 298 category="testing", 299 conditions=[functools.partial(conditions.is_buildapp_in, apps=SUPPORTED_APPS)], 300 description="Run any flavor of mochitest (integration test).", 301 parser=setup_argument_parser, 302 ) 303 def run_mochitest_general( 304 command_context, flavor=None, test_objects=None, resolve_tests=True, **kwargs 305 ): 306 from mochitest_options import ALL_FLAVORS 307 from mozlog.commandline import setup_logging 308 from mozlog.handlers import ResourceHandler, StreamHandler 309 from moztest.resolve import get_suite_definition 310 311 # TODO: This is only strictly necessary while mochitest is using Python 312 # 2 and can be removed once the command is migrated to Python 3. 313 command_context.activate_virtualenv() 314 315 buildapp = None 316 for app in SUPPORTED_APPS: 317 if conditions.is_buildapp_in(command_context, apps=[app]): 318 buildapp = app 319 break 320 321 # Force the buildapp to be android if requested 322 if kwargs.get("android"): 323 buildapp = "android" 324 325 flavors = None 326 if flavor: 327 for fname, fobj in ALL_FLAVORS.items(): 328 if flavor in fobj["aliases"]: 329 if buildapp not in fobj["enabled_apps"]: 330 continue 331 flavors = [fname] 332 break 333 else: 334 flavors = [f for f, v in ALL_FLAVORS.items() if buildapp in v["enabled_apps"]] 335 336 from mozbuild.controller.building import BuildDriver 337 338 command_context._ensure_state_subdir_exists(".") 339 340 test_paths = kwargs["test_paths"] 341 kwargs["test_paths"] = [] 342 343 if kwargs.get("debugger", None): 344 import mozdebug 345 346 if not mozdebug.get_debugger_info(kwargs.get("debugger")): 347 sys.exit(1) 348 349 mochitest = command_context._spawn(MochitestRunner) 350 tests = [] 351 if resolve_tests: 352 tests = mochitest.resolve_tests( 353 test_paths, test_objects, cwd=command_context._mach_context.cwd 354 ) 355 356 if not kwargs.get("log"): 357 # Create shared logger 358 format_args = {"level": command_context._mach_context.settings["test"]["level"]} 359 if len(tests) == 1: 360 format_args["verbose"] = True 361 format_args["compact"] = False 362 363 default_format = command_context._mach_context.settings["test"]["format"] 364 log = setup_logging( 365 "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args 366 ) 367 kwargs["log"] = log 368 for handler in log.handlers: 369 if isinstance(handler, StreamHandler): 370 handler.formatter.inner.summary_on_shutdown = True 371 372 log.add_handler(ResourceHandler(command_context)) 373 374 driver = command_context._spawn(BuildDriver) 375 driver.install_tests() 376 377 subsuite = kwargs.get("subsuite") 378 if subsuite == "default": 379 kwargs["subsuite"] = None 380 381 suites = defaultdict(list) 382 is_webrtc_tag_present = False 383 unsupported = set() 384 for test in tests: 385 # Check if we're running a webrtc test so we can enable webrtc 386 # specific test logic later if needed. 387 if "webrtc" in test.get("tags", ""): 388 is_webrtc_tag_present = True 389 390 # Filter out non-mochitests and unsupported flavors. 391 if test["flavor"] not in ALL_FLAVORS: 392 continue 393 394 key = (test["flavor"], test.get("subsuite", "")) 395 if test["flavor"] not in flavors: 396 unsupported.add(key) 397 continue 398 399 if subsuite == "default": 400 # "--subsuite default" means only run tests that don't have a subsuite 401 if test.get("subsuite"): 402 unsupported.add(key) 403 continue 404 elif subsuite and test.get("subsuite", "") != subsuite: 405 unsupported.add(key) 406 continue 407 408 suites[key].append(test) 409 410 # Only webrtc mochitests in the media suite need the websocketprocessbridge. 411 if ("mochitest", "media") in suites and is_webrtc_tag_present: 412 req = os.path.join( 413 "testing", 414 "tools", 415 "websocketprocessbridge", 416 "websocketprocessbridge_requirements_3.txt", 417 ) 418 command_context.virtualenv_manager.activate() 419 command_context.virtualenv_manager.install_pip_requirements( 420 req, require_hashes=False 421 ) 422 423 # sys.executable is used to start the websocketprocessbridge, though for some 424 # reason it doesn't get set when calling `activate_this.py` in the virtualenv. 425 sys.executable = command_context.virtualenv_manager.python_path 426 427 if ("browser-chrome", "a11y") in suites and sys.platform == "win32": 428 # Only Windows a11y browser tests need this. 429 req = os.path.join( 430 "accessible", 431 "tests", 432 "browser", 433 "windows", 434 "a11y_setup_requirements.txt", 435 ) 436 command_context.virtualenv_manager.activate() 437 command_context.virtualenv_manager.install_pip_requirements( 438 req, require_hashes=False 439 ) 440 441 # This is a hack to introduce an option in mach to not send 442 # filtered tests to the mochitest harness. Mochitest harness will read 443 # the master manifest in that case. 444 if not resolve_tests: 445 for flavor_name in flavors: 446 key = (flavor_name, kwargs.get("subsuite")) 447 suites[key] = [] 448 449 if not suites: 450 # Make it very clear why no tests were found 451 if not unsupported: 452 print( 453 TESTS_NOT_FOUND.format( 454 "\n".join(sorted(list(test_paths or test_objects))) 455 ) 456 ) 457 return 1 458 459 msg = [] 460 for f, s in unsupported: 461 fobj = ALL_FLAVORS[f] 462 apps = fobj["enabled_apps"] 463 name = fobj["aliases"][0] 464 if s: 465 name = f"{name} --subsuite {s}" 466 467 if buildapp not in apps: 468 reason = "requires {}".format(" or ".join(apps)) 469 else: 470 reason = "excluded by the command line" 471 msg.append(f" mochitest -f {name} ({reason})") 472 print(SUPPORTED_TESTS_NOT_FOUND.format(buildapp, "\n".join(sorted(msg)))) 473 return 1 474 475 if buildapp == "android": 476 from mozrunner.devices.android_device import ( 477 InstallIntent, 478 get_adb_path, 479 verify_android_device, 480 ) 481 482 app = kwargs.get("app") 483 if not app: 484 app = "org.mozilla.geckoview.test_runner" 485 device_serial = kwargs.get("deviceSerial") 486 install = InstallIntent.NO if kwargs.get("no_install") else InstallIntent.YES 487 aab = kwargs.get("aab") 488 489 # verify installation 490 verify_android_device( 491 command_context, 492 install=install, 493 xre=False, 494 network=True, 495 app=app, 496 aab=aab, 497 device_serial=device_serial, 498 ) 499 500 if not kwargs["adbPath"]: 501 kwargs["adbPath"] = get_adb_path(command_context) 502 503 run_mochitest = mochitest.run_android_test 504 elif buildapp == "ios": 505 from mozrunner.devices.ios_device import verify_ios_device 506 507 app = kwargs.get("app") 508 if not app: 509 app = "org.mozilla.ios.GeckoTestBrowser" 510 install = not kwargs.get("no_install") 511 512 # verify installation 513 verify_ios_device(command_context, install=install, xre=False, app=app) 514 515 run_mochitest = mochitest.run_ios_test 516 else: 517 run_mochitest = mochitest.run_desktop_test 518 519 overall = None 520 for (test_flavor, subsuite), tests in sorted(suites.items()): 521 suite_name, suite = get_suite_definition(test_flavor, subsuite) 522 if "test_paths" in suite["kwargs"]: 523 del suite["kwargs"]["test_paths"] 524 525 harness_args = kwargs.copy() 526 harness_args.update(suite["kwargs"]) 527 # Pass in the full suite name as defined in moztest/resolve.py in case 528 # chunk-by-runtime is called, in which case runtime information for 529 # specific mochitest suite has to be loaded. See Bug 1637463. 530 harness_args.update({"suite_name": suite_name}) 531 532 result = run_mochitest( 533 command_context._mach_context, tests=tests, **harness_args 534 ) 535 536 if result: 537 overall = result 538 539 # Halt tests on keyboard interrupt 540 if result == -1: 541 break 542 543 # Only shutdown the logger if we created it 544 if kwargs["log"].name == "mach-mochitest": 545 kwargs["log"].shutdown() 546 547 return overall 548 549 550 @Command( 551 "geckoview-junit", 552 category="testing", 553 conditions=[conditions.is_android], 554 description="Run remote geckoview junit tests.", 555 parser=setup_junit_argument_parser, 556 ) 557 @CommandArgument( 558 "--no-install", 559 help="Do not try to install application on device before " 560 + "running (default: False)", 561 action="store_true", 562 default=False, 563 ) 564 def run_junit(command_context, no_install, **kwargs): 565 command_context._ensure_state_subdir_exists(".") 566 567 from mozrunner.devices.android_device import ( 568 InstallIntent, 569 get_adb_path, 570 verify_android_device, 571 ) 572 573 # verify installation 574 app = kwargs.get("app") 575 device_serial = kwargs.get("deviceSerial") 576 verify_android_device( 577 command_context, 578 install=InstallIntent.NO if no_install else InstallIntent.YES, 579 xre=False, 580 app=app, 581 device_serial=device_serial, 582 ) 583 584 if not kwargs.get("adbPath"): 585 kwargs["adbPath"] = get_adb_path(command_context) 586 587 if not kwargs.get("log"): 588 from mozlog.commandline import setup_logging 589 590 format_args = {"level": command_context._mach_context.settings["test"]["level"]} 591 default_format = command_context._mach_context.settings["test"]["format"] 592 kwargs["log"] = setup_logging( 593 "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args 594 ) 595 596 if kwargs.get("mach_test") and kwargs.get("test_objects"): 597 test_classes = [] 598 test_objects = kwargs.get("test_objects") 599 for test_object in test_objects: 600 test_classes.append(classname_for_test(test_object["name"])) 601 kwargs["test_filters"] = test_classes 602 603 mochitest = command_context._spawn(MochitestRunner) 604 return mochitest.run_geckoview_junit_test(command_context._mach_context, **kwargs) 605 606 607 def classname_for_test(test): 608 """Convert path of test file to gradle recognized test suite name""" 609 test_path = os.path.join( 610 "mobile", 611 "android", 612 "geckoview", 613 "src", 614 "androidTest", 615 "java", 616 ) 617 return ( 618 os.path.normpath(test) 619 .split(os.path.normpath(test_path))[-1] 620 .removeprefix(os.path.sep) 621 .replace(os.path.sep, ".") 622 .removesuffix(".kt") 623 )