xref: /aosp_15_r20/external/toolchain-utils/rust_tools/auto_update_rust_bootstrap_test.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2023 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 auto_update_rust_bootstrap."""
7
8import os
9from pathlib import Path
10import shutil
11import tempfile
12import textwrap
13import unittest
14from unittest import mock
15
16import auto_update_rust_bootstrap
17
18
19_GIT_PUSH_OUTPUT = r"""
20remote: Waiting for private key checker: 2/2 objects left
21remote:
22remote: Processing changes: new: 1 (\)
23remote: Processing changes: new: 1 (|)
24remote: Processing changes: new: 1 (/)
25remote: Processing changes: refs: 1, new: 1 (/)
26remote: Processing changes: refs: 1, new: 1 (/)
27remote: Processing changes: refs: 1, new: 1 (/)
28remote: Processing changes: refs: 1, new: 1, done
29remote:
30remote: SUCCESS
31remote:
32remote:   https://chromium-review.googlesource.com/c/chromiumos/overlays/chromiumos-overlay/+/5018826 rust-bootstrap: use prebuilts [WIP] [NEW]
33remote:
34To https://chromium.googlesource.com/chromiumos/overlays/chromiumos-overlay
35 * [new reference]             HEAD -> refs/for/main
36"""
37
38_GIT_PUSH_MULTI_CL_OUTPUT = r"""
39remote: Waiting for private key checker: 2/2 objects left
40remote:
41remote: Processing changes: new: 1 (\)
42remote: Processing changes: new: 1 (|)
43remote: Processing changes: new: 1 (/)
44remote: Processing changes: refs: 1, new: 1 (/)
45remote: Processing changes: refs: 1, new: 1 (/)
46remote: Processing changes: refs: 1, new: 1 (/)
47remote: Processing changes: refs: 1, new: 1, done
48remote:
49remote: SUCCESS
50remote:
51remote:   https://chromium-review.googlesource.com/c/chromiumos/overlays/chromiumos-overlay/+/5339923 rust-bootstrap: add version 1.75.0 [NEW]
52remote:   https://chromium-review.googlesource.com/c/chromiumos/overlays/chromiumos-overlay/+/5339924 rust-bootstrap: remove unused ebuilds [NEW]
53remote:
54To https://chromium.googlesource.com/chromiumos/overlays/chromiumos-overlay
55 * [new reference]             HEAD -> refs/for/main
56"""
57
58
59class Test(unittest.TestCase):
60    """Tests for auto_update_rust_bootstrap."""
61
62    def make_tempdir(self) -> Path:
63        tempdir = Path(
64            tempfile.mkdtemp(prefix="auto_update_rust_bootstrap_test_")
65        )
66        self.addCleanup(shutil.rmtree, tempdir)
67        return tempdir
68
69    def test_git_cl_id_scraping(self):
70        self.assertEqual(
71            auto_update_rust_bootstrap.scrape_git_push_cl_id_strs(
72                _GIT_PUSH_OUTPUT
73            ),
74            ["5018826"],
75        )
76
77        self.assertEqual(
78            auto_update_rust_bootstrap.scrape_git_push_cl_id_strs(
79                _GIT_PUSH_MULTI_CL_OUTPUT
80            ),
81            ["5339923", "5339924"],
82        )
83
84    def test_ebuild_linking_logic_handles_direct_relative_symlinks(self):
85        tempdir = self.make_tempdir()
86        target = tempdir / "target.ebuild"
87        target.touch()
88        (tempdir / "symlink.ebuild").symlink_to(target.name)
89        self.assertTrue(
90            auto_update_rust_bootstrap.is_ebuild_linked_to_in_dir(target)
91        )
92
93    def test_ebuild_linking_logic_handles_direct_absolute_symlinks(self):
94        tempdir = self.make_tempdir()
95        target = tempdir / "target.ebuild"
96        target.touch()
97        (tempdir / "symlink.ebuild").symlink_to(target)
98        self.assertTrue(
99            auto_update_rust_bootstrap.is_ebuild_linked_to_in_dir(target)
100        )
101
102    def test_ebuild_linking_logic_handles_indirect_relative_symlinks(self):
103        tempdir = self.make_tempdir()
104        target = tempdir / "target.ebuild"
105        target.touch()
106        (tempdir / "symlink.ebuild").symlink_to(
107            Path("..") / tempdir.name / target.name
108        )
109        self.assertTrue(
110            auto_update_rust_bootstrap.is_ebuild_linked_to_in_dir(target)
111        )
112
113    def test_ebuild_linking_logic_handles_broken_symlinks(self):
114        tempdir = self.make_tempdir()
115        target = tempdir / "target.ebuild"
116        target.touch()
117        (tempdir / "symlink.ebuild").symlink_to("doesnt_exist.ebuild")
118        self.assertFalse(
119            auto_update_rust_bootstrap.is_ebuild_linked_to_in_dir(target)
120        )
121
122    def test_ebuild_linking_logic_only_steps_through_one_symlink(self):
123        tempdir = self.make_tempdir()
124        target = tempdir / "target.ebuild"
125        target.symlink_to("doesnt_exist.ebuild")
126        (tempdir / "symlink.ebuild").symlink_to(target.name)
127        self.assertTrue(
128            auto_update_rust_bootstrap.is_ebuild_linked_to_in_dir(target)
129        )
130
131    def test_raw_bootstrap_seq_finding_functions(self):
132        ebuild_contents = textwrap.dedent(
133            """\
134            # Some copyright
135            FOO=bar
136            # Comment about RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
137            RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( # another comment
138                1.2.3 # (with a comment with parens)
139                4.5.6
140            )
141            """
142        )
143
144        ebuild_lines = ebuild_contents.splitlines()
145        (
146            start,
147            end,
148        ) = auto_update_rust_bootstrap.find_raw_bootstrap_sequence_lines(
149            ebuild_lines
150        )
151        self.assertEqual(start, len(ebuild_lines) - 4)
152        self.assertEqual(end, len(ebuild_lines) - 1)
153
154    def test_collect_ebuilds_by_version_ignores_older_versions(self):
155        tempdir = self.make_tempdir()
156        ebuild_170 = tempdir / "rust-bootstrap-1.70.0.ebuild"
157        ebuild_170.touch()
158        ebuild_170_r1 = tempdir / "rust-bootstrap-1.70.0-r1.ebuild"
159        ebuild_170_r1.touch()
160        ebuild_171_r2 = tempdir / "rust-bootstrap-1.71.1-r2.ebuild"
161        ebuild_171_r2.touch()
162
163        self.assertEqual(
164            auto_update_rust_bootstrap.collect_ebuilds_by_version(tempdir),
165            [
166                (
167                    auto_update_rust_bootstrap.EbuildVersion(
168                        major=1, minor=70, patch=0, rev=1
169                    ),
170                    ebuild_170_r1,
171                ),
172                (
173                    auto_update_rust_bootstrap.EbuildVersion(
174                        major=1, minor=71, patch=1, rev=2
175                    ),
176                    ebuild_171_r2,
177                ),
178            ],
179        )
180
181    def test_has_prebuilt_works(self):
182        tempdir = self.make_tempdir()
183        ebuild = tempdir / "rust-bootstrap-1.70.0.ebuild"
184        ebuild.write_text(
185            textwrap.dedent(
186                """\
187                # Some copyright
188                FOO=bar
189                # Comment about RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
190                RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=( # another comment
191                    1.67.0
192                    1.68.1
193                    1.69.0
194                )
195                """
196            ),
197            encoding="utf-8",
198        )
199
200        self.assertTrue(
201            auto_update_rust_bootstrap.version_listed_in_bootstrap_sequence(
202                ebuild,
203                auto_update_rust_bootstrap.EbuildVersion(
204                    major=1,
205                    minor=69,
206                    patch=0,
207                    rev=0,
208                ),
209            )
210        )
211
212        self.assertFalse(
213            auto_update_rust_bootstrap.version_listed_in_bootstrap_sequence(
214                ebuild,
215                auto_update_rust_bootstrap.EbuildVersion(
216                    major=1,
217                    minor=70,
218                    patch=0,
219                    rev=0,
220                ),
221            )
222        )
223
224    def test_ebuild_updating_works(self):
225        tempdir = self.make_tempdir()
226        ebuild = tempdir / "rust-bootstrap-1.70.0.ebuild"
227        ebuild.write_text(
228            textwrap.dedent(
229                """\
230                # Some copyright
231                FOO=bar
232                RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
233                \t1.67.0
234                \t1.68.1
235                \t1.69.0
236                )
237                """
238            ),
239            encoding="utf-8",
240        )
241
242        auto_update_rust_bootstrap.add_version_to_bootstrap_sequence(
243            ebuild,
244            auto_update_rust_bootstrap.EbuildVersion(
245                major=1,
246                minor=70,
247                patch=1,
248                rev=2,
249            ),
250            dry_run=False,
251        )
252
253        self.assertEqual(
254            ebuild.read_text(encoding="utf-8"),
255            textwrap.dedent(
256                """\
257                # Some copyright
258                FOO=bar
259                RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
260                \t1.67.0
261                \t1.68.1
262                \t1.69.0
263                \t1.70.1-r2
264                )
265                """
266            ),
267        )
268
269    def test_ebuild_version_parsing_works(self):
270        self.assertEqual(
271            auto_update_rust_bootstrap.parse_ebuild_version(
272                "rust-bootstrap-1.70.0-r2.ebuild"
273            ),
274            auto_update_rust_bootstrap.EbuildVersion(
275                major=1, minor=70, patch=0, rev=2
276            ),
277        )
278
279        self.assertEqual(
280            auto_update_rust_bootstrap.parse_ebuild_version(
281                "rust-bootstrap-2.80.3.ebuild"
282            ),
283            auto_update_rust_bootstrap.EbuildVersion(
284                major=2, minor=80, patch=3, rev=0
285            ),
286        )
287
288        with self.assertRaises(ValueError):
289            auto_update_rust_bootstrap.parse_ebuild_version(
290                "rust-bootstrap-2.80.3_pre1234.ebuild"
291            )
292
293    def test_raw_ebuild_version_parsing_works(self):
294        self.assertEqual(
295            auto_update_rust_bootstrap.parse_raw_ebuild_version("1.70.0-r2"),
296            auto_update_rust_bootstrap.EbuildVersion(
297                major=1, minor=70, patch=0, rev=2
298            ),
299        )
300
301        with self.assertRaises(ValueError):
302            auto_update_rust_bootstrap.parse_ebuild_version("2.80.3_pre1234")
303
304    def test_ensure_newest_version_does_nothing_if_no_new_rust_version(self):
305        tempdir = self.make_tempdir()
306        rust = tempdir / "rust"
307        rust.mkdir()
308        (rust / "rust-1.70.0-r1.ebuild").touch()
309        rust_bootstrap = tempdir / "rust-bootstrap"
310        rust_bootstrap.mkdir()
311        (rust_bootstrap / "rust-bootstrap-1.70.0.ebuild").touch()
312
313        self.assertFalse(
314            auto_update_rust_bootstrap.maybe_add_new_rust_bootstrap_version(
315                tempdir, rust_bootstrap, dry_run=True
316            )
317        )
318
319    @mock.patch.object(auto_update_rust_bootstrap, "update_ebuild_manifest")
320    def test_ensure_newest_version_upgrades_rust_bootstrap_properly(
321        self, update_ebuild_manifest
322    ):
323        tempdir = self.make_tempdir()
324        rust = tempdir / "rust"
325        rust.mkdir()
326        (rust / "rust-1.71.0-r1.ebuild").touch()
327        rust_bootstrap = tempdir / "rust-bootstrap"
328        rust_bootstrap.mkdir()
329        rust_bootstrap_1_70 = rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild"
330
331        rust_bootstrap_contents = textwrap.dedent(
332            """\
333            # Some copyright
334            FOO=bar
335            RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
336            \t1.67.0
337            \t1.68.1
338            \t1.69.0
339            \t1.70.0-r1
340            )
341            """
342        )
343        rust_bootstrap_1_70.write_text(
344            rust_bootstrap_contents, encoding="utf-8"
345        )
346
347        self.assertTrue(
348            auto_update_rust_bootstrap.maybe_add_new_rust_bootstrap_version(
349                tempdir, rust_bootstrap, dry_run=False, commit=False
350            )
351        )
352        update_ebuild_manifest.assert_called_once()
353        rust_bootstrap_1_71 = rust_bootstrap / "rust-bootstrap-1.71.0.ebuild"
354
355        self.assertTrue(rust_bootstrap_1_70.is_symlink())
356        self.assertEqual(
357            os.readlink(rust_bootstrap_1_70),
358            rust_bootstrap_1_71.name,
359        )
360        self.assertFalse(rust_bootstrap_1_71.is_symlink())
361        self.assertEqual(
362            rust_bootstrap_1_71.read_text(encoding="utf-8"),
363            rust_bootstrap_contents,
364        )
365
366    def test_ensure_newest_version_breaks_if_prebuilt_is_not_available(self):
367        tempdir = self.make_tempdir()
368        rust = tempdir / "rust"
369        rust.mkdir()
370        (rust / "rust-1.71.0-r1.ebuild").touch()
371        rust_bootstrap = tempdir / "rust-bootstrap"
372        rust_bootstrap.mkdir()
373        rust_bootstrap_1_70 = rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild"
374
375        rust_bootstrap_contents = textwrap.dedent(
376            """\
377            # Some copyright
378            FOO=bar
379            RUSTC_RAW_FULL_BOOTSTRAP_SEQUENCE=(
380            \t1.67.0
381            \t1.68.1
382            \t1.69.0
383            # Note: Missing 1.70.0 for rust-bootstrap-1.71.1
384            )
385            """
386        )
387        rust_bootstrap_1_70.write_text(
388            rust_bootstrap_contents, encoding="utf-8"
389        )
390
391        with self.assertRaises(
392            auto_update_rust_bootstrap.MissingRustBootstrapPrebuiltError
393        ):
394            auto_update_rust_bootstrap.maybe_add_new_rust_bootstrap_version(
395                tempdir, rust_bootstrap, dry_run=True
396            )
397
398    def test_version_deletion_does_nothing_if_all_versions_are_needed(self):
399        tempdir = self.make_tempdir()
400        rust = tempdir / "rust"
401        rust.mkdir()
402        (rust / "rust-1.71.0-r1.ebuild").touch()
403        rust_bootstrap = tempdir / "rust-bootstrap"
404        rust_bootstrap.mkdir()
405        (rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild").touch()
406
407        self.assertFalse(
408            auto_update_rust_bootstrap.maybe_delete_old_rust_bootstrap_ebuilds(
409                tempdir, rust_bootstrap, dry_run=True
410            )
411        )
412
413    def test_version_deletion_ignores_newer_than_needed_versions(self):
414        tempdir = self.make_tempdir()
415        rust = tempdir / "rust"
416        rust.mkdir()
417        (rust / "rust-1.71.0-r1.ebuild").touch()
418        rust_bootstrap = tempdir / "rust-bootstrap"
419        rust_bootstrap.mkdir()
420        (rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild").touch()
421        (rust_bootstrap / "rust-bootstrap-1.71.0-r1.ebuild").touch()
422        (rust_bootstrap / "rust-bootstrap-1.72.0.ebuild").touch()
423
424        self.assertFalse(
425            auto_update_rust_bootstrap.maybe_delete_old_rust_bootstrap_ebuilds(
426                tempdir, rust_bootstrap, dry_run=True
427            )
428        )
429
430    @mock.patch.object(auto_update_rust_bootstrap, "update_ebuild_manifest")
431    def test_version_deletion_deletes_old_files(self, update_ebuild_manifest):
432        tempdir = self.make_tempdir()
433        rust = tempdir / "rust"
434        rust.mkdir()
435        (rust / "rust-1.71.0-r1.ebuild").touch()
436        rust_bootstrap = tempdir / "rust-bootstrap"
437        rust_bootstrap.mkdir()
438        needed_rust_bootstrap = (
439            rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild"
440        )
441        needed_rust_bootstrap.touch()
442
443        # There are quite a few of these, so corner-cases are tested.
444
445        # Symlink to outside of the group of files to delete.
446        bootstrap_1_68_symlink = rust_bootstrap / "rust-bootstrap-1.68.0.ebuild"
447        bootstrap_1_68_symlink.symlink_to(needed_rust_bootstrap.name)
448        # Ensure that absolute symlinks are caught.
449        bootstrap_1_68_symlink_abs = (
450            rust_bootstrap / "rust-bootstrap-1.68.0-r1.ebuild"
451        )
452        bootstrap_1_68_symlink_abs.symlink_to(needed_rust_bootstrap)
453        # Regular files should be no issue.
454        bootstrap_1_69_regular = rust_bootstrap / "rust-bootstrap-1.69.0.ebuild"
455        bootstrap_1_69_regular.touch()
456        # Symlinks linking back into the set of files to delete should also be
457        # no issue.
458        bootstrap_1_69_symlink = (
459            rust_bootstrap / "rust-bootstrap-1.69.0-r2.ebuild"
460        )
461        bootstrap_1_69_symlink.symlink_to(bootstrap_1_69_regular.name)
462
463        self.assertTrue(
464            auto_update_rust_bootstrap.maybe_delete_old_rust_bootstrap_ebuilds(
465                tempdir,
466                rust_bootstrap,
467                dry_run=False,
468                commit=False,
469            )
470        )
471        update_ebuild_manifest.assert_called_once()
472
473        self.assertFalse(bootstrap_1_68_symlink.exists())
474        self.assertFalse(bootstrap_1_68_symlink_abs.exists())
475        self.assertFalse(bootstrap_1_69_regular.exists())
476        self.assertFalse(bootstrap_1_69_symlink.exists())
477        self.assertTrue(needed_rust_bootstrap.exists())
478
479    def test_version_deletion_raises_when_old_file_has_dep(self):
480        tempdir = self.make_tempdir()
481        rust = tempdir / "rust"
482        rust.mkdir()
483        (rust / "rust-1.71.0-r1.ebuild").touch()
484        rust_bootstrap = tempdir / "rust-bootstrap"
485        rust_bootstrap.mkdir()
486        old_rust_bootstrap = rust_bootstrap / "rust-bootstrap-1.69.0-r1.ebuild"
487        old_rust_bootstrap.touch()
488        (rust_bootstrap / "rust-bootstrap-1.70.0-r2.ebuild").symlink_to(
489            old_rust_bootstrap.name
490        )
491
492        with self.assertRaises(
493            auto_update_rust_bootstrap.OldEbuildIsLinkedToError
494        ):
495            auto_update_rust_bootstrap.maybe_delete_old_rust_bootstrap_ebuilds(
496                tempdir, rust_bootstrap, dry_run=True
497            )
498
499
500if __name__ == "__main__":
501    unittest.main()
502