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