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

mozilla / relman-auto-nag / #4558

pending completion
#4558

push

coveralls-python

web-flow
Bump coverage from 7.2.6 to 7.2.7

Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.6 to 7.2.7.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.6...7.2.7)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

641 of 3222 branches covered (19.89%)

1820 of 8004 relevant lines covered (22.74%)

0.23 hits per line

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

25.28
/bugbot/rules/inactive_ni_pending.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 collections import defaultdict
1✔
6
from datetime import timedelta
1✔
7
from enum import IntEnum, auto
1✔
8

9
from libmozdata import utils as lmdutils
1✔
10

11
from bugbot import utils
1✔
12
from bugbot.bzcleaner import BzCleaner
1✔
13
from bugbot.constants import HIGH_PRIORITY, HIGH_SEVERITY
1✔
14
from bugbot.user_activity import UserActivity, UserStatus
1✔
15
from bugbot.utils import plural
1✔
16

17
RECENT_BUG_LIMIT = lmdutils.get_date("today", timedelta(weeks=5).days)
1✔
18
RECENT_NEEDINFO_LIMIT = lmdutils.get_date("today", timedelta(weeks=2).days)
1✔
19

20

21
class NeedinfoAction(IntEnum):
1✔
22
    FORWARD = auto()
1✔
23
    CLEAR = auto()
1✔
24
    CLOSE_BUG = auto()
1✔
25

26
    def __str__(self):
1✔
27
        return self.name.title().replace("_", " ")
×
28

29

30
class InactiveNeedinfoPending(BzCleaner):
1✔
31
    def __init__(self):
1✔
32
        super(InactiveNeedinfoPending, self).__init__()
1✔
33
        self.max_actions = utils.get_config(self.name(), "max_actions", 7)
1✔
34

35
    def get_max_actions(self):
1✔
36
        return self.max_actions
×
37

38
    def description(self):
1✔
39
        return "Bugs with needinfo pending on inactive people"
1✔
40

41
    def columns(self):
1✔
42
        return [
×
43
            "id",
44
            "summary",
45
            "inactive_ni",
46
            "inactive_ni_count",
47
            "action",
48
            "triage_owner",
49
        ]
50

51
    def get_bugs(self, *args, **kwargs):
1✔
52
        bugs = super().get_bugs(*args, **kwargs)
×
53
        bugs = self.handle_inactive_requestee(bugs)
×
54

55
        # Resolving https://github.com/mozilla/bugbot/issues/1300 should clean this
56
        # including improve the wording in the template (i.e., "See the search query on Bugzilla").
57
        self.query_url = utils.get_bz_search_url({"bug_id": ",".join(bugs.keys())})
×
58

59
        return bugs
×
60

61
    def handle_inactive_requestee(self, bugs):
1✔
62
        """
63
        Detect inactive users and filter bugs to keep only the ones with needinfo pending on
64
        inactive users.
65
        """
66
        requestee_bugs = defaultdict(list)
×
67
        for bugid, bug in bugs.items():
×
68
            for flag in bug["needinfo_flags"]:
×
69
                if "requestee" not in flag:
×
70
                    flag["requestee"] = ""
×
71

72
                requestee_bugs[flag["requestee"]].append(bugid)
×
73

74
        user_activity = UserActivity(include_fields=["groups"])
×
75
        needinfo_requestees = set(requestee_bugs.keys())
×
76
        triage_owners = {bug["triage_owner"] for bug in bugs.values()}
×
77
        inactive_users = user_activity.check_users(
×
78
            needinfo_requestees | triage_owners, ignore_bots=True
79
        )
80

81
        inactive_requestee_bugs = {
×
82
            bugid
83
            for requestee, bugids in requestee_bugs.items()
84
            if requestee in inactive_users
85
            for bugid in bugids
86
        }
87

88
        def has_canconfirm_group(user_email):
×
89
            for group in inactive_users[user_email].get("groups", []):
×
90
                if group["name"] == "canconfirm":
×
91
                    return True
×
92
            return False
×
93

94
        def get_inactive_ni(bug):
×
95
            return [
×
96
                {
97
                    "id": flag["id"],
98
                    "setter": flag["setter"],
99
                    "requestee": flag["requestee"],
100
                    "requestee_status": user_activity.get_string_status(
101
                        inactive_users[flag["requestee"]]["status"]
102
                    ),
103
                    "requestee_canconfirm": has_canconfirm_group(flag["requestee"]),
104
                }
105
                for flag in bug["needinfo_flags"]
106
                if flag["requestee"] in inactive_users
107
                and (
108
                    # Exclude recent needinfos to allow some time for external
109
                    # users to respond.
110
                    flag["modification_date"] < RECENT_NEEDINFO_LIMIT
111
                    or inactive_users[flag["requestee"]]["status"]
112
                    in [UserStatus.DISABLED, UserStatus.UNDEFINED]
113
                )
114
            ]
115

116
        res = {}
×
117
        skiplist = self.get_auto_ni_skiplist()
×
118
        for bugid, bug in bugs.items():
×
119
            if (
×
120
                bugid not in inactive_requestee_bugs
121
                or bug["triage_owner"] in inactive_users
122
                or bug["triage_owner"] in skiplist
123
            ):
124
                continue
×
125

126
            inactive_ni = get_inactive_ni(bug)
×
127
            if len(inactive_ni) == 0:
×
128
                continue
×
129

130
            bug = {
×
131
                **bug,
132
                "inactive_ni": inactive_ni,
133
                "inactive_ni_count": len(inactive_ni),
134
                "action": self.get_action_type(bug, inactive_ni),
135
            }
136
            res[bugid] = bug
×
137
            self.add_action(bug)
×
138

139
        return res
×
140

141
    @staticmethod
1✔
142
    def get_action_type(bug, inactive_ni):
1✔
143
        """
144
        Determine if should forward needinfos to the triage owner, clear the
145
        needinfos, or close the bug.
146
        """
147

148
        if (
×
149
            bug["priority"] in HIGH_PRIORITY
150
            or bug["severity"] in HIGH_SEVERITY
151
            or bug["last_change_time"] >= RECENT_BUG_LIMIT
152
        ):
153
            return NeedinfoAction.FORWARD
×
154

155
        if (
×
156
            len(bug["needinfo_flags"]) == 1
157
            and bug["type"] == "defect"
158
            and inactive_ni[0]["requestee"] == bug["creator"]
159
            and not inactive_ni[0]["requestee_canconfirm"]
160
            and not any(
161
                attachment["content_type"] == "text/x-phabricator-request"
162
                and not attachment["is_obsolete"]
163
                for attachment in bug["attachments"]
164
            )
165
        ):
166
            return NeedinfoAction.CLOSE_BUG
×
167

168
        if bug["severity"] == "--":
×
169
            return NeedinfoAction.FORWARD
×
170

171
        return NeedinfoAction.CLEAR
×
172

173
    @staticmethod
1✔
174
    def _clear_inactive_ni_flags(bug):
1✔
175
        return [
×
176
            {
177
                "id": flag["id"],
178
                "status": "X",
179
            }
180
            for flag in bug["inactive_ni"]
181
        ]
182

183
    @staticmethod
1✔
184
    def _needinfo_triage_owner_flag(bug):
1✔
185
        return [
×
186
            {
187
                "name": "needinfo",
188
                "requestee": bug["triage_owner"],
189
                "status": "?",
190
                "new": "true",
191
            }
192
        ]
193

194
    @staticmethod
1✔
195
    def _request_from_triage_owner(bug):
1✔
196
        reasons = []
×
197
        if bug["priority"] in HIGH_PRIORITY:
×
198
            reasons.append("high priority")
×
199
        if bug["severity"] in HIGH_SEVERITY:
×
200
            reasons.append("high severity")
×
201
        if bug["last_change_time"] >= RECENT_BUG_LIMIT:
×
202
            reasons.append("recent activity")
×
203

204
        if len(reasons) == 0 and bug["severity"] == "--":
×
205
            return "since the bug doesn't have a severity set, could you please set the severity or close the bug?"
×
206

207
        comment = []
×
208
        if reasons:
×
209
            comment.append(f"since the bug has {utils.english_list(reasons)}")
×
210

211
        if (
×
212
            len(bug["inactive_ni"]) == 1
213
            and bug["inactive_ni"][0]["setter"] == bug["triage_owner"]
214
        ):
215
            comment.append(
×
216
                "could you please find another way to get the information or close the bug as `INCOMPLETE` if it is not actionable?"
217
            )
218
        else:
219
            comment.append("could you have a look please?")
×
220

221
        return ", ".join(comment)
×
222

223
    def add_action(self, bug):
1✔
224
        users_num = len(set([flag["requestee"] for flag in bug["inactive_ni"]]))
×
225

226
        if bug["action"] == NeedinfoAction.FORWARD:
×
227
            autofix = {
×
228
                "flags": (
229
                    self._clear_inactive_ni_flags(bug)
230
                    + self._needinfo_triage_owner_flag(bug)
231
                ),
232
                "comment": {
233
                    "body": (
234
                        f'Redirect { plural("a needinfo that is", bug["inactive_ni"], "needinfos that are") } pending on { plural("an inactive user", users_num, "inactive users") } to the triage owner.'
235
                        f'\n:{ bug["triage_owner_nic"] }, {self._request_from_triage_owner(bug)}'
236
                    )
237
                },
238
            }
239

240
        elif bug["action"] == NeedinfoAction.CLEAR:
×
241
            autofix = {
×
242
                "flags": self._clear_inactive_ni_flags(bug),
243
                "comment": {
244
                    "body": (
245
                        f'Clear { plural("a needinfo that is", bug["inactive_ni"], "needinfos that are") } pending on { plural("an inactive user", users_num, "inactive users") }.'
246
                        "\n\nInactive users most likely will not respond; "
247
                        "if the missing information is essential and cannot be collected another way, "
248
                        "the bug maybe should be closed as `INCOMPLETE`."
249
                    ),
250
                },
251
            }
252

253
        elif bug["action"] == NeedinfoAction.CLOSE_BUG:
×
254
            autofix = {
×
255
                "flags": self._clear_inactive_ni_flags(bug),
256
                "status": "RESOLVED",
257
                "resolution": "INCOMPLETE",
258
                "comment": {
259
                    "body": "A needinfo is requested from the reporter, however, the reporter is inactive on Bugzilla. Closing the bug as incomplete."
260
                },
261
            }
262

263
        autofix["comment"]["body"] += f"\n\n{self.get_documentation()}\n"
×
264
        self.add_prioritized_action(bug, bug["triage_owner"], autofix=autofix)
×
265

266
    def get_bug_sort_key(self, bug):
1✔
267
        return (
×
268
            bug["action"],
269
            utils.get_sort_by_bug_importance_key(bug),
270
        )
271

272
    def handle_bug(self, bug, data):
1✔
273
        bugid = str(bug["id"])
×
274
        triage_owner_nic = (
×
275
            bug["triage_owner_detail"]["nick"] if "triage_owner_detail" in bug else ""
276
        )
277
        data[bugid] = {
×
278
            "priority": bug["priority"],
279
            "severity": bug["severity"],
280
            "creation_time": bug["creation_time"],
281
            "last_change_time": utils.get_last_no_bot_comment_date(bug),
282
            "creator": bug["creator"],
283
            "type": bug["type"],
284
            "attachments": bug["attachments"],
285
            "triage_owner": bug["triage_owner"],
286
            "triage_owner_nic": triage_owner_nic,
287
            "needinfo_flags": [
288
                flag for flag in bug["flags"] if flag["name"] == "needinfo"
289
            ],
290
        }
291

292
        return bug
×
293

294
    def get_bz_params(self, date):
1✔
295
        fields = [
1✔
296
            "type",
297
            "attachments.content_type",
298
            "attachments.is_obsolete",
299
            "triage_owner",
300
            "flags",
301
            "priority",
302
            "severity",
303
            "creation_time",
304
            "comments",
305
            "creator",
306
        ]
307

308
        params = {
1✔
309
            "include_fields": fields,
310
            "resolution": "---",
311
            "f1": "flagtypes.name",
312
            "o1": "equals",
313
            "v1": "needinfo?",
314
        }
315

316
        # Run monthly on all bugs and weekly on recently changed bugs
317
        if lmdutils.get_date_ymd(date).day > 7:
1!
318
            params.update(
×
319
                {
320
                    "f2": "anything",
321
                    "o2": "changedafter",
322
                    "v2": "-1m",
323
                }
324
            )
325

326
        return params
1✔
327

328

329
if __name__ == "__main__":
1!
330
    InactiveNeedinfoPending().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