• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 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 for clusterfuzz_deployment.py"""
15
16import os
17import unittest
18from unittest import mock
19
20import parameterized
21from pyfakefs import fake_filesystem_unittest
22
23import clusterfuzz_deployment
24import config_utils
25import test_helpers
26import workspace_utils
27
28# NOTE: This integration test relies on
29# https://github.com/google/oss-fuzz/tree/master/projects/example project.
30EXAMPLE_PROJECT = 'example'
31
32# An example fuzzer that triggers an error.
33EXAMPLE_FUZZER = 'example_crash_fuzzer'
34
35WORKSPACE = '/workspace'
36EXPECTED_LATEST_BUILD_PATH = os.path.join(WORKSPACE, 'cifuzz-prev-build')
37
38# pylint: disable=unused-argument
39
40
41def _create_config(**kwargs):
42  """Creates a config object and then sets every attribute that is a key in
43  |kwargs| to the corresponding value. Asserts that each key in |kwargs| is an
44  attribute of Config."""
45  defaults = {
46      'is_github': True,
47      'oss_fuzz_project_name': EXAMPLE_PROJECT,
48      'workspace': WORKSPACE,
49  }
50  for default_key, default_value in defaults.items():
51    if default_key not in kwargs:
52      kwargs[default_key] = default_value
53
54  return test_helpers.create_run_config(**kwargs)
55
56
57def _create_deployment(**kwargs):
58  config = _create_config(**kwargs)
59  workspace = workspace_utils.Workspace(config)
60  return clusterfuzz_deployment.get_clusterfuzz_deployment(config, workspace)
61
62
63class OSSFuzzTest(fake_filesystem_unittest.TestCase):
64  """Tests OSSFuzz."""
65
66  def setUp(self):
67    self.setUpPyfakefs()
68    self.deployment = _create_deployment()
69    self.corpus_dir = os.path.join(self.deployment.workspace.corpora,
70                                   EXAMPLE_FUZZER)
71
72  @mock.patch('http_utils.download_and_unpack_zip', return_value=True)
73  def test_download_corpus(self, mock_download_and_unpack_zip):
74    """Tests that we can download a corpus for a valid project."""
75    self.deployment.download_corpus(EXAMPLE_FUZZER, self.corpus_dir)
76    expected_url = ('https://storage.googleapis.com/example-backup.'
77                    'clusterfuzz-external.appspot.com/corpus/libFuzzer/'
78                    'example_crash_fuzzer/public.zip')
79    call_args, _ = mock_download_and_unpack_zip.call_args
80    self.assertEqual(call_args, (expected_url, self.corpus_dir))
81    self.assertTrue(os.path.exists(self.corpus_dir))
82
83  @mock.patch('http_utils.download_and_unpack_zip', return_value=False)
84  def test_download_corpus_fail(self, _):
85    """Tests that when downloading fails, an empty corpus directory is still
86    returned."""
87    self.deployment.download_corpus(EXAMPLE_FUZZER, self.corpus_dir)
88    self.assertEqual(os.listdir(self.corpus_dir), [])
89
90  def test_get_latest_build_name(self):
91    """Tests that the latest build name can be retrieved from GCS."""
92    latest_build_name = self.deployment.get_latest_build_name()
93    self.assertTrue(latest_build_name.endswith('.zip'))
94    self.assertTrue('address' in latest_build_name)
95
96  @parameterized.parameterized.expand([
97      ('upload_build', ('commit',),
98       'Not uploading latest build because on OSS-Fuzz.'),
99      ('upload_corpus', ('target', 'corpus-dir'),
100       'Not uploading corpus because on OSS-Fuzz.'),
101      ('upload_crashes', tuple(), 'Not uploading crashes because on OSS-Fuzz.'),
102  ])
103  def test_noop_methods(self, method, method_args, expected_message):
104    """Tests that certain methods are noops for OSS-Fuzz."""
105    with mock.patch('logging.info') as mock_info:
106      method = getattr(self.deployment, method)
107      self.assertIsNone(method(*method_args))
108      mock_info.assert_called_with(expected_message)
109
110  @mock.patch('http_utils.download_and_unpack_zip', return_value=True)
111  def test_download_latest_build(self, mock_download_and_unpack_zip):
112    """Tests that downloading the latest build works as intended under normal
113    circumstances."""
114    self.assertEqual(self.deployment.download_latest_build(),
115                     EXPECTED_LATEST_BUILD_PATH)
116    expected_url = ('https://storage.googleapis.com/clusterfuzz-builds/example/'
117                    'example-address-202008030600.zip')
118    mock_download_and_unpack_zip.assert_called_with(expected_url,
119                                                    EXPECTED_LATEST_BUILD_PATH)
120
121  @mock.patch('http_utils.download_and_unpack_zip', return_value=False)
122  def test_download_latest_build_fail(self, _):
123    """Tests that download_latest_build returns None when it fails to download a
124    build."""
125    self.assertIsNone(self.deployment.download_latest_build())
126
127
128class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase):
129  """Tests for ClusterFuzzLite."""
130
131  def setUp(self):
132    self.setUpPyfakefs()
133    self.deployment = _create_deployment(run_fuzzers_mode='batch',
134                                         oss_fuzz_project_name='',
135                                         is_github=True)
136    self.corpus_dir = os.path.join(self.deployment.workspace.corpora,
137                                   EXAMPLE_FUZZER)
138
139  @mock.patch('filestore.github_actions.GithubActionsFilestore.download_corpus',
140              return_value=True)
141  def test_download_corpus(self, mock_download_corpus):
142    """Tests that download_corpus works for a valid project."""
143    self.deployment.download_corpus(EXAMPLE_FUZZER, self.corpus_dir)
144    mock_download_corpus.assert_called_with('example_crash_fuzzer',
145                                            self.corpus_dir)
146    self.assertTrue(os.path.exists(self.corpus_dir))
147
148  @mock.patch('filestore.github_actions.GithubActionsFilestore.download_corpus',
149              side_effect=Exception)
150  def test_download_corpus_fail(self, _):
151    """Tests that when downloading fails, an empty corpus directory is still
152    returned."""
153    self.deployment.download_corpus(EXAMPLE_FUZZER, self.corpus_dir)
154    self.assertEqual(os.listdir(self.corpus_dir), [])
155
156  @mock.patch('filestore.github_actions.GithubActionsFilestore.download_build',
157              side_effect=[False, True])
158  @mock.patch('repo_manager.RepoManager.get_commit_list',
159              return_value=['commit1', 'commit2'])
160  @mock.patch('continuous_integration.BaseCi.repo_dir',
161              return_value='/path/to/repo')
162  def test_download_latest_build(self, mock_repo_dir, mock_get_commit_list,
163                                 mock_download_build):
164    """Tests that downloading the latest build works as intended under normal
165    circumstances."""
166    self.assertEqual(self.deployment.download_latest_build(),
167                     EXPECTED_LATEST_BUILD_PATH)
168    expected_artifact_name = 'address-commit2'
169    mock_download_build.assert_called_with(expected_artifact_name,
170                                           EXPECTED_LATEST_BUILD_PATH)
171
172  @mock.patch('filestore.github_actions.GithubActionsFilestore.download_build',
173              side_effect=Exception)
174  @mock.patch('repo_manager.RepoManager.get_commit_list',
175              return_value=['commit1', 'commit2'])
176  @mock.patch('continuous_integration.BaseCi.repo_dir',
177              return_value='/path/to/repo')
178  def test_download_latest_build_fail(self, mock_repo_dir, mock_get_commit_list,
179                                      _):
180    """Tests that download_latest_build returns None when it fails to download a
181    build."""
182    self.assertIsNone(self.deployment.download_latest_build())
183
184  @mock.patch('filestore.github_actions.GithubActionsFilestore.upload_build')
185  def test_upload_build(self, mock_upload_build):
186    """Tests that upload_build works as intended."""
187    self.deployment.upload_build('commit')
188    mock_upload_build.assert_called_with('address-commit',
189                                         '/workspace/build-out')
190
191
192class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase):
193  """Tests for NoClusterFuzzDeployment."""
194
195  def setUp(self):
196    self.setUpPyfakefs()
197    config = test_helpers.create_run_config(workspace=WORKSPACE,
198                                            is_github=False)
199    workspace = workspace_utils.Workspace(config)
200    self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(
201        config, workspace)
202    self.corpus_dir = os.path.join(workspace.corpora, EXAMPLE_FUZZER)
203
204  @mock.patch('logging.info')
205  def test_download_corpus(self, mock_info):
206    """Tests that download corpus returns the path to the empty corpus
207    directory."""
208    self.deployment.download_corpus(EXAMPLE_FUZZER, self.corpus_dir)
209    mock_info.assert_called_with(
210        'Not downloading corpus because no ClusterFuzz deployment.')
211    self.assertTrue(os.path.exists(self.corpus_dir))
212
213  @parameterized.parameterized.expand([
214      ('upload_build', ('commit',),
215       'Not uploading latest build because no ClusterFuzz deployment.'),
216      ('upload_corpus', ('target', 'corpus-dir'),
217       'Not uploading corpus because no ClusterFuzz deployment.'),
218      ('upload_crashes', tuple(),
219       'Not uploading crashes because no ClusterFuzz deployment.'),
220      ('download_latest_build', tuple(),
221       'Not downloading latest build because no ClusterFuzz deployment.')
222  ])
223  def test_noop_methods(self, method, method_args, expected_message):
224    """Tests that certain methods are noops for NoClusterFuzzDeployment."""
225    with mock.patch('logging.info') as mock_info:
226      method = getattr(self.deployment, method)
227      self.assertIsNone(method(*method_args))
228      mock_info.assert_called_with(expected_message)
229
230
231class GetClusterFuzzDeploymentTest(unittest.TestCase):
232  """Tests for get_clusterfuzz_deployment."""
233
234  def setUp(self):
235    test_helpers.patch_environ(self)
236    os.environ['GITHUB_REPOSITORY'] = 'owner/myproject'
237
238  @parameterized.parameterized.expand([
239      (config_utils.BaseConfig.Platform.INTERNAL_GENERIC_CI,
240       clusterfuzz_deployment.OSSFuzz),
241      (config_utils.BaseConfig.Platform.INTERNAL_GITHUB,
242       clusterfuzz_deployment.OSSFuzz),
243      (config_utils.BaseConfig.Platform.EXTERNAL_GENERIC_CI,
244       clusterfuzz_deployment.NoClusterFuzzDeployment),
245      (config_utils.BaseConfig.Platform.EXTERNAL_GITHUB,
246       clusterfuzz_deployment.ClusterFuzzLite),
247  ])
248  def test_get_clusterfuzz_deployment(self, platform, expected_deployment_cls):
249    """Tests that get_clusterfuzz_deployment returns the correct value."""
250    with mock.patch('config_utils.BaseConfig.platform',
251                    return_value=platform,
252                    new_callable=mock.PropertyMock):
253      with mock.patch('filestore_utils.get_filestore', return_value=None):
254        config = _create_config()
255        workspace = workspace_utils.Workspace(config)
256
257        self.assertIsInstance(
258            clusterfuzz_deployment.get_clusterfuzz_deployment(
259                config, workspace), expected_deployment_cls)
260
261
262if __name__ == '__main__':
263  unittest.main()
264