Source code

Revision control

Other Tools

1
import json
2
import os
3
import platform
4
import signal
5
import subprocess
6
import sys
7
8
import mozinfo
9
import mozleak
10
import mozversion
11
from mozprocess import ProcessHandler
12
from mozprofile import FirefoxProfile, Preferences
13
from mozrunner import FirefoxRunner
14
from mozrunner.utils import test_environment, get_stack_fixer_function
15
from mozcrash import mozcrash
16
17
from .base import (get_free_port,
18
Browser,
19
ExecutorBrowser,
20
require_arg,
21
cmd_arg,
22
browser_command)
23
from ..executors import executor_kwargs as base_executor_kwargs
24
from ..executors.executormarionette import (MarionetteTestharnessExecutor, # noqa: F401
25
MarionetteRefTestExecutor, # noqa: F401
26
MarionetteWdspecExecutor) # noqa: F401
27
28
29
here = os.path.join(os.path.split(__file__)[0])
30
31
__wptrunner__ = {"product": "firefox",
32
"check_args": "check_args",
33
"browser": "FirefoxBrowser",
34
"executor": {"testharness": "MarionetteTestharnessExecutor",
35
"reftest": "MarionetteRefTestExecutor",
36
"wdspec": "MarionetteWdspecExecutor"},
37
"browser_kwargs": "browser_kwargs",
38
"executor_kwargs": "executor_kwargs",
39
"env_extras": "env_extras",
40
"env_options": "env_options",
41
"run_info_extras": "run_info_extras",
42
"update_properties": "update_properties",
43
"timeout_multiplier": "get_timeout_multiplier"}
44
45
46
def get_timeout_multiplier(test_type, run_info_data, **kwargs):
47
if kwargs["timeout_multiplier"] is not None:
48
return kwargs["timeout_multiplier"]
49
if test_type == "reftest":
50
if run_info_data["debug"] or run_info_data.get("asan"):
51
return 4
52
else:
53
return 2
54
elif run_info_data["debug"] or run_info_data.get("asan"):
55
if run_info_data.get("ccov"):
56
return 4
57
else:
58
return 3
59
elif run_info_data["os"] == "android":
60
return 4
62
elif run_info_data["os"] == "win" and run_info_data["processor"] == "aarch64":
63
return 4
64
return 1
65
66
67
def check_args(**kwargs):
68
require_arg(kwargs, "binary")
69
70
71
def browser_kwargs(test_type, run_info_data, config, **kwargs):
72
return {"binary": kwargs["binary"],
73
"prefs_root": kwargs["prefs_root"],
74
"extra_prefs": kwargs["extra_prefs"],
75
"test_type": test_type,
76
"debug_info": kwargs["debug_info"],
77
"symbols_path": kwargs["symbols_path"],
78
"stackwalk_binary": kwargs["stackwalk_binary"],
79
"certutil_binary": kwargs["certutil_binary"],
80
"ca_certificate_path": config.ssl_config["ca_cert_path"],
81
"e10s": kwargs["gecko_e10s"],
82
"enable_webrender": kwargs["enable_webrender"],
83
"stackfix_dir": kwargs["stackfix_dir"],
84
"binary_args": kwargs["binary_args"],
85
"timeout_multiplier": get_timeout_multiplier(test_type,
86
run_info_data,
87
**kwargs),
88
"leak_check": run_info_data["debug"] and (kwargs["leak_check"] is not False),
89
"asan": run_info_data.get("asan"),
90
"stylo_threads": kwargs["stylo_threads"],
91
"chaos_mode_flags": kwargs["chaos_mode_flags"],
92
"config": config,
93
"browser_channel": kwargs["browser_channel"],
94
"headless": kwargs["headless"]}
95
96
97
def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
98
**kwargs):
99
executor_kwargs = base_executor_kwargs(test_type, server_config,
100
cache_manager, run_info_data,
101
**kwargs)
102
executor_kwargs["close_after_done"] = test_type != "reftest"
103
executor_kwargs["timeout_multiplier"] = get_timeout_multiplier(test_type,
104
run_info_data,
105
**kwargs)
106
executor_kwargs["e10s"] = run_info_data["e10s"]
107
capabilities = {}
108
if test_type == "testharness":
109
capabilities["pageLoadStrategy"] = "eager"
110
if test_type == "reftest":
111
executor_kwargs["reftest_internal"] = kwargs["reftest_internal"]
112
executor_kwargs["reftest_screenshot"] = kwargs["reftest_screenshot"]
113
if test_type == "wdspec":
114
options = {}
115
if kwargs["binary"]:
116
options["binary"] = kwargs["binary"]
117
if kwargs["binary_args"]:
118
options["args"] = kwargs["binary_args"]
119
if kwargs["headless"]:
120
if "args" not in options:
121
options["args"] = []
122
if "--headless" not in options["args"]:
123
options["args"].append("--headless")
124
options["prefs"] = {
125
"network.dns.localDomains": ",".join(server_config.domains_set)
126
}
127
for pref, value in kwargs["extra_prefs"]:
128
options["prefs"].update({pref: Preferences.cast(value)})
129
capabilities["moz:firefoxOptions"] = options
130
if kwargs["certutil_binary"] is None:
131
capabilities["acceptInsecureCerts"] = True
132
if capabilities:
133
executor_kwargs["capabilities"] = capabilities
134
executor_kwargs["debug"] = run_info_data["debug"]
135
executor_kwargs["ccov"] = run_info_data.get("ccov", False)
136
return executor_kwargs
137
138
139
def env_extras(**kwargs):
140
return []
141
142
143
def env_options():
144
# The server host is set to 127.0.0.1 as Firefox is configured (through the
145
# network.dns.localDomains preference set below) to resolve the test
146
# domains to localhost without relying on the network stack.
147
#
149
return {"server_host": "127.0.0.1",
150
"supports_debugger": True}
151
152
153
def run_info_extras(**kwargs):
154
155
def get_bool_pref_if_exists(pref):
156
for key, value in kwargs.get('extra_prefs', []):
157
if pref == key:
158
return value.lower() in ('true', '1')
159
return None
160
161
def get_bool_pref(pref):
162
pref_value = get_bool_pref_if_exists(pref)
163
return pref_value if pref_value is not None else False
164
165
rv = {"e10s": kwargs["gecko_e10s"],
166
"wasm": kwargs.get("wasm", True),
167
"verify": kwargs["verify"],
168
"headless": kwargs.get("headless", False) or "MOZ_HEADLESS" in os.environ,
169
"fission": get_bool_pref("fission.autostart")}
170
171
# The value of `sw-e10s` defaults to whether the "parent_intercept"
172
# implementation is enabled for the current build. This value, however,
173
# can be overridden by explicitly setting the pref with the `--setpref` CLI
174
# flag, which is checked here. If not supplied, the default value of
175
# `sw-e10s` will be filled in in `RunInfo`'s constructor.
176
#
177
# We can't capture the default value right now because (currently), it
178
# defaults to the value of `nightly_build`, which isn't known until
179
# `RunInfo`'s constructor.
180
sw_e10s_override = get_bool_pref_if_exists("dom.serviceWorkers.parent_intercept")
181
if sw_e10s_override is not None:
182
rv["sw-e10s"] = sw_e10s_override
183
184
rv.update(run_info_browser_version(kwargs["binary"]))
185
return rv
186
187
188
def run_info_browser_version(binary):
189
try:
190
version_info = mozversion.get_version(binary)
191
except mozversion.errors.VersionError:
192
version_info = None
193
if version_info:
194
return {"browser_build_id": version_info.get("application_buildid", None),
195
"browser_changeset": version_info.get("application_changeset", None)}
196
return {}
197
198
199
def update_properties():
200
return (["os", "debug", "webrender", "fission", "e10s", "sw-e10s", "processor"],
201
{"os": ["version"], "processor": ["bits"]})
202
203
204
class FirefoxBrowser(Browser):
205
init_timeout = 70
206
shutdown_timeout = 70
207
208
def __init__(self, logger, binary, prefs_root, test_type, extra_prefs=None, debug_info=None,
209
symbols_path=None, stackwalk_binary=None, certutil_binary=None,
210
ca_certificate_path=None, e10s=False, enable_webrender=False, stackfix_dir=None,
211
binary_args=None, timeout_multiplier=None, leak_check=False, asan=False,
212
stylo_threads=1, chaos_mode_flags=None, config=None, browser_channel="nightly", headless=None, **kwargs):
213
Browser.__init__(self, logger)
214
self.binary = binary
215
self.prefs_root = prefs_root
216
self.test_type = test_type
217
self.extra_prefs = extra_prefs
218
self.marionette_port = None
219
self.runner = None
220
self.debug_info = debug_info
221
self.profile = None
222
self.symbols_path = symbols_path
223
self.stackwalk_binary = stackwalk_binary
224
self.ca_certificate_path = ca_certificate_path
225
self.certutil_binary = certutil_binary
226
self.e10s = e10s
227
self.enable_webrender = enable_webrender
228
self.binary_args = binary_args
229
self.config = config
230
if stackfix_dir:
231
self.stack_fixer = get_stack_fixer_function(stackfix_dir,
232
self.symbols_path)
233
else:
234
self.stack_fixer = None
235
236
if timeout_multiplier:
237
self.init_timeout = self.init_timeout * timeout_multiplier
238
239
self.asan = asan
240
self.lsan_allowed = None
241
self.lsan_max_stack_depth = None
242
self.mozleak_allowed = None
243
self.mozleak_thresholds = None
244
self.leak_check = leak_check
245
self.leak_report_file = None
246
self.lsan_handler = None
247
self.stylo_threads = stylo_threads
248
self.chaos_mode_flags = chaos_mode_flags
249
self.browser_channel = browser_channel
250
self.headless = headless
251
252
def settings(self, test):
253
return {"check_leaks": self.leak_check and not test.leaks,
254
"lsan_allowed": test.lsan_allowed,
255
"lsan_max_stack_depth": test.lsan_max_stack_depth,
256
"mozleak_allowed": self.leak_check and test.mozleak_allowed,
257
"mozleak_thresholds": self.leak_check and test.mozleak_threshold}
258
259
def start(self, group_metadata=None, **kwargs):
260
if group_metadata is None:
261
group_metadata = {}
262
263
self.group_metadata = group_metadata
264
self.lsan_allowed = kwargs.get("lsan_allowed")
265
self.lsan_max_stack_depth = kwargs.get("lsan_max_stack_depth")
266
self.mozleak_allowed = kwargs.get("mozleak_allowed")
267
self.mozleak_thresholds = kwargs.get("mozleak_thresholds")
268
269
if self.marionette_port is None:
270
self.marionette_port = get_free_port()
271
272
if self.asan:
273
self.lsan_handler = mozleak.LSANLeaks(self.logger,
274
scope=group_metadata.get("scope", "/"),
275
allowed=self.lsan_allowed,
276
maxNumRecordedFrames=self.lsan_max_stack_depth)
277
278
env = test_environment(xrePath=os.path.dirname(self.binary),
279
debugger=self.debug_info is not None,
280
useLSan=True, log=self.logger)
281
282
env["STYLO_THREADS"] = str(self.stylo_threads)
283
if self.chaos_mode_flags is not None:
284
env["MOZ_CHAOSMODE"] = str(self.chaos_mode_flags)
285
if self.headless:
286
env["MOZ_HEADLESS"] = "1"
287
if self.enable_webrender:
288
env["MOZ_WEBRENDER"] = "1"
289
env["MOZ_ACCELERATED"] = "1"
290
else:
291
env["MOZ_WEBRENDER"] = "0"
292
293
preferences = self.load_prefs()
294
295
self.profile = FirefoxProfile(preferences=preferences)
296
self.profile.set_preferences({
297
"marionette.port": self.marionette_port,
298
"network.dns.localDomains": ",".join(self.config.domains_set),
299
"dom.file.createInChild": True,
300
# TODO: Remove preferences once Firefox 64 is stable (Bug 905404)
301
"network.proxy.type": 0,
302
"places.history.enabled": False,
303
"network.preload": True,
304
})
305
if self.e10s:
306
self.profile.set_preferences({"browser.tabs.remote.autostart": True})
307
308
if self.test_type == "reftest":
309
self.profile.set_preferences({"layout.interruptible-reflow.enabled": False})
310
311
if self.leak_check:
312
self.leak_report_file = os.path.join(self.profile.profile, "runtests_leaks_%s.log" % os.getpid())
313
if os.path.exists(self.leak_report_file):
314
os.remove(self.leak_report_file)
315
env["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
316
else:
317
self.leak_report_file = None
318
319
# Bug 1262954: winxp + e10s, disable hwaccel
320
if (self.e10s and platform.system() in ("Windows", "Microsoft") and
321
'5.1' in platform.version()):
322
self.profile.set_preferences({"layers.acceleration.disabled": True})
323
324
if self.ca_certificate_path is not None:
325
self.setup_ssl()
326
327
args = self.binary_args[:] if self.binary_args else []
328
args += [cmd_arg("marionette"), "about:blank"]
329
330
debug_args, cmd = browser_command(self.binary,
331
args,
332
self.debug_info)
333
334
self.runner = FirefoxRunner(profile=self.profile,
335
binary=cmd[0],
336
cmdargs=cmd[1:],
337
env=env,
338
process_class=ProcessHandler,
339
process_args={"processOutputLine": [self.on_output]})
340
341
self.logger.debug("Starting Firefox")
342
343
self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive)
344
self.logger.debug("Firefox Started")
345
346
def load_prefs(self):
347
prefs = Preferences()
348
349
pref_paths = []
350
351
profiles = os.path.join(self.prefs_root, 'profiles.json')
352
if os.path.isfile(profiles):
353
with open(profiles, 'r') as fh:
354
for name in json.load(fh)['web-platform-tests']:
355
if self.browser_channel in (None, 'nightly'):
356
pref_paths.append(os.path.join(self.prefs_root, name, 'user.js'))
357
elif name != 'unittest-features':
358
pref_paths.append(os.path.join(self.prefs_root, name, 'user.js'))
359
else:
360
# Old preference files used before the creation of profiles.json (remove when no longer supported)
361
legacy_pref_paths = (
362
os.path.join(self.prefs_root, 'prefs_general.js'), # Used in Firefox 60 and below
363
os.path.join(self.prefs_root, 'common', 'user.js'), # Used in Firefox 61
364
)
365
for path in legacy_pref_paths:
366
if os.path.isfile(path):
367
pref_paths.append(path)
368
369
for path in pref_paths:
370
if os.path.exists(path):
371
prefs.add(Preferences.read_prefs(path))
372
else:
373
self.logger.warning("Failed to find base prefs file in %s" % path)
374
375
# Add any custom preferences
376
prefs.add(self.extra_prefs, cast=True)
377
378
return prefs()
379
380
def stop(self, force=False):
381
if self.runner is not None and self.runner.is_running():
382
try:
383
# For Firefox we assume that stopping the runner prompts the
384
# browser to shut down. This allows the leak log to be written
385
for clean, stop_f in [(True, lambda: self.runner.wait(self.shutdown_timeout)),
386
(False, lambda: self.runner.stop(signal.SIGTERM)),
387
(False, lambda: self.runner.stop(signal.SIGKILL))]:
388
if not force or not clean:
389
retcode = stop_f()
390
if retcode is not None:
391
self.logger.info("Browser exited with return code %s" % retcode)
392
break
393
except OSError:
394
# This can happen on Windows if the process is already dead
395
pass
396
self.process_leaks()
397
self.logger.debug("stopped")
398
399
def process_leaks(self):
400
self.logger.info("PROCESS LEAKS %s" % self.leak_report_file)
401
if self.lsan_handler:
402
self.lsan_handler.process()
403
if self.leak_report_file is not None:
404
# We have to ignore missing leaks in the tab because it can happen that the
405
# content process crashed and in that case we don't want the test to fail.
406
# Ideally we would record which content process crashed and just skip those.
407
mozleak.process_leak_log(
408
self.leak_report_file,
409
leak_thresholds=self.mozleak_thresholds,
410
ignore_missing_leaks=["tab", "gmplugin"],
411
log=self.logger,
412
stack_fixer=self.stack_fixer,
413
scope=self.group_metadata.get("scope"),
414
allowed=self.mozleak_allowed
415
)
416
417
def pid(self):
418
if self.runner.process_handler is None:
419
return None
420
421
try:
422
return self.runner.process_handler.pid
423
except AttributeError:
424
return None
425
426
def on_output(self, line):
427
"""Write a line of output from the firefox process to the log"""
428
if "GLib-GObject-CRITICAL" in line:
429
return
430
if line:
431
data = line.decode("utf8", "replace")
432
if self.stack_fixer:
433
data = self.stack_fixer(data)
434
if self.lsan_handler:
435
data = self.lsan_handler.log(data)
436
if data is not None:
437
self.logger.process_output(self.pid(),
438
data,
439
command=" ".join(self.runner.command))
440
441
def is_alive(self):
442
if self.runner:
443
return self.runner.is_running()
444
return False
445
446
def cleanup(self, force=False):
447
self.stop(force)
448
449
def executor_browser(self):
450
assert self.marionette_port is not None
451
return ExecutorBrowser, {"marionette_port": self.marionette_port}
452
453
def check_crash(self, process, test):
454
dump_dir = os.path.join(self.profile.profile, "minidumps")
455
456
try:
457
return bool(mozcrash.log_crashes(self.logger,
458
dump_dir,
459
symbols_path=self.symbols_path,
460
stackwalk_binary=self.stackwalk_binary,
461
process=process,
462
test=test))
463
except IOError:
464
self.logger.warning("Looking for crash dump files failed")
465
return False
466
467
def setup_ssl(self):
468
"""Create a certificate database to use in the test profile. This is configured
469
to trust the CA Certificate that has signed the web-platform.test server
470
certificate."""
471
if self.certutil_binary is None:
472
self.logger.info("--certutil-binary not supplied; Firefox will not check certificates")
473
return
474
475
self.logger.info("Setting up ssl")
476
477
# Make sure the certutil libraries from the source tree are loaded when using a
478
# local copy of certutil
479
# TODO: Maybe only set this if certutil won't launch?
480
env = os.environ.copy()
481
certutil_dir = os.path.dirname(self.binary or self.certutil_binary)
482
if mozinfo.isMac:
483
env_var = "DYLD_LIBRARY_PATH"
484
elif mozinfo.isUnix:
485
env_var = "LD_LIBRARY_PATH"
486
else:
487
env_var = "PATH"
488
489
490
env[env_var] = (os.path.pathsep.join([certutil_dir, env[env_var]])
491
if env_var in env else certutil_dir).encode(
492
sys.getfilesystemencoding() or 'utf-8', 'replace')
493
494
def certutil(*args):
495
cmd = [self.certutil_binary] + list(args)
496
self.logger.process_output("certutil",
497
subprocess.check_output(cmd,
498
env=env,
499
stderr=subprocess.STDOUT),
500
" ".join(cmd))
501
502
pw_path = os.path.join(self.profile.profile, ".crtdbpw")
503
with open(pw_path, "w") as f:
504
# Use empty password for certificate db
505
f.write("\n")
506
507
cert_db_path = self.profile.profile
508
509
# Create a new certificate db
510
certutil("-N", "-d", cert_db_path, "-f", pw_path)
511
512
# Add the CA certificate to the database and mark as trusted to issue server certs
513
certutil("-A", "-d", cert_db_path, "-f", pw_path, "-t", "CT,,",
514
"-n", "web-platform-tests", "-i", self.ca_certificate_path)
515
516
# List all certs in the database
517
certutil("-L", "-d", cert_db_path)