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

mozilla / relman-auto-nag / #5156

09 Jul 2024 07:35PM CUT coverage: 21.54% (-0.2%) from 21.783%
#5156

push

coveralls-python

benjaminmah
Removed initialization of `patch_activity_months` arg

716 of 3671 branches covered (19.5%)

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

286 existing lines in 6 files now uncovered.

1933 of 8974 relevant lines covered (21.54%)

0.22 hits per line

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

0.0
/bugbot/rules/inactive_revision.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 typing import Dict, List
×
7

8
from dateutil.relativedelta import relativedelta
×
9
from jinja2 import Environment, FileSystemLoader, Template
×
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
×
18
from bugbot.user_activity import PHAB_CHUNK_SIZE, UserActivity
×
19

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

23

24
class InactiveRevision(BzCleaner):
×
25
    """Bugs with inactive patches that are awaiting action from authors or reviewers."""
26

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

30
        Args:
31
            old_patch_months: number of months since creation of the patch to be
32
                considered old. If the bug has an old patch, we will mention
33
                abandon the patch as an option.
34
            patch_activity_months: Number of months since the last activity on the patch.
35
        """
36
        super(InactiveRevision, 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_author_template = self.load_template(
×
40
            self.name() + "_needinfo_author.txt"
41
        )
42
        self.ni_reviewer_template = self.load_template(
×
43
            self.name() + "_needinfo_reviewer.txt"
44
        )
45
        self.old_patch_limit = (
×
46
            lmdutils.get_date_ymd("today") - relativedelta(months=old_patch_months)
47
        ).timestamp()
48
        self.patch_activity_limit = (
×
49
            lmdutils.get_date_ymd("today") - relativedelta(months=patch_activity_months)
50
        ).timestamp()
51

52
    def description(self):
×
53
        return "Bugs with inactive patches that are awaiting action from authors or reviewers."
×
54

55
    def columns(self):
×
56
        return ["id", "summary", "revisions"]
×
57

58
    def get_bugs(self, date="today", bug_ids=[], chunk_size=None):
×
59
        bugs = super().get_bugs(date, bug_ids, chunk_size)
×
60

61
        rev_ids = {rev_id for bug in bugs.values() for rev_id in bug["rev_ids"]}
×
62
        revisions = self._get_revisions_with_inactive_action(list(rev_ids))
×
63

64
        for bugid, bug in list(bugs.items()):
×
65
            inactive_revs = [
×
66
                revisions[rev_id] for rev_id in bug["rev_ids"] if rev_id in revisions
67
            ]
68
            if inactive_revs:
×
69
                bug["revisions"] = inactive_revs
×
70
                self._add_needinfo(bugid, inactive_revs)
×
71
            else:
72
                del bugs[bugid]
×
73

74
        self.query_url = utils.get_bz_search_url({"bug_id": ",".join(bugs.keys())})
×
75

76
        return bugs
×
77

78
    def load_template(self, template_filename: str) -> Template:
×
79
        env = Environment(loader=FileSystemLoader("templates"))
×
80
        template = env.get_template(template_filename)
×
81
        return template
×
82

83
    def _add_needinfo(self, bugid: str, inactive_revs: list) -> None:
×
84
        has_old_patch = any(
×
85
            revision["created_at"] < self.old_patch_limit for revision in inactive_revs
86
        )
87

88
        for revision in inactive_revs:
×
89
            last_action_by, _ = self._find_last_action(revision["rev_id"])
×
90
            if last_action_by == "author" and revision["reviewers"]:
×
91
                ni_mail = revision["reviewers"][0]["phab_username"]
×
92
                summary = (
×
93
                    "The last action was by the author, so needinfoing the reviewer."
94
                )
95
                template = self.ni_reviewer_template
×
96
                nickname = revision["reviewers"][0]["phab_username"]
×
97
            elif last_action_by == "reviewer":
×
98
                ni_mail = revision["author"]["phab_username"]
×
99
                summary = (
×
100
                    "The last action was by the reviewer, so needinfoing the author."
101
                )
102
                template = self.ni_author_template
×
103
                nickname = revision["author"]["phab_username"]
×
104
            else:
105
                continue
×
106

107
            comment = template.render(
×
108
                revisions=[revision],
109
                nicknames=nickname,
110
                reviewers_status_summary=summary,
111
                has_old_patch=has_old_patch,
112
                plural=utils.plural,
113
                documentation=self.get_documentation(),
114
            )
115

116
            self.autofix_changes[bugid] = {
×
117
                "comment": {"body": comment},
118
                "flags": [
119
                    {
120
                        "name": "needinfo",
121
                        "requestee": ni_mail,
122
                        "status": "?",
123
                        "new": "true",
124
                    }
125
                ],
126
            }
127

128
    def _find_last_action(self, revision_id):
×
129
        details = self._fetch_revisions([revision_id])
×
130

131
        if not details:
×
132
            return None, None
×
133

134
        revision = details[0]
×
135
        author_phid = revision["fields"]["authorPHID"]
×
136
        reviewers = [
×
137
            reviewer["reviewerPHID"]
138
            for reviewer in revision["attachments"]["reviewers"]["reviewers"]
139
        ]
140

141
        transactions = self._fetch_revision_transactions(revision["phid"])
×
142
        last_transaction = transactions[0] if transactions else None
×
143

144
        if last_transaction:
×
145
            last_action_by_phid = last_transaction["authorPHID"]
×
146
            if last_action_by_phid == author_phid:
×
147
                last_action_by = "author"
×
148
            elif last_action_by_phid in reviewers:
×
149
                last_action_by = "reviewer"
×
150
            else:
151
                last_action_by = "other"
×
152
        else:
153
            last_action_by = "unknown"
×
154

155
        return last_action_by, last_transaction
×
156

157
    def _get_revisions_with_inactive_action(self, rev_ids: list) -> Dict[int, dict]:
×
158
        revisions: List[dict] = []
×
159

160
        for _rev_ids in Connection.chunks(rev_ids, PHAB_CHUNK_SIZE):
×
161
            for revision in self._fetch_revisions(_rev_ids):
×
162
                if (
×
163
                    len(revision["attachments"]["reviewers"]["reviewers"]) == 0
164
                    or revision["fields"]["status"]["value"] != "needs-review"
165
                    or revision["fields"]["isDraft"]
166
                ):
167
                    continue
×
168

169
                _, last_transaction = self._find_last_action(revision["id"])
×
170

171
                if (
×
172
                    last_transaction
173
                    and last_transaction["dateCreated"] < self.patch_activity_limit
174
                ):
175
                    reviewers = [
×
176
                        {
177
                            "phid": reviewer["reviewerPHID"],
178
                            "is_group": reviewer["reviewerPHID"].startswith(
179
                                "PHID-PROJ"
180
                            ),
181
                            "is_blocking": reviewer["isBlocking"],
182
                            "is_accepted": reviewer["status"] == "accepted",
183
                            "is_resigned": reviewer["status"] == "resigned",
184
                        }
185
                        for reviewer in revision["attachments"]["reviewers"][
186
                            "reviewers"
187
                        ]
188
                    ]
189

190
                    if any(
×
191
                        reviewer["is_group"] or reviewer["is_accepted"]
192
                        for reviewer in reviewers
193
                    ) and not any(
194
                        not reviewer["is_accepted"]
195
                        for reviewer in reviewers
196
                        if reviewer["is_blocking"]
197
                    ):
198
                        continue
×
199

200
                    revisions.append(
×
201
                        {
202
                            "rev_id": revision["id"],
203
                            "title": revision["fields"]["title"],
204
                            "created_at": revision["fields"]["dateCreated"],
205
                            "author_phid": revision["fields"]["authorPHID"],
206
                            "reviewers": reviewers,
207
                        }
208
                    )
209

210
        user_phids = set()
×
211
        for revision in revisions:
×
212
            user_phids.add(revision["author_phid"])
×
213
            for reviewer in revision["reviewers"]:
×
214
                user_phids.add(reviewer["phid"])
×
215

216
        users = self.user_activity.get_phab_users_with_status(
×
217
            list(user_phids), keep_active=True
218
        )
219

220
        result: Dict[int, dict] = {}
×
221
        for revision in revisions:
×
222
            author_phid = revision["author_phid"]
×
223
            if author_phid in users:
×
224
                author_info = users[author_phid]
×
225
                revision["author"] = author_info
×
226
            else:
227
                continue
×
228

229
            reviewers = []
×
230
            for reviewer in revision["reviewers"]:
×
231
                reviewer_phid = reviewer["phid"]
×
232
                if reviewer_phid in users:
×
233
                    reviewer_info = users[reviewer_phid]
×
234
                    reviewer["info"] = reviewer_info
×
235
                else:
236
                    continue
×
237
                reviewers.append(reviewer)
×
238

239
            revision["reviewers"] = [
×
240
                {
241
                    "phab_username": reviewer["info"]["phab_username"],
242
                }
243
                for reviewer in reviewers
244
            ]
245
            result[revision["rev_id"]] = revision
×
246

247
        return result
×
248

249
    @retry(
×
250
        wait=wait_exponential(min=4),
251
        stop=stop_after_attempt(5),
252
    )
253
    def _fetch_revisions(self, ids: list):
×
254
        return self.phab.request(
×
255
            "differential.revision.search",
256
            constraints={"ids": ids},
257
            attachments={"reviewers": True},
258
        )["data"]
259

260
    def _fetch_revision_transactions(self, revision_phid):
×
261
        response = self.phab.request(
×
262
            "transaction.search", objectIdentifier=revision_phid
263
        )
264
        return response["data"]
×
265

266
    def handle_bug(self, bug, data):
×
267
        rev_ids = [
×
268
            # To avoid loading the attachment content (which can be very large),
269
            # we extract the revision id from the file name, which is in the
270
            # format of "phabricator-D{revision_id}-url.txt".
271
            # len("phabricator-D") == 13
272
            # len("-url.txt") == 8
273
            int(attachment["file_name"][13:-8])
274
            for attachment in bug["attachments"]
275
            if attachment["content_type"] == "text/x-phabricator-request"
276
            and PHAB_FILE_NAME_PAT.match(attachment["file_name"])
277
            and not attachment["is_obsolete"]
278
        ]
279

280
        if not rev_ids:
×
281
            return
×
282

283
        # We should not comment about the same patch more than once.
284
        rev_ids_with_ni = set()
×
285
        for comment in bug["comments"]:
×
286
            if comment["creator"] == History.BOT and comment["raw_text"].startswith(
×
287
                "The following patch"
288
            ):
289
                rev_ids_with_ni.update(
×
290
                    int(id) for id in PHAB_TABLE_PAT.findall(comment["raw_text"])
291
                )
292

293
        if rev_ids_with_ni:
×
294
            rev_ids = [id for id in rev_ids if id not in rev_ids_with_ni]
×
295

296
        if not rev_ids:
×
297
            return
×
298

299
        # It will be nicer to show a sorted list of patches
300
        rev_ids.sort()
×
301

302
        bugid = str(bug["id"])
×
303
        data[bugid] = {
×
304
            "rev_ids": rev_ids,
305
        }
306
        return bug
×
307

308
    def get_bz_params(self, date):
×
309
        fields = [
×
310
            "comments.raw_text",
311
            "comments.creator",
312
            "attachments.file_name",
313
            "attachments.content_type",
314
            "attachments.is_obsolete",
315
        ]
316
        params = {
×
317
            "include_fields": fields,
318
            "resolution": "---",
319
            "f1": "attachments.mimetype",
320
            "o1": "equals",
321
            "v1": "text/x-phabricator-request",
322
        }
323

324
        return params
×
325

326

327
if __name__ == "__main__":
×
NEW
328
    InactiveRevision().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