Source code

Revision control

Other Tools

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
"""
6
Runs the Mochitest test harness.
7
"""
8
9
from __future__ import with_statement
10
import os
11
import sys
12
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
13
sys.path.insert(0, SCRIPT_DIR)
14
15
from argparse import Namespace
16
from collections import defaultdict
17
from contextlib import closing
18
from distutils import spawn
19
import copy
20
import ctypes
21
import glob
22
import json
23
import mozcrash
24
import mozdebug
25
import mozinfo
26
import mozprocess
27
import mozrunner
28
import numbers
29
import platform
30
import re
31
import shutil
32
import signal
33
import socket
34
import subprocess
35
import sys
36
import tempfile
37
import time
38
import traceback
39
import urllib2
40
import uuid
41
import zipfile
42
import bisection
43
44
from ctypes.util import find_library
45
from datetime import datetime, timedelta
46
from manifestparser import TestManifest
47
from manifestparser.util import normsep
48
from manifestparser.filters import (
49
chunk_by_dir,
50
chunk_by_runtime,
51
chunk_by_slice,
52
pathprefix,
53
subsuite,
54
tags,
55
)
56
57
try:
58
from marionette_driver.addons import Addons
59
from marionette_harness import Marionette
60
except ImportError as e: # noqa
61
# Defer ImportError until attempt to use Marionette
62
def reraise(*args, **kwargs):
63
raise(e) # noqa
64
Marionette = reraise
65
66
from leaks import ShutdownLeaks, LSANLeaks
67
from mochitest_options import (
68
MochitestArgumentParser, build_obj, get_default_valgrind_suppression_files
69
)
70
from mozprofile import Profile
71
from mozprofile.cli import parse_preferences, parse_key_value, KeyValueParseError
72
from mozprofile.permissions import ServerLocations
73
from urllib import quote_plus as encodeURIComponent
74
from mozlog.formatters import TbplFormatter
75
from mozlog import commandline
76
from mozrunner.utils import get_stack_fixer_function, test_environment
77
from mozscreenshot import dump_screen
78
import mozleak
79
80
HAVE_PSUTIL = False
81
try:
82
import psutil
83
HAVE_PSUTIL = True
84
except ImportError:
85
pass
86
87
import six
88
89
here = os.path.abspath(os.path.dirname(__file__))
90
91
NO_TESTS_FOUND = """
92
No tests were found for flavor '{}' and the following manifest filters:
93
{}
94
95
Make sure the test paths (if any) are spelt correctly and the corresponding
96
--flavor and --subsuite are being used. See `mach mochitest --help` for a
97
list of valid flavors.
98
""".lstrip()
99
100
101
########################################
102
# Option for MOZ (former NSPR) logging #
103
########################################
104
105
# Set the desired log modules you want a log be produced
106
# by a try run for, or leave blank to disable the feature.
107
# This will be passed to MOZ_LOG environment variable.
108
# Try run will then put a download link for a zip archive
109
# of all the log files on treeherder.
110
MOZ_LOG = ""
111
112
#####################
113
# Test log handling #
114
#####################
115
116
# output processing
117
118
119
class MochitestFormatter(TbplFormatter):
120
121
"""
122
The purpose of this class is to maintain compatibility with legacy users.
123
Mozharness' summary parser expects the count prefix, and others expect python
124
logging to contain a line prefix picked up by TBPL (bug 1043420).
125
Those directly logging "TEST-UNEXPECTED" require no prefix to log output
126
in order to turn a build orange (bug 1044206).
127
128
Once updates are propagated to Mozharness, this class may be removed.
129
"""
130
log_num = 0
131
132
def __init__(self):
133
super(MochitestFormatter, self).__init__()
134
135
def __call__(self, data):
136
output = super(MochitestFormatter, self).__call__(data)
137
if not output:
138
return None
139
log_level = data.get('level', 'info').upper()
140
141
if 'js_source' in data or log_level == 'ERROR':
142
data.pop('js_source', None)
143
output = '%d %s %s' % (
144
MochitestFormatter.log_num, log_level, output)
145
MochitestFormatter.log_num += 1
146
147
return output
148
149
# output processing
150
151
152
class MessageLogger(object):
153
154
"""File-like object for logging messages (structured logs)"""
155
BUFFERING_THRESHOLD = 100
156
# This is a delimiter used by the JS side to avoid logs interleaving
157
DELIMITER = u'\ue175\uee31\u2c32\uacbf'
158
BUFFERED_ACTIONS = set(['test_status', 'log'])
159
VALID_ACTIONS = set(['suite_start', 'suite_end', 'test_start', 'test_end',
160
'test_status', 'log', 'assertion_count',
161
'buffering_on', 'buffering_off'])
162
TEST_PATH_PREFIXES = ['/tests/',
166
167
def __init__(self, logger, buffering=True, structured=True):
168
self.logger = logger
169
self.structured = structured
170
self.gecko_id = 'GECKO'
171
172
# Even if buffering is enabled, we only want to buffer messages between
173
# TEST-START/TEST-END. So it is off to begin, but will be enabled after
174
# a TEST-START comes in.
175
self.buffering = False
176
self.restore_buffering = buffering
177
178
# Message buffering
179
self.buffered_messages = []
180
181
def validate(self, obj):
182
"""Tests whether the given object is a valid structured message
183
(only does a superficial validation)"""
184
if not (isinstance(obj, dict) and 'action' in obj and obj[
185
'action'] in MessageLogger.VALID_ACTIONS):
186
raise ValueError
187
188
def _fix_subtest_name(self, message):
189
"""Make sure subtest name is a string"""
190
if 'subtest' in message and not isinstance(message['subtest'], six.string_types):
191
message['subtest'] = str(message['subtest'])
192
193
def _fix_test_name(self, message):
194
"""Normalize a logged test path to match the relative path from the sourcedir.
195
"""
196
if 'test' in message:
197
test = message['test']
198
for prefix in MessageLogger.TEST_PATH_PREFIXES:
199
if test.startswith(prefix):
200
message['test'] = test[len(prefix):]
201
break
202
203
def _fix_message_format(self, message):
204
if 'message' in message:
205
if isinstance(message['message'], bytes):
206
message['message'] = message['message'].decode('utf-8', 'replace')
207
elif not isinstance(message['message'], unicode):
208
message['message'] = unicode(message['message'])
209
210
def parse_line(self, line):
211
"""Takes a given line of input (structured or not) and
212
returns a list of structured messages"""
213
if isinstance(line, six.binary_type):
214
# if line is a sequence of bytes, let's decode it
215
line = line.rstrip().decode("UTF-8", "replace")
216
else:
217
# line is in unicode - so let's use it as it is
218
line = line.rstrip()
219
220
messages = []
221
for fragment in line.split(MessageLogger.DELIMITER):
222
if not fragment:
223
continue
224
try:
225
message = json.loads(fragment)
226
self.validate(message)
227
except ValueError:
228
if self.structured:
229
message = dict(
230
action='process_output',
231
process=self.gecko_id,
232
data=fragment,
233
)
234
else:
235
message = dict(
236
action='log',
237
level='info',
238
message=fragment,
239
)
240
241
self._fix_subtest_name(message)
242
self._fix_test_name(message)
243
self._fix_message_format(message)
244
messages.append(message)
245
246
return messages
247
248
def process_message(self, message):
249
"""Processes a structured message. Takes into account buffering, errors, ..."""
250
# Activation/deactivating message buffering from the JS side
251
if message['action'] == 'buffering_on':
252
self.buffering = True
253
return
254
if message['action'] == 'buffering_off':
255
self.buffering = False
256
return
257
258
# Error detection also supports "raw" errors (in log messages) because some tests
259
# manually dump 'TEST-UNEXPECTED-FAIL'.
260
if ('expected' in message or (message['action'] == 'log' and message[
261
'message'].startswith('TEST-UNEXPECTED'))):
262
self.restore_buffering = self.restore_buffering or self.buffering
263
self.buffering = False
264
if self.buffered_messages:
265
snipped = len(
266
self.buffered_messages) - self.BUFFERING_THRESHOLD
267
if snipped > 0:
268
self.logger.info(
269
"<snipped {0} output lines - "
270
"if you need more context, please use "
271
"SimpleTest.requestCompleteLog() in your test>" .format(snipped))
272
# Dumping previously buffered messages
273
self.dump_buffered(limit=True)
274
275
# Logging the error message
276
self.logger.log_raw(message)
277
# Determine if message should be buffered
278
elif self.buffering and self.structured and message['action'] in self.BUFFERED_ACTIONS:
279
self.buffered_messages.append(message)
280
# Otherwise log the message directly
281
else:
282
self.logger.log_raw(message)
283
284
# If a test ended, we clean the buffer
285
if message['action'] == 'test_end':
286
self.buffered_messages = []
287
self.restore_buffering = self.restore_buffering or self.buffering
288
self.buffering = False
289
290
if message['action'] == 'test_start':
291
if self.restore_buffering:
292
self.restore_buffering = False
293
self.buffering = True
294
295
def write(self, line):
296
messages = self.parse_line(line)
297
for message in messages:
298
self.process_message(message)
299
return messages
300
301
def flush(self):
302
sys.stdout.flush()
303
304
def dump_buffered(self, limit=False):
305
if limit:
306
dumped_messages = self.buffered_messages[-self.BUFFERING_THRESHOLD:]
307
else:
308
dumped_messages = self.buffered_messages
309
310
last_timestamp = None
311
for buf in dumped_messages:
312
timestamp = datetime.fromtimestamp(buf['time'] / 1000).strftime('%H:%M:%S')
313
if timestamp != last_timestamp:
314
self.logger.info("Buffered messages logged at {}".format(timestamp))
315
last_timestamp = timestamp
316
317
self.logger.log_raw(buf)
318
self.logger.info("Buffered messages finished")
319
# Cleaning the list of buffered messages
320
self.buffered_messages = []
321
322
def finish(self):
323
self.dump_buffered()
324
self.buffering = False
325
self.logger.suite_end()
326
327
####################
328
# PROCESS HANDLING #
329
####################
330
331
332
def call(*args, **kwargs):
333
"""front-end function to mozprocess.ProcessHandler"""
334
# TODO: upstream -> mozprocess
336
process = mozprocess.ProcessHandler(*args, **kwargs)
337
process.run()
338
return process.wait()
339
340
341
def killPid(pid, log):
343
344
if HAVE_PSUTIL:
345
# Kill a process tree (including grandchildren) with signal.SIGTERM
346
if pid == os.getpid():
347
raise RuntimeError("Error: trying to kill ourselves, not another process")
348
try:
349
parent = psutil.Process(pid)
350
children = parent.children(recursive=True)
351
children.append(parent)
352
for p in children:
353
p.send_signal(signal.SIGTERM)
354
gone, alive = psutil.wait_procs(children, timeout=30)
355
for p in gone:
356
log.info('psutil found pid %s dead' % p.pid)
357
for p in alive:
358
log.info('failed to kill pid %d after 30s' % p.pid)
359
except Exception as e:
360
log.info("Error: Failed to kill process %d: %s" % (pid, str(e)))
361
else:
362
try:
363
os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
364
except Exception as e:
365
log.info("Failed to kill process %d: %s" % (pid, str(e)))
366
367
368
if mozinfo.isWin:
369
import ctypes.wintypes
370
371
def isPidAlive(pid):
372
STILL_ACTIVE = 259
373
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
374
pHandle = ctypes.windll.kernel32.OpenProcess(
375
PROCESS_QUERY_LIMITED_INFORMATION,
376
0,
377
pid)
378
if not pHandle:
379
return False
380
381
try:
382
pExitCode = ctypes.wintypes.DWORD()
383
ctypes.windll.kernel32.GetExitCodeProcess(
384
pHandle,
385
ctypes.byref(pExitCode))
386
387
if pExitCode.value != STILL_ACTIVE:
388
return False
389
390
# We have a live process handle. But Windows aggressively
391
# re-uses pids, so let's attempt to verify that this is
392
# actually Firefox.
393
namesize = 1024
394
pName = ctypes.create_string_buffer(namesize)
395
namelen = ctypes.windll.kernel32.GetProcessImageFileNameA(pHandle,
396
pName,
397
namesize)
398
if namelen == 0:
399
# Still an active process, so conservatively assume it's Firefox.
400
return True
401
402
return pName.value.endswith(('firefox.exe', 'plugin-container.exe'))
403
finally:
404
ctypes.windll.kernel32.CloseHandle(pHandle)
405
else:
406
import errno
407
408
def isPidAlive(pid):
409
try:
410
# kill(pid, 0) checks for a valid PID without actually sending a signal
411
# The method throws OSError if the PID is invalid, which we catch
412
# below.
413
os.kill(pid, 0)
414
415
# Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
416
# the process terminates before we get to this point.
417
wpid, wstatus = os.waitpid(pid, os.WNOHANG)
418
return wpid == 0
419
except OSError as err:
420
# Catch the errors we might expect from os.kill/os.waitpid,
421
# and re-raise any others
422
if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
423
return False
424
raise
425
# TODO: ^ upstream isPidAlive to mozprocess
426
427
#######################
428
# HTTP SERVER SUPPORT #
429
#######################
430
431
432
class MochitestServer(object):
433
434
"Web server used to serve Mochitests, for closer fidelity to the real web."
435
436
def __init__(self, options, logger):
437
if isinstance(options, Namespace):
438
options = vars(options)
439
self._log = logger
440
self._keep_open = bool(options['keep_open'])
441
self._utilityPath = options['utilityPath']
442
self._xrePath = options['xrePath']
443
self._profileDir = options['profilePath']
444
self.webServer = options['webServer']
445
self.httpPort = options['httpPort']
446
if options.get('remoteWebServer') == "10.0.2.2":
447
# probably running an Android emulator and 10.0.2.2 will
448
# not be visible from host
449
shutdownServer = "127.0.0.1"
450
else:
451
shutdownServer = self.webServer
452
self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % {
453
"server": shutdownServer,
454
"port": self.httpPort}
455
self.testPrefix = "undefined"
456
457
if options.get('httpdPath'):
458
self._httpdPath = options['httpdPath']
459
else:
460
self._httpdPath = SCRIPT_DIR
461
self._httpdPath = os.path.abspath(self._httpdPath)
462
463
def start(self):
464
"Run the Mochitest server, returning the process ID of the server."
465
466
# get testing environment
467
env = test_environment(xrePath=self._xrePath, log=self._log)
468
env["XPCOM_DEBUG_BREAK"] = "warn"
469
if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None:
470
env["LD_LIBRARY_PATH"] = self._xrePath
471
else:
472
env["LD_LIBRARY_PATH"] = ":".join([self._xrePath, env["LD_LIBRARY_PATH"]])
473
474
# When running with an ASan build, our xpcshell server will also be ASan-enabled,
475
# thus consuming too much resources when running together with the browser on
476
# the test slaves. Try to limit the amount of resources by disabling certain
477
# features.
478
env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
479
480
# Likewise, when running with a TSan build, our xpcshell server will
481
# also be TSan-enabled. Except that in this case, we don't really
482
# care about races in xpcshell. So disable TSan for the server.
483
env["TSAN_OPTIONS"] = "report_bugs=0"
484
485
if mozinfo.isWin:
486
env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
487
488
args = [
489
"-g",
490
self._xrePath,
491
"-f",
492
os.path.join(
493
self._httpdPath,
494
"httpd.js"),
495
"-e",
496
"const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; "
497
"const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; "
498
"const _DISPLAY_RESULTS = %(displayResults)s;" % {
499
"profile": self._profileDir.replace(
500
'\\',
501
'\\\\'),
502
"port": self.httpPort,
503
"server": self.webServer,
504
"testPrefix": self.testPrefix,
505
"displayResults": str(
506
self._keep_open).lower()},
507
"-f",
508
os.path.join(
509
SCRIPT_DIR,
510
"server.js")]
511
512
xpcshell = os.path.join(self._utilityPath,
513
"xpcshell" + mozinfo.info['bin_suffix'])
514
command = [xpcshell] + args
515
self._process = mozprocess.ProcessHandler(
516
command,
517
cwd=SCRIPT_DIR,
518
env=env)
519
self._process.run()
520
self._log.info(
521
"%s : launching %s" %
522
(self.__class__.__name__, command))
523
pid = self._process.pid
524
self._log.info("runtests.py | Server pid: %d" % pid)
525
526
def ensureReady(self, timeout):
527
assert timeout >= 0
528
529
aliveFile = os.path.join(self._profileDir, "server_alive.txt")
530
i = 0
531
while i < timeout:
532
if os.path.exists(aliveFile):
533
break
534
time.sleep(.05)
535
i += .05
536
else:
537
self._log.error(
538
"TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup.")
539
self.stop()
540
sys.exit(1)
541
542
def stop(self):
543
try:
544
with closing(urllib2.urlopen(self.shutdownURL)) as c:
545
c.read()
546
547
# TODO: need ProcessHandler.poll()
549
# rtncode = self._process.poll()
550
rtncode = self._process.proc.poll()
551
if rtncode is None:
552
# TODO: need ProcessHandler.terminate() and/or .send_signal()
554
# self._process.terminate()
555
self._process.proc.terminate()
556
except Exception:
557
self._log.info("Failed to stop web server on %s" % self.shutdownURL)
558
traceback.print_exc()
559
self._process.kill()
560
561
562
class WebSocketServer(object):
563
564
"Class which encapsulates the mod_pywebsocket server"
565
566
def __init__(self, options, scriptdir, logger, debuggerInfo=None):
567
self.port = options.webSocketPort
568
self.debuggerInfo = debuggerInfo
569
self._log = logger
570
self._scriptdir = scriptdir
571
572
def start(self):
573
# Invoke pywebsocket through a wrapper which adds special SIGINT handling.
574
#
575
# If we're in an interactive debugger, the wrapper causes the server to
576
# ignore SIGINT so the server doesn't capture a ctrl+c meant for the
577
# debugger.
578
#
579
# If we're not in an interactive debugger, the wrapper causes the server to
580
# die silently upon receiving a SIGINT.
581
scriptPath = 'pywebsocket_wrapper.py'
582
script = os.path.join(self._scriptdir, scriptPath)
583
584
cmd = [sys.executable, script]
585
if self.debuggerInfo and self.debuggerInfo.interactive:
586
cmd += ['--interactive']
587
cmd += ['-H', '127.0.0.1', '-p', str(self.port), '-w', self._scriptdir,
588
'-l', os.path.join(self._scriptdir, "websock.log"),
589
'--log-level=debug', '--allow-handlers-outside-root-dir']
590
# start the process
591
self._process = mozprocess.ProcessHandler(cmd, cwd=SCRIPT_DIR)
592
self._process.run()
593
pid = self._process.pid
594
self._log.info("runtests.py | Websocket server pid: %d" % pid)
595
596
def stop(self):
597
self._process.kill()
598
599
600
class SSLTunnel:
601
602
def __init__(self, options, logger):
603
self.log = logger
604
self.process = None
605
self.utilityPath = options.utilityPath
606
self.xrePath = options.xrePath
607
self.certPath = options.certPath
608
self.sslPort = options.sslPort
609
self.httpPort = options.httpPort
610
self.webServer = options.webServer
611
self.webSocketPort = options.webSocketPort
612
613
self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
614
self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
615
self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
616
617
def writeLocation(self, config, loc):
618
for option in loc.options:
619
match = self.customCertRE.match(option)
620
if match:
621
customcert = match.group("nickname")
622
config.write("listen:%s:%s:%s:%s\n" %
623
(loc.host, loc.port, self.sslPort, customcert))
624
625
match = self.clientAuthRE.match(option)
626
if match:
627
clientauth = match.group("clientauth")
628
config.write("clientauth:%s:%s:%s:%s\n" %
629
(loc.host, loc.port, self.sslPort, clientauth))
630
631
match = self.redirRE.match(option)
632
if match:
633
redirhost = match.group("redirhost")
634
config.write("redirhost:%s:%s:%s:%s\n" %
635
(loc.host, loc.port, self.sslPort, redirhost))
636
637
if option in (
638
'tls1',
639
'tls1_1',
640
'tls1_2',
641
'tls1_3',
642
'ssl3',
643
'rc4',
644
'failHandshake'):
645
config.write(
646
"%s:%s:%s:%s\n" %
647
(option, loc.host, loc.port, self.sslPort))
648
649
def buildConfig(self, locations, public=None):
650
"""Create the ssltunnel configuration file"""
651
configFd, self.configFile = tempfile.mkstemp(
652
prefix="ssltunnel", suffix=".cfg")
653
with os.fdopen(configFd, "w") as config:
654
config.write("httpproxy:1\n")
655
config.write("certdbdir:%s\n" % self.certPath)
656
config.write("forward:127.0.0.1:%s\n" % self.httpPort)
657
config.write(
658
"websocketserver:%s:%s\n" %
659
(self.webServer, self.webSocketPort))
660
# Use "*" to tell ssltunnel to listen on the public ip
661
# address instead of the loopback address 127.0.0.1. This
662
# may have the side-effect of causing firewall warnings on
663
# macOS and Windows. Use "127.0.0.1" to listen on the
664
# loopback address. Remote tests using physical or
665
# emulated Android devices must use the public ip address
666
# in order for the sslproxy to work but Desktop tests
667
# which run on the same host as ssltunnel may use the
668
# loopback address.
669
listen_address = "*" if public else "127.0.0.1"
670
config.write("listen:%s:%s:pgoserver\n" % (listen_address, self.sslPort))
671
672
for loc in locations:
673
if loc.scheme == "https" and "nocert" not in loc.options:
674
self.writeLocation(config, loc)
675
676
def start(self):
677
""" Starts the SSL Tunnel """
678
679
# start ssltunnel to provide https:// URLs capability
680
bin_suffix = mozinfo.info.get('bin_suffix', '')
681
ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix)
682
if not os.path.exists(ssltunnel):
683
self.log.error(
684
"INFO | runtests.py | expected to find ssltunnel at %s" %
685
ssltunnel)
686
exit(1)
687
688
env = test_environment(xrePath=self.xrePath, log=self.log)
689
env["LD_LIBRARY_PATH"] = self.xrePath
690
self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile],
691
env=env)
692
self.process.run()
693
self.log.info("runtests.py | SSL tunnel pid: %d" % self.process.pid)
694
695
def stop(self):
696
""" Stops the SSL Tunnel and cleans up """
697
if self.process is not None:
698
self.process.kill()
699
if os.path.exists(self.configFile):
700
os.remove(self.configFile)
701
702
703
def checkAndConfigureV4l2loopback(device):
704
'''
705
Determine if a given device path is a v4l2loopback device, and if so
706
toggle a few settings on it via fcntl. Very linux-specific.
707
708
Returns (status, device name) where status is a boolean.
709
'''
710
if not mozinfo.isLinux:
711
return False, ''
712
713
libc = ctypes.cdll.LoadLibrary(find_library("c"))
714
O_RDWR = 2
715
# These are from linux/videodev2.h
716
717
class v4l2_capability(ctypes.Structure):
718
_fields_ = [
719
('driver', ctypes.c_char * 16),
720
('card', ctypes.c_char * 32),
721
('bus_info', ctypes.c_char * 32),
722
('version', ctypes.c_uint32),
723
('capabilities', ctypes.c_uint32),
724
('device_caps', ctypes.c_uint32),
725
('reserved', ctypes.c_uint32 * 3)
726
]
727
VIDIOC_QUERYCAP = 0x80685600
728
729
fd = libc.open(device, O_RDWR)
730
if fd < 0:
731
return False, ''
732
733
vcap = v4l2_capability()
734
if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0:
735
return False, ''
736
737
if vcap.driver != 'v4l2 loopback':
738
return False, ''
739
740
class v4l2_control(ctypes.Structure):
741
_fields_ = [
742
('id', ctypes.c_uint32),
743
('value', ctypes.c_int32)
744
]
745
746
# These are private v4l2 control IDs, see:
748
KEEP_FORMAT = 0x8000000
749
SUSTAIN_FRAMERATE = 0x8000001
750
VIDIOC_S_CTRL = 0xc008561c
751
752
control = v4l2_control()
753
control.id = KEEP_FORMAT
754
control.value = 1
755
libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
756
757
control.id = SUSTAIN_FRAMERATE
758
control.value = 1
759
libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
760
libc.close(fd)
761
762
return True, vcap.card
763
764
765
def findTestMediaDevices(log):
766
'''
767
Find the test media devices configured on this system, and return a dict
768
containing information about them. The dict will have keys for 'audio'
769
and 'video', each containing the name of the media device to use.
770
771
If audio and video devices could not be found, return None.
772
773
This method is only currently implemented for Linux.
774
'''
775
if not mozinfo.isLinux:
776
return None
777
778
info = {}
779
# Look for a v4l2loopback device.
780
name = None
781
device = None
782
for dev in sorted(glob.glob('/dev/video*')):
783
result, name_ = checkAndConfigureV4l2loopback(dev)
784
if result:
785
name = name_
786
device = dev
787
break
788
789
if not (name and device):
790
log.error('Couldn\'t find a v4l2loopback video device')
791
return None
792
793
# Feed it a frame of output so it has something to display
794
gst01 = spawn.find_executable("gst-launch-0.1")
795
gst010 = spawn.find_executable("gst-launch-0.10")
796
gst10 = spawn.find_executable("gst-launch-1.0")
797
if gst01:
798
gst = gst01
799
if gst010:
800
gst = gst010
801
else:
802
gst = gst10
803
subprocess.check_call([gst, 'videotestsrc',
804
'pattern=green', 'num-buffers=1', '!',
805
'v4l2sink', 'device=%s' % device])
806
info['video'] = name
807
808
if platform.linux_distribution()[0] == 'debian':
809
# Debian 10 doesn't seem to appreciate starting pactl here.
810
# Still WIP (bug 1565332)
811
pass
812
else:
813
# Use pactl to see if the PulseAudio module-null-sink module is loaded.
814
pactl = spawn.find_executable("pactl")
815
816
def null_sink_loaded():
817
o = subprocess.check_output(
818
[pactl, 'list', 'short', 'modules'])
819
return filter(lambda x: 'module-null-sink' in x, o.splitlines())
820
821
if not null_sink_loaded():
822
subprocess.check_call([
823
pactl,
824
'load-module',
825
'module-null-sink'
826
])
827
828
if not null_sink_loaded():
829
log.error('Couldn\'t load module-null-sink')
830
return None
831
832
# Hardcode the name since it's always the same.
833
info['audio'] = 'Monitor of Null Output'
834
return info
835
836
837
def create_zip(path):
838
"""
839
Takes a `path` on disk and creates a zipfile with its contents. Returns a
840
path to the location of the temporary zip file.
841
"""
842
with tempfile.NamedTemporaryFile() as f:
843
# `shutil.make_archive` writes to "{f.name}.zip", so we're really just
844
# using `NamedTemporaryFile` as a way to get a random path.
845
return shutil.make_archive(f.name, "zip", path)
846
847
848
def update_mozinfo():
849
"""walk up directories to find mozinfo.json update the info"""
850
# TODO: This should go in a more generic place, e.g. mozinfo
851
852
path = SCRIPT_DIR
853
dirs = set()
854
while path != os.path.expanduser('~'):
855
if path in dirs:
856
break
857
dirs.add(path)
858
path = os.path.split(path)[0]
859
860
mozinfo.find_and_update_from_json(*dirs)
861
862
863
class MochitestDesktop(object):
864
"""
865
Mochitest class for desktop firefox.
866
"""
867
oldcwd = os.getcwd()
868
869
# Path to the test script on the server
870
TEST_PATH = "tests"
871
CHROME_PATH = "redirect.html"
872
873
certdbNew = False
874
sslTunnel = None
875
DEFAULT_TIMEOUT = 60.0
876
mediaDevices = None
877
878
patternFiles = {}
879
880
# XXX use automation.py for test name to avoid breaking legacy
881
# TODO: replace this with 'runtests.py' or 'mochitest' or the like
882
test_name = 'automation.py'
883
884
def __init__(self, flavor, logger_options, staged_addons=None, quiet=False):
885
update_mozinfo()
886
self.flavor = flavor
887
self.staged_addons = staged_addons
888
self.server = None
889
self.wsserver = None
890
self.websocketProcessBridge = None
891
self.sslTunnel = None
892
self.manifest = None
893
self.tests_by_manifest = defaultdict(list)
894
self.prefs_by_manifest = defaultdict(set)
895
self.env_vars_by_manifest = defaultdict(set)
896
self._active_tests = None
897
self.currentTests = None
898
self._locations = None
899
900
self.marionette = None
901
self.start_script = None
902
self.mozLogs = None
903
self.start_script_kwargs = {}
904
self.extraPrefs = {}
905
self.extraEnv = {}
906
907
if logger_options.get('log'):
908
self.log = logger_options['log']
909
else:
910
commandline.log_formatters["tbpl"] = (
911
MochitestFormatter,
912
"Mochitest specific tbpl formatter")
913
self.log = commandline.setup_logging("mochitest", logger_options, {"tbpl": sys.stdout})
914
915
self.message_logger = MessageLogger(
916
logger=self.log, buffering=quiet, structured=True)
917
918
# Max time in seconds to wait for server startup before tests will fail -- if
919
# this seems big, it's mostly for debug machines where cold startup
920
# (particularly after a build) takes forever.
921
self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
922
923
# metro browser sub process id
924
self.browserProcessId = None
925
926
self.haveDumpedScreen = False
927
# Create variables to count the number of passes, fails, todos.
928
self.countpass = 0
929
self.countfail = 0
930
self.counttodo = 0
931
932
self.expectedError = {}
933
self.result = {}
934
935
self.start_script = os.path.join(here, 'start_desktop.js')
936
937
def environment(self, **kwargs):
938
kwargs['log'] = self.log
939
return test_environment(**kwargs)
940
941
def getFullPath(self, path):
942
" Get an absolute path relative to self.oldcwd."
943
return os.path.normpath(
944
os.path.join(
945
self.oldcwd,
946
os.path.expanduser(path)))
947
948
def getLogFilePath(self, logFile):
949
""" return the log file path relative to the device we are testing on, in most cases
950
it will be the full path on the local system
951
"""
952
return self.getFullPath(logFile)
953
954
@property
955
def locations(self):
956
if self._locations is not None:
957
return self._locations
958
locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt')
959
self._locations = ServerLocations(locations_file)
960
return self._locations
961
962
def buildURLOptions(self, options, env):
963
""" Add test control options from the command line to the url
964
965
URL parameters to test URL:
966
967
autorun -- kick off tests automatically
968
closeWhenDone -- closes the browser after the tests
969
hideResultsTable -- hides the table of individual test results
970
logFile -- logs test run to an absolute path
971
startAt -- name of test to start at
972
endAt -- name of test to end at
973
timeout -- per-test timeout in seconds
974
repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
975
"""
976
self.urlOpts = []
977
978
if not hasattr(options, 'logFile'):
979
options.logFile = ""
980
if not hasattr(options, 'fileLevel'):
981
options.fileLevel = 'INFO'
982
983
# allow relative paths for logFile
984
if options.logFile:
985
options.logFile = self.getLogFilePath(options.logFile)
986
987
if options.flavor in ('a11y', 'browser', 'chrome'):
988
self.makeTestConfig(options)
989
else:
990
if options.autorun:
991
self.urlOpts.append("autorun=1")
992
if options.timeout:
993
self.urlOpts.append("timeout=%d" % options.timeout)
994
if options.maxTimeouts:
995
self.urlOpts.append("maxTimeouts=%d" % options.maxTimeouts)
996
if not options.keep_open:
997
self.urlOpts.append("closeWhenDone=1")
998
if options.logFile:
999
self.urlOpts.append(
1000
"logFile=" +
1001
encodeURIComponent(
1002
options.logFile))
1003
self.urlOpts.append(
1004
"fileLevel=" +
1005
encodeURIComponent(
1006
options.fileLevel))
1007
if options.consoleLevel:
1008
self.urlOpts.append(
1009
"consoleLevel=" +
1010
encodeURIComponent(
1011
options.consoleLevel))
1012
if options.startAt:
1013
self.urlOpts.append("startAt=%s" % options.startAt)
1014
if options.endAt:
1015
self.urlOpts.append("endAt=%s" % options.endAt)
1016
if options.shuffle:
1017
self.urlOpts.append("shuffle=1")
1018
if "MOZ_HIDE_RESULTS_TABLE" in env and env[
1019
"MOZ_HIDE_RESULTS_TABLE"] == "1":
1020
self.urlOpts.append("hideResultsTable=1")
1021
if options.runUntilFailure:
1022
self.urlOpts.append("runUntilFailure=1")
1023
if options.repeat:
1024
self.urlOpts.append("repeat=%d" % options.repeat)
1025
if len(options.test_paths) == 1 and os.path.isfile(
1026
os.path.join(
1027
self.oldcwd,
1028
os.path.dirname(__file__),
1029
self.TEST_PATH,
1030
options.test_paths[0])):
1031
self.urlOpts.append("testname=%s" % "/".join(
1032
[self.TEST_PATH, options.test_paths[0]]))
1033
if options.manifestFile:
1034
self.urlOpts.append("manifestFile=%s" % options.manifestFile)
1035
if options.failureFile:
1036
self.urlOpts.append(
1037
"failureFile=%s" %
1038
self.getFullPath(
1039
options.failureFile))
1040
if options.runSlower:
1041
self.urlOpts.append("runSlower=true")
1042
if options.debugOnFailure:
1043
self.urlOpts.append("debugOnFailure=true")
1044
if options.dumpOutputDirectory:
1045
self.urlOpts.append(
1046
"dumpOutputDirectory=%s" %
1047
encodeURIComponent(
1048
options.dumpOutputDirectory))
1049
if options.dumpAboutMemoryAfterTest:
1050
self.urlOpts.append("dumpAboutMemoryAfterTest=true")
1051
if options.dumpDMDAfterTest:
1052
self.urlOpts.append("dumpDMDAfterTest=true")
1053
if options.debugger:
1054
self.urlOpts.append("interactiveDebugger=true")
1055
if options.jscov_dir_prefix:
1056
self.urlOpts.append("jscovDirPrefix=%s" % options.jscov_dir_prefix)
1057
if options.cleanupCrashes:
1058
self.urlOpts.append("cleanupCrashes=true")
1059
1060
def normflavor(self, flavor):
1061
"""
1062
In some places the string 'browser-chrome' is expected instead of
1063
'browser' and 'mochitest' instead of 'plain'. Normalize the flavor
1064
strings for those instances.
1065
"""
1066
# TODO Use consistent flavor strings everywhere and remove this
1067
if flavor == 'browser':
1068
return 'browser-chrome'
1069
elif flavor == 'plain':
1070
return 'mochitest'
1071
return flavor
1072
1073
# This check can be removed when bug 983867 is fixed.
1074
def isTest(self, options, filename):
1075
allow_js_css = False
1076
if options.flavor == 'browser':
1077
allow_js_css = True
1078
testPattern = re.compile(r"browser_.+\.js")
1079
elif options.flavor in ('a11y', 'chrome'):
1080
testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
1081
else:
1082
testPattern = re.compile(r"test_")
1083
1084
if not allow_js_css and (".js" in filename or ".css" in filename):
1085
return False
1086
1087
pathPieces = filename.split("/")
1088
1089
return (testPattern.match(pathPieces[-1]) and
1090
not re.search(r'\^headers\^$', filename))
1091
1092
def setTestRoot(self, options):
1093
if options.flavor != 'plain':
1094
self.testRoot = options.flavor
1095
else:
1096
self.testRoot = self.TEST_PATH
1097
self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot)
1098
1099
def buildTestURL(self, options, scheme='http'):
1100
if scheme == 'https':
1101
testHost = "https://example.com:443"
1102
else:
1103
testHost = "http://mochi.test:8888"
1104
testURL = "/".join([testHost, self.TEST_PATH])
1105
1106
if len(options.test_paths) == 1:
1107
if os.path.isfile(
1108
os.path.join(
1109
self.oldcwd,
1110
os.path.dirname(__file__),
1111
self.TEST_PATH,
1112
options.test_paths[0])):
1113
testURL = "/".join([testURL, os.path.dirname(options.test_paths[0])])
1114
else:
1115
testURL = "/".join([testURL, options.test_paths[0]])
1116
1117
if options.flavor in ('a11y', 'chrome'):
1118
testURL = "/".join([testHost, self.CHROME_PATH])
1119
elif options.flavor == 'browser':
1120
testURL = "about:blank"
1121
return testURL
1122
1123
def getTestsByScheme(self, options, testsToFilter=None, disabled=True):
1124
""" Build the url path to the specific test harness and test file or directory
1125
Build a manifest of tests to run and write out a json file for the harness to read
1126
testsToFilter option is used to filter/keep the tests provided in the list
1127
1128
disabled -- This allows to add all disabled tests on the build side
1129
and then on the run side to only run the enabled ones
1130
"""
1131
1132
tests = self.getActiveTests(options, disabled)
1133
paths = []
1134
for test in tests:
1135
if testsToFilter and (test['path'] not in testsToFilter):
1136
continue
1137
paths.append(test)
1138
1139
# Generate test by schemes
1140
for (scheme, grouped_tests) in self.groupTestsByScheme(paths).items():
1141
# Bug 883865 - add this functionality into manifestparser
1142
with open(os.path.join(SCRIPT_DIR, options.testRunManifestFile), 'w') as manifestFile:
1143
manifestFile.write(json.dumps({'tests': grouped_tests}))
1144
options.manifestFile = options.testRunManifestFile
1145
yield (scheme, grouped_tests)
1146
1147
def startWebSocketServer(self, options, debuggerInfo):
1148
""" Launch the websocket server """
1149
self.wsserver = WebSocketServer(
1150
options,
1151
SCRIPT_DIR,
1152
self.log,
1153
debuggerInfo)
1154
self.wsserver.start()
1155
1156
def startWebServer(self, options):
1157
"""Create the webserver and start it up"""
1158
1159
self.server = MochitestServer(options, self.log)
1160
self.server.start()
1161
1162
if options.pidFile != "":
1163
with open(options.pidFile + ".xpcshell.pid", 'w') as f:
1164
f.write("%s" % self.server._process.pid)
1165
1166
def startWebsocketProcessBridge(self, options):
1167
"""Create a websocket server that can launch various processes that
1168
JS needs (eg; ICE server for webrtc testing)
1169
"""
1170
1171
command = [sys.executable,
1172
os.path.join("websocketprocessbridge",
1173
"websocketprocessbridge.py"),
1174
"--port",
1175
options.websocket_process_bridge_port]
1176
self.websocketProcessBridge = mozprocess.ProcessHandler(command,
1177
cwd=SCRIPT_DIR)
1178
self.websocketProcessBridge.run()
1179
self.log.info("runtests.py | websocket/process bridge pid: %d"
1180
% self.websocketProcessBridge.pid)
1181
1182
# ensure the server is up, wait for at most ten seconds
1183
for i in range(1, 100):
1184
if self.websocketProcessBridge.proc.poll() is not None:
1185
self.log.error("runtests.py | websocket/process bridge failed "
1186
"to launch. Are all the dependencies installed?")
1187
return
1188
1189
try:
1190
sock = socket.create_connection(("127.0.0.1", 8191))
1191
sock.close()
1192
break
1193
except Exception:
1194
time.sleep(0.1)
1195
else:
1196
self.log.error("runtests.py | Timed out while waiting for "
1197
"websocket/process bridge startup.")
1198
1199
def startServers(self, options, debuggerInfo, public=None):
1200
# start servers and set ports
1201
# TODO: pass these values, don't set on `self`
1202
self.webServer = options.webServer
1203
self.httpPort = options.httpPort
1204
self.sslPort = options.sslPort
1205
self.webSocketPort = options.webSocketPort
1206
1207
# httpd-path is specified by standard makefile targets and may be specified
1208
# on the command line to select a particular version of httpd.js. If not
1209
# specified, try to select the one from hostutils.zip, as required in
1210
# bug 882932.
1211
if not options.httpdPath:
1212
options.httpdPath = os.path.join(options.utilityPath, "components")
1213
1214
self.startWebServer(options)
1215
self.startWebSocketServer(options, debuggerInfo)
1216
1217
if options.subsuite in ["media"]:
1218
self.startWebsocketProcessBridge(options)
1219
1220
# start SSL pipe
1221
self.sslTunnel = SSLTunnel(
1222
options,
1223
logger=self.log)
1224
self.sslTunnel.buildConfig(self.locations, public=public)
1225
self.sslTunnel.start()
1226
1227
# If we're lucky, the server has fully started by now, and all paths are
1228
# ready, etc. However, xpcshell cold start times suck, at least for debug
1229
# builds. We'll try to connect to the server for awhile, and if we fail,
1230
# we'll try to kill the server and exit with an error.
1231
if self.server is not None:
1232
self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
1233
1234
def stopServers(self):
1235
"""Servers are no longer needed, and perhaps more importantly, anything they
1236
might spew to console might confuse things."""
1237
if self.server is not None:
1238
try:
1239
self.log.info('Stopping web server')
1240
self.server.stop()
1241
except Exception:
1242
self.log.critical('Exception when stopping web server')
1243
1244
if self.wsserver is not None:
1245
try:
1246
self.log.info('Stopping web socket server')
1247
self.wsserver.stop()
1248
except Exception:
1249
self.log.critical('Exception when stopping web socket server')
1250
1251
if self.sslTunnel is not None:
1252
try:
1253
self.log.info('Stopping ssltunnel')
1254
self.sslTunnel.stop()
1255
except Exception:
1256
self.log.critical('Exception stopping ssltunnel')
1257
1258
if self.websocketProcessBridge is not None:
1259
try:
1260
self.websocketProcessBridge.kill()
1261
self.websocketProcessBridge.wait()
1262
self.log.info('Stopping websocket/process bridge')
1263
except Exception:
1264
self.log.critical('Exception stopping websocket/process bridge')
1265
1266
def copyExtraFilesToProfile(self, options):
1267
"Copy extra files or dirs specified on the command line to the testing profile."
1268
for f in options.extraProfileFiles:
1269
abspath = self.getFullPath(f)
1270
if os.path.isfile(abspath):
1271
shutil.copy2(abspath, options.profilePath)
1272
elif os.path.isdir(abspath):
1273
dest = os.path.join(
1274
options.profilePath,
1275
os.path.basename(abspath))
1276
shutil.copytree(abspath, dest)
1277
else:
1278
self.log.warning(
1279
"runtests.py | Failed to copy %s to profile" %
1280
abspath)
1281
1282
def getChromeTestDir(self, options):
1283
dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
1284
if mozinfo.isWin:
1285
dir = "file:///" + dir.replace("\\", "/")
1286
return dir
1287
1288
def writeChromeManifest(self, options):
1289
manifest = os.path.join(options.profilePath, "tests.manifest")
1290
with open(manifest, "w") as manifestFile:
1291
# Register chrome directory.
1292
chrometestDir = self.getChromeTestDir(options)
1293
manifestFile.write(
1294
"content mochitests %s contentaccessible=yes\n" %
1295
chrometestDir)
1296
manifestFile.write(
1297
"content mochitests-any %s contentaccessible=yes remoteenabled=yes\n" %
1298
chrometestDir)
1299
manifestFile.write(
1300
"content mochitests-content %s contentaccessible=yes remoterequired=yes\n" %
1301
chrometestDir)
1302
1303
if options.testingModulesDir is not None:
1304
manifestFile.write("resource testing-common file:///%s\n" %
1305
options.testingModulesDir)
1306
if options.store_chrome_manifest:
1307
shutil.copyfile(manifest, options.store_chrome_manifest)
1308
return manifest
1309
1310
def addChromeToProfile(self, options):
1311
"Adds MochiKit chrome tests to the profile."
1312
1313
# Create (empty) chrome directory.
1314
chromedir = os.path.join(options.profilePath, "chrome")
1315
os.mkdir(chromedir)
1316
1317
# Write userChrome.css.
1318
chrome = """
1319
/* set default namespace to XUL */
1321
toolbar,
1322
toolbarpalette {
1323
background-color: rgb(235, 235, 235) !important;
1324
}
1325
toolbar#nav-bar {
1326
background-image: none !important;
1327
}
1328
"""
1329
with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
1330
chromeFile.write(chrome)
1331
1332
manifest = self.writeChromeManifest(options)
1333
1334
return manifest
1335
1336
def getExtensionsToInstall(self, options):
1337
"Return a list of extensions to install in the profile"
1338
extensions = []
1339
appDir = options.app[
1340
:options.app.rfind(
1341
os.sep)] if options.app else options.utilityPath
1342
1343
extensionDirs = [
1344
# Extensions distributed with the test harness.
1345
os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
1346
]
1347
if appDir:
1348
# Extensions distributed with the application.
1349
extensionDirs.append(
1350
os.path.join(
1351
appDir,
1352
"distribution",
1353
"extensions"))
1354
1355
for extensionDir in extensionDirs:
1356
if os.path.isdir(extensionDir):
1357
for dirEntry in os.listdir(extensionDir):
1358
if dirEntry not in options.extensionsToExclude:
1359
path = os.path.join(extensionDir, dirEntry)
1360
if os.path.isdir(path) or (
1361
os.path.isfile(path) and path.endswith(".xpi")):
1362
extensions.append(path)
1363
extensions.extend(options.extensionsToInstall)
1364
return extensions
1365
1366
def logPreamble(self, tests):
1367
"""Logs a suite_start message and test_start/test_end at the beginning of a run.
1368
"""
1369
self.log.suite_start(self.tests_by_manifest, name='mochitest-{}'.format(self.flavor))
1370
for test in tests:
1371
if 'disabled' in test:
1372
self.log.test_start(test['path'])
1373
self.log.test_end(
1374
test['path'],
1375
'SKIP',
1376
message=test['disabled'])
1377
1378
def loadFailurePatternFile(self, pat_file):
1379
if pat_file in self.patternFiles:
1380
return self.patternFiles[pat_file]
1381
if not os.path.isfile(pat_file):
1382
self.log.warning("runtests.py | Cannot find failure pattern file " +
1383
pat_file)
1384
return None
1385
1386
# Using ":error" to ensure it shows up in the failure summary.
1387
self.log.warning(
1388
"[runtests.py:error] Using {} to filter failures. If there "
1389
"is any number mismatch below, you could have fixed "
1390
"something documented in that file. Please reduce the "
1391
"failure count appropriately.".format(pat_file))
1392
patternRE = re.compile(r"""
1393
^\s*\*\s* # list bullet
1394
(test_\S+|\.{3}) # test name
1395
(?:\s*(`.+?`|asserts))? # failure pattern
1396
(?::.+)? # optional description
1397
\s*\[(\d+|\*)\] # expected count
1398
\s*$
1399
""", re.X)
1400
patterns = {}
1401
with open(pat_file) as f:
1402
last_name = None
1403
for line in f:
1404
match = patternRE.match(line)
1405
if not match:
1406
continue
1407
name = match.group(1)
1408
name = last_name if name == "..." else name
1409
last_name = name
1410
pat = match.group(2)
1411
if pat is not None:
1412
pat = "ASSERTION" if pat == "asserts" else pat[1:-1]
1413
count = match.group(3)
1414
count = None if count == "*" else int(count)
1415
if name not in patterns:
1416
patterns[name] = []
1417
patterns[name].append((pat, count))
1418
self.patternFiles[pat_file] = patterns
1419
return patterns
1420
1421
def getFailurePatterns(self, pat_file, test_name):
1422
patterns = self.loadFailurePatternFile(pat_file)
1423
if patterns:
1424
return patterns.get(test_name, None)
1425
1426
def getActiveTests(self, options, disabled=True):
1427
"""
1428
This method is used to parse the manifest and return active filtered tests.
1429
"""
1430
if self._active_tests:
1431
return self._active_tests
1432
1433
tests = []
1434
manifest = self.getTestManifest(options)
1435
if manifest:
1436
if options.extra_mozinfo_json:
1437
mozinfo.update(options.extra_mozinfo_json)
1438
1439
info = mozinfo.info
1440
1441
filters = [
1442
subsuite(options.subsuite),
1443
]
1444
1445
if options.test_tags:
1446
filters.append(tags(options.test_tags))
1447
1448
if options.test_paths:
1449
options.test_paths = self.normalize_paths(options.test_paths)
1450
filters.append(pathprefix(options.test_paths))
1451
1452
# Add chunking filters if specified
1453
if options.totalChunks:
1454
if options.chunkByDir:
1455
filters.append(chunk_by_dir(options.thisChunk,
1456
options.totalChunks,
1457
options.chunkByDir))
1458
elif options.chunkByRuntime:
1459
if mozinfo.info['os'] == 'android':
1460
platkey = 'android'
1461
elif mozinfo.isWin:
1462
platkey = 'windows'
1463
else:
1464
platkey = 'unix'
1465
1466
runtime_file = os.path.join(SCRIPT_DIR, 'runtimes',
1467
'manifest-runtimes-{}.json'.format(platkey))
1468
if not os.path.exists(runtime_file):
1469
self.log.error("runtime file %s not found!" % runtime_file)
1470
sys.exit(1)
1471
1472
with open(runtime_file, 'r') as f:
1473
runtimes = json.loads(f.read())
1474
filters.append(
1475
chunk_by_runtime(options.thisChunk,
1476
options.totalChunks,
1477
runtimes))
1478
else:
1479
filters.append(chunk_by_slice(options.thisChunk,
1480
options.totalChunks))
1481
1482
tests = manifest.active_tests(
1483
exists=False, disabled=disabled, filters=filters, **info)
1484
1485
if len(tests) == 0:
1486
self.log.error(NO_TESTS_FOUND.format(options.flavor, manifest.fmt_filters()))
1487
1488
paths = []
1489
for test in tests:
1490
if len(tests) == 1 and 'disabled' in test:
1491
del test['disabled']
1492
1493
pathAbs = os.path.abspath(test['path'])
1494
assert pathAbs.startswith(self.testRootAbs)
1495
tp = pathAbs[len(self.testRootAbs):].replace('\\', '/').strip('/')
1496
1497
if not self.isTest(options, tp):
1498
self.log.warning(
1499
'Warning: %s from manifest %s is not a valid test' %
1500
(test['name'], test['manifest']))
1501
continue
1502
1503
manifest_key = test['manifest_relpath']
1504
# Ignore ancestor_manifests that live at the root (e.g, don't have a
1505
# path separator).
1506
if 'ancestor_manifest' in test and '/' in normsep(test['ancestor_manifest']):
1507
manifest_key = '{}:{}'.format(test['ancestor_manifest'], manifest_key)
1508
1509
self.tests_by_manifest[manifest_key].append(tp)
1510
self.prefs_by_manifest[manifest_key].add(test.get('prefs'))
1511
self.env_vars_by_manifest[manifest_key].add(test.get('environment'))
1512
1513
for key in ['prefs', 'environment']:
1514
if key in test and not options.runByManifest and 'disabled' not in test:
1515
self.log.error("parsing {}: runByManifest mode must be enabled to "
1516
"set the `{}` key".format(test['manifest_relpath'], key))
1517
sys.exit(1)
1518
1519
testob = {'path': tp, 'manifest': manifest_key}
1520
if 'disabled' in test:
1521
testob['disabled'] = test['disabled']
1522
if 'expected' in test:
1523
testob['expected'] = test['expected']
1524
if 'uses-unsafe-cpows' in test:
1525
testob['uses-unsafe-cpows'] = test['uses-unsafe-cpows'] == 'true'
1526
if 'scheme' in test:
1527
testob['scheme'] = test['scheme']
1528
if options.failure_pattern_file:
1529
pat_file = os.path.join(os.path.dirname(test['manifest']),
1530
options.failure_pattern_file)
1531
patterns = self.getFailurePatterns(pat_file, test['name'])
1532
if patterns:
1533
testob['expected'] = patterns
1534
paths.append(testob)
1535
1536
# The 'prefs' key needs to be set in the DEFAULT section, unfortunately
1537
# we can't tell what comes from DEFAULT or not. So to validate this, we
1538
# stash all prefs from tests in the same manifest into a set. If the
1539
# length of the set > 1, then we know 'prefs' didn't come from DEFAULT.
1540
pref_not_default = [m for m, p in self.prefs_by_manifest.iteritems() if len(p) > 1]
1541
if pref_not_default:
1542
self.log.error("The 'prefs' key must be set in the DEFAULT section of a "
1543
"manifest. Fix the following manifests: {}".format(
1544
'\n'.join(pref_not_default)))
1545
sys.exit(1)
1546
# The 'environment' key needs to be set in the DEFAULT section too.
1547
env_not_default = [m for m, p in self.env_vars_by_manifest.iteritems() if len(p) > 1]
1548
if env_not_default:
1549
self.log.error("The 'environment' key must be set in the DEFAULT section of a "
1550
"manifest. Fix the following manifests: {}".format(
1551
'\n'.join(env_not_default)))
1552
sys.exit(1)
1553
1554
def path_sort(ob1, ob2):
1555
path1 = ob1['path'].split('/')
1556
path2 = ob2['path'].split('/')
1557
return cmp(path1, path2)
1558
1559
paths.sort(path_sort)
1560
if options.dump_tests:
1561
options.dump_tests = os.path.expanduser(options.dump_tests)
1562
assert os.path.exists(os.path.dirname(options.dump_tests))
1563
with open(options.dump_tests, 'w') as dumpFile:
1564
dumpFile.write(json.dumps({'active_tests': paths}))
1565
1566
self.log.info("Dumping active_tests to %s file." % options.dump_tests)
1567
sys.exit()
1568
1569
# Upload a list of test manifests that were executed in this run.
1570
if 'MOZ_UPLOAD_DIR' in os.environ:
1571
artifact = os.path.join(os.environ['MOZ_UPLOAD_DIR'], 'manifests.list')
1572
with open(artifact, 'a') as fh:
1573
fh.write('\n'.join(sorted(self.tests_by_manifest.keys())))
1574
1575
self._active_tests = paths
1576
return self._active_tests
1577
1578
def getTestManifest(self, options):
1579
if isinstance(options.manifestFile, TestManifest):
1580
manifest = options.manifestFile
1581
elif options.manifestFile and os.path.isfile(options.manifestFile):
1582
manifestFileAbs = os.path.abspath(options.manifestFile)
1583
assert manifestFileAbs.startswith(SCRIPT_DIR)
1584
manifest = TestManifest([options.manifestFile], strict=False)
1585
elif (options.manifestFile and
1586
os.path.isfile(os.path.join(SCRIPT_DIR, options.manifestFile))):
1587
manifestFileAbs = os.path.abspath(
1588
os.path.join(
1589
SCRIPT_DIR,
1590
options.manifestFile))
1591
assert manifestFileAbs.startswith(SCRIPT_DIR)
1592
manifest = TestManifest([manifestFileAbs], strict=False)
1593
else:
1594
masterName = self.normflavor(options.flavor) + '.ini'
1595
masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
1596
1597
if os.path.exists(masterPath):
1598
manifest = TestManifest([masterPath], strict=False)
1599
else:
1600
manifest = None
1601
self.log.warning(
1602
'TestManifest masterPath %s does not exist' %
1603
masterPath)
1604
1605
return manifest
1606
1607
def makeTestConfig(self, options):
1608
"Creates a test configuration file for customizing test execution."
1609
options.logFile = options.logFile.replace("\\", "\\\\")
1610
1611
if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ[
1612
"MOZ_HIDE_RESULTS_TABLE"] == "1":
1613
options.hideResultsTable = True
1614
1615
# strip certain unnecessary items to avoid serialization errors in json.dumps()
1616
d = dict((k, v) for k, v in options.__dict__.items() if (v is None) or
1617
isinstance(v, (basestring, numbers.Number)))
1618
d['testRoot'] = self.testRoot
1619
if options.jscov_dir_prefix:
1620
d['jscovDirPrefix'] = options.jscov_dir_prefix
1621
if not options.keep_open:
1622
d['closeWhenDone'] = '1'
1623
content = json.dumps(d)
1624
1625
with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
1626
config.write(content)
1627
1628
def buildBrowserEnv(self, options, debugger=False, env=None):
1629
"""build the environment variables for the specific test and operating system"""
1630
if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
1631
useLSan = True
1632
else:
1633
useLSan = False
1634
1635
browserEnv = self.environment(
1636
xrePath=options.xrePath,
1637
env=env,
1638
debugger=debugger,
1639
useLSan=useLSan)
1640
1641
if hasattr(options, "topsrcdir"):
1642
browserEnv["MOZ_DEVELOPER_REPO_DIR"] = options.topsrcdir
1643
if hasattr(options, "topobjdir"):
1644
browserEnv["MOZ_DEVELOPER_OBJ_DIR"] = options.topobjdir
1645
1646
if options.headless:
1647
browserEnv["MOZ_HEADLESS"] = '1'
1648
1649
if options.dmd:
1650
browserEnv["DMD"] = os.environ.get('DMD', '1')
1651
1652
# bug 1443327: do not set MOZ_CRASHREPORTER_SHUTDOWN during browser-chrome
1653
# tests, since some browser-chrome tests test content process crashes;
1654
# also exclude non-e10s since at least one non-e10s mochitest is problematic
1655
if (options.flavor == 'browser' or not options.e10s) and \
1656
'MOZ_CRASHREPORTER_SHUTDOWN' in browserEnv:
1657
del browserEnv["MOZ_CRASHREPORTER_SHUTDOWN"]
1658
1659
try:
1660
browserEnv.update(
1661
dict(
1662
parse_key_value(
1663
self.extraEnv,
1664
context='environment variable in manifest')))
1665
except KeyValueParseError as e:
1666
self.log.error(str(e))
1667
return None
1668
1669
# These variables are necessary for correct application startup; change
1670
# via the commandline at your own risk.
1671
browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
1672
1673
# interpolate environment passed with options
1674
try:
1675
browserEnv.update(
1676
dict(
1677
parse_key_value(
1678
options.environment,
1679
context='--setenv')))
1680
except KeyValueParseError as e:
1681
self.log.error(str(e))
1682
return None
1683
1684
browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
1685
1686
try:
1687
gmp_path = self.getGMPPluginPath(options)
1688
if gmp_path is not None:
1689
browserEnv["MOZ_GMP_PATH"] = gmp_path
1690
except EnvironmentError:
1691
self.log.error('Could not find path to gmp-fake plugin!')
1692
return None
1693
1694
if options.fatalAssertions:
1695
browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
1696
1697
# Produce a mozlog, if setup (see MOZ_LOG global at the top of
1698
# this script).
1699
self.mozLogs = MOZ_LOG and "MOZ_UPLOAD_DIR" in os.environ
1700
if self.mozLogs:
1701
browserEnv["MOZ_LOG"] = MOZ_LOG
1702
1703
# For e10s, our tests default to suppressing the "unsafe CPOW usage"
1704
# warnings that can plague test logs.
1705
if not options.enableCPOWWarnings:
1706
browserEnv["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1"
1707
1708
if options.enable_webrender:
1709
browserEnv["MOZ_WEBRENDER"] = "1"
1710
browserEnv["MOZ_ACCELERATED"] = "1"
1711
else:
1712
browserEnv["MOZ_WEBRENDER"] = "0"
1713
1714
return browserEnv
1715
1716
def killNamedProc(self, pname, orphans=True):
1717
""" Kill processes matching the given command name """
1718
self.log.info("Checking for %s processes..." % pname)
1719
1720
if HAVE_PSUTIL:
1721
for proc in psutil.process_iter():
1722
try:
1723
if proc.name() == pname:
1724
procd = proc.as_dict(attrs=['pid', 'ppid', 'name', 'username'])
1725
if proc.ppid() == 1 or not orphans:
1726
self.log.info("killing %s" % procd)
1727
killPid(proc.pid, self.log)
1728
else:
1729
self.log.info("NOT killing %s (not an orphan?)" % procd)
1730
except Exception as e:
1731
self.log.info("Warning: Unable to kill process %s: %s" % (pname, str(e)))
1732
# may not be able to access process info for all processes
1733
continue
1734
else:
1735
def _psInfo(line):
1736
if pname in line:
1737
self.log.info(line)
1738
1739
process = mozprocess.ProcessHandler(['ps', '-f'],
1740
processOutputLine=_psInfo)
1741
process.run()
1742
process.wait()
1743
1744
def _psKill(line):
1745
parts = line.split()
1746
if len(parts) == 3 and parts[0].isdigit():
1747
pid = int(parts[0])
1748
ppid = int(parts[1])
1749
if parts[2] == pname:
1750
if ppid == 1 or not orphans:
1751
self.log.info("killing %s (pid %d)" % (pname, pid))
1752
killPid(pid, self.log)
1753
else:
1754
self.log.info("NOT killing %s (pid %d) (not an orphan?)" %
1755
(pname, pid))
1756
process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'],
1757
processOutputLine=_psKill)
1758
process.run()
1759
process.wait()
1760
1761
def execute_start_script(self):
1762
if not self.start_script or not self.marionette:
1763
return
1764
1765
if os.path.isfile(self.start_script):
1766
with open(self.start_script, 'r') as fh:
1767
script = fh.read()
1768
else:
1769
script = self.start_script
1770
1771
with self.marionette.using_context('chrome'):
1772
return self.marionette.execute_script(script, script_args=(self.start_script_kwargs, ))
1773
1774
def fillCertificateDB(self, options):
1775
# TODO: move -> mozprofile:
1777
1778
pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
1779
with open(pwfilePath, "w") as pwfile:
1780
pwfile.write("\n")
1781
1782
# Pre-create the certification database for the profile
1783
env = self.environment(xrePath=options.xrePath)
1784
env["LD_LIBRARY_PATH"] = options.xrePath
1785
bin_suffix = mozinfo.info.get('bin_suffix', '')
1786
certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
1787
pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
1788
toolsEnv = env
1789
if mozinfo.info["asan"]:
1790
# Disable leak checking when running these tools
1791
toolsEnv["ASAN_OPTIONS"] = "detect_leaks=0"
1792
if mozinfo.info["tsan"]:
1793
# Disable race checking when running these tools
1794
toolsEnv["TSAN_OPTIONS"] = "report_bugs=0"
1795
1796
if self.certdbNew:
1797
# android uses the new DB formats exclusively
1798
certdbPath = "sql:" + options.profilePath
1799
else:
1800
# desktop seems to use the old
1801
certdbPath = options.profilePath
1802
1803
status = call(
1804
[certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=toolsEnv)
1805
if status:
1806
return status
1807
1808
# Walk the cert directory and add custom CAs and client certs
1809
files = os.listdir(options.certPath)
1810
for item in files:
1811
root, ext = os.path.splitext(item)
1812
if ext == ".ca":
1813
trustBits = "CT,,"
1814
if root.endswith("-object"):
1815
trustBits = "CT,,CT"
1816
call([certutil,
1817
"-A",
1818
"-i",
1819
os.path.join(options.certPath,
1820
item),
1821
"-d",
1822
certdbPath,
1823
"-f",
1824
pwfilePath,
1825
"-n",
1826
root,
1827
"-t",
1828
trustBits],
1829
env=toolsEnv)
1830
elif ext == ".client":
1831
call([pk12util, "-i", os.path.join(options.certPath, item),
1832
"-w", pwfilePath, "-d", certdbPath],
1833
env=toolsEnv)
1834
1835
os.unlink(pwfilePath)
1836
return 0
1837
1838
def proxy(self, options):
1839
# proxy
1840
# use SSL port for legacy compatibility; see
1844
# 'ws': str(self.webSocketPort)
1845
return {
1846
'remote': options.webServer,
1847
'http': options.httpPort,
1848
'https': options.sslPort,
1849
'ws': options.sslPort,
1850
}
1851
1852
def merge_base_profiles(self, options, category):
1853
"""Merge extra profile data from testing/profiles."""
1854
profile_data_dir = os.path.join(SCRIPT_DIR, 'profile_data')
1855
1856
# If possible, read profile data from topsrcdir. This prevents us from
1857
# requiring a re-build to pick up newly added extensions in the
1858
# <profile>/extensions directory.
1859
if build_obj:
1860
path = os.path.join(build_obj.topsrcdir, 'testing', 'profiles')
1861
if os.path.isdir(path):
1862
profile_data_dir = path
1863
1864
with open(os.path.join(profile_data_dir, 'profiles.json'), 'r') as fh:
1865
base_profiles = json.load(fh)[category]
1866
1867
# values to use when interpolating preferences
1868
interpolation = {
1869
"server": "%s:%s" % (options.webServer, options.httpPort),
1870
}
1871
1872
for profile in base_profiles:
1873
path = os.path.join(profile_data_dir, profile)
1874
self.profile.merge(path, interpolation=interpolation)
1875
1876
def buildProfile(self, options):
1877
""" create the profile and add optional chrome bits and files if requested """
1878
# get extensions to install
1879
extensions = self.getExtensionsToInstall(options)
1880
1881
# Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work
1882
tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR))
1883
sandbox_whitelist_paths = [tests_dir] + options.sandboxReadWhitelist
1884
if (platform.system() == "Linux" or
1885
platform.system() in ("Windows", "Microsoft")):
1886
# Trailing slashes are needed to indicate directories on Linux and Windows
1887
sandbox_whitelist_paths = map(lambda p: os.path.join(p, ""),
1888
sandbox_whitelist_paths)
1889
1890
# Create the profile
1891
self.profile = Profile(profile=options.profilePath,
1892
addons=extensions,
1893
locations=self.locations,
1894
proxy=self.proxy(options),
1895
whitelistpaths=sandbox_whitelist_paths,
1896
)
1897
1898
# Fix options.profilePath for legacy consumers.
1899
options.profilePath = self.profile.profile
1900
1901
manifest = self.addChromeToProfile(options)
1902
self.copyExtraFilesToProfile(options)
1903
1904
# create certificate database for the profile
1905
# TODO: this should really be upstreamed somewhere, maybe mozprofile
1906
certificateStatus = self.fillCertificateDB(options)
1907
if certificateStatus:
1908
self.log.error(
1909
"TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
1910
return None
1911
1912
# Set preferences in the following order (latter overrides former):
1913
# 1) Preferences from base profile (e.g from testing/profiles)
1914
# 2) Prefs hardcoded in this function
1915
# 3) Prefs from --setpref
1916
1917
# Prefs from base profiles
1918
self.merge_base_profiles(options, 'mochitest')
1919
1920
# Hardcoded prefs (TODO move these into a base profile)
1921
prefs = {
1922
"browser.tabs.remote.autostart": options.e10s,
1923
# Enable tracing output for detailed failures in case of
1924
# failing connection attempts, and hangs (bug 1397201)
1925
"marionette.log.level": "Trace",
1926
}
1927
1928
# Ideally we should set this in a manifest, but a11y tests do not run by manifest.
1929
if options.flavor == 'a11y':
1930
prefs["plugin.load_flash_only"] = False
1931
1932
if options.flavor == 'browser' and options.timeout:
1933
prefs["testing.browserTestHarness.timeout"] = options.timeout
1934
1935
# browser-chrome tests use a fairly short default timeout of 45 seconds;
1936
# this is sometimes too short on asan and debug, where we expect reduced
1937
# performance.
1938
if (mozinfo.info["asan"] or mozinfo.info["debug"]) and \
1939
options.flavor == 'browser' and options.timeout is None:
1940
self.log.info("Increasing default timeout to 90 seconds")
1941
prefs["testing.browserTestHarness.timeout"] = 90
1942
1943
if (mozinfo.info["os"] == "win" and
1944
mozinfo.info["processor"] == "aarch64"):
1945
extended_timeout = self.DEFAULT_TIMEOUT * 4
1946
self.log.info("Increasing default timeout to {} seconds".format(
1947
extended_timeout
1948
))
1949
prefs["testing.browserTestHarness.timeout"] = extended_timeout
1950
1951
if getattr(self, 'testRootAbs', None):
1952
prefs['mochitest.testRoot'] = self.testRootAbs
1953
1954
# See if we should use fake media devices.
1955
if options.useTestMediaDevices:
1956
prefs['media.audio_loopback_dev'] = self.mediaDevices['audio']
1957
prefs['media.video_loopback_dev'] = self.mediaDevices['video']
1958
prefs['media.cubeb.output_device'] = "Null Output"
1959
prefs['media.volume_scale'] = "1.0"
1960
1961
# Disable web replay rewinding by default if recordings are being saved.
1962
if options.recordingPath:
1963
prefs["devtools.recordreplay.enableRewinding"] = False
1964
1965
self.profile.set_preferences(prefs)
1966
1967
# Extra prefs from --setpref
1968
self.profile.set_preferences(self.extraPrefs)
1969
return manifest
1970
1971
def getGMPPluginPath(self, options):
1972
if options.gmp_path:
1973
return options.gmp_path
1974
1975
gmp_parentdirs = [
1976
# For local builds, GMP plugins will be under dist/bin.
1977
options.xrePath,
1978
# For packaged builds, GMP plugins will get copied under
1979
# $profile/plugins.
1980
os.path.join(self.profile.profile, 'plugins'),
1981
]
1982
1983
gmp_subdirs = [
1984
os.path.join('gmp-fake', '1.0'),
1985
os.path.join('gmp-fakeopenh264', '1.0'),
1986
os.path.join('gmp-clearkey', '0.1'),
1987
]
1988
1989
gmp_paths = [os.path.join(parent, sub)
1990
for parent in gmp_parentdirs
1991
for sub in gmp_subdirs
1992
if os.path.isdir(os.path.join(parent, sub))]
1993
1994
if not gmp_paths:
1995
# This is fatal for desktop environments.
1996
raise EnvironmentError('Could not find test gmp plugins')
1997
1998
return os.pathsep.join(gmp_paths)
1999
2000
def cleanup(self, options, final=False):
2001
""" remove temporary files and profile """
2002
if hasattr(self, 'manifest') and self.manifest is not None:
2003
if os.path.exists(self.manifest):
2004
os.remove(self.manifest)
2005
if hasattr(self, 'profile'):
2006
del self.profile
2007
if options.pidFile != "" and os.path.exists(options.pidFile):
2008
try:
2009
os.remove(options.pidFile)
2010
if os.path.exists(options.pidFile + ".xpcshell.pid"):
2011
os.remove(options.pidFile + ".xpcshell.pid")
2012
except Exception:
2013
self.log.warning(
2014
"cleaning up pidfile '%s' was unsuccessful from the test harness" %
2015
options.pidFile)
2016
options.manifestFile = None
2017
2018
def dumpScreen(self, utilityPath):
2019
if self.haveDumpedScreen:
2020
self.log.info(
2021
"Not taking screenshot here: see the one that was previously logged")
2022
return
2023
self.haveDumpedScreen = True
2024
dump_screen(utilityPath, self.log)
2025
2026
def killAndGetStack(
2027
self,
2028
processPID,
2029
utilityPath,
2030
debuggerInfo,
2031
dump_screen=False):
2032
"""
2033
Kill the process, preferrably in a way that gets us a stack trace.
2034
Also attempts to obtain a screenshot before killing the process
2035
if specified.
2036
"""
2037
self.log.info("Killing process: %s" % processPID)
2038
if dump_screen:
2039
self.dumpScreen(utilityPath)
2040
2041
if mozinfo.info.get('crashreporter', True) and not debuggerInfo:
2042
try:
2043
minidump_path = os.path.join(self.profile.profile,
2044
'minidumps')
2045
mozcrash.kill_and_get_minidump(processPID, minidump_path,
2046
utilityPath)
2047
except OSError:
2049
self.log.info(
2050
"Can't trigger Breakpad, process no longer exists")
2051
return
2052
self.log.info("Can't trigger Breakpad, just killing process")
2053
killPid(processPID, self.log)
2054
2055
def extract_child_pids(self, process_log, parent_pid=None):
2056
"""Parses the given log file for the pids of any processes launched by
2057
the main process and returns them as a list.
2058
If parent_pid is provided, and psutil is available, returns children of
2059
parent_pid according to psutil.
2060
"""
2061
rv = []
2062
if parent_pid and HAVE_PSUTIL:
2063
self.log.info("Determining child pids from psutil...")
2064
try:
2065
rv = [p.pid for p in psutil.Process(parent_pid).children()]
2066
self.log.info(str(rv))
2067
except psutil.NoSuchProcess:
2068
self.log.warning("Failed to lookup children of pid %d" % parent_pid)
2069
2070
rv = set(rv)
2071
pid_re = re.compile(r'==> process \d+ launched child process (\d+)')
2072
with open(process_log) as fd:
2073
for line in fd:
2074
self.log.info(line.rstrip())
2075
m = pid_re.search(line)
2076
if m:
2077
rv.add(int(m.group(1)))
2078
return rv
2079
2080
def checkForZombies(self, processLog, utilityPath, debuggerInfo):
2081
"""Look for hung processes"""
2082
2083
if not os.path.exists(processLog):
2084
self.log.info(
2085
'Automation Error: PID log not found: %s' %
2086
processLog)
2087
# Whilst no hung process was found, the run should still display as
2088
# a failure
2089
return True
2090
2091
# scan processLog for zombies
2092
self.log.info('zombiecheck | Reading PID log: %s' % processLog)
2093
processList = self.extract_child_pids(processLog)
2094
# kill zombies
2095
foundZombie = False
2096
for processPID in processList:
2097
self.log.info(
2098
"zombiecheck | Checking for orphan process with PID: %d" %
2099
processPID)
2100
if isPidAlive(processPID):
2101
foundZombie = True
2102
self.log.error("TEST-UNEXPECTED-FAIL | zombiecheck | child process "
2103
"%d still alive after shutdown" % processPID)
2104
self.killAndGetStack(
2105
processPID,
2106
utilityPath,
2107
debuggerInfo,
2108
dump_screen=not debuggerInfo)
2109
2110
return foundZombie
2111
2112
def checkForRunningBrowsers(self):
2113
firefoxes = ""
2114
if HAVE_PSUTIL:
2115
attrs = ['pid', 'ppid', 'name', 'cmdline', 'username']
2116
for proc in psutil.process_iter():
2117
try:
2118
if 'firefox' in proc.name():
2119
firefoxes = "%s%s\n" % (firefoxes, proc.as_dict(attrs=attrs))
2120
except Exception:
2121
# may not be able to access process info for all processes
2122
continue
2123
if len(firefoxes) > 0:
2124
# In automation, this warning is unexpected and should be investigated.
2125
# In local testing, this is probably okay, as long as the browser is not
2126
# running a marionette server.
2127
self.log.warning("Found 'firefox' running before starting test browser!")
2128
self.log.warning(firefoxes)
2129
2130
def runApp(self,
2131
testUrl,
2132
env,
2133
app,
2134
profile,
2135
extraArgs,
2136
utilityPath,
2137
debuggerInfo=None,
2138
valgrindPath=None,
2139
valgrindArgs=None,
2140
valgrindSuppFiles=None,
2141
symbolsPath=None,
2142
timeout=-1,
2143
detectShutdownLeaks=False,
2144
screenshotOnFail=False,
2145
bisectChunk=None,
2146
marionette_args=None,
2147
e10s=True):
2148
"""
2149
Run the app, log the duration it took to execute, return the status code.
2150
Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing
2151
for |timeout| seconds.
2152
"""
2153
# It can't be the case that both a with-debugger and an
2154
# on-Valgrind run have been requested. doTests() should have
2155
# already excluded this possibility.
2156
assert not(valgrindPath and debuggerInfo)
2157
2158
# debugger information
2159
interactive = False
2160
debug_args = None
2161
if debuggerInfo:
2162
interactive = debuggerInfo.interactive
2163
debug_args = [debuggerInfo.path] + debuggerInfo.args
2164
2165
# Set up Valgrind arguments.
2166
if valgrindPath:
2167
interactive = False
2168
valgrindArgs_split = ([] if valgrindArgs is None
2169
else valgrindArgs.split(","))
2170
2171
valgrindSuppFiles_final = []
2172
if valgrindSuppFiles is not None:
2173
valgrindSuppFiles_final = ["--suppressions=" +
2174
path for path in valgrindSuppFiles.split(",")]
2175
2176
debug_args = ([valgrindPath]
2177
+ mozdebug.get_default_valgrind_args()
2178
+ valgrindArgs_split
2179
+ valgrindSuppFiles_final)
2180
2181
# fix default timeout
2182
if timeout == -1:
2183
timeout = self.DEFAULT_TIMEOUT
2184
2185
# Note in the log if running on Valgrind
2186
if valgrindPath:
2187
self.log.info("runtests.py | Running on Valgrind. "
2188
+ "Using timeout of %d seconds." % timeout)
2189
2190
# copy env so we don't munge the caller's environment
2191
env = env.copy()
2192
2193
# Used to defer a possible IOError exception from Marionette
2194
marionette_exception = None
2195
2196
# make sure we clean up after ourselves.
2197
try:
2198
# set process log environment variable
2199
tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
2200
os.close(tmpfd)
2201
env["MOZ_PROCESS_LOG"] = processLog
2202
2203
if debuggerInfo:
2204
# If a debugger is attached, don't use timeouts, and don't
2205
# capture ctrl-c.
2206
timeout = None
2207
signal.signal(signal.SIGINT, lambda sigid, frame: None)
2208
2209
# build command line
2210
cmd = os.path.abspath(app)
2211
args = list(extraArgs)
2212
args.append('-marionette')
2213
# TODO: mozrunner should use -foreground at least for mac
2215
args.append('-foreground')
2216
self.start_script_kwargs['testUrl'] = testUrl or 'about:blank'
2217
2218
if detectShutdownLeaks:
2219
env['MOZ_LOG'] = (env['MOZ_LOG'] + "," if env['MOZ_LOG'] else "") + \
2220
"DocShellAndDOMWindowLeak:3"
2221
shutdownLeaks = ShutdownLeaks(self.log)
2222
else:
2223
shutdownLeaks = None
2224
2225
if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
2226
lsanLeaks = LSANLeaks(self.log)
2227
else:
2228
lsanLeaks = None
2229
2230
# create an instance to process the output
2231
outputHandler = self.OutputHandler(
2232
harness=self,
2233
utilityPath=utilityPath,
2234
symbolsPath=symbolsPath,
2235
dump_screen_on_timeout=not debuggerInfo,
2236
dump_screen_on_fail=screenshotOnFail,
2237
shutdownLeaks=shutdownLeaks,
2238
lsanLeaks=lsanLeaks,
2239
bisectChunk=bisectChunk)
2240
2241
def timeoutHandler():
2242
browserProcessId = outputHandler.browserProcessId
2243
self.handleTimeout(
2244
timeout,
2245
proc,
2246
utilityPath,
2247
debuggerInfo,
2248
browserProcessId,
2249
processLog)
2250
kp_kwargs = {'kill_on_timeout': False,
2251
'cwd': SCRIPT_DIR,
2252
'onTimeout': [timeoutHandler]}
2253
kp_kwargs['processOutputLine'] = [outputHandler]
2254
2255
self.checkForRunningBrowsers()
2256
2257
# create mozrunner instance and start the system under test process
2258
self.lastTestSeen = self.test_name
2259
startTime = datetime.now()
2260
2261
runner_cls = mozrunner.runners.get(
2262
mozinfo.info.get(
2263
'appname',
2264
'firefox'),
2265
mozrunner.Runner)
2266
runner = runner_cls(profile=self.profile,
2267
binary=cmd,
2268
cmdargs=args,
2269
env=env,
2270
process_class=mozprocess.ProcessHandlerMixin,
2271
process_args=kp_kwargs)
2272
2273
# start the runner
2274
runner.start(debug_args=debug_args,
2275
interactive=interactive,
2276
outputTimeout=timeout)
2277
proc = runner.process_handler
2278
self.log.info("runtests.py | Application pid: %d" % proc.pid)
2279
2280
gecko_id = "GECKO(%d)" % proc.pid
2281
self.log.process_start(gecko_id)
2282
self.message_logger.gecko_id = gecko_id
2283
2284
try:
2285
# start marionette and kick off the tests
2286
marionette_args = marionette_args or {}
2287
self.marionette = Marionette(**marionette_args)
2288
self.marionette.start_session()
2289
2290
# install specialpowers and mochikit addons
2291
addons = Addons(self.marionette)
2292
2293
if self.staged_addons:
2294
for addon_path in self.staged_addons:
2295
if not os.path.isdir(addon_path):
2296
self.log.error(
2297
"TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s" %
2298
addon_path)
2299
return 1, self.lastTestSeen
2300
addons.install(create_zip(addon_path))
2301
2302
self.execute_start_script()
2303
2304
# an open marionette session interacts badly with mochitest,
2305
# delete it until we figure out why.
2306
self.marionette.delete_session()
2307
del self.marionette
2308
2309
except IOError:
2310
# Any IOError as thrown by Marionette means that something is
2311
# wrong with the process, like a crash or the socket is no
2312
# longer open. We defer raising this specific error so that
2313
# post-test checks for leaks and crashes are performed and
2314
# reported first.
2315
marionette_exception = sys.exc_info()
2316
2317
# wait until app is finished
2318
# XXX copy functionality from
2320
# until bug 913970 is fixed regarding mozrunner `wait` not returning status
2322
self.log.info("runtests.py | Waiting for browser...")
2323
status = proc.wait()
2324
if status is None:
2325
self.log.warning("runtests.py | Failed to get app exit code - running/crashed?")
2326
# must report an integer to process_exit()
2327
status = 0
2328
self.log.process_exit("Main app process", status)
2329
runner.process_handler = None
2330
2331
# finalize output handler
2332
outputHandler.finish()
2333
2334
# record post-test information
2335
if status:
2336
self.message_logger.dump_buffered()
2337
self.log.error(
2338
"TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s" %
2339
(self.lastTestSeen, status))
2340
else:
2341
self.lastTestSeen = 'Main app process exited normally'
2342
2343
sel