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

mozilla / relman-auto-nag / #5070

06 Jun 2024 04:59PM CUT coverage: 21.86%. Remained the same
#5070

push

coveralls-python

benjaminmah
Cleaned up code

716 of 3608 branches covered (19.84%)

1932 of 8838 relevant lines covered (21.86%)

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

5
from typing import Dict
×
6

7
from libmozdata import utils as lmdutils
×
8

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

15
MAX_SIGNATURES_PER_QUERY = 30
×
16

17

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

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

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

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

55
    def description(self):
×
56
        return "Bugs with small crash volume"
×
57

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

61
    def _get_last_topcrash_added(self, bug):
×
62
        pass
×
63

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

75
            new_criteria.append(criterion)
×
76

77
        return new_criteria
×
78

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

82
        if self._is_topcrash_recently_added(bug):
×
83
            return None
×
84

85
        signatures = utils.get_signatures(bug["cf_crash_signature"])
×
86

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

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

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

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

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

132
        return bug
×
133

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

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

146
        if not signatures:
×
147
            return set()
×
148

149
        signature_volume = Topcrash().fetch_signature_volume(signatures)
×
150

151
        low_volume_signatures = {
×
152
            signature
153
            for signature, volume in signature_volume.items()
154
            if volume < self.min_crash_volume
155
        }
156

157
        return low_volume_signatures
×
158

159
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
160
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
161
        self.set_autofix(bugs)
×
162

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

168
        return bugs
×
169

170
    def set_autofix(self, bugs):
×
171
        """Set the autofix for each bug."""
172

173
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
174

175
        for bugid, bug in bugs.items():
×
176
            autofix = {}
×
177
            reasons = []
×
178
            if bug["keywords_to_remove"]:
×
179
                reasons.append(
×
180
                    "Based on the [topcrash criteria](https://wiki.mozilla.org/CrashKill/Topcrash), the crash "
181
                    + (
182
                        "signature linked to this bug is not a topcrash signature anymore."
183
                        if len(bug["signatures"]) == 1
184
                        else "signatures linked to this bug are not in the topcrash signatures anymore."
185
                    )
186
                )
187
                autofix["keywords"] = {"remove": list(bug["keywords_to_remove"])}
×
188
                autofix["flags"] = [
×
189
                    {
190
                        "id": flag_id,
191
                        "status": "X",
192
                    }
193
                    for flag_id in bug.get("needinfo_ids", [])
194
                ]
195

196
            if not bug["ignore_severity"] and all(
×
197
                signature in low_volume_signatures for signature in bug["signatures"]
198
            ):
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
                )
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
                )
211
                reasons.append(self.get_documentation())
×
212
                autofix["comment"] = {
×
213
                    "body": "\n\n".join(reasons),
214
                }
215
                self.autofix_changes[bugid] = autofix
×
216

217
    def get_needinfo_topcrash_ids(self, bug: dict) -> list[int]:
×
218
        """Get the IDs of the needinfo flags requested by the bot regarding increasing the severity."""
219
        needinfo_flags = [
×
220
            flag
221
            for flag in bug.get("flags", [])
222
            if flag["name"] == "needinfo" and flag["requestee"] == History.BOT
223
        ]
224

225
        needinfo_comment = (
×
226
            "could you consider increasing the severity of this top-crash bug?"
227
        )
228

229
        severity_comment_times = [
×
230
            comment["creation_time"]
231
            for comment in bug["comments"]
232
            if comment["creator"] == History.BOT
233
            and needinfo_comment in comment["raw_text"]
234
        ]
235

236
        return [
×
237
            flag["id"]
238
            for flag in needinfo_flags
239
            if flag["creation_date"] in severity_comment_times
240
        ]
241

242
    @staticmethod
×
243
    def _has_severity_downgrade_comment(bug):
×
244
        for comment in reversed(bug["comments"]):
×
245
            if (
×
246
                comment["creator"] == History.BOT
247
                and "the severity is downgraded to" in comment["raw_text"]
248
            ):
249
                return True
×
250
        return False
×
251

252
    def _is_topcrash_recently_added(self, bug: dict):
×
253
        """Return True if the topcrash keyword was added recently."""
254

255
        for entry in reversed(bug["history"]):
×
256
            if entry["when"] < self.oldest_topcrash_added_date:
×
257
                break
×
258

259
            for change in entry["changes"]:
×
260
                if change["field_name"] == "keywords" and "topcrash" in change["added"]:
×
261
                    return True
×
262

263
        return False
×
264

265
    def _is_severity_recently_changed_by_human_or_bugbot(self, bug):
×
266
        for entry in reversed(bug["history"]):
×
267
            if entry["when"] < self.oldest_severity_change_date:
×
268
                break
×
269

270
            # We ignore bot changes except for bugbot
271
            if utils.is_bot_email(entry["who"]) and entry["who"] not in (
×
272
                "autonag-nomail-bot@mozilla.tld",
273
                "release-mgmt-account-bot@mozilla.tld",
274
            ):
275
                continue
×
276

277
            if any(change["field_name"] == "severity" for change in entry["changes"]):
×
278
                return True
×
279

280
        return False
×
281

282
    def get_bz_params(self, date):
×
283
        fields = [
×
284
            "severity",
285
            "keywords",
286
            "whiteboard",
287
            "cf_crash_signature",
288
            "comments.raw_text",
289
            "comments.creator",
290
            "comments.creation_time",
291
            "history",
292
        ]
293
        params = {
×
294
            "include_fields": fields,
295
            "resolution": "---",
296
            "f1": "OP",
297
            "j1": "OR",
298
            "f2": "keywords",
299
            "o2": "anywords",
300
            "v2": ["topcrash", "topcrash-startup"],
301
            "f3": "OP",
302
            "j3": "AND",
303
            "f4": "bug_severity",
304
            "o4": "anyexact",
305
            "v4": list(HIGH_SEVERITY),
306
            "f6": "cf_crash_signature",
307
            "o6": "isnotempty",
308
            "f7": "CP",
309
            "f8": "CP",
310
            "f9": "creation_ts",
311
            "o9": "lessthan",
312
            "v9": "-1w",
313
        }
314

315
        return params
×
316

317

318
if __name__ == "__main__":
×
319
    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