• 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 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