xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/update_chromeos_llvm_hash_unittest.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2019 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"""Unit tests for updating LLVM hashes."""
7
8import os
9from pathlib import Path
10import subprocess
11import sys
12import tempfile
13from typing import Optional, Union
14import unittest
15from unittest import mock
16
17import chroot
18import failure_modes
19import get_llvm_hash
20import patch_utils
21import test_helpers
22import update_chromeos_llvm_hash
23
24
25# These are unittests; protected access is OK to a point.
26# pylint: disable=protected-access
27
28
29class UpdateLLVMHashTest(unittest.TestCase):
30    """Test class for updating LLVM hashes of packages."""
31
32    @staticmethod
33    def _make_patch_entry(
34        relpath: Union[str, Path], workdir: Optional[Path] = None
35    ) -> patch_utils.PatchEntry:
36        if workdir is None:
37            workdir = Path("llvm_tools/update_chromeos_llvm_hash_unittest.py")
38        return patch_utils.PatchEntry(
39            workdir=workdir,
40            rel_patch_path=str(relpath),
41            metadata={},
42            platforms=["chromiumos"],
43            version_range={"from": None, "until": None},
44            verify_workdir=False,
45        )
46
47    @mock.patch.object(os.path, "realpath")
48    def testDefaultCrosRootFromCrOSCheckout(self, mock_llvm_tools):
49        llvm_tools_path = (
50            "/path/to/cros/src/third_party/toolchain-utils/llvm_tools"
51        )
52        mock_llvm_tools.return_value = llvm_tools_path
53        self.assertEqual(
54            update_chromeos_llvm_hash.defaultCrosRoot(), Path("/path/to/cros")
55        )
56
57    @mock.patch.object(os.path, "realpath")
58    def testDefaultCrosRootFromOutsideCrOSCheckout(self, mock_llvm_tools):
59        mock_llvm_tools.return_value = "~/toolchain-utils/llvm_tools"
60        self.assertEqual(
61            update_chromeos_llvm_hash.defaultCrosRoot(),
62            Path.home() / "chromiumos",
63        )
64
65    # Simulate behavior of 'os.path.isfile()' when the ebuild path to a package
66    # does not exist.
67    @mock.patch.object(os.path, "isfile", return_value=False)
68    def testFailedToUpdateLLVMHashForInvalidEbuildPath(self, mock_isfile):
69        ebuild_path = Path("/some/path/to/package.ebuild")
70        llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
71        git_hash = "a123testhash1"
72        svn_version = 1000
73
74        # Verify the exception is raised when the ebuild path does not exist.
75        with self.assertRaises(ValueError) as err:
76            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
77                ebuild_path, llvm_variant, git_hash, svn_version
78            )
79
80        self.assertEqual(
81            str(err.exception),
82            "Invalid ebuild path provided: %s" % ebuild_path,
83        )
84
85        mock_isfile.assert_called_once()
86
87    # Simulate 'os.path.isfile' behavior on a valid ebuild path.
88    @mock.patch.object(os.path, "isfile", return_value=True)
89    def testFailedToUpdateLLVMHash(self, mock_isfile):
90        # Create a temporary file to simulate an ebuild file of a package.
91        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
92            with open(ebuild_file, "w", encoding="utf-8") as f:
93                f.write(
94                    "\n".join(
95                        [
96                            "First line in the ebuild",
97                            "Second line in the ebuild",
98                            "Last line in the ebuild",
99                        ]
100                    )
101                )
102
103            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
104            git_hash = "a123testhash1"
105            svn_version = 1000
106
107            # Verify the exception is raised when the ebuild file does not have
108            # 'LLVM_HASH'.
109            with self.assertRaises(ValueError) as err:
110                update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
111                    Path(ebuild_file), llvm_variant, git_hash, svn_version
112                )
113
114            self.assertEqual(str(err.exception), "Failed to update LLVM_HASH")
115
116            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
117
118        self.assertEqual(mock_isfile.call_count, 2)
119
120    # Simulate 'os.path.isfile' behavior on a valid ebuild path.
121    @mock.patch.object(os.path, "isfile", return_value=True)
122    def testFailedToUpdateLLVMNextHash(self, mock_isfile):
123        # Create a temporary file to simulate an ebuild file of a package.
124        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
125            with open(ebuild_file, "w", encoding="utf-8") as f:
126                f.write(
127                    "\n".join(
128                        [
129                            "First line in the ebuild",
130                            "Second line in the ebuild",
131                            "Last line in the ebuild",
132                        ]
133                    )
134                )
135
136            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
137            git_hash = "a123testhash1"
138            svn_version = 1000
139
140            # Verify the exception is raised when the ebuild file does not have
141            # 'LLVM_NEXT_HASH'.
142            with self.assertRaises(ValueError) as err:
143                update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
144                    Path(ebuild_file), llvm_variant, git_hash, svn_version
145                )
146
147            self.assertEqual(
148                str(err.exception), "Failed to update LLVM_NEXT_HASH"
149            )
150
151        self.assertEqual(mock_isfile.call_count, 2)
152
153    @mock.patch.object(os.path, "isfile", return_value=True)
154    @mock.patch.object(subprocess, "check_output", return_value=None)
155    def testSuccessfullyStageTheEbuildForCommitForLLVMHashUpdate(
156        self, mock_stage_commit_command, mock_isfile
157    ):
158        # Create a temporary file to simulate an ebuild file of a package.
159        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
160            # Updates LLVM_HASH to 'git_hash' and revision to
161            # 'svn_version'.
162            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
163            git_hash = "a123testhash1"
164            svn_version = 1000
165
166            with open(ebuild_file, "w", encoding="utf-8") as f:
167                f.write(
168                    "\n".join(
169                        [
170                            "First line in the ebuild",
171                            "Second line in the ebuild",
172                            'LLVM_HASH="a12b34c56d78e90" # r500',
173                            "Last line in the ebuild",
174                        ]
175                    )
176                )
177
178            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
179                Path(ebuild_file), llvm_variant, git_hash, svn_version
180            )
181
182            expected_file_contents = [
183                "First line in the ebuild\n",
184                "Second line in the ebuild\n",
185                'LLVM_HASH="a123testhash1" # r1000\n',
186                "Last line in the ebuild",
187            ]
188
189            # Verify the new file contents of the ebuild file match the expected
190            # file contents.
191            with open(ebuild_file, encoding="utf-8") as new_file:
192                self.assertListEqual(
193                    new_file.readlines(), expected_file_contents
194                )
195
196        self.assertEqual(mock_isfile.call_count, 2)
197
198        mock_stage_commit_command.assert_called_once()
199
200    @mock.patch.object(os.path, "isfile", return_value=True)
201    @mock.patch.object(subprocess, "check_output", return_value=None)
202    def testSuccessfullyStageTheEbuildForCommitForLLVMNextHashUpdate(
203        self, mock_stage_commit_command, mock_isfile
204    ):
205        # Create a temporary file to simulate an ebuild file of a package.
206        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
207            # Updates LLVM_NEXT_HASH to 'git_hash' and revision to
208            # 'svn_version'.
209            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
210            git_hash = "a123testhash1"
211            svn_version = 1000
212
213            with open(ebuild_file, "w", encoding="utf-8") as f:
214                f.write(
215                    "\n".join(
216                        [
217                            "First line in the ebuild",
218                            "Second line in the ebuild",
219                            'LLVM_NEXT_HASH="a12b34c56d78e90" # r500',
220                            "Last line in the ebuild",
221                        ]
222                    )
223                )
224
225            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
226                Path(ebuild_file), llvm_variant, git_hash, svn_version
227            )
228
229            expected_file_contents = [
230                "First line in the ebuild\n",
231                "Second line in the ebuild\n",
232                'LLVM_NEXT_HASH="a123testhash1" # r1000\n',
233                "Last line in the ebuild",
234            ]
235
236            # Verify the new file contents of the ebuild file match the expected
237            # file contents.
238            with open(ebuild_file, encoding="utf-8") as new_file:
239                self.assertListEqual(
240                    new_file.readlines(), expected_file_contents
241                )
242
243        self.assertEqual(mock_isfile.call_count, 2)
244
245        mock_stage_commit_command.assert_called_once()
246
247    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
248    @mock.patch.object(os.path, "islink", return_value=False)
249    def testFailedToUprevEbuildToVersionForInvalidSymlink(
250        self, mock_islink, mock_llvm_version
251    ):
252        symlink_path = "/path/to/chromeos/package/package.ebuild"
253        svn_version = 1000
254        git_hash = "badf00d"
255        mock_llvm_version.return_value = "1234"
256
257        # Verify the exception is raised when a invalid symbolic link is
258        # passed in.
259        with self.assertRaises(ValueError) as err:
260            update_chromeos_llvm_hash.UprevEbuildToVersion(
261                symlink_path, svn_version, git_hash
262            )
263
264        self.assertEqual(
265            str(err.exception), "Invalid symlink provided: %s" % symlink_path
266        )
267
268        mock_islink.assert_called_once()
269        mock_llvm_version.assert_not_called()
270
271    @mock.patch.object(os.path, "islink", return_value=False)
272    def testFailedToUprevEbuildSymlinkForInvalidSymlink(self, mock_islink):
273        symlink_path = "/path/to/chromeos/package/package.ebuild"
274
275        # Verify the exception is raised when a invalid symbolic link is
276        # passed in.
277        with self.assertRaises(ValueError) as err:
278            update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_path)
279
280        self.assertEqual(
281            str(err.exception), "Invalid symlink provided: %s" % symlink_path
282        )
283
284        mock_islink.assert_called_once()
285
286    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
287    # Simulate 'os.path.islink' when a symbolic link is passed in.
288    @mock.patch.object(os.path, "islink", return_value=True)
289    # Simulate 'os.path.realpath' when a symbolic link is passed in.
290    @mock.patch.object(os.path, "realpath", return_value=True)
291    def testFailedToUprevEbuildToVersion(
292        self, mock_realpath, mock_islink, mock_llvm_version
293    ):
294        symlink_path = "/path/to/chromeos/llvm/llvm_pre123_p.ebuild"
295        mock_realpath.return_value = "/abs/path/to/llvm/llvm_pre123_p.ebuild"
296        git_hash = "badf00d"
297        mock_llvm_version.return_value = "1234"
298        svn_version = 1000
299
300        # Verify the exception is raised when the symlink does not match the
301        # expected pattern
302        with self.assertRaises(ValueError) as err:
303            update_chromeos_llvm_hash.UprevEbuildToVersion(
304                symlink_path, svn_version, git_hash
305            )
306
307        self.assertEqual(str(err.exception), "Failed to uprev the ebuild.")
308
309        mock_llvm_version.assert_called_once_with(git_hash)
310        mock_islink.assert_called_once_with(symlink_path)
311
312    # Simulate 'os.path.islink' when a symbolic link is passed in.
313    @mock.patch.object(os.path, "islink", return_value=True)
314    def testFailedToUprevEbuildSymlink(self, mock_islink):
315        symlink_path = "/path/to/chromeos/llvm/llvm_pre123_p.ebuild"
316
317        # Verify the exception is raised when the symlink does not match the
318        # expected pattern
319        with self.assertRaises(ValueError) as err:
320            update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_path)
321
322        self.assertEqual(str(err.exception), "Failed to uprev the symlink.")
323
324        mock_islink.assert_called_once_with(symlink_path)
325
326    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
327    @mock.patch.object(os.path, "islink", return_value=True)
328    @mock.patch.object(os.path, "realpath")
329    @mock.patch.object(subprocess, "check_output", return_value=None)
330    def testSuccessfullyUprevEbuildToVersionLLVM(
331        self,
332        mock_command_output,
333        mock_realpath,
334        mock_islink,
335        mock_llvm_version,
336    ):
337        symlink = "/path/to/llvm/llvm-12.0_pre3_p2-r10.ebuild"
338        ebuild = "/abs/path/to/llvm/llvm-12.0_pre3_p2.ebuild"
339        mock_realpath.return_value = ebuild
340        git_hash = "badf00d"
341        mock_llvm_version.return_value = "1234"
342        svn_version = 1000
343
344        update_chromeos_llvm_hash.UprevEbuildToVersion(
345            symlink, svn_version, git_hash
346        )
347
348        mock_llvm_version.assert_called_once_with(git_hash)
349
350        mock_islink.assert_called()
351
352        mock_realpath.assert_called_once_with(symlink)
353
354        mock_command_output.assert_called()
355
356        # Verify commands
357        symlink_dir = os.path.dirname(symlink)
358        new_ebuild = "/abs/path/to/llvm/llvm-1234.0_pre1000.ebuild"
359        new_symlink = new_ebuild[: -len(".ebuild")] + "-r1.ebuild"
360
361        expected_cmd = ["git", "-C", symlink_dir, "mv", ebuild, new_ebuild]
362        self.assertEqual(
363            mock_command_output.call_args_list[0], mock.call(expected_cmd)
364        )
365
366        expected_cmd = ["ln", "-s", "-r", new_ebuild, new_symlink]
367        self.assertEqual(
368            mock_command_output.call_args_list[1], mock.call(expected_cmd)
369        )
370
371        expected_cmd = ["git", "-C", symlink_dir, "add", new_symlink]
372        self.assertEqual(
373            mock_command_output.call_args_list[2], mock.call(expected_cmd)
374        )
375
376        expected_cmd = ["git", "-C", symlink_dir, "rm", symlink]
377        self.assertEqual(
378            mock_command_output.call_args_list[3], mock.call(expected_cmd)
379        )
380
381    @mock.patch.object(
382        chroot,
383        "GetChrootEbuildPaths",
384        return_value=["/chroot/path/test.ebuild"],
385    )
386    @mock.patch.object(subprocess, "check_output", return_value="")
387    def testManifestUpdate(self, mock_subprocess, mock_ebuild_paths):
388        manifest_packages = ["sys-devel/llvm"]
389        chromeos_path = "/path/to/chromeos"
390        update_chromeos_llvm_hash.UpdatePortageManifests(
391            manifest_packages, Path(chromeos_path)
392        )
393
394        args = mock_subprocess.call_args_list[0]
395        manifest_cmd = (
396            [
397                "cros_sdk",
398                "--chroot=chroot",
399                "--out-dir=out",
400                "--",
401                "ebuild",
402                "/chroot/path/test.ebuild",
403                "manifest",
404            ],
405        )
406        self.assertEqual(args[0], manifest_cmd)
407
408        args = mock_subprocess.call_args_list[1]
409        git_add_cmd = (
410            [
411                "cros_sdk",
412                "--chroot=chroot",
413                "--out-dir=out",
414                "--",
415                "git",
416                "-C",
417                "/chroot/path",
418                "add",
419                "Manifest",
420            ],
421        )
422        self.assertEqual(args[0], git_add_cmd)
423        mock_ebuild_paths.assert_called_once()
424
425    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
426    @mock.patch.object(os.path, "islink", return_value=True)
427    @mock.patch.object(os.path, "realpath")
428    @mock.patch.object(subprocess, "check_output", return_value=None)
429    def testSuccessfullyUprevEbuildToVersionNonLLVM(
430        self, mock_command_output, mock_realpath, mock_islink, mock_llvm_version
431    ):
432        symlink = (
433            "/abs/path/to/compiler-rt/compiler-rt-12.0_pre314159265-r4.ebuild"
434        )
435        ebuild = "/abs/path/to/compiler-rt/compiler-rt-12.0_pre314159265.ebuild"
436        mock_realpath.return_value = ebuild
437        mock_llvm_version.return_value = "1234"
438        svn_version = 1000
439        git_hash = "5678"
440
441        update_chromeos_llvm_hash.UprevEbuildToVersion(
442            symlink, svn_version, git_hash
443        )
444
445        mock_islink.assert_called()
446
447        mock_realpath.assert_called_once_with(symlink)
448
449        mock_llvm_version.assert_called_once_with(git_hash)
450
451        mock_command_output.assert_called()
452
453        # Verify commands
454        symlink_dir = os.path.dirname(symlink)
455        new_ebuild = (
456            "/abs/path/to/compiler-rt/compiler-rt-1234.0_pre1000.ebuild"
457        )
458        new_symlink = new_ebuild[: -len(".ebuild")] + "-r1.ebuild"
459
460        expected_cmd = ["git", "-C", symlink_dir, "mv", ebuild, new_ebuild]
461        self.assertEqual(
462            mock_command_output.call_args_list[0], mock.call(expected_cmd)
463        )
464
465        expected_cmd = ["ln", "-s", "-r", new_ebuild, new_symlink]
466        self.assertEqual(
467            mock_command_output.call_args_list[1], mock.call(expected_cmd)
468        )
469
470        expected_cmd = ["git", "-C", symlink_dir, "add", new_symlink]
471        self.assertEqual(
472            mock_command_output.call_args_list[2], mock.call(expected_cmd)
473        )
474
475        expected_cmd = ["git", "-C", symlink_dir, "rm", symlink]
476        self.assertEqual(
477            mock_command_output.call_args_list[3], mock.call(expected_cmd)
478        )
479
480    @mock.patch.object(os.path, "islink", return_value=True)
481    @mock.patch.object(subprocess, "check_output", return_value=None)
482    def testSuccessfullyUprevEbuildSymlink(
483        self, mock_command_output, mock_islink
484    ):
485        symlink_to_uprev = "/symlink/to/package-r1.ebuild"
486
487        update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_to_uprev)
488
489        mock_islink.assert_called_once_with(symlink_to_uprev)
490
491        mock_command_output.assert_called_once()
492
493    @mock.patch.object(subprocess, "check_output", return_value=None)
494    def testSuccessfullyRemovedPatchesFromFilesDir(self, mock_run_cmd):
495        patches_to_remove_list = [
496            "/abs/path/to/filesdir/cherry/fix_output.patch",
497            "/abs/path/to/filesdir/display_results.patch",
498        ]
499
500        update_chromeos_llvm_hash.RemovePatchesFromFilesDir(
501            patches_to_remove_list
502        )
503
504        self.assertEqual(mock_run_cmd.call_count, 2)
505
506    @mock.patch.object(os.path, "isfile", return_value=False)
507    def testInvalidPatchMetadataFileStagedForCommit(self, mock_isfile):
508        patch_metadata_path = "/abs/path/to/filesdir/PATCHES"
509
510        # Verify the exception is raised when the absolute path to the patch
511        # metadata file does not exist or is not a file.
512        with self.assertRaises(ValueError) as err:
513            update_chromeos_llvm_hash.StagePatchMetadataFileForCommit(
514                patch_metadata_path
515            )
516
517        self.assertEqual(
518            str(err.exception),
519            "Invalid patch metadata file provided: " "%s" % patch_metadata_path,
520        )
521
522        mock_isfile.assert_called_once()
523
524    @mock.patch.object(os.path, "isfile", return_value=True)
525    @mock.patch.object(subprocess, "check_output", return_value=None)
526    def testSuccessfullyStagedPatchMetadataFileForCommit(self, mock_run_cmd, _):
527        patch_metadata_path = "/abs/path/to/filesdir/PATCHES.json"
528
529        update_chromeos_llvm_hash.StagePatchMetadataFileForCommit(
530            patch_metadata_path
531        )
532
533        mock_run_cmd.assert_called_once()
534
535    def testNoPatchResultsForCommit(self):
536        package_1_patch_info = patch_utils.PatchInfo(
537            applied_patches=[self._make_patch_entry("display_results.patch")],
538            failed_patches=[self._make_patch_entry("fixes_output.patch")],
539            non_applicable_patches=[],
540            disabled_patches=[],
541            removed_patches=[],
542            modified_metadata=None,
543        )
544
545        package_2_patch_info = patch_utils.PatchInfo(
546            applied_patches=[
547                self._make_patch_entry("redirects_stdout.patch"),
548                self._make_patch_entry("fix_display.patch"),
549            ],
550            failed_patches=[],
551            non_applicable_patches=[],
552            disabled_patches=[],
553            removed_patches=[],
554            modified_metadata=None,
555        )
556
557        test_package_info_dict = {
558            "test-packages/package1": package_1_patch_info,
559            "test-packages/package2": package_2_patch_info,
560        }
561
562        test_commit_message = ["Updated packages"]
563
564        self.assertListEqual(
565            update_chromeos_llvm_hash.StagePackagesPatchResultsForCommit(
566                test_package_info_dict, test_commit_message
567            ),
568            test_commit_message,
569        )
570
571    @mock.patch.object(
572        update_chromeos_llvm_hash, "StagePatchMetadataFileForCommit"
573    )
574    @mock.patch.object(update_chromeos_llvm_hash, "RemovePatchesFromFilesDir")
575    def testAddedPatchResultsForCommit(
576        self, mock_remove_patches, mock_stage_patches_for_commit
577    ):
578        package_1_patch_info = patch_utils.PatchInfo(
579            applied_patches=[],
580            failed_patches=[],
581            non_applicable_patches=[],
582            disabled_patches=["fixes_output.patch"],
583            removed_patches=[],
584            modified_metadata="/abs/path/to/filesdir/PATCHES.json",
585        )
586
587        package_2_patch_info = patch_utils.PatchInfo(
588            applied_patches=[self._make_patch_entry("fix_display.patch")],
589            failed_patches=[],
590            non_applicable_patches=[],
591            disabled_patches=[],
592            removed_patches=["/abs/path/to/filesdir/redirect_stdout.patch"],
593            modified_metadata="/abs/path/to/filesdir/PATCHES.json",
594        )
595
596        test_package_info_dict = {
597            "test-packages/package1": package_1_patch_info,
598            "test-packages/package2": package_2_patch_info,
599        }
600
601        test_commit_message = ["Updated packages"]
602
603        expected_commit_messages = [
604            "Updated packages",
605            "\nFor the package test-packages/package1:",
606            "The patch metadata file PATCHES.json was modified",
607            "The following patches were disabled:",
608            "fixes_output.patch",
609            "\nFor the package test-packages/package2:",
610            "The patch metadata file PATCHES.json was modified",
611            "The following patches were removed:",
612            "redirect_stdout.patch",
613        ]
614
615        self.assertListEqual(
616            update_chromeos_llvm_hash.StagePackagesPatchResultsForCommit(
617                test_package_info_dict, test_commit_message
618            ),
619            expected_commit_messages,
620        )
621
622        path_to_removed_patch = "/abs/path/to/filesdir/redirect_stdout.patch"
623
624        mock_remove_patches.assert_called_once_with([path_to_removed_patch])
625
626        self.assertEqual(mock_stage_patches_for_commit.call_count, 2)
627
628    def setup_mock_src_tree(self, src_tree: Path):
629        package_dir = (
630            src_tree / "src/third_party/chromiumos-overlay/sys-devel/llvm"
631        )
632        package_dir.mkdir(parents=True)
633        ebuild_path = package_dir / "llvm-00.00_pre0_p0.ebuild"
634        with ebuild_path.open("w", encoding="utf-8") as f:
635            f.writelines(
636                [
637                    'LLVM_HASH="abcdef123456" # r123456',
638                    'LLVM_NEXT_HASH="987654321fedcba" # r99453',
639                ]
640            )
641        symlink_path = package_dir / "llvm-00.00_pre0_p0-r1234.ebuild"
642        symlink_path.symlink_to(ebuild_path)
643        return package_dir, ebuild_path, symlink_path
644
645    def testPortagePackageConstruction(self):
646        with tempfile.TemporaryDirectory(
647            "update_chromeos_llvm_hash.tmp"
648        ) as workdir_str:
649            src_tree = Path(workdir_str)
650            package_dir, ebuild_path, symlink_path = self.setup_mock_src_tree(
651                src_tree
652            )
653
654            # Test that we're upreving if there's a symlink.
655            def mock_find_package_ebuild(_, package_name):
656                self.assertEqual(
657                    package_name,
658                    f"{package_dir.parent.name}/{package_dir.name}",
659                )
660                return symlink_path
661
662            with mock.patch(
663                "update_chromeos_llvm_hash.PortagePackage.find_package_ebuild",
664                mock_find_package_ebuild,
665            ):
666                pkg = update_chromeos_llvm_hash.PortagePackage(
667                    src_tree, "sys-devel/llvm"
668                )
669                self.assertEqual(pkg.uprev_target, symlink_path.absolute())
670                self.assertEqual(pkg.ebuild_path, ebuild_path.absolute())
671                self.assertEqual(pkg.live_ebuild(), None)
672
673                # Make sure if the live ebuild is there, we find it.
674                live_ebuild_path = package_dir / "llvm-9999.ebuild"
675                live_ebuild_path.touch()
676
677                pkg = update_chromeos_llvm_hash.PortagePackage(
678                    src_tree, "sys-devel/llvm"
679                )
680                self.assertEqual(pkg.live_ebuild(), live_ebuild_path)
681
682    @mock.patch("subprocess.run")
683    @mock.patch("subprocess.check_output")
684    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
685    def testUpdatePackages(
686        self, mock_llvm_major_version, _mock_check_output, _mock_run
687    ):
688        mock_llvm_major_version.return_value = "17"
689        with tempfile.TemporaryDirectory(
690            "update_chromeos_llvm_hash.tmp"
691        ) as workdir_str:
692            src_tree = Path(workdir_str)
693            _package_dir, _ebuild_path, symlink_path = self.setup_mock_src_tree(
694                src_tree
695            )
696
697            def mock_find_package_ebuild(*_):
698                return symlink_path
699
700            with mock.patch(
701                "update_chromeos_llvm_hash.PortagePackage.find_package_ebuild",
702                mock_find_package_ebuild,
703            ):
704                pkg = update_chromeos_llvm_hash.PortagePackage(
705                    src_tree, "sys-devel/llvm"
706                )
707                pkg.update(
708                    update_chromeos_llvm_hash.LLVMVariant.current,
709                    "beef3333",
710                    3333,
711                )
712
713    @mock.patch.object(chroot, "VerifyChromeOSRoot")
714    @mock.patch.object(chroot, "VerifyOutsideChroot")
715    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
716    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
717    def testMainDefaults(
718        self,
719        mock_update_packages,
720        mock_gethash,
721        mock_outside_chroot,
722        mock_chromeos_root,
723    ):
724        git_hash = "1234abcd"
725        svn_version = 5678
726        mock_gethash.return_value = (git_hash, svn_version)
727        argv = [
728            "./update_chromeos_llvm_hash_unittest.py",
729            "--no_repo_manifest",
730            "--llvm_version",
731            "google3",
732        ]
733
734        with mock.patch.object(sys, "argv", argv) as mock.argv:
735            update_chromeos_llvm_hash.main()
736
737        expected_packages = set(update_chromeos_llvm_hash.DEFAULT_PACKAGES)
738        expected_manifest_packages = set(
739            update_chromeos_llvm_hash.DEFAULT_MANIFEST_PACKAGES,
740        )
741        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
742        expected_chroot = update_chromeos_llvm_hash.defaultCrosRoot()
743        mock_update_packages.assert_called_once_with(
744            packages=expected_packages,
745            manifest_packages=expected_manifest_packages,
746            llvm_variant=expected_llvm_variant,
747            git_hash=git_hash,
748            svn_version=svn_version,
749            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(expected_chroot),
750            mode=failure_modes.FailureModes.FAIL,
751            git_hash_source="google3",
752            extra_commit_msg_lines=None,
753            delete_branch=True,
754            upload_changes=True,
755        )
756        mock_outside_chroot.assert_called()
757        mock_chromeos_root.assert_called()
758
759    @mock.patch.object(chroot, "VerifyChromeOSRoot")
760    @mock.patch.object(chroot, "VerifyOutsideChroot")
761    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
762    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
763    def testMainLlvmNext(
764        self,
765        mock_update_packages,
766        mock_gethash,
767        mock_outside_chroot,
768        mock_chromeos_root,
769    ):
770        git_hash = "1234abcd"
771        svn_version = 5678
772        mock_gethash.return_value = (git_hash, svn_version)
773        argv = [
774            "./update_chromeos_llvm_hash_unittest.py",
775            "--llvm_version",
776            "google3",
777            "--is_llvm_next",
778        ]
779
780        with mock.patch.object(sys, "argv", argv) as mock.argv:
781            update_chromeos_llvm_hash.main()
782
783        expected_packages = set(update_chromeos_llvm_hash.DEFAULT_PACKAGES)
784        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
785        expected_chroot = update_chromeos_llvm_hash.defaultCrosRoot()
786        # llvm-next upgrade does not update manifest by default.
787        mock_update_packages.assert_called_once_with(
788            packages=expected_packages,
789            manifest_packages=set(),
790            llvm_variant=expected_llvm_variant,
791            git_hash=git_hash,
792            svn_version=svn_version,
793            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(expected_chroot),
794            mode=failure_modes.FailureModes.FAIL,
795            git_hash_source="google3",
796            extra_commit_msg_lines=None,
797            delete_branch=True,
798            upload_changes=True,
799        )
800        mock_outside_chroot.assert_called()
801        mock_chromeos_root.assert_called()
802
803    @mock.patch.object(chroot, "VerifyChromeOSRoot")
804    @mock.patch.object(chroot, "VerifyOutsideChroot")
805    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
806    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
807    def testMainAllArgs(
808        self,
809        mock_update_packages,
810        mock_gethash,
811        mock_outside_chroot,
812        mock_chromeos_root,
813    ):
814        packages_to_update = "test-packages/package1,test-libs/lib1"
815        manifest_packages = "test-libs/lib1,test-libs/lib2"
816        failure_mode = failure_modes.FailureModes.DISABLE_PATCHES
817        chromeos_path = Path("/some/path/to/chromeos")
818        llvm_ver = 435698
819        git_hash = "1234abcd"
820        svn_version = 5678
821        mock_gethash.return_value = (git_hash, svn_version)
822
823        argv = [
824            "./update_chromeos_llvm_hash_unittest.py",
825            "--llvm_version",
826            str(llvm_ver),
827            "--is_llvm_next",
828            "--chromeos_path",
829            str(chromeos_path),
830            "--update_packages",
831            packages_to_update,
832            "--manifest_packages",
833            manifest_packages,
834            "--failure_mode",
835            failure_mode.value,
836            "--patch_metadata_file",
837            "META.json",
838            "--no_repo_manifest",
839        ]
840
841        with mock.patch.object(sys, "argv", argv) as mock.argv:
842            update_chromeos_llvm_hash.main()
843
844        expected_packages = {"test-packages/package1", "test-libs/lib1"}
845        expected_manifest_packages = {"test-libs/lib1", "test-libs/lib2"}
846        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
847        mock_update_packages.assert_called_once_with(
848            packages=expected_packages,
849            manifest_packages=expected_manifest_packages,
850            llvm_variant=expected_llvm_variant,
851            git_hash=git_hash,
852            svn_version=svn_version,
853            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(chromeos_path),
854            mode=failure_mode,
855            git_hash_source=llvm_ver,
856            extra_commit_msg_lines=None,
857            delete_branch=True,
858            upload_changes=True,
859        )
860        mock_outside_chroot.assert_called()
861        mock_chromeos_root.assert_called()
862
863    @mock.patch.object(subprocess, "check_output", return_value=None)
864    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
865    def testEnsurePackageMaskContainsExisting(
866        self, mock_llvm_version, mock_git_add
867    ):
868        chromeos_path = "absolute/path/to/chromeos"
869        git_hash = "badf00d"
870        mock_llvm_version.return_value = "1234"
871        with mock.patch(
872            "update_chromeos_llvm_hash.open",
873            mock.mock_open(read_data="\n=sys-devel/llvm-1234.0_pre*\n"),
874            create=True,
875        ) as mock_file:
876            update_chromeos_llvm_hash.EnsurePackageMaskContains(
877                chromeos_path, git_hash
878            )
879            handle = mock_file()
880            handle.write.assert_not_called()
881        mock_llvm_version.assert_called_once_with(git_hash)
882
883        overlay_dir = (
884            "absolute/path/to/chromeos/src/third_party/chromiumos-overlay"
885        )
886        mask_path = overlay_dir + "/profiles/targets/chromeos/package.mask"
887        mock_git_add.assert_called_once_with(
888            ["git", "-C", overlay_dir, "add", mask_path]
889        )
890
891    @mock.patch.object(subprocess, "check_output", return_value=None)
892    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
893    def testEnsurePackageMaskContainsNotExisting(
894        self, mock_llvm_version, mock_git_add
895    ):
896        chromeos_path = "absolute/path/to/chromeos"
897        git_hash = "badf00d"
898        mock_llvm_version.return_value = "1234"
899        with mock.patch(
900            "update_chromeos_llvm_hash.open",
901            mock.mock_open(read_data="nothing relevant"),
902            create=True,
903        ) as mock_file:
904            update_chromeos_llvm_hash.EnsurePackageMaskContains(
905                chromeos_path, git_hash
906            )
907            handle = mock_file()
908            handle.write.assert_called_once_with(
909                "=sys-devel/llvm-1234.0_pre*\n"
910            )
911        mock_llvm_version.assert_called_once_with(git_hash)
912
913        overlay_dir = (
914            "absolute/path/to/chromeos/src/third_party/chromiumos-overlay"
915        )
916        mask_path = overlay_dir + "/profiles/targets/chromeos/package.mask"
917        mock_git_add.assert_called_once_with(
918            ["git", "-C", overlay_dir, "add", mask_path]
919        )
920
921
922if __name__ == "__main__":
923    unittest.main()
924