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