Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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 file, |
michael@0 | 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 4 | |
michael@0 | 5 | import mozcrash |
michael@0 | 6 | import threading |
michael@0 | 7 | import os |
michael@0 | 8 | import Queue |
michael@0 | 9 | import re |
michael@0 | 10 | import shutil |
michael@0 | 11 | import tempfile |
michael@0 | 12 | import time |
michael@0 | 13 | import traceback |
michael@0 | 14 | |
michael@0 | 15 | from automation import Automation |
michael@0 | 16 | from mozprocess import ProcessHandlerMixin |
michael@0 | 17 | |
michael@0 | 18 | |
michael@0 | 19 | class StdOutProc(ProcessHandlerMixin): |
michael@0 | 20 | """Process handler for b2g which puts all output in a Queue. |
michael@0 | 21 | """ |
michael@0 | 22 | |
michael@0 | 23 | def __init__(self, cmd, queue, **kwargs): |
michael@0 | 24 | self.queue = queue |
michael@0 | 25 | kwargs.setdefault('processOutputLine', []).append(self.handle_output) |
michael@0 | 26 | ProcessHandlerMixin.__init__(self, cmd, **kwargs) |
michael@0 | 27 | |
michael@0 | 28 | def handle_output(self, line): |
michael@0 | 29 | self.queue.put_nowait(line) |
michael@0 | 30 | |
michael@0 | 31 | |
michael@0 | 32 | class B2GRemoteAutomation(Automation): |
michael@0 | 33 | _devicemanager = None |
michael@0 | 34 | |
michael@0 | 35 | def __init__(self, deviceManager, appName='', remoteLog=None, |
michael@0 | 36 | marionette=None, context_chrome=True): |
michael@0 | 37 | self._devicemanager = deviceManager |
michael@0 | 38 | self._appName = appName |
michael@0 | 39 | self._remoteProfile = None |
michael@0 | 40 | self._remoteLog = remoteLog |
michael@0 | 41 | self.marionette = marionette |
michael@0 | 42 | self.context_chrome = context_chrome |
michael@0 | 43 | self._is_emulator = False |
michael@0 | 44 | self.test_script = None |
michael@0 | 45 | self.test_script_args = None |
michael@0 | 46 | |
michael@0 | 47 | # Default our product to b2g |
michael@0 | 48 | self._product = "b2g" |
michael@0 | 49 | self.lastTestSeen = "b2gautomation.py" |
michael@0 | 50 | # Default log finish to mochitest standard |
michael@0 | 51 | self.logFinish = 'INFO SimpleTest FINISHED' |
michael@0 | 52 | Automation.__init__(self) |
michael@0 | 53 | |
michael@0 | 54 | def setEmulator(self, is_emulator): |
michael@0 | 55 | self._is_emulator = is_emulator |
michael@0 | 56 | |
michael@0 | 57 | def setDeviceManager(self, deviceManager): |
michael@0 | 58 | self._devicemanager = deviceManager |
michael@0 | 59 | |
michael@0 | 60 | def setAppName(self, appName): |
michael@0 | 61 | self._appName = appName |
michael@0 | 62 | |
michael@0 | 63 | def setRemoteProfile(self, remoteProfile): |
michael@0 | 64 | self._remoteProfile = remoteProfile |
michael@0 | 65 | |
michael@0 | 66 | def setProduct(self, product): |
michael@0 | 67 | self._product = product |
michael@0 | 68 | |
michael@0 | 69 | def setRemoteLog(self, logfile): |
michael@0 | 70 | self._remoteLog = logfile |
michael@0 | 71 | |
michael@0 | 72 | def installExtension(self, extensionSource, profileDir, extensionID=None): |
michael@0 | 73 | # Bug 827504 - installing special-powers extension separately causes problems in B2G |
michael@0 | 74 | if extensionID != "special-powers@mozilla.org": |
michael@0 | 75 | Automation.installExtension(self, extensionSource, profileDir, extensionID) |
michael@0 | 76 | |
michael@0 | 77 | # Set up what we need for the remote environment |
michael@0 | 78 | def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False): |
michael@0 | 79 | # Because we are running remote, we don't want to mimic the local env |
michael@0 | 80 | # so no copying of os.environ |
michael@0 | 81 | if env is None: |
michael@0 | 82 | env = {} |
michael@0 | 83 | |
michael@0 | 84 | if crashreporter: |
michael@0 | 85 | env['MOZ_CRASHREPORTER'] = '1' |
michael@0 | 86 | env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' |
michael@0 | 87 | |
michael@0 | 88 | # We always hide the results table in B2G; it's much slower if we don't. |
michael@0 | 89 | env['MOZ_HIDE_RESULTS_TABLE'] = '1' |
michael@0 | 90 | return env |
michael@0 | 91 | |
michael@0 | 92 | def waitForNet(self): |
michael@0 | 93 | active = False |
michael@0 | 94 | time_out = 0 |
michael@0 | 95 | while not active and time_out < 40: |
michael@0 | 96 | data = self._devicemanager._runCmd(['shell', '/system/bin/netcfg']).stdout.readlines() |
michael@0 | 97 | data.pop(0) |
michael@0 | 98 | for line in data: |
michael@0 | 99 | if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): |
michael@0 | 100 | active = True |
michael@0 | 101 | break |
michael@0 | 102 | time_out += 1 |
michael@0 | 103 | time.sleep(1) |
michael@0 | 104 | return active |
michael@0 | 105 | |
michael@0 | 106 | def checkForCrashes(self, directory, symbolsPath): |
michael@0 | 107 | crashed = False |
michael@0 | 108 | remote_dump_dir = self._remoteProfile + '/minidumps' |
michael@0 | 109 | print "checking for crashes in '%s'" % remote_dump_dir |
michael@0 | 110 | if self._devicemanager.dirExists(remote_dump_dir): |
michael@0 | 111 | local_dump_dir = tempfile.mkdtemp() |
michael@0 | 112 | self._devicemanager.getDirectory(remote_dump_dir, local_dump_dir) |
michael@0 | 113 | try: |
michael@0 | 114 | crashed = mozcrash.check_for_crashes(local_dump_dir, symbolsPath, test_name=self.lastTestSeen) |
michael@0 | 115 | except: |
michael@0 | 116 | traceback.print_exc() |
michael@0 | 117 | finally: |
michael@0 | 118 | shutil.rmtree(local_dump_dir) |
michael@0 | 119 | self._devicemanager.removeDir(remote_dump_dir) |
michael@0 | 120 | return crashed |
michael@0 | 121 | |
michael@0 | 122 | def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): |
michael@0 | 123 | # if remote profile is specified, use that instead |
michael@0 | 124 | if (self._remoteProfile): |
michael@0 | 125 | profileDir = self._remoteProfile |
michael@0 | 126 | |
michael@0 | 127 | cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) |
michael@0 | 128 | |
michael@0 | 129 | return app, args |
michael@0 | 130 | |
michael@0 | 131 | def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, |
michael@0 | 132 | debuggerInfo, symbolsPath): |
michael@0 | 133 | """ Wait for tests to finish (as evidenced by a signature string |
michael@0 | 134 | in logcat), or for a given amount of time to elapse with no |
michael@0 | 135 | output. |
michael@0 | 136 | """ |
michael@0 | 137 | timeout = timeout or 120 |
michael@0 | 138 | while True: |
michael@0 | 139 | currentlog = proc.getStdoutLines(timeout) |
michael@0 | 140 | if currentlog: |
michael@0 | 141 | print currentlog |
michael@0 | 142 | # Match the test filepath from the last TEST-START line found in the new |
michael@0 | 143 | # log content. These lines are in the form: |
michael@0 | 144 | # ... INFO TEST-START | /filepath/we/wish/to/capture.html\n |
michael@0 | 145 | testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", currentlog) |
michael@0 | 146 | if testStartFilenames: |
michael@0 | 147 | self.lastTestSeen = testStartFilenames[-1] |
michael@0 | 148 | if hasattr(self, 'logFinish') and self.logFinish in currentlog: |
michael@0 | 149 | return 0 |
michael@0 | 150 | else: |
michael@0 | 151 | self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed " |
michael@0 | 152 | "out after %d seconds with no output", |
michael@0 | 153 | self.lastTestSeen, int(timeout)) |
michael@0 | 154 | return 1 |
michael@0 | 155 | |
michael@0 | 156 | def getDeviceStatus(self, serial=None): |
michael@0 | 157 | # Get the current status of the device. If we know the device |
michael@0 | 158 | # serial number, we look for that, otherwise we use the (presumably |
michael@0 | 159 | # only) device shown in 'adb devices'. |
michael@0 | 160 | serial = serial or self._devicemanager._deviceSerial |
michael@0 | 161 | status = 'unknown' |
michael@0 | 162 | |
michael@0 | 163 | for line in self._devicemanager._runCmd(['devices']).stdout.readlines(): |
michael@0 | 164 | result = re.match('(.*?)\t(.*)', line) |
michael@0 | 165 | if result: |
michael@0 | 166 | thisSerial = result.group(1) |
michael@0 | 167 | if not serial or thisSerial == serial: |
michael@0 | 168 | serial = thisSerial |
michael@0 | 169 | status = result.group(2) |
michael@0 | 170 | |
michael@0 | 171 | return (serial, status) |
michael@0 | 172 | |
michael@0 | 173 | def restartB2G(self): |
michael@0 | 174 | # TODO hangs in subprocess.Popen without this delay |
michael@0 | 175 | time.sleep(5) |
michael@0 | 176 | self._devicemanager._checkCmd(['shell', 'stop', 'b2g']) |
michael@0 | 177 | # Wait for a bit to make sure B2G has completely shut down. |
michael@0 | 178 | time.sleep(10) |
michael@0 | 179 | self._devicemanager._checkCmd(['shell', 'start', 'b2g']) |
michael@0 | 180 | if self._is_emulator: |
michael@0 | 181 | self.marionette.emulator.wait_for_port() |
michael@0 | 182 | |
michael@0 | 183 | def rebootDevice(self): |
michael@0 | 184 | # find device's current status and serial number |
michael@0 | 185 | serial, status = self.getDeviceStatus() |
michael@0 | 186 | |
michael@0 | 187 | # reboot! |
michael@0 | 188 | self._devicemanager._runCmd(['shell', '/system/bin/reboot']) |
michael@0 | 189 | |
michael@0 | 190 | # The above command can return while adb still thinks the device is |
michael@0 | 191 | # connected, so wait a little bit for it to disconnect from adb. |
michael@0 | 192 | time.sleep(10) |
michael@0 | 193 | |
michael@0 | 194 | # wait for device to come back to previous status |
michael@0 | 195 | print 'waiting for device to come back online after reboot' |
michael@0 | 196 | start = time.time() |
michael@0 | 197 | rserial, rstatus = self.getDeviceStatus(serial) |
michael@0 | 198 | while rstatus != 'device': |
michael@0 | 199 | if time.time() - start > 120: |
michael@0 | 200 | # device hasn't come back online in 2 minutes, something's wrong |
michael@0 | 201 | raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus)) |
michael@0 | 202 | time.sleep(5) |
michael@0 | 203 | rserial, rstatus = self.getDeviceStatus(serial) |
michael@0 | 204 | print 'device:', serial, 'status:', rstatus |
michael@0 | 205 | |
michael@0 | 206 | def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None): |
michael@0 | 207 | # On a desktop or fennec run, the Process method invokes a gecko |
michael@0 | 208 | # process in which to the tests. For B2G, we simply |
michael@0 | 209 | # reboot the device (which was configured with a test profile |
michael@0 | 210 | # already), wait for B2G to start up, and then navigate to the |
michael@0 | 211 | # test url using Marionette. There doesn't seem to be any way |
michael@0 | 212 | # to pass env variables into the B2G process, but this doesn't |
michael@0 | 213 | # seem to matter. |
michael@0 | 214 | |
michael@0 | 215 | # reboot device so it starts up with the mochitest profile |
michael@0 | 216 | # XXX: We could potentially use 'stop b2g' + 'start b2g' to achieve |
michael@0 | 217 | # a similar effect; will see which is more stable while attempting |
michael@0 | 218 | # to bring up the continuous integration. |
michael@0 | 219 | if not self._is_emulator: |
michael@0 | 220 | self.rebootDevice() |
michael@0 | 221 | time.sleep(5) |
michael@0 | 222 | #wait for wlan to come up |
michael@0 | 223 | if not self.waitForNet(): |
michael@0 | 224 | raise Exception("network did not come up, please configure the network" + |
michael@0 | 225 | " prior to running before running the automation framework") |
michael@0 | 226 | |
michael@0 | 227 | # stop b2g |
michael@0 | 228 | self._devicemanager._runCmd(['shell', 'stop', 'b2g']) |
michael@0 | 229 | time.sleep(5) |
michael@0 | 230 | |
michael@0 | 231 | # relaunch b2g inside b2g instance |
michael@0 | 232 | instance = self.B2GInstance(self._devicemanager, env=env) |
michael@0 | 233 | |
michael@0 | 234 | time.sleep(5) |
michael@0 | 235 | |
michael@0 | 236 | # Set up port forwarding again for Marionette, since any that |
michael@0 | 237 | # existed previously got wiped out by the reboot. |
michael@0 | 238 | if not self._is_emulator: |
michael@0 | 239 | self._devicemanager._checkCmd(['forward', |
michael@0 | 240 | 'tcp:%s' % self.marionette.port, |
michael@0 | 241 | 'tcp:%s' % self.marionette.port]) |
michael@0 | 242 | |
michael@0 | 243 | if self._is_emulator: |
michael@0 | 244 | self.marionette.emulator.wait_for_port() |
michael@0 | 245 | else: |
michael@0 | 246 | time.sleep(5) |
michael@0 | 247 | |
michael@0 | 248 | # start a marionette session |
michael@0 | 249 | session = self.marionette.start_session() |
michael@0 | 250 | if 'b2g' not in session: |
michael@0 | 251 | raise Exception("bad session value %s returned by start_session" % session) |
michael@0 | 252 | |
michael@0 | 253 | if self._is_emulator: |
michael@0 | 254 | # Disable offline status management (bug 777145), otherwise the network |
michael@0 | 255 | # will be 'offline' when the mochitests start. Presumably, the network |
michael@0 | 256 | # won't be offline on a real device, so we only do this for emulators. |
michael@0 | 257 | self.marionette.set_context(self.marionette.CONTEXT_CHROME) |
michael@0 | 258 | self.marionette.execute_script(""" |
michael@0 | 259 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 260 | Services.io.manageOfflineStatus = false; |
michael@0 | 261 | Services.io.offline = false; |
michael@0 | 262 | """) |
michael@0 | 263 | |
michael@0 | 264 | if self.context_chrome: |
michael@0 | 265 | self.marionette.set_context(self.marionette.CONTEXT_CHROME) |
michael@0 | 266 | else: |
michael@0 | 267 | self.marionette.set_context(self.marionette.CONTEXT_CONTENT) |
michael@0 | 268 | |
michael@0 | 269 | # run the script that starts the tests |
michael@0 | 270 | if self.test_script: |
michael@0 | 271 | if os.path.isfile(self.test_script): |
michael@0 | 272 | script = open(self.test_script, 'r') |
michael@0 | 273 | self.marionette.execute_script(script.read(), script_args=self.test_script_args) |
michael@0 | 274 | script.close() |
michael@0 | 275 | elif isinstance(self.test_script, basestring): |
michael@0 | 276 | self.marionette.execute_script(self.test_script, script_args=self.test_script_args) |
michael@0 | 277 | else: |
michael@0 | 278 | # assumes the tests are started on startup automatically |
michael@0 | 279 | pass |
michael@0 | 280 | |
michael@0 | 281 | return instance |
michael@0 | 282 | |
michael@0 | 283 | # be careful here as this inner class doesn't have access to outer class members |
michael@0 | 284 | class B2GInstance(object): |
michael@0 | 285 | """Represents a B2G instance running on a device, and exposes |
michael@0 | 286 | some process-like methods/properties that are expected by the |
michael@0 | 287 | automation. |
michael@0 | 288 | """ |
michael@0 | 289 | |
michael@0 | 290 | def __init__(self, dm, env=None): |
michael@0 | 291 | self.dm = dm |
michael@0 | 292 | self.env = env or {} |
michael@0 | 293 | self.stdout_proc = None |
michael@0 | 294 | self.queue = Queue.Queue() |
michael@0 | 295 | |
michael@0 | 296 | # Launch b2g in a separate thread, and dump all output lines |
michael@0 | 297 | # into a queue. The lines in this queue are |
michael@0 | 298 | # retrieved and returned by accessing the stdout property of |
michael@0 | 299 | # this class. |
michael@0 | 300 | cmd = [self.dm._adbPath] |
michael@0 | 301 | if self.dm._deviceSerial: |
michael@0 | 302 | cmd.extend(['-s', self.dm._deviceSerial]) |
michael@0 | 303 | cmd.append('shell') |
michael@0 | 304 | for k, v in self.env.iteritems(): |
michael@0 | 305 | cmd.append("%s=%s" % (k, v)) |
michael@0 | 306 | cmd.append('/system/bin/b2g.sh') |
michael@0 | 307 | proc = threading.Thread(target=self._save_stdout_proc, args=(cmd, self.queue)) |
michael@0 | 308 | proc.daemon = True |
michael@0 | 309 | proc.start() |
michael@0 | 310 | |
michael@0 | 311 | def _save_stdout_proc(self, cmd, queue): |
michael@0 | 312 | self.stdout_proc = StdOutProc(cmd, queue) |
michael@0 | 313 | self.stdout_proc.run() |
michael@0 | 314 | if hasattr(self.stdout_proc, 'processOutput'): |
michael@0 | 315 | self.stdout_proc.processOutput() |
michael@0 | 316 | self.stdout_proc.wait() |
michael@0 | 317 | self.stdout_proc = None |
michael@0 | 318 | |
michael@0 | 319 | @property |
michael@0 | 320 | def pid(self): |
michael@0 | 321 | # a dummy value to make the automation happy |
michael@0 | 322 | return 0 |
michael@0 | 323 | |
michael@0 | 324 | def getStdoutLines(self, timeout): |
michael@0 | 325 | # Return any lines in the queue used by the |
michael@0 | 326 | # b2g process handler. |
michael@0 | 327 | lines = [] |
michael@0 | 328 | # get all of the lines that are currently available |
michael@0 | 329 | while True: |
michael@0 | 330 | try: |
michael@0 | 331 | lines.append(self.queue.get_nowait()) |
michael@0 | 332 | except Queue.Empty: |
michael@0 | 333 | break |
michael@0 | 334 | |
michael@0 | 335 | # wait 'timeout' for any additional lines |
michael@0 | 336 | try: |
michael@0 | 337 | lines.append(self.queue.get(True, timeout)) |
michael@0 | 338 | except Queue.Empty: |
michael@0 | 339 | pass |
michael@0 | 340 | return '\n'.join(lines) |
michael@0 | 341 | |
michael@0 | 342 | def wait(self, timeout=None): |
michael@0 | 343 | # this should never happen |
michael@0 | 344 | raise Exception("'wait' called on B2GInstance") |
michael@0 | 345 | |
michael@0 | 346 | def kill(self): |
michael@0 | 347 | # this should never happen |
michael@0 | 348 | raise Exception("'kill' called on B2GInstance") |
michael@0 | 349 |