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 github_actions.""" 15import os 16import shutil 17import sys 18import tarfile 19import tempfile 20import unittest 21from unittest import mock 22 23from pyfakefs import fake_filesystem_unittest 24 25# pylint: disable=wrong-import-position 26INFRA_DIR = os.path.dirname( 27 os.path.dirname(os.path.dirname(os.path.dirname( 28 os.path.abspath(__file__))))) 29sys.path.append(INFRA_DIR) 30 31from filestore import github_actions 32import test_helpers 33 34# pylint: disable=protected-access,no-self-use 35 36 37class GithubActionsFilestoreTest(fake_filesystem_unittest.TestCase): 38 """Tests for GithubActionsFilestore.""" 39 40 def setUp(self): 41 test_helpers.patch_environ(self) 42 self.token = 'example githubtoken' 43 self.owner = 'exampleowner' 44 self.repo = 'examplerepo' 45 os.environ['GITHUB_REPOSITORY'] = f'{self.owner}/{self.repo}' 46 os.environ['GITHUB_EVENT_PATH'] = '/fake' 47 self.config = test_helpers.create_run_config(token=self.token) 48 self.local_dir = '/local-dir' 49 self.testcase = os.path.join(self.local_dir, 'testcase') 50 51 def _get_expected_http_headers(self): 52 return { 53 'Authorization': f'token {self.token}', 54 'Accept': 'application/vnd.github.v3+json', 55 } 56 57 @mock.patch('filestore.github_actions.github_api.list_artifacts') 58 def test_list_artifacts(self, mock_list_artifacts): 59 """Tests that _list_artifacts works as intended.""" 60 filestore = github_actions.GithubActionsFilestore(self.config) 61 filestore._list_artifacts() 62 mock_list_artifacts.assert_called_with(self.owner, self.repo, 63 self._get_expected_http_headers()) 64 65 @mock.patch('logging.warning') 66 @mock.patch('filestore.github_actions.GithubActionsFilestore._list_artifacts', 67 return_value=None) 68 @mock.patch('filestore.github_actions.github_api.find_artifact', 69 return_value=None) 70 def test_download_build_no_artifact(self, _, __, mock_warning): 71 """Tests that download_build returns None and doesn't exception when 72 find_artifact can't find an artifact.""" 73 filestore = github_actions.GithubActionsFilestore(self.config) 74 name = 'name' 75 build_dir = 'build-dir' 76 self.assertFalse(filestore.download_build(name, build_dir)) 77 mock_warning.assert_called_with('Could not download artifact: %s.', 78 'cifuzz-build-' + name) 79 80 @mock.patch('logging.warning') 81 @mock.patch('filestore.github_actions.GithubActionsFilestore._list_artifacts', 82 return_value=None) 83 @mock.patch('filestore.github_actions.github_api.find_artifact', 84 return_value=None) 85 def test_download_corpus_no_artifact(self, _, __, mock_warning): 86 """Tests that download_corpus_build returns None and doesn't exception when 87 find_artifact can't find an artifact.""" 88 filestore = github_actions.GithubActionsFilestore(self.config) 89 name = 'name' 90 dst_dir = 'local-dir' 91 self.assertFalse(filestore.download_corpus(name, dst_dir)) 92 mock_warning.assert_called_with('Could not download artifact: %s.', 93 'cifuzz-corpus-' + name) 94 95 @mock.patch('filestore.github_actions.tar_directory') 96 @mock.patch('filestore.github_actions._upload_artifact_with_upload_js') 97 def test_upload_corpus(self, mock_upload_artifact, mock_tar_directory): 98 """Test uploading corpus.""" 99 self._create_local_dir() 100 101 def mock_tar_directory_impl(_, archive_path): 102 self.fs.create_file(archive_path) 103 104 mock_tar_directory.side_effect = mock_tar_directory_impl 105 106 filestore = github_actions.GithubActionsFilestore(self.config) 107 filestore.upload_corpus('target', self.local_dir) 108 self.assert_upload(mock_upload_artifact, mock_tar_directory, 109 'corpus-target') 110 111 @mock.patch('filestore.github_actions._upload_artifact_with_upload_js') 112 def test_upload_crashes(self, mock_upload_artifact): 113 """Test uploading crashes.""" 114 self._create_local_dir() 115 116 filestore = github_actions.GithubActionsFilestore(self.config) 117 filestore.upload_crashes('current', self.local_dir) 118 mock_upload_artifact.assert_has_calls( 119 [mock.call('crashes-current', ['/local-dir/testcase'], '/local-dir')]) 120 121 @mock.patch('filestore.github_actions.tar_directory') 122 @mock.patch('filestore.github_actions._upload_artifact_with_upload_js') 123 def test_upload_build(self, mock_upload_artifact, mock_tar_directory): 124 """Test uploading build.""" 125 self._create_local_dir() 126 127 def mock_tar_directory_impl(_, archive_path): 128 self.fs.create_file(archive_path) 129 130 mock_tar_directory.side_effect = mock_tar_directory_impl 131 132 filestore = github_actions.GithubActionsFilestore(self.config) 133 filestore.upload_build('sanitizer', self.local_dir) 134 self.assert_upload(mock_upload_artifact, mock_tar_directory, 135 'build-sanitizer') 136 137 @mock.patch('filestore.github_actions.tar_directory') 138 @mock.patch('filestore.github_actions._upload_artifact_with_upload_js') 139 def test_upload_coverage(self, mock_upload_artifact, mock_tar_directory): 140 """Test uploading coverage.""" 141 self._create_local_dir() 142 143 def mock_tar_directory_impl(_, archive_path): 144 self.fs.create_file(archive_path) 145 146 mock_tar_directory.side_effect = mock_tar_directory_impl 147 148 filestore = github_actions.GithubActionsFilestore(self.config) 149 filestore.upload_coverage('latest', self.local_dir) 150 self.assert_upload(mock_upload_artifact, mock_tar_directory, 151 'coverage-latest') 152 153 def assert_upload(self, mock_upload_artifact, mock_tar_directory, 154 expected_artifact_name): 155 """Tests that upload_directory invokes tar_directory and 156 artifact_client.upload_artifact properly.""" 157 # Don't assert what second argument will be since it's a temporary 158 # directory. 159 self.assertEqual(mock_tar_directory.call_args_list[0][0][0], self.local_dir) 160 161 # Don't assert what second and third arguments will be since they are 162 # temporary directories. 163 expected_artifact_name = 'cifuzz-' + expected_artifact_name 164 self.assertEqual(mock_upload_artifact.call_args_list[0][0][0], 165 expected_artifact_name) 166 167 # Assert artifacts list contains one tarfile. 168 artifacts_list = mock_upload_artifact.call_args_list[0][0][1] 169 self.assertEqual(len(artifacts_list), 1) 170 self.assertEqual(os.path.basename(artifacts_list[0]), 171 expected_artifact_name + '.tar') 172 173 def _create_local_dir(self): 174 """Sets up pyfakefs and creates a corpus directory containing 175 self.testcase.""" 176 self.setUpPyfakefs() 177 self.fs.create_file(self.testcase, contents='hi') 178 179 @mock.patch('filestore.github_actions.GithubActionsFilestore._find_artifact') 180 @mock.patch('http_utils.download_and_unpack_zip') 181 def test_download_artifact(self, mock_download_and_unpack_zip, 182 mock_find_artifact): 183 """Tests that _download_artifact works as intended.""" 184 artifact_download_url = 'http://example.com/download' 185 artifact_listing = { 186 'expired': False, 187 'name': 'corpus', 188 'archive_download_url': artifact_download_url 189 } 190 mock_find_artifact.return_value = artifact_listing 191 192 self._create_local_dir() 193 with tempfile.TemporaryDirectory() as temp_dir: 194 # Create a tarball. 195 archive_path = os.path.join(temp_dir, 'cifuzz-corpus.tar') 196 github_actions.tar_directory(self.local_dir, archive_path) 197 198 artifact_download_dst_dir = os.path.join(temp_dir, 'dst') 199 os.mkdir(artifact_download_dst_dir) 200 201 def mock_download_and_unpack_zip_impl(url, download_artifact_temp_dir, 202 headers): 203 self.assertEqual(url, artifact_download_url) 204 self.assertEqual(headers, self._get_expected_http_headers()) 205 shutil.copy( 206 archive_path, 207 os.path.join(download_artifact_temp_dir, 208 os.path.basename(archive_path))) 209 return True 210 211 mock_download_and_unpack_zip.side_effect = ( 212 mock_download_and_unpack_zip_impl) 213 filestore = github_actions.GithubActionsFilestore(self.config) 214 self.assertTrue( 215 filestore._download_artifact('corpus', artifact_download_dst_dir)) 216 mock_find_artifact.assert_called_with('cifuzz-corpus') 217 self.assertTrue( 218 os.path.exists( 219 os.path.join(artifact_download_dst_dir, 220 os.path.basename(self.testcase)))) 221 222 @mock.patch('filestore.github_actions.github_api.list_artifacts') 223 def test_find_artifact(self, mock_list_artifacts): 224 """Tests that _find_artifact works as intended.""" 225 artifact_listing_1 = { 226 'expired': False, 227 'name': 'other', 228 'archive_download_url': 'http://download1' 229 } 230 artifact_listing_2 = { 231 'expired': False, 232 'name': 'artifact', 233 'archive_download_url': 'http://download2' 234 } 235 artifact_listing_3 = { 236 'expired': True, 237 'name': 'artifact', 238 'archive_download_url': 'http://download3' 239 } 240 artifact_listing_4 = { 241 'expired': False, 242 'name': 'artifact', 243 'archive_download_url': 'http://download4' 244 } 245 artifacts = [ 246 artifact_listing_1, artifact_listing_2, artifact_listing_3, 247 artifact_listing_4 248 ] 249 mock_list_artifacts.return_value = artifacts 250 filestore = github_actions.GithubActionsFilestore(self.config) 251 # Test that find_artifact will return the most recent unexpired artifact 252 # with the correct name. 253 self.assertEqual(filestore._find_artifact('artifact'), artifact_listing_2) 254 mock_list_artifacts.assert_called_with(self.owner, self.repo, 255 self._get_expected_http_headers()) 256 257 258class TarDirectoryTest(unittest.TestCase): 259 """Tests for tar_directory.""" 260 261 def test_tar_directory(self): 262 """Tests that tar_directory writes the archive to the correct location and 263 archives properly.""" 264 with tempfile.TemporaryDirectory() as temp_dir: 265 archive_path = os.path.join(temp_dir, 'myarchive.tar') 266 archived_dir = os.path.join(temp_dir, 'toarchive') 267 os.mkdir(archived_dir) 268 archived_filename = 'file1' 269 archived_file_path = os.path.join(archived_dir, archived_filename) 270 with open(archived_file_path, 'w') as file_handle: 271 file_handle.write('hi') 272 github_actions.tar_directory(archived_dir, archive_path) 273 self.assertTrue(os.path.exists(archive_path)) 274 275 # Now check it archives correctly. 276 unpacked_directory = os.path.join(temp_dir, 'unpacked') 277 with tarfile.TarFile(archive_path) as artifact_tarfile: 278 artifact_tarfile.extractall(unpacked_directory) 279 unpacked_archived_file_path = os.path.join(unpacked_directory, 280 archived_filename) 281 self.assertTrue(os.path.exists(unpacked_archived_file_path)) 282