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

mozilla / relman-auto-nag / #5432

25 Feb 2025 02:50AM UTC coverage: 21.14% (-0.06%) from 21.2%
#5432

push

coveralls-python

web-flow
Add logic to check deeper for perfalert resolution comments (#2587)

426 of 2966 branches covered (14.36%)

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

3 existing lines in 1 file now uncovered.

1943 of 9191 relevant lines covered (21.14%)

0.21 hits per line

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

0.0
/bugbot/rules/perfalert_resolved_regression.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 datetime import timedelta
×
6

7
from libmozdata import utils as lmdutils
×
8
from libmozdata.bugzilla import BugzillaUser
×
9

10
from bugbot.bzcleaner import BzCleaner
×
11
from bugbot.constants import BOT_MAIN_ACCOUNT
×
NEW
12
from bugbot.utils import is_bot_email
×
13

NEW
14
RESOLUTION_KEYWORDS = (
×
15
    "backedout",
16
    "backed out",
17
    "back out",
18
    "backout",
19
    "wontfix",
20
    "invalid",
21
    "incomplete",
22
    "duplicate",
23
    "fixed",
24
    "resolved",
25
    "resolve",
26
    "resolution",
27
)
28

29

30
class PerfAlertResolvedRegression(BzCleaner):
×
NEW
31
    def __init__(self, max_seconds_before_status=86400):
×
32
        """
33
        Initializes the bugbot rule for ensuring performance alerts
34
        have a valid resolution comment when they are closed.
35

36
        max_seconds_before_status int: When a resolution comment is not provided
37
            at the time of resolution, the preceding comment can be considered as a
38
            resolution comment as long it hasn't been more than `max_seconds_before_status`
39
            seconds since the comment was made. Only applies when the resolution
40
            author is different from the preceding comment author, otherwise, the
41
            comment is accepted without checking the time that has elapsed.
42
        """
NEW
43
        super().__init__()
×
NEW
44
        self.max_seconds_before_status = max_seconds_before_status
×
UNCOV
45
        self.extra_ni = {}
×
46

47
    def description(self):
×
48
        return "PerfAlert regressions whose resolution has changed recently"
×
49

50
    def columns(self):
×
51
        return [
×
52
            "id",
53
            "summary",
54
            "status",
55
            "status_author",
56
            "resolution",
57
            "resolution_comment",
58
            "resolution_previous",
59
            "needinfo",
60
        ]
61

62
    def get_extra_for_needinfo_template(self):
×
63
        return self.extra_ni
×
64

65
    def get_bz_params(self, date):
×
66
        end_date = lmdutils.get_date_ymd("today")
×
67
        start_date = end_date - timedelta(1)
×
68

69
        fields = [
×
70
            "id",
71
            "history",
72
            "comments.text",
73
            "comments.creation_time",
74
            "comments.author",
75
        ]
76

77
        # Find all bugs that have perf-alert, and regression in their keywords. Search
78
        # for bugs that have been changed in the last day. Only look for bugs after
79
        # October 1st, 2024 to prevent triggering comments on older performance regressions
80
        params = {
×
81
            "include_fields": fields,
82
            "f3": "creation_ts",
83
            "o3": "greaterthan",
84
            "v3": "2024-10-01T00:00:00Z",
85
            "f1": "regressed_by",
86
            "o1": "isnotempty",
87
            "f2": "keywords",
88
            "o2": "allwords",
89
            "v2": ["regression", "perf-alert"],
90
            "f4": "resolution",
91
            "o4": "changedafter",
92
            "v4": start_date,
93
            "f5": "resolution",
94
            "o5": "changedbefore",
95
            "v5": end_date,
96
        }
97

98
        return params
×
99

100
    def should_needinfo(self, bug_comments, status_time):
×
101
        # Check if the bugbot has already needinfo'ed on the bug since
102
        # the last status change before making one
103
        for comment in bug_comments[::-1]:
×
104
            if comment["creation_time"] <= status_time:
×
105
                break
×
106

107
            if comment["author"] == BOT_MAIN_ACCOUNT:
×
108
                if (
×
109
                    "could you provide a comment explaining the resolution?"
110
                    in comment["text"]
111
                ):
112
                    # Bugbot has already commented on this bug since the last
113
                    # status change. No need to comment again since this was
114
                    # just a resolution change
115
                    return False
×
116

117
        return True
×
118

NEW
119
    def get_resolution_comments(self, comments, status_time):
×
NEW
120
        resolution_comment = None
×
NEW
121
        preceding_comment = None
×
122

NEW
123
        for comment in comments:
×
NEW
124
            if comment["creation_time"] > status_time:
×
NEW
125
                break
×
NEW
126
            if comment["creation_time"] == status_time:
×
NEW
127
                resolution_comment = comment
×
NEW
128
            if not is_bot_email(comment["author"]):
×
NEW
129
                preceding_comment = comment
×
130

NEW
131
        return resolution_comment, preceding_comment
×
132

NEW
133
    def get_resolution_comment(self, comments, bug_history):
×
NEW
134
        status_time = bug_history["status_time"]
×
NEW
135
        resolution_comment, preceding_comment = self.get_resolution_comments(
×
136
            comments, status_time
137
        )
138

NEW
139
        if (
×
140
            resolution_comment
141
            and resolution_comment["author"] == bug_history["status_author"]
142
        ):
143
            # Accept if status author provided a comment at the same time
NEW
144
            return resolution_comment["text"]
×
NEW
145
        if preceding_comment:
×
NEW
146
            if preceding_comment["author"] == bug_history["status_author"]:
×
147
                # Accept if status author provided a comment before setting
148
                # resolution
NEW
149
                return preceding_comment["text"]
×
150

NEW
151
            preceding_resolution_comment = f"{preceding_comment['text']} (provided by {preceding_comment['author']})"
×
NEW
152
            if any(
×
153
                keyword in preceding_comment["text"].lower()
154
                for keyword in RESOLUTION_KEYWORDS
155
            ):
156
                # Accept if a non-status author provided a comment before a
157
                # resolution was set, and hit some keywords
NEW
158
                return preceding_resolution_comment
×
NEW
159
            if (
×
160
                lmdutils.get_timestamp(status_time)
161
                - lmdutils.get_timestamp(preceding_comment["creation_time"])
162
            ) < self.max_seconds_before_status:
163
                # Accept if the previous comment from another author is
164
                # within the time limit
NEW
165
                return preceding_resolution_comment
×
166

NEW
167
        return None
×
168

169
    def get_resolution_history(self, bug):
×
170
        bug_info = {}
×
171

172
        # Get the last resolution change that was made in this bug
173
        for change in bug["history"][::-1]:
×
174
            # Get the most recent resolution change first, this is because
175
            # it could have changed since the status was changed and by who
176
            if not bug_info.get("resolution"):
×
177
                for specific_change in change["changes"]:
×
178
                    if specific_change["field_name"] == "resolution":
×
179
                        bug_info["resolution"] = specific_change["added"]
×
180
                        bug_info["resolution_previous"] = (
×
181
                            specific_change["removed"].strip() or "---"
182
                        )
183
                        bug_info["resolution_time"] = change["when"]
×
184
                        break
×
185

186
            if bug_info.get("resolution"):
×
187
                # Find the status that the bug was resolved to, and by who
188
                for specific_change in change["changes"]:
×
189
                    if specific_change["field_name"] == "status" and specific_change[
×
190
                        "added"
191
                    ] in ("RESOLVED", "REOPENED"):
192
                        bug_info["status"] = specific_change["added"]
×
193
                        bug_info["status_author"] = change["who"]
×
194
                        bug_info["status_time"] = change["when"]
×
195
                        break
×
196

197
            if bug_info.get("status"):
×
198
                break
×
199

200
        return bug_info
×
201

202
    def set_autofix(self, bugs):
×
203
        for bug_id, bug_info in bugs.items():
×
204
            if bug_info["needinfo"]:
×
205
                self.extra_ni[bug_id] = {
×
206
                    "resolution": bug_info["resolution"],
207
                    "status": bug_info["status"],
208
                }
209
                self.add_auto_ni(
×
210
                    bug_id,
211
                    {
212
                        "mail": bug_info["status_author"],
213
                        "nickname": bug_info["nickname"],
214
                    },
215
                )
216

217
    def get_needinfo_nicks(self, bugs):
×
218
        def _user_handler(user, data):
×
219
            data[user["name"]] = user["nick"]
×
220

221
        authors_to_ni = set()
×
222
        for bug_id, bug_info in bugs.items():
×
223
            if bug_info["needinfo"]:
×
224
                authors_to_ni.add(bug_info["status_author"])
×
225

226
        if not authors_to_ni:
×
227
            return
×
228

229
        user_emails_to_names = {}
×
230
        BugzillaUser(
×
231
            user_names=list(authors_to_ni),
232
            include_fields=["nick", "name"],
233
            user_handler=_user_handler,
234
            user_data=user_emails_to_names,
235
        ).wait()
236

237
        for bug_id, bug_info in bugs.items():
×
238
            if bug_info["needinfo"]:
×
239
                bug_info["nickname"] = user_emails_to_names[bug_info["status_author"]]
×
240

241
    def handle_bug(self, bug, data):
×
242
        # Match all the resolutions with resolution comments if they exist
243
        bug_id = str(bug["id"])
×
244
        bug_comments = bug["comments"]
×
245
        bug_history = self.get_resolution_history(bug)
×
246

UNCOV
247
        bug_history["needinfo"] = False
×
NEW
248
        bug_history["resolution_comment"] = self.get_resolution_comment(
×
249
            bug_comments, bug_history
250
        )
NEW
251
        if bug_history["resolution_comment"] is None:
×
252
            # Use N/A to signify no resolution comment was provided
NEW
253
            bug_history["resolution_comment"] = "N/A"
×
UNCOV
254
            bug_history["needinfo"] = self.should_needinfo(
×
255
                bug_comments, bug_history["status_time"]
256
            )
257

258
        data[bug_id] = bug_history
×
259

260
        return bug
×
261

262
    def get_bugs(self, *args, **kwargs):
×
263
        bugs = super().get_bugs(*args, **kwargs)
×
264
        self.get_needinfo_nicks(bugs)
×
265
        self.set_autofix(bugs)
×
266
        return bugs
×
267

268

269
if __name__ == "__main__":
×
270
    PerfAlertResolvedRegression().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