diff options
Diffstat (limited to 'llvm_tools/patch_utils_unittest.py')
-rwxr-xr-x | llvm_tools/patch_utils_unittest.py | 381 |
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() |