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 5from __future__ import absolute_import 6import datetime 7import os 8import posixpath 9import shutil 10import sys 11import tempfile 12import traceback 13import uuid 14 15sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(__file__)))) 16 17from runtests import MochitestDesktop, MessageLogger 18from mochitest_options import MochitestArgumentParser, build_obj 19from mozdevice import ADBDeviceFactory, ADBTimeoutError, RemoteProcessMonitor 20from mozscreenshot import dump_screen, dump_device_screen 21import mozcrash 22import mozinfo 23 24SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) 25 26 27class MochiRemote(MochitestDesktop): 28 localProfile = None 29 logMessages = [] 30 31 def __init__(self, options): 32 MochitestDesktop.__init__(self, options.flavor, vars(options)) 33 34 verbose = False 35 if ( 36 options.log_mach_verbose 37 or options.log_tbpl_level == "debug" 38 or options.log_mach_level == "debug" 39 or options.log_raw_level == "debug" 40 ): 41 verbose = True 42 if hasattr(options, "log"): 43 delattr(options, "log") 44 45 self.certdbNew = True 46 self.chromePushed = False 47 48 expected = options.app.split("/")[-1] 49 self.device = ADBDeviceFactory( 50 adb=options.adbPath or "adb", 51 device=options.deviceSerial, 52 test_root=options.remoteTestRoot, 53 verbose=verbose, 54 run_as_package=expected, 55 ) 56 57 if options.remoteTestRoot is None: 58 options.remoteTestRoot = self.device.test_root 59 options.dumpOutputDirectory = options.remoteTestRoot 60 self.remoteLogFile = posixpath.join( 61 options.remoteTestRoot, "logs", "mochitest.log" 62 ) 63 logParent = posixpath.dirname(self.remoteLogFile) 64 self.device.rm(logParent, force=True, recursive=True) 65 self.device.mkdir(logParent, parents=True) 66 67 self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile") 68 self.device.rm(self.remoteProfile, force=True, recursive=True) 69 70 self.message_logger = MessageLogger(logger=None) 71 self.message_logger.logger = self.log 72 73 # Check that Firefox is installed 74 expected = options.app.split("/")[-1] 75 if not self.device.is_app_installed(expected): 76 raise Exception("%s is not installed on this device" % expected) 77 78 self.device.clear_logcat() 79 80 self.remoteModulesDir = posixpath.join(options.remoteTestRoot, "modules/") 81 82 self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/") 83 self.device.rm(self.remoteCache, force=True, recursive=True) 84 85 # move necko cache to a location that can be cleaned up 86 options.extraPrefs += [ 87 "browser.cache.disk.parent_directory=%s" % self.remoteCache 88 ] 89 90 self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog") 91 self.device.rm(self.remoteMozLog, force=True, recursive=True) 92 self.device.mkdir(self.remoteMozLog, parents=True) 93 94 self.remoteChromeTestDir = posixpath.join(options.remoteTestRoot, "chrome") 95 self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) 96 self.device.mkdir(self.remoteChromeTestDir, parents=True) 97 98 self.appName = options.remoteappname 99 self.device.stop_application(self.appName) 100 if self.device.process_exist(self.appName): 101 self.log.warning("unable to kill %s before running tests!" % self.appName) 102 103 # Add Android version (SDK level) to mozinfo so that manifest entries 104 # can be conditional on android_version. 105 self.log.info( 106 "Android sdk version '%s'; will use this to filter manifests" 107 % str(self.device.version) 108 ) 109 mozinfo.info["android_version"] = str(self.device.version) 110 mozinfo.info["is_fennec"] = not ("geckoview" in options.app) 111 mozinfo.info["is_emulator"] = self.device._device_serial.startswith("emulator-") 112 113 def cleanup(self, options, final=False): 114 if final: 115 self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) 116 self.chromePushed = False 117 uploadDir = os.environ.get("MOZ_UPLOAD_DIR", None) 118 if uploadDir and self.device.is_dir(self.remoteMozLog): 119 self.device.pull(self.remoteMozLog, uploadDir) 120 self.device.rm(self.remoteLogFile, force=True) 121 self.device.rm(self.remoteProfile, force=True, recursive=True) 122 self.device.rm(self.remoteCache, force=True, recursive=True) 123 MochitestDesktop.cleanup(self, options, final) 124 self.localProfile = None 125 126 def dumpScreen(self, utilityPath): 127 if self.haveDumpedScreen: 128 self.log.info( 129 "Not taking screenshot here: see the one that was previously logged" 130 ) 131 return 132 self.haveDumpedScreen = True 133 if self.device._device_serial.startswith("emulator-"): 134 dump_screen(utilityPath, self.log) 135 else: 136 dump_device_screen(self.device, self.log) 137 138 def findPath(self, paths, filename=None): 139 for path in paths: 140 p = path 141 if filename: 142 p = os.path.join(p, filename) 143 if os.path.exists(self.getFullPath(p)): 144 return path 145 return None 146 147 # This seems kludgy, but this class uses paths from the remote host in the 148 # options, except when calling up to the base class, which doesn't 149 # understand the distinction. This switches out the remote values for local 150 # ones that the base class understands. This is necessary for the web 151 # server, SSL tunnel and profile building functions. 152 def switchToLocalPaths(self, options): 153 """ Set local paths in the options, return a function that will restore remote values """ 154 remoteXrePath = options.xrePath 155 remoteProfilePath = options.profilePath 156 remoteUtilityPath = options.utilityPath 157 158 paths = [ 159 options.xrePath, 160 ] 161 if build_obj: 162 paths.append(os.path.join(build_obj.topobjdir, "dist", "bin")) 163 options.xrePath = self.findPath(paths) 164 if options.xrePath is None: 165 self.log.error( 166 "unable to find xulrunner path for %s, please specify with --xre-path" 167 % os.name 168 ) 169 sys.exit(1) 170 171 xpcshell = "xpcshell" 172 if os.name == "nt": 173 xpcshell += ".exe" 174 175 if options.utilityPath: 176 paths = [options.utilityPath, options.xrePath] 177 else: 178 paths = [options.xrePath] 179 options.utilityPath = self.findPath(paths, xpcshell) 180 181 if options.utilityPath is None: 182 self.log.error( 183 "unable to find utility path for %s, please specify with --utility-path" 184 % os.name 185 ) 186 sys.exit(1) 187 188 xpcshell_path = os.path.join(options.utilityPath, xpcshell) 189 if RemoteProcessMonitor.elf_arm(xpcshell_path): 190 self.log.error( 191 "xpcshell at %s is an ARM binary; please use " 192 "the --utility-path argument to specify the path " 193 "to a desktop version." % xpcshell_path 194 ) 195 sys.exit(1) 196 197 if self.localProfile: 198 options.profilePath = self.localProfile 199 else: 200 options.profilePath = None 201 202 def fixup(): 203 options.xrePath = remoteXrePath 204 options.utilityPath = remoteUtilityPath 205 options.profilePath = remoteProfilePath 206 207 return fixup 208 209 def startServers(self, options, debuggerInfo, public=None): 210 """ Create the servers on the host and start them up """ 211 restoreRemotePaths = self.switchToLocalPaths(options) 212 MochitestDesktop.startServers(self, options, debuggerInfo, public=True) 213 restoreRemotePaths() 214 215 def buildProfile(self, options): 216 restoreRemotePaths = self.switchToLocalPaths(options) 217 if options.testingModulesDir: 218 try: 219 self.device.push(options.testingModulesDir, self.remoteModulesDir) 220 self.device.chmod(self.remoteModulesDir, recursive=True) 221 except Exception: 222 self.log.error( 223 "Automation Error: Unable to copy test modules to device." 224 ) 225 raise 226 savedTestingModulesDir = options.testingModulesDir 227 options.testingModulesDir = self.remoteModulesDir 228 else: 229 savedTestingModulesDir = None 230 manifest = MochitestDesktop.buildProfile(self, options) 231 if savedTestingModulesDir: 232 options.testingModulesDir = savedTestingModulesDir 233 self.localProfile = options.profilePath 234 235 restoreRemotePaths() 236 options.profilePath = self.remoteProfile 237 return manifest 238 239 def buildURLOptions(self, options, env): 240 saveLogFile = options.logFile 241 options.logFile = self.remoteLogFile 242 options.profilePath = self.localProfile 243 env["MOZ_HIDE_RESULTS_TABLE"] = "1" 244 retVal = MochitestDesktop.buildURLOptions(self, options, env) 245 246 # we really need testConfig.js (for browser chrome) 247 try: 248 self.device.push(options.profilePath, self.remoteProfile) 249 self.device.chmod(self.remoteProfile, recursive=True) 250 except Exception: 251 self.log.error("Automation Error: Unable to copy profile to device.") 252 raise 253 254 options.profilePath = self.remoteProfile 255 options.logFile = saveLogFile 256 return retVal 257 258 def getChromeTestDir(self, options): 259 local = super(MochiRemote, self).getChromeTestDir(options) 260 remote = self.remoteChromeTestDir 261 if options.flavor == "chrome" and not self.chromePushed: 262 self.log.info("pushing %s to %s on device..." % (local, remote)) 263 local = os.path.join(local, "chrome") 264 self.device.push(local, remote) 265 self.chromePushed = True 266 return remote 267 268 def getLogFilePath(self, logFile): 269 return logFile 270 271 def getGMPPluginPath(self, options): 272 # TODO: bug 1149374 273 return None 274 275 def environment(self, env=None, crashreporter=True, **kwargs): 276 # Since running remote, do not mimic the local env: do not copy os.environ 277 if env is None: 278 env = {} 279 280 if crashreporter: 281 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 282 env["MOZ_CRASHREPORTER"] = "1" 283 env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1" 284 else: 285 env["MOZ_CRASHREPORTER_DISABLE"] = "1" 286 287 # Crash on non-local network connections by default. 288 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily 289 # enable non-local connections for the purposes of local testing. 290 # Don't override the user's choice here. See bug 1049688. 291 env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1") 292 293 # Send an env var noting that we are in automation. Passing any 294 # value except the empty string will declare the value to exist. 295 # 296 # This may be used to disabled network connections during testing, e.g. 297 # Switchboard & telemetry uploads. 298 env.setdefault("MOZ_IN_AUTOMATION", "1") 299 300 # Set WebRTC logging in case it is not set yet. 301 env.setdefault("R_LOG_LEVEL", "6") 302 env.setdefault("R_LOG_DESTINATION", "stderr") 303 env.setdefault("R_LOG_VERBOSE", "1") 304 305 return env 306 307 def buildBrowserEnv(self, options, debugger=False): 308 browserEnv = MochitestDesktop.buildBrowserEnv(self, options, debugger=debugger) 309 # remove desktop environment not used on device 310 if "XPCOM_MEM_BLOAT_LOG" in browserEnv: 311 del browserEnv["XPCOM_MEM_BLOAT_LOG"] 312 if self.mozLogs: 313 browserEnv["MOZ_LOG_FILE"] = os.path.join( 314 self.remoteMozLog, "moz-pid=%PID-uid={}.log".format(str(uuid.uuid4())) 315 ) 316 if options.dmd: 317 browserEnv["DMD"] = "1" 318 # Contents of remoteMozLog will be pulled from device and copied to the 319 # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make 320 # MOZ_UPLOAD_DIR available to the browser environment so that tests 321 # can use it as though they were running on the host. 322 browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog 323 return browserEnv 324 325 def runApp( 326 self, 327 testUrl, 328 env, 329 app, 330 profile, 331 extraArgs, 332 utilityPath, 333 debuggerInfo=None, 334 valgrindPath=None, 335 valgrindArgs=None, 336 valgrindSuppFiles=None, 337 symbolsPath=None, 338 timeout=-1, 339 detectShutdownLeaks=False, 340 screenshotOnFail=False, 341 bisectChunk=None, 342 marionette_args=None, 343 e10s=True, 344 runFailures=False, 345 crashAsPass=False, 346 ): 347 """ 348 Run the app, log the duration it took to execute, return the status code. 349 Kill the app if it outputs nothing for |timeout| seconds. 350 """ 351 352 if timeout == -1: 353 timeout = self.DEFAULT_TIMEOUT 354 355 rpm = RemoteProcessMonitor( 356 self.appName, 357 self.device, 358 self.log, 359 self.message_logger, 360 self.remoteLogFile, 361 self.remoteProfile, 362 ) 363 startTime = datetime.datetime.now() 364 status = 0 365 profileDirectory = self.remoteProfile + "/" 366 args = [] 367 args.extend(extraArgs) 368 args.extend(("-no-remote", "-profile", profileDirectory)) 369 370 pid = rpm.launch( 371 app, 372 debuggerInfo, 373 testUrl, 374 args, 375 env=self.environment(env=env, crashreporter=not debuggerInfo), 376 e10s=e10s, 377 ) 378 379 # TODO: not using runFailures or crashAsPass, if we choose to use them 380 # we need to adjust status and check_for_crashes 381 self.log.info("runtestsremote.py | Application pid: %d" % pid) 382 if not rpm.wait(timeout): 383 status = 1 384 self.log.info( 385 "runtestsremote.py | Application ran for: %s" 386 % str(datetime.datetime.now() - startTime) 387 ) 388 crashed = self.check_for_crashes(symbolsPath, rpm.last_test_seen) 389 if crashed: 390 status = 1 391 392 self.countpass += rpm.counts["pass"] 393 self.countfail += rpm.counts["fail"] 394 self.counttodo += rpm.counts["todo"] 395 396 return status, rpm.last_test_seen 397 398 def check_for_crashes(self, symbols_path, last_test_seen): 399 """ 400 Pull any minidumps from remote profile and log any associated crashes. 401 """ 402 try: 403 dump_dir = tempfile.mkdtemp() 404 remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps") 405 if not self.device.is_dir(remote_crash_dir): 406 return False 407 self.device.pull(remote_crash_dir, dump_dir) 408 crashed = mozcrash.log_crashes( 409 self.log, dump_dir, symbols_path, test=last_test_seen 410 ) 411 finally: 412 try: 413 shutil.rmtree(dump_dir) 414 except Exception as e: 415 self.log.warning( 416 "unable to remove directory %s: %s" % (dump_dir, str(e)) 417 ) 418 return crashed 419 420 421def run_test_harness(parser, options): 422 parser.validate(options) 423 424 if options is None: 425 raise ValueError( 426 "Invalid options specified, use --help for a list of valid options" 427 ) 428 429 options.runByManifest = True 430 431 mochitest = MochiRemote(options) 432 433 try: 434 if options.verify: 435 retVal = mochitest.verifyTests(options) 436 else: 437 retVal = mochitest.runTests(options) 438 except Exception as e: 439 mochitest.log.error("Automation Error: Exception caught while running tests") 440 traceback.print_exc() 441 if isinstance(e, ADBTimeoutError): 442 mochitest.log.info("Device disconnected. Will not run mochitest.cleanup().") 443 else: 444 try: 445 mochitest.cleanup(options) 446 except Exception: 447 # device error cleaning up... oh well! 448 traceback.print_exc() 449 retVal = 1 450 451 mochitest.archiveMozLogs() 452 mochitest.message_logger.finish() 453 454 return retVal 455 456 457def main(args=sys.argv[1:]): 458 parser = MochitestArgumentParser(app="android") 459 options = parser.parse_args(args) 460 461 return run_test_harness(parser, options) 462 463 464if __name__ == "__main__": 465 sys.exit(main()) 466