1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2019 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Unit tests when handling patches.""" 8 9from __future__ import print_function 10 11import json 12import os 13import subprocess 14import unittest 15import unittest.mock as mock 16 17import patch_manager 18from failure_modes import FailureModes 19from test_helpers import CallCountsToMockFunctions 20from test_helpers import CreateTemporaryJsonFile 21from test_helpers import WritePrettyJsonFile 22 23 24class PatchManagerTest(unittest.TestCase): 25 """Test class when handling patches of packages.""" 26 27 # Simulate behavior of 'os.path.isdir()' when the path is not a directory. 28 @mock.patch.object(os.path, 'isdir', return_value=False) 29 def testInvalidDirectoryPassedAsCommandLineArgument(self, mock_isdir): 30 test_dir = '/some/path/that/is/not/a/directory' 31 32 # Verify the exception is raised when the command line argument for 33 # '--filesdir_path' or '--src_path' is not a directory. 34 with self.assertRaises(ValueError) as err: 35 patch_manager.is_directory(test_dir) 36 37 self.assertEqual( 38 str(err.exception), 'Path is not a directory: ' 39 '%s' % test_dir) 40 41 mock_isdir.assert_called_once() 42 43 # Simulate the behavior of 'os.path.isdir()' when a path to a directory is 44 # passed as the command line argument for '--filesdir_path' or '--src_path'. 45 @mock.patch.object(os.path, 'isdir', return_value=True) 46 def testValidDirectoryPassedAsCommandLineArgument(self, mock_isdir): 47 test_dir = '/some/path/that/is/a/directory' 48 49 self.assertEqual(patch_manager.is_directory(test_dir), test_dir) 50 51 mock_isdir.assert_called_once() 52 53 # Simulate behavior of 'os.path.isfile()' when the patch metadata file is does 54 # not exist. 55 @mock.patch.object(os.path, 'isfile', return_value=False) 56 def testInvalidPathToPatchMetadataFilePassedAsCommandLineArgument( 57 self, mock_isfile): 58 59 abs_path_to_patch_file = '/abs/path/to/PATCHES.json' 60 61 # Verify the exception is raised when the command line argument for 62 # '--patch_metadata_file' does not exist or is not a file. 63 with self.assertRaises(ValueError) as err: 64 patch_manager.is_patch_metadata_file(abs_path_to_patch_file) 65 66 self.assertEqual( 67 str(err.exception), 'Invalid patch metadata file provided: ' 68 '%s' % abs_path_to_patch_file) 69 70 mock_isfile.assert_called_once() 71 72 # Simulate the behavior of 'os.path.isfile()' when the path to the patch 73 # metadata file exists and is a file. 74 @mock.patch.object(os.path, 'isfile', return_value=True) 75 def testPatchMetadataFileDoesNotEndInJson(self, mock_isfile): 76 abs_path_to_patch_file = '/abs/path/to/PATCHES' 77 78 # Verify the exception is raises when the command line argument for 79 # '--patch_metadata_file' exists and is a file but does not end in 80 # '.json'. 81 with self.assertRaises(ValueError) as err: 82 patch_manager.is_patch_metadata_file(abs_path_to_patch_file) 83 84 self.assertEqual( 85 str(err.exception), 'Patch metadata file does not end in ".json": ' 86 '%s' % abs_path_to_patch_file) 87 88 mock_isfile.assert_called_once() 89 90 # Simulate the behavior of 'os.path.isfile()' when the command line argument 91 # for '--patch_metadata_file' exists and is a file. 92 @mock.patch.object(os.path, 'isfile', return_value=True) 93 def testValidPatchMetadataFilePassedAsCommandLineArgument(self, mock_isfile): 94 abs_path_to_patch_file = '/abs/path/to/PATCHES.json' 95 96 self.assertEqual( 97 patch_manager.is_patch_metadata_file(abs_path_to_patch_file), 98 '%s' % abs_path_to_patch_file) 99 100 mock_isfile.assert_called_once() 101 102 # Simulate behavior of 'os.path.isdir()' when the path to $FILESDIR 103 # does not exist. 104 @mock.patch.object(os.path, 'isdir', return_value=False) 105 def testInvalidPathToFilesDirWhenConstructingPathToPatch(self, mock_isdir): 106 abs_path_to_filesdir = '/abs/path/to/filesdir' 107 108 rel_patch_path = 'cherry/fixes_stdout.patch' 109 110 # Verify the exception is raised when the the absolute path to $FILESDIR of 111 # a package is not a directory. 112 with self.assertRaises(ValueError) as err: 113 patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path) 114 115 self.assertEqual( 116 str(err.exception), 'Invalid path to $FILESDIR provided: ' 117 '%s' % abs_path_to_filesdir) 118 119 mock_isdir.assert_called_once() 120 121 # Simulate behavior of 'os.path.isdir()' when the absolute path to the 122 # $FILESDIR of a package exists and is a directory. 123 @mock.patch.object(os.path, 'isdir', return_value=True) 124 # Simulate the behavior of 'os.path.isfile()' when the absolute path to the 125 # patch does not exist. 126 @mock.patch.object(os.path, 'isfile', return_value=False) 127 def testConstructedPathToPatchDoesNotExist(self, mock_isfile, mock_isdir): 128 abs_path_to_filesdir = '/abs/path/to/filesdir' 129 130 rel_patch_path = 'cherry/fixes_stdout.patch' 131 132 abs_patch_path = os.path.join(abs_path_to_filesdir, rel_patch_path) 133 134 # Verify the exception is raised when the absolute path to the patch does 135 # not exist. 136 with self.assertRaises(ValueError) as err: 137 patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path) 138 139 self.assertEqual( 140 str(err.exception), 'The absolute path %s to the patch %s does not ' 141 'exist' % (abs_patch_path, rel_patch_path)) 142 143 mock_isdir.assert_called_once() 144 145 mock_isfile.assert_called_once() 146 147 # Simulate behavior of 'os.path.isdir()' when the absolute path to the 148 # $FILESDIR of a package exists and is a directory. 149 @mock.patch.object(os.path, 'isdir', return_value=True) 150 # Simulate behavior of 'os.path.isfile()' when the absolute path to the 151 # patch exists and is a file. 152 @mock.patch.object(os.path, 'isfile', return_value=True) 153 def testConstructedPathToPatchSuccessfully(self, mock_isfile, mock_isdir): 154 abs_path_to_filesdir = '/abs/path/to/filesdir' 155 156 rel_patch_path = 'cherry/fixes_stdout.patch' 157 158 abs_patch_path = os.path.join(abs_path_to_filesdir, rel_patch_path) 159 160 self.assertEqual( 161 patch_manager.GetPathToPatch(abs_path_to_filesdir, rel_patch_path), 162 abs_patch_path) 163 164 mock_isdir.assert_called_once() 165 166 mock_isfile.assert_called_once() 167 168 def testSuccessfullyGetPatchMetadataForPatchWithNoMetadata(self): 169 expected_patch_metadata = 0, None, False 170 171 test_patch = { 172 'comment': 'Redirects output to stdout', 173 'rel_patch_path': 'cherry/fixes_stdout.patch' 174 } 175 176 self.assertEqual( 177 patch_manager.GetPatchMetadata(test_patch), expected_patch_metadata) 178 179 def testSuccessfullyGetPatchMetdataForPatchWithSomeMetadata(self): 180 expected_patch_metadata = 0, 1000, False 181 182 test_patch = { 183 'comment': 'Redirects output to stdout', 184 'rel_patch_path': 'cherry/fixes_stdout.patch', 185 'end_version': 1000 186 } 187 188 self.assertEqual( 189 patch_manager.GetPatchMetadata(test_patch), expected_patch_metadata) 190 191 def testFailedToApplyPatchWhenInvalidSrcPathIsPassedIn(self): 192 src_path = '/abs/path/to/src' 193 194 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' 195 196 # Verify the exception is raised when the absolute path to the unpacked 197 # sources of a package is not a directory. 198 with self.assertRaises(ValueError) as err: 199 patch_manager.ApplyPatch(src_path, abs_patch_path) 200 201 self.assertEqual( 202 str(err.exception), 'Invalid src path provided: %s' % src_path) 203 204 # Simulate behavior of 'os.path.isdir()' when the absolute path to the 205 # unpacked sources of the package is valid and exists. 206 @mock.patch.object(os.path, 'isdir', return_value=True) 207 def testFailedToApplyPatchWhenPatchPathIsInvalid(self, mock_isdir): 208 src_path = '/abs/path/to/src' 209 210 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' 211 212 # Verify the exception is raised when the absolute path to the patch does 213 # not exist or is not a file. 214 with self.assertRaises(ValueError) as err: 215 patch_manager.ApplyPatch(src_path, abs_patch_path) 216 217 self.assertEqual( 218 str(err.exception), 'Invalid patch file provided: ' 219 '%s' % abs_patch_path) 220 221 mock_isdir.assert_called_once() 222 223 # Simulate behavior of 'os.path.isdir()' when the absolute path to the 224 # unpacked sources of the package is valid and exists. 225 @mock.patch.object(os.path, 'isdir', return_value=True) 226 @mock.patch.object(os.path, 'isfile', return_value=True) 227 # Simulate behavior of 'os.path.isfile()' when the absolute path to the 228 # patch exists and is a file. 229 @mock.patch.object(patch_manager, 'check_output') 230 def testFailedToApplyPatchInDryRun(self, mock_dry_run, mock_isfile, 231 mock_isdir): 232 233 # Simulate behavior of 'subprocess.check_output()' when '--dry-run' 234 # fails on the applying patch. 235 def FailedToApplyPatch(test_patch_cmd): 236 # First argument is the return error code, the second argument is the 237 # command that was run, and the third argument is the output. 238 raise subprocess.CalledProcessError(1, test_patch_cmd, None) 239 240 mock_dry_run.side_effect = FailedToApplyPatch 241 242 src_path = '/abs/path/to/src' 243 244 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' 245 246 self.assertEqual(patch_manager.ApplyPatch(src_path, abs_patch_path), False) 247 248 mock_isdir.assert_called_once() 249 250 mock_isfile.assert_called_once() 251 252 mock_dry_run.assert_called_once() 253 254 # Simulate behavior of 'os.path.isdir()' when the absolute path to the 255 # unpacked sources of the package is valid and exists. 256 @mock.patch.object(os.path, 'isdir', return_value=True) 257 @mock.patch.object(os.path, 'isfile', return_value=True) 258 # Simulate behavior of 'os.path.isfile()' when the absolute path to the 259 # patch exists and is a file. 260 @mock.patch.object(patch_manager, 'check_output') 261 def testSuccessfullyAppliedPatch(self, mock_dry_run, mock_isfile, mock_isdir): 262 src_path = '/abs/path/to/src' 263 264 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_stdout.patch' 265 266 self.assertEqual(patch_manager.ApplyPatch(src_path, abs_patch_path), True) 267 268 mock_isdir.assert_called_once() 269 270 mock_isfile.assert_called_once() 271 272 self.assertEqual(mock_dry_run.call_count, 2) 273 274 def testFailedToUpdatePatchMetadataFileWhenPatchFileNotEndInJson(self): 275 patch = [{ 276 'comment': 'Redirects output to stdout', 277 'rel_patch_path': 'cherry/fixes_output.patch', 278 'start_version': 10 279 }] 280 281 abs_patch_path = '/abs/path/to/filesdir/PATCHES' 282 283 # Verify the exception is raised when the absolute path to the patch 284 # metadata file does not end in '.json'. 285 with self.assertRaises(ValueError) as err: 286 patch_manager.UpdatePatchMetadataFile(abs_patch_path, patch) 287 288 self.assertEqual( 289 str(err.exception), 'File does not end in ".json": ' 290 '%s' % abs_patch_path) 291 292 def testSuccessfullyUpdatedPatchMetadataFile(self): 293 test_updated_patch_metadata = [{ 294 'comment': 'Redirects output to stdout', 295 'rel_patch_path': 'cherry/fixes_output.patch', 296 'start_version': 10 297 }] 298 299 expected_patch_metadata = { 300 'comment': 'Redirects output to stdout', 301 'rel_patch_path': 'cherry/fixes_output.patch', 302 'start_version': 10 303 } 304 305 with CreateTemporaryJsonFile() as json_test_file: 306 patch_manager.UpdatePatchMetadataFile(json_test_file, 307 test_updated_patch_metadata) 308 309 # Make sure the updated patch metadata was written into the temporary 310 # .json file. 311 with open(json_test_file) as patch_file: 312 patch_contents = json.load(patch_file) 313 314 self.assertEqual(len(patch_contents), 1) 315 316 self.assertDictEqual(patch_contents[0], expected_patch_metadata) 317 318 @mock.patch.object(patch_manager, 'GetPathToPatch') 319 def testExceptionThrownWhenHandlingPatches(self, mock_get_path_to_patch): 320 filesdir_path = '/abs/path/to/filesdir' 321 322 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_output.patch' 323 324 rel_patch_path = 'cherry/fixes_output.patch' 325 326 # Simulate behavior of 'GetPathToPatch()' when the absolute path to the 327 # patch does not exist. 328 def PathToPatchDoesNotExist(filesdir_path, rel_patch_path): 329 raise ValueError('The absolute path to %s does not exist' % os.path.join( 330 filesdir_path, rel_patch_path)) 331 332 # Use the test function to simulate the behavior of 'GetPathToPatch()'. 333 mock_get_path_to_patch.side_effect = PathToPatchDoesNotExist 334 335 test_patch_metadata = [{ 336 'comment': 'Redirects output to stdout', 337 'rel_patch_path': rel_patch_path, 338 'start_version': 10 339 }] 340 341 with CreateTemporaryJsonFile() as json_test_file: 342 # Write the test patch metadata to the temporary .json file. 343 with open(json_test_file, 'w') as json_file: 344 WritePrettyJsonFile(test_patch_metadata, json_file) 345 346 src_path = '/some/path/to/src' 347 348 revision = 1000 349 350 # Verify the exception is raised when the absolute path to a patch does 351 # not exist. 352 with self.assertRaises(ValueError) as err: 353 patch_manager.HandlePatches(revision, json_test_file, filesdir_path, 354 src_path, FailureModes.FAIL) 355 356 self.assertEqual( 357 str(err.exception), 358 'The absolute path to %s does not exist' % abs_patch_path) 359 360 mock_get_path_to_patch.assert_called_once_with(filesdir_path, 361 rel_patch_path) 362 363 @mock.patch.object(patch_manager, 'GetPathToPatch') 364 # Simulate behavior for 'ApplyPatch()' when an applicable patch failed to 365 # apply. 366 @mock.patch.object(patch_manager, 'ApplyPatch', return_value=False) 367 def testExceptionThrownOnAFailedPatchInFailMode(self, mock_apply_patch, 368 mock_get_path_to_patch): 369 filesdir_path = '/abs/path/to/filesdir' 370 371 abs_patch_path = '/abs/path/to/filesdir/cherry/fixes_output.patch' 372 373 rel_patch_path = 'cherry/fixes_output.patch' 374 375 # Simulate behavior for 'GetPathToPatch()' when successfully constructed the 376 # absolute path to the patch and the patch exists. 377 mock_get_path_to_patch.return_value = abs_patch_path 378 379 test_patch_metadata = [{ 380 'comment': 'Redirects output to stdout', 381 'rel_patch_path': rel_patch_path, 382 'start_version': 1000 383 }] 384 385 with CreateTemporaryJsonFile() as json_test_file: 386 # Write the test patch metadata to the temporary .json file. 387 with open(json_test_file, 'w') as json_file: 388 WritePrettyJsonFile(test_patch_metadata, json_file) 389 390 src_path = '/some/path/to/src' 391 392 revision = 1000 393 394 patch_name = 'fixes_output.patch' 395 396 # Verify the exception is raised when the mode is 'fail' and an applicable 397 # patch fails to apply. 398 with self.assertRaises(ValueError) as err: 399 patch_manager.HandlePatches(revision, json_test_file, filesdir_path, 400 src_path, FailureModes.FAIL) 401 402 self.assertEqual( 403 str(err.exception), 'Failed to apply patch: %s' % patch_name) 404 405 mock_get_path_to_patch.assert_called_once_with(filesdir_path, 406 rel_patch_path) 407 408 mock_apply_patch.assert_called_once_with(src_path, abs_patch_path) 409 410 @mock.patch.object(patch_manager, 'GetPathToPatch') 411 @mock.patch.object(patch_manager, 'ApplyPatch') 412 def testSomePatchesFailedToApplyInContinueMode(self, mock_apply_patch, 413 mock_get_path_to_patch): 414 415 test_patch_1 = { 416 'comment': 'Redirects output to stdout', 417 'rel_patch_path': 'cherry/fixes_output.patch', 418 'start_version': 1000, 419 'end_version': 1250 420 } 421 422 test_patch_2 = { 423 'comment': 'Fixes input', 424 'rel_patch_path': 'cherry/fixes_input.patch', 425 'start_version': 1000 426 } 427 428 test_patch_3 = { 429 'comment': 'Adds a warning', 430 'rel_patch_path': 'add_warning.patch', 431 'start_version': 750, 432 'end_version': 1500 433 } 434 435 test_patch_4 = { 436 'comment': 'Adds a helper function', 437 'rel_patch_path': 'add_helper.patch', 438 'start_version': 20, 439 'end_version': 900 440 } 441 442 test_patch_metadata = [ 443 test_patch_1, test_patch_2, test_patch_3, test_patch_4 444 ] 445 446 abs_path_to_filesdir = '/abs/path/to/filesdir' 447 448 # Simulate behavior for 'GetPathToPatch()' when successfully constructed the 449 # absolute path to the patch and the patch exists. 450 @CallCountsToMockFunctions 451 def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): 452 self.assertEqual(filesdir_path, abs_path_to_filesdir) 453 454 if call_count < 4: 455 self.assertEqual(rel_patch_path, 456 test_patch_metadata[call_count]['rel_patch_path']) 457 458 return os.path.join(abs_path_to_filesdir, 459 test_patch_metadata[call_count]['rel_patch_path']) 460 461 assert False, 'Unexpectedly called more than 4 times.' 462 463 # Simulate behavior for 'ApplyPatch()' when applying multiple applicable 464 # patches. 465 @CallCountsToMockFunctions 466 def MultipleCallsToApplyPatches(call_count, _src_path, path_to_patch): 467 if call_count < 3: 468 self.assertEqual( 469 path_to_patch, 470 os.path.join(abs_path_to_filesdir, 471 test_patch_metadata[call_count]['rel_patch_path'])) 472 473 # Simulate that the first patch successfully applied. 474 return call_count == 0 475 476 # 'ApplyPatch()' was called more times than expected (3 times). 477 assert False, 'Unexpectedly called more than 3 times.' 478 479 # Use test functions to simulate behavior. 480 mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath 481 mock_apply_patch.side_effect = MultipleCallsToApplyPatches 482 483 expected_applied_patches = ['fixes_output.patch'] 484 expected_failed_patches = ['fixes_input.patch', 'add_warning.patch'] 485 expected_non_applicable_patches = ['add_helper.patch'] 486 487 expected_patch_info_dict = { 488 'applied_patches': expected_applied_patches, 489 'failed_patches': expected_failed_patches, 490 'non_applicable_patches': expected_non_applicable_patches, 491 'disabled_patches': [], 492 'removed_patches': [], 493 'modified_metadata': None 494 } 495 496 with CreateTemporaryJsonFile() as json_test_file: 497 # Write the test patch metadata to the temporary .json file. 498 with open(json_test_file, 'w') as json_file: 499 WritePrettyJsonFile(test_patch_metadata, json_file) 500 501 src_path = '/some/path/to/src/' 502 503 revision = 1000 504 505 patch_info = patch_manager.HandlePatches(revision, json_test_file, 506 abs_path_to_filesdir, src_path, 507 FailureModes.CONTINUE) 508 509 self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) 510 511 self.assertEqual(mock_get_path_to_patch.call_count, 4) 512 513 self.assertEqual(mock_apply_patch.call_count, 3) 514 515 @mock.patch.object(patch_manager, 'GetPathToPatch') 516 @mock.patch.object(patch_manager, 'ApplyPatch') 517 def testSomePatchesAreDisabled(self, mock_apply_patch, 518 mock_get_path_to_patch): 519 520 test_patch_1 = { 521 'comment': 'Redirects output to stdout', 522 'rel_patch_path': 'cherry/fixes_output.patch', 523 'start_version': 1000, 524 'end_version': 1190 525 } 526 527 test_patch_2 = { 528 'comment': 'Fixes input', 529 'rel_patch_path': 'cherry/fixes_input.patch', 530 'start_version': 1000 531 } 532 533 test_patch_3 = { 534 'comment': 'Adds a warning', 535 'rel_patch_path': 'add_warning.patch', 536 'start_version': 750, 537 'end_version': 1500 538 } 539 540 test_patch_4 = { 541 'comment': 'Adds a helper function', 542 'rel_patch_path': 'add_helper.patch', 543 'start_version': 20, 544 'end_version': 2000 545 } 546 547 test_patch_metadata = [ 548 test_patch_1, test_patch_2, test_patch_3, test_patch_4 549 ] 550 551 abs_path_to_filesdir = '/abs/path/to/filesdir' 552 553 # Simulate behavior for 'GetPathToPatch()' when successfully constructed the 554 # absolute path to the patch and the patch exists. 555 @CallCountsToMockFunctions 556 def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): 557 self.assertEqual(filesdir_path, abs_path_to_filesdir) 558 559 if call_count < 4: 560 self.assertEqual(rel_patch_path, 561 test_patch_metadata[call_count]['rel_patch_path']) 562 563 return os.path.join(abs_path_to_filesdir, 564 test_patch_metadata[call_count]['rel_patch_path']) 565 566 # 'GetPathToPatch()' was called more times than expected (4 times). 567 assert False, 'Unexpectedly called more than 4 times.' 568 569 # Simulate behavior for 'ApplyPatch()' when applying multiple applicable 570 # patches. 571 @CallCountsToMockFunctions 572 def MultipleCallsToApplyPatches(call_count, _src_path, path_to_patch): 573 if call_count < 3: 574 self.assertEqual( 575 path_to_patch, 576 os.path.join(abs_path_to_filesdir, 577 test_patch_metadata[call_count + 1]['rel_patch_path'])) 578 579 # Simulate that the second patch applied successfully. 580 return call_count == 1 581 582 # 'ApplyPatch()' was called more times than expected (3 times). 583 assert False, 'Unexpectedly called more than 3 times.' 584 585 # Use test functions to simulate behavior. 586 mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath 587 mock_apply_patch.side_effect = MultipleCallsToApplyPatches 588 589 expected_applied_patches = ['add_warning.patch'] 590 expected_failed_patches = ['fixes_input.patch', 'add_helper.patch'] 591 expected_disabled_patches = ['fixes_input.patch', 'add_helper.patch'] 592 expected_non_applicable_patches = ['fixes_output.patch'] 593 594 # Assigned 'None' for now, but it is expected that the patch metadata file 595 # will be modified, so the 'expected_patch_info_dict's' value for the 596 # key 'modified_metadata' will get updated to the temporary .json file once 597 # the file is created. 598 expected_modified_metadata_file = None 599 600 expected_patch_info_dict = { 601 'applied_patches': expected_applied_patches, 602 'failed_patches': expected_failed_patches, 603 'non_applicable_patches': expected_non_applicable_patches, 604 'disabled_patches': expected_disabled_patches, 605 'removed_patches': [], 606 'modified_metadata': expected_modified_metadata_file 607 } 608 609 with CreateTemporaryJsonFile() as json_test_file: 610 # Write the test patch metadata to the temporary .json file. 611 with open(json_test_file, 'w') as json_file: 612 WritePrettyJsonFile(test_patch_metadata, json_file) 613 614 expected_patch_info_dict['modified_metadata'] = json_test_file 615 616 src_path = '/some/path/to/src/' 617 618 revision = 1200 619 620 patch_info = patch_manager.HandlePatches(revision, json_test_file, 621 abs_path_to_filesdir, src_path, 622 FailureModes.DISABLE_PATCHES) 623 624 self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) 625 626 # 'test_patch_1' and 'test_patch_3' were not modified/disabled, so their 627 # dictionary is the same, but 'test_patch_2' and 'test_patch_4' were 628 # disabled, so their 'end_version' would be set to 1200, which was the 629 # value passed into 'HandlePatches()' for the 'svn_version'. 630 test_patch_2['end_version'] = 1200 631 test_patch_4['end_version'] = 1200 632 633 expected_json_file = [ 634 test_patch_1, test_patch_2, test_patch_3, test_patch_4 635 ] 636 637 # Make sure the updated patch metadata was written into the temporary 638 # .json file. 639 with open(json_test_file) as patch_file: 640 new_json_file_contents = json.load(patch_file) 641 642 self.assertListEqual(new_json_file_contents, expected_json_file) 643 644 self.assertEqual(mock_get_path_to_patch.call_count, 4) 645 646 self.assertEqual(mock_apply_patch.call_count, 3) 647 648 @mock.patch.object(patch_manager, 'GetPathToPatch') 649 @mock.patch.object(patch_manager, 'ApplyPatch') 650 def testSomePatchesAreRemoved(self, mock_apply_patch, mock_get_path_to_patch): 651 # For the 'remove_patches' mode, this patch is expected to be in the 652 # 'non_applicable_patches' list and 'removed_patches' list because 653 # the 'svn_version' (1500) >= 'end_version' (1190). 654 test_patch_1 = { 655 'comment': 'Redirects output to stdout', 656 'rel_patch_path': 'cherry/fixes_output.patch', 657 'start_version': 1000, 658 'end_version': 1190 659 } 660 661 # For the 'remove_patches' mode, this patch is expected to be in the 662 # 'applicable_patches' list (which is the list that the .json file will be 663 # updated with) because the 'svn_version' < 'inf' (this patch does not have 664 # an 'end_version' value which implies 'end_version' == 'inf'). 665 test_patch_2 = { 666 'comment': 'Fixes input', 667 'rel_patch_path': 'cherry/fixes_input.patch', 668 'start_version': 1000 669 } 670 671 # For the 'remove_patches' mode, this patch is expected to be in the 672 # 'non_applicable_patches' list and 'removed_patches' list because 673 # the 'svn_version' (1500) >= 'end_version' (1500). 674 test_patch_3 = { 675 'comment': 'Adds a warning', 676 'rel_patch_path': 'add_warning.patch', 677 'start_version': 750, 678 'end_version': 1500 679 } 680 681 # For the 'remove_patches' mode, this patch is expected to be in the 682 # 'non_applicable_patches' list and 'removed_patches' list because 683 # the 'svn_version' (1500) >= 'end_version' (1400). 684 test_patch_4 = { 685 'comment': 'Adds a helper function', 686 'rel_patch_path': 'add_helper.patch', 687 'start_version': 20, 688 'end_version': 1400 689 } 690 691 test_patch_metadata = [ 692 test_patch_1, test_patch_2, test_patch_3, test_patch_4 693 ] 694 695 abs_path_to_filesdir = '/abs/path/to/filesdir' 696 697 # Simulate behavior for 'GetPathToPatch()' when successfully constructed the 698 # absolute path to the patch and the patch exists. 699 @CallCountsToMockFunctions 700 def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): 701 self.assertEqual(filesdir_path, abs_path_to_filesdir) 702 703 if call_count < 4: 704 self.assertEqual(rel_patch_path, 705 test_patch_metadata[call_count]['rel_patch_path']) 706 707 return os.path.join(abs_path_to_filesdir, 708 test_patch_metadata[call_count]['rel_patch_path']) 709 710 assert False, 'Unexpectedly called more than 4 times.' 711 712 # Use the test function to simulate behavior of 'GetPathToPatch()'. 713 mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath 714 715 expected_applied_patches = [] 716 expected_failed_patches = [] 717 expected_disabled_patches = [] 718 expected_non_applicable_patches = [ 719 'fixes_output.patch', 'add_warning.patch', 'add_helper.patch' 720 ] 721 expected_removed_patches = [ 722 '/abs/path/to/filesdir/cherry/fixes_output.patch', 723 '/abs/path/to/filesdir/add_warning.patch', 724 '/abs/path/to/filesdir/add_helper.patch' 725 ] 726 727 # Assigned 'None' for now, but it is expected that the patch metadata file 728 # will be modified, so the 'expected_patch_info_dict's' value for the 729 # key 'modified_metadata' will get updated to the temporary .json file once 730 # the file is created. 731 expected_modified_metadata_file = None 732 733 expected_patch_info_dict = { 734 'applied_patches': expected_applied_patches, 735 'failed_patches': expected_failed_patches, 736 'non_applicable_patches': expected_non_applicable_patches, 737 'disabled_patches': expected_disabled_patches, 738 'removed_patches': expected_removed_patches, 739 'modified_metadata': expected_modified_metadata_file 740 } 741 742 with CreateTemporaryJsonFile() as json_test_file: 743 # Write the test patch metadata to the temporary .json file. 744 with open(json_test_file, 'w') as json_file: 745 WritePrettyJsonFile(test_patch_metadata, json_file) 746 747 expected_patch_info_dict['modified_metadata'] = json_test_file 748 749 abs_path_to_filesdir = '/abs/path/to/filesdir' 750 751 src_path = '/some/path/to/src/' 752 753 revision = 1500 754 755 patch_info = patch_manager.HandlePatches(revision, json_test_file, 756 abs_path_to_filesdir, src_path, 757 FailureModes.REMOVE_PATCHES) 758 759 self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) 760 761 # 'test_patch_2' was an applicable patch, so this patch will be the only 762 # patch that is in temporary .json file. The other patches were not 763 # applicable (they failed the applicable check), so they will not be in 764 # the .json file. 765 expected_json_file = [test_patch_2] 766 767 # Make sure the updated patch metadata was written into the temporary 768 # .json file. 769 with open(json_test_file) as patch_file: 770 new_json_file_contents = json.load(patch_file) 771 772 self.assertListEqual(new_json_file_contents, expected_json_file) 773 774 self.assertEqual(mock_get_path_to_patch.call_count, 4) 775 776 mock_apply_patch.assert_not_called() 777 778 @mock.patch.object(patch_manager, 'GetPathToPatch') 779 @mock.patch.object(patch_manager, 'ApplyPatch') 780 def testSuccessfullyDidNotRemoveAFuturePatch(self, mock_apply_patch, 781 mock_get_path_to_patch): 782 783 # For the 'remove_patches' mode, this patch is expected to be in the 784 # 'non_applicable_patches' list and 'removed_patches' list because 785 # the 'svn_version' (1200) >= 'end_version' (1190). 786 test_patch_1 = { 787 'comment': 'Redirects output to stdout', 788 'rel_patch_path': 'cherry/fixes_output.patch', 789 'start_version': 1000, 790 'end_version': 1190 791 } 792 793 # For the 'remove_patches' mode, this patch is expected to be in the 794 # 'applicable_patches' list (which is the list that the .json file will be 795 # updated with) because the 'svn_version' < 'inf' (this patch does not have 796 # an 'end_version' value which implies 'end_version' == 'inf'). 797 test_patch_2 = { 798 'comment': 'Fixes input', 799 'rel_patch_path': 'cherry/fixes_input.patch', 800 'start_version': 1000 801 } 802 803 # For the 'remove_patches' mode, this patch is expected to be in the 804 # 'applicable_patches' list because 'svn_version' >= 'start_version' and 805 # 'svn_version' < 'end_version'. 806 test_patch_3 = { 807 'comment': 'Adds a warning', 808 'rel_patch_path': 'add_warning.patch', 809 'start_version': 750, 810 'end_version': 1500 811 } 812 813 # For the 'remove_patches' mode, this patch is expected to be in the 814 # 'applicable_patches' list because the patch is from the future (e.g. 815 # 'start_version' > 'svn_version' (1200), so it should NOT be removed. 816 test_patch_4 = { 817 'comment': 'Adds a helper function', 818 'rel_patch_path': 'add_helper.patch', 819 'start_version': 1600, 820 'end_version': 2000 821 } 822 823 test_patch_metadata = [ 824 test_patch_1, test_patch_2, test_patch_3, test_patch_4 825 ] 826 827 abs_path_to_filesdir = '/abs/path/to/filesdir' 828 829 # Simulate behavior for 'GetPathToPatch()' when successfully constructed the 830 # absolute path to the patch and the patch exists. 831 @CallCountsToMockFunctions 832 def MultipleCallsToGetPatchPath(call_count, filesdir_path, rel_patch_path): 833 self.assertEqual(filesdir_path, abs_path_to_filesdir) 834 835 if call_count < 4: 836 self.assertEqual(rel_patch_path, 837 test_patch_metadata[call_count]['rel_patch_path']) 838 839 return os.path.join(abs_path_to_filesdir, 840 test_patch_metadata[call_count]['rel_patch_path']) 841 842 # 'GetPathToPatch()' was called more times than expected (4 times). 843 assert False, 'Unexpectedly called more than 4 times.' 844 845 # Use the test function to simulate behavior of 'GetPathToPatch()'. 846 mock_get_path_to_patch.side_effect = MultipleCallsToGetPatchPath 847 848 expected_applied_patches = [] 849 expected_failed_patches = [] 850 expected_disabled_patches = [] 851 852 # 'add_helper.patch' is still a 'non applicable' patch meaning it does not 853 # apply in revision 1200 but it will NOT be removed because it is a future 854 # patch. 855 expected_non_applicable_patches = ['fixes_output.patch', 'add_helper.patch'] 856 expected_removed_patches = [ 857 '/abs/path/to/filesdir/cherry/fixes_output.patch' 858 ] 859 860 # Assigned 'None' for now, but it is expected that the patch metadata file 861 # will be modified, so the 'expected_patch_info_dict's' value for the 862 # key 'modified_metadata' will get updated to the temporary .json file once 863 # the file is created. 864 expected_modified_metadata_file = None 865 866 expected_patch_info_dict = { 867 'applied_patches': expected_applied_patches, 868 'failed_patches': expected_failed_patches, 869 'non_applicable_patches': expected_non_applicable_patches, 870 'disabled_patches': expected_disabled_patches, 871 'removed_patches': expected_removed_patches, 872 'modified_metadata': expected_modified_metadata_file 873 } 874 875 with CreateTemporaryJsonFile() as json_test_file: 876 # Write the test patch metadata to the temporary .json file. 877 with open(json_test_file, 'w') as json_file: 878 WritePrettyJsonFile(test_patch_metadata, json_file) 879 880 expected_patch_info_dict['modified_metadata'] = json_test_file 881 882 src_path = '/some/path/to/src/' 883 884 revision = 1200 885 886 patch_info = patch_manager.HandlePatches(revision, json_test_file, 887 abs_path_to_filesdir, src_path, 888 FailureModes.REMOVE_PATCHES) 889 890 self.assertDictEqual(patch_info._asdict(), expected_patch_info_dict) 891 892 # 'test_patch_2' was an applicable patch, so this patch will be the only 893 # patch that is in temporary .json file. The other patches were not 894 # applicable (they failed the applicable check), so they will not be in 895 # the .json file. 896 expected_json_file = [test_patch_2, test_patch_3, test_patch_4] 897 898 # Make sure the updated patch metadata was written into the temporary 899 # .json file. 900 with open(json_test_file) as patch_file: 901 new_json_file_contents = json.load(patch_file) 902 903 self.assertListEqual(new_json_file_contents, expected_json_file) 904 905 self.assertEqual(mock_get_path_to_patch.call_count, 4) 906 907 mock_apply_patch.assert_not_called() 908 909 910if __name__ == '__main__': 911 unittest.main() 912