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

mozilla / relman-auto-nag / #5247

01 Oct 2024 12:36PM CUT coverage: 21.657% (+0.01%) from 21.646%
#5247

push

coveralls-python

jgraham
Make webcompat platform bugs without keyword use BQ data

This is considered the canonical source for the list of webcompat core bugs,
so using it as the backend for the bug-bot rules avoids needing to duplicate
the logic across multiple systems.

585 of 3506 branches covered (16.69%)

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

2 existing lines in 2 files now uncovered.

1940 of 8958 relevant lines covered (21.66%)

0.22 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
                        inactive_users[flag["requestee"]]["creation_time"],
106
                    ),
107
                    "requestee_canconfirm": has_canconfirm_group(flag["requestee"]),
108
                }
109
                for flag in bug["needinfo_flags"]
110
                if flag["requestee"] in inactive_users
111
                and (
112
                    # Exclude recent needinfos to allow some time for external
113
                    # users to respond.
114
                    flag["modification_date"] < RECENT_NEEDINFO_LIMIT
115
                    or inactive_users[flag["requestee"]]["status"]
116
                    in [UserStatus.DISABLED, UserStatus.UNDEFINED]
117
                )
118
            ]
119

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

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

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

143
        return res
×
144

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

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

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

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

177
        return NeedinfoAction.CLEAR
×
178

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

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

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

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

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

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

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

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

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

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

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

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

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

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

303
        return bug
×
304

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

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

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

339
        return params
1✔
340

341

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

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

351
    had_unconfirmed_status = False
×
352

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

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

368
    return had_unconfirmed_status
×
369

370

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