• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #5253

11 Oct 2024 10:21AM CUT coverage: 21.668% (+0.02%) from 21.646%
#5253

push

coveralls-python

jgraham
Make webcompat platform bugs without keyword use BQ data

This is considered the canonical source for the list of webcompat core bugs,
so using it as the backend for the bug-bot rules avoids needing to duplicate
the logic across multiple systems.

426 of 2874 branches covered (14.82%)

0 of 13 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1941 of 8958 relevant lines covered (21.67%)

0.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/bugbot/crash/socorro_util.py
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 file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
"""The code in this module was borrowed from Socorro (some parts were adjusted).
6
Each function, class, or dictionary is documented with a link to the original
7
source.
8
"""
9

10

11
import re
×
12
from functools import cached_property
×
13
from itertools import islice
×
14

15

16
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/templatetags/jinja_helpers.py#L182-L203
17
def generate_bug_description_data(report) -> dict:
×
18
    crashing_thread = get_crashing_thread(report)
×
19
    parsed_dump = get_parsed_dump(report) or {}
×
20

21
    frames = None
×
22
    threads = parsed_dump.get("threads")
×
23
    if threads:
×
24
        thread_index = crashing_thread or 0
×
25
        frames = bugzilla_thread_frames(parsed_dump["threads"][thread_index])
×
26

27
    return {
×
28
        "uuid": report["uuid"],
29
        # NOTE(willkg): this is the redacted stack trace--not the raw one that can
30
        # have PII in it
31
        "java_stack_trace": report.get("java_stack_trace", None),
32
        # NOTE(willkg): this is the redacted mozcrashreason--not the raw one that
33
        # can have PII in it
34
        "moz_crash_reason": report.get("moz_crash_reason", None),
35
        "reason": report.get("reason", None),
36
        "frames": frames,
37
        "crashing_thread": crashing_thread,
38
    }
39

40

41
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/templatetags/jinja_helpers.py#L227-L278
42
def bugzilla_thread_frames(thread):
×
43
    """Build frame information for bug creation link
44

45
    Extract frame info for the top frames of a crashing thread to be included in the
46
    Bugzilla summary when reporting the crash.
47

48
    :arg thread: dict of thread information including "frames" list
49

50
    :returns: list of frame information dicts
51

52
    """
53

54
    def frame_generator(thread):
×
55
        """Yield frames in a thread factoring in inlines"""
56
        for frame in thread["frames"]:
×
57
            for inline in frame.get("inlines") or []:
×
58
                yield {
×
59
                    "frame": frame.get("frame", "?"),
60
                    "module": frame.get("module", ""),
61
                    "signature": inline["function"],
62
                    "file": inline["file"],
63
                    "line": inline["line"],
64
                }
65

66
            yield frame
×
67

68
    # We only want to include 10 frames in the link
69
    MAX_FRAMES = 10
×
70

71
    frames = []
×
72
    for frame in islice(frame_generator(thread), MAX_FRAMES):
×
73
        # Source is an empty string if data isn't available
74
        source = frame.get("file") or ""
×
75
        if frame.get("line"):
×
76
            source += ":{}".format(frame["line"])
×
77

78
        signature = frame.get("signature") or ""
×
79

80
        # Remove function arguments
81
        if not signature.startswith("(unloaded"):
×
82
            signature = re.sub(r"\(.*\)", "", signature)
×
83

84
        frames.append(
×
85
            {
86
                "frame": frame.get("frame", "?"),
87
                "module": frame.get("module") or "?",
88
                "signature": signature,
89
                "source": source,
90
            }
91
        )
92

93
    return frames
×
94

95

96
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/utils.py#L343-L359
97
def enhance_json_dump(dump, vcs_mappings):
×
98
    """
99
    Add some information to the stackwalker's json_dump output
100
    for display. Mostly applying vcs_mappings to stack frames.
101
    """
102
    for thread_index, thread in enumerate(dump.get("threads", [])):
×
103
        if "thread" not in thread:
×
104
            thread["thread"] = thread_index
×
105

106
        frames = thread["frames"]
×
107
        for frame in frames:
×
108
            enhance_frame(frame, vcs_mappings)
×
109
            for inline in frame.get("inlines") or []:
×
110
                enhance_frame(inline, vcs_mappings)
×
111

112
        thread["frames"] = frames
×
113
    return dump
×
114

115

116
# https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/utils.py#L259-L340
117
def enhance_frame(frame, vcs_mappings):
×
118
    """Add additional info to a stack frame
119

120
    This adds signature and source links from vcs_mappings.
121

122
    """
123
    # If this is a truncation frame, then we don't need to enhance it in any way
124
    if frame.get("truncated") is not None:
×
125
        return
×
126

127
    if frame.get("function"):
×
128
        # Remove spaces before all stars, ampersands, and commas
129
        function = re.sub(r" (?=[\*&,])", "", frame["function"])
×
130
        # Ensure a space after commas
131
        function = re.sub(r",(?! )", ", ", function)
×
132
        frame["function"] = function
×
133
        signature = function
×
134
    elif frame.get("file") and frame.get("line"):
×
135
        signature = "%s#%d" % (frame["file"], frame["line"])
×
136
    elif frame.get("module") and frame.get("module_offset"):
×
137
        signature = "%s@%s" % (
×
138
            frame["module"],
139
            strip_leading_zeros(frame["module_offset"]),
140
        )
141
    elif frame.get("unloaded_modules"):
×
142
        first_module = frame["unloaded_modules"][0]
×
143
        if first_module.get("offsets"):
×
144
            signature = "(unloaded %s@%s)" % (
×
145
                first_module.get("module") or "",
146
                strip_leading_zeros(first_module.get("offsets")[0]),
147
            )
148
        else:
149
            signature = "(unloaded %s)" % first_module
×
150
    else:
151
        signature = "@%s" % frame["offset"]
×
152

153
    frame["signature"] = signature
×
154
    if signature.startswith("(unloaded"):
×
155
        # If the signature is based on an unloaded module, leave the string as is
156
        frame["short_signature"] = signature
×
157
    else:
158
        # Remove arguments which are enclosed in parens
159
        frame["short_signature"] = re.sub(r"\(.*\)", "", signature)
×
160

161
    if frame.get("file"):
×
162
        vcsinfo = frame["file"].split(":")
×
163
        if len(vcsinfo) == 4:
×
164
            vcstype, root, vcs_source_file, revision = vcsinfo
×
165
            if "/" in root:
×
166
                # The root is something like 'hg.mozilla.org/mozilla-central'
167
                server, repo = root.split("/", 1)
×
168
            else:
169
                # E.g. 'gecko-generated-sources' or something without a '/'
170
                repo = server = root
×
171

172
            if (
×
173
                vcs_source_file.count("/") > 1
174
                and len(vcs_source_file.split("/")[0]) == 128
175
            ):
176
                # In this case, the 'vcs_source_file' will be something like
177
                # '{SHA-512 hex}/ipc/ipdl/PCompositorBridgeChild.cpp'
178
                # So drop the sha part for the sake of the 'file' because
179
                # we don't want to display a 128 character hex code in the
180
                # hyperlink text.
181
                vcs_source_file_display = "/".join(vcs_source_file.split("/")[1:])
×
182
            else:
183
                # Leave it as is if it's not unwieldy long.
184
                vcs_source_file_display = vcs_source_file
×
185

186
            if vcstype in vcs_mappings:
×
187
                if server in vcs_mappings[vcstype]:
×
188
                    link = vcs_mappings[vcstype][server]
×
189
                    frame["file"] = vcs_source_file_display
×
190
                    frame["source_link"] = link % {
×
191
                        "repo": repo,
192
                        "file": vcs_source_file,
193
                        "revision": revision,
194
                        "line": frame["line"],
195
                    }
196
            else:
197
                path_parts = vcs_source_file.split("/")
×
198
                frame["file"] = path_parts.pop()
×
199

200

201
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/socorro/signature/utils.py#L405-L422
202
def strip_leading_zeros(text):
×
203
    """Strips leading zeros from a hex string.
204

205
    Example:
206

207
    >>> strip_leading_zeros("0x0000000000032ec0")
208
    "0x32ec0"
209

210
    :param text: the text to strip leading zeros from
211

212
    :returns: stripped text
213

214
    """
215
    try:
×
216
        return hex(int(text, base=16))
×
217
    except (ValueError, TypeError):
×
218
        return text
×
219

220

221
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/settings/base.py#L268-L293
222
# Link to source if possible
223
VCS_MAPPINGS = {
×
224
    "cvs": {
225
        "cvs.mozilla.org": (
226
            "http://bonsai.mozilla.org/cvsblame.cgi?file=%(file)s&rev=%(revision)s&mark=%(line)s#%(line)s"
227
        )
228
    },
229
    "hg": {
230
        "hg.mozilla.org": (
231
            "https://hg.mozilla.org/%(repo)s/file/%(revision)s/%(file)s#l%(line)s"
232
        )
233
    },
234
    "git": {
235
        "git.mozilla.org": (
236
            "http://git.mozilla.org/?p=%(repo)s;a=blob;f=%(file)s;h=%(revision)s#l%(line)s"
237
        ),
238
        "github.com": (
239
            "https://github.com/%(repo)s/blob/%(revision)s/%(file)s#L%(line)s"
240
        ),
241
    },
242
    "s3": {
243
        "gecko-generated-sources": (
244
            "/sources/highlight/?url=https://gecko-generated-sources.s3.amazonaws.com/%(file)s&line=%(line)s#L-%(line)s"
245
        )
246
    },
247
}
248

249

250
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/views.py#L141-L153
251
def get_parsed_dump(report):
×
252
    # For C++/Rust crashes
253
    if "json_dump" in report:
×
254
        json_dump = report["json_dump"]
×
255

256
        # This is for displaying on the "Details" tab
257
        enhance_json_dump(json_dump, VCS_MAPPINGS)
×
258
        parsed_dump = json_dump
×
259
    else:
260
        parsed_dump = {}
×
261

262
    return parsed_dump
×
263

264

265
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/views.py#L155-L160
266
def get_crashing_thread(report):
×
267
    if report["signature"].startswith("shutdownhang"):
×
268
        # For shutdownhang signatures, we want to use thread 0 as the crashing thread,
269
        # because that's the thread that actually contains the useful data about what
270
        # happened.
271
        return 0
×
272

273
    return report.get("crashing_thread")
×
274

275

276
# Original Socorro code: https://github.com/mozilla-services/socorro/blob/ff8f5d6b41689e34a6b800577d8ffe383e1e62eb/webapp/crashstats/crashstats/utils.py#L73-L195
277
class SignatureStats:
×
278
    def __init__(
×
279
        self,
280
        signature,
281
        num_total_crashes,
282
        rank=0,
283
        platforms=None,
284
        previous_signature=None,
285
    ):
286
        self.signature = signature
×
287
        self.num_total_crashes = num_total_crashes
×
288
        self.rank = rank
×
289
        self.platforms = platforms
×
290
        self.previous_signature = previous_signature
×
291

292
    @cached_property
×
293
    def platform_codes(self):
×
294
        return [x["short_name"] for x in self.platforms if x["short_name"] != "unknown"]
×
295

296
    @cached_property
×
297
    def signature_term(self):
×
298
        return self.signature["term"]
×
299

300
    @cached_property
×
301
    def percent_of_total_crashes(self):
×
302
        return 100.0 * self.signature["count"] / self.num_total_crashes
×
303

304
    @cached_property
×
305
    def num_crashes(self):
×
306
        return self.signature["count"]
×
307

308
    @cached_property
×
309
    def num_crashes_per_platform(self):
×
310
        num_crashes_per_platform = {
×
311
            platform + "_count": 0 for platform in self.platform_codes
312
        }
313
        for platform in self.signature["facets"]["platform"]:
×
314
            code = platform["term"][:3].lower()
×
315
            if code in self.platform_codes:
×
316
                num_crashes_per_platform[code + "_count"] = platform["count"]
×
317
        return num_crashes_per_platform
×
318

319
    @cached_property
×
320
    def num_crashes_in_garbage_collection(self):
×
321
        num_crashes_in_garbage_collection = 0
×
322
        for row in self.signature["facets"]["is_garbage_collecting"]:
×
323
            if row["term"].lower() == "t":
×
324
                num_crashes_in_garbage_collection = row["count"]
×
325
        return num_crashes_in_garbage_collection
×
326

327
    @cached_property
×
328
    def num_installs(self):
×
329
        return self.signature["facets"]["cardinality_install_time"]["value"]
×
330

331
    @cached_property
×
332
    def percent_of_total_crashes_diff(self):
×
333
        if self.previous_signature:
×
334
            # The number should go "up" when moving towards 100 and "down" when moving
335
            # towards 0
336
            return (
×
337
                self.percent_of_total_crashes
338
                - self.previous_signature.percent_of_total_crashes
339
            )
340
        return "new"
×
341

342
    @cached_property
×
343
    def rank_diff(self):
×
344
        if self.previous_signature:
×
345
            # The number should go "up" when moving towards 1 and "down" when moving
346
            # towards infinity
347
            return self.previous_signature.rank - self.rank
×
348
        return 0
×
349

350
    @cached_property
×
351
    def previous_percent_of_total_crashes(self):
×
352
        if self.previous_signature:
×
353
            return self.previous_signature.percent_of_total_crashes
×
354
        return 0
×
355

356
    @cached_property
×
357
    def num_startup_crashes(self):
×
358
        return sum(
×
359
            row["count"]
360
            for row in self.signature["facets"]["startup_crash"]
361
            if row["term"] in ("T", "1")
362
        )
363

364
    @cached_property
×
365
    def is_startup_crash(self):
×
366
        return self.num_startup_crashes == self.num_crashes
×
367

368
    @cached_property
×
369
    def is_potential_startup_crash(self):
×
370
        return (
×
371
            self.num_startup_crashes > 0 and self.num_startup_crashes < self.num_crashes
372
        )
373

374
    @cached_property
×
375
    def is_startup_window_crash(self):
×
376
        is_startup_window_crash = False
×
377
        for row in self.signature["facets"]["histogram_uptime"]:
×
378
            # Aggregation buckets use the lowest value of the bucket as
379
            # term. So for everything between 0 and 60 excluded, the
380
            # term will be `0`.
381
            if row["term"] < 60:
×
382
                ratio = 1.0 * row["count"] / self.num_crashes
×
383
                is_startup_window_crash = ratio > 0.5
×
384
        return is_startup_window_crash
×
385

386
    @cached_property
×
387
    def is_plugin_crash(self):
×
388
        for row in self.signature["facets"]["process_type"]:
×
389
            if row["term"].lower() == "plugin":
×
390
                return row["count"] > 0
×
391
        return False
×
392

393
    @cached_property
×
394
    def is_startup_related_crash(self):
×
395
        return (
×
396
            self.is_startup_crash
397
            or self.is_potential_startup_crash
398
            or self.is_startup_window_crash
399
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc