xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/nightly_revert_checker_test.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2020 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Tests for nightly_revert_checker."""
7
8import unittest
9from unittest import mock
10
11from cros_utils import tiny_render
12import get_upstream_patch
13import nightly_revert_checker
14import revert_checker
15
16
17# pylint: disable=protected-access
18
19
20class Test(unittest.TestCase):
21    """Tests for nightly_revert_checker."""
22
23    def test_email_rendering_works_for_singular_revert(self):
24        def prettify_sha(sha: str) -> tiny_render.Piece:
25            return "pretty_" + sha
26
27        def get_sha_description(sha: str) -> tiny_render.Piece:
28            return "subject_" + sha
29
30        email = nightly_revert_checker._generate_revert_email(
31            repository_name="${repo}",
32            friendly_name="${name}",
33            sha="${sha}",
34            prettify_sha=prettify_sha,
35            get_sha_description=get_sha_description,
36            new_reverts=[
37                revert_checker.Revert(
38                    sha="${revert_sha}", reverted_sha="${reverted_sha}"
39                )
40            ],
41        )
42
43        expected_email = nightly_revert_checker._Email(
44            subject="[revert-checker/${repo}] new revert discovered across "
45            "${name}",
46            body=[
47                "It looks like there may be a new revert across ${name} (",
48                "pretty_${sha}",
49                ").",
50                tiny_render.line_break,
51                tiny_render.line_break,
52                "That is:",
53                tiny_render.UnorderedList(
54                    [
55                        [
56                            "pretty_${revert_sha}",
57                            " (appears to revert ",
58                            "pretty_${reverted_sha}",
59                            "): ",
60                            "subject_${revert_sha}",
61                        ]
62                    ]
63                ),
64                tiny_render.line_break,
65                "PTAL and consider reverting them locally.",
66            ],
67        )
68
69        self.assertEqual(email, expected_email)
70
71    def test_email_rendering_works_for_multiple_reverts(self):
72        def prettify_sha(sha: str) -> tiny_render.Piece:
73            return "pretty_" + sha
74
75        def get_sha_description(sha: str) -> tiny_render.Piece:
76            return "subject_" + sha
77
78        email = nightly_revert_checker._generate_revert_email(
79            repository_name="${repo}",
80            friendly_name="${name}",
81            sha="${sha}",
82            prettify_sha=prettify_sha,
83            get_sha_description=get_sha_description,
84            new_reverts=[
85                revert_checker.Revert(
86                    sha="${revert_sha1}", reverted_sha="${reverted_sha1}"
87                ),
88                revert_checker.Revert(
89                    sha="${revert_sha2}", reverted_sha="${reverted_sha2}"
90                ),
91                # Keep this out-of-order to check that we sort based on SHAs
92                revert_checker.Revert(
93                    sha="${revert_sha0}", reverted_sha="${reverted_sha0}"
94                ),
95            ],
96        )
97
98        expected_email = nightly_revert_checker._Email(
99            subject="[revert-checker/${repo}] new reverts discovered across "
100            "${name}",
101            body=[
102                "It looks like there may be new reverts across ${name} (",
103                "pretty_${sha}",
104                ").",
105                tiny_render.line_break,
106                tiny_render.line_break,
107                "These are:",
108                tiny_render.UnorderedList(
109                    [
110                        [
111                            "pretty_${revert_sha0}",
112                            " (appears to revert ",
113                            "pretty_${reverted_sha0}",
114                            "): ",
115                            "subject_${revert_sha0}",
116                        ],
117                        [
118                            "pretty_${revert_sha1}",
119                            " (appears to revert ",
120                            "pretty_${reverted_sha1}",
121                            "): ",
122                            "subject_${revert_sha1}",
123                        ],
124                        [
125                            "pretty_${revert_sha2}",
126                            " (appears to revert ",
127                            "pretty_${reverted_sha2}",
128                            "): ",
129                            "subject_${revert_sha2}",
130                        ],
131                    ]
132                ),
133                tiny_render.line_break,
134                "PTAL and consider reverting them locally.",
135            ],
136        )
137
138        self.assertEqual(email, expected_email)
139
140    @mock.patch("revert_checker.find_reverts")
141    @mock.patch("get_upstream_patch.get_from_upstream")
142    def test_do_cherrypick_is_called(self, do_cherrypick, find_reverts):
143        find_reverts.return_value = [
144            revert_checker.Revert("12345abcdef", "fedcba54321")
145        ]
146        nightly_revert_checker.do_cherrypick(
147            chroot_path="/path/to/chroot",
148            llvm_dir="/path/to/llvm",
149            repository="repository_name",
150            interesting_shas=[("12345abcdef", "fedcba54321")],
151            state=nightly_revert_checker.State(),
152            reviewers=["[email protected]"],
153            cc=["[email protected]"],
154        )
155
156        do_cherrypick.assert_called_once()
157        find_reverts.assert_called_once()
158
159    @mock.patch("revert_checker.find_reverts")
160    @mock.patch("get_upstream_patch.get_from_upstream")
161    def test_do_cherrypick_handles_cherrypick_error(
162        self, do_cherrypick, find_reverts
163    ):
164        find_reverts.return_value = [
165            revert_checker.Revert("12345abcdef", "fedcba54321")
166        ]
167        do_cherrypick.side_effect = get_upstream_patch.CherrypickError(
168            "Patch at 12345abcdef already exists in PATCHES.json"
169        )
170        nightly_revert_checker.do_cherrypick(
171            chroot_path="/path/to/chroot",
172            llvm_dir="/path/to/llvm",
173            repository="repository_name",
174            interesting_shas=[("12345abcdef", "fedcba54321")],
175            state=nightly_revert_checker.State(),
176            reviewers=["[email protected]"],
177            cc=["[email protected]"],
178        )
179
180        do_cherrypick.assert_called_once()
181        find_reverts.assert_called_once()
182
183    def test_sha_prettification_for_email(self):
184        sha = "a" * 40
185        rev = 123456
186        self.assertEqual(
187            nightly_revert_checker.prettify_sha_for_email(sha, rev),
188            tiny_render.Switch(
189                text=f"r{rev} ({sha[:12]})",
190                html=tiny_render.Link(
191                    href=f"https://github.com/llvm/llvm-project/commit/{sha}",
192                    inner=f"r{rev}",
193                ),
194            ),
195        )
196
197    @mock.patch("time.time")
198    def test_emailing_about_stale_heads_skips_in_simple_cases(self, time_time):
199        now = 1_000_000_000
200        time_time.return_value = now
201
202        def assert_no_email(state: nightly_revert_checker.State):
203            self.assertFalse(
204                nightly_revert_checker.maybe_email_about_stale_heads(
205                    state,
206                    repository_name="foo",
207                    recipients=nightly_revert_checker._EmailRecipients(
208                        well_known=[], direct=[]
209                    ),
210                    prettify_sha=lambda *args: self.fail(
211                        "SHAs shouldn't be prettified"
212                    ),
213                    is_dry_run=True,
214                )
215            )
216
217        assert_no_email(nightly_revert_checker.State())
218        assert_no_email(
219            nightly_revert_checker.State(
220                heads={
221                    "foo": nightly_revert_checker.HeadInfo(
222                        last_sha="",
223                        first_seen_timestamp=0,
224                        next_notification_timestamp=now + 1,
225                    ),
226                    "bar": nightly_revert_checker.HeadInfo(
227                        last_sha="",
228                        first_seen_timestamp=0,
229                        next_notification_timestamp=now * 2,
230                    ),
231                }
232            )
233        )
234
235    def test_state_autoupgrades_from_json_properly(self):
236        state = nightly_revert_checker.State.from_json({"abc123": ["def456"]})
237        self.assertEqual(state.seen_reverts, {"abc123": ["def456"]})
238        self.assertEqual(state.heads, {})
239
240    def test_state_round_trips_through_json(self):
241        state = nightly_revert_checker.State(
242            seen_reverts={"abc123": ["def456"]},
243            heads={
244                "head_name": nightly_revert_checker.HeadInfo(
245                    last_sha="abc",
246                    first_seen_timestamp=123,
247                    next_notification_timestamp=456,
248                ),
249            },
250        )
251        self.assertEqual(
252            state, nightly_revert_checker.State.from_json(state.to_json())
253        )
254
255    @mock.patch("time.time")
256    @mock.patch("nightly_revert_checker._send_revert_email")
257    def test_emailing_about_stale_with_one_report(
258        self, send_revert_email, time_time
259    ):
260        def prettify_sha(sha: str) -> str:
261            return f"pretty({sha})"
262
263        now = 1_000_000_000
264        two_days_ago = now - 2 * nightly_revert_checker.ONE_DAY_SECS
265        time_time.return_value = now
266        recipients = nightly_revert_checker._EmailRecipients(
267            well_known=[], direct=[]
268        )
269        self.assertTrue(
270            nightly_revert_checker.maybe_email_about_stale_heads(
271                nightly_revert_checker.State(
272                    heads={
273                        "foo": nightly_revert_checker.HeadInfo(
274                            last_sha="<foo sha>",
275                            first_seen_timestamp=two_days_ago,
276                            next_notification_timestamp=now - 1,
277                        ),
278                        "bar": nightly_revert_checker.HeadInfo(
279                            last_sha="",
280                            first_seen_timestamp=0,
281                            next_notification_timestamp=now + 1,
282                        ),
283                    }
284                ),
285                repository_name="repo",
286                recipients=recipients,
287                prettify_sha=prettify_sha,
288                is_dry_run=False,
289            )
290        )
291        send_revert_email.assert_called_once()
292        recipients, email = send_revert_email.call_args[0]
293
294        self.assertEqual(
295            tiny_render.render_text_pieces(email.body),
296            "Hi! This is a friendly notification that the current upstream "
297            "LLVM SHA is being tracked by the LLVM revert checker:\n"
298            "  - foo at pretty(<foo sha>), which was last updated ~2 days "
299            "ago.\n"
300            "If that's still correct, great! If it looks wrong, the revert "
301            "checker's SHA autodetection may need an update. Please file a "
302            "bug at go/crostc-bug if an update is needed. Thanks!",
303        )
304
305
306if __name__ == "__main__":
307    unittest.main()
308