# Copyright 2020 Google LLC # # 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 the functionality of the cifuzz module.""" import os import shutil import sys import tempfile import unittest from unittest import mock import parameterized # pylint: disable=wrong-import-position INFRA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(INFRA_DIR) OSS_FUZZ_DIR = os.path.dirname(INFRA_DIR) import build_fuzzers import continuous_integration import repo_manager import test_helpers # NOTE: This integration test relies on # https://github.com/google/oss-fuzz/tree/master/projects/example project. EXAMPLE_PROJECT = 'example' # Location of data used for testing. TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_data') # An example fuzzer that triggers an crash. # Binary is a copy of the example project's do_stuff_fuzzer and can be # generated by running "python3 infra/helper.py build_fuzzers example". EXAMPLE_CRASH_FUZZER = 'example_crash_fuzzer' # An example fuzzer that does not trigger a crash. # Binary is a modified version of example project's do_stuff_fuzzer. It is # created by removing the bug in my_api.cpp. EXAMPLE_NOCRASH_FUZZER = 'example_nocrash_fuzzer' # A fuzzer to be built in build_fuzzers integration tests. EXAMPLE_BUILD_FUZZER = 'do_stuff_fuzzer' # pylint: disable=no-self-use,protected-access,too-few-public-methods,unused-argument def docker_command_has_env_var_arg(command, env_var_arg): """Returns True if a docker command has a specific env var argument.""" for idx, element in enumerate(command): if idx == 0: # We're looking for the variable which can't be the first argument. continue if element == env_var_arg and command[idx - 1] == '-e': return True return False class BuildFuzzersTest(unittest.TestCase): """Unit tests for build_fuzzers.""" @mock.patch('build_specified_commit.detect_main_repo', return_value=('example.com', '/path')) @mock.patch('repo_manager._clone', return_value=None) @mock.patch('continuous_integration.checkout_specified_commit') @mock.patch('helper.docker_run', return_value=False) # We want to quit early. def test_cifuzz_env_var(self, mock_docker_run, _, __, ___): """Tests that the CIFUZZ env var is set.""" with tempfile.TemporaryDirectory() as tmp_dir: build_fuzzers.build_fuzzers( test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name=EXAMPLE_PROJECT, workspace=tmp_dir, pr_ref='refs/pull/1757/merge')) docker_run_command = mock_docker_run.call_args_list[0][0][0] self.assertTrue( docker_command_has_env_var_arg(docker_run_command, 'CIFUZZ=True')) @mock.patch('build_specified_commit.detect_main_repo', return_value=('example.com', '/path')) @mock.patch('repo_manager._clone', return_value=None) @mock.patch('continuous_integration.checkout_specified_commit') @mock.patch('helper.docker_run', return_value=False) # We want to quit early. def test_extra_env_var(self, mock_docker_run, _, __, ___): """Tests that the CIFUZZ env var is set.""" extra_env_var = 'CFL_EXTRA_TOKEN' with tempfile.TemporaryDirectory() as tmp_dir: with mock.patch.dict(os.environ, {extra_env_var: 'BLAH'}): build_fuzzers.build_fuzzers( test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name=EXAMPLE_PROJECT, workspace=tmp_dir, pr_ref='refs/pull/1757/merge')) docker_run_command = mock_docker_run.call_args_list[0][0][0] self.assertTrue( docker_command_has_env_var_arg(docker_run_command, extra_env_var)) class InternalGithubBuildTest(unittest.TestCase): """Tests for building OSS-Fuzz projects on GitHub actions.""" PROJECT_REPO_NAME = 'myproject' SANITIZER = 'address' GIT_SHA = 'fake' PR_REF = 'fake' def _create_builder(self, tmp_dir, oss_fuzz_project_name='myproject'): """Creates an InternalGithubBuilder and returns it.""" config = test_helpers.create_build_config( oss_fuzz_project_name=oss_fuzz_project_name, project_repo_name=self.PROJECT_REPO_NAME, workspace=tmp_dir, sanitizer=self.SANITIZER, git_sha=self.GIT_SHA, pr_ref=self.PR_REF, cfl_platform='github') cfl_platform = continuous_integration.get_ci(config) builder = build_fuzzers.Builder(config, cfl_platform) builder.repo_manager = repo_manager.RepoManager('/fake') return builder @mock.patch('helper.docker_run', return_value=True) @mock.patch('continuous_integration.checkout_specified_commit', side_effect=None) def test_correct_host_repo_path(self, _, __): """Tests that the correct self.host_repo_path is set by build_image_and_checkout_src. Specifically, we want the name of the directory the repo is in to match the name used in the docker image/container, so that it will replace the host's copy properly.""" image_repo_path = '/src/repo_dir' with tempfile.TemporaryDirectory() as tmp_dir, mock.patch( 'build_specified_commit.detect_main_repo', return_value=('inferred_url', image_repo_path)): builder = self._create_builder(tmp_dir) builder.build_image_and_checkout_src() self.assertEqual(os.path.basename(builder.host_repo_path), os.path.basename(image_repo_path)) @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_build', return_value=True) def test_upload_build_disabled(self, mock_upload_build): """Test upload build (disabled).""" with tempfile.TemporaryDirectory() as tmp_dir: builder = self._create_builder(tmp_dir) builder.upload_build() mock_upload_build.assert_not_called() @mock.patch('repo_manager.RepoManager.get_current_commit', return_value='commit') @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_build', return_value=True) def test_upload_build(self, mock_upload_build, mock_get_current_commit): """Test upload build.""" with tempfile.TemporaryDirectory() as tmp_dir: builder = self._create_builder(tmp_dir, oss_fuzz_project_name='') builder.config.upload_build = True builder.upload_build() mock_upload_build.assert_called_with('commit') @unittest.skipIf(not os.getenv('INTEGRATION_TESTS'), 'INTEGRATION_TESTS=1 not set') class BuildFuzzersIntegrationTest(unittest.TestCase): """Integration tests for build_fuzzers.""" def setUp(self): self.temp_dir_ctx_manager = test_helpers.docker_temp_dir() self.workspace = self.temp_dir_ctx_manager.__enter__() self.out_dir = os.path.join(self.workspace, 'build-out') test_helpers.patch_environ(self) base_runner_path = os.path.join(INFRA_DIR, 'base-images', 'base-runner') os.environ['PATH'] = os.environ['PATH'] + os.pathsep + base_runner_path def tearDown(self): self.temp_dir_ctx_manager.__exit__(None, None, None) def test_external_github_project(self): """Tests building fuzzers from an external project on Github.""" project_repo_name = 'external-project' git_url = 'https://github.com/jonathanmetzman/cifuzz-external-example.git' # This test is dependant on the state of # github.com/jonathanmetzman/cifuzz-external-example. config = test_helpers.create_build_config( project_repo_name=project_repo_name, workspace=self.workspace, git_url=git_url, git_sha='HEAD', cfl_platform='github', base_commit='HEAD^1') self.assertTrue(build_fuzzers.build_fuzzers(config)) self.assertTrue( os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) def test_external_generic_project(self): """Tests building fuzzers from an external project not on Github.""" project_repo_name = 'cifuzz-external-example' git_url = 'https://github.com/jonathanmetzman/cifuzz-external-example.git' # This test is dependant on the state of # github.com/jonathanmetzman/cifuzz-external-example. manager = repo_manager.clone_repo_and_get_manager( 'https://github.com/jonathanmetzman/cifuzz-external-example', self.workspace) project_src_path = manager.repo_dir config = test_helpers.create_build_config( project_repo_name=project_repo_name, workspace=self.workspace, git_url=git_url, filestore='no_filestore', git_sha='HEAD', project_src_path=project_src_path, base_commit='HEAD^1') self.assertTrue(build_fuzzers.build_fuzzers(config)) self.assertTrue( os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) def test_valid_commit(self): """Tests building fuzzers with valid inputs.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='oss-fuzz', workspace=self.workspace, git_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523', base_commit='da0746452433dc18bae699e355a9821285d863c8', cfl_platform='github') self.assertTrue(build_fuzzers.build_fuzzers(config)) self.assertTrue( os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) def test_valid_pull_request(self): """Tests building fuzzers with valid pull request.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='oss-fuzz', workspace=self.workspace, pr_ref='refs/pull/1757/merge', base_ref='master', cfl_platform='github') self.assertTrue(build_fuzzers.build_fuzzers(config)) self.assertTrue( os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) def test_invalid_pull_request(self): """Tests building fuzzers with invalid pull request.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='oss-fuzz', workspace=self.workspace, pr_ref='ref-1/merge', base_ref='master', cfl_platform='github') self.assertTrue(build_fuzzers.build_fuzzers(config)) def test_invalid_oss_fuzz_project_name(self): """Tests building fuzzers with invalid project name.""" config = test_helpers.create_build_config( oss_fuzz_project_name='not_a_valid_project', project_repo_name='oss-fuzz', workspace=self.workspace, git_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') self.assertFalse(build_fuzzers.build_fuzzers(config)) def test_invalid_repo_name(self): """Tests building fuzzers with invalid repo name.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='not-real-repo', workspace=self.workspace, git_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') self.assertFalse(build_fuzzers.build_fuzzers(config)) def test_invalid_git_sha(self): """Tests building fuzzers with invalid commit SHA.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='oss-fuzz', workspace=self.workspace, git_sha='', cfl_platform='github') with self.assertRaises(AssertionError): build_fuzzers.build_fuzzers(config) def test_invalid_workspace(self): """Tests building fuzzers with invalid workspace.""" config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, project_repo_name='oss-fuzz', workspace=os.path.join(self.workspace, 'not', 'a', 'dir'), git_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') self.assertFalse(build_fuzzers.build_fuzzers(config)) class CheckFuzzerBuildTest(unittest.TestCase): """Tests the check_fuzzer_build function in the cifuzz module.""" SANITIZER = 'address' LANGUAGE = 'c++' def setUp(self): self.temp_dir_obj = tempfile.TemporaryDirectory() workspace_path = os.path.join(self.temp_dir_obj.name, 'workspace') self.config = test_helpers.create_build_config( oss_fuzz_project_name=EXAMPLE_PROJECT, sanitizer=self.SANITIZER, language=self.LANGUAGE, workspace=workspace_path, pr_ref='refs/pull/1757/merge') self.workspace = test_helpers.create_workspace(workspace_path) shutil.copytree(TEST_DATA_PATH, workspace_path) test_helpers.patch_environ(self, runner=True) def tearDown(self): self.temp_dir_obj.cleanup() def test_correct_fuzzer_build(self): """Checks check_fuzzer_build function returns True for valid fuzzers.""" self.assertTrue(build_fuzzers.check_fuzzer_build(self.config)) def test_not_a_valid_path(self): """Tests that False is returned when a nonexistent path is given.""" self.config.workspace = 'not/a/valid/path' self.assertFalse(build_fuzzers.check_fuzzer_build(self.config)) def test_no_valid_fuzzers(self): """Tests that False is returned when an empty directory is given.""" with tempfile.TemporaryDirectory() as tmp_dir: self.config.workspace = tmp_dir os.mkdir(os.path.join(self.config.workspace, 'build-out')) self.assertFalse(build_fuzzers.check_fuzzer_build(self.config)) @mock.patch('utils.execute', return_value=(None, None, 0)) def test_allow_broken_fuzz_targets_percentage(self, mock_execute): """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running docker if passed to check_fuzzer_build.""" percentage = '0' self.config.allowed_broken_targets_percentage = percentage build_fuzzers.check_fuzzer_build(self.config) self.assertEqual( mock_execute.call_args[1]['env']['ALLOWED_BROKEN_TARGETS_PERCENTAGE'], percentage) @unittest.skip('Test is too long to be run with presubmit.') class BuildSantizerIntegrationTest(unittest.TestCase): """Integration tests for the build_fuzzers. Note: This test relies on "curl" being an OSS-Fuzz project.""" PROJECT_NAME = 'curl' PR_REF = 'fake_pr' @classmethod def _create_config(cls, tmp_dir, sanitizer): return test_helpers.create_build_config( oss_fuzz_project_name=cls.PROJECT_NAME, project_repo_name=cls.PROJECT_NAME, workspace=tmp_dir, pr_ref=cls.PR_REF, sanitizer=sanitizer) @parameterized.parameterized.expand([('memory',), ('undefined',)]) def test_valid_project_curl(self, sanitizer): """Tests that MSAN can be detected from project.yaml""" with tempfile.TemporaryDirectory() as tmp_dir: self.assertTrue( build_fuzzers.build_fuzzers(self._create_config(tmp_dir, sanitizer))) class GetDockerBuildFuzzersArgsNotContainerTest(unittest.TestCase): """Tests that _get_docker_build_fuzzers_args_not_container works as intended.""" def test_get_docker_build_fuzzers_args_no_container(self): """Tests that _get_docker_build_fuzzers_args_not_container works as intended.""" host_repo_path = '/host/repo' result = build_fuzzers._get_docker_build_fuzzers_args_not_container( host_repo_path) expected_result = ['-v', '/host/repo:/host/repo'] self.assertEqual(result, expected_result) if __name__ == '__main__': unittest.main()