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