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

mozilla / relman-auto-nag / #5067

06 Jun 2024 02:01PM CUT coverage: 21.85% (-0.003%) from 21.853%
#5067

push

coveralls-python

benjaminmah
Pass the needinfo flags as a list, even if it does not remove `topcrash` keyword

716 of 3612 branches covered (19.82%)

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

1932 of 8842 relevant lines covered (21.85%)

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
        }
126

NEW
127
        data[bugid]["needinfo_ids"] = []
×
128

129
        # Add needinfo IDs only if the keyword to remove is "topcrash"
130
        if "topcrash" in keywords_to_remove:
×
131
            data[bugid]["needinfo_ids"] = self.get_needinfo_topcrash_ids(bug)
×
132

133
        return bug
×
134

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

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

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

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

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

158
        return low_volume_signatures
×
159

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

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

169
        return bugs
×
170

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

174
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
175

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

190
                # Clear needinfo flags requested by BugBot relating to increasing severity
191
                if "topcrash" in bug["keywords_to_remove"]:
×
192
                    autofix["flags"] = [
×
193
                        {
194
                            "id": flag_id,
195
                            "status": "X",
196
                        }
197
                        for flag_id in bug.get("needinfo_ids", [])
198
                    ]
199

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

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

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

229
        needinfo_comment = (
×
230
            "could you consider increasing the severity of this top-crash bug?"
231
        )
232

233
        severity_comment_times = [
×
234
            comment["creation_time"]
235
            for comment in bug["comments"]
236
            if comment["creator"] == History.BOT
237
            and needinfo_comment in comment["raw_text"]
238
        ]
239

240
        return [
×
241
            flag["id"]
242
            for flag in needinfo_flags
243
            if flag["creation_date"] in severity_comment_times
244
        ]
245

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

256
    def _is_topcrash_recently_added(self, bug: dict):
×
257
        """Return True if the topcrash keyword was added recently."""
258

259
        for entry in reversed(bug["history"]):
×
260
            if entry["when"] < self.oldest_topcrash_added_date:
×
261
                break
×
262

263
            for change in entry["changes"]:
×
264
                if change["field_name"] == "keywords" and "topcrash" in change["added"]:
×
265
                    return True
×
266

267
        return False
×
268

269
    def _is_severity_recently_changed_by_human_or_bugbot(self, bug):
×
270
        for entry in reversed(bug["history"]):
×
271
            if entry["when"] < self.oldest_severity_change_date:
×
272
                break
×
273

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

281
            if any(change["field_name"] == "severity" for change in entry["changes"]):
×
282
                return True
×
283

284
        return False
×
285

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

319
        return params
×
320

321

322
if __name__ == "__main__":
×
323
    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