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

mozilla / relman-auto-nag / #3917

pending completion
#3917

push

coveralls-python

suhaibmujahid
Handle errors when creating variant bugs

542 of 3044 branches covered (17.81%)

6 of 6 new or added lines in 1 file covered. (100.0%)

1759 of 7897 relevant lines covered (22.27%)

0.22 hits per line

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

0.0
/auto_nag/scripts/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 auto_nag import utils
×
10
from auto_nag.bzcleaner import BzCleaner
×
11
from auto_nag.constants import HIGH_SEVERITY
×
12
from auto_nag.history import History
×
13
from auto_nag.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 autonag 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
        self.topcrash_signatures = topcrash.get_signatures()
×
43
        self.blocked_signatures = topcrash.get_blocked_signatures()
×
44
        self.oldest_severity_change_date = lmdutils.get_date(
×
45
            "today", oldest_severity_change_days
46
        )
47
        self.oldest_topcrash_added_date = lmdutils.get_date(
×
48
            "today", oldest_topcrash_added_days
49
        )
50

51
    def description(self):
×
52
        return "Bugs with small crash volume"
×
53

54
    def columns(self):
×
55
        return ["id", "summary", "severity", "deleted_keywords_count"]
×
56

57
    def _get_last_topcrash_added(self, bug):
×
58
        pass
×
59

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

71
            new_criteria.append(criterion)
×
72

73
        return new_criteria
×
74

75
    def handle_bug(self, bug, data):
×
76
        bugid = str(bug["id"])
×
77

78
        if self._is_topcrash_recently_added(bug):
×
79
            return None
×
80

81
        signatures = utils.get_signatures(bug["cf_crash_signature"])
×
82

83
        if any(signature in self.blocked_signatures for signature in signatures):
×
84
            # Ignore those bugs as we can't be sure.
85
            return None
×
86

87
        top_crash_signatures = [
×
88
            signature
89
            for signature in signatures
90
            if signature in self.topcrash_signatures
91
        ]
92

93
        keep_topcrash_startup = any(
×
94
            any(
95
                criterion["is_startup"]
96
                for criterion in self.topcrash_signatures[signature]
97
            )
98
            for signature in top_crash_signatures
99
        )
100

101
        keywords_to_remove = None
×
102
        if not top_crash_signatures:
×
103
            keywords_to_remove = set(bug["keywords"]) & {"topcrash", "topcrash-startup"}
×
104
        elif not keep_topcrash_startup:
×
105
            keywords_to_remove = set(bug["keywords"]) & {"topcrash-startup"}
×
106
        else:
107
            return None
×
108

109
        data[bugid] = {
×
110
            "severity": bug["severity"],
111
            "ignore_severity": (
112
                bug["severity"] not in HIGH_SEVERITY
113
                or bug["groups"] == "security"
114
                or any(keyword.startswith("sec-") for keyword in bug["keywords"])
115
                or "[fuzzblocker]" in bug["whiteboard"]
116
                or self._is_severity_recently_changed_by_human_or_autonag(bug)
117
                or self._has_severity_downgrade_comment(bug)
118
            ),
119
            "keywords_to_remove": keywords_to_remove,
120
            "signatures": signatures,
121
        }
122

123
        return bug
×
124

125
    def _get_low_volume_crash_signatures(self, bugs: Dict[str, dict]) -> set:
×
126
        """From the provided bugs, return the list of signatures that have a
127
        low crash volume.
128
        """
129

130
        signatures = {
×
131
            signature
132
            for bug in bugs.values()
133
            if not bug["ignore_severity"]
134
            for signature in bug["signatures"]
135
        }
136

137
        if not signatures:
×
138
            return set()
×
139

140
        signature_volume = Topcrash().fetch_signature_volume(signatures)
×
141

142
        low_volume_signatures = {
×
143
            signature
144
            for signature, volume in signature_volume.items()
145
            if volume < self.min_crash_volume
146
        }
147

148
        return low_volume_signatures
×
149

150
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
151
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
152
        self.set_autofix(bugs)
×
153

154
        # Keep only bugs with an autofix
155
        bugs = {
×
156
            bugid: bug for bugid, bug in bugs.items() if bugid in self.autofix_changes
157
        }
158

159
        return bugs
×
160

161
    def set_autofix(self, bugs):
×
162
        """Set the autofix for each bug."""
163

164
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
165

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

180
            if not bug["ignore_severity"] and all(
×
181
                signature in low_volume_signatures for signature in bug["signatures"]
182
            ):
183
                reasons.append(
×
184
                    f"Since the crash volume is low (less than {self.min_crash_volume} per week), "
185
                    "the severity is downgraded to `S3`. "
186
                    "Feel free to change it back if you think the bug is still critical."
187
                )
188
                autofix["severity"] = "S3"
×
189
                bug["severity"] += " → " + autofix["severity"]
×
190

191
            if autofix:
×
192
                bug["deleted_keywords_count"] = (
×
193
                    len(bug["keywords_to_remove"]) if bug["keywords_to_remove"] else "-"
194
                )
195
                reasons.append(self.get_documentation())
×
196
                autofix["comment"] = {
×
197
                    "body": "\n\n".join(reasons),
198
                }
199
                self.autofix_changes[bugid] = autofix
×
200

201
    @staticmethod
×
202
    def _has_severity_downgrade_comment(bug):
203
        for comment in reversed(bug["comments"]):
×
204
            if (
×
205
                comment["creator"] == History.BOT
206
                and "the severity is downgraded to" in comment["raw_text"]
207
            ):
208
                return True
×
209
        return False
×
210

211
    def _is_topcrash_recently_added(self, bug: dict):
×
212
        """Return True if the topcrash keyword was added recently."""
213

214
        for entry in reversed(bug["history"]):
×
215
            if entry["when"] < self.oldest_topcrash_added_date:
×
216
                break
×
217

218
            for change in entry["changes"]:
×
219
                if change["field_name"] == "keywords" and "topcrash" in change["added"]:
×
220
                    return True
×
221

222
        return False
×
223

224
    def _is_severity_recently_changed_by_human_or_autonag(self, bug):
×
225
        for entry in reversed(bug["history"]):
×
226
            if entry["when"] < self.oldest_severity_change_date:
×
227
                break
×
228

229
            # We ignore bot changes except for autonag
230
            if utils.is_bot_email(entry["who"]) and entry["who"] not in (
×
231
                "autonag-nomail-bot@mozilla.tld",
232
                "release-mgmt-account-bot@mozilla.tld",
233
            ):
234
                continue
×
235

236
            if any(change["field_name"] == "severity" for change in entry["changes"]):
×
237
                return True
×
238

239
        return False
×
240

241
    def get_bz_params(self, date):
×
242
        fields = [
×
243
            "severity",
244
            "keywords",
245
            "whiteboard",
246
            "cf_crash_signature",
247
            "comments.raw_text",
248
            "comments.creator",
249
            "history",
250
        ]
251
        params = {
×
252
            "include_fields": fields,
253
            "resolution": "---",
254
            "f1": "OP",
255
            "j1": "OR",
256
            "f2": "keywords",
257
            "o2": "anywords",
258
            "v2": ["topcrash", "topcrash-startup"],
259
            "f3": "OP",
260
            "j3": "AND",
261
            "f4": "bug_severity",
262
            "o4": "anyexact",
263
            "v4": list(HIGH_SEVERITY),
264
            "f6": "cf_crash_signature",
265
            "o6": "isnotempty",
266
            "f7": "CP",
267
            "f8": "CP",
268
            "f9": "creation_ts",
269
            "o9": "lessthan",
270
            "v9": "-1w",
271
        }
272

273
        return params
×
274

275

276
if __name__ == "__main__":
×
277
    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