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