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