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 5from __future__ import absolute_import, unicode_literals 6 7import json 8import logging 9import mozinfo 10import os 11 12from mach.decorators import ( 13 Command, 14 CommandArgument, 15 CommandProvider, 16) 17from mozbuild.base import ( 18 MachCommandBase, 19 MachCommandConditions as conditions, 20 BinaryNotFoundException, 21) 22 23 24def is_valgrind_build(cls): 25 """Must be a build with --enable-valgrind and --disable-jemalloc.""" 26 defines = cls.config_environment.defines 27 return "MOZ_VALGRIND" in defines and "MOZ_MEMORY" not in defines 28 29 30@CommandProvider 31class MachCommands(MachCommandBase): 32 """ 33 Run Valgrind tests. 34 """ 35 36 @Command( 37 "valgrind-test", 38 category="testing", 39 conditions=[conditions.is_firefox_or_thunderbird, is_valgrind_build], 40 description="Run the Valgrind test job (memory-related errors).", 41 ) 42 @CommandArgument( 43 "--suppressions", 44 default=[], 45 action="append", 46 metavar="FILENAME", 47 help="Specify a suppression file for Valgrind to use. Use " 48 "--suppression multiple times to specify multiple suppression " 49 "files.", 50 ) 51 def valgrind_test(self, command_context, suppressions): 52 53 from mozfile import TemporaryDirectory 54 from mozhttpd import MozHttpd 55 from mozprofile import FirefoxProfile, Preferences 56 from mozprofile.permissions import ServerLocations 57 from mozrunner import FirefoxRunner 58 from mozrunner.utils import findInPath 59 from six import string_types 60 from valgrind.output_handler import OutputHandler 61 62 build_dir = os.path.join(self.topsrcdir, "build") 63 64 # XXX: currently we just use the PGO inputs for Valgrind runs. This may 65 # change in the future. 66 httpd = MozHttpd(docroot=os.path.join(build_dir, "pgo")) 67 httpd.start(block=False) 68 69 with TemporaryDirectory() as profilePath: 70 # TODO: refactor this into mozprofile 71 profile_data_dir = os.path.join(self.topsrcdir, "testing", "profiles") 72 with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh: 73 base_profiles = json.load(fh)["valgrind"] 74 75 prefpaths = [ 76 os.path.join(profile_data_dir, profile, "user.js") 77 for profile in base_profiles 78 ] 79 prefs = {} 80 for path in prefpaths: 81 prefs.update(Preferences.read_prefs(path)) 82 83 interpolation = { 84 "server": "%s:%d" % httpd.httpd.server_address, 85 } 86 for k, v in prefs.items(): 87 if isinstance(v, string_types): 88 v = v.format(**interpolation) 89 prefs[k] = Preferences.cast(v) 90 91 quitter = os.path.join( 92 self.topsrcdir, "tools", "quitter", "quitter@mozilla.org.xpi" 93 ) 94 95 locations = ServerLocations() 96 locations.add_host( 97 host="127.0.0.1", port=httpd.httpd.server_port, options="primary" 98 ) 99 100 profile = FirefoxProfile( 101 profile=profilePath, 102 preferences=prefs, 103 addons=[quitter], 104 locations=locations, 105 ) 106 107 firefox_args = [httpd.get_url()] 108 109 env = os.environ.copy() 110 env["G_SLICE"] = "always-malloc" 111 env["MOZ_CC_RUN_DURING_SHUTDOWN"] = "1" 112 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 113 env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" 114 env["XPCOM_DEBUG_BREAK"] = "warn" 115 116 outputHandler = OutputHandler(self.log) 117 kp_kwargs = { 118 "processOutputLine": [outputHandler], 119 "universal_newlines": True, 120 } 121 122 valgrind = "valgrind" 123 if not os.path.exists(valgrind): 124 valgrind = findInPath(valgrind) 125 126 valgrind_args = [ 127 valgrind, 128 "--sym-offsets=yes", 129 "--smc-check=all-non-file", 130 "--vex-iropt-register-updates=allregs-at-mem-access", 131 "--gen-suppressions=all", 132 "--num-callers=36", 133 "--leak-check=full", 134 "--show-possibly-lost=no", 135 "--track-origins=yes", 136 "--trace-children=yes", 137 "-v", # Enable verbosity to get the list of used suppressions 138 # Avoid excessive delays in the presence of spinlocks. 139 # See bug 1309851. 140 "--fair-sched=yes", 141 # Keep debuginfo after library unmap. See bug 1382280. 142 "--keep-debuginfo=yes", 143 # Reduce noise level on rustc and/or LLVM compiled code. 144 # See bug 1365915 145 "--expensive-definedness-checks=yes", 146 # Compensate for the compiler inlining `new` but not `delete` 147 # or vice versa. 148 "--show-mismatched-frees=no", 149 ] 150 151 for s in suppressions: 152 valgrind_args.append("--suppressions=" + s) 153 154 supps_dir = os.path.join(build_dir, "valgrind") 155 supps_file1 = os.path.join(supps_dir, "cross-architecture.sup") 156 valgrind_args.append("--suppressions=" + supps_file1) 157 158 if mozinfo.os == "linux": 159 machtype = { 160 "x86_64": "x86_64-pc-linux-gnu", 161 "x86": "i386-pc-linux-gnu", 162 }.get(mozinfo.processor) 163 if machtype: 164 supps_file2 = os.path.join(supps_dir, machtype + ".sup") 165 if os.path.isfile(supps_file2): 166 valgrind_args.append("--suppressions=" + supps_file2) 167 168 exitcode = None 169 timeout = 1800 170 binary_not_found_exception = None 171 try: 172 runner = FirefoxRunner( 173 profile=profile, 174 binary=self.get_binary_path(), 175 cmdargs=firefox_args, 176 env=env, 177 process_args=kp_kwargs, 178 ) 179 runner.start(debug_args=valgrind_args) 180 exitcode = runner.wait(timeout=timeout) 181 except BinaryNotFoundException as e: 182 binary_not_found_exception = e 183 finally: 184 errs = outputHandler.error_count 185 supps = outputHandler.suppression_count 186 if errs != supps: 187 status = 1 # turns the TBPL job orange 188 self.log( 189 logging.ERROR, 190 "valgrind-fail-parsing", 191 {"errs": errs, "supps": supps}, 192 "TEST-UNEXPECTED-FAIL | valgrind-test | error parsing: {errs} errors " 193 "seen, but {supps} generated suppressions seen", 194 ) 195 196 elif errs == 0: 197 status = 0 198 self.log( 199 logging.INFO, 200 "valgrind-pass", 201 {}, 202 "TEST-PASS | valgrind-test | valgrind found no errors", 203 ) 204 else: 205 status = 1 # turns the TBPL job orange 206 # We've already printed details of the errors. 207 208 if binary_not_found_exception: 209 status = 2 # turns the TBPL job red 210 self.log( 211 logging.ERROR, 212 "valgrind-fail-errors", 213 {"error": str(binary_not_found_exception)}, 214 "TEST-UNEXPECTED-FAIL | valgrind-test | {error}", 215 ) 216 self.log( 217 logging.INFO, 218 "valgrind-fail-errors", 219 {"help": binary_not_found_exception.help()}, 220 "{help}", 221 ) 222 elif exitcode is None: 223 status = 2 # turns the TBPL job red 224 self.log( 225 logging.ERROR, 226 "valgrind-fail-timeout", 227 {"timeout": timeout}, 228 "TEST-UNEXPECTED-FAIL | valgrind-test | Valgrind timed out " 229 "(reached {timeout} second limit)", 230 ) 231 elif exitcode != 0: 232 status = 2 # turns the TBPL job red 233 self.log( 234 logging.ERROR, 235 "valgrind-fail-errors", 236 {"exitcode": exitcode}, 237 "TEST-UNEXPECTED-FAIL | valgrind-test | non-zero exit code " 238 "from Valgrind: {exitcode}", 239 ) 240 241 httpd.stop() 242 243 return status 244