llvm_tools: abandon CLs after completing LLVM bisection.

Abandon CLs created for bisection if LLVM bisection successfully found
the root cause.

BUG=chromium:1081457
TEST=Verified locally.

Change-Id: I3702c38432fbaf87f7df62418c0b29cbe4ca722a
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2382420
Reviewed-by: Manoj Gupta <manojgupta@chromium.org>
Reviewed-by: George Burgess <gbiv@chromium.org>
Tested-by: Jian Cai <jiancai@google.com>
diff --git a/llvm_tools/llvm_bisection.py b/llvm_tools/llvm_bisection.py
index 37320ba..c8d694c 100755
--- a/llvm_tools/llvm_bisection.py
+++ b/llvm_tools/llvm_bisection.py
@@ -13,6 +13,7 @@
 import errno
 import json
 import os
+import subprocess
 import sys
 
 import chroot
@@ -117,6 +118,13 @@
       help='display contents of a command to the terminal '
       '(default: %(default)s)')
 
+  # Add argument for whether to display command contents to `stdout`.
+  parser.add_argument(
+      '--nocleanup',
+      action='store_false',
+      dest='cleanup',
+      help='Abandon CLs created for bisectoin')
+
   args_output = parser.parse_args()
 
   assert args_output.start_rev < args_output.end_rev, (
@@ -348,6 +356,19 @@
                                 '\n'.join(str(rev) for rev in skip_revisions))
       print(skip_revisions_message)
 
+    if args_output.cleanup:
+      # Abondon all the CLs created for bisection
+      gerrit = os.path.join(args_output.chroot_path, 'chromite/bin/gerrit')
+      for build in bisect_state['jobs']:
+        try:
+          subprocess.check_output([gerrit, 'abandon', build['cl']],
+                                  stderr=subprocess.STDOUT,
+                                  encoding='utf-8')
+        except subprocess.CalledProcessError as err:
+          # the CL may have been abandoned
+          if 'chromite.lib.gob_util.GOBError' not in err.output:
+            raise
+
     return BisectionExitStatus.BISECTION_COMPLETE.value
 
   for rev in revisions:
diff --git a/llvm_tools/llvm_bisection_unittest.py b/llvm_tools/llvm_bisection_unittest.py
index 8478f82..a40770a 100755
--- a/llvm_tools/llvm_bisection_unittest.py
+++ b/llvm_tools/llvm_bisection_unittest.py
@@ -11,6 +11,8 @@
 from __future__ import print_function
 
 import json
+import os
+import subprocess
 import unittest
 import unittest.mock as mock
 
@@ -237,6 +239,7 @@
 
     self.assertEqual(mock_add_tryjob.call_count, 3)
 
+  @mock.patch.object(subprocess, 'check_output', return_value=None)
   @mock.patch.object(
       get_llvm_hash.LLVMHash, 'GetLLVMHash', return_value='a123testhash4')
   @mock.patch.object(llvm_bisection, 'GetCommitsBetween')
@@ -245,7 +248,7 @@
   @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True)
   def testMainPassed(self, mock_outside_chroot, mock_load_status_file,
                      mock_get_range, mock_get_revision_and_hash_list,
-                     _mock_get_bad_llvm_hash):
+                     _mock_get_bad_llvm_hash, mock_abandon_cl):
 
     start = 500
     end = 502
@@ -277,6 +280,7 @@
     args_output.parallel = 3
     args_output.src_path = None
     args_output.chroot_path = 'somepath'
+    args_output.cleanup = True
 
     self.assertEqual(
         llvm_bisection.main(args_output),
@@ -290,6 +294,19 @@
 
     mock_get_revision_and_hash_list.assert_called_once()
 
+    mock_abandon_cl.assert_called_once()
+    self.assertEqual(
+        mock_abandon_cl.call_args,
+        mock.call(
+            [
+                os.path.join(args_output.chroot_path, 'chromite/bin/gerrit'),
+                'abandon',
+                cl,
+            ],
+            stderr=subprocess.STDOUT,
+            encoding='utf-8',
+        ))
+
   @mock.patch.object(llvm_bisection, 'LoadStatusFile')
   @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True)
   def testMainFailedWithInvalidRange(self, mock_outside_chroot,
@@ -430,6 +447,63 @@
 
     mock_get_revision_and_hash_list.assert_called_once()
 
+  @mock.patch.object(subprocess, 'check_output', return_value=None)
+  @mock.patch.object(
+      get_llvm_hash.LLVMHash, 'GetLLVMHash', return_value='a123testhash4')
+  @mock.patch.object(llvm_bisection, 'GetCommitsBetween')
+  @mock.patch.object(llvm_bisection, 'GetRemainingRange')
+  @mock.patch.object(llvm_bisection, 'LoadStatusFile')
+  @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True)
+  def testMainFailedToAbandonCL(self, mock_outside_chroot,
+                                mock_load_status_file, mock_get_range,
+                                mock_get_revision_and_hash_list,
+                                _mock_get_bad_llvm_hash, mock_abandon_cl):
+
+    start = 500
+    end = 502
+
+    bisect_state = {
+        'start': start,
+        'end': end,
+        'jobs': [{
+            'rev': 501,
+            'status': 'bad',
+            'cl': 0
+        }]
+    }
+
+    skip_revisions = {501}
+    pending_revisions = {}
+
+    mock_load_status_file.return_value = bisect_state
+
+    mock_get_range.return_value = (start, end, pending_revisions,
+                                   skip_revisions)
+
+    mock_get_revision_and_hash_list.return_value = ([], [])
+
+    error_message = 'Error message.'
+    mock_abandon_cl.side_effect = subprocess.CalledProcessError(
+        returncode=1, cmd=[], output=error_message)
+
+    args_output = test_helpers.ArgsOutputTest()
+    args_output.start_rev = start
+    args_output.end_rev = end
+    args_output.parallel = 3
+    args_output.src_path = None
+    args_output.cleanup = True
+
+    with self.assertRaises(subprocess.CalledProcessError) as err:
+      llvm_bisection.main(args_output)
+
+    self.assertEqual(err.exception.output, error_message)
+
+    mock_outside_chroot.assert_called_once()
+
+    mock_load_status_file.assert_called_once()
+
+    mock_get_range.assert_called_once()
+
 
 if __name__ == '__main__':
   unittest.main()