diff options
Diffstat (limited to 'llvm_tools/patch_manager_unittest.py')
-rwxr-xr-x | llvm_tools/patch_manager_unittest.py | 1141 |
1 files changed, 200 insertions, 941 deletions
diff --git a/llvm_tools/patch_manager_unittest.py b/llvm_tools/patch_manager_unittest.py index 69bb683e..42697d91 100755 --- a/llvm_tools/patch_manager_unittest.py +++ b/llvm_tools/patch_manager_unittest.py @@ -1,955 +1,214 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright 2019 The Chromium OS Authors. All rights reserved. +# Copyright 2019 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 when handling patches.""" -from __future__ import print_function - import json -import os -import subprocess +from pathlib import Path +import tempfile +from typing import Callable import unittest -import unittest.mock as mock +from unittest import mock import patch_manager -from failure_modes import FailureModes -from test_helpers import CallCountsToMockFunctions -from test_helpers import CreateTemporaryJsonFile -from test_helpers import WritePrettyJsonFile +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(os.path, 'isdir', return_value=False) - def testInvalidDirectoryPassedAsCommandLineArgument(self, mock_isdir): - test_dir = '/some/path/that/is/not/a/directory' - - # Verify the exception is raised when the command line argument for - # '--filesdir_path' or '--src_path' is not a directory. - with self.assertRaises(ValueError) as err: - patch_manager.is_directory(test_dir) - - self.assertEqual( - str(err.exception), 'Path is not a directory: ' - '%s' % test_dir) - - mock_isdir.assert_called_once() - - # Simulate the behavior of 'os.path.isdir()' when a path to a directory is - # passed as the command line argument for '--filesdir_path' or '--src_path'. - @mock.patch.object(os.path, 'isdir', return_value=True) - def testValidDirectoryPassedAsCommandLineArgument(self, mock_isdir): - test_dir = '/some/path/that/is/a/directory' - - self.assertEqual(patch_manager.is_directory(test_dir), test_dir) - - mock_isdir.assert_called_once() - - # Simulate behavior of 'os.path.isfile()' when the patch metadata file is does - # not exist. - @mock.patch.object(os.path, 'isfile', return_value=False) - def testInvalidPathToPatchMetadataFilePassedAsCommandLineArgument( - self, mock_isfile): - - abs_path_to_patch_file = '/abs/path/to/PATCHES.json' - - # Verify the exception is raised when the command line argument for - # '--patch_metadata_file' does not exist or is not a file. - with self.assertRaises(ValueError) as err: - patch_manager.is_patch_metadata_file(abs_path_to_patch_file) - - self.assertEqual( - str(err.exception), 'Invalid patch metadata file provided: ' - '%s' % abs_path_to_patch_file) - - mock_isfile.assert_called_once() - - # Simulate the behavior of 'os.path.isfile()' when the path to the patch - # metadata file exists and is a file. - @mock.patch.object(os.path, 'isfile', return_value=True) - def testPatchMetadataFileDoesNotEndInJson(self, mock_isfile): - abs_path_to_patch_file = '/abs/path/to/PATCHES' - - # Verify the exception is raises when the command line argument for - # '--patch_metadata_file' exists and is a file but does not end in - # '.json'. - with self.assertRaises(ValueError) as err: - patch_manager.is_patch_metadata_file(abs_path_to_patch_file) - - self.assertEqual( - str(err.exception), 'Patch metadata file does not end in ".json": ' - '%s' % abs_path_to_patch_file) - - mock_isfile.assert_called_once() - - # Simulate the behavior of 'os.path.isfile()' when the command line argument - # for '--patch_metadata_file' exists and is a file. - @mock.patch.object(os.path, 'isfile', return_value=True) - def testValidPatchMetadataFilePassedAsCommandLineArgument(self, mock_isfile): - abs_path_to_patch_file = '/abs/path/to/PATCHES.json' - - self.assertEqual( - patch_manager.is_patch_metadata_file(abs_path_to_patch_file), - '%s' % abs_path_to_patch_file) - - mock_isfile.assert_called_once() - - # Simulate behavior of 'os.path.isdir()' when the path to $FILESDIR - # does not exist. - @mock.patch.object(os.path, 'isdir', return_value=False) - def testInvalidPathToFilesDirWhenConstructingPathToPatch(self, mock_isdir): - abs_path_to_filesdir = '/abs/path/to/filesdir' - - rel_patch_path = 'cherry/fixes_stdout.patch' - - # Verify the exception is raised when the the absolute path to $FILESDIR of - # a package is not a directory. - with self.assertRaises(ValueError) as err: - patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path) - - self.assertEqual( - str(err.exception), 'Invalid path to $FILESDIR provided: ' - '%s' % abs_path_to_filesdir) - - mock_isdir.assert_called_once() - - # Simulate behavior of 'os.path.isdir()' when the absolute path to the - # $FILESDIR of a package exists and is a directory. - @mock.patch.object(os.path, 'isdir', return_value=True) - # Simulate the behavior of 'os.path.isfile()' when the absolute path to the - # patch does not exist. - @mock.patch.object(os.path, 'isfile', return_value=False) - def testConstructedPathToPatchDoesNotExist(self, mock_isfile, mock_isdir): - abs_path_to_filesdir = '/abs/path/to/filesdir' - - rel_patch_path = 'cherry/fixes_stdout.patch' - - abs_patch_path = os.path.join(abs_path_to_filesdir, rel_patch_path) - - # Verify the exception is raised when the absolute path to the patch does + """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. - with self.assertRaises(ValueError) as err: - patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path) - - self.assertEqual( - str(err.exception), 'The absolute path %s to the patch %s does not ' - 'exist' % (abs_patch_path, rel_patch_path)) - - mock_isdir.assert_called_once() - - mock_isfile.assert_called_once() - - # Simulate behavior of 'os.path.isdir()' when the absolute path to the - # $FILESDIR of a package exists and is a directory. - @mock.patch.object(os.path, 'isdir', return_value=True) - # Simulate behavior of 'os.path.isfile()' when the absolute path to the - # patch exists and is a file. - @mock.patch.object(os.path, 'isfile', return_value=True) - def testConstructedPathToPatchSuccessfully(self, mock_isfile, mock_isdir): - abs_path_to_filesdir = '/abs/path/to/filesdir' - - rel_patch_path = 'cherry/fixes_stdout.patch' - - abs_patch_path = os.path.join(abs_path_to_filesdir, rel_patch_path) - - self.assertEqual( - patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path), - abs_patch_path) - - mock_isdir.assert_called_once() - - mock_isfile.assert_called_once() - - def testSuccessfullyGetPatchMetadataForPatchWithNoMetadata(self): - expected_patch_metadata = 0, None, False - - test_patch = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_stdout.patch' - } - - self.assertEqual( - patch_manager.GetPatchMetadata(test_patch), expected_patch_metadata) - - def testSuccessfullyGetPatchMetdataForPatchWithSomeMetadata(self): - expected_patch_metadata = 0, 1000, False - - test_patch = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_stdout.patch', - 'version_range': { - 'until': 1000, - } - } - - self.assertEqual( - patch_manager.GetPatchMetadata(test_patch), expected_patch_metadata) - - def testFailedToApplyPatchWhenInvalidSrcPathIsPassedIn(self): - src_path = '/abs/path/to/src' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' - - # Verify the exception is raised when the absolute path to the unpacked - # sources of a package is not a directory. - with self.assertRaises(ValueError) as err: - patch_manager.ApplyPatch(src_path, abs_patch_path) - - self.assertEqual( - str(err.exception), 'Invalid src path provided: %s' % src_path) - - # Simulate behavior of 'os.path.isdir()' when the absolute path to the - # unpacked sources of the package is valid and exists. - @mock.patch.object(os.path, 'isdir', return_value=True) - def testFailedToApplyPatchWhenPatchPathIsInvalid(self, mock_isdir): - src_path = '/abs/path/to/src' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' - - # Verify the exception is raised when the absolute path to the patch does - # not exist or is not a file. - with self.assertRaises(ValueError) as err: - patch_manager.ApplyPatch(src_path, abs_patch_path) - - self.assertEqual( - str(err.exception), 'Invalid patch file provided: ' - '%s' % abs_patch_path) - - mock_isdir.assert_called_once() - - # Simulate behavior of 'os.path.isdir()' when the absolute path to the - # unpacked sources of the package is valid and exists. - @mock.patch.object(os.path, 'isdir', return_value=True) - @mock.patch.object(os.path, 'isfile', return_value=True) - # Simulate behavior of 'os.path.isfile()' when the absolute path to the - # patch exists and is a file. - @mock.patch.object(patch_manager, 'check_output') - def testFailedToApplyPatchInDryRun(self, mock_dry_run, mock_isfile, - mock_isdir): - - # Simulate behavior of 'subprocess.check_output()' when '--dry-run' - # fails on the applying patch. - def FailedToApplyPatch(test_patch_cmd): - # First argument is the return error code, the second argument is the - # command that was run, and the third argument is the output. - raise subprocess.CalledProcessError(1, test_patch_cmd, None) - - mock_dry_run.side_effect = FailedToApplyPatch - - src_path = '/abs/path/to/src' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' - - self.assertEqual(patch_manager.ApplyPatch(src_path, abs_patch_path), False) - - mock_isdir.assert_called_once() - - mock_isfile.assert_called_once() - - mock_dry_run.assert_called_once() - - # Simulate behavior of 'os.path.isdir()' when the absolute path to the - # unpacked sources of the package is valid and exists. - @mock.patch.object(os.path, 'isdir', return_value=True) - @mock.patch.object(os.path, 'isfile', return_value=True) - # Simulate behavior of 'os.path.isfile()' when the absolute path to the - # patch exists and is a file. - @mock.patch.object(patch_manager, 'check_output') - def testSuccessfullyAppliedPatch(self, mock_dry_run, mock_isfile, mock_isdir): - src_path = '/abs/path/to/src' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' - - self.assertEqual(patch_manager.ApplyPatch(src_path, abs_patch_path), True) - - mock_isdir.assert_called_once() - - mock_isfile.assert_called_once() - - self.assertEqual(mock_dry_run.call_count, 2) - - def testFailedToUpdatePatchMetadataFileWhenPatchFileNotEndInJson(self): - patch = [{ - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 10, - }, - }] - - abs_patch_path = '/abs/path/to/filesdir/PATCHES' - - # Verify the exception is raised when the absolute path to the patch - # metadata file does not end in '.json'. - with self.assertRaises(ValueError) as err: - patch_manager.UpdatePatchMetadataFile(abs_patch_path, patch) - - self.assertEqual( - str(err.exception), 'File does not end in ".json": ' - '%s' % abs_patch_path) - - def testSuccessfullyUpdatedPatchMetadataFile(self): - test_updated_patch_metadata = [{ - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 10, - } - }] - - expected_patch_metadata = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 10, - } - } - - with CreateTemporaryJsonFile() as json_test_file: - patch_manager.UpdatePatchMetadataFile(json_test_file, - test_updated_patch_metadata) - - # Make sure the updated patch metadata was written into the temporary - # .json file. - with open(json_test_file) as patch_file: - patch_contents = json.load(patch_file) - - self.assertEqual(len(patch_contents), 1) - - self.assertDictEqual(patch_contents[0], expected_patch_metadata) - - @mock.patch.object(patch_manager, 'GetPathToPatch') - def testExceptionThrownWhenHandlingPatches(self, mock_get_path_to_patch): - filesdir_path = '/abs/path/to/filesdir' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_output.patch' - - rel_patch_path = 'cherry/fixes_output.patch' - - # Simulate behavior of 'GetPathToPatch()' when the absolute path to the - # patch does not exist. - def PathToPatchDoesNotExist(filesdir_path, rel_patch_path): - raise ValueError('The absolute path to %s does not exist' % os.path.join( - filesdir_path, rel_patch_path)) - - # Use the test function to simulate the behavior of 'GetPathToPatch()'. - mock_get_path_to_patch.side_effect = PathToPatchDoesNotExist - - test_patch_metadata = [{ - 'comment': 'Redirects output to stdout', - 'rel_patch_path': rel_patch_path, - 'version_range': { - 'from': 10, - } - }] - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - src_path = '/some/path/to/src' - - revision = 1000 - - # Verify the exception is raised when the absolute path to a patch does - # not exist. - with self.assertRaises(ValueError) as err: - patch_manager.HandlePatches(revision, json_test_file, filesdir_path, - src_path, FailureModes.FAIL) - - self.assertEqual( - str(err.exception), - 'The absolute path to %s does not exist' % abs_patch_path) - - mock_get_path_to_patch.assert_called_once_with(filesdir_path, - rel_patch_path) - - @mock.patch.object(patch_manager, 'GetPathToPatch') - # Simulate behavior for 'ApplyPatch()' when an applicable patch failed to - # apply. - @mock.patch.object(patch_manager, 'ApplyPatch', return_value=False) - def testExceptionThrownOnAFailedPatchInFailMode(self, mock_apply_patch, - mock_get_path_to_patch): - filesdir_path = '/abs/path/to/filesdir' - - abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_output.patch' - - rel_patch_path = 'cherry/fixes_output.patch' - - # Simulate behavior for 'GetPathToPatch()' when successfully constructed the - # absolute path to the patch and the patch exists. - mock_get_path_to_patch.return_value = abs_patch_path - - test_patch_metadata = [{ - 'comment': 'Redirects output to stdout', - 'rel_patch_path': rel_patch_path, - 'version_range': { - 'from': 1000, - }, - }] - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - src_path = '/some/path/to/src' - - revision = 1000 - - patch_name = 'fixes_output.patch' - - # Verify the exception is raised when the mode is 'fail' and an applicable - # patch fails to apply. - with self.assertRaises(ValueError) as err: - patch_manager.HandlePatches(revision, json_test_file, filesdir_path, - src_path, FailureModes.FAIL) - - self.assertEqual( - str(err.exception), 'Failed to apply patch: %s' % patch_name) - - mock_get_path_to_patch.assert_called_once_with(filesdir_path, - rel_patch_path) - - mock_apply_patch.assert_called_once_with(src_path, abs_patch_path) - - @mock.patch.object(patch_manager, 'GetPathToPatch') - @mock.patch.object(patch_manager, 'ApplyPatch') - def testSomePatchesFailedToApplyInContinueMode(self, mock_apply_patch, - mock_get_path_to_patch): - - test_patch_1 = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 1000, - 'until': 1250 - } - } - - test_patch_2 = { - 'comment': 'Fixes input', - 'rel_patch_path': 'cherry/fixes_input.patch', - 'version_range': { - 'from': 1000 - } - } - - test_patch_3 = { - 'comment': 'Adds a warning', - 'rel_patch_path': 'add_warning.patch', - 'version_range': { - 'from': 750, - 'until': 1500 - } - } - - test_patch_4 = { - 'comment': 'Adds a helper function', - 'rel_patch_path': 'add_helper.patch', - 'version_range': { - 'from': 20, - 'until': 900 - } - } - - test_patch_metadata = [ - test_patch_1, test_patch_2, test_patch_3, test_patch_4 - ] - - abs_path_to_filesdir = '/abs/path/to/filesdir' - - # Simulate behavior for 'GetPathToPatch()' when successfully constructed the - # absolute path to the patch and the patch exists. - @CallCountsToMockFunctions - def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): - self.assertEqual(filesdir_path, abs_path_to_filesdir) - - if call_count < 4: - self.assertEqual(rel_patch_path, - test_patch_metadata[call_count]['rel_patch_path']) - - return os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count]['rel_patch_path']) - - assert False, 'Unexpectedly called more than 4 times.' - - # Simulate behavior for 'ApplyPatch()' when applying multiple applicable - # patches. - @CallCountsToMockFunctions - def MultipleCallsToApplyPatches(call_count, _src_path, path_to_patch): - if call_count < 3: - self.assertEqual( - path_to_patch, - os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count]['rel_patch_path'])) - - # Simulate that the first patch successfully applied. - return call_count == 0 - - # 'ApplyPatch()' was called more times than expected (3 times). - assert False, 'Unexpectedly called more than 3 times.' - - # Use test functions to simulate behavior. - mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath - mock_apply_patch.side_effect = MultipleCallsToApplyPatches - - expected_applied_patches = ['fixes_output.patch'] - expected_failed_patches = ['fixes_input.patch', 'add_warning.patch'] - expected_non_applicable_patches = ['add_helper.patch'] - - expected_patch_info_dict = { - 'applied_patches': expected_applied_patches, - 'failed_patches': expected_failed_patches, - 'non_applicable_patches': expected_non_applicable_patches, - 'disabled_patches': [], - 'removed_patches': [], - 'modified_metadata': None - } - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - src_path = '/some/path/to/src/' - - revision = 1000 - - patch_info = patch_manager.HandlePatches(revision, json_test_file, - abs_path_to_filesdir, src_path, - FailureModes.CONTINUE) - - self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) - - self.assertEqual(mock_get_path_to_patch.call_count, 4) - - self.assertEqual(mock_apply_patch.call_count, 3) - - @mock.patch.object(patch_manager, 'GetPathToPatch') - @mock.patch.object(patch_manager, 'ApplyPatch') - def testSomePatchesAreDisabled(self, mock_apply_patch, - mock_get_path_to_patch): - - test_patch_1 = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 1000, - 'until': 1190 - } - } - - test_patch_2 = { - 'comment': 'Fixes input', - 'rel_patch_path': 'cherry/fixes_input.patch', - 'version_range': { - 'from': 1000 - } - } - - test_patch_3 = { - 'comment': 'Adds a warning', - 'rel_patch_path': 'add_warning.patch', - 'version_range': { - 'from': 750, - 'until': 1500 - } - } - - test_patch_4 = { - 'comment': 'Adds a helper function', - 'rel_patch_path': 'add_helper.patch', - 'version_range': { - 'from': 20, - 'until': 2000 - } - } - - test_patch_metadata = [ - test_patch_1, test_patch_2, test_patch_3, test_patch_4 - ] - - abs_path_to_filesdir = '/abs/path/to/filesdir' - - # Simulate behavior for 'GetPathToPatch()' when successfully constructed the - # absolute path to the patch and the patch exists. - @CallCountsToMockFunctions - def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): - self.assertEqual(filesdir_path, abs_path_to_filesdir) - - if call_count < 4: - self.assertEqual(rel_patch_path, - test_patch_metadata[call_count]['rel_patch_path']) - - return os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count]['rel_patch_path']) - - # 'GetPathToPatch()' was called more times than expected (4 times). - assert False, 'Unexpectedly called more than 4 times.' - - # Simulate behavior for 'ApplyPatch()' when applying multiple applicable - # patches. - @CallCountsToMockFunctions - def MultipleCallsToApplyPatches(call_count, _src_path, path_to_patch): - if call_count < 3: - self.assertEqual( - path_to_patch, - os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count + 1]['rel_patch_path'])) - - # Simulate that the second patch applied successfully. - return call_count == 1 - - # 'ApplyPatch()' was called more times than expected (3 times). - assert False, 'Unexpectedly called more than 3 times.' - - # Use test functions to simulate behavior. - mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath - mock_apply_patch.side_effect = MultipleCallsToApplyPatches - - expected_applied_patches = ['add_warning.patch'] - expected_failed_patches = ['fixes_input.patch', 'add_helper.patch'] - expected_disabled_patches = ['fixes_input.patch', 'add_helper.patch'] - expected_non_applicable_patches = ['fixes_output.patch'] - - # Assigned 'None' for now, but it is expected that the patch metadata file - # will be modified, so the 'expected_patch_info_dict's' value for the - # key 'modified_metadata' will get updated to the temporary .json file once - # the file is created. - expected_modified_metadata_file = None - - expected_patch_info_dict = { - 'applied_patches': expected_applied_patches, - 'failed_patches': expected_failed_patches, - 'non_applicable_patches': expected_non_applicable_patches, - 'disabled_patches': expected_disabled_patches, - 'removed_patches': [], - 'modified_metadata': expected_modified_metadata_file - } - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - expected_patch_info_dict['modified_metadata'] = json_test_file - - src_path = '/some/path/to/src/' - - revision = 1200 - - patch_info = patch_manager.HandlePatches(revision, json_test_file, - abs_path_to_filesdir, src_path, - FailureModes.DISABLE_PATCHES) - - self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) - - # 'test_patch_1' and 'test_patch_3' were not modified/disabled, so their - # dictionary is the same, but 'test_patch_2' and 'test_patch_4' were - # disabled, so their 'end_version' would be set to 1200, which was the - # value passed into 'HandlePatches()' for the 'svn_version'. - test_patch_2['end_version'] = 1200 - test_patch_4['end_version'] = 1200 - - expected_json_file = [ - test_patch_1, test_patch_2, test_patch_3, test_patch_4 - ] - - # Make sure the updated patch metadata was written into the temporary - # .json file. - with open(json_test_file) as patch_file: - new_json_file_contents = json.load(patch_file) - - self.assertListEqual(new_json_file_contents, expected_json_file) - - self.assertEqual(mock_get_path_to_patch.call_count, 4) - - self.assertEqual(mock_apply_patch.call_count, 3) - - @mock.patch.object(patch_manager, 'GetPathToPatch') - @mock.patch.object(patch_manager, 'ApplyPatch') - def testSomePatchesAreRemoved(self, mock_apply_patch, mock_get_path_to_patch): - # For the 'remove_patches' mode, this patch is expected to be in the - # 'non_applicable_patches' list and 'removed_patches' list because - # the 'svn_version' (1500) >= 'end_version' (1190). - test_patch_1 = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 1000, - 'until': 1190 - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'applicable_patches' list (which is the list that the .json file will be - # updated with) because the 'svn_version' < 'inf' (this patch does not have - # an 'end_version' value which implies 'end_version' == 'inf'). - test_patch_2 = { - 'comment': 'Fixes input', - 'rel_patch_path': 'cherry/fixes_input.patch', - 'version_range': { - 'from': 1000 - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'non_applicable_patches' list and 'removed_patches' list because - # the 'svn_version' (1500) >= 'end_version' (1500). - test_patch_3 = { - 'comment': 'Adds a warning', - 'rel_patch_path': 'add_warning.patch', - 'version_range': { - 'from': 750, - 'until': 1500 - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'non_applicable_patches' list and 'removed_patches' list because - # the 'svn_version' (1500) >= 'end_version' (1400). - test_patch_4 = { - 'comment': 'Adds a helper function', - 'rel_patch_path': 'add_helper.patch', - 'version_range': { - 'from': 20, - 'until': 1400 - } - } - - test_patch_metadata = [ - test_patch_1, test_patch_2, test_patch_3, test_patch_4 - ] - - abs_path_to_filesdir = '/abs/path/to/filesdir' - - # Simulate behavior for 'GetPathToPatch()' when successfully constructed the - # absolute path to the patch and the patch exists. - @CallCountsToMockFunctions - def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): - self.assertEqual(filesdir_path, abs_path_to_filesdir) - - if call_count < 4: - self.assertEqual(rel_patch_path, - test_patch_metadata[call_count]['rel_patch_path']) - - return os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count]['rel_patch_path']) - - assert False, 'Unexpectedly called more than 4 times.' - - # Use the test function to simulate behavior of 'GetPathToPatch()'. - mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath - - expected_applied_patches = [] - expected_failed_patches = [] - expected_disabled_patches = [] - expected_non_applicable_patches = [ - 'fixes_output.patch', 'add_warning.patch', 'add_helper.patch' - ] - expected_removed_patches = [ - '/abs/path/to/filesdir/cherry/fixes_output.patch', - '/abs/path/to/filesdir/add_warning.patch', - '/abs/path/to/filesdir/add_helper.patch' - ] - - # Assigned 'None' for now, but it is expected that the patch metadata file - # will be modified, so the 'expected_patch_info_dict's' value for the - # key 'modified_metadata' will get updated to the temporary .json file once - # the file is created. - expected_modified_metadata_file = None - - expected_patch_info_dict = { - 'applied_patches': expected_applied_patches, - 'failed_patches': expected_failed_patches, - 'non_applicable_patches': expected_non_applicable_patches, - 'disabled_patches': expected_disabled_patches, - 'removed_patches': expected_removed_patches, - 'modified_metadata': expected_modified_metadata_file - } - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - expected_patch_info_dict['modified_metadata'] = json_test_file - - abs_path_to_filesdir = '/abs/path/to/filesdir' - - src_path = '/some/path/to/src/' - - revision = 1500 - - patch_info = patch_manager.HandlePatches(revision, json_test_file, - abs_path_to_filesdir, src_path, - FailureModes.REMOVE_PATCHES) - - self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) - - # 'test_patch_2' was an applicable patch, so this patch will be the only - # patch that is in temporary .json file. The other patches were not - # applicable (they failed the applicable check), so they will not be in - # the .json file. - expected_json_file = [test_patch_2] - - # Make sure the updated patch metadata was written into the temporary - # .json file. - with open(json_test_file) as patch_file: - new_json_file_contents = json.load(patch_file) - - self.assertListEqual(new_json_file_contents, expected_json_file) - - self.assertEqual(mock_get_path_to_patch.call_count, 4) - - mock_apply_patch.assert_not_called() - - @mock.patch.object(patch_manager, 'GetPathToPatch') - @mock.patch.object(patch_manager, 'ApplyPatch') - def testSuccessfullyDidNotRemoveAFuturePatch(self, mock_apply_patch, - mock_get_path_to_patch): - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'non_applicable_patches' list and 'removed_patches' list because - # the 'svn_version' (1200) >= 'end_version' (1190). - test_patch_1 = { - 'comment': 'Redirects output to stdout', - 'rel_patch_path': 'cherry/fixes_output.patch', - 'version_range': { - 'from': 1000, - 'until': 1190 - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'applicable_patches' list (which is the list that the .json file will be - # updated with) because the 'svn_version' < 'inf' (this patch does not have - # an 'end_version' value which implies 'end_version' == 'inf'). - test_patch_2 = { - 'comment': 'Fixes input', - 'rel_patch_path': 'cherry/fixes_input.patch', - 'version_range': { - 'from': 1000, - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'applicable_patches' list because 'svn_version' >= 'start_version' and - # 'svn_version' < 'end_version'. - test_patch_3 = { - 'comment': 'Adds a warning', - 'rel_patch_path': 'add_warning.patch', - 'version_range': { - 'from': 750, - 'until': 1500 - } - } - - # For the 'remove_patches' mode, this patch is expected to be in the - # 'applicable_patches' list because the patch is from the future (e.g. - # 'start_version' > 'svn_version' (1200), so it should NOT be removed. - test_patch_4 = { - 'comment': 'Adds a helper function', - 'rel_patch_path': 'add_helper.patch', - 'version_range': { - 'from': 1600, - 'until': 2000 - } - } - - test_patch_metadata = [ - test_patch_1, test_patch_2, test_patch_3, test_patch_4 - ] - - abs_path_to_filesdir = '/abs/path/to/filesdir' - - # Simulate behavior for 'GetPathToPatch()' when successfully constructed the - # absolute path to the patch and the patch exists. - @CallCountsToMockFunctions - def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): - self.assertEqual(filesdir_path, abs_path_to_filesdir) - - if call_count < 4: - self.assertEqual(rel_patch_path, - test_patch_metadata[call_count]['rel_patch_path']) - - return os.path.join(abs_path_to_filesdir, - test_patch_metadata[call_count]['rel_patch_path']) - - # 'GetPathToPatch()' was called more times than expected (4 times). - assert False, 'Unexpectedly called more than 4 times.' - - # Use the test function to simulate behavior of 'GetPathToPatch()'. - mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath - - expected_applied_patches = [] - expected_failed_patches = [] - expected_disabled_patches = [] - - # 'add_helper.patch' is still a 'non applicable' patch meaning it does not - # apply in revision 1200 but it will NOT be removed because it is a future - # patch. - expected_non_applicable_patches = ['fixes_output.patch', 'add_helper.patch'] - expected_removed_patches = [ - '/abs/path/to/filesdir/cherry/fixes_output.patch' - ] - - # Assigned 'None' for now, but it is expected that the patch metadata file - # will be modified, so the 'expected_patch_info_dict's' value for the - # key 'modified_metadata' will get updated to the temporary .json file once - # the file is created. - expected_modified_metadata_file = None - - expected_patch_info_dict = { - 'applied_patches': expected_applied_patches, - 'failed_patches': expected_failed_patches, - 'non_applicable_patches': expected_non_applicable_patches, - 'disabled_patches': expected_disabled_patches, - 'removed_patches': expected_removed_patches, - 'modified_metadata': expected_modified_metadata_file - } - - with CreateTemporaryJsonFile() as json_test_file: - # Write the test patch metadata to the temporary .json file. - with open(json_test_file, 'w') as json_file: - WritePrettyJsonFile(test_patch_metadata, json_file) - - expected_patch_info_dict['modified_metadata'] = json_test_file - - src_path = '/some/path/to/src/' - - revision = 1200 - - patch_info = patch_manager.HandlePatches(revision, json_test_file, - abs_path_to_filesdir, src_path, - FailureModes.REMOVE_PATCHES) - - self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) - - # 'test_patch_2' was an applicable patch, so this patch will be the only - # patch that is in temporary .json file. The other patches were not - # applicable (they failed the applicable check), so they will not be in - # the .json file. - expected_json_file = [test_patch_2, test_patch_3, test_patch_4] - - # Make sure the updated patch metadata was written into the temporary - # .json file. - with open(json_test_file) as patch_file: - new_json_file_contents = json.load(patch_file) - - self.assertListEqual(new_json_file_contents, expected_json_file) - - self.assertEqual(mock_get_path_to_patch.call_count, 4) - - mock_apply_patch.assert_not_called() - - -if __name__ == '__main__': - unittest.main() + @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") + @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, + ) + + +if __name__ == "__main__": + unittest.main() |