1# Copyright 2019 Google Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# 15################################################################################ 16"""Tests for bisect_clang.py""" 17import os 18from unittest import mock 19import unittest 20 21import bisect_clang 22 23FILE_DIRECTORY = os.path.dirname(__file__) 24LLVM_REPO_PATH = '/llvm-project' 25 26 27def get_git_command(*args): 28 """Returns a git command for the LLVM repo with |args| as arguments.""" 29 return ['git', '-C', LLVM_REPO_PATH] + list(args) 30 31 32def patch_environ(testcase_obj): 33 """Patch environment.""" 34 env = {} 35 patcher = mock.patch.dict(os.environ, env) 36 testcase_obj.addCleanup(patcher.stop) 37 patcher.start() 38 39 40class BisectClangTestMixin: # pylint: disable=too-few-public-methods 41 """Useful mixin for bisect_clang unittests.""" 42 43 def setUp(self): # pylint: disable=invalid-name 44 """Initialization method for unittests.""" 45 patch_environ(self) 46 os.environ['SRC'] = '/src' 47 os.environ['WORK'] = '/work' 48 49 50class GetClangBuildEnvTest(BisectClangTestMixin, unittest.TestCase): 51 """Tests for get_clang_build_env.""" 52 53 def test_cflags(self): 54 """Test that CFLAGS are not used compiling clang.""" 55 os.environ['CFLAGS'] = 'blah' 56 self.assertNotIn('CFLAGS', bisect_clang.get_clang_build_env()) 57 58 def test_cxxflags(self): 59 """Test that CXXFLAGS are not used compiling clang.""" 60 os.environ['CXXFLAGS'] = 'blah' 61 self.assertNotIn('CXXFLAGS', bisect_clang.get_clang_build_env()) 62 63 def test_other_variables(self): 64 """Test that other env vars are used when compiling clang.""" 65 key = 'other' 66 value = 'blah' 67 os.environ[key] = value 68 self.assertEqual(value, bisect_clang.get_clang_build_env()[key]) 69 70 71def read_test_data(filename): 72 """Returns data from |filename| in the test_data directory.""" 73 with open(os.path.join(FILE_DIRECTORY, 'test_data', filename)) as file_handle: 74 return file_handle.read() 75 76 77class SearchBisectOutputTest(BisectClangTestMixin, unittest.TestCase): 78 """Tests for search_bisect_output.""" 79 80 def test_search_bisect_output(self): 81 """Test that search_bisect_output finds the responsible commit when one 82 exists.""" 83 test_data = read_test_data('culprit-commit.txt') 84 self.assertEqual('ac9ee01fcbfac745aaedca0393a8e1c8a33acd8d', 85 bisect_clang.search_bisect_output(test_data)) 86 87 def test_search_bisect_output_none(self): 88 """Test that search_bisect_output doesnt find a non-existent culprit 89 commit.""" 90 self.assertIsNone(bisect_clang.search_bisect_output('hello')) 91 92 93def create_mock_popen( 94 output=bytes('', 'utf-8'), err=bytes('', 'utf-8'), returncode=0): 95 """Creates a mock subprocess.Popen.""" 96 97 class MockPopen: 98 """Mock subprocess.Popen.""" 99 commands = [] 100 testcases_written = [] 101 102 def __init__(self, command, *args, **kwargs): # pylint: disable=unused-argument 103 """Inits the MockPopen.""" 104 stdout = kwargs.pop('stdout', None) 105 self.command = command 106 self.commands.append(command) 107 self.stdout = None 108 self.stderr = None 109 self.returncode = returncode 110 if hasattr(stdout, 'write'): 111 self.stdout = stdout 112 113 def communicate(self, input_data=None): # pylint: disable=unused-argument 114 """Mock subprocess.Popen.communicate.""" 115 if self.stdout: 116 self.stdout.write(output) 117 118 if self.stderr: 119 self.stderr.write(err) 120 121 return output, err 122 123 def poll(self, input_data=None): # pylint: disable=unused-argument 124 """Mock subprocess.Popen.poll.""" 125 return self.returncode 126 127 return MockPopen 128 129 130def mock_prepare_build_impl(llvm_project_path): # pylint: disable=unused-argument 131 """Mocked prepare_build function.""" 132 return '/work/llvm-build' 133 134 135class BuildClangTest(BisectClangTestMixin, unittest.TestCase): 136 """Tests for build_clang.""" 137 138 def test_build_clang_test(self): 139 """Tests that build_clang works as intended.""" 140 with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen: 141 with mock.patch('bisect_clang.prepare_build', mock_prepare_build_impl): 142 llvm_src_dir = '/src/llvm-project' 143 bisect_clang.build_clang(llvm_src_dir) 144 self.assertEqual([['ninja', '-C', '/work/llvm-build', 'install']], 145 mock_popen.commands) 146 147 148class GitRepoTest(BisectClangTestMixin, unittest.TestCase): 149 """Tests for GitRepo.""" 150 151 # TODO(metzman): Mock filesystem. Until then, use a real directory. 152 153 def setUp(self): 154 super().setUp() 155 self.git = bisect_clang.GitRepo(LLVM_REPO_PATH) 156 self.good_commit = 'good_commit' 157 self.bad_commit = 'bad_commit' 158 self.test_command = 'testcommand' 159 160 def test_do_command(self): 161 """Test do_command creates a new process as intended.""" 162 # TODO(metzman): Test directory changing behavior. 163 command = ['subcommand', '--option'] 164 with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen: 165 self.git.do_command(command) 166 self.assertEqual([get_git_command('subcommand', '--option')], 167 mock_popen.commands) 168 169 def _test_test_start_commit_unexpected(self, label, commit, returncode): 170 """Tests test_start_commit works as intended when the test returns an 171 unexpected value.""" 172 173 def mock_execute_impl(command, *args, **kwargs): # pylint: disable=unused-argument 174 if command == self.test_command: 175 return returncode, '', '' 176 return 0, '', '' 177 178 with mock.patch('bisect_clang.execute', mock_execute_impl): 179 with mock.patch('bisect_clang.prepare_build', mock_prepare_build_impl): 180 with self.assertRaises(bisect_clang.BisectError): 181 self.git.test_start_commit(commit, label, self.test_command) 182 183 def test_test_start_commit_bad_zero(self): 184 """Tests test_start_commit works as intended when the test on the first bad 185 commit returns 0.""" 186 self._test_test_start_commit_unexpected('bad', self.bad_commit, 0) 187 188 def test_test_start_commit_good_nonzero(self): 189 """Tests test_start_commit works as intended when the test on the first good 190 commit returns nonzero.""" 191 self._test_test_start_commit_unexpected('good', self.good_commit, 1) 192 193 def test_test_start_commit_good_zero(self): 194 """Tests test_start_commit works as intended when the test on the first good 195 commit returns 0.""" 196 self._test_test_start_commit_expected('good', self.good_commit, 0) # pylint: disable=no-value-for-parameter 197 198 @mock.patch('bisect_clang.build_clang') 199 def _test_test_start_commit_expected(self, label, commit, returncode, 200 mock_build_clang): 201 """Tests test_start_commit works as intended when the test returns an 202 expected value.""" 203 command_args = [] 204 205 def mock_execute_impl(command, *args, **kwargs): # pylint: disable=unused-argument 206 command_args.append(command) 207 if command == self.test_command: 208 return returncode, '', '' 209 return 0, '', '' 210 211 with mock.patch('bisect_clang.execute', mock_execute_impl): 212 self.git.test_start_commit(commit, label, self.test_command) 213 self.assertEqual([ 214 get_git_command('checkout', commit), self.test_command, 215 get_git_command('bisect', label) 216 ], command_args) 217 mock_build_clang.assert_called_once_with(LLVM_REPO_PATH) 218 219 def test_test_start_commit_bad_nonzero(self): 220 """Tests test_start_commit works as intended when the test on the first bad 221 commit returns nonzero.""" 222 self._test_test_start_commit_expected('bad', self.bad_commit, 1) # pylint: disable=no-value-for-parameter 223 224 @mock.patch('bisect_clang.GitRepo.test_start_commit') 225 def test_bisect_start(self, mock_test_start_commit): 226 """Tests bisect_start works as intended.""" 227 with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen: 228 self.git.bisect_start(self.good_commit, self.bad_commit, 229 self.test_command) 230 self.assertEqual(get_git_command('bisect', 'start'), 231 mock_popen.commands[0]) 232 mock_test_start_commit.assert_has_calls([ 233 mock.call('bad_commit', 'bad', 'testcommand'), 234 mock.call('good_commit', 'good', 'testcommand') 235 ]) 236 237 def test_do_bisect_command(self): 238 """Test do_bisect_command executes a git bisect subcommand as intended.""" 239 subcommand = 'subcommand' 240 with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen: 241 self.git.do_bisect_command(subcommand) 242 self.assertEqual([get_git_command('bisect', subcommand)], 243 mock_popen.commands) 244 245 @mock.patch('bisect_clang.build_clang') 246 def _test_test_commit(self, label, output, returncode, mock_build_clang): 247 """Test test_commit works as intended.""" 248 command_args = [] 249 250 def mock_execute_impl(command, *args, **kwargs): # pylint: disable=unused-argument 251 command_args.append(command) 252 if command == self.test_command: 253 return returncode, output, '' 254 return 0, output, '' 255 256 with mock.patch('bisect_clang.execute', mock_execute_impl): 257 result = self.git.test_commit(self.test_command) 258 self.assertEqual([self.test_command, 259 get_git_command('bisect', label)], command_args) 260 mock_build_clang.assert_called_once_with(LLVM_REPO_PATH) 261 return result 262 263 def test_test_commit_good(self): 264 """Test test_commit labels a good commit as good.""" 265 self.assertIsNone(self._test_test_commit('good', '', 0)) # pylint: disable=no-value-for-parameter 266 267 def test_test_commit_bad(self): 268 """Test test_commit labels a bad commit as bad.""" 269 self.assertIsNone(self._test_test_commit('bad', '', 1)) # pylint: disable=no-value-for-parameter 270 271 def test_test_commit_culprit(self): 272 """Test test_commit returns the culprit""" 273 test_data = read_test_data('culprit-commit.txt') 274 self.assertEqual('ac9ee01fcbfac745aaedca0393a8e1c8a33acd8d', 275 self._test_test_commit('good', test_data, 0)) # pylint: disable=no-value-for-parameter 276 277 278class GetTargetArchToBuildTest(unittest.TestCase): 279 """Tests for get_target_arch_to_build.""" 280 281 def test_unrecognized(self): 282 """Test that an unrecognized architecture raises an exception.""" 283 with mock.patch('bisect_clang.execute') as mock_execute: 284 mock_execute.return_value = (None, 'mips', None) 285 with self.assertRaises(Exception): 286 bisect_clang.get_clang_target_arch() 287 288 def test_recognized(self): 289 """Test that a recognized architecture returns the expected value.""" 290 arch_pairs = {'x86_64': 'X86', 'aarch64': 'AArch64'} 291 for uname_result, clang_target in arch_pairs.items(): 292 with mock.patch('bisect_clang.execute') as mock_execute: 293 mock_execute.return_value = (None, uname_result, None) 294 self.assertEqual(clang_target, bisect_clang.get_clang_target_arch()) 295