| # Copyright 2024 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Tests for verify_patch_consistency.""" |
| |
| import dataclasses |
| import json |
| import os |
| from pathlib import Path |
| import subprocess |
| from typing import Callable |
| from unittest import mock |
| |
| from cros_utils import git_utils |
| from llvm_tools import git_llvm_rev |
| from llvm_tools import llvm_project_base_commit |
| from llvm_tools import patch_utils |
| from llvm_tools import test_helpers |
| from llvm_tools import verify_patch_consistency |
| |
| |
| GERRIT_JSON_FIXTURE = """\ |
| [ |
| { |
| "project": "github.com/llvm/llvm-project", |
| "branch": "chromeos/llvm-r516547-1", |
| "createdOn": 1718657891, |
| "lastUpdated": 1720459316, |
| "id": "If250deb1c592b3cf054cca8c0cd530f3d6fd4f89", |
| "owner": { |
| "name": "User McUserface", |
| "email": "someuserhere@google.com", |
| "username": "User McUserface" |
| }, |
| "number": "5637483", |
| "url": "https://crrev.com/c/5637483", |
| "status": "ABANDONED", |
| "subject": "Revert \\"add_tablegen: Quick fix to reflect LLVM_TABLEGEN\\"", |
| "private": false, |
| "topic": null, |
| "currentPatchSet": { |
| "approvals": [ |
| { |
| "type": "CRVW", |
| "description": "Code-Review", |
| "value": "0", |
| "grantedOn": 1718657891, |
| "by": { |
| "name": "User McUserface", |
| "email": "someuserhere@google.com", |
| "username": "User McUserface" |
| } |
| }, |
| { |
| "type": "COMR", |
| "description": "Commit-Queue", |
| "value": "0", |
| "grantedOn": 1718752263, |
| "by": { |
| "name": "User McUserface", |
| "email": "someuserhere@google.com", |
| "username": "User McUserface" |
| } |
| }, |
| { |
| "type": "VRIF", |
| "description": "Verified", |
| "value": "0", |
| "grantedOn": 1718657891, |
| "by": { |
| "name": "User McUserface", |
| "email": "someuserhere@google.com", |
| "username": "User McUserface" |
| } |
| } |
| ], |
| "ref": "refs/changes/83/5637483/1", |
| "revision": "9f316824661b96d0ba586ff48b6128f7e9783f19", |
| "number": "1", |
| "date": 1718657800, |
| "draft": false |
| }, |
| "commitMessage": "A commit message", |
| "dependsOn": [ |
| { |
| "revision": "210497ee293346804b43ece37fb9d6658c39ab34" |
| } |
| ] |
| } |
| ] |
| """ |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class _RunnerArgs: |
| main_sha: str |
| patch_branch: str |
| toolchain_utils_dir: Path |
| llvm_src_dir: Path |
| patches_json: Path |
| chromiumos_overlay: Path |
| |
| |
| class TestVerifyPatchConsistency(test_helpers.TempDirTestCase): |
| |
| """Test verify_patch_consistency.""" |
| |
| def __init__(self, *nargs, **kwargs): |
| super().__init__(*nargs, **kwargs) |
| self.git_utils_patcher = None |
| self.git_llvm_rev_patcher = None |
| self.verify_patch_consistency_patcher = None |
| |
| @mock.patch.object(verify_patch_consistency, "_gerrit_inspect") |
| def test_parse_branch_simple(self, mock_gerrit_inspect): |
| """Test we're extracting the llvm revision and ref from gerrit json.""" |
| |
| mock_gerrit_inspect.return_value = [ |
| { |
| "branch": "chromeos/llvm-r1234567-42", |
| "currentPatchSet": {"ref": "some_remote_ref"}, |
| } |
| ] |
| llvm_rev, ref = verify_patch_consistency.parse_branch(10101, Path()) |
| self.assertEqual(llvm_rev, 1234567) |
| self.assertEqual(ref, "some_remote_ref") |
| |
| @mock.patch.object(verify_patch_consistency, "_gerrit_inspect") |
| def test_parse_branch_complex(self, mock_gerrit_inspect): |
| """Test parse_branch with a real JSON response.""" |
| |
| mock_gerrit_inspect.return_value = json.loads(GERRIT_JSON_FIXTURE) |
| llvm_rev, ref = verify_patch_consistency.parse_branch(5637483, Path()) |
| self.assertEqual(llvm_rev, 516547) |
| self.assertEqual(ref, "refs/changes/83/5637483/1") |
| |
| def _set_up_mocking(self, translate_sha: str, fetch_head_ref: str): |
| # Patch git_utils ---------------------------------------------- |
| self.git_utils_patcher = mock.patch.multiple( |
| git_utils, |
| # Do not allow network calls. |
| fetch=lambda *_, **__: None, |
| # FETCH_HEAD may not exist, so just mock resolve_ref. It's only |
| # used in printing here. |
| resolve_ref=lambda *_, **__: "MOCK", |
| ) |
| self.git_utils_patcher.start() |
| |
| # Patch git_llvm_rev ------------------------------------------ |
| self.git_llvm_rev_patcher = mock.patch.multiple( |
| git_llvm_rev, |
| # Don't actually try to search through the LLVM project |
| # git shas. We don't have the known reference SHAs to |
| # do that. |
| translate_rev_to_sha=lambda _, __: translate_sha, |
| ) |
| self.git_llvm_rev_patcher.start() |
| |
| # Mock verify_patch_consistency ------------------------------- |
| |
| # NOTE: |
| # We need to capture the original ref_diff reference here, |
| # because otherwise by the time that _mock_ref_diff evaluation |
| # actually happens, the original verify_patch_consistency will |
| # be fully monkeypatched out, and we'll get infinite recursion. |
| ref_diff_capture = verify_patch_consistency.ref_diff |
| |
| def _mock_ref_diff(cwd: Path, ref1: str, _: str) -> str: |
| return ref_diff_capture(cwd, ref1, fetch_head_ref) |
| |
| self.verify_patch_consistency_patcher = mock.patch.multiple( |
| verify_patch_consistency, |
| ref_diff=_mock_ref_diff, |
| ) |
| self.verify_patch_consistency_patcher.start() |
| |
| def _stop_mocking(self): |
| if self.verify_patch_consistency_patcher: |
| self.verify_patch_consistency_patcher.stop() |
| if self.git_llvm_rev_patcher: |
| self.git_llvm_rev_patcher.stop() |
| if self.git_utils_patcher: |
| self.git_utils_patcher.stop() |
| |
| def _run_llvm_harness(self, tempdir: Path, runner: Callable): |
| """Set up for full verification tests.""" |
| |
| # Set up directories and paths. |
| # Current layout is: |
| # |
| # toolchain-utils/ |
| # OWNERS |
| # OWNERS.toolchain |
| # sys-devel/llvm/ |
| # llvm-9999.ebuild |
| # llvm-project/ |
| # PATCHES.json |
| # patch.patch |
| # |
| fake_toolchain_utils = tempdir / "toolchain-utils" |
| fake_toolchain_utils.mkdir() |
| (fake_toolchain_utils / "OWNERS").touch() |
| (fake_toolchain_utils / "OWNERS.toolchain").touch() |
| fake_patches_json_path = tempdir / "PATCHES.json" |
| fake_chromiumos_overlay = tempdir / "chromiumos-overlay" |
| for p in patch_utils.CHROMEOS_PATCHES_JSON_PACKAGES: |
| package_name = os.path.basename(p) |
| live_ebuild = ( |
| fake_chromiumos_overlay / p / f"{package_name}-9999.ebuild" |
| ) |
| live_ebuild.parent.mkdir(parents=True) |
| # Always use llvm as the CMAKE_USE_DIR; it's simplest to mock, and |
| # we don't gain meaningful additional coverage by varying it. |
| live_ebuild.write_text( |
| 'export CMAKE_USE_DIR="${S}/llvm"\n', encoding="utf-8" |
| ) |
| patch_name = "patch.patch" |
| with fake_patches_json_path.open("w", encoding="utf-8") as f: |
| json.dump([{"rel_patch_path": patch_name}], f) |
| fake_llvm_src_dir = tempdir / "llvm-project" |
| fake_llvm_src_dir.mkdir() |
| cmake_file = fake_llvm_src_dir / "llvm" / "CMakeLists.txt" |
| cmake_file.parent.mkdir() |
| cmake_file.touch() |
| |
| # Set up git state. |
| # There's a lot going on here, but it mostly sets up commits |
| # like so: |
| # |
| # a main |
| # \ |
| # -> b -> c patch_branch |
| # |
| # where |
| # a = Initial commit |
| # b = ChromeOS Base commit |
| # c = Hello World Commit |
| subprocess.run( |
| ["git", "init", "-b", "main", "-q"], |
| cwd=fake_llvm_src_dir, |
| check=True, |
| ) |
| a_file = fake_llvm_src_dir / "a.txt" |
| a_file.touch() |
| git_utils.commit_all_changes(fake_llvm_src_dir, "Initial commit") |
| main_sha = subprocess.run( |
| ["git", "log", "-n1", "--format=%H"], |
| check=True, |
| cwd=fake_llvm_src_dir, |
| stdout=subprocess.PIPE, |
| encoding="utf-8", |
| ).stdout.strip() |
| patch_branch = "patch_branch" |
| subprocess.run( |
| ["git", "switch", "-C", patch_branch], |
| cwd=fake_llvm_src_dir, |
| check=True, |
| ) |
| llvm_project_base_commit.make_base_commit( |
| fake_toolchain_utils, |
| fake_llvm_src_dir, |
| chromiumos_overlay=fake_chromiumos_overlay, |
| ) |
| a_file.write_text("hello world", encoding="utf-8") |
| git_utils.commit_all_changes(fake_llvm_src_dir, "Hello world commit") |
| diff = git_utils.format_patch(fake_llvm_src_dir, "HEAD") |
| subprocess.run( |
| ["git", "switch", "-C", "main"], cwd=fake_llvm_src_dir, check=True |
| ) |
| (tempdir / patch_name).write_text(diff, encoding="utf-8") |
| runner( |
| _RunnerArgs( |
| main_sha=main_sha, |
| patch_branch=patch_branch, |
| toolchain_utils_dir=fake_toolchain_utils, |
| llvm_src_dir=fake_llvm_src_dir, |
| patches_json=fake_patches_json_path, |
| chromiumos_overlay=fake_chromiumos_overlay, |
| ) |
| ) |
| |
| def test_failed_verification(self): |
| """Test we can catch a bad patch stack.""" |
| tempdir = self.make_tempdir() |
| |
| def _runner(args: _RunnerArgs): |
| # Actually run the (failing) verification. |
| # This fails because the "upstream" of fetch_head_ref |
| # is the same as the translated_sha (which is in |
| # the "main" that matches the svn_revision). |
| self._set_up_mocking( |
| translate_sha=args.main_sha, fetch_head_ref=args.main_sha |
| ) |
| try: |
| self.assertFalse( |
| verify_patch_consistency.verify_in_worktree( |
| toolchain_utils_dir=args.toolchain_utils_dir, |
| llvm_src_dir=args.llvm_src_dir, |
| patches_json=args.patches_json, |
| chromiumos_overlay=args.chromiumos_overlay, |
| svn_revision=1234, |
| cl_ref=args.main_sha, |
| ) |
| ) |
| finally: |
| self._stop_mocking() |
| |
| self._run_llvm_harness(tempdir, _runner) |
| |
| def test_successful_verification(self): |
| """Test we can successfully verify a patch stack.""" |
| tempdir = self.make_tempdir() |
| |
| def _runner(args: _RunnerArgs): |
| # Actually run the verification. Notably, |
| # the difference here between the fail case is the |
| # fetch_head_ref is the patch_branch which acts |
| # as our "upstream". |
| self._set_up_mocking( |
| translate_sha=args.main_sha, |
| fetch_head_ref=args.patch_branch, |
| ) |
| try: |
| self.assertTrue( |
| verify_patch_consistency.verify_in_worktree( |
| toolchain_utils_dir=args.toolchain_utils_dir, |
| llvm_src_dir=args.llvm_src_dir, |
| patches_json=args.patches_json, |
| chromiumos_overlay=args.chromiumos_overlay, |
| svn_revision=1234, |
| cl_ref=args.patch_branch, |
| ) |
| ) |
| finally: |
| self._stop_mocking() |
| |
| self._run_llvm_harness(tempdir, _runner) |