• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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