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

mozilla / relman-auto-nag / #5065

05 Jun 2024 08:01PM CUT coverage: 21.853% (-0.005%) from 21.858%
#5065

push

coveralls-python

benjaminmah
Moved the needinfo flag ID collection logic to `handle_bug()`

716 of 3612 branches covered (19.82%)

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

1 existing line in 1 file now uncovered.

1932 of 8841 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

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

UNCOV
131
        return bug
×
132

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

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

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

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

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

156
        return low_volume_signatures
×
157

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

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

167
        return bugs
×
168

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

172
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
173

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

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

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

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

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

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

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

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

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

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

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

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

265
        return False
×
266

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

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

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

282
        return False
×
283

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

317
        return params
×
318

319

320
if __name__ == "__main__":
×
321
    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