[CIFuzz] Support uploading coverage reports (#6078)

This commit is contained in:
jonathanmetzman 2021-07-21 09:19:47 -07:00 committed by GitHub
parent 6004ddf363
commit fde0a30492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 51 additions and 24 deletions

View File

@ -178,10 +178,8 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
def upload_coverage(self):
"""Uploads the coverage report to the filestore."""
# TODO(jonathanmetzman): Implement this.
raise NotImplementedError(
'Not implemented yet. Waiting until we can specify a directory for '
'coverage report directories.')
self.filestore.upload_directory(self.COVERAGE_NAME,
self.workspace.coverage_report)
def get_coverage(self, repo_path):
"""Returns the project coverage object for the project."""
@ -314,7 +312,7 @@ class NoClusterFuzzDeployment(BaseClusterFuzzDeployment):
"""Noop Implementation of upload_crashes."""
logging.info('Not uploading crashes because no ClusterFuzz deployment.')
def download_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument
def download_corpus(self, target_name):
"""Noop Implementation of download_corpus."""
logging.info('Not downloading corpus because no ClusterFuzz deployment.')
return self.make_empty_corpus_dir(target_name)

View File

@ -12,8 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Implementation of a filestore using Github actions artifacts."""
import os
import logging
import os
import shutil
import tarfile
import tempfile
import http_utils
import filestore
@ -21,6 +24,21 @@ from filestore.github_actions import github_api
from third_party.github_actions_toolkit.artifact import artifact_client
def tar_directory(directory, archive_path):
"""Tars a |directory| and stores archive at |archive_path|. |archive_path|
must end in .tar"""
assert archive_path.endswith('.tar')
# Do this because make_archive will append the extension to archive_path.
archive_path = os.path.splitext('.tar')[0]
parent_directory = os.path.dirname(os.path.abspath(directory))
basename = os.path.basename(directory)
shutil.make_archive(archive_path,
'tar',
root_dir=parent_directory,
base_dir=basename)
class GithubActionsFilestore(filestore.BaseFilestore):
"""Implementation of BaseFilestore using Github actions artifacts. Relies on
github_actions_toolkit for using the GitHub actions API and the github_api
@ -35,20 +53,12 @@ class GithubActionsFilestore(filestore.BaseFilestore):
def upload_directory(self, name, directory): # pylint: disable=no-self-use
"""Uploads |directory| as artifact with |name|."""
directory = os.path.abspath(directory)
with tempfile.TemporaryDirectory() as temp_dir:
archive_path = os.path.join(temp_dir, name + '.tar')
tar_directory(directory, archive_path)
file_paths = [archive_path]
# Get file paths.
file_paths = []
for root, _, curr_file_paths in os.walk(directory):
for file_path in curr_file_paths:
file_paths.append(os.path.join(root, file_path))
logging.debug('file_paths: %s', file_paths)
# TODO(metzman): Zip so that we can upload directories within directories
# and save time?
return artifact_client.upload_artifact(name, file_paths, directory)
return artifact_client.upload_artifact(name, file_paths, temp_dir)
def download_corpus(self, name, dst_directory): # pylint: disable=unused-argument,no-self-use
"""Downloads the corpus located at |name| to |dst_directory|."""
@ -69,8 +79,21 @@ class GithubActionsFilestore(filestore.BaseFilestore):
logging.warning('Could not download artifact: %s.', name)
return artifact
download_url = artifact['archive_download_url']
return http_utils.download_and_unpack_zip(
download_url, dst_directory, headers=self.github_api_http_headers)
with tempfile.TemporaryDirectory() as temp_dir:
if not http_utils.download_and_unpack_zip(
download_url, temp_dir, headers=self.github_api_http_headers):
return False
artifact_tarfile_path = os.path.join(temp_dir, name + '.tar')
if not os.path.exists(artifact_tarfile_path):
logging.error('Artifact zip did not contain a tarfile.')
return False
# TODO(jonathanmetzman): Replace this with archive.unpack from
# libClusterFuzz so we can avoid path traversal issues.
with tarfile.TarFile(artifact_tarfile_path) as artifact_tarfile:
artifact_tarfile.extractall(dst_directory)
return True
def _list_artifacts(self):
"""Returns a list of artifacts."""

View File

@ -44,4 +44,4 @@ def generate_coverage_report(fuzz_target_paths, workspace,
"""Generates a coverage report using Clang's source based coverage."""
download_corpora(fuzz_target_paths, clusterfuzz_deployment)
run_coverage_command(workspace, config)
# TODO(metzman): Upload this build to the filestore.
clusterfuzz_deployment.upload_coverage()

View File

@ -344,7 +344,10 @@ class CoverageReportIntegrationTest(unittest.TestCase):
def setUp(self):
test_helpers.patch_environ(self)
def test_coverage_report(self):
@mock.patch('third_party.github_actions_toolkit.artifact.artifact_client'
'.upload_artifact',
return_value=True)
def test_coverage_report(self, _):
"""Tests generation of coverage reports end-to-end, from building to
generation."""
@ -367,7 +370,10 @@ class CoverageReportIntegrationTest(unittest.TestCase):
workspace=workspace,
oss_fuzz_project_name=EXAMPLE_PROJECT,
sanitizer=self.SANITIZER,
run_fuzzers_mode='coverage')
run_fuzzers_mode='coverage',
is_github=True,
# Set build integration path so it's not internal.
build_integration_path='/')
result = run_fuzzers.run_fuzzers(run_config)
self.assertEqual(result, run_fuzzers.RunFuzzersResult.NO_BUG_FOUND)
expected_summary_path = os.path.join(