diff options
author | Jian Cai <jiancai@google.com> | 2020-11-19 13:50:24 -0800 |
---|---|---|
committer | Jian Cai <jiancai@google.com> | 2021-04-28 20:56:35 +0000 |
commit | 4a12a1285b9992b158afbbc79c91244da4b7cc4b (patch) | |
tree | 430d3827c9bed2a915aedbf80b455a6ae43014d5 | |
parent | b5bce2002ee5b944297c7d6c217e04a164f1aaae (diff) | |
download | toolchain-utils-4a12a1285b9992b158afbbc79c91244da4b7cc4b.tar.gz |
llvm_tools: refactor automatic LLVM bisection
This removes update_all_tryjobs_with_auto.py and move a function to its
only user module. Also stopped raising errors when a build is not ready
for "cros buildresult" to avoid confusion.
BUG=chromium:1151055
TEST=local and CQ tests.
Change-Id: I1591d17f1fe76cf6fb223c18ab0c96349982f53c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/2551918
Tested-by: Jian Cai <jiancai@google.com>
Reviewed-by: Manoj Gupta <manojgupta@chromium.org>
-rw-r--r-- | llvm_tools/README.md | 24 | ||||
-rwxr-xr-x | llvm_tools/auto_llvm_bisection.py | 100 | ||||
-rwxr-xr-x | llvm_tools/auto_llvm_bisection_unittest.py | 304 | ||||
-rwxr-xr-x | llvm_tools/update_all_tryjobs_with_auto.py | 80 | ||||
-rwxr-xr-x | llvm_tools/update_tryjob_status.py | 79 | ||||
-rwxr-xr-x | llvm_tools/update_tryjob_status_unittest.py | 214 |
6 files changed, 284 insertions, 517 deletions
diff --git a/llvm_tools/README.md b/llvm_tools/README.md index 8eff98ba..fabb37a9 100644 --- a/llvm_tools/README.md +++ b/llvm_tools/README.md @@ -360,30 +360,6 @@ $ ./update_tryjob_status.py \ --custom_script /abs/path/to/script.py ``` -### `update_all_tryjobs_with_auto.py` - -#### Usage - -This script updates all tryjobs that are 'pending' to the result provided by -`cros buildresult`. - -For example: - -``` -$ ./update_all_tryjobs_with_auto.py \ - --last_tested /abs/path/to/last_tested_file.json \ - --chroot_path /abs/path/to/chroot -``` - -The above example will update all tryjobs whose 'status' is 'pending' in the -file provided by `--last_tested`. - -For help with the command line arguments of the script, run: - -``` -$ ./update_all_tryjobs_with_auto.py --help -``` - ### `modify_a_tryjob.py` #### Usage diff --git a/llvm_tools/auto_llvm_bisection.py b/llvm_tools/auto_llvm_bisection.py index dd29cf4f..7e8fb1dd 100755 --- a/llvm_tools/auto_llvm_bisection.py +++ b/llvm_tools/auto_llvm_bisection.py @@ -8,6 +8,8 @@ from __future__ import print_function +import enum +import json import os import subprocess import sys @@ -17,7 +19,7 @@ import traceback import chroot from llvm_bisection import BisectionExitStatus import llvm_bisection -from update_all_tryjobs_with_auto import GetPathToUpdateAllTryjobsWithAutoScript +import update_tryjob_status # Used to re-try for 'llvm_bisection.py' to attempt to launch more tryjobs. BISECTION_RETRY_TIME_SECS = 10 * 60 @@ -39,6 +41,52 @@ BISECTION_ATTEMPTS = 3 POLLING_LIMIT_SECS = 18 * 60 * 60 +class BuilderStatus(enum.Enum): + """Actual values given via 'cros buildresult'.""" + + PASS = 'pass' + FAIL = 'fail' + RUNNING = 'running' + + +builder_status_mapping = { + BuilderStatus.PASS.value: update_tryjob_status.TryjobStatus.GOOD.value, + BuilderStatus.FAIL.value: update_tryjob_status.TryjobStatus.BAD.value, + BuilderStatus.RUNNING.value: update_tryjob_status.TryjobStatus.PENDING.value +} + + +def GetBuildResult(chroot_path, buildbucket_id): + """Returns the conversion of the result of 'cros buildresult'.""" + + # Calls 'cros buildresult' to get the status of the tryjob. + try: + tryjob_json = subprocess.check_output( + [ + 'cros_sdk', '--', 'cros', 'buildresult', '--buildbucket-id', + str(buildbucket_id), '--report', 'json' + ], + cwd=chroot_path, + stderr=subprocess.STDOUT, + encoding='UTF-8', + ) + except subprocess.CalledProcessError as err: + if 'No build found. Perhaps not started' not in err.output: + raise + return None + + tryjob_content = json.loads(tryjob_json) + + build_result = str(tryjob_content['%d' % buildbucket_id]['status']) + + # The string returned by 'cros buildresult' might not be in the mapping. + if build_result not in builder_status_mapping: + raise ValueError('"cros buildresult" return value is invalid: %s' % + build_result) + + return builder_status_mapping[build_result] + + def main(): """Bisects LLVM using the result of `cros buildresult` of each tryjob. @@ -50,51 +98,58 @@ def main(): args_output = llvm_bisection.GetCommandLineArgs() - exec_update_tryjobs = [ - GetPathToUpdateAllTryjobsWithAutoScript(), '--chroot_path', - args_output.chroot_path, '--last_tested', args_output.last_tested - ] - if os.path.isfile(args_output.last_tested): print('Resuming bisection for %s' % args_output.last_tested) else: print('Starting a new bisection for %s' % args_output.last_tested) while True: + # Update the status of existing tryjobs if os.path.isfile(args_output.last_tested): update_start_time = time.time() - - # Update all tryjobs whose status is 'pending' to the result of `cros - # buildresult`. + with open(args_output.last_tested) as json_file: + json_dict = json.load(json_file) while True: print('\nAttempting to update all tryjobs whose "status" is ' '"pending":') print('-' * 40) - update_ret = subprocess.call(exec_update_tryjobs) + completed = True + for tryjob in json_dict['jobs']: + if tryjob[ + 'status'] == update_tryjob_status.TryjobStatus.PENDING.value: + status = GetBuildResult(args_output.chroot_path, + tryjob['buildbucket_id']) + if status: + tryjob['status'] = status + else: + completed = False print('-' * 40) - # Successfully updated all tryjobs whose 'status' was 'pending'/ no - # updates were needed (all tryjobs already have been updated). - if update_ret == 0: + # Proceed to the next step if all the existing tryjobs have completed. + if completed: break delta_time = time.time() - update_start_time if delta_time > POLLING_LIMIT_SECS: - print('Unable to update tryjobs whose status is "pending" to ' - 'the result of `cros buildresult`.') - # Something is wrong with updating the tryjobs's 'status' via # `cros buildresult` (e.g. network issue, etc.). - sys.exit(1) + sys.exit('Failed to update pending tryjobs.') + print('-' * 40) print('Sleeping for %d minutes.' % (POLL_RETRY_TIME_SECS // 60)) time.sleep(POLL_RETRY_TIME_SECS) - # Launch more tryjobs if possible to narrow down the bad commit/revision or - # terminate the bisection because the bad commit/revision was found. + # There should always be update from the tryjobs launched in the + # last iteration. + temp_filename = '%s.new' % args_output.last_tested + with open(temp_filename, 'w') as temp_file: + json.dump(json_dict, temp_file, indent=4, separators=(',', ': ')) + os.rename(temp_filename, args_output.last_tested) + + # Launch more tryjobs. for cur_try in range(1, BISECTION_ATTEMPTS + 1): try: print('\nAttempting to launch more tryjobs if possible:') @@ -104,8 +159,7 @@ def main(): print('-' * 40) - # Exit code 126 means that there are no more revisions to test between - # 'start' and 'end', so bisection is complete. + # Stop if the bisection has completed. if bisection_ret == BisectionExitStatus.BISECTION_COMPLETE.value: sys.exit(0) @@ -118,9 +172,7 @@ def main(): # Exceeded the number of times to launch more tryjobs. if cur_try == BISECTION_ATTEMPTS: - print('Unable to continue bisection.') - - sys.exit(1) + sys.exit('Unable to continue bisection.') num_retries_left = BISECTION_ATTEMPTS - cur_try diff --git a/llvm_tools/auto_llvm_bisection_unittest.py b/llvm_tools/auto_llvm_bisection_unittest.py index 56b556e6..07c0e715 100755 --- a/llvm_tools/auto_llvm_bisection_unittest.py +++ b/llvm_tools/auto_llvm_bisection_unittest.py @@ -8,6 +8,7 @@ from __future__ import print_function +import json import os import subprocess import time @@ -19,187 +20,128 @@ import auto_llvm_bisection import chroot import llvm_bisection import test_helpers +import update_tryjob_status class AutoLLVMBisectionTest(unittest.TestCase): """Unittests for auto bisection of LLVM.""" - # Simulate the behavior of `VerifyOutsideChroot()` when successfully invoking - # the script outside of the chroot. @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True) - # Simulate behavior of `time.sleep()` when waiting for errors to settle caused - # by `llvm_bisection.main()` (e.g. network issue, etc.). - @mock.patch.object(time, 'sleep') - # Simulate behavior of `traceback.print_exc()` when an exception happened in - # `llvm_bisection.main()`. - @mock.patch.object(traceback, 'print_exc') - # Simulate behavior of `llvm_bisection.main()` when failed to launch tryjobs - # (exception happened along the way, etc.). - @mock.patch.object(llvm_bisection, 'main') - # Simulate behavior of `os.path.isfile()` when starting a new bisection. - @mock.patch.object(os.path, 'isfile', return_value=False) - # Simulate behavior of `GetPathToUpdateAllTryjobsWithAutoScript()` when - # returning the absolute path to that script that updates all 'pending' - # tryjobs to the result of `cros buildresult`. - @mock.patch.object( - auto_llvm_bisection, - 'GetPathToUpdateAllTryjobsWithAutoScript', - return_value='/abs/path/to/update_tryjob.py') - # Simulate `llvm_bisection.GetCommandLineArgs()` when parsing the command line - # arguments required by the bisection script. @mock.patch.object( llvm_bisection, 'GetCommandLineArgs', return_value=test_helpers.ArgsOutputTest()) - def testFailedToStartBisection( - self, mock_get_args, mock_get_auto_script, mock_is_file, - mock_llvm_bisection, mock_traceback, mock_sleep, mock_outside_chroot): - - def MockLLVMBisectionRaisesException(_args_output): - raise ValueError('Failed to launch more tryjobs.') - - # Use the test function to simulate the behavior of an exception happening - # when launching more tryjobs. - mock_llvm_bisection.side_effect = MockLLVMBisectionRaisesException + @mock.patch.object(time, 'sleep') + @mock.patch.object(traceback, 'print_exc') + @mock.patch.object(llvm_bisection, 'main') + @mock.patch.object(os.path, 'isfile') + @mock.patch.object(auto_llvm_bisection, 'open') + @mock.patch.object(json, 'load') + @mock.patch.object(auto_llvm_bisection, 'GetBuildResult') + @mock.patch.object(os, 'rename') + def testAutoLLVMBisectionPassed( + self, + # pylint: disable=unused-argument + mock_rename, + mock_get_build_result, + mock_json_load, + # pylint: disable=unused-argument + mock_open, + mock_isfile, + mock_llvm_bisection, + mock_traceback, + mock_sleep, + mock_get_args, + mock_outside_chroot): + + mock_isfile.side_effect = [False, False, True, True] + mock_llvm_bisection.side_effect = [ + 0, + ValueError('Failed to launch more tryjobs.'), + llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value + ] + mock_json_load.return_value = { + 'start': + 369410, + 'end': + 369420, + 'jobs': [{ + 'buildbucket_id': 12345, + 'rev': 369411, + 'status': update_tryjob_status.TryjobStatus.PENDING.value, + }] + } + mock_get_build_result.return_value = ( + update_tryjob_status.TryjobStatus.GOOD.value) - # Verify the exception is raised when the number of attempts to launched - # more tryjobs is exceeded, so unable to continue - # bisection. + # Verify the excpetion is raised when successfully found the bad revision. + # Uses `sys.exit(0)` to indicate success. with self.assertRaises(SystemExit) as err: auto_llvm_bisection.main() - self.assertEqual(err.exception.code, 1) + self.assertEqual(err.exception.code, 0) mock_outside_chroot.assert_called_once() mock_get_args.assert_called_once() - mock_get_auto_script.assert_called_once() - self.assertEqual(mock_is_file.call_count, 2) + self.assertEqual(mock_isfile.call_count, 3) self.assertEqual(mock_llvm_bisection.call_count, 3) - self.assertEqual(mock_traceback.call_count, 3) - self.assertEqual(mock_sleep.call_count, 2) + mock_traceback.assert_called_once() + mock_sleep.assert_called_once() - # Simulate the behavior of `subprocess.call()` when successfully updated all - # tryjobs whose 'status' value is 'pending'. - @mock.patch.object(subprocess, 'call', return_value=0) - # Simulate the behavior of `VerifyOutsideChroot()` when successfully invoking - # the script outside of the chroot. @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True) - # Simulate behavior of `time.sleep()` when waiting for errors to settle caused - # by `llvm_bisection.main()` (e.g. network issue, etc.). @mock.patch.object(time, 'sleep') - # Simulate behavior of `traceback.print_exc()` when an exception happened in - # `llvm_bisection.main()`. @mock.patch.object(traceback, 'print_exc') - # Simulate behavior of `llvm_bisection.main()` when failed to launch tryjobs - # (exception happened along the way, etc.). @mock.patch.object(llvm_bisection, 'main') - # Simulate behavior of `os.path.isfile()` when starting a new bisection. @mock.patch.object(os.path, 'isfile') - # Simulate behavior of `GetPathToUpdateAllTryjobsWithAutoScript()` when - # returning the absolute path to that script that updates all 'pending' - # tryjobs to the result of `cros buildresult`. - @mock.patch.object( - auto_llvm_bisection, - 'GetPathToUpdateAllTryjobsWithAutoScript', - return_value='/abs/path/to/update_tryjob.py') - # Simulate `llvm_bisection.GetCommandLineArgs()` when parsing the command line - # arguments required by the bisection script. @mock.patch.object( llvm_bisection, 'GetCommandLineArgs', return_value=test_helpers.ArgsOutputTest()) - def testSuccessfullyBisectedLLVMRevision( - self, mock_get_args, mock_get_auto_script, mock_is_file, - mock_llvm_bisection, mock_traceback, mock_sleep, mock_outside_chroot, - mock_update_tryjobs): - - # Simulate the behavior of `os.path.isfile()` when checking whether the - # status file provided exists. - @test_helpers.CallCountsToMockFunctions - def MockStatusFileCheck(call_count, _last_tested): - # Simulate that the status file does not exist, so the LLVM bisection - # script would create the status file and launch tryjobs. - if call_count < 2: - return False - - # Simulate when the status file exists and `subprocess.call()` executes - # the script that updates all the 'pending' tryjobs to the result of `cros - # buildresult`. - if call_count == 2: - return True - - assert False, 'os.path.isfile() called more times than expected.' - - # Simulate behavior of `llvm_bisection.main()` when successfully bisected - # between the good and bad LLVM revision. - @test_helpers.CallCountsToMockFunctions - def MockLLVMBisectionReturnValue(call_count, _args_output): - # Simulate that successfully launched more tryjobs. - if call_count == 0: - return 0 - - # Simulate that failed to launch more tryjobs. - if call_count == 1: - raise ValueError('Failed to launch more tryjobs.') - - # Simulate that the bad revision has been found. - if call_count == 2: - return llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value + def testFailedToStartBisection(self, mock_get_args, mock_isfile, + mock_llvm_bisection, mock_traceback, + mock_sleep, mock_outside_chroot): - assert False, 'Called `llvm_bisection.main()` more than expected.' + mock_isfile.return_value = False + mock_llvm_bisection.side_effect = ValueError( + 'Failed to launch more tryjobs.') - # Use the test function to simulate the behavior of `llvm_bisection.main()`. - mock_llvm_bisection.side_effect = MockLLVMBisectionReturnValue - - # Use the test function to simulate the behavior of `os.path.isfile()`. - mock_is_file.side_effect = MockStatusFileCheck - - # Verify the excpetion is raised when successfully found the bad revision. - # Uses `sys.exit(0)` to indicate success. + # Verify the exception is raised when the number of attempts to launched + # more tryjobs is exceeded, so unable to continue + # bisection. with self.assertRaises(SystemExit) as err: auto_llvm_bisection.main() - self.assertEqual(err.exception.code, 0) + self.assertEqual(err.exception.code, 'Unable to continue bisection.') mock_outside_chroot.assert_called_once() mock_get_args.assert_called_once() - mock_get_auto_script.assert_called_once() - self.assertEqual(mock_is_file.call_count, 3) + self.assertEqual(mock_isfile.call_count, 2) self.assertEqual(mock_llvm_bisection.call_count, 3) - mock_traceback.assert_called_once() - mock_sleep.assert_called_once() - mock_update_tryjobs.assert_called_once() + self.assertEqual(mock_traceback.call_count, 3) + self.assertEqual(mock_sleep.call_count, 2) - # Simulate behavior of `subprocess.call()` when failed to update tryjobs to - # `cros buildresult` (script failed). - @mock.patch.object(subprocess, 'call', return_value=1) - # Simulate behavior of `time.time()` when determining the time passed when - # updating tryjobs whose 'status' is 'pending'. - @mock.patch.object(time, 'time') - # Simulate the behavior of `VerifyOutsideChroot()` when successfully invoking - # the script outside of the chroot. @mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True) - # Simulate behavior of `time.sleep()` when waiting for errors to settle caused - # by `llvm_bisection.main()` (e.g. network issue, etc.). - @mock.patch.object(time, 'sleep') - # Simulate behavior of `traceback.print_exc()` when resuming bisection. - @mock.patch.object(os.path, 'isfile', return_value=True) - # Simulate behavior of `GetPathToUpdateAllTryjobsWithAutoScript()` when - # returning the absolute path to that script that updates all 'pending' - # tryjobs to the result of `cros buildresult`. - @mock.patch.object( - auto_llvm_bisection, - 'GetPathToUpdateAllTryjobsWithAutoScript', - return_value='/abs/path/to/update_tryjob.py') - # Simulate `llvm_bisection.GetCommandLineArgs()` when parsing the command line - # arguments required by the bisection script. @mock.patch.object( llvm_bisection, 'GetCommandLineArgs', return_value=test_helpers.ArgsOutputTest()) + @mock.patch.object(time, 'time') + @mock.patch.object(time, 'sleep') + @mock.patch.object(os.path, 'isfile') + @mock.patch.object(auto_llvm_bisection, 'open') + @mock.patch.object(json, 'load') + @mock.patch.object(auto_llvm_bisection, 'GetBuildResult') def testFailedToUpdatePendingTryJobs( - self, mock_get_args, mock_get_auto_script, mock_is_file, mock_sleep, - mock_outside_chroot, mock_time, mock_update_tryjobs): + self, + mock_get_build_result, + mock_json_load, + # pylint: disable=unused-argument + mock_open, + mock_isfile, + mock_sleep, + mock_time, + mock_get_args, + mock_outside_chroot): # Simulate behavior of `time.time()` for time passed. @test_helpers.CallCountsToMockFunctions @@ -209,9 +151,20 @@ class AutoLLVMBisectionTest(unittest.TestCase): assert False, 'Called `time.time()` more than expected.' - # Use the test function to simulate the behavior of `time.time()`. + mock_isfile.return_value = True + mock_json_load.return_value = { + 'start': + 369410, + 'end': + 369420, + 'jobs': [{ + 'buildbucket_id': 12345, + 'rev': 369411, + 'status': update_tryjob_status.TryjobStatus.PENDING.value, + }] + } + mock_get_build_result.return_value = None mock_time.side_effect = MockTimePassed - # Reduce the polling limit for the test case to terminate faster. auto_llvm_bisection.POLLING_LIMIT_SECS = 1 @@ -220,15 +173,80 @@ class AutoLLVMBisectionTest(unittest.TestCase): with self.assertRaises(SystemExit) as err: auto_llvm_bisection.main() - self.assertEqual(err.exception.code, 1) + self.assertEqual(err.exception.code, 'Failed to update pending tryjobs.') mock_outside_chroot.assert_called_once() mock_get_args.assert_called_once() - mock_get_auto_script.assert_called_once() - self.assertEqual(mock_is_file.call_count, 2) + self.assertEqual(mock_isfile.call_count, 2) mock_sleep.assert_called_once() self.assertEqual(mock_time.call_count, 3) - self.assertEqual(mock_update_tryjobs.call_count, 2) + + @mock.patch.object(subprocess, 'check_output') + def testGetBuildResult(self, mock_chroot_command): + buildbucket_id = 192 + status = auto_llvm_bisection.BuilderStatus.PASS.value + tryjob_contents = {buildbucket_id: {'status': status}} + mock_chroot_command.return_value = json.dumps(tryjob_contents) + chroot_path = '/some/path/to/chroot' + + self.assertEqual( + auto_llvm_bisection.GetBuildResult(chroot_path, buildbucket_id), + update_tryjob_status.TryjobStatus.GOOD.value) + + mock_chroot_command.assert_called_once_with( + [ + 'cros_sdk', '--', 'cros', 'buildresult', '--buildbucket-id', + str(buildbucket_id), '--report', 'json' + ], + cwd='/some/path/to/chroot', + stderr=subprocess.STDOUT, + encoding='UTF-8', + ) + + @mock.patch.object(subprocess, 'check_output') + def testGetBuildResultPassedWithUnstartedTryjob(self, mock_chroot_command): + buildbucket_id = 192 + chroot_path = '/some/path/to/chroot' + mock_chroot_command.side_effect = subprocess.CalledProcessError( + returncode=1, cmd=[], output='No build found. Perhaps not started') + auto_llvm_bisection.GetBuildResult(chroot_path, buildbucket_id) + mock_chroot_command.assert_called_once_with( + [ + 'cros_sdk', '--', 'cros', 'buildresult', '--buildbucket-id', '192', + '--report', 'json' + ], + cwd=chroot_path, + stderr=subprocess.STDOUT, + encoding='UTF-8', + ) + + @mock.patch.object(subprocess, 'check_output') + def testGetBuildReusultFailedWithInvalidBuildStatus(self, + mock_chroot_command): + chroot_path = '/some/path/to/chroot' + buildbucket_id = 50 + invalid_build_status = 'querying' + tryjob_contents = {buildbucket_id: {'status': invalid_build_status}} + mock_chroot_command.return_value = json.dumps(tryjob_contents) + + # Verify the exception is raised when the return value of `cros buildresult` + # is not in the `builder_status_mapping`. + with self.assertRaises(ValueError) as err: + auto_llvm_bisection.GetBuildResult(chroot_path, buildbucket_id) + + self.assertEqual( + str(err.exception), + '"cros buildresult" return value is invalid: %s' % invalid_build_status) + + mock_chroot_command.assert_called_once_with( + [ + 'cros_sdk', '--', 'cros', 'buildresult', '--buildbucket-id', + str(buildbucket_id), '--report', 'json' + ], + cwd=chroot_path, + stderr=subprocess.STDOUT, + encoding='UTF-8', + ) if __name__ == '__main__': diff --git a/llvm_tools/update_all_tryjobs_with_auto.py b/llvm_tools/update_all_tryjobs_with_auto.py deleted file mode 100755 index 11e67ed4..00000000 --- a/llvm_tools/update_all_tryjobs_with_auto.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright 2019 The Chromium OS Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -"""Updates the status of all tryjobs to the result of `cros buildresult`.""" - -from __future__ import print_function - -import argparse -import json -import os - -import chroot -import update_tryjob_status - - -def GetPathToUpdateAllTryjobsWithAutoScript(): - """Returns the absolute path to this script.""" - - return os.path.abspath(__file__) - - -def GetCommandLineArgs(): - """Parses the command line for the command line arguments.""" - - # Default absoute path to the chroot if not specified. - cros_root = os.path.expanduser('~') - cros_root = os.path.join(cros_root, 'chromiumos') - - # Create parser and add optional command-line arguments. - parser = argparse.ArgumentParser(description=__doc__) - - # Add argument for the JSON file to use for the update of a tryjob. - parser.add_argument( - '--last_tested', - required=True, - help='The absolute path to the JSON file that contains the tryjobs used ' - 'for bisecting LLVM.') - - # Add argument for a specific chroot path. - parser.add_argument( - '--chroot_path', - default=cros_root, - help='the path to the chroot (default: %(default)s)') - - args_output = parser.parse_args() - - if not os.path.isfile(args_output.last_tested) or \ - not args_output.last_tested.endswith('.json'): - raise ValueError('File does not exist or does not ending in ".json" ' - ': %s' % args_output.last_tested) - - return args_output - - -def main(): - """Updates the status of a tryjob.""" - - chroot.VerifyOutsideChroot() - - args_output = GetCommandLineArgs() - - with open(args_output.last_tested) as tryjobs: - bisect_contents = json.load(tryjobs) - - for tryjob in bisect_contents['jobs']: - if tryjob['status'] == update_tryjob_status.TryjobStatus.PENDING.value: - tryjob['status'] = update_tryjob_status.GetAutoResult( - args_output.chroot_path, tryjob['buildbucket_id']) - - new_file = '%s.new' % args_output.last_tested - with open(new_file, 'w') as update_tryjobs: - json.dump(bisect_contents, update_tryjobs, indent=4, separators=(',', ': ')) - os.rename(new_file, args_output.last_tested) - - -if __name__ == '__main__': - main() diff --git a/llvm_tools/update_tryjob_status.py b/llvm_tools/update_tryjob_status.py index f1500364..f25fadca 100755 --- a/llvm_tools/update_tryjob_status.py +++ b/llvm_tools/update_tryjob_status.py @@ -16,7 +16,6 @@ import subprocess import sys import chroot -from subprocess_helpers import ChrootRunCommand from test_helpers import CreateTemporaryJsonFile @@ -32,17 +31,6 @@ class TryjobStatus(enum.Enum): # determines the 'status' value of the tryjob). CUSTOM_SCRIPT = 'custom_script' - # Uses the result returned by 'cros buildresult'. - AUTO = 'auto' - - -class BuilderStatus(enum.Enum): - """Actual values given via 'cros buildresult'.""" - - PASS = 'pass' - FAIL = 'fail' - RUNNING = 'running' - class CustomScriptStatus(enum.Enum): """Exit code values of a custom script.""" @@ -66,12 +54,6 @@ custom_script_exit_value_mapping = { CustomScriptStatus.SKIP.value: TryjobStatus.SKIP.value } -builder_status_mapping = { - BuilderStatus.PASS.value: TryjobStatus.GOOD.value, - BuilderStatus.FAIL.value: TryjobStatus.BAD.value, - BuilderStatus.RUNNING.value: TryjobStatus.PENDING.value -} - def GetCommandLineArgs(): """Parses the command line for the command line arguments.""" @@ -106,12 +88,6 @@ def GetCommandLineArgs(): type=int, help='The revision to set its status.') - # Add argument for a specific chroot path. - parser.add_argument( - '--chroot_path', - default=cros_root, - help='the path to the chroot (default: %(default)s)') - # Add argument for the custom script to execute for the 'custom_script' # option in '--set_status'. parser.add_argument( @@ -123,13 +99,14 @@ def GetCommandLineArgs(): args_output = parser.parse_args() - if not os.path.isfile(args_output.status_file) or \ - not args_output.status_file.endswith('.json'): + if not (os.path.isfile( + args_output.status_file and + not args_output.status_file.endswith('.json'))): raise ValueError('File does not exist or does not ending in ".json" ' ': %s' % args_output.status_file) - if args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value and \ - not args_output.custom_script: + if (args_output.set_status == TryjobStatus.CUSTOM_SCRIPT.value and + not args_output.custom_script): raise ValueError('Please provide the absolute path to the script to ' 'execute.') @@ -165,35 +142,6 @@ def FindTryjobIndex(revision, tryjobs_list): return None -def GetStatusFromCrosBuildResult(chroot_path, buildbucket_id): - """Retrieves the 'status' using 'cros buildresult'.""" - - get_buildbucket_id_cmd = [ - 'cros', 'buildresult', '--buildbucket-id', - str(buildbucket_id), '--report', 'json' - ] - - tryjob_json = ChrootRunCommand(chroot_path, get_buildbucket_id_cmd) - - tryjob_contents = json.loads(tryjob_json) - - return str(tryjob_contents['%d' % buildbucket_id]['status']) - - -def GetAutoResult(chroot_path, buildbucket_id): - """Returns the conversion of the result of 'cros buildresult'.""" - - # Calls 'cros buildresult' to get the status of the tryjob. - build_result = GetStatusFromCrosBuildResult(chroot_path, buildbucket_id) - - # The string returned by 'cros buildresult' might not be in the mapping. - if build_result not in builder_status_mapping: - raise ValueError( - '"cros buildresult" return value is invalid: %s' % build_result) - - return builder_status_mapping[build_result] - - def GetCustomScriptResult(custom_script, status_file, tryjob_contents): """Returns the conversion of the exit code of the custom script. @@ -245,18 +193,15 @@ def GetCustomScriptResult(custom_script, status_file, tryjob_contents): return custom_script_exit_value_mapping[exec_script_cmd_obj.returncode] -def UpdateTryjobStatus(revision, set_status, status_file, chroot_path, - custom_script): +def UpdateTryjobStatus(revision, set_status, status_file, custom_script): """Updates a tryjob's 'status' field based off of 'set_status'. Args: revision: The revision associated with the tryjob. set_status: What to update the 'status' field to. Ex: TryjobStatus.Good, TryjobStatus.BAD, TryjobStatus.PENDING, or - TryjobStatus.AUTO where TryjobStatus.AUTO uses the result of - 'cros buildresult'. + TryjobStatus. status_file: The .JSON file that contains the tryjobs. - chroot_path: The absolute path to the chroot (used by 'cros buildresult'). custom_script: The absolute path to a script that will be executed which will determine the 'status' value of the tryjob. """ @@ -282,8 +227,8 @@ def UpdateTryjobStatus(revision, set_status, status_file, chroot_path, # 'FindTryjobIndex()' returns None if the revision was not found. if tryjob_index is None: - raise ValueError( - 'Unable to find tryjob for %d in %s' % (revision, status_file)) + raise ValueError('Unable to find tryjob for %d in %s' % + (revision, status_file)) # Set 'status' depending on 'set_status' for the tryjob. if set_status == TryjobStatus.GOOD: @@ -292,9 +237,6 @@ def UpdateTryjobStatus(revision, set_status, status_file, chroot_path, bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.BAD.value elif set_status == TryjobStatus.PENDING: bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.PENDING.value - elif set_status == TryjobStatus.AUTO: - bisect_contents['jobs'][tryjob_index]['status'] = GetAutoResult( - chroot_path, bisect_contents['jobs'][tryjob_index]['buildbucket_id']) elif set_status == TryjobStatus.SKIP: bisect_contents['jobs'][tryjob_index]['status'] = TryjobStatus.SKIP.value elif set_status == TryjobStatus.CUSTOM_SCRIPT: @@ -315,8 +257,7 @@ def main(): args_output = GetCommandLineArgs() UpdateTryjobStatus(args_output.revision, TryjobStatus(args_output.set_status), - args_output.status_file, args_output.chroot_path, - args_output.custom_script) + args_output.status_file, args_output.custom_script) if __name__ == '__main__': diff --git a/llvm_tools/update_tryjob_status_unittest.py b/llvm_tools/update_tryjob_status_unittest.py index b5e6556c..c42c6718 100755 --- a/llvm_tools/update_tryjob_status_unittest.py +++ b/llvm_tools/update_tryjob_status_unittest.py @@ -31,14 +31,13 @@ class UpdateTryjobStatusTest(unittest.TestCase): 'cl': 'https://some_link_to_tryjob.com', 'status': 'good', 'buildbucket_id': 91835 - }, - { - 'rev': 1000, - 'url': 'https://some_url_to_CL.com', - 'cl': 'https://some_link_to_tryjob.com', - 'status': 'pending', - 'buildbucket_id': 10931 - }] + }, { + 'rev': 1000, + 'url': 'https://some_url_to_CL.com', + 'cl': 'https://some_link_to_tryjob.com', + 'status': 'pending', + 'buildbucket_id': 10931 + }] expected_index = 0 @@ -55,87 +54,19 @@ class UpdateTryjobStatusTest(unittest.TestCase): 'cl': 'https://some_link_to_tryjob.com', 'status': 'bad', 'buildbucket_id': 390 - }, - { - 'rev': 10, - 'url': 'https://some_url_to_CL.com', - 'cl': 'https://some_link_to_tryjob.com', - 'status': 'skip', - 'buildbucket_id': 10 - }] + }, { + 'rev': 10, + 'url': 'https://some_url_to_CL.com', + 'cl': 'https://some_link_to_tryjob.com', + 'status': 'skip', + 'buildbucket_id': 10 + }] revision_to_find = 250 self.assertIsNone( update_tryjob_status.FindTryjobIndex(revision_to_find, test_tryjobs)) - # Simulate the behavior of `ChrootRunCommand()` when executing a command - # inside the chroot. - @mock.patch.object(update_tryjob_status, 'ChrootRunCommand') - def testGetStatusFromCrosBuildResult(self, mock_chroot_command): - tryjob_contents = { - '192': { - 'status': 'good', - 'CleanUpChroot': 'pass', - 'artifacts_url': None - } - } - - # Use the test function to simulate 'ChrootRunCommand()' behavior. - mock_chroot_command.return_value = json.dumps(tryjob_contents) - - buildbucket_id = 192 - - chroot_path = '/some/path/to/chroot' - - self.assertEqual( - update_tryjob_status.GetStatusFromCrosBuildResult( - chroot_path, buildbucket_id), 'good') - - expected_cmd = [ - 'cros', 'buildresult', '--buildbucket-id', - str(buildbucket_id), '--report', 'json' - ] - - mock_chroot_command.assert_called_once_with(chroot_path, expected_cmd) - - # Simulate the behavior of `GetStatusFromCrosBuildResult()` when `cros - # buildresult` returned a string that is not in the mapping. - @mock.patch.object( - update_tryjob_status, - 'GetStatusFromCrosBuildResult', - return_value='querying') - def testInvalidCrosBuildResultValue(self, mock_cros_buildresult): - chroot_path = '/some/path/to/chroot' - buildbucket_id = 50 - - # Verify the exception is raised when the return value of `cros buildresult` - # is not in the `builder_status_mapping`. - with self.assertRaises(ValueError) as err: - update_tryjob_status.GetAutoResult(chroot_path, buildbucket_id) - - self.assertEqual( - str(err.exception), - '"cros buildresult" return value is invalid: querying') - - mock_cros_buildresult.assert_called_once_with(chroot_path, buildbucket_id) - - # Simulate the behavior of `GetStatusFromCrosBuildResult()` when `cros - # buildresult` returned a string that is in the mapping. - @mock.patch.object( - update_tryjob_status, - 'GetStatusFromCrosBuildResult', - return_value=update_tryjob_status.BuilderStatus.PASS.value) - def testValidCrosBuildResultValue(self, mock_cros_buildresult): - chroot_path = '/some/path/to/chroot' - buildbucket_id = 100 - - self.assertEqual( - update_tryjob_status.GetAutoResult(chroot_path, buildbucket_id), - TryjobStatus.GOOD.value) - - mock_cros_buildresult.assert_called_once_with(chroot_path, buildbucket_id) - @mock.patch.object(subprocess, 'Popen') # Simulate the behavior of `os.rename()` when successfully renamed a file. @mock.patch.object(os, 'rename', return_value=None) @@ -186,8 +117,9 @@ class UpdateTryjobStatusTest(unittest.TestCase): # does not match any of the exit codes in the mapping of # `custom_script_exit_value_mapping`. with self.assertRaises(ValueError) as err: - update_tryjob_status.GetCustomScriptResult( - custom_script_path, status_file_path, tryjob_contents) + update_tryjob_status.GetCustomScriptResult(custom_script_path, + status_file_path, + tryjob_contents) self.assertEqual(str(err.exception), expected_error_message) @@ -212,8 +144,8 @@ class UpdateTryjobStatusTest(unittest.TestCase): # `Popen.communicate()` returns a tuple of `stdout` and `stderr`. mock_exec_custom_script.return_value.communicate.return_value = (None, None) - mock_exec_custom_script.return_value.returncode = \ - CustomScriptStatus.GOOD.value + mock_exec_custom_script.return_value.returncode = ( + CustomScriptStatus.GOOD.value) tryjob_contents = { 'status': 'good', @@ -226,8 +158,9 @@ class UpdateTryjobStatusTest(unittest.TestCase): status_file_path = '/abs/path/to/status_file.json' self.assertEqual( - update_tryjob_status.GetCustomScriptResult( - custom_script_path, status_file_path, tryjob_contents), + update_tryjob_status.GetCustomScriptResult(custom_script_path, + status_file_path, + tryjob_contents), TryjobStatus.GOOD.value) mock_exec_custom_script.assert_called_once() @@ -247,16 +180,14 @@ class UpdateTryjobStatusTest(unittest.TestCase): revision_to_update = 369412 - chroot_path = '/abs/path/to/chroot' - custom_script = None # Verify the exception is raised when the `status_file` does not have any # `jobs` (empty). with self.assertRaises(SystemExit) as err: - update_tryjob_status.UpdateTryjobStatus( - revision_to_update, TryjobStatus.GOOD, temp_json_file, chroot_path, - custom_script) + update_tryjob_status.UpdateTryjobStatus(revision_to_update, + TryjobStatus.GOOD, + temp_json_file, custom_script) self.assertEqual(str(err.exception), 'No tryjobs in %s' % temp_json_file) @@ -283,16 +214,14 @@ class UpdateTryjobStatusTest(unittest.TestCase): revision_to_update = 369416 - chroot_path = '/abs/path/to/chroot' - custom_script = None # Verify the exception is raised when the `status_file` does not have any # `jobs` (empty). with self.assertRaises(ValueError) as err: - update_tryjob_status.UpdateTryjobStatus( - revision_to_update, TryjobStatus.SKIP, temp_json_file, chroot_path, - custom_script) + update_tryjob_status.UpdateTryjobStatus(revision_to_update, + TryjobStatus.SKIP, + temp_json_file, custom_script) self.assertEqual( str(err.exception), 'Unable to find tryjob for %d in %s' % @@ -324,13 +253,11 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Index of the tryjob that is going to have its 'status' value updated. tryjob_index = 0 - chroot_path = '/abs/path/to/chroot' - custom_script = None update_tryjob_status.UpdateTryjobStatus(revision_to_update, TryjobStatus.GOOD, temp_json_file, - chroot_path, custom_script) + custom_script) # Verify that the tryjob's 'status' has been updated in the status file. with open(temp_json_file) as status_file: @@ -365,13 +292,11 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Index of the tryjob that is going to have its 'status' value updated. tryjob_index = 0 - chroot_path = '/abs/path/to/chroot' - custom_script = None update_tryjob_status.UpdateTryjobStatus(revision_to_update, TryjobStatus.BAD, temp_json_file, - chroot_path, custom_script) + custom_script) # Verify that the tryjob's 'status' has been updated in the status file. with open(temp_json_file) as status_file: @@ -407,13 +332,11 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Index of the tryjob that is going to have its 'status' value updated. tryjob_index = 0 - chroot_path = '/abs/path/to/chroot' - custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, update_tryjob_status.TryjobStatus.SKIP, - temp_json_file, chroot_path, custom_script) + temp_json_file, custom_script) # Verify that the tryjob's 'status' has been updated in the status file. with open(temp_json_file) as status_file: @@ -448,13 +371,11 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Index of the tryjob that is going to have its 'status' value updated. tryjob_index = 0 - chroot_path = '/abs/path/to/chroot' - custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, update_tryjob_status.TryjobStatus.PENDING, - temp_json_file, chroot_path, custom_script) + temp_json_file, custom_script) # Verify that the tryjob's 'status' has been updated in the status file. with open(temp_json_file) as status_file: @@ -465,69 +386,12 @@ class UpdateTryjobStatusTest(unittest.TestCase): mock_find_tryjob_index.assert_called_once() - # Simulate the behavior of `FindTryjobIndex()` when the tryjob exists in the - # status file. - @mock.patch.object(update_tryjob_status, 'FindTryjobIndex', return_value=0) - # Simulate the behavior of `GetAutoResult()` when `cros buildresult` returns - # a value that is in the mapping. - @mock.patch.object( - update_tryjob_status, - 'GetAutoResult', - return_value=TryjobStatus.GOOD.value) - def testSuccessfullyUpdatedTryjobStatusToAuto(self, mock_get_auto_result, - mock_find_tryjob_index): - bisect_test_contents = { - 'start': 369410, - 'end': 369420, - 'jobs': [{ - 'rev': 369411, - 'status': 'pending', - 'buildbucket_id': 1200 - }] - } - - # Create a temporary .JSON file to simulate a .JSON file that has bisection - # contents. - with CreateTemporaryJsonFile() as temp_json_file: - with open(temp_json_file, 'w') as f: - WritePrettyJsonFile(bisect_test_contents, f) - - revision_to_update = 369411 - - # Index of the tryjob that is going to have its 'status' value updated. - tryjob_index = 0 - - path_to_chroot = '/abs/path/to/chroot' - - custom_script = None - - update_tryjob_status.UpdateTryjobStatus( - revision_to_update, update_tryjob_status.TryjobStatus.AUTO, - temp_json_file, path_to_chroot, custom_script) - - # Verify that the tryjob's 'status' has been updated in the status file. - with open(temp_json_file) as status_file: - bisect_contents = json.load(status_file) - - self.assertEqual(bisect_contents['jobs'][tryjob_index]['status'], - update_tryjob_status.TryjobStatus.GOOD.value) - - mock_get_auto_result.assert_called_once_with( - path_to_chroot, - bisect_test_contents['jobs'][tryjob_index]['buildbucket_id']) - - mock_find_tryjob_index.assert_called_once() - - # Simulate the behavior of `FindTryjobIndex()` when the tryjob exists in the - # status file. @mock.patch.object(update_tryjob_status, 'FindTryjobIndex', return_value=0) - # Simulate the behavior of `GetCustomScriptResult()` when the custom script - # exit code is in the mapping. @mock.patch.object( update_tryjob_status, 'GetCustomScriptResult', return_value=TryjobStatus.SKIP.value) - def testSuccessfullyUpdatedTryjobStatusToAuto( + def testUpdatedTryjobStatusToAutoPassedWithCustomScript( self, mock_get_custom_script_result, mock_find_tryjob_index): bisect_test_contents = { 'start': 369410, @@ -550,13 +414,11 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Index of the tryjob that is going to have its 'status' value updated. tryjob_index = 0 - path_to_chroot = '/abs/path/to/chroot' - custom_script_path = '/abs/path/to/custom_script.py' update_tryjob_status.UpdateTryjobStatus( revision_to_update, update_tryjob_status.TryjobStatus.CUSTOM_SCRIPT, - temp_json_file, path_to_chroot, custom_script_path) + temp_json_file, custom_script_path) # Verify that the tryjob's 'status' has been updated in the status file. with open(temp_json_file) as status_file: @@ -593,8 +455,6 @@ class UpdateTryjobStatusTest(unittest.TestCase): revision_to_update = 369411 - path_to_chroot = '/abs/path/to/chroot' - nonexistent_update_status = 'revert_status' custom_script = None @@ -602,9 +462,9 @@ class UpdateTryjobStatusTest(unittest.TestCase): # Verify the exception is raised when the `set_status` command line # argument does not exist in the mapping. with self.assertRaises(ValueError) as err: - update_tryjob_status.UpdateTryjobStatus( - revision_to_update, nonexistent_update_status, temp_json_file, - path_to_chroot, custom_script) + update_tryjob_status.UpdateTryjobStatus(revision_to_update, + nonexistent_update_status, + temp_json_file, custom_script) self.assertEqual( str(err.exception), |