| #!/usr/bin/env python3 |
| # Copyright 2019 The ChromiumOS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Unit tests when handling patches.""" |
| |
| import json |
| from pathlib import Path |
| import tempfile |
| from typing import Callable |
| import unittest |
| from unittest import mock |
| |
| import patch_manager |
| import patch_utils |
| |
| |
| class PatchManagerTest(unittest.TestCase): |
| """Test class when handling patches of packages.""" |
| |
| # Simulate behavior of 'os.path.isdir()' when the path is not a directory. |
| @mock.patch.object(Path, 'is_dir', return_value=False) |
| def testInvalidDirectoryPassedAsCommandLineArgument(self, mock_isdir): |
| src_dir = '/some/path/that/is/not/a/directory' |
| patch_metadata_file = '/some/path/that/is/not/a/file' |
| |
| # Verify the exception is raised when the command line argument for |
| # '--filesdir_path' or '--src_path' is not a directory. |
| with self.assertRaises(ValueError): |
| patch_manager.main([ |
| '--src_path', src_dir, '--patch_metadata_file', patch_metadata_file |
| ]) |
| mock_isdir.assert_called_once() |
| |
| # Simulate behavior of 'os.path.isfile()' when the patch metadata file is does |
| # not exist. |
| @mock.patch.object(Path, 'is_file', return_value=False) |
| def testInvalidPathToPatchMetadataFilePassedAsCommandLineArgument( |
| self, mock_isfile): |
| src_dir = '/some/path/that/is/not/a/directory' |
| patch_metadata_file = '/some/path/that/is/not/a/file' |
| |
| # Verify the exception is raised when the command line argument for |
| # '--filesdir_path' or '--src_path' is not a directory. |
| with mock.patch.object(Path, 'is_dir', return_value=True): |
| with self.assertRaises(ValueError): |
| patch_manager.main([ |
| '--src_path', src_dir, '--patch_metadata_file', patch_metadata_file |
| ]) |
| mock_isfile.assert_called_once() |
| |
| @mock.patch('builtins.print') |
| def testRemoveOldPatches(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) |
| patch_manager.RemoveOldPatches(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_manager_unittest') as dirname: |
| for r, a in cases: |
| _t(dirname, r, a) |
| |
| @mock.patch('builtins.print') |
| @mock.patch.object(patch_utils, 'git_clean_context') |
| def testCheckPatchApplies(self, _, mock_git_clean_context): |
| """Tests whether we can apply a single patch for a given svn_version.""" |
| mock_git_clean_context.return_value = mock.MagicMock() |
| with tempfile.TemporaryDirectory( |
| prefix='patch_manager_unittest') as dirname: |
| dirpath = Path(dirname) |
| patch_entries = [ |
| patch_utils.PatchEntry(dirpath, |
| metadata=None, |
| platforms=[], |
| rel_patch_path='another.patch', |
| version_range={ |
| 'from': 9, |
| 'until': 20, |
| }), |
| patch_utils.PatchEntry(dirpath, |
| metadata=None, |
| platforms=['chromiumos'], |
| rel_patch_path='example.patch', |
| version_range={ |
| 'from': 1, |
| 'until': 10, |
| }), |
| patch_utils.PatchEntry(dirpath, |
| metadata=None, |
| platforms=['chromiumos'], |
| rel_patch_path='patch_after.patch', |
| version_range={ |
| 'from': 1, |
| 'until': 5, |
| }) |
| ] |
| patches_path = dirpath / 'PATCHES.json' |
| with patch_utils.atomic_write(patches_path, encoding='utf-8') as f: |
| json.dump([pe.to_dict() for pe in patch_entries], f) |
| |
| def _harness1(version: int, return_value: patch_utils.PatchResult, |
| expected: patch_manager.GitBisectionCode): |
| with mock.patch.object( |
| patch_utils.PatchEntry, |
| 'apply', |
| return_value=return_value, |
| ) as m: |
| result = patch_manager.CheckPatchApplies( |
| version, |
| dirpath, |
| patches_path, |
| 'example.patch', |
| ) |
| self.assertEqual(result, expected) |
| m.assert_called() |
| |
| _harness1(1, patch_utils.PatchResult(True, {}), |
| patch_manager.GitBisectionCode.GOOD) |
| _harness1(2, patch_utils.PatchResult(True, {}), |
| patch_manager.GitBisectionCode.GOOD) |
| _harness1(2, patch_utils.PatchResult(False, {}), |
| patch_manager.GitBisectionCode.BAD) |
| _harness1(11, patch_utils.PatchResult(False, {}), |
| patch_manager.GitBisectionCode.BAD) |
| |
| def _harness2(version: int, application_func: Callable, |
| expected: patch_manager.GitBisectionCode): |
| with mock.patch.object( |
| patch_utils, |
| 'apply_single_patch_entry', |
| application_func, |
| ): |
| result = patch_manager.CheckPatchApplies( |
| version, |
| dirpath, |
| patches_path, |
| 'example.patch', |
| ) |
| self.assertEqual(result, expected) |
| |
| # Check patch can apply and fail with good return codes. |
| def _apply_patch_entry_mock1(v, _, patch_entry, **__): |
| return patch_entry.can_patch_version(v), None |
| |
| _harness2( |
| 1, |
| _apply_patch_entry_mock1, |
| patch_manager.GitBisectionCode.GOOD, |
| ) |
| _harness2( |
| 11, |
| _apply_patch_entry_mock1, |
| patch_manager.GitBisectionCode.BAD, |
| ) |
| |
| # Early exit check, shouldn't apply later failing patch. |
| def _apply_patch_entry_mock2(v, _, patch_entry, **__): |
| if (patch_entry.can_patch_version(v) |
| and patch_entry.rel_patch_path == 'patch_after.patch'): |
| return False, {'filename': mock.Mock()} |
| return True, None |
| |
| _harness2( |
| 1, |
| _apply_patch_entry_mock2, |
| patch_manager.GitBisectionCode.GOOD, |
| ) |
| |
| # Skip check, should exit early on the first patch. |
| def _apply_patch_entry_mock3(v, _, patch_entry, **__): |
| if (patch_entry.can_patch_version(v) |
| and patch_entry.rel_patch_path == 'another.patch'): |
| return False, {'filename': mock.Mock()} |
| return True, None |
| |
| _harness2( |
| 9, |
| _apply_patch_entry_mock3, |
| patch_manager.GitBisectionCode.SKIP, |
| ) |
| |
| @mock.patch('patch_utils.git_clean_context', mock.MagicMock) |
| def testUpdateVersionRanges(self): |
| """Test the UpdateVersionRanges function.""" |
| with tempfile.TemporaryDirectory( |
| prefix='patch_manager_unittest') as dirname: |
| dirpath = Path(dirname) |
| patches = [ |
| patch_utils.PatchEntry(workdir=dirpath, |
| rel_patch_path='x.patch', |
| metadata=None, |
| platforms=None, |
| version_range={ |
| 'from': 0, |
| 'until': 2, |
| }), |
| patch_utils.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=patch_utils.PatchResult( |
| succeeded=False, failed_hunks={'a/b/c': []})) |
| patches[1].apply = mock.MagicMock(return_value=patch_utils.PatchResult( |
| succeeded=True)) |
| results = patch_manager.UpdateVersionRangesWithEntries( |
| 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}) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |