1#!/usr/bin/env python3 2# Copyright 2024 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"""Tests for get_patch.""" 7 8import json 9from pathlib import Path 10import tempfile 11from typing import Any, Dict, Generator, List, Set 12import unittest 13from unittest import mock 14 15import get_patch 16import git_llvm_rev 17 18 19COMMIT_FIXTURES: List[Dict[str, Any]] = [ 20 {"subject": "A commit subject", "sha": "abcdef1234567890", "rev": 5}, 21 {"subject": "Another commit subject", "sha": "feed9999", "rev": 9}, 22] 23 24JSON_FIXTURE: List[Dict[str, Any]] = [ 25 { 26 "metadata": {"title": "An existing patch"}, 27 "platforms": ["another platform"], 28 "rel_patch_path": "cherry/nowhere.patch", 29 "version_range": {"from": 1, "until": 256}, 30 }, 31] 32 33 34def _mock_get_commit_subj(_, sha: str) -> str: 35 gen: Generator[Dict[str, Any], None, None] = ( 36 fixture for fixture in COMMIT_FIXTURES if fixture["sha"] == sha 37 ) 38 return next(gen)["subject"] 39 40 41def _mock_to_rev(sha: get_patch.LLVMGitRef, _) -> git_llvm_rev.Rev: 42 gen: Generator[Dict[str, Any], None, None] = ( 43 fixture for fixture in COMMIT_FIXTURES if fixture["sha"] == sha.git_ref 44 ) 45 return git_llvm_rev.Rev("main", next(gen)["rev"]) 46 47 48def _mock_from_rev(_, rev: git_llvm_rev.Rev) -> get_patch.LLVMGitRef: 49 gen: Generator[Dict[str, Any], None, None] = ( 50 fixture for fixture in COMMIT_FIXTURES if fixture["rev"] == rev.number 51 ) 52 return get_patch.LLVMGitRef(next(gen)["sha"]) 53 54 55def _mock_git_format_patch(*_) -> str: 56 return "[category] This is a fake commit fixture" 57 58 59def _mock_write_patch(*_) -> None: 60 return 61 62 63def _mock_get_changed_packages(*_) -> Set[Path]: 64 return {get_patch.LLVM_PKG_PATH} 65 66 67class TestGetPatch(unittest.TestCase): 68 """Test case harness for get_patch.""" 69 70 def setUp(self) -> None: 71 """Set up the mocks and directory structure.""" 72 73 self.module_patcher = mock.patch.multiple( 74 "get_patch", 75 get_commit_subj=_mock_get_commit_subj, 76 git_format_patch=_mock_git_format_patch, 77 get_changed_packages=_mock_get_changed_packages, 78 _write_patch=_mock_write_patch, 79 ) 80 self.module_patcher.start() 81 self.addCleanup(self.module_patcher.stop) 82 self.llvm_gitsha_patcher = mock.patch.multiple( 83 "get_patch.LLVMGitRef", 84 to_rev=_mock_to_rev, 85 from_rev=_mock_from_rev, 86 ) 87 self.llvm_gitsha_patcher.start() 88 self.addCleanup(self.llvm_gitsha_patcher.stop) 89 90 self.llvm_project_dir = Path(tempfile.mkdtemp()) 91 self.addCleanup(self.llvm_project_dir.rmdir) 92 self.chromiumos_root = Path(tempfile.mkdtemp()) 93 self.addCleanup(self.chromiumos_root.rmdir) 94 self.workdir = self.chromiumos_root / get_patch.LLVM_PKG_PATH / "files" 95 self.workdir.mkdir(parents=True, exist_ok=True) 96 97 def _cleanup_workdir(): 98 # We individually clean up these directories as a guarantee 99 # we aren't creating any extraneous files. We don't want to 100 # use shm.rmtree here because we don't want clean up any 101 # files unaccounted for. 102 workdir_recurse = self.workdir 103 while workdir_recurse not in (self.chromiumos_root, Path.root): 104 workdir_recurse.rmdir() 105 workdir_recurse = workdir_recurse.parent 106 107 self.addCleanup(_cleanup_workdir) 108 109 self.patches_json_file = ( 110 self.workdir / get_patch.PATCH_METADATA_FILENAME 111 ) 112 start_ref = get_patch.LLVMGitRef("abcdef1234567890") 113 self.ctx = get_patch.PatchContext( 114 self.llvm_project_dir, 115 self.chromiumos_root, 116 start_ref, 117 platforms=["some platform"], 118 ) 119 120 def write_json_fixture(self) -> None: 121 with self.patches_json_file.open("w", encoding="utf-8") as f: 122 json.dump(JSON_FIXTURE, f) 123 f.write("\n") 124 125 def test_bad_cherrypick_version(self) -> None: 126 """Test that bad cherrypick versions raises.""" 127 start_sha_fixture = COMMIT_FIXTURES[0] 128 129 def _try_make_patches(): 130 # This fixture is the same as the start_sha. 131 self.ctx.make_patches( 132 get_patch.LLVMGitRef(start_sha_fixture["sha"]) 133 ) 134 135 self.assertRaises(get_patch.CherrypickVersionError, _try_make_patches) 136 137 def test_make_patches(self) -> None: 138 """Test we can make patch entries from a git commit.""" 139 140 fixture = COMMIT_FIXTURES[1] 141 # We manually write and delete this file because it must have the name 142 # as specified by get_patch. tempfile cannot guarantee us this name. 143 self.write_json_fixture() 144 try: 145 entries = self.ctx.make_patches( 146 get_patch.LLVMGitRef(fixture["sha"]) 147 ) 148 self.assertEqual(len(entries), 1) 149 if entries[0].metadata: 150 self.assertEqual( 151 entries[0].metadata["title"], fixture["subject"] 152 ) 153 else: 154 self.fail("metadata was None") 155 finally: 156 self.patches_json_file.unlink() 157 158 def test_apply_patch_to_json(self) -> None: 159 """Test we can apply patches to the JSON file.""" 160 161 fixture = COMMIT_FIXTURES[1] 162 fixture_sha = fixture["sha"] 163 expected_json_entry = { 164 "metadata": {"title": fixture["subject"], "info": []}, 165 "platforms": ["some platform"], 166 "rel_patch_path": f"cherry/{fixture_sha}.patch", 167 "version_range": { 168 "from": self.ctx.start_ref.to_rev(self.llvm_project_dir).number, 169 "until": fixture["rev"], 170 }, 171 } 172 cherrydir = self.workdir / "cherry" 173 cherrydir.mkdir() 174 self._apply_patch_to_json_helper(fixture, expected_json_entry) 175 cherrydir.rmdir() 176 177 def test_apply_patch_to_json_no_cherry(self) -> None: 178 """Test we can apply patches to the JSON file, without a cherry dir.""" 179 180 fixture = COMMIT_FIXTURES[1] 181 fixture_sha = fixture["sha"] 182 expected_json_entry = { 183 "metadata": {"title": fixture["subject"], "info": []}, 184 "platforms": ["some platform"], 185 "rel_patch_path": f"{fixture_sha}.patch", 186 "version_range": { 187 "from": self.ctx.start_ref.to_rev(self.llvm_project_dir).number, 188 "until": fixture["rev"], 189 }, 190 } 191 self._apply_patch_to_json_helper(fixture, expected_json_entry) 192 193 def _apply_patch_to_json_helper(self, fixture, expected_json_entry) -> None: 194 # We manually write and delete this file because it must have the name 195 # as specified by get_patch. tempfile cannot guarantee us this name. 196 self.write_json_fixture() 197 patch_source = get_patch.LLVMGitRef.from_rev( 198 self.llvm_project_dir, 199 git_llvm_rev.Rev("origin", fixture["rev"]), 200 ) 201 try: 202 self.ctx.apply_patches(patch_source) 203 with self.patches_json_file.open(encoding="utf-8") as f: 204 edited = json.load(f) 205 self.assertEqual(edited, JSON_FIXTURE + [expected_json_entry]) 206 finally: 207 self.patches_json_file.unlink() 208 209 def test_apply_patch_dry_run(self) -> None: 210 """Test dry running patches does nothing.""" 211 212 fixture = COMMIT_FIXTURES[1] 213 old_dry_run = self.ctx.dry_run 214 self.ctx.dry_run = True 215 # We manually write and delete this file because it must have the name 216 # as specified by get_patch. tempfile cannot guarantee us this name. 217 self.write_json_fixture() 218 patch_source = get_patch.LLVMGitRef.from_rev( 219 self.llvm_project_dir, 220 git_llvm_rev.Rev("origin", fixture["rev"]), 221 ) 222 try: 223 self.ctx.apply_patches(patch_source) 224 with self.patches_json_file.open(encoding="utf-8") as f: 225 maybe_edited = json.load(f) 226 self.assertEqual(maybe_edited, JSON_FIXTURE) 227 finally: 228 self.ctx.dry_run = old_dry_run 229 self.patches_json_file.unlink() 230 231 232if __name__ == "__main__": 233 unittest.main() 234