blob: 1e86a67886326771acb13047ba9c9c401744a0ae [file] [log] [blame]
#!/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.
# pylint: disable=protected-access
"""Tests for LLVM bisection."""
import json
import os
import subprocess
import unittest
import unittest.mock as mock
import chroot
import get_llvm_hash
import git_llvm_rev
import llvm_bisection
import modify_a_tryjob
import test_helpers
class LLVMBisectionTest(unittest.TestCase):
"""Unittests for LLVM bisection."""
def testGetRemainingRangePassed(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.assertEqual(
llvm_bisection.GetRemainingRange(start, end, test_tryjobs),
expected_revisions_tuple,
)
def testGetRemainingRangeFailedWithMissingStatus(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",
},
]
with self.assertRaises(ValueError) as err:
llvm_bisection.GetRemainingRange(start, end, test_tryjobs)
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), error_message)
def testGetRemainingRangeFailedWithInvalidRange(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",
},
]
with self.assertRaises(AssertionError) as err:
llvm_bisection.GetRemainingRange(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)
@mock.patch.object(get_llvm_hash, "GetGitHashFrom")
def testGetCommitsBetweenPassed(self, mock_get_git_hash):
start = git_llvm_rev.base_llvm_revision
end = start + 10
test_pending_revisions = {start + 7}
test_skip_revisions = {
start + 1,
start + 2,
start + 4,
start + 8,
start + 9,
}
parallel = 3
abs_path_to_src = "/abs/path/to/src"
revs = ["a123testhash3", "a123testhash5"]
mock_get_git_hash.side_effect = revs
git_hashes = [
git_llvm_rev.base_llvm_revision + 3,
git_llvm_rev.base_llvm_revision + 5,
]
self.assertEqual(
llvm_bisection.GetCommitsBetween(
start,
end,
parallel,
abs_path_to_src,
test_pending_revisions,
test_skip_revisions,
),
(git_hashes, revs),
)
def testLoadStatusFilePassedWithExistingFile(self):
start = 100
end = 150
test_bisect_state = {"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_state, f)
self.assertEqual(
llvm_bisection.LoadStatusFile(temp_json_file, start, end),
test_bisect_state,
)
def testLoadStatusFilePassedWithoutExistingFile(self):
start = 200
end = 250
expected_bisect_state = {"start": start, "end": end, "jobs": []}
last_tested = "/abs/path/to/file_that_does_not_exist.json"
self.assertEqual(
llvm_bisection.LoadStatusFile(last_tested, start, end),
expected_bisect_state,
)
@mock.patch.object(modify_a_tryjob, "AddTryjob")
def testBisectPassed(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.Bisect(
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.assertEqual(json_contents, expected_bisection_contents)
self.assertEqual(str(err.exception), "Unable to launch tryjob")
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")
@mock.patch.object(llvm_bisection, "GetRemainingRange")
@mock.patch.object(llvm_bisection, "LoadStatusFile")
@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_abandon_cl,
):
start = 500
end = 502
cl = 1
bisect_state = {
"start": start,
"end": end,
"jobs": [{"rev": 501, "status": "bad", "cl": cl}],
}
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 = [], []
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.chroot_path = "somepath"
args_output.cleanup = True
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_get_range.assert_called_once()
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",
str(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, mock_load_status_file
):
start = 500
end = 502
bisect_state = {
"start": start - 1,
"end": end,
}
mock_load_status_file.return_value = bisect_state
args_output = test_helpers.ArgsOutputTest()
args_output.start_rev = start
args_output.end_rev = end
args_output.parallel = 3
args_output.src_path = None
with self.assertRaises(ValueError) as err:
llvm_bisection.main(args_output)
error_message = (
f"The start {start} or the end {end} version provided is "
f'different than "start" {bisect_state["start"]} or "end" '
f'{bisect_state["end"]} in the .JSON file'
)
self.assertEqual(str(err.exception), error_message)
mock_outside_chroot.assert_called_once()
mock_load_status_file.assert_called_once()
@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 testMainFailedWithPendingBuilds(
self,
mock_outside_chroot,
mock_load_status_file,
mock_get_range,
mock_get_revision_and_hash_list,
):
start = 500
end = 502
rev = 501
bisect_state = {
"start": start,
"end": end,
"jobs": [{"rev": rev, "status": "pending"}],
}
skip_revisions = {}
pending_revisions = {rev}
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 = [], []
args_output = test_helpers.ArgsOutputTest()
args_output.start_rev = start
args_output.end_rev = end
args_output.parallel = 3
args_output.src_path = None
with self.assertRaises(ValueError) as err:
llvm_bisection.main(args_output)
error_message = (
f"No revisions between start {start} and end {end} to "
"create tryjobs\nThe following tryjobs are pending:\n"
f"{rev}\n"
)
self.assertEqual(str(err.exception), error_message)
mock_outside_chroot.assert_called_once()
mock_load_status_file.assert_called_once()
mock_get_range.assert_called_once()
mock_get_revision_and_hash_list.assert_called_once()
@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 testMainFailedWithDuplicateBuilds(
self,
mock_outside_chroot,
mock_load_status_file,
mock_get_range,
mock_get_revision_and_hash_list,
):
start = 500
end = 502
rev = 501
git_hash = "a123testhash1"
bisect_state = {
"start": start,
"end": end,
"jobs": [{"rev": rev, "status": "pending"}],
}
skip_revisions = {}
pending_revisions = {rev}
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 = [rev], [git_hash]
args_output = test_helpers.ArgsOutputTest()
args_output.start_rev = start
args_output.end_rev = end
args_output.parallel = 3
args_output.src_path = None
with self.assertRaises(ValueError) as err:
llvm_bisection.main(args_output)
error_message = 'Revision %d exists already in "jobs"' % rev
self.assertEqual(str(err.exception), error_message)
mock_outside_chroot.assert_called_once()
mock_load_status_file.assert_called_once()
mock_get_range.assert_called_once()
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()