blob: e730293bfd0f0e3491460738d11106521c4b7461 [file] [log] [blame]
#!/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.
# pylint: disable=protected-access
"""Tests for LLVM bisection."""
from __future__ import print_function
import json
import unittest
import unittest.mock as mock
import chroot
import get_llvm_hash
import llvm_bisection
import modify_a_tryjob
import test_helpers
import update_tryjob_status
class LLVMBisectionTest(unittest.TestCase):
"""Unittests for LLVM bisection."""
def testStartAndEndDoNotMatchJsonStartAndEnd(self):
start = 100
end = 150
json_start = 110
json_end = 150
# Verify the exception is raised when the start and end revision for LLVM
# bisection do not match the .JSON's 'start' and 'end' values.
with self.assertRaises(ValueError) as err:
llvm_bisection._ValidateStartAndEndAgainstJSONStartAndEnd(
start, end, json_start, json_end)
expected_error_message = ('The start %d or the end %d version provided is '
'different than "start" %d or "end" %d in the '
'.JSON file' % (start, end, json_start, json_end))
self.assertEqual(str(err.exception), expected_error_message)
def testStartAndEndMatchJsonStartAndEnd(self):
start = 100
end = 150
json_start = 100
json_end = 150
llvm_bisection._ValidateStartAndEndAgainstJSONStartAndEnd(
start, end, json_start, json_end)
def testTryjobStatusIsMissing(self):
start = 100
end = 150
test_tryjobs = [{
'rev': 105,
'status': 'good',
'link': 'https://some_tryjob_1_url.com'
}, {
'rev': 120,
'status': None,
'link': 'https://some_tryjob_2_url.com'
}, {
'rev': 140,
'status': 'bad',
'link': 'https://some_tryjob_3_url.com'
}]
# Verify the exception is raised when a tryjob does not have a value for
# the 'status' key or the 'status' key is missing.
with self.assertRaises(ValueError) as err:
llvm_bisection.GetStartAndEndRevision(start, end, test_tryjobs)
expected_error_message = (
'"status" is missing or has no value, please '
'go to %s and update it' % test_tryjobs[1]['link'])
self.assertEqual(str(err.exception), expected_error_message)
def testGoodRevisionGreaterThanBadRevision(self):
start = 100
end = 150
test_tryjobs = [{
'rev': 110,
'status': 'bad',
'link': 'https://some_tryjob_1_url.com'
}, {
'rev': 125,
'status': 'skip',
'link': 'https://some_tryjob_2_url.com'
}, {
'rev': 140,
'status': 'good',
'link': 'https://some_tryjob_3_url.com'
}]
# Verify the exception is raised when the new 'start' revision is greater
# than the new 'bad' revision for bisection (i.e. bisection is broken).
with self.assertRaises(AssertionError) as err:
llvm_bisection.GetStartAndEndRevision(start, end, test_tryjobs)
expected_error_message = (
'Bisection is broken because %d (good) is >= '
'%d (bad)' % (test_tryjobs[2]['rev'], test_tryjobs[0]['rev']))
self.assertEqual(str(err.exception), expected_error_message)
def testSuccessfullyGetNewStartAndNewEndRevision(self):
start = 100
end = 150
test_tryjobs = [{
'rev': 110,
'status': 'good',
'link': 'https://some_tryjob_1_url.com'
}, {
'rev': 120,
'status': 'good',
'link': 'https://some_tryjob_2_url.com'
}, {
'rev': 130,
'status': 'pending',
'link': 'https://some_tryjob_3_url.com'
}, {
'rev': 135,
'status': 'skip',
'link': 'https://some_tryjob_4_url.com'
}, {
'rev': 140,
'status': 'bad',
'link': 'https://some_tryjob_5_url.com'
}]
# Tuple consists of the new good revision, the new bad revision, a set of
# 'pending' revisions, and a set of 'skip' revisions.
expected_revisions_tuple = 120, 140, {130}, {135}
self.assertTupleEqual(
llvm_bisection.GetStartAndEndRevision(start, end, test_tryjobs),
expected_revisions_tuple)
@mock.patch.object(get_llvm_hash, 'GetGitHashFrom')
def testNoRevisionsBetweenStartAndEnd(self, mock_get_git_hash):
start = 100
end = 110
test_pending_revisions = {107}
test_skip_revisions = {101, 102, 103, 104, 108, 109}
# Simulate behavior of `GetGitHashFrom()` when the revision does not
# exist in the LLVM source tree.
def MockGetGitHashForRevisionRaiseException(_src_path, _revision):
raise ValueError('Revision does not exist')
mock_get_git_hash.side_effect = MockGetGitHashForRevisionRaiseException
parallel = 3
abs_path_to_src = '/abs/path/to/src'
self.assertListEqual(
llvm_bisection.GetRevisionsBetweenBisection(
start, end, parallel, abs_path_to_src, test_pending_revisions,
test_skip_revisions), [])
# Assume llvm_bisection module has imported GetGitHashFrom
@mock.patch.object(get_llvm_hash, 'GetGitHashFrom')
def testSuccessfullyRetrievedRevisionsBetweenStartAndEnd(
self, mock_get_git_hash):
start = 100
end = 110
test_pending_revisions = set()
test_skip_revisions = {101, 102, 103, 104, 106, 108, 109}
parallel = 3
abs_path_to_src = '/abs/path/to/src'
# Valid revision that exist in the LLVM source tree between 'start' and
# 'end' and were not in the 'pending' set or 'skip' set.
expected_revisions_between_start_and_end = [105, 107]
self.assertListEqual(
llvm_bisection.GetRevisionsBetweenBisection(
start, end, parallel, abs_path_to_src, test_pending_revisions,
test_skip_revisions), expected_revisions_between_start_and_end)
self.assertEqual(mock_get_git_hash.call_count, 2)
# Simulate behavior of `GetGitHashFrom()` when successfully retrieved
# a list git hashes for each revision in the revisions list.
# Assume llvm_bisection module has imported GetGitHashFrom
@mock.patch.object(get_llvm_hash, 'GetGitHashFrom')
# Simulate behavior of `GetRevisionsBetweenBisection()` when successfully
# retrieved a list of valid revisions between 'start' and 'end'.
@mock.patch.object(llvm_bisection, 'GetRevisionsBetweenBisection')
# Simulate behavior of `CreatTempLLVMRepo()` when successfully created a
# worktree when a source path was not provided.
@mock.patch.object(get_llvm_hash, 'CreateTempLLVMRepo')
def testSuccessfullyGetRevisionsListAndHashList(
self, mock_create_temp_llvm_repo, mock_get_revisions_between_bisection,
mock_get_git_hash):
expected_revisions_and_hash_tuple = ([102, 105, 108], [
'a123testhash1', 'a123testhash2', 'a123testhash3'
])
@test_helpers.CallCountsToMockFunctions
def MockGetGitHashForRevision(call_count, _src_path, _rev):
# Simulate retrieving the git hash for the revision.
if call_count < 3:
return expected_revisions_and_hash_tuple[1][call_count]
assert False, 'Called `GetGitHashFrom()` more than expected.'
temp_worktree = '/abs/path/to/tmpDir'
mock_create_temp_llvm_repo.return_value.__enter__.return_value.name = \
temp_worktree
# Simulate the valid revisions list.
mock_get_revisions_between_bisection.return_value = \
expected_revisions_and_hash_tuple[0]
# Simulate behavior of `GetGitHashFrom()` by using the testing
# function.
mock_get_git_hash.side_effect = MockGetGitHashForRevision
start = 100
end = 110
parallel = 3
src_path = None
pending_revisions = {103, 104}
skip_revisions = {101, 106, 107, 109}
self.assertTupleEqual(
llvm_bisection.GetRevisionsListAndHashList(
start, end, parallel, src_path, pending_revisions, skip_revisions),
expected_revisions_and_hash_tuple)
mock_get_revisions_between_bisection.assert_called_once()
self.assertEqual(mock_get_git_hash.call_count, 3)
def testSuccessfullyDieWithNoRevisionsError(self):
start = 100
end = 110
pending_revisions = {105, 108}
skip_revisions = {101, 102, 103, 104, 106, 107, 109}
expected_no_revisions_message = ('No revisions between start %d and end '
'%d to create tryjobs' % (start, end))
expected_no_revisions_message += '\nThe following tryjobs are pending:\n' \
+ '\n'.join(str(rev) for rev in pending_revisions)
expected_no_revisions_message += '\nThe following tryjobs were skipped:\n' \
+ '\n'.join(str(rev) for rev in skip_revisions)
# Verify that an exception is raised when there are no revisions to launch
# tryjobs for between 'start' and 'end' and some tryjobs are 'pending'.
with self.assertRaises(ValueError) as err:
llvm_bisection.DieWithNoRevisionsError(start, end, skip_revisions,
pending_revisions)
self.assertEqual(str(err.exception), expected_no_revisions_message)
# Simulate behavior of `FindTryjobIndex()` when the index of the tryjob was
# found.
@mock.patch.object(update_tryjob_status, 'FindTryjobIndex', return_value=0)
def testTryjobExistsInRevisionsToLaunch(self, mock_find_tryjob_index):
test_existing_jobs = [{'rev': 102, 'status': 'good'}]
revision_to_launch = [102]
expected_revision_that_exists = 102
with self.assertRaises(ValueError) as err:
llvm_bisection.CheckForExistingTryjobsInRevisionsToLaunch(
revision_to_launch, test_existing_jobs)
expected_found_tryjob_index_error_message = (
'Revision %d exists already '
'in "jobs"' % expected_revision_that_exists)
self.assertEqual(
str(err.exception), expected_found_tryjob_index_error_message)
mock_find_tryjob_index.assert_called_once()
@mock.patch.object(modify_a_tryjob, 'AddTryjob')
def testSuccessfullyUpdatedStatusFileWhenExceptionIsRaised(
self, mock_add_tryjob):
git_hash_list = ['a123testhash1', 'a123testhash2', 'a123testhash3']
revisions_list = [102, 104, 106]
# Simulate behavior of `AddTryjob()` when successfully launched a tryjob for
# the updated packages.
@test_helpers.CallCountsToMockFunctions
def MockAddTryjob(call_count, _packages, _git_hash, _revision, _chroot_path,
_patch_file, _extra_cls, _options, _builder, _verbose,
_svn_revision):
if call_count < 2:
return {'rev': revisions_list[call_count], 'status': 'pending'}
# Simulate an exception happened along the way when updating the
# packages' `LLVM_NEXT_HASH`.
if call_count == 2:
raise ValueError('Unable to launch tryjob')
assert False, 'Called `AddTryjob()` more than expected.'
# Use the test function to simulate `AddTryjob()`.
mock_add_tryjob.side_effect = MockAddTryjob
start = 100
end = 110
bisection_contents = {'start': start, 'end': end, 'jobs': []}
args_output = test_helpers.ArgsOutputTest()
packages = ['sys-devel/llvm']
patch_file = '/abs/path/to/PATCHES.json'
# Create a temporary .JSON file to simulate a status file for bisection.
with test_helpers.CreateTemporaryJsonFile() as temp_json_file:
with open(temp_json_file, 'w') as f:
test_helpers.WritePrettyJsonFile(bisection_contents, f)
# Verify that the status file is updated when an exception happened when
# attempting to launch a revision (i.e. progress is not lost).
with self.assertRaises(ValueError) as err:
llvm_bisection.UpdateBisection(
revisions_list, git_hash_list, bisection_contents, temp_json_file,
packages, args_output.chroot_path, patch_file,
args_output.extra_change_lists, args_output.options,
args_output.builders, args_output.verbose)
expected_bisection_contents = {
'start':
start,
'end':
end,
'jobs': [{
'rev': revisions_list[0],
'status': 'pending'
}, {
'rev': revisions_list[1],
'status': 'pending'
}]
}
# Verify that the launched tryjobs were added to the status file when
# an exception happened.
with open(temp_json_file) as f:
json_contents = json.load(f)
self.assertDictEqual(json_contents, expected_bisection_contents)
self.assertEqual(str(err.exception), 'Unable to launch tryjob')
self.assertEqual(mock_add_tryjob.call_count, 3)
# Simulate behavior of `GetGitHashFrom()` when successfully retrieved
# the git hash of the bad revision. Assume llvm_bisection has imported
# GetGitHashFrom
@mock.patch.object(
get_llvm_hash, 'GetGitHashFrom', return_value='a123testhash4')
def testCompletedBisectionWhenProvidedSrcPath(self, mock_get_git_hash):
last_tested = '/some/last/tested_file.json'
src_path = '/abs/path/to/src/path'
# The bad revision.
end = 150
llvm_bisection._NoteCompletedBisection(last_tested, src_path, end)
mock_get_git_hash.assert_called_once()
# Simulate behavior of `GetLLVMHash()` when successfully retrieved
# the git hash of the bad revision.
@mock.patch.object(
get_llvm_hash.LLVMHash, 'GetLLVMHash', return_value='a123testhash5')
def testCompletedBisectionWhenNotProvidedSrcPath(self, mock_get_git_hash):
last_tested = '/some/last/tested_file.json'
src_path = None
# The bad revision.
end = 200
llvm_bisection._NoteCompletedBisection(last_tested, src_path, end)
mock_get_git_hash.assert_called_once()
def testSuccessfullyLoadedStatusFile(self):
start = 100
end = 150
test_bisect_contents = {'start': start, 'end': end, 'jobs': []}
# Simulate that the status file exists.
with test_helpers.CreateTemporaryJsonFile() as temp_json_file:
with open(temp_json_file, 'w') as f:
test_helpers.WritePrettyJsonFile(test_bisect_contents, f)
self.assertDictEqual(
llvm_bisection.LoadStatusFile(temp_json_file, start, end),
test_bisect_contents)
def testLoadedStatusFileThatDoesNotExist(self):
start = 200
end = 250
expected_bisect_contents = {'start': start, 'end': end, 'jobs': []}
last_tested = '/abs/path/to/file_that_does_not_exist.json'
self.assertDictEqual(
llvm_bisection.LoadStatusFile(last_tested, start, end),
expected_bisect_contents)
# Simulate behavior of `_NoteCompletedBisection()` when there are no more
# tryjobs to launch between start and end, so bisection is complete.
@mock.patch.object(llvm_bisection, '_NoteCompletedBisection')
@mock.patch.object(llvm_bisection, 'GetRevisionsListAndHashList')
@mock.patch.object(llvm_bisection, 'GetStartAndEndRevision')
# Simulate behavior of `_ValidateStartAndEndAgainstJSONStartAndEnd()` when
# both start and end revisions match.
@mock.patch.object(llvm_bisection,
'_ValidateStartAndEndAgainstJSONStartAndEnd')
@mock.patch.object(llvm_bisection, 'LoadStatusFile')
# Simulate behavior of `VerifyOutsideChroot()` when successfully invoked the
# script outside of the chroot.
@mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True)
def testSuccessfullyBisectedLLVM(
self, mock_outside_chroot, mock_load_status_file,
mock_validate_start_and_end, mock_get_start_and_end_revision,
mock_get_revision_and_hash_list, mock_note_completed_bisection):
start = 500
end = 502
bisect_contents = {
'start': start,
'end': end,
'jobs': [{
'rev': 501,
'status': 'skip'
}]
}
skip_revisions = {501}
pending_revisions = {}
# Simulate behavior of `LoadStatusFile()` when successfully loaded the
# status file.
mock_load_status_file.return_value = bisect_contents
# Simulate behavior of `GetStartAndEndRevision()` when successfully found
# the new start and end revision of the bisection.
#
# Returns new start revision, new end revision, a set of pending revisions,
# and a set of skip revisions.
mock_get_start_and_end_revision.return_value = (start, end,
pending_revisions,
skip_revisions)
# Simulate behavior of `GetRevisionsListAndHashList()` when successfully
# retrieved valid revisions (along with their git hashes) between start and
# end (in this case, none).
mock_get_revision_and_hash_list.return_value = [], []
args_output = test_helpers.ArgsOutputTest()
args_output.start_rev = start
args_output.end_rev = end
args_output.parallel = 3
args_output.src_path = None
self.assertEqual(
llvm_bisection.main(args_output),
llvm_bisection.BisectionExitStatus.BISECTION_COMPLETE.value)
mock_outside_chroot.assert_called_once()
mock_load_status_file.assert_called_once()
mock_validate_start_and_end.assert_called_once()
mock_get_start_and_end_revision.assert_called_once()
mock_get_revision_and_hash_list.assert_called_once()
mock_note_completed_bisection.assert_called_once()
@mock.patch.object(llvm_bisection, 'DieWithNoRevisionsError')
# Simulate behavior of `_NoteCompletedBisection()` when there are no more
# tryjobs to launch between start and end, so bisection is complete.
@mock.patch.object(llvm_bisection, 'GetRevisionsListAndHashList')
@mock.patch.object(llvm_bisection, 'GetStartAndEndRevision')
# Simulate behavior of `_ValidateStartAndEndAgainstJSONStartAndEnd()` when
# both start and end revisions match.
@mock.patch.object(llvm_bisection,
'_ValidateStartAndEndAgainstJSONStartAndEnd')
@mock.patch.object(llvm_bisection, 'LoadStatusFile')
# Simulate behavior of `VerifyOutsideChroot()` when successfully invoked the
# script outside of the chroot.
@mock.patch.object(chroot, 'VerifyOutsideChroot', return_value=True)
def testNoMoreTryjobsToLaunch(
self, mock_outside_chroot, mock_load_status_file,
mock_validate_start_and_end, mock_get_start_and_end_revision,
mock_get_revision_and_hash_list, mock_die_with_no_revisions_error):
start = 500
end = 502
bisect_contents = {
'start': start,
'end': end,
'jobs': [{
'rev': 501,
'status': 'pending'
}]
}
skip_revisions = {}
pending_revisions = {501}
no_revisions_error_message = ('No more tryjobs to launch between %d and '
'%d' % (start, end))
def MockNoRevisionsErrorException(_start, _end, _skip, _pending):
raise ValueError(no_revisions_error_message)
# Simulate behavior of `LoadStatusFile()` when successfully loaded the
# status file.
mock_load_status_file.return_value = bisect_contents
# Simulate behavior of `GetStartAndEndRevision()` when successfully found
# the new start and end revision of the bisection.
#
# Returns new start revision, new end revision, a set of pending revisions,
# and a set of skip revisions.
mock_get_start_and_end_revision.return_value = (start, end,
pending_revisions,
skip_revisions)
# Simulate behavior of `GetRevisionsListAndHashList()` when successfully
# retrieved valid revisions (along with their git hashes) between start and
# end (in this case, none).
mock_get_revision_and_hash_list.return_value = [], []
# Use the test function to simulate `DieWithNoRevisionsWithError()`
# behavior.
mock_die_with_no_revisions_error.side_effect = MockNoRevisionsErrorException
# Simulate behavior of arguments passed into the command line.
args_output = test_helpers.ArgsOutputTest()
args_output.start_rev = start
args_output.end_rev = end
args_output.parallel = 3
args_output.src_path = None
# Verify the exception is raised when there are no more tryjobs to launch
# between start and end when there are tryjobs that are 'pending', so
# the actual bad revision can change when those tryjobs's 'status' are
# updated.
with self.assertRaises(ValueError) as err:
llvm_bisection.main(args_output)
self.assertEqual(str(err.exception), no_revisions_error_message)
mock_outside_chroot.assert_called_once()
mock_load_status_file.assert_called_once()
mock_validate_start_and_end.assert_called_once()
mock_get_start_and_end_revision.assert_called_once()
mock_get_revision_and_hash_list.assert_called_once()
mock_die_with_no_revisions_error.assert_called_once()
if __name__ == '__main__':
unittest.main()