#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Unit tests for updating LLVM hashes."""

import os
from pathlib import Path
import subprocess
import sys
import tempfile
from typing import Optional, Union
import unittest
from unittest import mock

import chroot
import failure_modes
import get_llvm_hash
import patch_utils
import test_helpers
import update_chromeos_llvm_hash


# These are unittests; protected access is OK to a point.
# pylint: disable=protected-access


class UpdateLLVMHashTest(unittest.TestCase):
    """Test class for updating LLVM hashes of packages."""

    @staticmethod
    def _make_patch_entry(
        relpath: Union[str, Path], workdir: Optional[Path] = None
    ) -> patch_utils.PatchEntry:
        if workdir is None:
            workdir = Path("llvm_tools/update_chromeos_llvm_hash_unittest.py")
        return patch_utils.PatchEntry(
            workdir=workdir,
            rel_patch_path=str(relpath),
            metadata={},
            platforms=["chromiumos"],
            version_range={"from": None, "until": None},
            verify_workdir=False,
        )

    @mock.patch.object(os.path, "realpath")
    def testDefaultCrosRootFromCrOSCheckout(self, mock_llvm_tools):
        llvm_tools_path = (
            "/path/to/cros/src/third_party/toolchain-utils/llvm_tools"
        )
        mock_llvm_tools.return_value = llvm_tools_path
        self.assertEqual(
            update_chromeos_llvm_hash.defaultCrosRoot(), Path("/path/to/cros")
        )

    @mock.patch.object(os.path, "realpath")
    def testDefaultCrosRootFromOutsideCrOSCheckout(self, mock_llvm_tools):
        mock_llvm_tools.return_value = "~/toolchain-utils/llvm_tools"
        self.assertEqual(
            update_chromeos_llvm_hash.defaultCrosRoot(),
            Path.home() / "chromiumos",
        )

    # Simulate behavior of 'os.path.isfile()' when the ebuild path to a package
    # does not exist.
    @mock.patch.object(os.path, "isfile", return_value=False)
    def testFailedToUpdateLLVMHashForInvalidEbuildPath(self, mock_isfile):
        ebuild_path = Path("/some/path/to/package.ebuild")
        llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
        git_hash = "a123testhash1"
        svn_version = 1000

        # Verify the exception is raised when the ebuild path does not exist.
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
                ebuild_path, llvm_variant, git_hash, svn_version
            )

        self.assertEqual(
            str(err.exception),
            "Invalid ebuild path provided: %s" % ebuild_path,
        )

        mock_isfile.assert_called_once()

    # Simulate 'os.path.isfile' behavior on a valid ebuild path.
    @mock.patch.object(os.path, "isfile", return_value=True)
    def testFailedToUpdateLLVMHash(self, mock_isfile):
        # Create a temporary file to simulate an ebuild file of a package.
        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
            with open(ebuild_file, "w", encoding="utf-8") as f:
                f.write(
                    "\n".join(
                        [
                            "First line in the ebuild",
                            "Second line in the ebuild",
                            "Last line in the ebuild",
                        ]
                    )
                )

            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
            git_hash = "a123testhash1"
            svn_version = 1000

            # Verify the exception is raised when the ebuild file does not have
            # 'LLVM_HASH'.
            with self.assertRaises(ValueError) as err:
                update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
                    Path(ebuild_file), llvm_variant, git_hash, svn_version
                )

            self.assertEqual(str(err.exception), "Failed to update LLVM_HASH")

            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next

        self.assertEqual(mock_isfile.call_count, 2)

    # Simulate 'os.path.isfile' behavior on a valid ebuild path.
    @mock.patch.object(os.path, "isfile", return_value=True)
    def testFailedToUpdateLLVMNextHash(self, mock_isfile):
        # Create a temporary file to simulate an ebuild file of a package.
        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
            with open(ebuild_file, "w", encoding="utf-8") as f:
                f.write(
                    "\n".join(
                        [
                            "First line in the ebuild",
                            "Second line in the ebuild",
                            "Last line in the ebuild",
                        ]
                    )
                )

            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
            git_hash = "a123testhash1"
            svn_version = 1000

            # Verify the exception is raised when the ebuild file does not have
            # 'LLVM_NEXT_HASH'.
            with self.assertRaises(ValueError) as err:
                update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
                    Path(ebuild_file), llvm_variant, git_hash, svn_version
                )

            self.assertEqual(
                str(err.exception), "Failed to update LLVM_NEXT_HASH"
            )

        self.assertEqual(mock_isfile.call_count, 2)

    @mock.patch.object(os.path, "isfile", return_value=True)
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyStageTheEbuildForCommitForLLVMHashUpdate(
        self, mock_stage_commit_command, mock_isfile
    ):
        # Create a temporary file to simulate an ebuild file of a package.
        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
            # Updates LLVM_HASH to 'git_hash' and revision to
            # 'svn_version'.
            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
            git_hash = "a123testhash1"
            svn_version = 1000

            with open(ebuild_file, "w", encoding="utf-8") as f:
                f.write(
                    "\n".join(
                        [
                            "First line in the ebuild",
                            "Second line in the ebuild",
                            'LLVM_HASH="a12b34c56d78e90" # r500',
                            "Last line in the ebuild",
                        ]
                    )
                )

            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
                Path(ebuild_file), llvm_variant, git_hash, svn_version
            )

            expected_file_contents = [
                "First line in the ebuild\n",
                "Second line in the ebuild\n",
                'LLVM_HASH="a123testhash1" # r1000\n',
                "Last line in the ebuild",
            ]

            # Verify the new file contents of the ebuild file match the expected
            # file contents.
            with open(ebuild_file, encoding="utf-8") as new_file:
                self.assertListEqual(
                    new_file.readlines(), expected_file_contents
                )

        self.assertEqual(mock_isfile.call_count, 2)

        mock_stage_commit_command.assert_called_once()

    @mock.patch.object(os.path, "isfile", return_value=True)
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyStageTheEbuildForCommitForLLVMNextHashUpdate(
        self, mock_stage_commit_command, mock_isfile
    ):
        # Create a temporary file to simulate an ebuild file of a package.
        with test_helpers.CreateTemporaryJsonFile() as ebuild_file:
            # Updates LLVM_NEXT_HASH to 'git_hash' and revision to
            # 'svn_version'.
            llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
            git_hash = "a123testhash1"
            svn_version = 1000

            with open(ebuild_file, "w", encoding="utf-8") as f:
                f.write(
                    "\n".join(
                        [
                            "First line in the ebuild",
                            "Second line in the ebuild",
                            'LLVM_NEXT_HASH="a12b34c56d78e90" # r500',
                            "Last line in the ebuild",
                        ]
                    )
                )

            update_chromeos_llvm_hash.UpdateEbuildLLVMHash(
                Path(ebuild_file), llvm_variant, git_hash, svn_version
            )

            expected_file_contents = [
                "First line in the ebuild\n",
                "Second line in the ebuild\n",
                'LLVM_NEXT_HASH="a123testhash1" # r1000\n',
                "Last line in the ebuild",
            ]

            # Verify the new file contents of the ebuild file match the expected
            # file contents.
            with open(ebuild_file, encoding="utf-8") as new_file:
                self.assertListEqual(
                    new_file.readlines(), expected_file_contents
                )

        self.assertEqual(mock_isfile.call_count, 2)

        mock_stage_commit_command.assert_called_once()

    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    @mock.patch.object(os.path, "islink", return_value=False)
    def testFailedToUprevEbuildToVersionForInvalidSymlink(
        self, mock_islink, mock_llvm_version
    ):
        symlink_path = "/path/to/chromeos/package/package.ebuild"
        svn_version = 1000
        git_hash = "badf00d"
        mock_llvm_version.return_value = "1234"

        # Verify the exception is raised when a invalid symbolic link is
        # passed in.
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.UprevEbuildToVersion(
                symlink_path, svn_version, git_hash
            )

        self.assertEqual(
            str(err.exception), "Invalid symlink provided: %s" % symlink_path
        )

        mock_islink.assert_called_once()
        mock_llvm_version.assert_not_called()

    @mock.patch.object(os.path, "islink", return_value=False)
    def testFailedToUprevEbuildSymlinkForInvalidSymlink(self, mock_islink):
        symlink_path = "/path/to/chromeos/package/package.ebuild"

        # Verify the exception is raised when a invalid symbolic link is
        # passed in.
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_path)

        self.assertEqual(
            str(err.exception), "Invalid symlink provided: %s" % symlink_path
        )

        mock_islink.assert_called_once()

    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    # Simulate 'os.path.islink' when a symbolic link is passed in.
    @mock.patch.object(os.path, "islink", return_value=True)
    # Simulate 'os.path.realpath' when a symbolic link is passed in.
    @mock.patch.object(os.path, "realpath", return_value=True)
    def testFailedToUprevEbuildToVersion(
        self, mock_realpath, mock_islink, mock_llvm_version
    ):
        symlink_path = "/path/to/chromeos/llvm/llvm_pre123_p.ebuild"
        mock_realpath.return_value = "/abs/path/to/llvm/llvm_pre123_p.ebuild"
        git_hash = "badf00d"
        mock_llvm_version.return_value = "1234"
        svn_version = 1000

        # Verify the exception is raised when the symlink does not match the
        # expected pattern
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.UprevEbuildToVersion(
                symlink_path, svn_version, git_hash
            )

        self.assertEqual(str(err.exception), "Failed to uprev the ebuild.")

        mock_llvm_version.assert_called_once_with(git_hash)
        mock_islink.assert_called_once_with(symlink_path)

    # Simulate 'os.path.islink' when a symbolic link is passed in.
    @mock.patch.object(os.path, "islink", return_value=True)
    def testFailedToUprevEbuildSymlink(self, mock_islink):
        symlink_path = "/path/to/chromeos/llvm/llvm_pre123_p.ebuild"

        # Verify the exception is raised when the symlink does not match the
        # expected pattern
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_path)

        self.assertEqual(str(err.exception), "Failed to uprev the symlink.")

        mock_islink.assert_called_once_with(symlink_path)

    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    @mock.patch.object(os.path, "islink", return_value=True)
    @mock.patch.object(os.path, "realpath")
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyUprevEbuildToVersionLLVM(
        self,
        mock_command_output,
        mock_realpath,
        mock_islink,
        mock_llvm_version,
    ):
        symlink = "/path/to/llvm/llvm-12.0_pre3_p2-r10.ebuild"
        ebuild = "/abs/path/to/llvm/llvm-12.0_pre3_p2.ebuild"
        mock_realpath.return_value = ebuild
        git_hash = "badf00d"
        mock_llvm_version.return_value = "1234"
        svn_version = 1000

        update_chromeos_llvm_hash.UprevEbuildToVersion(
            symlink, svn_version, git_hash
        )

        mock_llvm_version.assert_called_once_with(git_hash)

        mock_islink.assert_called()

        mock_realpath.assert_called_once_with(symlink)

        mock_command_output.assert_called()

        # Verify commands
        symlink_dir = os.path.dirname(symlink)
        new_ebuild = "/abs/path/to/llvm/llvm-1234.0_pre1000.ebuild"
        new_symlink = new_ebuild[: -len(".ebuild")] + "-r1.ebuild"

        expected_cmd = ["git", "-C", symlink_dir, "mv", ebuild, new_ebuild]
        self.assertEqual(
            mock_command_output.call_args_list[0], mock.call(expected_cmd)
        )

        expected_cmd = ["ln", "-s", "-r", new_ebuild, new_symlink]
        self.assertEqual(
            mock_command_output.call_args_list[1], mock.call(expected_cmd)
        )

        expected_cmd = ["git", "-C", symlink_dir, "add", new_symlink]
        self.assertEqual(
            mock_command_output.call_args_list[2], mock.call(expected_cmd)
        )

        expected_cmd = ["git", "-C", symlink_dir, "rm", symlink]
        self.assertEqual(
            mock_command_output.call_args_list[3], mock.call(expected_cmd)
        )

    @mock.patch.object(
        chroot,
        "GetChrootEbuildPaths",
        return_value=["/chroot/path/test.ebuild"],
    )
    @mock.patch.object(subprocess, "check_output", return_value="")
    def testManifestUpdate(self, mock_subprocess, mock_ebuild_paths):
        manifest_packages = ["sys-devel/llvm"]
        chromeos_path = "/path/to/chromeos"
        update_chromeos_llvm_hash.UpdatePortageManifests(
            manifest_packages, Path(chromeos_path)
        )

        args = mock_subprocess.call_args_list[0]
        manifest_cmd = (
            [
                "cros_sdk",
                "--chroot=chroot",
                "--out-dir=out",
                "--",
                "ebuild",
                "/chroot/path/test.ebuild",
                "manifest",
            ],
        )
        self.assertEqual(args[0], manifest_cmd)

        args = mock_subprocess.call_args_list[1]
        git_add_cmd = (
            [
                "cros_sdk",
                "--chroot=chroot",
                "--out-dir=out",
                "--",
                "git",
                "-C",
                "/chroot/path",
                "add",
                "Manifest",
            ],
        )
        self.assertEqual(args[0], git_add_cmd)
        mock_ebuild_paths.assert_called_once()

    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    @mock.patch.object(os.path, "islink", return_value=True)
    @mock.patch.object(os.path, "realpath")
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyUprevEbuildToVersionNonLLVM(
        self, mock_command_output, mock_realpath, mock_islink, mock_llvm_version
    ):
        symlink = (
            "/abs/path/to/compiler-rt/compiler-rt-12.0_pre314159265-r4.ebuild"
        )
        ebuild = "/abs/path/to/compiler-rt/compiler-rt-12.0_pre314159265.ebuild"
        mock_realpath.return_value = ebuild
        mock_llvm_version.return_value = "1234"
        svn_version = 1000
        git_hash = "5678"

        update_chromeos_llvm_hash.UprevEbuildToVersion(
            symlink, svn_version, git_hash
        )

        mock_islink.assert_called()

        mock_realpath.assert_called_once_with(symlink)

        mock_llvm_version.assert_called_once_with(git_hash)

        mock_command_output.assert_called()

        # Verify commands
        symlink_dir = os.path.dirname(symlink)
        new_ebuild = (
            "/abs/path/to/compiler-rt/compiler-rt-1234.0_pre1000.ebuild"
        )
        new_symlink = new_ebuild[: -len(".ebuild")] + "-r1.ebuild"

        expected_cmd = ["git", "-C", symlink_dir, "mv", ebuild, new_ebuild]
        self.assertEqual(
            mock_command_output.call_args_list[0], mock.call(expected_cmd)
        )

        expected_cmd = ["ln", "-s", "-r", new_ebuild, new_symlink]
        self.assertEqual(
            mock_command_output.call_args_list[1], mock.call(expected_cmd)
        )

        expected_cmd = ["git", "-C", symlink_dir, "add", new_symlink]
        self.assertEqual(
            mock_command_output.call_args_list[2], mock.call(expected_cmd)
        )

        expected_cmd = ["git", "-C", symlink_dir, "rm", symlink]
        self.assertEqual(
            mock_command_output.call_args_list[3], mock.call(expected_cmd)
        )

    @mock.patch.object(os.path, "islink", return_value=True)
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyUprevEbuildSymlink(
        self, mock_command_output, mock_islink
    ):
        symlink_to_uprev = "/symlink/to/package-r1.ebuild"

        update_chromeos_llvm_hash.UprevEbuildSymlink(symlink_to_uprev)

        mock_islink.assert_called_once_with(symlink_to_uprev)

        mock_command_output.assert_called_once()

    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyRemovedPatchesFromFilesDir(self, mock_run_cmd):
        patches_to_remove_list = [
            "/abs/path/to/filesdir/cherry/fix_output.patch",
            "/abs/path/to/filesdir/display_results.patch",
        ]

        update_chromeos_llvm_hash.RemovePatchesFromFilesDir(
            patches_to_remove_list
        )

        self.assertEqual(mock_run_cmd.call_count, 2)

    @mock.patch.object(os.path, "isfile", return_value=False)
    def testInvalidPatchMetadataFileStagedForCommit(self, mock_isfile):
        patch_metadata_path = "/abs/path/to/filesdir/PATCHES"

        # Verify the exception is raised when the absolute path to the patch
        # metadata file does not exist or is not a file.
        with self.assertRaises(ValueError) as err:
            update_chromeos_llvm_hash.StagePatchMetadataFileForCommit(
                patch_metadata_path
            )

        self.assertEqual(
            str(err.exception),
            "Invalid patch metadata file provided: " "%s" % patch_metadata_path,
        )

        mock_isfile.assert_called_once()

    @mock.patch.object(os.path, "isfile", return_value=True)
    @mock.patch.object(subprocess, "check_output", return_value=None)
    def testSuccessfullyStagedPatchMetadataFileForCommit(self, mock_run_cmd, _):
        patch_metadata_path = "/abs/path/to/filesdir/PATCHES.json"

        update_chromeos_llvm_hash.StagePatchMetadataFileForCommit(
            patch_metadata_path
        )

        mock_run_cmd.assert_called_once()

    def testNoPatchResultsForCommit(self):
        package_1_patch_info = patch_utils.PatchInfo(
            applied_patches=[self._make_patch_entry("display_results.patch")],
            failed_patches=[self._make_patch_entry("fixes_output.patch")],
            non_applicable_patches=[],
            disabled_patches=[],
            removed_patches=[],
            modified_metadata=None,
        )

        package_2_patch_info = patch_utils.PatchInfo(
            applied_patches=[
                self._make_patch_entry("redirects_stdout.patch"),
                self._make_patch_entry("fix_display.patch"),
            ],
            failed_patches=[],
            non_applicable_patches=[],
            disabled_patches=[],
            removed_patches=[],
            modified_metadata=None,
        )

        test_package_info_dict = {
            "test-packages/package1": package_1_patch_info,
            "test-packages/package2": package_2_patch_info,
        }

        test_commit_message = ["Updated packages"]

        self.assertListEqual(
            update_chromeos_llvm_hash.StagePackagesPatchResultsForCommit(
                test_package_info_dict, test_commit_message
            ),
            test_commit_message,
        )

    @mock.patch.object(
        update_chromeos_llvm_hash, "StagePatchMetadataFileForCommit"
    )
    @mock.patch.object(update_chromeos_llvm_hash, "RemovePatchesFromFilesDir")
    def testAddedPatchResultsForCommit(
        self, mock_remove_patches, mock_stage_patches_for_commit
    ):
        package_1_patch_info = patch_utils.PatchInfo(
            applied_patches=[],
            failed_patches=[],
            non_applicable_patches=[],
            disabled_patches=["fixes_output.patch"],
            removed_patches=[],
            modified_metadata="/abs/path/to/filesdir/PATCHES.json",
        )

        package_2_patch_info = patch_utils.PatchInfo(
            applied_patches=[self._make_patch_entry("fix_display.patch")],
            failed_patches=[],
            non_applicable_patches=[],
            disabled_patches=[],
            removed_patches=["/abs/path/to/filesdir/redirect_stdout.patch"],
            modified_metadata="/abs/path/to/filesdir/PATCHES.json",
        )

        test_package_info_dict = {
            "test-packages/package1": package_1_patch_info,
            "test-packages/package2": package_2_patch_info,
        }

        test_commit_message = ["Updated packages"]

        expected_commit_messages = [
            "Updated packages",
            "\nFor the package test-packages/package1:",
            "The patch metadata file PATCHES.json was modified",
            "The following patches were disabled:",
            "fixes_output.patch",
            "\nFor the package test-packages/package2:",
            "The patch metadata file PATCHES.json was modified",
            "The following patches were removed:",
            "redirect_stdout.patch",
        ]

        self.assertListEqual(
            update_chromeos_llvm_hash.StagePackagesPatchResultsForCommit(
                test_package_info_dict, test_commit_message
            ),
            expected_commit_messages,
        )

        path_to_removed_patch = "/abs/path/to/filesdir/redirect_stdout.patch"

        mock_remove_patches.assert_called_once_with([path_to_removed_patch])

        self.assertEqual(mock_stage_patches_for_commit.call_count, 2)

    def setup_mock_src_tree(self, src_tree: Path):
        package_dir = (
            src_tree / "src/third_party/chromiumos-overlay/sys-devel/llvm"
        )
        package_dir.mkdir(parents=True)
        ebuild_path = package_dir / "llvm-00.00_pre0_p0.ebuild"
        with ebuild_path.open("w", encoding="utf-8") as f:
            f.writelines(
                [
                    'LLVM_HASH="abcdef123456" # r123456',
                    'LLVM_NEXT_HASH="987654321fedcba" # r99453',
                ]
            )
        symlink_path = package_dir / "llvm-00.00_pre0_p0-r1234.ebuild"
        symlink_path.symlink_to(ebuild_path)
        return package_dir, ebuild_path, symlink_path

    def testPortagePackageConstruction(self):
        with tempfile.TemporaryDirectory(
            "update_chromeos_llvm_hash.tmp"
        ) as workdir_str:
            src_tree = Path(workdir_str)
            package_dir, ebuild_path, symlink_path = self.setup_mock_src_tree(
                src_tree
            )

            # Test that we're upreving if there's a symlink.
            def mock_find_package_ebuild(_, package_name):
                self.assertEqual(
                    package_name,
                    f"{package_dir.parent.name}/{package_dir.name}",
                )
                return symlink_path

            with mock.patch(
                "update_chromeos_llvm_hash.PortagePackage.find_package_ebuild",
                mock_find_package_ebuild,
            ):
                pkg = update_chromeos_llvm_hash.PortagePackage(
                    src_tree, "sys-devel/llvm"
                )
                self.assertEqual(pkg.uprev_target, symlink_path.absolute())
                self.assertEqual(pkg.ebuild_path, ebuild_path.absolute())
                self.assertEqual(pkg.live_ebuild(), None)

                # Make sure if the live ebuild is there, we find it.
                live_ebuild_path = package_dir / "llvm-9999.ebuild"
                live_ebuild_path.touch()

                pkg = update_chromeos_llvm_hash.PortagePackage(
                    src_tree, "sys-devel/llvm"
                )
                self.assertEqual(pkg.live_ebuild(), live_ebuild_path)

    @mock.patch("subprocess.run")
    @mock.patch("subprocess.check_output")
    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    def testUpdatePackages(
        self, mock_llvm_major_version, _mock_check_output, _mock_run
    ):
        mock_llvm_major_version.return_value = "17"
        with tempfile.TemporaryDirectory(
            "update_chromeos_llvm_hash.tmp"
        ) as workdir_str:
            src_tree = Path(workdir_str)
            _package_dir, _ebuild_path, symlink_path = self.setup_mock_src_tree(
                src_tree
            )

            def mock_find_package_ebuild(*_):
                return symlink_path

            with mock.patch(
                "update_chromeos_llvm_hash.PortagePackage.find_package_ebuild",
                mock_find_package_ebuild,
            ):
                pkg = update_chromeos_llvm_hash.PortagePackage(
                    src_tree, "sys-devel/llvm"
                )
                pkg.update(
                    update_chromeos_llvm_hash.LLVMVariant.current,
                    "beef3333",
                    3333,
                )

    @mock.patch.object(chroot, "VerifyChromeOSRoot")
    @mock.patch.object(chroot, "VerifyOutsideChroot")
    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
    def testMainDefaults(
        self,
        mock_update_packages,
        mock_gethash,
        mock_outside_chroot,
        mock_chromeos_root,
    ):
        git_hash = "1234abcd"
        svn_version = 5678
        mock_gethash.return_value = (git_hash, svn_version)
        argv = [
            "./update_chromeos_llvm_hash_unittest.py",
            "--no_repo_manifest",
            "--llvm_version",
            "google3",
        ]

        with mock.patch.object(sys, "argv", argv) as mock.argv:
            update_chromeos_llvm_hash.main()

        expected_packages = set(update_chromeos_llvm_hash.DEFAULT_PACKAGES)
        expected_manifest_packages = set(
            update_chromeos_llvm_hash.DEFAULT_MANIFEST_PACKAGES,
        )
        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.current
        expected_chroot = update_chromeos_llvm_hash.defaultCrosRoot()
        mock_update_packages.assert_called_once_with(
            packages=expected_packages,
            manifest_packages=expected_manifest_packages,
            llvm_variant=expected_llvm_variant,
            git_hash=git_hash,
            svn_version=svn_version,
            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(expected_chroot),
            mode=failure_modes.FailureModes.FAIL,
            git_hash_source="google3",
            extra_commit_msg_lines=None,
            delete_branch=True,
            upload_changes=True,
        )
        mock_outside_chroot.assert_called()
        mock_chromeos_root.assert_called()

    @mock.patch.object(chroot, "VerifyChromeOSRoot")
    @mock.patch.object(chroot, "VerifyOutsideChroot")
    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
    def testMainLlvmNext(
        self,
        mock_update_packages,
        mock_gethash,
        mock_outside_chroot,
        mock_chromeos_root,
    ):
        git_hash = "1234abcd"
        svn_version = 5678
        mock_gethash.return_value = (git_hash, svn_version)
        argv = [
            "./update_chromeos_llvm_hash_unittest.py",
            "--llvm_version",
            "google3",
            "--is_llvm_next",
        ]

        with mock.patch.object(sys, "argv", argv) as mock.argv:
            update_chromeos_llvm_hash.main()

        expected_packages = set(update_chromeos_llvm_hash.DEFAULT_PACKAGES)
        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
        expected_chroot = update_chromeos_llvm_hash.defaultCrosRoot()
        # llvm-next upgrade does not update manifest by default.
        mock_update_packages.assert_called_once_with(
            packages=expected_packages,
            manifest_packages=set(),
            llvm_variant=expected_llvm_variant,
            git_hash=git_hash,
            svn_version=svn_version,
            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(expected_chroot),
            mode=failure_modes.FailureModes.FAIL,
            git_hash_source="google3",
            extra_commit_msg_lines=None,
            delete_branch=True,
            upload_changes=True,
        )
        mock_outside_chroot.assert_called()
        mock_chromeos_root.assert_called()

    @mock.patch.object(chroot, "VerifyChromeOSRoot")
    @mock.patch.object(chroot, "VerifyOutsideChroot")
    @mock.patch.object(get_llvm_hash, "GetLLVMHashAndVersionFromSVNOption")
    @mock.patch.object(update_chromeos_llvm_hash, "UpdatePackages")
    def testMainAllArgs(
        self,
        mock_update_packages,
        mock_gethash,
        mock_outside_chroot,
        mock_chromeos_root,
    ):
        packages_to_update = "test-packages/package1,test-libs/lib1"
        manifest_packages = "test-libs/lib1,test-libs/lib2"
        failure_mode = failure_modes.FailureModes.DISABLE_PATCHES
        chromeos_path = Path("/some/path/to/chromeos")
        llvm_ver = 435698
        git_hash = "1234abcd"
        svn_version = 5678
        mock_gethash.return_value = (git_hash, svn_version)

        argv = [
            "./update_chromeos_llvm_hash_unittest.py",
            "--llvm_version",
            str(llvm_ver),
            "--is_llvm_next",
            "--chromeos_path",
            str(chromeos_path),
            "--update_packages",
            packages_to_update,
            "--manifest_packages",
            manifest_packages,
            "--failure_mode",
            failure_mode.value,
            "--patch_metadata_file",
            "META.json",
            "--no_repo_manifest",
        ]

        with mock.patch.object(sys, "argv", argv) as mock.argv:
            update_chromeos_llvm_hash.main()

        expected_packages = {"test-packages/package1", "test-libs/lib1"}
        expected_manifest_packages = {"test-libs/lib1", "test-libs/lib2"}
        expected_llvm_variant = update_chromeos_llvm_hash.LLVMVariant.next
        mock_update_packages.assert_called_once_with(
            packages=expected_packages,
            manifest_packages=expected_manifest_packages,
            llvm_variant=expected_llvm_variant,
            git_hash=git_hash,
            svn_version=svn_version,
            chroot_opts=update_chromeos_llvm_hash.ChrootOpts(chromeos_path),
            mode=failure_mode,
            git_hash_source=llvm_ver,
            extra_commit_msg_lines=None,
            delete_branch=True,
            upload_changes=True,
        )
        mock_outside_chroot.assert_called()
        mock_chromeos_root.assert_called()

    @mock.patch.object(subprocess, "check_output", return_value=None)
    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    def testEnsurePackageMaskContainsExisting(
        self, mock_llvm_version, mock_git_add
    ):
        chromeos_path = "absolute/path/to/chromeos"
        git_hash = "badf00d"
        mock_llvm_version.return_value = "1234"
        with mock.patch(
            "update_chromeos_llvm_hash.open",
            mock.mock_open(read_data="\n=sys-devel/llvm-1234.0_pre*\n"),
            create=True,
        ) as mock_file:
            update_chromeos_llvm_hash.EnsurePackageMaskContains(
                chromeos_path, git_hash
            )
            handle = mock_file()
            handle.write.assert_not_called()
        mock_llvm_version.assert_called_once_with(git_hash)

        overlay_dir = (
            "absolute/path/to/chromeos/src/third_party/chromiumos-overlay"
        )
        mask_path = overlay_dir + "/profiles/targets/chromeos/package.mask"
        mock_git_add.assert_called_once_with(
            ["git", "-C", overlay_dir, "add", mask_path]
        )

    @mock.patch.object(subprocess, "check_output", return_value=None)
    @mock.patch.object(get_llvm_hash, "GetLLVMMajorVersion")
    def testEnsurePackageMaskContainsNotExisting(
        self, mock_llvm_version, mock_git_add
    ):
        chromeos_path = "absolute/path/to/chromeos"
        git_hash = "badf00d"
        mock_llvm_version.return_value = "1234"
        with mock.patch(
            "update_chromeos_llvm_hash.open",
            mock.mock_open(read_data="nothing relevant"),
            create=True,
        ) as mock_file:
            update_chromeos_llvm_hash.EnsurePackageMaskContains(
                chromeos_path, git_hash
            )
            handle = mock_file()
            handle.write.assert_called_once_with(
                "=sys-devel/llvm-1234.0_pre*\n"
            )
        mock_llvm_version.assert_called_once_with(git_hash)

        overlay_dir = (
            "absolute/path/to/chromeos/src/third_party/chromiumos-overlay"
        )
        mask_path = overlay_dir + "/profiles/targets/chromeos/package.mask"
        mock_git_add.assert_called_once_with(
            ["git", "-C", overlay_dir, "add", mask_path]
        )


if __name__ == "__main__":
    unittest.main()
