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