Add `git cl upload --retry-failed`

The expected behavior is that for CLs that were already uploaded
before, where some tryjobs were run and failed, git cl upload
--retry-failed will be kind of like git cl upload --cq-dry-run
except it will only trigger tryjobs that failed.

Bug: 985887
Change-Id: I6371bca3ba501b1ea2cd7160e2f933530d7e633f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1828322
Auto-Submit: Quinten Yearsley <qyearsley@chromium.org>
Commit-Queue: Andrii Shyshkalov <tandrii@google.com>
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
diff --git a/tests/git_cl_test.py b/tests/git_cl_test.py
index 2ae36b3..9881bd2 100755
--- a/tests/git_cl_test.py
+++ b/tests/git_cl_test.py
@@ -3262,7 +3262,6 @@
     mockCallBuildbucket.assert_called_with(
         mock.ANY, 'cr-buildbucket.appspot.com', 'Batch', expected_request)
 
-
   @mock.patch('git_cl.Changelist._GetChangeDetail')
   def testScheduleOnBuildbucket_WrongBucket(self, mockGetChangeDetail):
     mockGetChangeDetail.return_value = {
@@ -3322,6 +3321,87 @@
             'WARNING Please specify buckets', git_cl.sys.stdout.getvalue())
 
 
+class CMDUploadTestCase(unittest.TestCase):
+
+  def setUp(self):
+    super(CMDUploadTestCase, self).setUp()
+    mock.patch('git_cl.sys.stdout', StringIO.StringIO()).start()
+    mock.patch('git_cl.uuid.uuid4', _constantFn('uuid4')).start()
+    mock.patch('git_cl.Changelist.GetIssue', _constantFn(123456)).start()
+    mock.patch('git_cl.Changelist.GetCodereviewServer',
+               _constantFn('https://chromium-review.googlesource.com')).start()
+    mock.patch('git_cl.Changelist.GetMostRecentPatchset',
+               _constantFn(7)).start()
+    mock.patch('git_cl.auth.get_authenticator_for_host', AuthenticatorMock())
+    self.addCleanup(mock.patch.stopall)
+
+  @mock.patch('git_cl.fetch_try_jobs')
+  @mock.patch('git_cl._trigger_try_jobs')
+  @mock.patch('git_cl.Changelist._GetChangeDetail')
+  @mock.patch('git_cl.Changelist.CMDUpload', _constantFn(0))
+  def testUploadRetryFailed(self, mockGetChangeDetail, mockTriggerTryJobs,
+                            mockFetchTryJobs):
+    # This test mocks out the actual upload part, and just asserts that after
+    # upload, if --retry-failed is added, then the tool will fetch try jobs
+    # from the previous patchset and trigger the right builders on the latest
+    # patchset.
+    mockGetChangeDetail.return_value = {
+        'project': 'depot_tools',
+        'status': 'OPEN',
+        'owner': {'email': 'owner@e.mail'},
+        'current_revision': 'beeeeeef',
+        'revisions': {
+            'deadbeaf': {
+                '_number': 6,
+            },
+            'beeeeeef': {
+                '_number': 7,
+                'fetch': {'http': {
+                    'url': 'https://chromium.googlesource.com/depot_tools',
+                    'ref': 'refs/changes/56/123456/7'
+                }},
+            },
+        },
+    }
+    mockFetchTryJobs.return_value = {
+      '9000': {
+        'id': '9000',
+        'project': 'infra',
+        'bucket': 'luci.infra.try',
+        'created_by': 'user:someone@chromium.org',
+        'created_ts': '147200002222000',
+        'experimental': False,
+        'parameters_json': json.dumps({
+          'builder_name': 'red-bot',
+          'properties': {'category': 'cq'},
+        }),
+        'status': 'COMPLETED',
+        'result': 'FAILURE',
+        'tags': ['user_agent:cq'],
+      },
+      8000: {
+        'id': '8000',
+        'project': 'infra',
+        'bucket': 'luci.infra.try',
+        'created_by': 'user:someone@chromium.org',
+        'created_ts': '147200002222020',
+        'experimental': False,
+        'parameters_json': json.dumps({
+          'builder_name': 'green-bot',
+          'properties': {'category': 'cq'},
+        }),
+        'status': 'COMPLETED',
+        'result': 'SUCCESS',
+        'tags': ['user_agent:cq'],
+      },
+    }
+    self.assertEqual(0, git_cl.main(['upload', '--retry-failed']))
+    mockFetchTryJobs.assert_called_with(
+        mock.ANY, mock.ANY, 'cr-buildbucket.appspot.com', 7)
+    buckets = {'infra/try': {'red-bot': []}}
+    mockTriggerTryJobs.assert_called_once_with(
+        mock.ANY, mock.ANY, buckets, mock.ANY, 8)
+
 if __name__ == '__main__':
   logging.basicConfig(
       level=logging.DEBUG if '-v' in sys.argv else logging.ERROR)