1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2019 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"""Unit tests when handling patches.""" 7*760c253cSXin Li 8*760c253cSXin Liimport json 9*760c253cSXin Lifrom pathlib import Path 10*760c253cSXin Liimport tempfile 11*760c253cSXin Lifrom typing import Callable 12*760c253cSXin Liimport unittest 13*760c253cSXin Lifrom unittest import mock 14*760c253cSXin Li 15*760c253cSXin Liimport atomic_write_file 16*760c253cSXin Liimport patch_manager 17*760c253cSXin Liimport patch_utils 18*760c253cSXin Li 19*760c253cSXin Li 20*760c253cSXin Liclass PatchManagerTest(unittest.TestCase): 21*760c253cSXin Li """Test class when handling patches of packages.""" 22*760c253cSXin Li 23*760c253cSXin Li # Simulate behavior of 'os.path.isdir()' when the path is not a directory. 24*760c253cSXin Li @mock.patch.object(Path, "is_dir", return_value=False) 25*760c253cSXin Li def testInvalidDirectoryPassedAsCommandLineArgument(self, mock_isdir): 26*760c253cSXin Li src_dir = "/some/path/that/is/not/a/directory" 27*760c253cSXin Li patch_metadata_file = "/some/path/that/is/not/a/file" 28*760c253cSXin Li 29*760c253cSXin Li # Verify the exception is raised when the command line argument for 30*760c253cSXin Li # '--filesdir_path' or '--src_path' is not a directory. 31*760c253cSXin Li with self.assertRaises(ValueError): 32*760c253cSXin Li patch_manager.main( 33*760c253cSXin Li [ 34*760c253cSXin Li "--src_path", 35*760c253cSXin Li src_dir, 36*760c253cSXin Li "--patch_metadata_file", 37*760c253cSXin Li patch_metadata_file, 38*760c253cSXin Li ] 39*760c253cSXin Li ) 40*760c253cSXin Li mock_isdir.assert_called_once() 41*760c253cSXin Li 42*760c253cSXin Li # Simulate behavior of 'os.path.isfile()' when the patch metadata file is 43*760c253cSXin Li # does not exist. 44*760c253cSXin Li @mock.patch.object(Path, "is_file", return_value=False) 45*760c253cSXin Li def testInvalidPathToPatchMetadataFilePassedAsCommandLineArgument( 46*760c253cSXin Li self, mock_isfile 47*760c253cSXin Li ): 48*760c253cSXin Li src_dir = "/some/path/that/is/not/a/directory" 49*760c253cSXin Li patch_metadata_file = "/some/path/that/is/not/a/file" 50*760c253cSXin Li 51*760c253cSXin Li # Verify the exception is raised when the command line argument for 52*760c253cSXin Li # '--filesdir_path' or '--src_path' is not a directory. 53*760c253cSXin Li with mock.patch.object(Path, "is_dir", return_value=True): 54*760c253cSXin Li with self.assertRaises(ValueError): 55*760c253cSXin Li patch_manager.main( 56*760c253cSXin Li [ 57*760c253cSXin Li "--src_path", 58*760c253cSXin Li src_dir, 59*760c253cSXin Li "--patch_metadata_file", 60*760c253cSXin Li patch_metadata_file, 61*760c253cSXin Li ] 62*760c253cSXin Li ) 63*760c253cSXin Li mock_isfile.assert_called_once() 64*760c253cSXin Li 65*760c253cSXin Li @mock.patch("builtins.print") 66*760c253cSXin Li @mock.patch.object(patch_utils, "git_clean_context") 67*760c253cSXin Li def testCheckPatchApplies(self, _, mock_git_clean_context): 68*760c253cSXin Li """Tests whether we can apply a single patch for a given svn_version.""" 69*760c253cSXin Li mock_git_clean_context.return_value = mock.MagicMock() 70*760c253cSXin Li with tempfile.TemporaryDirectory( 71*760c253cSXin Li prefix="patch_manager_unittest" 72*760c253cSXin Li ) as dirname: 73*760c253cSXin Li dirpath = Path(dirname) 74*760c253cSXin Li patch_entries = [ 75*760c253cSXin Li patch_utils.PatchEntry( 76*760c253cSXin Li dirpath, 77*760c253cSXin Li metadata=None, 78*760c253cSXin Li platforms=[], 79*760c253cSXin Li rel_patch_path="another.patch", 80*760c253cSXin Li version_range={ 81*760c253cSXin Li "from": 9, 82*760c253cSXin Li "until": 20, 83*760c253cSXin Li }, 84*760c253cSXin Li ), 85*760c253cSXin Li patch_utils.PatchEntry( 86*760c253cSXin Li dirpath, 87*760c253cSXin Li metadata=None, 88*760c253cSXin Li platforms=["chromiumos"], 89*760c253cSXin Li rel_patch_path="example.patch", 90*760c253cSXin Li version_range={ 91*760c253cSXin Li "from": 1, 92*760c253cSXin Li "until": 10, 93*760c253cSXin Li }, 94*760c253cSXin Li ), 95*760c253cSXin Li patch_utils.PatchEntry( 96*760c253cSXin Li dirpath, 97*760c253cSXin Li metadata=None, 98*760c253cSXin Li platforms=["chromiumos"], 99*760c253cSXin Li rel_patch_path="patch_after.patch", 100*760c253cSXin Li version_range={ 101*760c253cSXin Li "from": 1, 102*760c253cSXin Li "until": 5, 103*760c253cSXin Li }, 104*760c253cSXin Li ), 105*760c253cSXin Li ] 106*760c253cSXin Li patches_path = dirpath / "PATCHES.json" 107*760c253cSXin Li with atomic_write_file.atomic_write( 108*760c253cSXin Li patches_path, encoding="utf-8" 109*760c253cSXin Li ) as f: 110*760c253cSXin Li json.dump([pe.to_dict() for pe in patch_entries], f) 111*760c253cSXin Li 112*760c253cSXin Li def _harness1( 113*760c253cSXin Li version: int, 114*760c253cSXin Li return_value: patch_utils.PatchResult, 115*760c253cSXin Li expected: patch_manager.GitBisectionCode, 116*760c253cSXin Li ): 117*760c253cSXin Li with mock.patch.object( 118*760c253cSXin Li patch_utils.PatchEntry, 119*760c253cSXin Li "apply", 120*760c253cSXin Li return_value=return_value, 121*760c253cSXin Li ) as m: 122*760c253cSXin Li result = patch_manager.CheckPatchApplies( 123*760c253cSXin Li version, 124*760c253cSXin Li dirpath, 125*760c253cSXin Li patches_path, 126*760c253cSXin Li "example.patch", 127*760c253cSXin Li ) 128*760c253cSXin Li self.assertEqual(result, expected) 129*760c253cSXin Li m.assert_called() 130*760c253cSXin Li 131*760c253cSXin Li _harness1( 132*760c253cSXin Li 1, 133*760c253cSXin Li patch_utils.PatchResult(True, {}), 134*760c253cSXin Li patch_manager.GitBisectionCode.GOOD, 135*760c253cSXin Li ) 136*760c253cSXin Li _harness1( 137*760c253cSXin Li 2, 138*760c253cSXin Li patch_utils.PatchResult(True, {}), 139*760c253cSXin Li patch_manager.GitBisectionCode.GOOD, 140*760c253cSXin Li ) 141*760c253cSXin Li _harness1( 142*760c253cSXin Li 2, 143*760c253cSXin Li patch_utils.PatchResult(False, {}), 144*760c253cSXin Li patch_manager.GitBisectionCode.BAD, 145*760c253cSXin Li ) 146*760c253cSXin Li _harness1( 147*760c253cSXin Li 11, 148*760c253cSXin Li patch_utils.PatchResult(False, {}), 149*760c253cSXin Li patch_manager.GitBisectionCode.BAD, 150*760c253cSXin Li ) 151*760c253cSXin Li 152*760c253cSXin Li def _harness2( 153*760c253cSXin Li version: int, 154*760c253cSXin Li application_func: Callable, 155*760c253cSXin Li expected: patch_manager.GitBisectionCode, 156*760c253cSXin Li ): 157*760c253cSXin Li with mock.patch.object( 158*760c253cSXin Li patch_utils, 159*760c253cSXin Li "apply_single_patch_entry", 160*760c253cSXin Li application_func, 161*760c253cSXin Li ): 162*760c253cSXin Li result = patch_manager.CheckPatchApplies( 163*760c253cSXin Li version, 164*760c253cSXin Li dirpath, 165*760c253cSXin Li patches_path, 166*760c253cSXin Li "example.patch", 167*760c253cSXin Li ) 168*760c253cSXin Li self.assertEqual(result, expected) 169*760c253cSXin Li 170*760c253cSXin Li # Check patch can apply and fail with good return codes. 171*760c253cSXin Li def _apply_patch_entry_mock1(v, _, patch_entry, _func, **__): 172*760c253cSXin Li return patch_entry.can_patch_version(v), None 173*760c253cSXin Li 174*760c253cSXin Li _harness2( 175*760c253cSXin Li 1, 176*760c253cSXin Li _apply_patch_entry_mock1, 177*760c253cSXin Li patch_manager.GitBisectionCode.GOOD, 178*760c253cSXin Li ) 179*760c253cSXin Li _harness2( 180*760c253cSXin Li 11, 181*760c253cSXin Li _apply_patch_entry_mock1, 182*760c253cSXin Li patch_manager.GitBisectionCode.BAD, 183*760c253cSXin Li ) 184*760c253cSXin Li 185*760c253cSXin Li # Early exit check, shouldn't apply later failing patch. 186*760c253cSXin Li def _apply_patch_entry_mock2(v, _, patch_entry, _func, **__): 187*760c253cSXin Li if ( 188*760c253cSXin Li patch_entry.can_patch_version(v) 189*760c253cSXin Li and patch_entry.rel_patch_path == "patch_after.patch" 190*760c253cSXin Li ): 191*760c253cSXin Li return False, {"filename": mock.Mock()} 192*760c253cSXin Li return True, None 193*760c253cSXin Li 194*760c253cSXin Li _harness2( 195*760c253cSXin Li 1, 196*760c253cSXin Li _apply_patch_entry_mock2, 197*760c253cSXin Li patch_manager.GitBisectionCode.GOOD, 198*760c253cSXin Li ) 199*760c253cSXin Li 200*760c253cSXin Li # Skip check, should exit early on the first patch. 201*760c253cSXin Li def _apply_patch_entry_mock3(v, _, patch_entry, _func, **__): 202*760c253cSXin Li if ( 203*760c253cSXin Li patch_entry.can_patch_version(v) 204*760c253cSXin Li and patch_entry.rel_patch_path == "another.patch" 205*760c253cSXin Li ): 206*760c253cSXin Li return False, {"filename": mock.Mock()} 207*760c253cSXin Li return True, None 208*760c253cSXin Li 209*760c253cSXin Li _harness2( 210*760c253cSXin Li 9, 211*760c253cSXin Li _apply_patch_entry_mock3, 212*760c253cSXin Li patch_manager.GitBisectionCode.SKIP, 213*760c253cSXin Li ) 214*760c253cSXin Li 215*760c253cSXin Li 216*760c253cSXin Liif __name__ == "__main__": 217*760c253cSXin Li unittest.main() 218