#!/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 EnableCHR("enable-chr-npm", cl::init(true), cl::Hidden, cl::desc("Enable control height reduction optimization (CHR)")); +static cl::opt 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 EnableHotColdSplit; extern cl::opt 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()