1# Copyright 2020 Google LLC 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"""Tests the functionality of the cifuzz module.""" 15import os 16import shutil 17import sys 18import tempfile 19import unittest 20from unittest import mock 21 22import parameterized 23 24# pylint: disable=wrong-import-position 25INFRA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 26sys.path.append(INFRA_DIR) 27 28OSS_FUZZ_DIR = os.path.dirname(INFRA_DIR) 29 30import build_fuzzers 31import continuous_integration 32import repo_manager 33import test_helpers 34 35# NOTE: This integration test relies on 36# https://github.com/google/oss-fuzz/tree/master/projects/example project. 37EXAMPLE_PROJECT = 'example' 38 39# Location of data used for testing. 40TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 41 'test_data') 42 43# An example fuzzer that triggers an crash. 44# Binary is a copy of the example project's do_stuff_fuzzer and can be 45# generated by running "python3 infra/helper.py build_fuzzers example". 46EXAMPLE_CRASH_FUZZER = 'example_crash_fuzzer' 47 48# An example fuzzer that does not trigger a crash. 49# Binary is a modified version of example project's do_stuff_fuzzer. It is 50# created by removing the bug in my_api.cpp. 51EXAMPLE_NOCRASH_FUZZER = 'example_nocrash_fuzzer' 52 53# A fuzzer to be built in build_fuzzers integration tests. 54EXAMPLE_BUILD_FUZZER = 'do_stuff_fuzzer' 55 56# pylint: disable=no-self-use,protected-access,too-few-public-methods,unused-argument 57 58 59class BuildFuzzersTest(unittest.TestCase): 60 """Unit tests for build_fuzzers.""" 61 62 @mock.patch('build_specified_commit.detect_main_repo', 63 return_value=('example.com', '/path')) 64 @mock.patch('repo_manager._clone', return_value=None) 65 @mock.patch('continuous_integration.checkout_specified_commit') 66 @mock.patch('helper.docker_run', return_value=False) # We want to quit early. 67 def test_cifuzz_env_var(self, mock_docker_run, _, __, ___): 68 """Tests that the CIFUZZ env var is set.""" 69 70 with tempfile.TemporaryDirectory() as tmp_dir: 71 build_fuzzers.build_fuzzers( 72 test_helpers.create_build_config( 73 oss_fuzz_project_name=EXAMPLE_PROJECT, 74 project_repo_name=EXAMPLE_PROJECT, 75 workspace=tmp_dir, 76 pr_ref='refs/pull/1757/merge')) 77 78 docker_run_command = mock_docker_run.call_args_list[0][0][0] 79 80 def command_has_env_var_arg(command, env_var_arg): 81 for idx, element in enumerate(command): 82 if idx == 0: 83 continue 84 85 if element == env_var_arg and command[idx - 1] == '-e': 86 return True 87 return False 88 89 self.assertTrue(command_has_env_var_arg(docker_run_command, 'CIFUZZ=True')) 90 91 92class InternalGithubBuildTest(unittest.TestCase): 93 """Tests for building OSS-Fuzz projects on GitHub actions.""" 94 PROJECT_REPO_NAME = 'myproject' 95 SANITIZER = 'address' 96 COMMIT_SHA = 'fake' 97 PR_REF = 'fake' 98 99 def _create_builder(self, tmp_dir, oss_fuzz_project_name='myproject'): 100 """Creates an InternalGithubBuilder and returns it.""" 101 config = test_helpers.create_build_config( 102 oss_fuzz_project_name=oss_fuzz_project_name, 103 project_repo_name=self.PROJECT_REPO_NAME, 104 workspace=tmp_dir, 105 sanitizer=self.SANITIZER, 106 commit_sha=self.COMMIT_SHA, 107 pr_ref=self.PR_REF, 108 is_github=True) 109 ci_system = continuous_integration.get_ci(config) 110 builder = build_fuzzers.Builder(config, ci_system) 111 builder.repo_manager = repo_manager.RepoManager('/fake') 112 return builder 113 114 @mock.patch('repo_manager._clone', side_effect=None) 115 @mock.patch('continuous_integration.checkout_specified_commit', 116 side_effect=None) 117 def test_correct_host_repo_path(self, _, __): 118 """Tests that the correct self.host_repo_path is set by 119 build_image_and_checkout_src. Specifically, we want the name of the 120 directory the repo is in to match the name used in the docker 121 image/container, so that it will replace the host's copy properly.""" 122 image_repo_path = '/src/repo_dir' 123 with tempfile.TemporaryDirectory() as tmp_dir, mock.patch( 124 'build_specified_commit.detect_main_repo', 125 return_value=('inferred_url', image_repo_path)): 126 builder = self._create_builder(tmp_dir) 127 builder.build_image_and_checkout_src() 128 129 self.assertEqual(os.path.basename(builder.host_repo_path), 130 os.path.basename(image_repo_path)) 131 132 @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_build', 133 return_value=True) 134 def test_upload_build_disabled(self, mock_upload_build): 135 """Test upload build (disabled).""" 136 with tempfile.TemporaryDirectory() as tmp_dir: 137 builder = self._create_builder(tmp_dir) 138 builder.upload_build() 139 140 mock_upload_build.assert_not_called() 141 142 @mock.patch('repo_manager.RepoManager.get_current_commit', 143 return_value='commit') 144 @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.upload_build', 145 return_value=True) 146 def test_upload_build(self, mock_upload_build, mock_get_current_commit): 147 """Test upload build.""" 148 with tempfile.TemporaryDirectory() as tmp_dir: 149 builder = self._create_builder(tmp_dir, oss_fuzz_project_name='') 150 builder.config.upload_build = True 151 builder.upload_build() 152 153 mock_upload_build.assert_called_with('commit') 154 155 156@unittest.skipIf(not os.getenv('INTEGRATION_TESTS'), 157 'INTEGRATION_TESTS=1 not set') 158class BuildFuzzersIntegrationTest(unittest.TestCase): 159 """Integration tests for build_fuzzers.""" 160 161 def setUp(self): 162 self.temp_dir_obj = tempfile.TemporaryDirectory() 163 self.workspace = self.temp_dir_obj.name 164 self.out_dir = os.path.join(self.workspace, 'build-out') 165 test_helpers.patch_environ(self) 166 167 base_runner_path = os.path.join(INFRA_DIR, 'base-images', 'base-runner') 168 os.environ['PATH'] = os.environ['PATH'] + os.pathsep + base_runner_path 169 170 def tearDown(self): 171 self.temp_dir_obj.cleanup() 172 173 def test_external_github_project(self): 174 """Tests building fuzzers from an external project on Github.""" 175 project_repo_name = 'external-project' 176 git_url = 'https://github.com/jonathanmetzman/cifuzz-external-example.git' 177 # This test is dependant on the state of 178 # github.com/jonathanmetzman/cifuzz-external-example. 179 config = test_helpers.create_build_config( 180 project_repo_name=project_repo_name, 181 workspace=self.workspace, 182 git_url=git_url, 183 commit_sha='HEAD', 184 is_github=True, 185 base_commit='HEAD^1') 186 self.assertTrue(build_fuzzers.build_fuzzers(config)) 187 self.assertTrue( 188 os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) 189 190 def test_external_generic_project(self): 191 """Tests building fuzzers from an external project not on Github.""" 192 project_repo_name = 'cifuzz-external-example' 193 git_url = 'https://github.com/jonathanmetzman/cifuzz-external-example.git' 194 # This test is dependant on the state of 195 # github.com/jonathanmetzman/cifuzz-external-example. 196 manager = repo_manager.clone_repo_and_get_manager( 197 'https://github.com/jonathanmetzman/cifuzz-external-example', 198 self.temp_dir_obj.name) 199 project_src_path = manager.repo_dir 200 config = test_helpers.create_build_config( 201 project_repo_name=project_repo_name, 202 workspace=self.workspace, 203 git_url=git_url, 204 commit_sha='HEAD', 205 project_src_path=project_src_path, 206 base_commit='HEAD^1') 207 self.assertTrue(build_fuzzers.build_fuzzers(config)) 208 self.assertTrue( 209 os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) 210 211 def test_valid_commit(self): 212 """Tests building fuzzers with valid inputs.""" 213 config = test_helpers.create_build_config( 214 oss_fuzz_project_name=EXAMPLE_PROJECT, 215 project_repo_name='oss-fuzz', 216 workspace=self.workspace, 217 commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523', 218 base_commit='da0746452433dc18bae699e355a9821285d863c8', 219 is_github=True) 220 self.assertTrue(build_fuzzers.build_fuzzers(config)) 221 self.assertTrue( 222 os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) 223 224 def test_valid_pull_request(self): 225 """Tests building fuzzers with valid pull request.""" 226 config = test_helpers.create_build_config( 227 oss_fuzz_project_name=EXAMPLE_PROJECT, 228 project_repo_name='oss-fuzz', 229 workspace=self.workspace, 230 pr_ref='refs/pull/1757/merge', 231 base_ref='master', 232 is_github=True) 233 self.assertTrue(build_fuzzers.build_fuzzers(config)) 234 self.assertTrue( 235 os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER))) 236 237 def test_invalid_pull_request(self): 238 """Tests building fuzzers with invalid pull request.""" 239 config = test_helpers.create_build_config( 240 oss_fuzz_project_name=EXAMPLE_PROJECT, 241 project_repo_name='oss-fuzz', 242 workspace=self.workspace, 243 pr_ref='ref-1/merge', 244 base_ref='master', 245 is_github=True) 246 self.assertTrue(build_fuzzers.build_fuzzers(config)) 247 248 def test_invalid_oss_fuzz_project_name(self): 249 """Tests building fuzzers with invalid project name.""" 250 config = test_helpers.create_build_config( 251 oss_fuzz_project_name='not_a_valid_project', 252 project_repo_name='oss-fuzz', 253 workspace=self.workspace, 254 commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') 255 self.assertFalse(build_fuzzers.build_fuzzers(config)) 256 257 def test_invalid_repo_name(self): 258 """Tests building fuzzers with invalid repo name.""" 259 config = test_helpers.create_build_config( 260 oss_fuzz_project_name=EXAMPLE_PROJECT, 261 project_repo_name='not-real-repo', 262 workspace=self.workspace, 263 commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') 264 self.assertFalse(build_fuzzers.build_fuzzers(config)) 265 266 def test_invalid_commit_sha(self): 267 """Tests building fuzzers with invalid commit SHA.""" 268 config = test_helpers.create_build_config( 269 oss_fuzz_project_name=EXAMPLE_PROJECT, 270 project_repo_name='oss-fuzz', 271 workspace=self.workspace, 272 commit_sha='', 273 is_github=True) 274 with self.assertRaises(AssertionError): 275 build_fuzzers.build_fuzzers(config) 276 277 def test_invalid_workspace(self): 278 """Tests building fuzzers with invalid workspace.""" 279 config = test_helpers.create_build_config( 280 oss_fuzz_project_name=EXAMPLE_PROJECT, 281 project_repo_name='oss-fuzz', 282 workspace=os.path.join(self.workspace, 'not', 'a', 'dir'), 283 commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523') 284 self.assertFalse(build_fuzzers.build_fuzzers(config)) 285 286 287class CheckFuzzerBuildTest(unittest.TestCase): 288 """Tests the check_fuzzer_build function in the cifuzz module.""" 289 290 SANITIZER = 'address' 291 LANGUAGE = 'c++' 292 293 def setUp(self): 294 self.temp_dir_obj = tempfile.TemporaryDirectory() 295 workspace_path = os.path.join(self.temp_dir_obj.name, 'workspace') 296 self.config = test_helpers.create_build_config( 297 oss_fuzz_project_name=EXAMPLE_PROJECT, 298 sanitizer=self.SANITIZER, 299 language=self.LANGUAGE, 300 workspace=workspace_path, 301 pr_ref='refs/pull/1757/merge') 302 self.workspace = test_helpers.create_workspace(workspace_path) 303 shutil.copytree(TEST_DATA_PATH, workspace_path) 304 test_helpers.patch_environ(self, runner=True) 305 306 def tearDown(self): 307 self.temp_dir_obj.cleanup() 308 309 def test_correct_fuzzer_build(self): 310 """Checks check_fuzzer_build function returns True for valid fuzzers.""" 311 self.assertTrue(build_fuzzers.check_fuzzer_build(self.config)) 312 313 def test_not_a_valid_path(self): 314 """Tests that False is returned when a nonexistent path is given.""" 315 self.config.workspace = 'not/a/valid/path' 316 self.assertFalse(build_fuzzers.check_fuzzer_build(self.config)) 317 318 def test_no_valid_fuzzers(self): 319 """Tests that False is returned when an empty directory is given.""" 320 with tempfile.TemporaryDirectory() as tmp_dir: 321 self.config.workspace = tmp_dir 322 os.mkdir(os.path.join(self.config.workspace, 'build-out')) 323 self.assertFalse(build_fuzzers.check_fuzzer_build(self.config)) 324 325 @mock.patch('utils.execute', return_value=(None, None, 0)) 326 def test_allow_broken_fuzz_targets_percentage(self, mock_execute): 327 """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running 328 docker if passed to check_fuzzer_build.""" 329 percentage = '0' 330 self.config.allowed_broken_targets_percentage = percentage 331 build_fuzzers.check_fuzzer_build(self.config) 332 self.assertEqual( 333 mock_execute.call_args[1]['env']['ALLOWED_BROKEN_TARGETS_PERCENTAGE'], 334 percentage) 335 336 337@unittest.skip('Test is too long to be run with presubmit.') 338class BuildSantizerIntegrationTest(unittest.TestCase): 339 """Integration tests for the build_fuzzers. 340 Note: This test relies on "curl" being an OSS-Fuzz project.""" 341 PROJECT_NAME = 'curl' 342 PR_REF = 'fake_pr' 343 344 @classmethod 345 def _create_config(cls, tmp_dir, sanitizer): 346 return test_helpers.create_build_config( 347 oss_fuzz_project_name=cls.PROJECT_NAME, 348 project_repo_name=cls.PROJECT_NAME, 349 workspace=tmp_dir, 350 pr_ref=cls.PR_REF, 351 sanitizer=sanitizer) 352 353 @parameterized.parameterized.expand([('memory',), ('undefined',)]) 354 def test_valid_project_curl(self, sanitizer): 355 """Tests that MSAN can be detected from project.yaml""" 356 with tempfile.TemporaryDirectory() as tmp_dir: 357 self.assertTrue( 358 build_fuzzers.build_fuzzers(self._create_config(tmp_dir, sanitizer))) 359 360 361class GetDockerBuildFuzzersArgsNotContainerTest(unittest.TestCase): 362 """Tests that _get_docker_build_fuzzers_args_not_container works as 363 intended.""" 364 365 def test_get_docker_build_fuzzers_args_no_container(self): 366 """Tests that _get_docker_build_fuzzers_args_not_container works 367 as intended.""" 368 host_repo_path = '/host/repo' 369 result = build_fuzzers._get_docker_build_fuzzers_args_not_container( 370 host_repo_path) 371 expected_result = ['-v', '/host/repo:/host/repo'] 372 self.assertEqual(result, expected_result) 373 374 375if __name__ == '__main__': 376 unittest.main() 377