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

mozilla / relman-auto-nag / #5028

23 May 2024 04:00PM CUT coverage: 21.802% (-0.08%) from 21.886%
#5028

push

coveralls-python

benjaminmah
Added search to find `needinfo` flag created closest to the comment regarding increasing severity

716 of 3622 branches covered (19.77%)

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

30 existing lines in 1 file now uncovered.

1933 of 8866 relevant lines covered (21.8%)

0.22 hits per line

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

0.0
/bugbot/rules/crash_small_volume.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

NEW
5
from datetime import datetime
×
UNCOV
6
from typing import Dict
×
7

NEW
8
import requests
×
UNCOV
9
from libmozdata import utils as lmdutils
×
10

11
from bugbot import utils
×
12
from bugbot.bzcleaner import BzCleaner
×
13
from bugbot.constants import HIGH_SEVERITY, SECURITY_KEYWORDS
×
14
from bugbot.history import History
×
15
from bugbot.topcrash import TOP_CRASH_IDENTIFICATION_CRITERIA, Topcrash
×
16

17
MAX_SIGNATURES_PER_QUERY = 30
×
18

19

20
class CrashSmallVolume(BzCleaner):
×
21
    def __init__(
×
22
        self,
23
        min_crash_volume: int = 15,
24
        oldest_severity_change_days: int = 30,
25
        oldest_topcrash_added_days: int = 21,
26
    ):
27
        """Constructor.
28

29
        Args:
30
            min_crash_volume: the minimum number of crashes per week for a
31
                signature to not be considered low volume.
32
            oldest_severity_change_days: if the bug severity has been changed by
33
                a human or bugbot in the last X days, we will not downgrade the
34
                severity to `S3`.
35
            oldest_topcrash_added_days: if the bug has been marked as topcrash
36
                in the last X days, we will ignore it.
37
        """
38
        super().__init__()
×
39

40
        self.min_crash_volume = min_crash_volume
×
41
        topcrash = Topcrash(
×
42
            criteria=self._adjust_topcrash_criteria(TOP_CRASH_IDENTIFICATION_CRITERIA)
43
        )
44
        assert (
×
45
            topcrash.min_crashes >= min_crash_volume
46
        ), "min_crash_volume should not be higher than the min_crashes used for the topcrash criteria"
47

48
        self.topcrash_signatures = topcrash.get_signatures()
×
49
        self.blocked_signatures = topcrash.get_blocked_signatures()
×
50
        self.oldest_severity_change_date = lmdutils.get_date(
×
51
            "today", oldest_severity_change_days
52
        )
53
        self.oldest_topcrash_added_date = lmdutils.get_date(
×
54
            "today", oldest_topcrash_added_days
55
        )
56

57
    def description(self):
×
58
        return "Bugs with small crash volume"
×
59

60
    def columns(self):
×
61
        return ["id", "summary", "severity", "deleted_keywords_count"]
×
62

63
    def _get_last_topcrash_added(self, bug):
×
64
        pass
×
65

66
    def _adjust_topcrash_criteria(self, topcrash_criteria):
×
67
        factor = 2
×
68
        new_criteria = []
×
69
        for criterion in topcrash_criteria:
×
70
            criterion = {
×
71
                **criterion,
72
                "tc_limit": criterion["tc_limit"] * factor,
73
            }
74
            if "tc_startup_limit" in criterion:
×
75
                criterion["tc_startup_limit"] = criterion["tc_startup_limit"] * factor
×
76

77
            new_criteria.append(criterion)
×
78

79
        return new_criteria
×
80

81
    def handle_bug(self, bug, data):
×
82
        bugid = str(bug["id"])
×
83

84
        if self._is_topcrash_recently_added(bug):
×
85
            return None
×
86

87
        signatures = utils.get_signatures(bug["cf_crash_signature"])
×
88

89
        if any(signature in self.blocked_signatures for signature in signatures):
×
90
            # Ignore those bugs as we can't be sure.
91
            return None
×
92

93
        top_crash_signatures = [
×
94
            signature
95
            for signature in signatures
96
            if signature in self.topcrash_signatures
97
        ]
98

99
        keep_topcrash_startup = any(
×
100
            any(
101
                criterion["is_startup"]
102
                for criterion in self.topcrash_signatures[signature]
103
            )
104
            for signature in top_crash_signatures
105
        )
106

107
        keywords_to_remove = None
×
108
        if not top_crash_signatures:
×
109
            keywords_to_remove = set(bug["keywords"]) & {"topcrash", "topcrash-startup"}
×
110
        elif not keep_topcrash_startup:
×
111
            keywords_to_remove = set(bug["keywords"]) & {"topcrash-startup"}
×
112
        else:
113
            return None
×
114

115
        data[bugid] = {
×
116
            "severity": bug["severity"],
117
            "ignore_severity": (
118
                bug["severity"] not in HIGH_SEVERITY
119
                or bug["groups"] == "security"
120
                or any(keyword in SECURITY_KEYWORDS for keyword in bug["keywords"])
121
                or "[fuzzblocker]" in bug["whiteboard"]
122
                or self._is_severity_recently_changed_by_human_or_bugbot(bug)
123
                or self._has_severity_downgrade_comment(bug)
124
            ),
125
            "keywords_to_remove": keywords_to_remove,
126
            "signatures": signatures,
127
        }
128

129
        return bug
×
130

131
    def _get_low_volume_crash_signatures(self, bugs: Dict[str, dict]) -> set:
×
132
        """From the provided bugs, return the list of signatures that have a
133
        low crash volume.
134
        """
135

136
        signatures = {
×
137
            signature
138
            for bug in bugs.values()
139
            if not bug["ignore_severity"]
140
            for signature in bug["signatures"]
141
        }
142

143
        if not signatures:
×
144
            return set()
×
145

146
        signature_volume = Topcrash().fetch_signature_volume(signatures)
×
147

148
        low_volume_signatures = {
×
149
            signature
150
            for signature, volume in signature_volume.items()
151
            if volume < self.min_crash_volume
152
        }
153

154
        return low_volume_signatures
×
155

156
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
157
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
158
        self.set_autofix(bugs)
×
159

160
        # Keep only bugs with an autofix
161
        bugs = {
×
162
            bugid: bug for bugid, bug in bugs.items() if bugid in self.autofix_changes
163
        }
164

165
        return bugs
×
166

167
    def set_autofix(self, bugs):
×
168
        """Set the autofix for each bug."""
169

170
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
171

172
        for bugid, bug in bugs.items():
×
173
            autofix = {}
×
174
            reasons = []
×
175
            if bug["keywords_to_remove"]:
×
176
                reasons.append(
×
177
                    "Based on the [topcrash criteria](https://wiki.mozilla.org/CrashKill/Topcrash), the crash "
178
                    + (
179
                        "signature linked to this bug is not a topcrash signature anymore."
180
                        if len(bug["signatures"]) == 1
181
                        else "signatures linked to this bug are not in the topcrash signatures anymore."
182
                    )
183
                )
184
                autofix["keywords"] = {"remove": list(bug["keywords_to_remove"])}
×
185

186
                # Clear needinfo flags requested by BugBot relating to increasing severity
UNCOV
187
                if "topcrash" in bug["keywords_to_remove"]:
×
UNCOV
188
                    autofix["flags"] = [
×
189
                        {
190
                            "id": flag_id,
191
                            "status": "X",
192
                        }
193
                        for flag_id in self.get_needinfo_topcrash_ids(bug)
194
                    ]
195

UNCOV
196
            if not bug["ignore_severity"] and all(
×
197
                signature in low_volume_signatures for signature in bug["signatures"]
198
            ):
UNCOV
199
                reasons.append(
×
200
                    f"Since the crash volume is low (less than {self.min_crash_volume} per week), "
201
                    "the severity is downgraded to `S3`. "
202
                    "Feel free to change it back if you think the bug is still critical."
203
                )
UNCOV
204
                autofix["severity"] = "S3"
×
205
                bug["severity"] += " → " + autofix["severity"]
×
206

207
            if autofix:
×
208
                bug["deleted_keywords_count"] = (
×
209
                    len(bug["keywords_to_remove"]) if bug["keywords_to_remove"] else "-"
210
                )
UNCOV
211
                reasons.append(self.get_documentation())
×
UNCOV
212
                autofix["comment"] = {
×
213
                    "body": "\n\n".join(reasons),
214
                }
215
                self.autofix_changes[bugid] = autofix
×
216

NEW
217
    def get_comments(self, bug_id: int) -> list[dict]:
×
218
        """
219
        Fetch comments for a given bug ID.
220
        """
NEW
221
        url = f"https://bugzilla.mozilla.org/rest/bug/{bug_id}/comment"
×
NEW
222
        response = requests.get(url)
×
223

NEW
224
        if response.status_code != 200:
×
NEW
225
            return []
×
226

NEW
227
        comments_data = response.json()
×
NEW
228
        if "bugs" not in comments_data or str(bug_id) not in comments_data["bugs"]:
×
NEW
229
            return []
×
230

NEW
231
        return comments_data["bugs"][str(bug_id)]["comments"]
×
232

UNCOV
233
    def get_needinfo_topcrash_ids(self, bug: dict) -> list[str]:
×
234
        """Get the IDs of the needinfo flags requested by the bot regarding increasing the severity."""
NEW
235
        needinfo_flags = [
×
236
            flag
237
            for flag in bug.get("flags", [])
238
            if flag["name"] == "needinfo" and flag["requestee"] == History.BOT
239
        ]
240

NEW
241
        if not needinfo_flags:
×
NEW
242
            return []
×
243

NEW
244
        needinfo_comment = (
×
245
            "could you consider increasing the severity of this top-crash bug?"
246
        )
NEW
247
        severity_comment_time = None
×
248

NEW
249
        for comment in self.get_comments(bug["id"]):
×
NEW
250
            if needinfo_comment in comment["raw_text"]:
×
NEW
251
                severity_comment_time = datetime.strptime(
×
252
                    comment["creation_time"], "%Y-%m-%dT%H:%M:%SZ"
253
                )
NEW
254
                break
×
255

NEW
256
        if not severity_comment_time:
×
NEW
257
            return []
×
258

NEW
259
        closest_flag = None
×
NEW
260
        smallest_time_diff = None
×
261

NEW
262
        for flag in needinfo_flags:
×
NEW
263
            flag_creation_time = datetime.strptime(
×
264
                flag["creation_date"], "%Y-%m-%dT%H:%M:%SZ"
265
            )
NEW
266
            time_diff = abs(
×
267
                (flag_creation_time - severity_comment_time).total_seconds()
268
            )
269

NEW
270
            if smallest_time_diff is None or time_diff < smallest_time_diff:
×
NEW
271
                closest_flag = flag
×
NEW
272
                smallest_time_diff = time_diff
×
273

NEW
274
        return [closest_flag["id"]] if closest_flag else []
×
275

276
    @staticmethod
×
277
    def _has_severity_downgrade_comment(bug):
×
UNCOV
278
        for comment in reversed(bug["comments"]):
×
279
            if (
×
280
                comment["creator"] == History.BOT
281
                and "the severity is downgraded to" in comment["raw_text"]
282
            ):
283
                return True
×
284
        return False
×
285

UNCOV
286
    def _is_topcrash_recently_added(self, bug: dict):
×
287
        """Return True if the topcrash keyword was added recently."""
288

UNCOV
289
        for entry in reversed(bug["history"]):
×
UNCOV
290
            if entry["when"] < self.oldest_topcrash_added_date:
×
291
                break
×
292

293
            for change in entry["changes"]:
×
294
                if change["field_name"] == "keywords" and "topcrash" in change["added"]:
×
UNCOV
295
                    return True
×
296

UNCOV
297
        return False
×
298

299
    def _is_severity_recently_changed_by_human_or_bugbot(self, bug):
×
UNCOV
300
        for entry in reversed(bug["history"]):
×
UNCOV
301
            if entry["when"] < self.oldest_severity_change_date:
×
UNCOV
302
                break
×
303

304
            # We ignore bot changes except for bugbot
UNCOV
305
            if utils.is_bot_email(entry["who"]) and entry["who"] not in (
×
306
                "autonag-nomail-bot@mozilla.tld",
307
                "release-mgmt-account-bot@mozilla.tld",
308
            ):
UNCOV
309
                continue
×
310

UNCOV
311
            if any(change["field_name"] == "severity" for change in entry["changes"]):
×
UNCOV
312
                return True
×
313

UNCOV
314
        return False
×
315

UNCOV
316
    def get_bz_params(self, date):
×
UNCOV
317
        fields = [
×
318
            "severity",
319
            "keywords",
320
            "whiteboard",
321
            "cf_crash_signature",
322
            "comments.raw_text",
323
            "comments.creator",
324
            "history",
325
        ]
UNCOV
326
        params = {
×
327
            "include_fields": fields,
328
            "resolution": "---",
329
            "f1": "OP",
330
            "j1": "OR",
331
            "f2": "keywords",
332
            "o2": "anywords",
333
            "v2": ["topcrash", "topcrash-startup"],
334
            "f3": "OP",
335
            "j3": "AND",
336
            "f4": "bug_severity",
337
            "o4": "anyexact",
338
            "v4": list(HIGH_SEVERITY),
339
            "f6": "cf_crash_signature",
340
            "o6": "isnotempty",
341
            "f7": "CP",
342
            "f8": "CP",
343
            "f9": "creation_ts",
344
            "o9": "lessthan",
345
            "v9": "-1w",
346
        }
347

UNCOV
348
        return params
×
349

350

UNCOV
351
if __name__ == "__main__":
×
UNCOV
352
    CrashSmallVolume().run()
×
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