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

mozilla / relman-auto-nag / #5068

06 Jun 2024 02:33PM CUT coverage: 21.855% (+0.005%) from 21.85%
#5068

push

coveralls-python

benjaminmah
Changed the conditional for populating the `needinfo_ids` field

716 of 3610 branches covered (19.83%)

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

1932 of 8840 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
        }
126

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

134
        return bug
×
135

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

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

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

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

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

159
        return low_volume_signatures
×
160

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

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

170
        return bugs
×
171

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

175
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
176

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

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

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

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

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

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

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

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

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

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

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

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

268
        return False
×
269

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

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

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

285
        return False
×
286

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

320
        return params
×
321

322

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