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