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

mozilla / relman-auto-nag / #5676

09 Sep 2025 07:59PM UTC coverage: 20.759% (-0.05%) from 20.81%
#5676

push

coveralls-python

DonalMe
Add a needinfo even when there are multiple regressors

426 of 3034 branches covered (14.04%)

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

2 existing lines in 1 file now uncovered.

1943 of 9360 relevant lines covered (20.76%)

0.21 hits per line

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

0.0
/bugbot/rules/needinfo_regression_author.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

NEW
5
from typing import Any, Dict, List, Tuple
×
6

7
from libmozdata.bugzilla import Bugzilla
×
8

9
from bugbot import logger, utils
×
10
from bugbot.bzcleaner import BzCleaner
×
11
from bugbot.user_activity import UserActivity, UserStatus
×
12

13

14
class NeedinfoRegressionAuthor(BzCleaner):
×
15
    def __init__(self):
×
16
        super().__init__()
×
17
        self.extra_ni = {}
×
18
        self.private_regressor_ids: set[str] = set()
×
19
        # Cache of regressor bug metadata, keyed by bug id
NEW
20
        self.regressor_info: Dict[int, Dict[str, Any]] = {}
×
21

22
    def description(self):
×
23
        return "Unassigned regressions with non-empty Regressed By field"
×
24

25
    def handle_bug(self, bug, data):
×
26
        # Accept any non-empty 'regressed_by'. We will pick the most recent accessible regressor later.
NEW
27
        if not bug["regressed_by"]:
×
NEW
28
            return
×
29

30
        # Keep only numeric bug IDs (ignore changesets or non-bug references).
NEW
31
        regressor_ids: List[int] = []
×
NEW
32
        for r in bug["regressed_by"]:
×
NEW
33
            try:
×
NEW
34
                regressor_ids.append(int(r))
×
NEW
35
            except Exception:
×
36
                # Non-bug regressor (e.g., changeset); ignore here.
NEW
37
                continue
×
38

NEW
39
        if not regressor_ids:
×
40
            # No bug IDs among regressors; nothing to do.
UNCOV
41
            return
×
42

43
        data[str(bug["id"])] = {
×
44
            "creator": bug["creator"],
45
            # Defer selection; we'll resolve the most recent accessible regressor in retrieve_regressors.
46
            "regressor_ids": regressor_ids,
47
            "severity": bug["severity"],
48
        }
49

50
        return bug
×
51

52
    def get_extra_for_needinfo_template(self):
×
53
        return self.extra_ni
×
54

55
    def get_autofix_change(self):
×
56
        return {
×
57
            "keywords": {"add": ["regression"]},
58
        }
59

60
    def set_autofix(self, bugs):
×
61
        for bugid, info in bugs.items():
×
62
            self.extra_ni[bugid] = {
×
63
                "regressor_id": str(info["regressor_id"]),
64
                "suggest_set_severity": info["suggest_set_severity"],
65
            }
66
            self.add_auto_ni(
×
67
                bugid,
68
                {
69
                    "mail": info["regressor_author_email"],
70
                    "nickname": info["regressor_author_nickname"],
71
                },
72
            )
73

74
    def get_bz_params(self, date):
×
75
        start_date, _ = self.get_dates(date)
×
76

77
        fields = [
×
78
            "id",
79
            "creator",
80
            "regressed_by",
81
            "assigned_to",
82
            "severity",
83
        ]
84

85
        # Find all bugs with regressed_by information which were open after start_date or
86
        # whose regressed_by field was set after start_date.
87
        params = {
×
88
            "include_fields": fields,
89
            "f1": "OP",
90
            "j1": "OR",
91
            "f2": "creation_ts",
92
            "o2": "greaterthan",
93
            "v2": start_date,
94
            "f3": "regressed_by",
95
            "o3": "changedafter",
96
            "v3": start_date,
97
            "f4": "CP",
98
            "f5": "regressed_by",
99
            "o5": "isnotempty",
100
            "n6": 1,
101
            "f6": "longdesc",
102
            "o6": "casesubstring",
103
            "v6": "since you are the author of the regressor",
104
            "f7": "flagtypes.name",
105
            "o7": "notsubstring",
106
            "v7": "needinfo?",
107
            "status": ["UNCONFIRMED", "NEW", "REOPENED"],
108
            "resolution": ["---"],
109
        }
110

111
        utils.get_empty_assignees(params)
×
112

113
        return params
×
114

115
    def retrieve_regressors(self, bugs):
×
116
        # Collect all candidate regressor bug IDs across all bugs.
NEW
117
        candidate_ids = set()
×
118
        for bug in bugs.values():
×
NEW
119
            candidate_ids.update(bug["regressor_ids"])
×
120

121
        def bug_handler(regressor_bug):
×
122
            # Cache data for later selection (most recent)
NEW
123
            self.regressor_info[int(regressor_bug["id"])] = {
×
124
                "id": int(regressor_bug["id"]),
125
                "assigned_to": regressor_bug["assigned_to"],
126
                "assigned_to_nick": regressor_bug.get("assigned_to_detail", {}).get(
127
                    "nick"
128
                ),
129
                "groups": regressor_bug.get("groups") or [],
130
                "creation_time": regressor_bug.get("creation_time"),  # ISO 8601
131
            }
132

133
        Bugzilla(
×
134
            bugids=candidate_ids,
135
            bughandler=bug_handler,
136
            include_fields=[
137
                "id",
138
                "assigned_to",
139
                "assigned_to_detail",
140
                "groups",
141
                "creation_time",
142
            ],
143
        ).get_data().wait()
144

145
        # For each bug, pick the most recent accessible regressor (by creation_time).
NEW
146
        to_delete = []
×
NEW
147
        for bug in bugs.values():
×
NEW
148
            candidates: List[Tuple[str, Dict[str, Any]]] = []
×
NEW
149
            for rid in bug["regressor_ids"]:
×
NEW
150
                info = self.regressor_info.get(int(rid))
×
NEW
151
                if info and info.get("creation_time"):
×
NEW
152
                    candidates.append((info["creation_time"], info))
×
153

NEW
154
            if not candidates:
×
155
                # None of the regressors were accessible or had timestamps; skip this bug.
NEW
156
                to_delete.append(bug["id"])
×
NEW
157
                continue
×
158

159
            # Sort by creation_time descending (ISO strings compare correctly).
NEW
160
            candidates.sort(key=lambda x: x[0], reverse=True)
×
NEW
161
            chosen = candidates[0][1]
×
162

NEW
163
            bug["regressor_id"] = chosen["id"]
×
NEW
164
            bug["regressor_author_email"] = chosen["assigned_to"]
×
NEW
165
            bug["regressor_author_nickname"] = chosen.get("assigned_to_nick")
×
166

NEW
167
            if chosen.get("groups"):
×
NEW
168
                self.private_regressor_ids.add(str(chosen["id"]))
×
169

170
        # Drop bugs for which we couldn't resolve any accessible regressor
NEW
171
        for bid in to_delete:
×
NEW
172
            del bugs[bid]
×
173

UNCOV
174
    def filter_bugs(self, bugs):
×
175
        # Exclude bugs whose regressor author is nobody.
176
        for bug in list(bugs.values()):
×
177
            if utils.is_no_assignee(bug["regressor_author_email"]):
×
178
                logger.warning(
×
179
                    "Bug {}, regressor of bug {}, doesn't have an author".format(
180
                        bug["regressor_id"], bug["id"]
181
                    )
182
                )
183
                del bugs[bug["id"]]
×
184

185
        # Exclude bugs whose creator is the regressor author.
186
        bugs = {
×
187
            bug["id"]: bug
188
            for bug in bugs.values()
189
            if bug["creator"] != bug["regressor_author_email"]
190
        }
191

192
        # Exclude bugs where a commentor is the regressor author.
193
        def comment_handler(bug, bug_id):
×
194
            if any(
×
195
                comment["creator"] == bugs[bug_id]["regressor_author_email"]
196
                for comment in bug["comments"]
197
            ):
198
                del bugs[str(bug_id)]
×
199

200
        # Exclude bugs where the regressor author is inactive or blocked needinfo.
201
        # TODO: We can drop this when https://github.com/mozilla/bugbot/issues/1465 is implemented.
202
        users_info = UserActivity(include_fields=["groups", "requests"]).check_users(
×
203
            set(bug["regressor_author_email"] for bug in bugs.values()),
204
            keep_active=True,
205
            fetch_employee_info=True,
206
        )
207

208
        for bug_id, bug in list(bugs.items()):
×
209
            user_info = users_info[bug["regressor_author_email"]]
×
210
            if (
×
211
                user_info["status"] != UserStatus.ACTIVE
212
                or user_info["requests"]["needinfo"]["blocked"]
213
            ):
214
                del bugs[bug_id]
×
215
            else:
216
                bug["suggest_set_severity"] = bug["severity"] in (
×
217
                    "--",
218
                    "n/a",
219
                ) and user_info.get("is_employee")
220

221
        Bugzilla(
×
222
            bugids=self.get_list_bugs(bugs),
223
            commenthandler=comment_handler,
224
            comment_include_fields=["creator"],
225
        ).get_data().wait()
226

227
        return bugs
×
228

229
    def get_bugs(self, *args, **kwargs):
×
230
        bugs = super().get_bugs(*args, **kwargs)
×
231
        self.retrieve_regressors(bugs)
×
232
        bugs = self.filter_bugs(bugs)
×
233
        self.set_autofix(bugs)
×
234
        return bugs
×
235

236
    def set_needinfo(self):
×
237
        res = super().set_needinfo()
×
238
        for bug_id, needinfo_action in res.items():
×
239
            needinfo_action["comment"]["is_private"] = (
×
240
                bug_id in self.private_regressor_ids
241
            )
242

243
        return res
×
244

245

246
if __name__ == "__main__":
×
247
    NeedinfoRegressionAuthor().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