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

mozilla / relman-auto-nag / #4876

27 Dec 2023 07:07AM CUT coverage: 21.897% (-0.002%) from 21.899%
#4876

push

coveralls-python

shubham-s-del
Unified way of accessing security keywords

716 of 3590 branches covered (0.0%)

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

1928 of 8805 relevant lines covered (21.9%)

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
×
NEW
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
        return bug
×
128

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

134
        signatures = {
×
135
            signature
136
            for bug in bugs.values()
137
            if not bug["ignore_severity"]
138
            for signature in bug["signatures"]
139
        }
140

141
        if not signatures:
×
142
            return set()
×
143

144
        signature_volume = Topcrash().fetch_signature_volume(signatures)
×
145

146
        low_volume_signatures = {
×
147
            signature
148
            for signature, volume in signature_volume.items()
149
            if volume < self.min_crash_volume
150
        }
151

152
        return low_volume_signatures
×
153

154
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
155
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
156
        self.set_autofix(bugs)
×
157

158
        # Keep only bugs with an autofix
159
        bugs = {
×
160
            bugid: bug for bugid, bug in bugs.items() if bugid in self.autofix_changes
161
        }
162

163
        return bugs
×
164

165
    def set_autofix(self, bugs):
×
166
        """Set the autofix for each bug."""
167

168
        low_volume_signatures = self._get_low_volume_crash_signatures(bugs)
×
169

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

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

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

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

215
    def _is_topcrash_recently_added(self, bug: dict):
×
216
        """Return True if the topcrash keyword was added recently."""
217

218
        for entry in reversed(bug["history"]):
×
219
            if entry["when"] < self.oldest_topcrash_added_date:
×
220
                break
×
221

222
            for change in entry["changes"]:
×
223
                if change["field_name"] == "keywords" and "topcrash" in change["added"]:
×
224
                    return True
×
225

226
        return False
×
227

228
    def _is_severity_recently_changed_by_human_or_bugbot(self, bug):
×
229
        for entry in reversed(bug["history"]):
×
230
            if entry["when"] < self.oldest_severity_change_date:
×
231
                break
×
232

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

240
            if any(change["field_name"] == "severity" for change in entry["changes"]):
×
241
                return True
×
242

243
        return False
×
244

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

277
        return params
×
278

279

280
if __name__ == "__main__":
×
281
    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