aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/patch_utils_unittest.py
diff options
context:
space:
mode:
Diffstat (limited to 'llvm_tools/patch_utils_unittest.py')
-rwxr-xr-xllvm_tools/patch_utils_unittest.py381
1 files changed, 381 insertions, 0 deletions
diff --git a/llvm_tools/patch_utils_unittest.py b/llvm_tools/patch_utils_unittest.py
new file mode 100755
index 00000000..b8c21390
--- /dev/null
+++ b/llvm_tools/patch_utils_unittest.py
@@ -0,0 +1,381 @@
+#!/usr/bin/env python3
+# Copyright 2022 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 the patch_utils.py file."""
+
+import io
+import json
+from pathlib import Path
+import subprocess
+import tempfile
+from typing import Callable
+import unittest
+from unittest import mock
+
+import patch_utils as pu
+
+
+class TestPatchUtils(unittest.TestCase):
+ """Test the patch_utils."""
+
+ def test_atomic_write(self):
+ """Test that atomic write safely writes."""
+ prior_contents = "This is a test written by patch_utils_unittest.py\n"
+ new_contents = "I am a test written by patch_utils_unittest.py\n"
+ with tempfile.TemporaryDirectory(
+ prefix="patch_utils_unittest"
+ ) as dirname:
+ dirpath = Path(dirname)
+ filepath = dirpath / "test_atomic_write.txt"
+ with filepath.open("w", encoding="utf-8") as f:
+ f.write(prior_contents)
+
+ def _t():
+ with pu.atomic_write(filepath, encoding="utf-8") as f:
+ f.write(new_contents)
+ raise Exception("Expected failure")
+
+ self.assertRaises(Exception, _t)
+ with filepath.open(encoding="utf-8") as f:
+ lines = f.readlines()
+ self.assertEqual(lines[0], prior_contents)
+ with pu.atomic_write(filepath, encoding="utf-8") as f:
+ f.write(new_contents)
+ with filepath.open(encoding="utf-8") as f:
+ lines = f.readlines()
+ self.assertEqual(lines[0], new_contents)
+
+ def test_from_to_dict(self):
+ """Test to and from dict conversion."""
+ d = TestPatchUtils._default_json_dict()
+ d["metadata"] = {
+ "title": "hello world",
+ "info": [],
+ "other_extra_info": {
+ "extra_flags": [],
+ },
+ }
+ e = pu.PatchEntry.from_dict(TestPatchUtils._mock_dir(), d)
+ self.assertEqual(d, e.to_dict())
+
+ def test_patch_path(self):
+ """Test that we can get the full path from a PatchEntry."""
+ d = TestPatchUtils._default_json_dict()
+ with mock.patch.object(Path, "is_dir", return_value=True):
+ entry = pu.PatchEntry.from_dict(Path("/home/dir"), d)
+ self.assertEqual(
+ entry.patch_path(), Path("/home/dir") / d["rel_patch_path"]
+ )
+
+ def test_can_patch_version(self):
+ """Test that patch application based on version is correct."""
+ base_dict = TestPatchUtils._default_json_dict()
+ workdir = TestPatchUtils._mock_dir()
+ e1 = pu.PatchEntry.from_dict(workdir, base_dict)
+ self.assertFalse(e1.can_patch_version(3))
+ self.assertTrue(e1.can_patch_version(4))
+ self.assertTrue(e1.can_patch_version(5))
+ self.assertFalse(e1.can_patch_version(9))
+ base_dict["version_range"] = {"until": 9}
+ e2 = pu.PatchEntry.from_dict(workdir, base_dict)
+ self.assertTrue(e2.can_patch_version(0))
+ self.assertTrue(e2.can_patch_version(5))
+ self.assertFalse(e2.can_patch_version(9))
+ base_dict["version_range"] = {"from": 4}
+ e3 = pu.PatchEntry.from_dict(workdir, base_dict)
+ self.assertFalse(e3.can_patch_version(3))
+ self.assertTrue(e3.can_patch_version(5))
+ self.assertTrue(e3.can_patch_version(1 << 31))
+ base_dict["version_range"] = {"from": 4, "until": None}
+ e4 = pu.PatchEntry.from_dict(workdir, base_dict)
+ self.assertFalse(e4.can_patch_version(3))
+ self.assertTrue(e4.can_patch_version(5))
+ self.assertTrue(e4.can_patch_version(1 << 31))
+ base_dict["version_range"] = {"from": None, "until": 9}
+ e5 = pu.PatchEntry.from_dict(workdir, base_dict)
+ self.assertTrue(e5.can_patch_version(0))
+ self.assertTrue(e5.can_patch_version(5))
+ self.assertFalse(e5.can_patch_version(9))
+
+ def test_can_parse_from_json(self):
+ """Test that patches be loaded from json."""
+ patches_json = """
+[
+ {
+ "metadata": {},
+ "platforms": [],
+ "rel_patch_path": "cherry/nowhere.patch",
+ "version_range": {}
+ },
+ {
+ "metadata": {},
+ "rel_patch_path": "cherry/somewhere.patch",
+ "version_range": {}
+ },
+ {
+ "rel_patch_path": "where.patch",
+ "version_range": null
+ },
+ {
+ "rel_patch_path": "cherry/anywhere.patch"
+ }
+]
+ """
+ result = pu.json_to_patch_entries(Path(), io.StringIO(patches_json))
+ self.assertEqual(len(result), 4)
+
+ def test_parsed_hunks(self):
+ """Test that we can parse patch file hunks."""
+ m = mock.mock_open(read_data=_EXAMPLE_PATCH)
+
+ def mocked_open(self, *args, **kwargs):
+ return m(self, *args, **kwargs)
+
+ with mock.patch.object(Path, "open", mocked_open):
+ e = pu.PatchEntry.from_dict(
+ TestPatchUtils._mock_dir(), TestPatchUtils._default_json_dict()
+ )
+ hunk_dict = e.parsed_hunks()
+
+ m.assert_called()
+ filename1 = "clang/lib/Driver/ToolChains/Clang.cpp"
+ filename2 = "llvm/lib/Passes/PassBuilder.cpp"
+ self.assertEqual(set(hunk_dict.keys()), {filename1, filename2})
+ hunk_list1 = hunk_dict[filename1]
+ hunk_list2 = hunk_dict[filename2]
+ self.assertEqual(len(hunk_list1), 1)
+ self.assertEqual(len(hunk_list2), 2)
+
+ def test_apply_when_patch_nonexistent(self):
+ """Test that we error out when we try to apply a non-existent patch."""
+ src_dir = TestPatchUtils._mock_dir("somewhere/llvm-project")
+ patch_dir = TestPatchUtils._mock_dir()
+ e = pu.PatchEntry.from_dict(
+ patch_dir, TestPatchUtils._default_json_dict()
+ )
+ with mock.patch("subprocess.run", mock.MagicMock()):
+ self.assertRaises(RuntimeError, lambda: e.apply(src_dir))
+
+ def test_apply_success(self):
+ """Test that we can call apply."""
+ src_dir = TestPatchUtils._mock_dir("somewhere/llvm-project")
+ patch_dir = TestPatchUtils._mock_dir()
+ e = pu.PatchEntry.from_dict(
+ patch_dir, TestPatchUtils._default_json_dict()
+ )
+ with mock.patch("pathlib.Path.is_file", return_value=True):
+ with mock.patch("subprocess.run", mock.MagicMock()):
+ result = e.apply(src_dir)
+ self.assertTrue(result.succeeded)
+
+ def test_parse_failed_patch_output(self):
+ """Test that we can call parse `patch` output."""
+ fixture = """
+checking file a/b/c.cpp
+Hunk #1 SUCCEEDED at 96 with fuzz 1.
+Hunk #12 FAILED at 77.
+Hunk #42 FAILED at 1979.
+checking file x/y/z.h
+Hunk #4 FAILED at 30.
+checking file works.cpp
+Hunk #1 SUCCEEDED at 96 with fuzz 1.
+"""
+ result = pu.parse_failed_patch_output(fixture)
+ self.assertEqual(result["a/b/c.cpp"], [12, 42])
+ self.assertEqual(result["x/y/z.h"], [4])
+ self.assertNotIn("works.cpp", result)
+
+ def test_is_git_dirty(self):
+ """Test if a git directory has uncommitted changes."""
+ with tempfile.TemporaryDirectory(
+ prefix="patch_utils_unittest"
+ ) as dirname:
+ dirpath = Path(dirname)
+
+ def _run_h(cmd):
+ subprocess.run(
+ cmd,
+ cwd=dirpath,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ )
+
+ _run_h(["git", "init"])
+ self.assertFalse(pu.is_git_dirty(dirpath))
+ test_file = dirpath / "test_file"
+ test_file.touch()
+ self.assertTrue(pu.is_git_dirty(dirpath))
+ _run_h(["git", "add", "."])
+ _run_h(["git", "commit", "-m", "test"])
+ self.assertFalse(pu.is_git_dirty(dirpath))
+ test_file.touch()
+ self.assertFalse(pu.is_git_dirty(dirpath))
+ with test_file.open("w", encoding="utf-8"):
+ test_file.write_text("abc")
+ self.assertTrue(pu.is_git_dirty(dirpath))
+
+ @mock.patch("patch_utils.git_clean_context", mock.MagicMock)
+ def test_update_version_ranges(self):
+ """Test the UpdateVersionRanges function."""
+ with tempfile.TemporaryDirectory(
+ prefix="patch_manager_unittest"
+ ) as dirname:
+ dirpath = Path(dirname)
+ patches = [
+ pu.PatchEntry(
+ workdir=dirpath,
+ rel_patch_path="x.patch",
+ metadata=None,
+ platforms=None,
+ version_range={
+ "from": 0,
+ "until": 2,
+ },
+ ),
+ pu.PatchEntry(
+ workdir=dirpath,
+ rel_patch_path="y.patch",
+ metadata=None,
+ platforms=None,
+ version_range={
+ "from": 0,
+ "until": 2,
+ },
+ ),
+ ]
+ patches[0].apply = mock.MagicMock(
+ return_value=pu.PatchResult(
+ succeeded=False, failed_hunks={"a/b/c": []}
+ )
+ )
+ patches[1].apply = mock.MagicMock(
+ return_value=pu.PatchResult(succeeded=True)
+ )
+ results, _ = pu.update_version_ranges_with_entries(
+ 1, dirpath, patches
+ )
+ # We should only have updated the version_range of the first patch,
+ # as that one failed to apply.
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0].version_range, {"from": 0, "until": 1})
+ self.assertEqual(patches[0].version_range, {"from": 0, "until": 1})
+ self.assertEqual(patches[1].version_range, {"from": 0, "until": 2})
+
+ @mock.patch("builtins.print")
+ def test_remove_old_patches(self, _):
+ """Can remove old patches from PATCHES.json."""
+ one_patch_dict = {
+ "metadata": {
+ "title": "[some label] hello world",
+ },
+ "platforms": [
+ "chromiumos",
+ ],
+ "rel_patch_path": "x/y/z",
+ "version_range": {
+ "from": 4,
+ "until": 5,
+ },
+ }
+ patches = [
+ one_patch_dict,
+ {**one_patch_dict, "version_range": {"until": None}},
+ {**one_patch_dict, "version_range": {"from": 100}},
+ {**one_patch_dict, "version_range": {"until": 8}},
+ ]
+ cases = [
+ (0, lambda x: self.assertEqual(len(x), 4)),
+ (6, lambda x: self.assertEqual(len(x), 3)),
+ (8, lambda x: self.assertEqual(len(x), 2)),
+ (1000, lambda x: self.assertEqual(len(x), 2)),
+ ]
+
+ def _t(dirname: str, svn_version: int, assertion_f: Callable):
+ json_filepath = Path(dirname) / "PATCHES.json"
+ with json_filepath.open("w", encoding="utf-8") as f:
+ json.dump(patches, f)
+ pu.remove_old_patches(svn_version, Path(), json_filepath)
+ with json_filepath.open("r", encoding="utf-8") as f:
+ result = json.load(f)
+ assertion_f(result)
+
+ with tempfile.TemporaryDirectory(
+ prefix="patch_utils_unittest"
+ ) as dirname:
+ for r, a in cases:
+ _t(dirname, r, a)
+
+ @staticmethod
+ def _default_json_dict():
+ return {
+ "metadata": {
+ "title": "hello world",
+ },
+ "platforms": ["a"],
+ "rel_patch_path": "x/y/z",
+ "version_range": {
+ "from": 4,
+ "until": 9,
+ },
+ }
+
+ @staticmethod
+ def _mock_dir(path: str = "a/b/c"):
+ workdir = Path(path)
+ workdir = mock.MagicMock(workdir)
+ workdir.is_dir = lambda: True
+ workdir.joinpath = lambda x: Path(path).joinpath(x)
+ workdir.__truediv__ = lambda self, x: self.joinpath(x)
+ return workdir
+
+
+_EXAMPLE_PATCH = """
+diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
+index 5620a543438..099eb769ca5 100644
+--- a/clang/lib/Driver/ToolChains/Clang.cpp
++++ b/clang/lib/Driver/ToolChains/Clang.cpp
+@@ -3995,8 +3995,11 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA,
+ Args.hasArg(options::OPT_dA))
+ CmdArgs.push_back("-masm-verbose");
+
+- if (!TC.useIntegratedAs())
++ if (!TC.useIntegratedAs()) {
+ CmdArgs.push_back("-no-integrated-as");
++ CmdArgs.push_back("-mllvm");
++ CmdArgs.push_back("-enable-call-graph-profile-sort=false");
++ }
+
+ if (Args.hasArg(options::OPT_fdebug_pass_structure)) {
+ CmdArgs.push_back("-mdebug-pass");
+diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
+index c5fd68299eb..4c6e15eeeb9 100644
+--- a/llvm/lib/Passes/PassBuilder.cpp
++++ b/llvm/lib/Passes/PassBuilder.cpp
+@@ -212,6 +212,10 @@ static cl::opt<bool>
+ EnableCHR("enable-chr-npm", cl::init(true), cl::Hidden,
+ cl::desc("Enable control height reduction optimization (CHR)"));
+
++static cl::opt<bool> EnableCallGraphProfileSort(
++ "enable-call-graph-profile-sort", cl::init(true), cl::Hidden,
++ cl::desc("Enable call graph profile pass for the new PM (default = on)"));
++
+ extern cl::opt<bool> EnableHotColdSplit;
+ extern cl::opt<bool> EnableOrderFileInstrumentation;
+
+@@ -939,7 +943,8 @@ ModulePassManager PassBuilder::buildModuleOptimizationPipeline(
+ // Add the core optimizing pipeline.
+ MPM.addPass(createModuleToFunctionPassAdaptor(std::move(OptimizePM)));
+
+- MPM.addPass(CGProfilePass());
++ if (EnableCallGraphProfileSort)
++ MPM.addPass(CGProfilePass());
+
+ // Now we need to do some global optimization transforms.
+ // FIXME: It would seem like these should come first in the optimization
+"""
+
+if __name__ == "__main__":
+ unittest.main()