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

mozilla / relman-auto-nag / #5401

06 Feb 2025 01:35AM CUT coverage: 21.189% (-0.01%) from 21.2%
#5401

push

coveralls-python

web-flow
Bump filelock from 3.16.1 to 3.17.0 (#2578)

Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.16.1 to 3.17.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.16.1...3.17.0)

---
updated-dependencies:
- dependency-name: filelock
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

426 of 2950 branches covered (14.44%)

1942 of 9165 relevant lines covered (21.19%)

0.21 hits per line

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

33.09
/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
from libmozdata.bugzilla import Bugzilla
1✔
11

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

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

21

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

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

30

31
class InactiveNeedinfoPending(BzCleaner):
1✔
32
    normal_changes_max: int = 100
1✔
33

34
    def __init__(self):
1✔
35
        super(InactiveNeedinfoPending, self).__init__()
1✔
36
        self.max_actions = utils.get_config(self.name(), "max_actions", 7)
1✔
37

38
    def get_max_actions(self):
1✔
39
        return self.max_actions
×
40

41
    def description(self):
1✔
42
        return "Bugs with needinfo pending on inactive people"
1✔
43

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

54
    def get_bugs(self, *args, **kwargs):
1✔
55
        bugs = super().get_bugs(*args, **kwargs)
×
56
        bugs = self.handle_inactive_requestee(bugs)
×
57

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

62
        return bugs
×
63

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

75
                requestee_bugs[flag["requestee"]].append(bugid)
×
76

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

84
        inactive_requestee_bugs = {
×
85
            bugid
86
            for requestee, bugids in requestee_bugs.items()
87
            if requestee in inactive_users
88
            for bugid in bugids
89
        }
90

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

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

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

129
            inactive_ni = get_inactive_ni(bug)
×
130
            if len(inactive_ni) == 0:
×
131
                continue
×
132

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

142
        return res
×
143

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

151
        if (
×
152
            bug["priority"] in HIGH_PRIORITY
153
            or bug["severity"] in HIGH_SEVERITY
154
            or bug["last_change_time"] >= RECENT_BUG_LIMIT
155
            or any(keyword in SECURITY_KEYWORDS for keyword in bug["keywords"])
156
        ):
157
            return NeedinfoAction.FORWARD
×
158

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

173
        if bug["severity"] == "--":
×
174
            return NeedinfoAction.FORWARD
×
175

176
        return NeedinfoAction.CLEAR
×
177

178
    @staticmethod
1✔
179
    def _clear_inactive_ni_flags(bug):
1✔
180
        return [
×
181
            {
182
                "id": flag["id"],
183
                "status": "X",
184
            }
185
            for flag in bug["inactive_ni"]
186
        ]
187

188
    @staticmethod
1✔
189
    def _needinfo_triage_owner_flag(bug):
1✔
190
        return [
×
191
            {
192
                "name": "needinfo",
193
                "requestee": bug["triage_owner"],
194
                "status": "?",
195
                "new": "true",
196
            }
197
        ]
198

199
    @staticmethod
1✔
200
    def _request_from_triage_owner(bug):
1✔
201
        reasons = []
×
202
        if bug["priority"] in HIGH_PRIORITY:
×
203
            reasons.append("high priority")
×
204
        if bug["severity"] in HIGH_SEVERITY:
×
205
            reasons.append("high severity")
×
206
        if bug["last_change_time"] >= RECENT_BUG_LIMIT:
×
207
            reasons.append("recent activity")
×
208

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

212
        comment = []
×
213
        if reasons:
×
214
            comment.append(f"since the bug has {utils.english_list(reasons)}")
×
215

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

226
        return ", ".join(comment)
×
227

228
    def add_action(self, bug):
1✔
229
        users_num = len(set([flag["requestee"] for flag in bug["inactive_ni"]]))
×
230

231
        if bug["action"] == NeedinfoAction.FORWARD:
×
232
            autofix = {
×
233
                "flags": (
234
                    self._clear_inactive_ni_flags(bug)
235
                    + self._needinfo_triage_owner_flag(bug)
236
                ),
237
                "comment": {
238
                    "body": (
239
                        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.'
240
                        f'\n:{ bug["triage_owner_nic"] }, {self._request_from_triage_owner(bug)}'
241
                    )
242
                },
243
            }
244

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

258
        elif bug["action"] == NeedinfoAction.CLOSE_BUG:
×
259
            autofix = {
×
260
                "flags": self._clear_inactive_ni_flags(bug),
261
                "status": "RESOLVED",
262
                "resolution": "INCOMPLETE",
263
                "comment": {
264
                    "body": (
265
                        "A needinfo is requested from the reporter, however, the reporter is inactive on Bugzilla. "
266
                        "Given that the bug is still `UNCONFIRMED`, closing the bug as incomplete."
267
                    )
268
                },
269
            }
270

271
        autofix["comment"]["body"] += f"\n\n{self.get_documentation()}\n"
×
272
        self.add_prioritized_action(bug, bug["triage_owner"], autofix=autofix)
×
273

274
    def get_bug_sort_key(self, bug):
1✔
275
        return (
×
276
            bug["action"],
277
            utils.get_sort_by_bug_importance_key(bug),
278
        )
279

280
    def handle_bug(self, bug, data):
1✔
281
        bugid = str(bug["id"])
×
282
        triage_owner_nic = (
×
283
            bug["triage_owner_detail"]["nick"] if "triage_owner_detail" in bug else ""
284
        )
285
        data[bugid] = {
×
286
            "priority": bug["priority"],
287
            "severity": bug["severity"],
288
            "creation_time": bug["creation_time"],
289
            "last_change_time": utils.get_last_no_bot_comment_date(bug),
290
            "creator": bug["creator"],
291
            "type": bug["type"],
292
            "attachments": bug["attachments"],
293
            "triage_owner": bug["triage_owner"],
294
            "triage_owner_nic": triage_owner_nic,
295
            "is_confirmed": bug["is_confirmed"],
296
            "needinfo_flags": [
297
                flag for flag in bug["flags"] if flag["name"] == "needinfo"
298
            ],
299
            "keywords": bug["keywords"],
300
        }
301

302
        return bug
×
303

304
    def get_bz_params(self, date):
1✔
305
        fields = [
1✔
306
            "type",
307
            "attachments.content_type",
308
            "attachments.is_obsolete",
309
            "triage_owner",
310
            "flags",
311
            "priority",
312
            "severity",
313
            "creation_time",
314
            "comments",
315
            "creator",
316
            "keywords",
317
            "is_confirmed",
318
        ]
319

320
        params = {
1✔
321
            "include_fields": fields,
322
            "resolution": "---",
323
            "f1": "flagtypes.name",
324
            "o1": "equals",
325
            "v1": "needinfo?",
326
        }
327

328
        # Run monthly on all bugs and weekly on recently changed bugs
329
        if lmdutils.get_date_ymd(date).day > 7:
1!
330
            params.update(
×
331
                {
332
                    "f2": "anything",
333
                    "o2": "changedafter",
334
                    "v2": "-1m",
335
                }
336
            )
337

338
        return params
1✔
339

340

341
def was_unconfirmed(bug: dict) -> bool:
1✔
342
    """Check if a bug was unconfirmed.
343

344
    Returns:
345
        True if the bug was unconfirmed and now is confirmed, False otherwise.
346
    """
347
    if not bug["is_confirmed"]:
×
348
        return False
×
349

350
    had_unconfirmed_status = False
×
351

352
    def check_unconfirmed_in_history(bug):
×
353
        nonlocal had_unconfirmed_status
354
        for history in bug["history"]:
×
355
            for change in history["changes"]:
×
356
                if change["field_name"] == "status":
×
357
                    if change["removed"] == "UNCONFIRMED":
×
358
                        had_unconfirmed_status = True
×
359
                        return
×
360
                    break
×
361

362
    if "history" in bug:
×
363
        check_unconfirmed_in_history(bug)
×
364
    else:
365
        Bugzilla(bug["id"], historyhandler=check_unconfirmed_in_history).wait()
×
366

367
    return had_unconfirmed_status
×
368

369

370
if __name__ == "__main__":
1!
371
    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