| # 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 |
| |
| |
| 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, mocked_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 = mocked_docker_run.call_args_list[0][0][0] |
| |
| def command_has_env_var_arg(command, env_var_arg): |
| for idx, element in enumerate(command): |
| if idx == 0: |
| continue |
| |
| if element == env_var_arg and command[idx - 1] == '-e': |
| return True |
| return False |
| |
| self.assertTrue(command_has_env_var_arg(docker_run_command, 'CIFUZZ=True')) |
| |
| |
| class InternalGithubBuildTest(unittest.TestCase): |
| """Tests for building OSS-Fuzz projects on GitHub actions.""" |
| PROJECT_NAME = 'myproject' |
| PROJECT_REPO_NAME = 'myproject' |
| SANITIZER = 'address' |
| COMMIT_SHA = 'fake' |
| PR_REF = 'fake' |
| |
| def _create_builder(self, tmp_dir): |
| """Creates an InternalGithubBuilder and returns it.""" |
| config = test_helpers.create_build_config( |
| oss_fuzz_project_name=self.PROJECT_NAME, |
| project_repo_name=self.PROJECT_REPO_NAME, |
| workspace=tmp_dir, |
| sanitizer=self.SANITIZER, |
| commit_sha=self.COMMIT_SHA, |
| pr_ref=self.PR_REF, |
| is_github=True) |
| ci_system = continuous_integration.get_ci(config) |
| return build_fuzzers.Builder(config, ci_system) |
| |
| @mock.patch('repo_manager._clone', side_effect=None) |
| @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)) |
| |
| |
| @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_obj = tempfile.TemporaryDirectory() |
| self.workspace = self.temp_dir_obj.name |
| self.out_dir = os.path.join(self.workspace, 'build-out') |
| test_helpers.patch_environ(self) |
| |
| def tearDown(self): |
| self.temp_dir_obj.cleanup() |
| |
| 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, |
| commit_sha='HEAD', |
| is_github=True, |
| 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.temp_dir_obj.name) |
| 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, |
| commit_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, |
| commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523', |
| base_commit='da0746452433dc18bae699e355a9821285d863c8', |
| is_github=True) |
| 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', |
| is_github=True) |
| 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', |
| is_github=True) |
| 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, |
| commit_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, |
| commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') |
| self.assertFalse(build_fuzzers.build_fuzzers(config)) |
| |
| def test_invalid_commit_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, |
| commit_sha='', |
| is_github=True) |
| 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'), |
| commit_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.workspace = test_helpers.create_workspace(workspace_path) |
| shutil.copytree(TEST_DATA_PATH, workspace_path) |
| |
| 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.workspace, self.SANITIZER, |
| self.LANGUAGE)) |
| |
| def test_not_a_valid_path(self): |
| """Tests that False is returned when a nonexistent path is given.""" |
| workspace = test_helpers.create_workspace('not/a/valid/path') |
| self.assertFalse( |
| build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER, |
| self.LANGUAGE)) |
| |
| def test_no_valid_fuzzers(self): |
| """Tests that False is returned when an empty directory is given.""" |
| with tempfile.TemporaryDirectory() as tmp_dir: |
| workspace = test_helpers.create_workspace(tmp_dir) |
| self.assertFalse( |
| build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER, |
| self.LANGUAGE)) |
| |
| @mock.patch('helper.docker_run') |
| def test_allow_broken_fuzz_targets_percentage(self, mocked_docker_run): |
| """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running |
| docker if passed to check_fuzzer_build.""" |
| mocked_docker_run.return_value = 0 |
| build_fuzzers.check_fuzzer_build(self.workspace, |
| self.SANITIZER, |
| self.LANGUAGE, |
| allowed_broken_targets_percentage='0') |
| self.assertIn('-e ALLOWED_BROKEN_TARGETS_PERCENTAGE=0', |
| ' '.join(mocked_docker_run.call_args[0][0])) |
| |
| |
| @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) |
| |
| |
| class GetDockerBuildFuzzersArgsMsanTest(unittest.TestCase): |
| """Tests that _get_docker_build_fuzzers_args_msan works as intended.""" |
| |
| def test_get_docker_build_fuzzers_args_msan(self): |
| """Tests that _get_docker_build_fuzzers_args_msan works as intended.""" |
| work_dir = '/work_dir' |
| result = build_fuzzers._get_docker_build_fuzzers_args_msan(work_dir) |
| expected_result = ['-e', 'MSAN_LIBS_PATH=/work_dir/msan'] |
| self.assertEqual(result, expected_result) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |