mirror of https://github.com/google/oss-fuzz.git
292 lines
11 KiB
Python
292 lines
11 KiB
Python
|
# Copyright 2019 Google Inc.
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
#
|
||
|
################################################################################
|
||
|
"""Tests for bisect_clang.py"""
|
||
|
import os
|
||
|
from unittest import mock
|
||
|
import unittest
|
||
|
|
||
|
import bisect_clang
|
||
|
|
||
|
FILE_DIRECTORY = os.path.dirname(__file__)
|
||
|
LLVM_REPO_PATH = '/llvm-project'
|
||
|
|
||
|
|
||
|
def get_git_command(*args):
|
||
|
"""Returns a git command for the LLVM repo with |args| as arguments."""
|
||
|
return ['git', '-C', LLVM_REPO_PATH] + list(args)
|
||
|
|
||
|
|
||
|
def patch_environ(testcase_obj):
|
||
|
"""Patch environment."""
|
||
|
env = {}
|
||
|
patcher = mock.patch.dict(os.environ, env)
|
||
|
testcase_obj.addCleanup(patcher.stop)
|
||
|
patcher.start()
|
||
|
|
||
|
|
||
|
class BisectClangTestMixin:
|
||
|
"""Useful mixin for bisect_clang unittests."""
|
||
|
|
||
|
def setUp(self):
|
||
|
patch_environ(self)
|
||
|
os.environ['SRC'] = '/src'
|
||
|
os.environ['WORK'] = '/work'
|
||
|
|
||
|
|
||
|
class GetClangBuildEnvTest(BisectClangTestMixin, unittest.TestCase):
|
||
|
"""Tests for get_clang_build_env."""
|
||
|
|
||
|
def test_cflags(self):
|
||
|
"""Test that CFLAGS are not used compiling clang."""
|
||
|
os.environ['CFLAGS'] = 'blah'
|
||
|
self.assertNotIn('CFLAGS', bisect_clang.get_clang_build_env())
|
||
|
|
||
|
def test_cxxflags(self):
|
||
|
"""Test that CXXFLAGS are not used compiling clang."""
|
||
|
os.environ['CXXFLAGS'] = 'blah'
|
||
|
self.assertNotIn('CXXFLAGS', bisect_clang.get_clang_build_env())
|
||
|
|
||
|
def test_other_variables(self):
|
||
|
"""Test that other env vars are used when compiling clang."""
|
||
|
key = 'other'
|
||
|
value = 'blah'
|
||
|
os.environ[key] = value
|
||
|
self.assertEqual(value, bisect_clang.get_clang_build_env()[key])
|
||
|
|
||
|
|
||
|
def read_test_data(filename):
|
||
|
with open(os.path.join(FILE_DIRECTORY, 'test_data', filename)) as f:
|
||
|
return f.read()
|
||
|
|
||
|
|
||
|
class SearchBisectOutputTest(BisectClangTestMixin, unittest.TestCase):
|
||
|
"""Tests for search_bisect_output."""
|
||
|
|
||
|
def test_search_bisect_output(self):
|
||
|
"""Test that search_bisect_output finds the responsible commit when one
|
||
|
exists."""
|
||
|
test_data = read_test_data('culprit-commit.txt')
|
||
|
self.assertEqual('ac9ee01fcbfac745aaedca0393a8e1c8a33acd8d',
|
||
|
bisect_clang.search_bisect_output(test_data))
|
||
|
|
||
|
def test_search_bisect_output_none(self):
|
||
|
"""Test that search_bisect_output doesnt find a non-existent culprit
|
||
|
commit."""
|
||
|
self.assertIsNone(bisect_clang.search_bisect_output('hello'))
|
||
|
|
||
|
|
||
|
def create_mock_popen(
|
||
|
output=bytes('', 'utf-8'), err=bytes('', 'utf-8'), returncode=0):
|
||
|
"""Creates a mock subprocess.Popen."""
|
||
|
|
||
|
class MockPopen:
|
||
|
"""Mock subprocess.Popen."""
|
||
|
commands = []
|
||
|
testcases_written = []
|
||
|
|
||
|
def __init__(self, command, *args, **kwargs): # pylint: disable=unused-argument
|
||
|
"""Inits the MockPopen."""
|
||
|
stdout = kwargs.pop('stdout', None)
|
||
|
self.command = command
|
||
|
self.commands.append(command)
|
||
|
self.stdout = None
|
||
|
self.stderr = None
|
||
|
self.returncode = returncode
|
||
|
if hasattr(stdout, 'write'):
|
||
|
self.stdout = stdout
|
||
|
|
||
|
def communicate(self, input_data=None): # pylint: disable=unused-argument
|
||
|
"""Mock subprocess.Popen.communicate."""
|
||
|
if self.stdout:
|
||
|
self.stdout.write(output)
|
||
|
|
||
|
if self.stderr:
|
||
|
self.stderr.write(err)
|
||
|
|
||
|
return output, err
|
||
|
|
||
|
def poll(self, input_data=None): # pylint: disable=unused-argument
|
||
|
"""Mock subprocess.Popen.poll."""
|
||
|
return self.returncode
|
||
|
|
||
|
return MockPopen
|
||
|
|
||
|
|
||
|
def mock_prepare_build(llvm_project_path): # pylint: disable=unused-argument
|
||
|
return '/work/llvm-build'
|
||
|
|
||
|
|
||
|
class BuildClangTest(BisectClangTestMixin, unittest.TestCase):
|
||
|
"""Tests for build_clang."""
|
||
|
|
||
|
def test_build_clang_test(self):
|
||
|
"""Tests that build_clang works as intended."""
|
||
|
with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen:
|
||
|
with mock.patch('bisect_clang.prepare_build', mock_prepare_build):
|
||
|
llvm_src_dir = '/src/llvm-project'
|
||
|
bisect_clang.build_clang(llvm_src_dir)
|
||
|
self.assertEqual([['ninja', '-C', '/work/llvm-build', 'install']],
|
||
|
mock_popen.commands)
|
||
|
|
||
|
|
||
|
class GitRepoTest(BisectClangTestMixin, unittest.TestCase):
|
||
|
"""Tests for GitRepo."""
|
||
|
|
||
|
# TODO(metzman): Mock filesystem. Until then, use a real directory.
|
||
|
|
||
|
def setUp(self):
|
||
|
super().setUp()
|
||
|
self.git = bisect_clang.GitRepo(LLVM_REPO_PATH)
|
||
|
self.good_commit = 'good_commit'
|
||
|
self.bad_commit = 'bad_commit'
|
||
|
self.test_command = 'testcommand'
|
||
|
|
||
|
def test_do_command(self):
|
||
|
"""Test do_command creates a new process as intended."""
|
||
|
# TODO(metzman): Test directory changing behavior.
|
||
|
command = ['subcommand', '--option']
|
||
|
with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen:
|
||
|
self.git.do_command(command)
|
||
|
self.assertEqual([get_git_command('subcommand', '--option')],
|
||
|
mock_popen.commands)
|
||
|
|
||
|
def _test_test_start_commit_unexpected(self, label, commit, returncode):
|
||
|
"""Tests test_start_commit works as intended when the test returns an
|
||
|
unexpected value."""
|
||
|
|
||
|
def mock_execute(command, *args, **kwargs): # pylint: disable=unused-argument
|
||
|
if command == self.test_command:
|
||
|
return returncode, '', ''
|
||
|
return 0, '', ''
|
||
|
|
||
|
with mock.patch('bisect_clang.execute', mock_execute):
|
||
|
with mock.patch('bisect_clang.prepare_build', mock_prepare_build):
|
||
|
with self.assertRaises(bisect_clang.BisectError):
|
||
|
self.git.test_start_commit(commit, label, self.test_command)
|
||
|
|
||
|
def test_test_start_commit_bad_zero(self):
|
||
|
"""Tests test_start_commit works as intended when the test on the first bad
|
||
|
commit returns 0."""
|
||
|
self._test_test_start_commit_unexpected('bad', self.bad_commit, 0)
|
||
|
|
||
|
def test_test_start_commit_good_nonzero(self):
|
||
|
"""Tests test_start_commit works as intended when the test on the first good
|
||
|
commit returns nonzero."""
|
||
|
self._test_test_start_commit_unexpected('good', self.good_commit, 1)
|
||
|
|
||
|
def test_test_start_commit_good_zero(self):
|
||
|
"""Tests test_start_commit works as intended when the test on the first good
|
||
|
commit returns 0."""
|
||
|
self._test_test_start_commit_expected('good', self.good_commit, 0) # pylint: disable=no-value-for-parameter
|
||
|
|
||
|
@mock.patch('bisect_clang.build_clang')
|
||
|
def _test_test_start_commit_expected(self, label, commit, returncode,
|
||
|
mock_build_clang):
|
||
|
"""Tests test_start_commit works as intended when the test returns an
|
||
|
expected value."""
|
||
|
command_args = []
|
||
|
|
||
|
def mock_execute(command, *args, **kwargs): # pylint: disable=unused-argument
|
||
|
command_args.append(command)
|
||
|
if command == self.test_command:
|
||
|
return returncode, '', ''
|
||
|
return 0, '', ''
|
||
|
|
||
|
with mock.patch('bisect_clang.execute', mock_execute):
|
||
|
self.git.test_start_commit(commit, label, self.test_command)
|
||
|
self.assertEqual([
|
||
|
get_git_command('checkout', commit), self.test_command,
|
||
|
get_git_command('bisect', label)
|
||
|
], command_args)
|
||
|
mock_build_clang.assert_called_once_with(LLVM_REPO_PATH)
|
||
|
|
||
|
def test_test_start_commit_bad_nonzero(self):
|
||
|
"""Tests test_start_commit works as intended when the test on the first bad
|
||
|
commit returns nonzero."""
|
||
|
self._test_test_start_commit_expected('bad', self.bad_commit, 1) # pylint: disable=no-value-for-parameter
|
||
|
|
||
|
@mock.patch('bisect_clang.GitRepo.test_start_commit')
|
||
|
def test_bisect_start(self, mock_test_start_commit):
|
||
|
"""Tests bisect_start works as intended."""
|
||
|
with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen:
|
||
|
self.git.bisect_start(self.good_commit, self.bad_commit,
|
||
|
self.test_command)
|
||
|
self.assertEqual(
|
||
|
get_git_command('bisect', 'start'), mock_popen.commands[0])
|
||
|
mock_test_start_commit.assert_has_calls([
|
||
|
mock.call('bad_commit', 'bad', 'testcommand'),
|
||
|
mock.call('good_commit', 'good', 'testcommand')
|
||
|
])
|
||
|
|
||
|
def test_do_bisect_command(self):
|
||
|
"""Test do_bisect_command executes a git bisect subcommand as intended."""
|
||
|
subcommand = 'subcommand'
|
||
|
with mock.patch('subprocess.Popen', create_mock_popen()) as mock_popen:
|
||
|
self.git.do_bisect_command(subcommand)
|
||
|
self.assertEqual([get_git_command('bisect', subcommand)],
|
||
|
mock_popen.commands)
|
||
|
|
||
|
@mock.patch('bisect_clang.build_clang')
|
||
|
def _test_test_commit(self, label, output, returncode, mock_build_clang):
|
||
|
"""Test test_commit works as intended."""
|
||
|
command_args = []
|
||
|
|
||
|
def mock_execute(command, *args, **kwargs): # pylint: disable=unused-argument
|
||
|
command_args.append(command)
|
||
|
if command == self.test_command:
|
||
|
return returncode, output, ''
|
||
|
return 0, output, ''
|
||
|
|
||
|
with mock.patch('bisect_clang.execute', mock_execute):
|
||
|
result = self.git.test_commit(self.test_command)
|
||
|
self.assertEqual([self.test_command,
|
||
|
get_git_command('bisect', label)], command_args)
|
||
|
mock_build_clang.assert_called_once_with(LLVM_REPO_PATH)
|
||
|
return result
|
||
|
|
||
|
def test_test_commit_good(self):
|
||
|
"""Test test_commit labels a good commit as good."""
|
||
|
self.assertIsNone(self._test_test_commit('good', '', 0)) # pylint: disable=no-value-for-parameter
|
||
|
|
||
|
def test_test_commit_bad(self):
|
||
|
"""Test test_commit labels a bad commit as bad."""
|
||
|
self.assertIsNone(self._test_test_commit('bad', '', 1)) # pylint: disable=no-value-for-parameter
|
||
|
|
||
|
def test_test_commit_culprit(self):
|
||
|
"""Test test_commit returns the culprit"""
|
||
|
test_data = read_test_data('culprit-commit.txt')
|
||
|
self.assertEqual('ac9ee01fcbfac745aaedca0393a8e1c8a33acd8d',
|
||
|
self._test_test_commit('good', test_data, 0)) # pylint: disable=no-value-for-parameter
|
||
|
|
||
|
|
||
|
class GetTargetArchToBuildTest(unittest.TestCase):
|
||
|
"""Tests for get_target_arch_to_build."""
|
||
|
|
||
|
def test_unrecognized(self):
|
||
|
"""Test that an unrecognized architecture raises an exception."""
|
||
|
with mock.patch('bisect_clang.execute') as mock_execute:
|
||
|
mock_execute.return_value = (None, 'mips', None)
|
||
|
with self.assertRaises(Exception):
|
||
|
bisect_clang.get_clang_target_arch()
|
||
|
|
||
|
def test_recognized(self):
|
||
|
"""Test that a recognized architecture returns the expected value."""
|
||
|
arch_pairs = {'x86_64': 'X86', 'aarch64': 'AArch64'}
|
||
|
for uname_result, clang_target in arch_pairs.items():
|
||
|
with mock.patch('bisect_clang.execute') as mock_execute:
|
||
|
mock_execute.return_value = (None, uname_result, None)
|
||
|
self.assertEqual(clang_target, bisect_clang.get_clang_target_arch())
|