xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/patch_utils_unittest.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2022 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 the patch_utils.py file."""
7
8import copy
9import io
10import json
11from pathlib import Path
12import shutil
13import subprocess
14import tempfile
15import unittest
16from unittest import mock
17
18import patch_utils as pu
19
20
21class TestPatchUtils(unittest.TestCase):
22    """Test the patch_utils."""
23
24    def make_tempdir(self) -> Path:
25        tmpdir = Path(tempfile.mkdtemp(prefix="patch_utils_unittest"))
26        self.addCleanup(shutil.rmtree, tmpdir)
27        return tmpdir
28
29    def test_predict_indent(self):
30        test_str1 = """
31a
32  a
33      a
34  a
35a
36"""
37        self.assertEqual(pu.predict_indent(test_str1.splitlines()), 2)
38        test_str2 = """
39a
40    a
41        a
42    a
43a
44"""
45        self.assertEqual(pu.predict_indent(test_str2.splitlines()), 4)
46
47    def test_from_to_dict(self):
48        """Test to and from dict conversion."""
49        d = TestPatchUtils._default_json_dict()
50        d["metadata"] = {
51            "title": "hello world",
52            "info": [],
53            "other_extra_info": {
54                "extra_flags": [],
55            },
56        }
57        e = pu.PatchEntry.from_dict(TestPatchUtils._mock_dir(), d)
58        self.assertEqual(d, e.to_dict())
59
60        # Test that they aren't serialised the same, as 'd' isn't sorted.
61        self.assertNotEqual(
62            json.dumps(d["metadata"]), json.dumps(e.to_dict()["metadata"])
63        )
64        self.assertEqual(
65            ["info", "other_extra_info", "title"],
66            list(e.to_dict()["metadata"].keys()),
67        )
68
69    def test_patch_path(self):
70        """Test that we can get the full path from a PatchEntry."""
71        d = TestPatchUtils._default_json_dict()
72        with mock.patch.object(Path, "is_dir", return_value=True):
73            entry = pu.PatchEntry.from_dict(Path("/home/dir"), d)
74            self.assertEqual(
75                entry.patch_path(), Path("/home/dir") / d["rel_patch_path"]
76            )
77
78    def test_can_patch_version(self):
79        """Test that patch application based on version is correct."""
80        base_dict = TestPatchUtils._default_json_dict()
81        workdir = TestPatchUtils._mock_dir()
82        e1 = pu.PatchEntry.from_dict(workdir, base_dict)
83        self.assertFalse(e1.can_patch_version(3))
84        self.assertTrue(e1.can_patch_version(4))
85        self.assertTrue(e1.can_patch_version(5))
86        self.assertFalse(e1.can_patch_version(9))
87        base_dict["version_range"] = {"until": 9}
88        e2 = pu.PatchEntry.from_dict(workdir, base_dict)
89        self.assertTrue(e2.can_patch_version(0))
90        self.assertTrue(e2.can_patch_version(5))
91        self.assertFalse(e2.can_patch_version(9))
92        base_dict["version_range"] = {"from": 4}
93        e3 = pu.PatchEntry.from_dict(workdir, base_dict)
94        self.assertFalse(e3.can_patch_version(3))
95        self.assertTrue(e3.can_patch_version(5))
96        self.assertTrue(e3.can_patch_version(1 << 31))
97        base_dict["version_range"] = {"from": 4, "until": None}
98        e4 = pu.PatchEntry.from_dict(workdir, base_dict)
99        self.assertFalse(e4.can_patch_version(3))
100        self.assertTrue(e4.can_patch_version(5))
101        self.assertTrue(e4.can_patch_version(1 << 31))
102        base_dict["version_range"] = {"from": None, "until": 9}
103        e5 = pu.PatchEntry.from_dict(workdir, base_dict)
104        self.assertTrue(e5.can_patch_version(0))
105        self.assertTrue(e5.can_patch_version(5))
106        self.assertFalse(e5.can_patch_version(9))
107
108    def test_can_parse_from_json(self):
109        """Test that patches be loaded from json."""
110        patches_json = """
111[
112  {
113    "metadata": {},
114    "platforms": [],
115    "rel_patch_path": "cherry/nowhere.patch",
116    "version_range": {}
117  },
118  {
119    "metadata": {},
120    "rel_patch_path": "cherry/somewhere.patch",
121    "version_range": {}
122  },
123  {
124    "rel_patch_path": "where.patch",
125    "version_range": null
126  },
127  {
128    "rel_patch_path": "cherry/anywhere.patch"
129  }
130]
131    """
132        result = pu.json_str_to_patch_entries(Path(), patches_json)
133        self.assertEqual(len(result), 4)
134
135        result = pu.json_to_patch_entries(Path(), io.StringIO(patches_json))
136        self.assertEqual(len(result), 4)
137
138    def test_parsed_hunks(self):
139        """Test that we can parse patch file hunks."""
140        m = mock.mock_open(read_data=_EXAMPLE_PATCH)
141
142        def mocked_open(self, *args, **kwargs):
143            return m(self, *args, **kwargs)
144
145        with mock.patch.object(Path, "open", mocked_open):
146            e = pu.PatchEntry.from_dict(
147                TestPatchUtils._mock_dir(), TestPatchUtils._default_json_dict()
148            )
149            hunk_dict = e.parsed_hunks()
150
151        m.assert_called()
152        filename1 = "clang/lib/Driver/ToolChains/Clang.cpp"
153        filename2 = "llvm/lib/Passes/PassBuilder.cpp"
154        self.assertEqual(set(hunk_dict.keys()), {filename1, filename2})
155        hunk_list1 = hunk_dict[filename1]
156        hunk_list2 = hunk_dict[filename2]
157        self.assertEqual(len(hunk_list1), 1)
158        self.assertEqual(len(hunk_list2), 2)
159
160    def test_apply_when_patch_nonexistent(self):
161        """Test that we error out when we try to apply a non-existent patch."""
162        src_dir = TestPatchUtils._mock_dir("somewhere/llvm-project")
163        patch_dir = TestPatchUtils._mock_dir()
164        e = pu.PatchEntry.from_dict(
165            patch_dir, TestPatchUtils._default_json_dict()
166        )
167        with mock.patch("subprocess.run", mock.MagicMock()):
168            self.assertRaises(RuntimeError, lambda: e.apply(src_dir))
169
170    def test_apply_success(self):
171        """Test that we can call apply."""
172        src_dir = TestPatchUtils._mock_dir("somewhere/llvm-project")
173        patch_dir = TestPatchUtils._mock_dir()
174        e = pu.PatchEntry.from_dict(
175            patch_dir, TestPatchUtils._default_json_dict()
176        )
177
178        # Make a deepcopy of the case for testing commit patch option.
179        e1 = copy.deepcopy(e)
180
181        with mock.patch("pathlib.Path.is_file", return_value=True):
182            with mock.patch("subprocess.run", mock.MagicMock()):
183                result = e.apply(src_dir)
184        self.assertTrue(result.succeeded)
185
186        # Test that commit patch option works.
187        with mock.patch("pathlib.Path.is_file", return_value=True):
188            with mock.patch("subprocess.run", mock.MagicMock()):
189                result1 = e1.apply(src_dir, pu.git_am)
190        self.assertTrue(result1.succeeded)
191
192    def test_parse_failed_patch_output(self):
193        """Test that we can call parse `patch` output."""
194        fixture = """
195checking file a/b/c.cpp
196Hunk #1 SUCCEEDED at 96 with fuzz 1.
197Hunk #12 FAILED at 77.
198Hunk #42 FAILED at 1979.
199checking file x/y/z.h
200Hunk #4 FAILED at 30.
201checking file works.cpp
202Hunk #1 SUCCEEDED at 96 with fuzz 1.
203"""
204        result = pu.parse_failed_patch_output(fixture)
205        self.assertEqual(result["a/b/c.cpp"], [12, 42])
206        self.assertEqual(result["x/y/z.h"], [4])
207        self.assertNotIn("works.cpp", result)
208
209    def test_is_git_dirty(self):
210        """Test if a git directory has uncommitted changes."""
211        dirpath = self.make_tempdir()
212
213        def _run_h(cmd):
214            subprocess.run(
215                cmd,
216                cwd=dirpath,
217                stdout=subprocess.DEVNULL,
218                stderr=subprocess.DEVNULL,
219                check=True,
220            )
221
222        _run_h(["git", "init"])
223        self.assertFalse(pu.is_git_dirty(dirpath))
224        test_file = dirpath / "test_file"
225        test_file.touch()
226        self.assertTrue(pu.is_git_dirty(dirpath))
227        _run_h(["git", "add", "."])
228        _run_h(["git", "commit", "-m", "test"])
229        self.assertFalse(pu.is_git_dirty(dirpath))
230        test_file.touch()
231        self.assertFalse(pu.is_git_dirty(dirpath))
232        with test_file.open("w", encoding="utf-8"):
233            test_file.write_text("abc")
234        self.assertTrue(pu.is_git_dirty(dirpath))
235
236    @mock.patch("patch_utils.git_clean_context", mock.MagicMock)
237    def test_update_version_ranges(self):
238        """Test the UpdateVersionRanges function."""
239        dirpath = self.make_tempdir()
240        patches = [
241            pu.PatchEntry(
242                workdir=dirpath,
243                rel_patch_path="x.patch",
244                metadata=None,
245                platforms=None,
246                version_range={
247                    "from": 0,
248                    "until": 2,
249                },
250            ),
251            pu.PatchEntry(
252                workdir=dirpath,
253                rel_patch_path="y.patch",
254                metadata=None,
255                platforms=None,
256                version_range={
257                    "from": 0,
258                    "until": 2,
259                },
260            ),
261            pu.PatchEntry(
262                workdir=dirpath,
263                rel_patch_path="z.patch",
264                metadata=None,
265                platforms=None,
266                version_range={
267                    "from": 4,
268                    "until": 5,
269                },
270            ),
271        ]
272
273        patches[0].apply = mock.MagicMock(
274            return_value=pu.PatchResult(
275                succeeded=False, failed_hunks={"a/b/c": []}
276            )
277        )
278        patches[1].apply = mock.MagicMock(
279            return_value=pu.PatchResult(succeeded=True)
280        )
281        patches[2].apply = mock.MagicMock(
282            return_value=pu.PatchResult(succeeded=False)
283        )
284
285        # Make a deepcopy of patches to test commit patch option
286        patches2 = copy.deepcopy(patches)
287
288        results, _ = pu.update_version_ranges_with_entries(
289            1, dirpath, patches, pu.gnu_patch
290        )
291
292        # We should only have updated the version_range of the first patch,
293        # as that one failed to apply.
294        self.assertEqual(len(results), 1)
295        self.assertEqual(results[0].version_range, {"from": 0, "until": 1})
296        self.assertEqual(patches[0].version_range, {"from": 0, "until": 1})
297        self.assertEqual(patches[1].version_range, {"from": 0, "until": 2})
298        self.assertEqual(patches[2].version_range, {"from": 4, "until": 5})
299
300        # Test git am option
301        results2, _ = pu.update_version_ranges_with_entries(
302            1, dirpath, patches2, pu.git_am
303        )
304
305        # We should only have updated the version_range of the first patch
306        # via git am, as that one failed to apply.
307        self.assertEqual(len(results2), 1)
308        self.assertEqual(results2[0].version_range, {"from": 0, "until": 1})
309        self.assertEqual(patches2[0].version_range, {"from": 0, "until": 1})
310        self.assertEqual(patches2[1].version_range, {"from": 0, "until": 2})
311        self.assertEqual(patches2[2].version_range, {"from": 4, "until": 5})
312
313    def test_remove_old_patches(self):
314        patches = [
315            {"rel_patch_path": "foo.patch"},
316            {
317                "rel_patch_path": "bar.patch",
318                "version_range": {
319                    "from": 1,
320                },
321            },
322            {
323                "rel_patch_path": "baz.patch",
324                "version_range": {
325                    "until": 1,
326                },
327            },
328        ]
329
330        tempdir = self.make_tempdir()
331        patches_json = tempdir / "PATCHES.json"
332        with patches_json.open("w", encoding="utf-8") as f:
333            json.dump(patches, f)
334
335        removed_paths = pu.remove_old_patches(
336            svn_version=10, patches_json=patches_json
337        )
338        self.assertEqual(removed_paths, [tempdir / "baz.patch"])
339        expected_patches = [
340            x for x in patches if x["rel_patch_path"] != "baz.patch"
341        ]
342        self.assertEqual(
343            json.loads(patches_json.read_text(encoding="utf-8")),
344            expected_patches,
345        )
346
347    @staticmethod
348    def _default_json_dict():
349        return {
350            "metadata": {
351                "title": "hello world",
352            },
353            "platforms": ["a"],
354            "rel_patch_path": "x/y/z",
355            "version_range": {
356                "from": 4,
357                "until": 9,
358            },
359        }
360
361    @staticmethod
362    def _mock_dir(path: str = "a/b/c"):
363        workdir = Path(path)
364        workdir = mock.MagicMock(workdir)
365        workdir.is_dir = lambda: True
366        workdir.joinpath = lambda x: Path(path).joinpath(x)
367        workdir.__truediv__ = lambda self, x: self.joinpath(x)
368        return workdir
369
370
371_EXAMPLE_PATCH = """
372diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
373index 5620a543438..099eb769ca5 100644
374--- a/clang/lib/Driver/ToolChains/Clang.cpp
375+++ b/clang/lib/Driver/ToolChains/Clang.cpp
376@@ -3995,8 +3995,11 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA,
377       Args.hasArg(options::OPT_dA))
378     CmdArgs.push_back("-masm-verbose");
379
380-  if (!TC.useIntegratedAs())
381+  if (!TC.useIntegratedAs()) {
382     CmdArgs.push_back("-no-integrated-as");
383+    CmdArgs.push_back("-mllvm");
384+    CmdArgs.push_back("-enable-call-graph-profile-sort=false");
385+  }
386
387   if (Args.hasArg(options::OPT_fdebug_pass_structure)) {
388     CmdArgs.push_back("-mdebug-pass");
389diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
390index c5fd68299eb..4c6e15eeeb9 100644
391--- a/llvm/lib/Passes/PassBuilder.cpp
392+++ b/llvm/lib/Passes/PassBuilder.cpp
393@@ -212,6 +212,10 @@ static cl::opt<bool>
394     EnableCHR("enable-chr-npm", cl::init(true), cl::Hidden,
395               cl::desc("Enable control height reduction optimization (CHR)"));
396
397+static cl::opt<bool> EnableCallGraphProfileSort(
398+    "enable-call-graph-profile-sort", cl::init(true), cl::Hidden,
399+    cl::desc("Enable call graph profile pass for the new PM (default = on)"));
400+
401 extern cl::opt<bool> EnableHotColdSplit;
402 extern cl::opt<bool> EnableOrderFileInstrumentation;
403
404@@ -939,7 +943,8 @@ ModulePassManager PassBuilder::buildModuleOptimizationPipeline(
405   // Add the core optimizing pipeline.
406   MPM.addPass(createModuleToFunctionPassAdaptor(std::move(OptimizePM)));
407
408-  MPM.addPass(CGProfilePass());
409+  if (EnableCallGraphProfileSort)
410+    MPM.addPass(CGProfilePass());
411
412   // Now we need to do some global optimization transforms.
413   // FIXME: It would seem like these should come first in the optimization
414"""
415
416if __name__ == "__main__":
417    unittest.main()
418