| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this |
| # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| from __future__ import absolute_import, print_function, unicode_literals |
| |
| import os |
| import subprocess |
| |
| from mach.decorators import ( |
| Command, |
| CommandArgument, |
| CommandProvider, |
| ) |
| from mozbuild.base import ( |
| MachCommandBase, |
| MachCommandConditions as conditions, |
| ) |
| |
| |
| def is_valgrind_build(cls): |
| '''Must be a build with --enable-valgrind and --disable-jemalloc.''' |
| defines = cls.config_environment.defines |
| return 'MOZ_VALGRIND' in defines and 'MOZ_MEMORY' not in defines |
| |
| |
| @CommandProvider |
| class MachCommands(MachCommandBase): |
| ''' |
| Run Valgrind tests. |
| ''' |
| def __init__(self, context): |
| MachCommandBase.__init__(self, context) |
| |
| @Command('valgrind-test', category='testing', |
| conditions=[conditions.is_firefox, is_valgrind_build], |
| description='Run the Valgrind test job (memory-related errors).') |
| @CommandArgument('--suppressions', default=[], action='append', |
| metavar='FILENAME', |
| help='Specify a suppression file for Valgrind to use. Use ' |
| '--suppression multiple times to specify multiple suppression ' |
| 'files.') |
| def valgrind_test(self, suppressions): |
| import json |
| import sys |
| import tempfile |
| |
| from mozbuild.base import MozbuildObject |
| from mozfile import TemporaryDirectory |
| from mozhttpd import MozHttpd |
| from mozprofile import FirefoxProfile, Preferences |
| from mozprofile.permissions import ServerLocations |
| from mozrunner import FirefoxRunner |
| from mozrunner.utils import findInPath |
| from valgrind.output_handler import OutputHandler |
| |
| build_dir = os.path.join(self.topsrcdir, 'build') |
| |
| # XXX: currently we just use the PGO inputs for Valgrind runs. This may |
| # change in the future. |
| httpd = MozHttpd(docroot=os.path.join(build_dir, 'pgo')) |
| httpd.start(block=False) |
| |
| with TemporaryDirectory() as profilePath: |
| #TODO: refactor this into mozprofile |
| prefpath = os.path.join(self.topsrcdir, 'testing', 'profiles', 'prefs_general.js') |
| prefs = {} |
| prefs.update(Preferences.read_prefs(prefpath)) |
| interpolation = { 'server': '%s:%d' % httpd.httpd.server_address, |
| 'OOP': 'false'} |
| prefs = json.loads(json.dumps(prefs) % interpolation) |
| for pref in prefs: |
| prefs[pref] = Preferences.cast(prefs[pref]) |
| |
| quitter = os.path.join(self.distdir, 'xpi-stage', 'quitter') |
| |
| locations = ServerLocations() |
| locations.add_host(host='127.0.0.1', |
| port=httpd.httpd.server_port, |
| options='primary') |
| |
| profile = FirefoxProfile(profile=profilePath, |
| preferences=prefs, |
| addons=[quitter], |
| locations=locations) |
| |
| firefox_args = [httpd.get_url()] |
| |
| env = os.environ.copy() |
| env['G_SLICE'] = 'always-malloc' |
| env['MOZ_CC_RUN_DURING_SHUTDOWN'] = '1' |
| env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' |
| env['XPCOM_DEBUG_BREAK'] = 'warn' |
| |
| env.update(self.extra_environment_variables) |
| |
| outputHandler = OutputHandler() |
| kp_kwargs = {'processOutputLine': [outputHandler]} |
| |
| valgrind = 'valgrind' |
| if not os.path.exists(valgrind): |
| valgrind = findInPath(valgrind) |
| |
| valgrind_args = [ |
| valgrind, |
| '--smc-check=all-non-file', |
| '--vex-iropt-register-updates=allregs-at-mem-access', |
| '--gen-suppressions=all', |
| '--num-callers=36', |
| '--leak-check=full', |
| '--show-possibly-lost=no', |
| '--track-origins=yes', |
| '--trace-children=yes', |
| # The gstreamer plugin scanner can run as part of executing |
| # firefox, but is an external program. In some weird cases, |
| # valgrind finds errors while executing __libc_freeres when |
| # it runs, but those are not relevant, as it's related to |
| # executing third party code. So don't trace |
| # gst-plugin-scanner. |
| '--trace-children-skip=*/gst-plugin-scanner', |
| '-v', # Enable verbosity to get the list of used suppressions |
| ] |
| |
| for s in suppressions: |
| valgrind_args.append('--suppressions=' + s) |
| |
| supps_dir = os.path.join(build_dir, 'valgrind') |
| supps_file1 = os.path.join(supps_dir, 'cross-architecture.sup') |
| valgrind_args.append('--suppressions=' + supps_file1) |
| |
| # MACHTYPE is an odd bash-only environment variable that doesn't |
| # show up in os.environ, so we have to get it another way. |
| machtype = subprocess.check_output(['bash', '-c', 'echo $MACHTYPE']).rstrip() |
| supps_file2 = os.path.join(supps_dir, machtype + '.sup') |
| if os.path.isfile(supps_file2): |
| valgrind_args.append('--suppressions=' + supps_file2) |
| |
| exitcode = None |
| timeout = 1100 |
| try: |
| runner = FirefoxRunner(profile=profile, |
| binary=self.get_binary_path(), |
| cmdargs=firefox_args, |
| env=env, |
| process_args=kp_kwargs) |
| runner.start(debug_args=valgrind_args) |
| # This timeout is slightly less than the no-output timeout on |
| # TBPL, so we'll timeout here first and give an informative |
| # message. |
| exitcode = runner.wait(timeout=timeout) |
| |
| finally: |
| errs = outputHandler.error_count |
| supps = outputHandler.suppression_count |
| if errs != supps: |
| status = 1 # turns the TBPL job orange |
| print('TEST-UNEXPECTED-FAIL | valgrind-test | error parsing: {} errors seen, but {} generated suppressions seen'.format(errs, supps)) |
| |
| elif errs == 0: |
| status = 0 |
| print('TEST-PASS | valgrind-test | valgrind found no errors') |
| else: |
| status = 1 # turns the TBPL job orange |
| # We've already printed details of the errors. |
| |
| if exitcode == None: |
| status = 2 # turns the TBPL job red |
| print('TEST-UNEXPECTED-FAIL | valgrind-test | Valgrind timed out (reached {} second limit)'.format(timeout)) |
| elif exitcode != 0: |
| status = 2 # turns the TBPL job red |
| print('TEST-UNEXPECTED-FAIL | valgrind-test | non-zero exit code from Valgrind') |
| |
| httpd.stop() |
| |
| return status |