xref: /aosp_15_r20/external/toolchain-utils/rust_tools/rust_uprev_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 rust_uprev.py"""
7
8import os
9from pathlib import Path
10import shutil
11import subprocess
12import tempfile
13import unittest
14from unittest import mock
15
16from llvm_tools import git
17
18
19# rust_uprev sets SOURCE_ROOT to the output of `repo --show-toplevel`.
20# The mock below makes us not actually run repo but use a fake value
21# instead.
22with mock.patch("subprocess.check_output", return_value="/fake/chromiumos"):
23    import rust_uprev
24
25
26def _fail_command(cmd, *_args, **_kwargs):
27    err = subprocess.CalledProcessError(returncode=1, cmd=cmd)
28    err.stderr = b"mock failure"
29    raise err
30
31
32def start_mock(obj, *args, **kwargs):
33    """Creates a patcher, starts it, and registers a cleanup to stop it.
34
35    Args:
36        obj:
37            the object to attach the cleanup to
38        *args:
39            passed to mock.patch()
40        **kwargs:
41            passsed to mock.patch()
42    """
43    patcher = mock.patch(*args, **kwargs)
44    val = patcher.start()
45    obj.addCleanup(patcher.stop)
46    return val
47
48
49class FetchDistfileTest(unittest.TestCase):
50    """Tests rust_uprev.fetch_distfile_from_mirror()"""
51
52    @mock.patch.object(
53        rust_uprev, "get_distdir", return_value=Path("/fake/distfiles")
54    )
55    @mock.patch.object(subprocess, "call", side_effect=_fail_command)
56    def test_fetch_difstfile_fail(self, *_args) -> None:
57        with self.assertRaises(subprocess.CalledProcessError):
58            rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz")
59
60    @mock.patch.object(
61        rust_uprev,
62        "get_command_output_unchecked",
63        return_value="AccessDeniedException: Access denied.",
64    )
65    @mock.patch.object(
66        rust_uprev, "get_distdir", return_value=Path("/fake/distfiles")
67    )
68    @mock.patch.object(subprocess, "call", return_value=0)
69    def test_fetch_distfile_acl_access_denied(self, *_args) -> None:
70        rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz")
71
72    @mock.patch.object(
73        rust_uprev,
74        "get_command_output_unchecked",
75        return_value='[ { "entity": "allUsers", "role": "READER" } ]',
76    )
77    @mock.patch.object(
78        rust_uprev, "get_distdir", return_value=Path("/fake/distfiles")
79    )
80    @mock.patch.object(subprocess, "call", return_value=0)
81    def test_fetch_distfile_acl_ok(self, *_args) -> None:
82        rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz")
83
84    @mock.patch.object(
85        rust_uprev,
86        "get_command_output_unchecked",
87        return_value='[ { "entity": "[email protected]", "role": "OWNER" } ]',
88    )
89    @mock.patch.object(
90        rust_uprev, "get_distdir", return_value=Path("/fake/distfiles")
91    )
92    @mock.patch.object(subprocess, "call", return_value=0)
93    def test_fetch_distfile_acl_wrong(self, *_args) -> None:
94        with self.assertRaisesRegex(Exception, "allUsers.*READER"):
95            with self.assertLogs(level="ERROR") as log:
96                rust_uprev.fetch_distfile_from_mirror("test_distfile.tar.gz")
97                self.assertIn(
98                    '[ { "entity": "[email protected]", "role": "OWNER" } ]',
99                    "\n".join(log.output),
100                )
101
102
103class FetchRustSrcFromUpstreamTest(unittest.TestCase):
104    """Tests for rust_uprev.fetch_rust_src_from_upstream."""
105
106    def setUp(self) -> None:
107        self._mock_get_distdir = start_mock(
108            self,
109            "rust_uprev.get_distdir",
110            return_value=Path("/fake/distfiles"),
111        )
112
113        self._mock_gpg = start_mock(
114            self,
115            "subprocess.run",
116            side_effect=self.fake_gpg,
117        )
118
119        self._mock_urlretrieve = start_mock(
120            self,
121            "urllib.request.urlretrieve",
122            side_effect=self.fake_urlretrieve,
123        )
124
125        self._mock_rust_signing_key = start_mock(
126            self,
127            "rust_uprev.RUST_SIGNING_KEY",
128            "1234567",
129        )
130
131    @staticmethod
132    def fake_urlretrieve(src: str, dest: Path) -> None:
133        pass
134
135    @staticmethod
136    def fake_gpg(cmd, **_kwargs):
137        val = mock.Mock()
138        val.returncode = 0
139        val.stdout = ""
140        if "--verify" in cmd:
141            val.stdout = "GOODSIG 1234567"
142        return val
143
144    def test_success(self):
145        with mock.patch("rust_uprev.GPG", "gnupg"):
146            rust_uprev.fetch_rust_src_from_upstream(
147                "fakehttps://rustc-1.60.3-src.tar.gz",
148                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
149            )
150            self._mock_urlretrieve.has_calls(
151                [
152                    (
153                        "fakehttps://rustc-1.60.3-src.tar.gz",
154                        Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
155                    ),
156                    (
157                        "fakehttps://rustc-1.60.3-src.tar.gz.asc",
158                        Path("/fake/distfiles/rustc-1.60.3-src.tar.gz.asc"),
159                    ),
160                ]
161            )
162            self._mock_gpg.has_calls(
163                [
164                    (["gnupg", "--refresh-keys", "1234567"], {"check": True}),
165                ]
166            )
167
168    def test_no_signature_file(self):
169        def _urlretrieve(src, dest):
170            if src.endswith(".asc"):
171                raise Exception("404 not found")
172            return self.fake_urlretrieve(src, dest)
173
174        self._mock_urlretrieve.side_effect = _urlretrieve
175
176        with self.assertRaises(rust_uprev.SignatureVerificationError) as ctx:
177            rust_uprev.fetch_rust_src_from_upstream(
178                "fakehttps://rustc-1.60.3-src.tar.gz",
179                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
180            )
181        self.assertIn("error fetching signature file", ctx.exception.message)
182
183    def test_key_expired(self):
184        def _gpg_verify(cmd, *args, **kwargs):
185            val = self.fake_gpg(cmd, *args, **kwargs)
186            if "--verify" in cmd:
187                val.stdout = "EXPKEYSIG 1234567"
188            return val
189
190        self._mock_gpg.side_effect = _gpg_verify
191
192        with self.assertRaises(rust_uprev.SignatureVerificationError) as ctx:
193            rust_uprev.fetch_rust_src_from_upstream(
194                "fakehttps://rustc-1.60.3-src.tar.gz",
195                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
196            )
197        self.assertIn("key has expired", ctx.exception.message)
198
199    def test_key_revoked(self):
200        def _gpg_verify(cmd, *args, **kwargs):
201            val = self.fake_gpg(cmd, *args, **kwargs)
202            if "--verify" in cmd:
203                val.stdout = "REVKEYSIG 1234567"
204            return val
205
206        self._mock_gpg.side_effect = _gpg_verify
207
208        with self.assertRaises(rust_uprev.SignatureVerificationError) as ctx:
209            rust_uprev.fetch_rust_src_from_upstream(
210                "fakehttps://rustc-1.60.3-src.tar.gz",
211                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
212            )
213        self.assertIn("key has been revoked", ctx.exception.message)
214
215    def test_signature_expired(self):
216        def _gpg_verify(cmd, *args, **kwargs):
217            val = self.fake_gpg(cmd, *args, **kwargs)
218            if "--verify" in cmd:
219                val.stdout = "EXPSIG 1234567"
220            return val
221
222        self._mock_gpg.side_effect = _gpg_verify
223
224        with self.assertRaises(rust_uprev.SignatureVerificationError) as ctx:
225            rust_uprev.fetch_rust_src_from_upstream(
226                "fakehttps://rustc-1.60.3-src.tar.gz",
227                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
228            )
229        self.assertIn("signature has expired", ctx.exception.message)
230
231    def test_wrong_key(self):
232        def _gpg_verify(cmd, *args, **kwargs):
233            val = self.fake_gpg(cmd, *args, **kwargs)
234            if "--verify" in cmd:
235                val.stdout = "GOODSIG 0000000"
236            return val
237
238        self._mock_gpg.side_effect = _gpg_verify
239
240        with self.assertRaises(rust_uprev.SignatureVerificationError) as ctx:
241            rust_uprev.fetch_rust_src_from_upstream(
242                "fakehttps://rustc-1.60.3-src.tar.gz",
243                Path("/fake/distfiles/rustc-1.60.3-src.tar.gz"),
244            )
245        self.assertIn("1234567 not found", ctx.exception.message)
246
247
248class FindEbuildPathTest(unittest.TestCase):
249    """Tests for rust_uprev.find_ebuild_path()"""
250
251    def test_exact_version(self):
252        with tempfile.TemporaryDirectory() as t:
253            tmpdir = Path(t)
254            ebuild = tmpdir / "test-1.3.4.ebuild"
255            ebuild.touch()
256            (tmpdir / "test-1.2.3.ebuild").touch()
257            result = rust_uprev.find_ebuild_path(
258                tmpdir, "test", rust_uprev.RustVersion(1, 3, 4)
259            )
260            self.assertEqual(result, ebuild)
261
262    def test_no_version(self):
263        with tempfile.TemporaryDirectory() as t:
264            tmpdir = Path(t)
265            ebuild = tmpdir / "test-1.2.3.ebuild"
266            ebuild.touch()
267            result = rust_uprev.find_ebuild_path(tmpdir, "test")
268            self.assertEqual(result, ebuild)
269
270    def test_patch_version(self):
271        with tempfile.TemporaryDirectory() as t:
272            tmpdir = Path(t)
273            ebuild = tmpdir / "test-1.3.4-r3.ebuild"
274            ebuild.touch()
275            (tmpdir / "test-1.2.3.ebuild").touch()
276            result = rust_uprev.find_ebuild_path(
277                tmpdir, "test", rust_uprev.RustVersion(1, 3, 4)
278            )
279            self.assertEqual(result, ebuild)
280
281    def test_multiple_versions(self):
282        with tempfile.TemporaryDirectory() as t:
283            tmpdir = Path(t)
284            (tmpdir / "test-1.3.4-r3.ebuild").touch()
285            (tmpdir / "test-1.3.5.ebuild").touch()
286            with self.assertRaises(AssertionError):
287                rust_uprev.find_ebuild_path(tmpdir, "test")
288
289    def test_selected_version(self):
290        with tempfile.TemporaryDirectory() as t:
291            tmpdir = Path(t)
292            ebuild = tmpdir / "test-1.3.4-r3.ebuild"
293            ebuild.touch()
294            (tmpdir / "test-1.3.5.ebuild").touch()
295            result = rust_uprev.find_ebuild_path(
296                tmpdir, "test", rust_uprev.RustVersion(1, 3, 4)
297            )
298            self.assertEqual(result, ebuild)
299
300    def test_symlink(self):
301        # Symlinks to ebuilds in the same directory are allowed, and the return
302        # value is the regular file.
303        with tempfile.TemporaryDirectory() as t:
304            tmpdir = Path(t)
305            ebuild = tmpdir / "test-1.3.4.ebuild"
306            ebuild.touch()
307            (tmpdir / "test-1.3.4-r1.ebuild").symlink_to("test-1.3.4.ebuild")
308            result = rust_uprev.find_ebuild_path(tmpdir, "test")
309            self.assertEqual(result, ebuild)
310
311
312class FindRustVersionsTest(unittest.TestCase):
313    """Tests for rust_uprev.find_rust_versions."""
314
315    def test_with_symlinks(self):
316        with tempfile.TemporaryDirectory() as t:
317            tmpdir = Path(t)
318            rust_1_49_1_ebuild = tmpdir / "rust-1.49.1.ebuild"
319            rust_1_50_0_ebuild = tmpdir / "rust-1.50.0.ebuild"
320            rust_1_50_0_r1_ebuild = tmpdir / "rust-1.50.0-r1.ebuild"
321            rust_1_49_1_ebuild.touch()
322            rust_1_50_0_ebuild.touch()
323            rust_1_50_0_r1_ebuild.symlink_to(rust_1_50_0_ebuild)
324            with mock.patch("rust_uprev.RUST_PATH", tmpdir):
325                actual = rust_uprev.find_rust_versions()
326                expected = [
327                    (rust_uprev.RustVersion(1, 49, 1), rust_1_49_1_ebuild),
328                    (rust_uprev.RustVersion(1, 50, 0), rust_1_50_0_ebuild),
329                ]
330                self.assertEqual(actual, expected)
331
332
333class MirrorHasFileTest(unittest.TestCase):
334    """Tests for rust_uprev.mirror_has_file."""
335
336    @mock.patch.object(subprocess, "run")
337    def test_no(self, mock_run):
338        mock_run.return_value = mock.Mock(
339            returncode=1,
340            stdout="CommandException: One or more URLs matched no objects.",
341        )
342        self.assertFalse(rust_uprev.mirror_has_file("rustc-1.69.0-src.tar.gz"))
343
344    @mock.patch.object(subprocess, "run")
345    def test_yes(self, mock_run):
346        mock_run.return_value = mock.Mock(
347            returncode=0,
348            # pylint: disable=line-too-long
349            stdout="gs://chromeos-localmirror/distfiles/rustc-1.69.0-src.tar.gz",
350        )
351        self.assertTrue(rust_uprev.mirror_has_file("rustc-1.69.0-src.tar.gz"))
352
353
354class MirrorRustSourceTest(unittest.TestCase):
355    """Tests for rust_uprev.mirror_rust_source."""
356
357    def setUp(self) -> None:
358        start_mock(self, "rust_uprev.GSUTIL", "gsutil")
359        start_mock(self, "rust_uprev.MIRROR_PATH", "fakegs://fakemirror/")
360        start_mock(
361            self, "rust_uprev.get_distdir", return_value=Path("/fake/distfiles")
362        )
363        self.mock_mirror_has_file = start_mock(
364            self,
365            "rust_uprev.mirror_has_file",
366        )
367        self.mock_fetch_rust_src_from_upstream = start_mock(
368            self,
369            "rust_uprev.fetch_rust_src_from_upstream",
370        )
371        self.mock_subprocess_run = start_mock(
372            self,
373            "subprocess.run",
374        )
375
376    def test_already_present(self):
377        self.mock_mirror_has_file.return_value = True
378        rust_uprev.mirror_rust_source(
379            rust_uprev.RustVersion.parse("1.67.3"),
380        )
381        self.mock_fetch_rust_src_from_upstream.assert_not_called()
382        self.mock_subprocess_run.assert_not_called()
383
384    def test_fetch_and_upload(self):
385        self.mock_mirror_has_file.return_value = False
386        rust_uprev.mirror_rust_source(
387            rust_uprev.RustVersion.parse("1.67.3"),
388        )
389        self.mock_fetch_rust_src_from_upstream.called_once()
390        self.mock_subprocess_run.has_calls(
391            [
392                (
393                    [
394                        "gsutil",
395                        "cp",
396                        "-a",
397                        "public-read",
398                        "/fake/distdir/rustc-1.67.3-src.tar.gz",
399                        "fakegs://fakemirror/rustc-1.67.3-src.tar.gz",
400                    ]
401                ),
402            ]
403        )
404
405
406class RemoveEbuildVersionTest(unittest.TestCase):
407    """Tests for rust_uprev.remove_ebuild_version()"""
408
409    @mock.patch.object(subprocess, "check_call")
410    def test_single(self, check_call):
411        with tempfile.TemporaryDirectory() as tmpdir:
412            ebuild_dir = Path(tmpdir, "test-ebuilds")
413            ebuild_dir.mkdir()
414            ebuild = Path(ebuild_dir, "test-3.1.4.ebuild")
415            ebuild.touch()
416            Path(ebuild_dir, "unrelated-1.0.0.ebuild").touch()
417            rust_uprev.remove_ebuild_version(
418                ebuild_dir, "test", rust_uprev.RustVersion(3, 1, 4)
419            )
420            check_call.assert_called_once_with(
421                ["git", "rm", "test-3.1.4.ebuild"], cwd=ebuild_dir
422            )
423
424    @mock.patch.object(subprocess, "check_call")
425    def test_symlink(self, check_call):
426        with tempfile.TemporaryDirectory() as tmpdir:
427            ebuild_dir = Path(tmpdir, "test-ebuilds")
428            ebuild_dir.mkdir()
429            ebuild = Path(ebuild_dir, "test-3.1.4.ebuild")
430            ebuild.touch()
431            symlink = Path(ebuild_dir, "test-3.1.4-r5.ebuild")
432            symlink.symlink_to(ebuild.name)
433            Path(ebuild_dir, "unrelated-1.0.0.ebuild").touch()
434            rust_uprev.remove_ebuild_version(
435                ebuild_dir, "test", rust_uprev.RustVersion(3, 1, 4)
436            )
437            check_call.assert_has_calls(
438                [
439                    mock.call(
440                        ["git", "rm", "test-3.1.4.ebuild"], cwd=ebuild_dir
441                    ),
442                    mock.call(
443                        ["git", "rm", "test-3.1.4-r5.ebuild"], cwd=ebuild_dir
444                    ),
445                ],
446                any_order=True,
447            )
448
449
450class RustVersionTest(unittest.TestCase):
451    """Tests for RustVersion class"""
452
453    def test_str(self):
454        obj = rust_uprev.RustVersion(major=1, minor=2, patch=3)
455        self.assertEqual(str(obj), "1.2.3")
456
457    def test_parse_version_only(self):
458        expected = rust_uprev.RustVersion(major=1, minor=2, patch=3)
459        actual = rust_uprev.RustVersion.parse("1.2.3")
460        self.assertEqual(expected, actual)
461
462    def test_parse_ebuild_name(self):
463        expected = rust_uprev.RustVersion(major=1, minor=2, patch=3)
464        actual = rust_uprev.RustVersion.parse_from_ebuild("rust-1.2.3.ebuild")
465        self.assertEqual(expected, actual)
466
467        actual = rust_uprev.RustVersion.parse_from_ebuild(
468            "rust-1.2.3-r1.ebuild"
469        )
470        self.assertEqual(expected, actual)
471
472    def test_parse_fail(self):
473        with self.assertRaises(AssertionError) as context:
474            rust_uprev.RustVersion.parse("invalid-rust-1.2.3")
475        self.assertEqual(
476            "failed to parse 'invalid-rust-1.2.3'", str(context.exception)
477        )
478
479
480class PrepareUprevTest(unittest.TestCase):
481    """Tests for prepare_uprev step in rust_uprev"""
482
483    def setUp(self):
484        self.version_old = rust_uprev.RustVersion(1, 2, 3)
485        self.version_new = rust_uprev.RustVersion(1, 3, 5)
486
487    @mock.patch.object(
488        rust_uprev,
489        "find_ebuild_for_rust_version",
490        return_value=Path("/path/to/ebuild"),
491    )
492    @mock.patch.object(rust_uprev, "get_command_output")
493    def test_success_with_template(self, mock_command, _ebuild_for_version):
494        expected = rust_uprev.PreparedUprev(self.version_old)
495        actual = rust_uprev.prepare_uprev(
496            rust_version=self.version_new, template=self.version_old
497        )
498        self.assertEqual(expected, actual)
499        mock_command.assert_not_called()
500
501    @mock.patch.object(
502        rust_uprev,
503        "find_ebuild_for_rust_version",
504        return_value="/path/to/ebuild",
505    )
506    @mock.patch.object(rust_uprev, "get_command_output")
507    def test_return_none_with_template_larger_than_input(
508        self, mock_command, *_args
509    ):
510        ret = rust_uprev.prepare_uprev(
511            rust_version=self.version_old, template=self.version_new
512        )
513        self.assertIsNone(ret)
514        mock_command.assert_not_called()
515
516    def test_prepare_uprev_from_json(self):
517        json_result = (list(self.version_new),)
518        expected = rust_uprev.PreparedUprev(
519            self.version_new,
520        )
521        actual = rust_uprev.prepare_uprev_from_json(json_result)
522        self.assertEqual(expected, actual)
523
524
525class ToggleProfileData(unittest.TestCase):
526    """Tests functionality to include or exclude profile data from SRC_URI."""
527
528    ebuild_with_profdata = """
529# Some text here.
530INCLUDE_PROFDATA_IN_SRC_URI=yes
531some code here
532"""
533
534    ebuild_without_profdata = """
535# Some text here.
536INCLUDE_PROFDATA_IN_SRC_URI=
537some code here
538"""
539
540    ebuild_unexpected_content = """
541# Does not contain OMIT_PROFDATA_FROM_SRC_URI assignment
542"""
543
544    def setUp(self):
545        self.mock_read_text = start_mock(self, "pathlib.Path.read_text")
546
547    def test_turn_off_profdata(self):
548        # Test that a file with profdata on is rewritten to a file with
549        # profdata off.
550        self.mock_read_text.return_value = self.ebuild_with_profdata
551        ebuild_file = "/path/to/eclass/cros-rustc.eclass"
552        with mock.patch("pathlib.Path.write_text") as mock_write_text:
553            rust_uprev.set_include_profdata_src(ebuild_file, include=False)
554            mock_write_text.assert_called_once_with(
555                self.ebuild_without_profdata, encoding="utf-8"
556            )
557
558    def test_turn_on_profdata(self):
559        # Test that a file with profdata off is rewritten to a file with
560        # profdata on.
561        self.mock_read_text.return_value = self.ebuild_without_profdata
562        ebuild_file = "/path/to/eclass/cros-rustc.eclass"
563        with mock.patch("pathlib.Path.write_text") as mock_write_text:
564            rust_uprev.set_include_profdata_src(ebuild_file, include=True)
565            mock_write_text.assert_called_once_with(
566                self.ebuild_with_profdata, encoding="utf-8"
567            )
568
569    def test_turn_on_profdata_fails_if_no_assignment(self):
570        # Test that if the string the code expects to find is not found,
571        # this causes an exception and the file is not overwritten.
572        self.mock_read_text.return_value = self.ebuild_unexpected_content
573        ebuild_file = "/path/to/eclass/cros-rustc.eclass"
574        with mock.patch("pathlib.Path.write_text") as mock_write_text:
575            with self.assertRaises(Exception):
576                rust_uprev.set_include_profdata_src(ebuild_file, include=False)
577            mock_write_text.assert_not_called()
578
579
580class UpdateBootstrapVersionTest(unittest.TestCase):
581    """Tests for update_bootstrap_version step in rust_uprev"""
582
583    ebuild_file_before = """
584BOOTSTRAP_VERSION="1.2.0"
585    """
586    ebuild_file_after = """
587BOOTSTRAP_VERSION="1.3.6"
588    """
589
590    def setUp(self):
591        self.mock_read_text = start_mock(self, "pathlib.Path.read_text")
592
593    def test_success(self):
594        self.mock_read_text.return_value = self.ebuild_file_before
595        # ebuild_file and new bootstrap version are deliberately different
596        ebuild_file = "/path/to/rust/cros-rustc.eclass"
597        with mock.patch("pathlib.Path.write_text") as mock_write_text:
598            rust_uprev.update_bootstrap_version(
599                ebuild_file, rust_uprev.RustVersion.parse("1.3.6")
600            )
601            mock_write_text.assert_called_once_with(
602                self.ebuild_file_after, encoding="utf-8"
603            )
604
605    def test_fail_when_ebuild_misses_a_variable(self):
606        self.mock_read_text.return_value = ""
607        ebuild_file = "/path/to/rust/rust-1.3.5.ebuild"
608        with self.assertRaises(RuntimeError) as context:
609            rust_uprev.update_bootstrap_version(
610                ebuild_file, rust_uprev.RustVersion.parse("1.2.0")
611            )
612        self.assertEqual(
613            "BOOTSTRAP_VERSION not found in /path/to/rust/rust-1.3.5.ebuild",
614            str(context.exception),
615        )
616
617
618class UpdateRustPackagesTests(unittest.TestCase):
619    """Tests for update_rust_packages step."""
620
621    def setUp(self):
622        self.old_version = rust_uprev.RustVersion(1, 1, 0)
623        self.current_version = rust_uprev.RustVersion(1, 2, 3)
624        self.new_version = rust_uprev.RustVersion(1, 3, 5)
625        self.ebuild_file = os.path.join(
626            rust_uprev.RUST_PATH, "rust-{self.new_version}.ebuild"
627        )
628
629    def test_add_new_rust_packages(self):
630        package_before = (
631            f"dev-lang/rust-{self.old_version}\n"
632            f"dev-lang/rust-{self.current_version}"
633        )
634        package_after = (
635            f"dev-lang/rust-{self.old_version}\n"
636            f"dev-lang/rust-{self.current_version}\n"
637            f"dev-lang/rust-{self.new_version}"
638        )
639        mock_open = mock.mock_open(read_data=package_before)
640        with mock.patch("builtins.open", mock_open):
641            rust_uprev.update_rust_packages(
642                "dev-lang/rust", self.new_version, add=True
643            )
644        mock_open.return_value.__enter__().write.assert_called_once_with(
645            package_after
646        )
647
648    def test_remove_old_rust_packages(self):
649        package_before = (
650            f"dev-lang/rust-{self.old_version}\n"
651            f"dev-lang/rust-{self.current_version}\n"
652            f"dev-lang/rust-{self.new_version}"
653        )
654        package_after = (
655            f"dev-lang/rust-{self.current_version}\n"
656            f"dev-lang/rust-{self.new_version}"
657        )
658        mock_open = mock.mock_open(read_data=package_before)
659        with mock.patch("builtins.open", mock_open):
660            rust_uprev.update_rust_packages(
661                "dev-lang/rust", self.old_version, add=False
662            )
663        mock_open.return_value.__enter__().write.assert_called_once_with(
664            package_after
665        )
666
667
668class RustUprevOtherStagesTests(unittest.TestCase):
669    """Tests for other steps in rust_uprev"""
670
671    def setUp(self):
672        self.old_version = rust_uprev.RustVersion(1, 1, 0)
673        self.current_version = rust_uprev.RustVersion(1, 2, 3)
674        self.new_version = rust_uprev.RustVersion(1, 3, 5)
675        self.ebuild_file = os.path.join(
676            rust_uprev.RUST_PATH, "rust-{self.new_version}.ebuild"
677        )
678
679    @mock.patch.object(shutil, "copyfile")
680    @mock.patch.object(subprocess, "check_call")
681    def test_create_rust_ebuild(self, mock_call, mock_copy):
682        template_ebuild = (
683            rust_uprev.EBUILD_PREFIX
684            / f"dev-lang/rust/rust-{self.current_version}.ebuild"
685        )
686        new_ebuild = (
687            rust_uprev.EBUILD_PREFIX
688            / f"dev-lang/rust/rust-{self.new_version}.ebuild"
689        )
690        rust_uprev.create_ebuild(
691            "dev-lang", "rust", self.current_version, self.new_version
692        )
693        mock_copy.assert_called_once_with(
694            template_ebuild,
695            new_ebuild,
696        )
697        mock_call.assert_called_once_with(
698            ["git", "add", f"rust-{self.new_version}.ebuild"],
699            cwd=new_ebuild.parent,
700        )
701
702    @mock.patch.object(shutil, "copyfile")
703    @mock.patch.object(subprocess, "check_call")
704    def test_create_rust_host_ebuild(self, mock_call, mock_copy):
705        template_ebuild = (
706            rust_uprev.EBUILD_PREFIX
707            / f"dev-lang/rust-host/rust-host-{self.current_version}.ebuild"
708        )
709        new_ebuild = (
710            rust_uprev.EBUILD_PREFIX
711            / f"dev-lang/rust-host/rust-host-{self.new_version}.ebuild"
712        )
713        rust_uprev.create_ebuild(
714            "dev-lang", "rust-host", self.current_version, self.new_version
715        )
716        mock_copy.assert_called_once_with(
717            template_ebuild,
718            new_ebuild,
719        )
720        mock_call.assert_called_once_with(
721            ["git", "add", f"rust-host-{self.new_version}.ebuild"],
722            cwd=new_ebuild.parent,
723        )
724
725    @mock.patch.object(subprocess, "check_call")
726    def test_remove_virtual_rust(self, mock_call):
727        with tempfile.TemporaryDirectory() as tmpdir:
728            ebuild_path = Path(
729                tmpdir, f"virtual/rust/rust-{self.old_version}.ebuild"
730            )
731            os.makedirs(ebuild_path.parent)
732            ebuild_path.touch()
733            with mock.patch("rust_uprev.EBUILD_PREFIX", Path(tmpdir)):
734                rust_uprev.remove_virtual_rust(self.old_version)
735                mock_call.assert_called_once_with(
736                    ["git", "rm", str(ebuild_path.name)], cwd=ebuild_path.parent
737                )
738
739    @mock.patch.object(subprocess, "check_call")
740    def test_remove_virtual_rust_with_symlink(self, mock_call):
741        with tempfile.TemporaryDirectory() as tmpdir:
742            ebuild_path = Path(
743                tmpdir, f"virtual/rust/rust-{self.old_version}.ebuild"
744            )
745            symlink_path = Path(
746                tmpdir, f"virtual/rust/rust-{self.old_version}-r14.ebuild"
747            )
748            os.makedirs(ebuild_path.parent)
749            ebuild_path.touch()
750            symlink_path.symlink_to(ebuild_path.name)
751            with mock.patch("rust_uprev.EBUILD_PREFIX", Path(tmpdir)):
752                rust_uprev.remove_virtual_rust(self.old_version)
753                mock_call.assert_has_calls(
754                    [
755                        mock.call(
756                            ["git", "rm", ebuild_path.name],
757                            cwd=ebuild_path.parent,
758                        ),
759                        mock.call(
760                            ["git", "rm", symlink_path.name],
761                            cwd=ebuild_path.parent,
762                        ),
763                    ],
764                    any_order=True,
765                )
766
767    @mock.patch.object(rust_uprev, "find_ebuild_path")
768    @mock.patch.object(shutil, "copyfile")
769    @mock.patch.object(subprocess, "check_call")
770    def test_update_virtual_rust(self, mock_call, mock_copy, mock_find_ebuild):
771        ebuild_path = Path(
772            f"/some/dir/virtual/rust/rust-{self.current_version}.ebuild"
773        )
774        mock_find_ebuild.return_value = Path(ebuild_path)
775        rust_uprev.update_virtual_rust(self.current_version, self.new_version)
776        mock_call.assert_called_once_with(
777            ["git", "add", f"rust-{self.new_version}.ebuild"],
778            cwd=ebuild_path.parent,
779        )
780        mock_copy.assert_called_once_with(
781            ebuild_path.parent.joinpath(f"rust-{self.current_version}.ebuild"),
782            ebuild_path.parent.joinpath(f"rust-{self.new_version}.ebuild"),
783        )
784
785    @mock.patch("rust_uprev.find_rust_versions")
786    def test_find_oldest_rust_version_pass(self, rust_versions):
787        oldest_version_name = f"rust-{self.old_version}.ebuild"
788        rust_versions.return_value = [
789            (self.old_version, oldest_version_name),
790            (self.current_version, f"rust-{self.current_version}.ebuild"),
791            (self.new_version, f"rust-{self.new_version}.ebuild"),
792        ]
793        actual = rust_uprev.find_oldest_rust_version()
794        expected = self.old_version
795        self.assertEqual(expected, actual)
796
797    @mock.patch("rust_uprev.find_rust_versions")
798    def test_find_oldest_rust_version_fail_with_only_one_ebuild(
799        self, rust_versions
800    ):
801        rust_versions.return_value = [
802            (self.new_version, f"rust-{self.new_version}.ebuild"),
803        ]
804        with self.assertRaises(RuntimeError) as context:
805            rust_uprev.find_oldest_rust_version()
806        self.assertEqual(
807            "Expect to find more than one Rust versions", str(context.exception)
808        )
809
810    @mock.patch.object(rust_uprev, "get_command_output")
811    @mock.patch.object(git, "CreateBranch")
812    def test_create_new_repo(self, mock_branch, mock_output):
813        mock_output.return_value = ""
814        rust_uprev.create_new_repo(self.new_version)
815        mock_branch.assert_called_once_with(
816            rust_uprev.EBUILD_PREFIX, f"rust-to-{self.new_version}"
817        )
818
819    @mock.patch.object(rust_uprev, "run_in_chroot")
820    def test_build_cross_compiler(self, mock_run_in_chroot):
821        cros_targets = [
822            "x86_64-cros-linux-gnu",
823            "armv7a-cros-linux-gnueabihf",
824            "aarch64-cros-linux-gnu",
825        ]
826        all_triples = ["x86_64-pc-linux-gnu"] + cros_targets
827        rust_ebuild = "RUSTC_TARGET_TRIPLES=(" + "\n\t".join(all_triples) + ")"
828        with mock.patch("rust_uprev.find_ebuild_path") as mock_find_ebuild_path:
829            mock_path = mock.Mock()
830            mock_path.read_text.return_value = rust_ebuild
831            mock_find_ebuild_path.return_value = mock_path
832            rust_uprev.build_cross_compiler(rust_uprev.RustVersion(7, 3, 31))
833
834        mock_run_in_chroot.assert_called_once_with(
835            ["sudo", "emerge", "-j", "-G"]
836            + [f"cross-{x}/gcc" for x in cros_targets + ["arm-none-eabi"]]
837        )
838
839
840if __name__ == "__main__":
841    unittest.main()
842