xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/get_patch_unittest.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2024 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 get_patch."""
7
8import json
9from pathlib import Path
10import tempfile
11from typing import Any, Dict, Generator, List, Set
12import unittest
13from unittest import mock
14
15import get_patch
16import git_llvm_rev
17
18
19COMMIT_FIXTURES: List[Dict[str, Any]] = [
20    {"subject": "A commit subject", "sha": "abcdef1234567890", "rev": 5},
21    {"subject": "Another commit subject", "sha": "feed9999", "rev": 9},
22]
23
24JSON_FIXTURE: List[Dict[str, Any]] = [
25    {
26        "metadata": {"title": "An existing patch"},
27        "platforms": ["another platform"],
28        "rel_patch_path": "cherry/nowhere.patch",
29        "version_range": {"from": 1, "until": 256},
30    },
31]
32
33
34def _mock_get_commit_subj(_, sha: str) -> str:
35    gen: Generator[Dict[str, Any], None, None] = (
36        fixture for fixture in COMMIT_FIXTURES if fixture["sha"] == sha
37    )
38    return next(gen)["subject"]
39
40
41def _mock_to_rev(sha: get_patch.LLVMGitRef, _) -> git_llvm_rev.Rev:
42    gen: Generator[Dict[str, Any], None, None] = (
43        fixture for fixture in COMMIT_FIXTURES if fixture["sha"] == sha.git_ref
44    )
45    return git_llvm_rev.Rev("main", next(gen)["rev"])
46
47
48def _mock_from_rev(_, rev: git_llvm_rev.Rev) -> get_patch.LLVMGitRef:
49    gen: Generator[Dict[str, Any], None, None] = (
50        fixture for fixture in COMMIT_FIXTURES if fixture["rev"] == rev.number
51    )
52    return get_patch.LLVMGitRef(next(gen)["sha"])
53
54
55def _mock_git_format_patch(*_) -> str:
56    return "[category] This is a fake commit fixture"
57
58
59def _mock_write_patch(*_) -> None:
60    return
61
62
63def _mock_get_changed_packages(*_) -> Set[Path]:
64    return {get_patch.LLVM_PKG_PATH}
65
66
67class TestGetPatch(unittest.TestCase):
68    """Test case harness for get_patch."""
69
70    def setUp(self) -> None:
71        """Set up the mocks and directory structure."""
72
73        self.module_patcher = mock.patch.multiple(
74            "get_patch",
75            get_commit_subj=_mock_get_commit_subj,
76            git_format_patch=_mock_git_format_patch,
77            get_changed_packages=_mock_get_changed_packages,
78            _write_patch=_mock_write_patch,
79        )
80        self.module_patcher.start()
81        self.addCleanup(self.module_patcher.stop)
82        self.llvm_gitsha_patcher = mock.patch.multiple(
83            "get_patch.LLVMGitRef",
84            to_rev=_mock_to_rev,
85            from_rev=_mock_from_rev,
86        )
87        self.llvm_gitsha_patcher.start()
88        self.addCleanup(self.llvm_gitsha_patcher.stop)
89
90        self.llvm_project_dir = Path(tempfile.mkdtemp())
91        self.addCleanup(self.llvm_project_dir.rmdir)
92        self.chromiumos_root = Path(tempfile.mkdtemp())
93        self.addCleanup(self.chromiumos_root.rmdir)
94        self.workdir = self.chromiumos_root / get_patch.LLVM_PKG_PATH / "files"
95        self.workdir.mkdir(parents=True, exist_ok=True)
96
97        def _cleanup_workdir():
98            # We individually clean up these directories as a guarantee
99            # we aren't creating any extraneous files. We don't want to
100            # use shm.rmtree here because we don't want clean up any
101            # files unaccounted for.
102            workdir_recurse = self.workdir
103            while workdir_recurse not in (self.chromiumos_root, Path.root):
104                workdir_recurse.rmdir()
105                workdir_recurse = workdir_recurse.parent
106
107        self.addCleanup(_cleanup_workdir)
108
109        self.patches_json_file = (
110            self.workdir / get_patch.PATCH_METADATA_FILENAME
111        )
112        start_ref = get_patch.LLVMGitRef("abcdef1234567890")
113        self.ctx = get_patch.PatchContext(
114            self.llvm_project_dir,
115            self.chromiumos_root,
116            start_ref,
117            platforms=["some platform"],
118        )
119
120    def write_json_fixture(self) -> None:
121        with self.patches_json_file.open("w", encoding="utf-8") as f:
122            json.dump(JSON_FIXTURE, f)
123            f.write("\n")
124
125    def test_bad_cherrypick_version(self) -> None:
126        """Test that bad cherrypick versions raises."""
127        start_sha_fixture = COMMIT_FIXTURES[0]
128
129        def _try_make_patches():
130            # This fixture is the same as the start_sha.
131            self.ctx.make_patches(
132                get_patch.LLVMGitRef(start_sha_fixture["sha"])
133            )
134
135        self.assertRaises(get_patch.CherrypickVersionError, _try_make_patches)
136
137    def test_make_patches(self) -> None:
138        """Test we can make patch entries from a git commit."""
139
140        fixture = COMMIT_FIXTURES[1]
141        # We manually write and delete this file because it must have the name
142        # as specified by get_patch. tempfile cannot guarantee us this name.
143        self.write_json_fixture()
144        try:
145            entries = self.ctx.make_patches(
146                get_patch.LLVMGitRef(fixture["sha"])
147            )
148            self.assertEqual(len(entries), 1)
149            if entries[0].metadata:
150                self.assertEqual(
151                    entries[0].metadata["title"], fixture["subject"]
152                )
153            else:
154                self.fail("metadata was None")
155        finally:
156            self.patches_json_file.unlink()
157
158    def test_apply_patch_to_json(self) -> None:
159        """Test we can apply patches to the JSON file."""
160
161        fixture = COMMIT_FIXTURES[1]
162        fixture_sha = fixture["sha"]
163        expected_json_entry = {
164            "metadata": {"title": fixture["subject"], "info": []},
165            "platforms": ["some platform"],
166            "rel_patch_path": f"cherry/{fixture_sha}.patch",
167            "version_range": {
168                "from": self.ctx.start_ref.to_rev(self.llvm_project_dir).number,
169                "until": fixture["rev"],
170            },
171        }
172        cherrydir = self.workdir / "cherry"
173        cherrydir.mkdir()
174        self._apply_patch_to_json_helper(fixture, expected_json_entry)
175        cherrydir.rmdir()
176
177    def test_apply_patch_to_json_no_cherry(self) -> None:
178        """Test we can apply patches to the JSON file, without a cherry dir."""
179
180        fixture = COMMIT_FIXTURES[1]
181        fixture_sha = fixture["sha"]
182        expected_json_entry = {
183            "metadata": {"title": fixture["subject"], "info": []},
184            "platforms": ["some platform"],
185            "rel_patch_path": f"{fixture_sha}.patch",
186            "version_range": {
187                "from": self.ctx.start_ref.to_rev(self.llvm_project_dir).number,
188                "until": fixture["rev"],
189            },
190        }
191        self._apply_patch_to_json_helper(fixture, expected_json_entry)
192
193    def _apply_patch_to_json_helper(self, fixture, expected_json_entry) -> None:
194        # We manually write and delete this file because it must have the name
195        # as specified by get_patch. tempfile cannot guarantee us this name.
196        self.write_json_fixture()
197        patch_source = get_patch.LLVMGitRef.from_rev(
198            self.llvm_project_dir,
199            git_llvm_rev.Rev("origin", fixture["rev"]),
200        )
201        try:
202            self.ctx.apply_patches(patch_source)
203            with self.patches_json_file.open(encoding="utf-8") as f:
204                edited = json.load(f)
205            self.assertEqual(edited, JSON_FIXTURE + [expected_json_entry])
206        finally:
207            self.patches_json_file.unlink()
208
209    def test_apply_patch_dry_run(self) -> None:
210        """Test dry running patches does nothing."""
211
212        fixture = COMMIT_FIXTURES[1]
213        old_dry_run = self.ctx.dry_run
214        self.ctx.dry_run = True
215        # We manually write and delete this file because it must have the name
216        # as specified by get_patch. tempfile cannot guarantee us this name.
217        self.write_json_fixture()
218        patch_source = get_patch.LLVMGitRef.from_rev(
219            self.llvm_project_dir,
220            git_llvm_rev.Rev("origin", fixture["rev"]),
221        )
222        try:
223            self.ctx.apply_patches(patch_source)
224            with self.patches_json_file.open(encoding="utf-8") as f:
225                maybe_edited = json.load(f)
226            self.assertEqual(maybe_edited, JSON_FIXTURE)
227        finally:
228            self.ctx.dry_run = old_dry_run
229            self.patches_json_file.unlink()
230
231
232if __name__ == "__main__":
233    unittest.main()
234