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/.
14 sys
.path
.insert(0, os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(__file__
))))
18 from mochitest_options
import MochitestArgumentParser
, build_obj
19 from mozdevice
import ADBDeviceFactory
, ADBTimeoutError
, RemoteProcessMonitor
20 from mozscreenshot
import dump_device_screen
, dump_screen
21 from runtests
import MessageLogger
, MochitestDesktop
23 SCRIPT_DIR
= os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(__file__
)))
26 class MochiRemote(MochitestDesktop
):
30 def __init__(self
, options
):
31 MochitestDesktop
.__init
__(self
, options
.flavor
, vars(options
))
35 options
.log_mach_verbose
36 or options
.log_tbpl_level
== "debug"
37 or options
.log_mach_level
== "debug"
38 or options
.log_raw_level
== "debug"
41 if hasattr(options
, "log"):
42 delattr(options
, "log")
45 self
.chromePushed
= False
47 expected
= options
.app
.split("/")[-1]
48 self
.device
= ADBDeviceFactory(
49 adb
=options
.adbPath
or "adb",
50 device
=options
.deviceSerial
,
51 test_root
=options
.remoteTestRoot
,
53 run_as_package
=expected
,
56 if options
.remoteTestRoot
is None:
57 options
.remoteTestRoot
= self
.device
.test_root
58 options
.dumpOutputDirectory
= options
.remoteTestRoot
59 self
.remoteLogFile
= posixpath
.join(
60 options
.remoteTestRoot
, "logs", "mochitest.log"
62 logParent
= posixpath
.dirname(self
.remoteLogFile
)
63 self
.device
.rm(logParent
, force
=True, recursive
=True)
64 self
.device
.mkdir(logParent
, parents
=True)
66 self
.remoteProfile
= posixpath
.join(options
.remoteTestRoot
, "profile")
67 self
.device
.rm(self
.remoteProfile
, force
=True, recursive
=True)
69 self
.message_logger
= MessageLogger(logger
=None)
70 self
.message_logger
.logger
= self
.log
72 # Check that Firefox is installed
73 expected
= options
.app
.split("/")[-1]
74 if not self
.device
.is_app_installed(expected
):
75 raise Exception("%s is not installed on this device" % expected
)
77 self
.device
.clear_logcat()
79 self
.remoteModulesDir
= posixpath
.join(options
.remoteTestRoot
, "modules/")
81 self
.remoteCache
= posixpath
.join(options
.remoteTestRoot
, "cache/")
82 self
.device
.rm(self
.remoteCache
, force
=True, recursive
=True)
84 # move necko cache to a location that can be cleaned up
85 options
.extraPrefs
+= [
86 "browser.cache.disk.parent_directory=%s" % self
.remoteCache
89 self
.remoteMozLog
= posixpath
.join(options
.remoteTestRoot
, "mozlog")
90 self
.device
.rm(self
.remoteMozLog
, force
=True, recursive
=True)
91 self
.device
.mkdir(self
.remoteMozLog
, parents
=True)
93 self
.remoteChromeTestDir
= posixpath
.join(options
.remoteTestRoot
, "chrome")
94 self
.device
.rm(self
.remoteChromeTestDir
, force
=True, recursive
=True)
95 self
.device
.mkdir(self
.remoteChromeTestDir
, parents
=True)
97 self
.appName
= options
.remoteappname
98 self
.device
.stop_application(self
.appName
)
99 if self
.device
.process_exist(self
.appName
):
100 self
.log
.warning("unable to kill %s before running tests!" % self
.appName
)
102 # Add Android version (SDK level) to mozinfo so that manifest entries
103 # can be conditional on android_version.
105 "Android sdk version '%s'; will use this to filter manifests"
106 % str(self
.device
.version
)
108 mozinfo
.info
["android_version"] = str(self
.device
.version
)
109 mozinfo
.info
["is_emulator"] = self
.device
._device
_serial
.startswith("emulator-")
111 def cleanup(self
, options
, final
=False):
113 self
.device
.rm(self
.remoteChromeTestDir
, force
=True, recursive
=True)
114 self
.chromePushed
= False
115 uploadDir
= os
.environ
.get("MOZ_UPLOAD_DIR", None)
116 if uploadDir
and self
.device
.is_dir(self
.remoteMozLog
):
117 self
.device
.pull(self
.remoteMozLog
, uploadDir
)
118 self
.device
.rm(self
.remoteLogFile
, force
=True)
119 self
.device
.rm(self
.remoteProfile
, force
=True, recursive
=True)
120 self
.device
.rm(self
.remoteCache
, force
=True, recursive
=True)
121 MochitestDesktop
.cleanup(self
, options
, final
)
122 self
.localProfile
= None
124 def dumpScreen(self
, utilityPath
):
125 if self
.haveDumpedScreen
:
127 "Not taking screenshot here: see the one that was previously logged"
130 self
.haveDumpedScreen
= True
131 if self
.device
._device
_serial
.startswith("emulator-"):
132 dump_screen(utilityPath
, self
.log
)
134 dump_device_screen(self
.device
, self
.log
)
136 def findPath(self
, paths
, filename
=None):
140 p
= os
.path
.join(p
, filename
)
141 if os
.path
.exists(self
.getFullPath(p
)):
145 # This seems kludgy, but this class uses paths from the remote host in the
146 # options, except when calling up to the base class, which doesn't
147 # understand the distinction. This switches out the remote values for local
148 # ones that the base class understands. This is necessary for the web
149 # server, SSL tunnel and profile building functions.
150 def switchToLocalPaths(self
, options
):
151 """Set local paths in the options, return a function that will restore remote values"""
152 remoteXrePath
= options
.xrePath
153 remoteProfilePath
= options
.profilePath
154 remoteUtilityPath
= options
.utilityPath
160 paths
.append(os
.path
.join(build_obj
.topobjdir
, "dist", "bin"))
161 options
.xrePath
= self
.findPath(paths
)
162 if options
.xrePath
is None:
164 "unable to find xulrunner path for %s, please specify with --xre-path"
169 xpcshell
= "xpcshell"
173 if options
.utilityPath
:
174 paths
= [options
.utilityPath
, options
.xrePath
]
176 paths
= [options
.xrePath
]
177 options
.utilityPath
= self
.findPath(paths
, xpcshell
)
179 if options
.utilityPath
is None:
181 "unable to find utility path for %s, please specify with --utility-path"
186 xpcshell_path
= os
.path
.join(options
.utilityPath
, xpcshell
)
187 if RemoteProcessMonitor
.elf_arm(xpcshell_path
):
189 "xpcshell at %s is an ARM binary; please use "
190 "the --utility-path argument to specify the path "
191 "to a desktop version." % xpcshell_path
195 if self
.localProfile
:
196 options
.profilePath
= self
.localProfile
198 options
.profilePath
= None
201 options
.xrePath
= remoteXrePath
202 options
.utilityPath
= remoteUtilityPath
203 options
.profilePath
= remoteProfilePath
207 def startServers(self
, options
, debuggerInfo
, public
=None):
208 """Create the servers on the host and start them up"""
209 restoreRemotePaths
= self
.switchToLocalPaths(options
)
210 MochitestDesktop
.startServers(self
, options
, debuggerInfo
, public
=True)
213 def buildProfile(self
, options
):
214 restoreRemotePaths
= self
.switchToLocalPaths(options
)
215 if options
.testingModulesDir
:
217 self
.device
.push(options
.testingModulesDir
, self
.remoteModulesDir
)
218 self
.device
.chmod(self
.remoteModulesDir
, recursive
=True)
221 "Automation Error: Unable to copy test modules to device."
224 savedTestingModulesDir
= options
.testingModulesDir
225 options
.testingModulesDir
= self
.remoteModulesDir
227 savedTestingModulesDir
= None
228 manifest
= MochitestDesktop
.buildProfile(self
, options
)
229 if savedTestingModulesDir
:
230 options
.testingModulesDir
= savedTestingModulesDir
231 self
.localProfile
= options
.profilePath
234 options
.profilePath
= self
.remoteProfile
237 def buildURLOptions(self
, options
, env
):
238 saveLogFile
= options
.logFile
239 options
.logFile
= self
.remoteLogFile
240 options
.profilePath
= self
.localProfile
241 env
["MOZ_HIDE_RESULTS_TABLE"] = "1"
242 retVal
= MochitestDesktop
.buildURLOptions(self
, options
, env
)
244 # we really need testConfig.js (for browser chrome)
246 self
.device
.push(options
.profilePath
, self
.remoteProfile
)
247 self
.device
.chmod(self
.remoteProfile
, recursive
=True)
249 self
.log
.error("Automation Error: Unable to copy profile to device.")
252 options
.profilePath
= self
.remoteProfile
253 options
.logFile
= saveLogFile
256 def getChromeTestDir(self
, options
):
257 local
= super(MochiRemote
, self
).getChromeTestDir(options
)
258 remote
= self
.remoteChromeTestDir
259 if options
.flavor
== "chrome" and not self
.chromePushed
:
260 self
.log
.info("pushing %s to %s on device..." % (local
, remote
))
261 local
= os
.path
.join(local
, "chrome")
262 self
.device
.push(local
, remote
)
263 self
.chromePushed
= True
266 def getLogFilePath(self
, logFile
):
269 def getGMPPluginPath(self
, options
):
273 def environment(self
, env
=None, crashreporter
=True, **kwargs
):
274 # Since running remote, do not mimic the local env: do not copy os.environ
279 env
["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
280 env
["MOZ_CRASHREPORTER"] = "1"
281 env
["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
283 env
["MOZ_CRASHREPORTER_DISABLE"] = "1"
285 # Crash on non-local network connections by default.
286 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
287 # enable non-local connections for the purposes of local testing.
288 # Don't override the user's choice here. See bug 1049688.
289 env
.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
291 # Send an env var noting that we are in automation. Passing any
292 # value except the empty string will declare the value to exist.
294 # This may be used to disabled network connections during testing, e.g.
295 # Switchboard & telemetry uploads.
296 env
.setdefault("MOZ_IN_AUTOMATION", "1")
298 # Set WebRTC logging in case it is not set yet.
299 env
.setdefault("R_LOG_LEVEL", "6")
300 env
.setdefault("R_LOG_DESTINATION", "stderr")
301 env
.setdefault("R_LOG_VERBOSE", "1")
305 def buildBrowserEnv(self
, options
, debugger
=False):
306 browserEnv
= MochitestDesktop
.buildBrowserEnv(self
, options
, debugger
=debugger
)
307 # remove desktop environment not used on device
308 if "XPCOM_MEM_BLOAT_LOG" in browserEnv
:
309 del browserEnv
["XPCOM_MEM_BLOAT_LOG"]
311 browserEnv
["MOZ_LOG_FILE"] = os
.path
.join(
312 self
.remoteMozLog
, "moz-pid=%PID-uid={}.log".format(str(uuid
.uuid4()))
315 browserEnv
["DMD"] = "1"
316 # Contents of remoteMozLog will be pulled from device and copied to the
317 # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make
318 # MOZ_UPLOAD_DIR available to the browser environment so that tests
319 # can use it as though they were running on the host.
320 browserEnv
["MOZ_UPLOAD_DIR"] = self
.remoteMozLog
334 valgrindSuppFiles
=None,
337 detectShutdownLeaks
=False,
338 screenshotOnFail
=False,
340 restartAfterFailure
=False,
341 marionette_args
=None,
345 currentManifest
=None,
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.
353 timeout
= self
.DEFAULT_TIMEOUT
355 rpm
= RemoteProcessMonitor(
363 startTime
= datetime
.datetime
.now()
365 profileDirectory
= self
.remoteProfile
+ "/"
367 args
.extend(extraArgs
)
368 args
.extend(("-no-remote", "-profile", profileDirectory
))
375 env
=self
.environment(env
=env
, crashreporter
=not debuggerInfo
),
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
):
385 "runtestsremote.py | Application ran for: %s"
386 % str(datetime
.datetime
.now() - startTime
)
389 lastTestSeen
= currentManifest
or "Main app process exited normally"
391 crashed
= self
.check_for_crashes(symbolsPath
, lastTestSeen
)
395 self
.countpass
+= rpm
.counts
["pass"]
396 self
.countfail
+= rpm
.counts
["fail"]
397 self
.counttodo
+= rpm
.counts
["todo"]
399 return status
, lastTestSeen
401 def check_for_crashes(self
, symbols_path
, last_test_seen
):
403 Pull any minidumps from remote profile and log any associated crashes.
406 dump_dir
= tempfile
.mkdtemp()
407 remote_crash_dir
= posixpath
.join(self
.remoteProfile
, "minidumps")
408 if not self
.device
.is_dir(remote_crash_dir
):
410 self
.device
.pull(remote_crash_dir
, dump_dir
)
411 crashed
= mozcrash
.log_crashes(
412 self
.log
, dump_dir
, symbols_path
, test
=last_test_seen
416 shutil
.rmtree(dump_dir
)
417 except Exception as e
:
419 "unable to remove directory %s: %s" % (dump_dir
, str(e
))
424 def run_test_harness(parser
, options
):
425 parser
.validate(options
)
429 "Invalid options specified, use --help for a list of valid options"
432 options
.runByManifest
= True
434 mochitest
= MochiRemote(options
)
438 retVal
= mochitest
.verifyTests(options
)
440 retVal
= mochitest
.runTests(options
)
441 except Exception as e
:
442 mochitest
.log
.error("Automation Error: Exception caught while running tests")
443 traceback
.print_exc()
444 if isinstance(e
, ADBTimeoutError
):
445 mochitest
.log
.info("Device disconnected. Will not run mochitest.cleanup().")
448 mochitest
.cleanup(options
)
450 # device error cleaning up... oh well!
451 traceback
.print_exc()
454 mochitest
.archiveMozLogs()
455 mochitest
.message_logger
.finish()
460 def main(args
=sys
.argv
[1:]):
461 parser
= MochitestArgumentParser(app
="android")
462 options
= parser
.parse_args(args
)
464 return run_test_harness(parser
, options
)
467 if __name__
== "__main__":