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

mozilla / relman-auto-nag / #5221

28 Aug 2024 07:11PM CUT coverage: 21.627% (-0.02%) from 21.646%
#5221

push

coveralls-python

benjaminmah
Added `handle_bug_util()` util function

585 of 3508 branches covered (16.68%)

0 of 18 new or added lines in 2 files covered. (0.0%)

51 existing lines in 1 file now uncovered.

1941 of 8975 relevant lines covered (21.63%)

0.22 hits per line

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

0.0
/bugbot/rules/inactive_reviewer.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
import re
×
6
from datetime import datetime
×
7
from typing import Dict, List
×
8

9
from dateutil.relativedelta import relativedelta
×
10
from libmozdata import utils as lmdutils
×
11
from libmozdata.connection import Connection
×
12
from libmozdata.phabricator import PhabricatorAPI
×
13
from tenacity import retry, stop_after_attempt, wait_exponential
×
14

15
from bugbot import utils
×
16
from bugbot.bzcleaner import BzCleaner
×
17
from bugbot.history import History
×
NEW
18
from bugbot.inactive_utils import handle_bug_util, process_bugs
×
UNCOV
19
from bugbot.user_activity import PHAB_CHUNK_SIZE, UserActivity, UserStatus
×
20

21
PHAB_FILE_NAME_PAT = re.compile(r"phabricator-D([0-9]+)-url\.txt")
×
UNCOV
22
PHAB_TABLE_PAT = re.compile(r"^\|\ \[D([0-9]+)\]\(h", flags=re.M)
×
23

24

UNCOV
25
class InactiveReviewer(BzCleaner):
×
26
    """Bugs with patches that are waiting for review from inactive reviewers"""
27

UNCOV
28
    def __init__(self, old_patch_months: int = 6):
×
29
        """Constructor
30

31
        Args:
32
            old_patch_months: number of months since creation of the patch to be
33
                considered old. If the bug has an old patch, we will mention
34
                abandon the patch as an option.
35
        """
36
        super(InactiveReviewer, self).__init__()
×
37
        self.phab = PhabricatorAPI(utils.get_login_info()["phab_api_key"])
×
38
        self.user_activity = UserActivity(include_fields=["nick"], phab=self.phab)
×
39
        self.ni_template = self.get_needinfo_template()
×
UNCOV
40
        self.old_patch_limit = (
×
41
            lmdutils.get_date_ymd("today") - relativedelta(months=old_patch_months)
42
        ).timestamp()
43

44
    def description(self):
×
UNCOV
45
        return "Bugs with inactive patch reviewers"
×
46

47
    def columns(self):
×
UNCOV
48
        return ["id", "summary", "revisions"]
×
49

50
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
UNCOV
51
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
52
        bugs, self.query_url = process_bugs(
×
53
            bugs, self._get_revisions_with_inactive_reviewers, self._add_needinfo
54
        )
55

56
        return bugs
×
57

UNCOV
58
    def _add_needinfo(self, bugid: str, inactive_revs: list) -> None:
×
59
        ni_mails = {rev["author"]["name"] for rev in inactive_revs}
×
60
        nicknames = utils.english_list(
×
61
            sorted({rev["author"]["nick"] for rev in inactive_revs})
62
        )
63
        has_old_patch = any(
×
64
            revision["created_at"] < self.old_patch_limit for revision in inactive_revs
65
        )
66

67
        reviewers = {
×
68
            (reviewer["phab_username"], reviewer["status_note"])
69
            for revision in inactive_revs
70
            for reviewer in revision["reviewers"]
71
        }
72
        has_resigned = any(note == "Resigned from review" for _, note in reviewers)
×
73

UNCOV
74
        if len(reviewers) == 1:
×
UNCOV
75
            if has_resigned:
×
76
                summary = "a reviewer who resigned from the review"
×
77
            else:
UNCOV
78
                summary = "an inactive reviewer"
×
79
        else:
80
            if has_resigned:
×
UNCOV
81
                summary = "reviewers who are inactive or resigned from the review"
×
82
            else:
UNCOV
83
                summary = "inactive reviewers"
×
84

85
        comment = self.ni_template.render(
×
86
            revisions=inactive_revs,
87
            nicknames=nicknames,
88
            reviewers_status_summary=summary,
89
            has_old_patch=has_old_patch,
90
            plural=utils.plural,
91
            documentation=self.get_documentation(),
92
        )
93

94
        self.autofix_changes[bugid] = {
×
95
            "comment": {"body": comment},
96
            "flags": [
97
                {
98
                    "name": "needinfo",
99
                    "requestee": ni_mail,
100
                    "status": "?",
101
                    "new": "true",
102
                }
103
                for ni_mail in ni_mails
104
            ],
105
        }
106

107
    def _get_revisions_with_inactive_reviewers(self, rev_ids: list) -> Dict[int, dict]:
×
UNCOV
108
        revisions: List[dict] = []
×
UNCOV
109
        for _rev_ids in Connection.chunks(rev_ids, PHAB_CHUNK_SIZE):
×
UNCOV
110
            for revision in self._fetch_revisions(_rev_ids):
×
UNCOV
111
                if (
×
112
                    len(revision["attachments"]["reviewers"]["reviewers"]) == 0
113
                    or revision["fields"]["status"]["value"] != "needs-review"
114
                    or revision["fields"]["isDraft"]
115
                ):
UNCOV
116
                    continue
×
117

UNCOV
118
                reviewers = [
×
119
                    {
120
                        "phid": reviewer["reviewerPHID"],
121
                        "is_group": reviewer["reviewerPHID"].startswith("PHID-PROJ"),
122
                        "is_blocking": reviewer["isBlocking"],
123
                        "is_accepted": reviewer["status"] == "accepted",
124
                        "is_resigned": reviewer["status"] == "resigned",
125
                    }
126
                    for reviewer in revision["attachments"]["reviewers"]["reviewers"]
127
                ]
128

129
                # Group reviewers will be consider always active; so if there is
130
                # no other reviewers blocking, we don't need to check further.
131
                if any(
×
132
                    reviewer["is_group"] or reviewer["is_accepted"]
133
                    for reviewer in reviewers
134
                ) and not any(
135
                    not reviewer["is_accepted"]
136
                    for reviewer in reviewers
137
                    if reviewer["is_blocking"]
138
                ):
UNCOV
139
                    continue
×
140

UNCOV
141
                revisions.append(
×
142
                    {
143
                        "rev_id": revision["id"],
144
                        "title": revision["fields"]["title"],
145
                        "created_at": revision["fields"]["dateCreated"],
146
                        "author_phid": revision["fields"]["authorPHID"],
147
                        "reviewers": reviewers,
148
                    }
149
                )
150

UNCOV
151
        user_phids = set()
×
152
        for revision in revisions:
×
UNCOV
153
            user_phids.add(revision["author_phid"])
×
154
            for reviewer in revision["reviewers"]:
×
UNCOV
155
                user_phids.add(reviewer["phid"])
×
156

UNCOV
157
        users = self.user_activity.get_phab_users_with_status(
×
158
            list(user_phids), keep_active=True
159
        )
160

UNCOV
161
        result: Dict[int, dict] = {}
×
UNCOV
162
        for revision in revisions:
×
163
            # It is not useful to notify an inactive author about an inactive
164
            # reviewer, thus we should exclude revisions with inactive authors.
165
            author_info = users[revision["author_phid"]]
×
166
            if author_info["status"] != UserStatus.ACTIVE:
×
167
                continue
×
168

UNCOV
169
            revision["author"] = author_info
×
170

UNCOV
171
            inactive_reviewers = []
×
UNCOV
172
            for reviewer in revision["reviewers"]:
×
UNCOV
173
                if reviewer["is_group"]:
×
174
                    continue
×
175

UNCOV
176
                reviewer_info = users[reviewer["phid"]]
×
UNCOV
177
                if (
×
178
                    not reviewer["is_resigned"]
179
                    and reviewer_info["status"] == UserStatus.ACTIVE
180
                ):
UNCOV
181
                    continue
×
182

UNCOV
183
                reviewer["info"] = reviewer_info
×
184
                inactive_reviewers.append(reviewer)
×
185

186
            if len(inactive_reviewers) == len(revision["reviewers"]) or any(
×
187
                reviewer["is_blocking"] and not reviewer["is_accepted"]
188
                for reviewer in inactive_reviewers
189
            ):
190
                revision["reviewers"] = [
×
191
                    {
192
                        "phab_username": reviewer["info"]["phab_username"],
193
                        "status_note": self._get_reviewer_status_note(reviewer),
194
                    }
195
                    for reviewer in inactive_reviewers
196
                ]
197
                result[revision["rev_id"]] = revision
×
198

199
        return result
×
200

UNCOV
201
    @staticmethod
×
UNCOV
202
    def _get_reviewer_status_note(reviewer: dict) -> str:
×
203
        if reviewer["is_resigned"]:
×
UNCOV
204
            return "Resigned from review"
×
205

UNCOV
206
        status = reviewer["info"]["status"]
×
UNCOV
207
        if status == UserStatus.UNAVAILABLE:
×
UNCOV
208
            until = reviewer["info"]["unavailable_until"]
×
UNCOV
209
            if until:
×
210
                return_date = datetime.fromtimestamp(until).strftime("%b %-d, %Y")
×
UNCOV
211
                return f"Back {return_date}"
×
212

UNCOV
213
            return "Unavailable"
×
214

215
        if status == UserStatus.DISABLED:
×
216
            return "Disabled"
×
217

UNCOV
218
        return "Inactive"
×
219

220
    @retry(
×
221
        wait=wait_exponential(min=4),
222
        stop=stop_after_attempt(5),
223
    )
224
    def _fetch_revisions(self, ids: list):
×
UNCOV
225
        return self.phab.request(
×
226
            "differential.revision.search",
227
            constraints={"ids": ids},
228
            attachments={"reviewers": True},
229
        )["data"]
230

231
    def handle_bug(self, bug, data):
×
NEW
232
        return handle_bug_util(
×
233
            bug, data, PHAB_FILE_NAME_PAT, PHAB_TABLE_PAT, History.BOT
234
        )
235

UNCOV
236
    def get_bz_params(self, date):
×
237
        fields = [
×
238
            "comments.raw_text",
239
            "comments.creator",
240
            "attachments.file_name",
241
            "attachments.content_type",
242
            "attachments.is_obsolete",
243
        ]
244
        params = {
×
245
            "include_fields": fields,
246
            "resolution": "---",
247
            "f1": "attachments.mimetype",
248
            "o1": "equals",
249
            "v1": "text/x-phabricator-request",
250
        }
251

UNCOV
252
        return params
×
253

254

UNCOV
255
if __name__ == "__main__":
×
UNCOV
256
    InactiveReviewer().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