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