#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright 2019 The ChromiumOS Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Tests when updating a tryjob's status.""" import json import os import subprocess import unittest import unittest.mock as mock from test_helpers import CreateTemporaryJsonFile from test_helpers import WritePrettyJsonFile import update_tryjob_status from update_tryjob_status import CustomScriptStatus from update_tryjob_status import TryjobStatus class UpdateTryjobStatusTest(unittest.TestCase): """Unittests for updating a tryjob's 'status'.""" def testFoundTryjobIndex(self): test_tryjobs = [ { "rev": 123, "url": "https://some_url_to_CL.com", "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, }, ] expected_index = 0 revision_to_find = 123 self.assertEqual( update_tryjob_status.FindTryjobIndex( revision_to_find, test_tryjobs ), expected_index, ) def testNotFindTryjobIndex(self): test_tryjobs = [ { "rev": 500, "url": "https://some_url_to_CL.com", "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, }, ] revision_to_find = 250 self.assertIsNone( update_tryjob_status.FindTryjobIndex(revision_to_find, test_tryjobs) ) @mock.patch.object(subprocess, "Popen") # Simulate the behavior of `os.rename()` when successfully renamed a file. @mock.patch.object(os, "rename", return_value=None) # Simulate the behavior of `os.path.basename()` when successfully retrieved # the basename of the temp .JSON file. @mock.patch.object(os.path, "basename", return_value="tmpFile.json") def testInvalidExitCodeByCustomScript( self, mock_basename, mock_rename_file, mock_exec_custom_script ): error_message_by_custom_script = "Failed to parse .JSON file" # Simulate the behavior of 'subprocess.Popen()' when executing the custom # script. # # `Popen.communicate()` returns a tuple of `stdout` and `stderr`. mock_exec_custom_script.return_value.communicate.return_value = ( None, error_message_by_custom_script, ) # Exit code of 1 is not in the mapping, so an exception will be raised. custom_script_exit_code = 1 mock_exec_custom_script.return_value.returncode = ( custom_script_exit_code ) tryjob_contents = { "status": "good", "rev": 1234, "url": "https://some_url_to_CL.com", "link": "https://some_url_to_tryjob.com", } custom_script_path = "/abs/path/to/script.py" status_file_path = "/abs/path/to/status_file.json" name_json_file = os.path.join( os.path.dirname(status_file_path), "tmpFile.json" ) expected_error_message = ( "Custom script %s exit code %d did not match " 'any of the expected exit codes: %s for "good", ' '%d for "bad", or %d for "skip".\nPlease check ' "%s for information about the tryjob: %s" % ( custom_script_path, custom_script_exit_code, CustomScriptStatus.GOOD.value, CustomScriptStatus.BAD.value, CustomScriptStatus.SKIP.value, name_json_file, error_message_by_custom_script, ) ) # Verify the exception is raised when the exit code by the custom script # 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 ) self.assertEqual(str(err.exception), expected_error_message) mock_exec_custom_script.assert_called_once() mock_rename_file.assert_called_once() mock_basename.assert_called_once() @mock.patch.object(subprocess, "Popen") # Simulate the behavior of `os.rename()` when successfully renamed a file. @mock.patch.object(os, "rename", return_value=None) # Simulate the behavior of `os.path.basename()` when successfully retrieved # the basename of the temp .JSON file. @mock.patch.object(os.path, "basename", return_value="tmpFile.json") def testValidExitCodeByCustomScript( self, mock_basename, mock_rename_file, mock_exec_custom_script ): # Simulate the behavior of 'subprocess.Popen()' when executing the custom # script. # # `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 ) tryjob_contents = { "status": "good", "rev": 1234, "url": "https://some_url_to_CL.com", "link": "https://some_url_to_tryjob.com", } custom_script_path = "/abs/path/to/script.py" status_file_path = "/abs/path/to/status_file.json" self.assertEqual( update_tryjob_status.GetCustomScriptResult( custom_script_path, status_file_path, tryjob_contents ), TryjobStatus.GOOD.value, ) mock_exec_custom_script.assert_called_once() mock_rename_file.assert_not_called() mock_basename.assert_not_called() def testNoTryjobsInStatusFileWhenUpdatingTryjobStatus(self): bisect_test_contents = {"start": 369410, "end": 369420, "jobs": []} # 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 = 369412 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, custom_script, ) self.assertEqual( str(err.exception), "No tryjobs in %s" % temp_json_file ) # Simulate the behavior of `FindTryjobIndex()` when the tryjob does not exist # in the status file. @mock.patch.object( update_tryjob_status, "FindTryjobIndex", return_value=None ) def testNotFindTryjobIndexWhenUpdatingTryjobStatus( self, mock_find_tryjob_index ): bisect_test_contents = { "start": 369410, "end": 369420, "jobs": [{"rev": 369411, "status": "pending"}], } # 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 = 369416 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, custom_script, ) self.assertEqual( str(err.exception), "Unable to find tryjob for %d in %s" % (revision_to_update, temp_json_file), ) 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) def testSuccessfullyUpdatedTryjobStatusToGood(self, mock_find_tryjob_index): bisect_test_contents = { "start": 369410, "end": 369420, "jobs": [{"rev": 369411, "status": "pending"}], } # 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 custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, TryjobStatus.GOOD, 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: bisect_contents = json.load(status_file) self.assertEqual( bisect_contents["jobs"][tryjob_index]["status"], TryjobStatus.GOOD.value, ) 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) def testSuccessfullyUpdatedTryjobStatusToBad(self, mock_find_tryjob_index): bisect_test_contents = { "start": 369410, "end": 369420, "jobs": [{"rev": 369411, "status": "pending"}], } # 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 custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, TryjobStatus.BAD, 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: bisect_contents = json.load(status_file) self.assertEqual( bisect_contents["jobs"][tryjob_index]["status"], TryjobStatus.BAD.value, ) 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) def testSuccessfullyUpdatedTryjobStatusToPending( self, mock_find_tryjob_index ): bisect_test_contents = { "start": 369410, "end": 369420, "jobs": [{"rev": 369411, "status": "skip"}], } # 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 custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, update_tryjob_status.TryjobStatus.SKIP, 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: bisect_contents = json.load(status_file) self.assertEqual( bisect_contents["jobs"][tryjob_index]["status"], update_tryjob_status.TryjobStatus.SKIP.value, ) 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) def testSuccessfullyUpdatedTryjobStatusToSkip(self, mock_find_tryjob_index): bisect_test_contents = { "start": 369410, "end": 369420, "jobs": [ { "rev": 369411, "status": "pending", } ], } # 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 custom_script = None update_tryjob_status.UpdateTryjobStatus( revision_to_update, update_tryjob_status.TryjobStatus.PENDING, 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: bisect_contents = json.load(status_file) self.assertEqual( bisect_contents["jobs"][tryjob_index]["status"], update_tryjob_status.TryjobStatus.PENDING.value, ) mock_find_tryjob_index.assert_called_once() @mock.patch.object(update_tryjob_status, "FindTryjobIndex", return_value=0) @mock.patch.object( update_tryjob_status, "GetCustomScriptResult", return_value=TryjobStatus.SKIP.value, ) def testUpdatedTryjobStatusToAutoPassedWithCustomScript( self, mock_get_custom_script_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 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, custom_script_path, ) # 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.SKIP.value, ) mock_get_custom_script_result.assert_called_once() 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) def testSetStatusDoesNotExistWhenUpdatingTryjobStatus( self, 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 nonexistent_update_status = "revert_status" custom_script = None # 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, custom_script, ) self.assertEqual( str(err.exception), 'Invalid "set_status" option provided: revert_status', ) mock_find_tryjob_index.assert_called_once() if __name__ == "__main__": unittest.main()