|
| 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 |
| 12 | + |
| 13 | + |
| 14 | +class PerfAlertResolvedRegression(BzCleaner): |
| 15 | + def __init__(self, *args, **kwargs): |
| 16 | + super().__init__(*args, **kwargs) |
| 17 | + self.extra_ni = {} |
| 18 | + |
| 19 | + def description(self): |
| 20 | + return "PerfAlert regressions whose resolution has changed recently" |
| 21 | + |
| 22 | + def columns(self): |
| 23 | + return [ |
| 24 | + "id", |
| 25 | + "summary", |
| 26 | + "status", |
| 27 | + "status_author", |
| 28 | + "resolution", |
| 29 | + "resolution_comment", |
| 30 | + "resolution_previous", |
| 31 | + ] |
| 32 | + |
| 33 | + def get_extra_for_needinfo_template(self): |
| 34 | + return self.extra_ni |
| 35 | + |
| 36 | + def get_bz_params(self, date): |
| 37 | + end_date = lmdutils.get_date_ymd("today") |
| 38 | + start_date = end_date - timedelta(1) |
| 39 | + |
| 40 | + fields = [ |
| 41 | + "id", |
| 42 | + "history", |
| 43 | + "comments.text", |
| 44 | + "comments.creation_time", |
| 45 | + "comments.author", |
| 46 | + ] |
| 47 | + |
| 48 | + # Find all bugs that have perf-alert, and regression in their keywords. Search |
| 49 | + # for bugs that have been changed in the last day. Only look for bugs after |
| 50 | + # October 1st, 2024 to prevent triggering comments on older performance regressions |
| 51 | + params = { |
| 52 | + "include_fields": fields, |
| 53 | + "f3": "creation_ts", |
| 54 | + "o3": "greaterthan", |
| 55 | + "v3": "2024-10-01T00:00:00Z", |
| 56 | + "f1": "regressed_by", |
| 57 | + "o1": "isnotempty", |
| 58 | + "f2": "keywords", |
| 59 | + "o2": "allwords", |
| 60 | + "v2": ["regression", "perf-alert"], |
| 61 | + "f4": "resolution", |
| 62 | + "o4": "changedafter", |
| 63 | + "v4": start_date, |
| 64 | + "f5": "resolution", |
| 65 | + "o5": "changedbefore", |
| 66 | + "v5": end_date, |
| 67 | + } |
| 68 | + |
| 69 | + return params |
| 70 | + |
| 71 | + def should_needinfo(self, bug_comments, status_time): |
| 72 | + # Check if the bugbot has already needinfo'ed on the bug since |
| 73 | + # the last status change before making one |
| 74 | + for comment in bug_comments[::-1]: |
| 75 | + if comment["creation_time"] <= status_time: |
| 76 | + break |
| 77 | + |
| 78 | + if comment["author"] == BOT_MAIN_ACCOUNT: |
| 79 | + if ( |
| 80 | + "could you provide a comment explaining the resolution?" |
| 81 | + in comment["text"] |
| 82 | + ): |
| 83 | + # Bugbot has already commented on this bug since the last |
| 84 | + # status change. No need to comment again since this was |
| 85 | + # just a resolution change |
| 86 | + return False |
| 87 | + |
| 88 | + return True |
| 89 | + |
| 90 | + def get_resolution_history(self, bug): |
| 91 | + bug_info = {} |
| 92 | + |
| 93 | + # Get the last resolution change that was made in this bug |
| 94 | + for change in bug["history"][::-1]: |
| 95 | + # Get the most recent resolution change first, this is because |
| 96 | + # it could have changed since the status was changed and by who |
| 97 | + if not bug_info.get("resolution"): |
| 98 | + for specific_change in change["changes"]: |
| 99 | + if specific_change["field_name"] == "resolution": |
| 100 | + bug_info["resolution"] = specific_change["added"] |
| 101 | + bug_info["resolution_previous"] = ( |
| 102 | + specific_change["removed"].strip() or "---" |
| 103 | + ) |
| 104 | + bug_info["resolution_time"] = change["when"] |
| 105 | + break |
| 106 | + |
| 107 | + if bug_info.get("resolution"): |
| 108 | + # Find the status that the bug was resolved to, and by who |
| 109 | + for specific_change in change["changes"]: |
| 110 | + if specific_change["field_name"] == "status" and specific_change[ |
| 111 | + "added" |
| 112 | + ] in ("RESOLVED", "REOPENED"): |
| 113 | + bug_info["status"] = specific_change["added"] |
| 114 | + bug_info["status_author"] = change["who"] |
| 115 | + bug_info["status_time"] = change["when"] |
| 116 | + break |
| 117 | + |
| 118 | + if bug_info.get("status"): |
| 119 | + break |
| 120 | + |
| 121 | + return bug_info |
| 122 | + |
| 123 | + def set_autofix(self, bugs): |
| 124 | + for bug_id, bug_info in bugs.items(): |
| 125 | + if bug_info["needinfo"]: |
| 126 | + self.extra_ni[bug_id] = { |
| 127 | + "resolution": bug_info["resolution"], |
| 128 | + "status": bug_info["status"], |
| 129 | + } |
| 130 | + self.add_auto_ni( |
| 131 | + bug_id, |
| 132 | + { |
| 133 | + "mail": bug_info["status_author"], |
| 134 | + "nickname": bug_info["nickname"], |
| 135 | + }, |
| 136 | + ) |
| 137 | + |
| 138 | + def get_needinfo_nicks(self, bugs): |
| 139 | + def _user_handler(user, data): |
| 140 | + data[user["name"]] = user["nick"] |
| 141 | + |
| 142 | + authors_to_ni = set() |
| 143 | + for bug_id, bug_info in bugs.items(): |
| 144 | + if bug_info["needinfo"]: |
| 145 | + authors_to_ni.add(bug_info["status_author"]) |
| 146 | + |
| 147 | + if not authors_to_ni: |
| 148 | + return |
| 149 | + |
| 150 | + user_emails_to_names = {} |
| 151 | + BugzillaUser( |
| 152 | + user_names=list(authors_to_ni), |
| 153 | + include_fields=["nick", "name"], |
| 154 | + user_handler=_user_handler, |
| 155 | + user_data=user_emails_to_names, |
| 156 | + ).wait() |
| 157 | + |
| 158 | + for bug_id, bug_info in bugs.items(): |
| 159 | + if bug_info["needinfo"]: |
| 160 | + bug_info["nickname"] = user_emails_to_names[bug_info["status_author"]] |
| 161 | + |
| 162 | + def handle_bug(self, bug, data): |
| 163 | + # Match all the resolutions with resolution comments if they exist |
| 164 | + bug_id = str(bug["id"]) |
| 165 | + bug_comments = bug["comments"] |
| 166 | + bug_history = self.get_resolution_history(bug) |
| 167 | + |
| 168 | + # Sometimes a resolution comment is not provided so use a default |
| 169 | + bug_history["needinfo"] = False |
| 170 | + bug_history["resolution_comment"] = "N/A" |
| 171 | + for comment in bug_comments[::-1]: |
| 172 | + if ( |
| 173 | + comment["creation_time"] == bug_history["status_time"] |
| 174 | + and comment["author"] == bug_history["status_author"] |
| 175 | + ): |
| 176 | + bug_history["resolution_comment"] = comment["text"] |
| 177 | + break |
| 178 | + else: |
| 179 | + bug_history["needinfo"] = self.should_needinfo( |
| 180 | + bug_comments, bug_history["status_time"] |
| 181 | + ) |
| 182 | + |
| 183 | + data[bug_id] = bug_history |
| 184 | + |
| 185 | + return bug |
| 186 | + |
| 187 | + def get_bugs(self, *args, **kwargs): |
| 188 | + bugs = super().get_bugs(*args, **kwargs) |
| 189 | + self.get_needinfo_nicks(bugs) |
| 190 | + self.set_autofix(bugs) |
| 191 | + return bugs |
| 192 | + |
| 193 | + |
| 194 | +if __name__ == "__main__": |
| 195 | + PerfAlertResolvedRegression().run() |
0 commit comments