aboutsummaryrefslogtreecommitdiff
path: root/infra
diff options
context:
space:
mode:
authorjonathanmetzman <31354670+jonathanmetzman@users.noreply.github.com>2021-07-20 10:04:57 -0700
committerGitHub <noreply@github.com>2021-07-20 10:04:57 -0700
commit86392ca2f4f8fc19a728cf4d59da022e4408503e (patch)
treea061e18e11d610fbe1661d9a0ba9053091f97d47 /infra
parentcdeb7f2282e283ab72c03bb2d5ae1e21f4b672b7 (diff)
downloadoss-fuzz-86392ca2f4f8fc19a728cf4d59da022e4408503e.tar.gz
[CIFuzz][ClusterFuzzLite] Prepare to support coverage reports for external users (#6074)
1. Make coverage script accept an env var that controls where the generated report is written to. Use this in generate_coverage_report. 2. Rename CoverageGetter to OSSFuzzCoverage 3. Make affected_fuzz_targets get coverage object from clusterfuzz_deployment. 4. Add stubbed FilesystemCoverage class that will be used by non-OSS-Fuzz users. 5. Make ClusterFuzzLite able to return coverage object by downloading old coverage report and returning a FilesystemCoverage object. 6. Add stub to ClusterFuzzLite for uploading coverage. Related: #6054
Diffstat (limited to 'infra')
-rwxr-xr-xinfra/base-images/base-runner/coverage12
-rw-r--r--infra/cifuzz/affected_fuzz_targets.py24
-rw-r--r--infra/cifuzz/affected_fuzz_targets_test.py17
-rw-r--r--infra/cifuzz/build_fuzzers.py6
-rw-r--r--infra/cifuzz/build_fuzzers_test.py2
-rw-r--r--infra/cifuzz/clusterfuzz_deployment.py51
-rw-r--r--infra/cifuzz/docker.py10
-rw-r--r--infra/cifuzz/filestore/__init__.py4
-rw-r--r--infra/cifuzz/filestore/github_actions/__init__.py4
-rw-r--r--infra/cifuzz/filestore/github_actions/github_api.py4
-rw-r--r--infra/cifuzz/generate_coverage_report.py3
-rw-r--r--infra/cifuzz/generate_coverage_report_test.py3
-rw-r--r--infra/cifuzz/get_coverage.py139
-rw-r--r--infra/cifuzz/get_coverage_test.py86
-rw-r--r--infra/cifuzz/http_utils.py18
-rw-r--r--infra/cifuzz/run_fuzzers_test.py4
16 files changed, 257 insertions, 130 deletions
diff --git a/infra/base-images/base-runner/coverage b/infra/base-images/base-runner/coverage
index 3d7228097..69853891e 100755
--- a/infra/base-images/base-runner/coverage
+++ b/infra/base-images/base-runner/coverage
@@ -27,11 +27,13 @@ else
-e 'jazzer_driver_with_sanitizer')"
fi
-DUMPS_DIR="$OUT/dumps"
-FUZZER_STATS_DIR="$OUT/fuzzer_stats"
-LOGS_DIR="$OUT/logs"
-REPORT_ROOT_DIR="$OUT/report"
-REPORT_PLATFORM_DIR="$OUT/report/linux"
+COVERAGE_OUTPUT_DIR=${COVERAGE_OUTPUT_DIR:-$OUT}
+
+DUMPS_DIR="$COVERAGE_OUTPUT_DIR/dumps"
+FUZZER_STATS_DIR="$COVERAGE_OUTPUT_DIR/fuzzer_stats"
+LOGS_DIR="$COVERAGE_OUTPUT_DIR/logs"
+REPORT_ROOT_DIR="$COVERAGE_OUTPUT_DIR/report"
+REPORT_PLATFORM_DIR="$COVERAGE_OUTPUT_DIR/report/linux"
for directory in $DUMPS_DIR $FUZZER_STATS_DIR $LOGS_DIR $REPORT_ROOT_DIR \
$REPORT_PLATFORM_DIR; do
diff --git a/infra/cifuzz/affected_fuzz_targets.py b/infra/cifuzz/affected_fuzz_targets.py
index f1dfe9b77..959170c3c 100644
--- a/infra/cifuzz/affected_fuzz_targets.py
+++ b/infra/cifuzz/affected_fuzz_targets.py
@@ -17,15 +17,13 @@ import logging
import os
import sys
-import get_coverage
-
# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
-def remove_unaffected_fuzz_targets(project_name, out_dir, files_changed,
- repo_path):
+def remove_unaffected_fuzz_targets(clusterfuzz_deployment, out_dir,
+ files_changed, repo_path):
"""Removes all non affected fuzz targets in the out directory.
Args:
@@ -38,7 +36,6 @@ def remove_unaffected_fuzz_targets(project_name, out_dir, files_changed,
targets are unaffected. For example, this means that fuzz targets which don't
have coverage data on will not be deleted.
"""
- # TODO(metzman): Make this use clusterfuzz deployment.
if not files_changed:
# Don't remove any fuzz targets if there is no difference from HEAD.
logging.info('No files changed compared to HEAD.')
@@ -52,14 +49,13 @@ def remove_unaffected_fuzz_targets(project_name, out_dir, files_changed,
logging.error('No fuzz targets found in out dir.')
return
- coverage_getter = get_coverage.OssFuzzCoverageGetter(project_name, repo_path)
- if not coverage_getter.fuzzer_stats_url:
+ coverage = clusterfuzz_deployment.get_coverage(repo_path)
+ if not coverage:
# Don't remove any fuzz targets unless we have data.
logging.error('Could not find latest coverage report.')
return
- affected_fuzz_targets = get_affected_fuzz_targets(coverage_getter,
- fuzz_target_paths,
+ affected_fuzz_targets = get_affected_fuzz_targets(coverage, fuzz_target_paths,
files_changed)
if not affected_fuzz_targets:
@@ -79,11 +75,11 @@ def remove_unaffected_fuzz_targets(project_name, out_dir, files_changed,
fuzz_target_path)
-def is_fuzz_target_affected(coverage_getter, fuzz_target_path, files_changed):
+def is_fuzz_target_affected(coverage, fuzz_target_path, files_changed):
"""Returns True if a fuzz target (|fuzz_target_path|) is affected by
|files_changed|."""
fuzz_target = os.path.basename(fuzz_target_path)
- covered_files = coverage_getter.get_files_covered_by_target(fuzz_target)
+ covered_files = coverage.get_files_covered_by_target(fuzz_target)
if not covered_files:
# Assume a fuzz target is affected if we can't get its coverage from
# OSS-Fuzz.
@@ -104,13 +100,11 @@ def is_fuzz_target_affected(coverage_getter, fuzz_target_path, files_changed):
return False
-def get_affected_fuzz_targets(coverage_getter, fuzz_target_paths,
- files_changed):
+def get_affected_fuzz_targets(coverage, fuzz_target_paths, files_changed):
"""Returns a list of paths of affected targets."""
affected_fuzz_targets = set()
for fuzz_target_path in fuzz_target_paths:
- if is_fuzz_target_affected(coverage_getter, fuzz_target_path,
- files_changed):
+ if is_fuzz_target_affected(coverage, fuzz_target_path, files_changed):
affected_fuzz_targets.add(fuzz_target_path)
return affected_fuzz_targets
diff --git a/infra/cifuzz/affected_fuzz_targets_test.py b/infra/cifuzz/affected_fuzz_targets_test.py
index 96d6df7aa..c32c27776 100644
--- a/infra/cifuzz/affected_fuzz_targets_test.py
+++ b/infra/cifuzz/affected_fuzz_targets_test.py
@@ -21,6 +21,9 @@ from unittest import mock
import parameterized
import affected_fuzz_targets
+import clusterfuzz_deployment
+import docker
+import test_helpers
# pylint: disable=protected-access
@@ -57,18 +60,26 @@ class RemoveUnaffectedFuzzTargets(unittest.TestCase):
# yapf: enable
def test_remove_unaffected_fuzz_targets(self, side_effect, expected_dir_len):
"""Tests that remove_unaffected_fuzzers has the intended effect."""
+ config = test_helpers.create_run_config(is_github=True,
+ project_name=EXAMPLE_PROJECT,
+ workspace='/workspace')
+ workspace = docker.Workspace(config)
+ deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(
+ config, workspace)
# We can't use fakefs in this test because this test executes
# utils.is_fuzz_target_local. This function relies on the executable bit
# being set, which doesn't work properly in fakefs.
with tempfile.TemporaryDirectory() as tmp_dir, mock.patch(
- 'get_coverage.OssFuzzCoverageGetter.get_files_covered_by_target'
+ 'get_coverage.OSSFuzzCoverage.get_files_covered_by_target'
) as mocked_get_files:
- with mock.patch('get_coverage._get_fuzzer_stats_dir_url', return_value=1):
+ with mock.patch('get_coverage._get_oss_fuzz_fuzzer_stats_dir_url',
+ return_value=1):
mocked_get_files.side_effect = side_effect
shutil.copy(self.TEST_FUZZER_1, tmp_dir)
shutil.copy(self.TEST_FUZZER_2, tmp_dir)
+
affected_fuzz_targets.remove_unaffected_fuzz_targets(
- EXAMPLE_PROJECT, tmp_dir, [EXAMPLE_FILE_CHANGED], '')
+ deployment, tmp_dir, [EXAMPLE_FILE_CHANGED], '')
self.assertEqual(expected_dir_len, len(os.listdir(tmp_dir)))
diff --git a/infra/cifuzz/build_fuzzers.py b/infra/cifuzz/build_fuzzers.py
index 763659749..931784a21 100644
--- a/infra/cifuzz/build_fuzzers.py
+++ b/infra/cifuzz/build_fuzzers.py
@@ -19,6 +19,7 @@ import os
import sys
import affected_fuzz_targets
+import clusterfuzz_deployment
import continuous_integration
import docker
@@ -53,6 +54,9 @@ class Builder: # pylint: disable=too-many-instance-attributes
self.workspace = docker.Workspace(config)
self.workspace.initialize_dir(self.workspace.out)
self.workspace.initialize_dir(self.workspace.work)
+ self.clusterfuzz_deployment = (
+ clusterfuzz_deployment.get_clusterfuzz_deployment(
+ self.config, self.workspace))
self.image_repo_path = None
self.host_repo_path = None
self.repo_manager = None
@@ -146,7 +150,7 @@ class Builder: # pylint: disable=too-many-instance-attributes
changed_files = self.ci_system.get_changed_code_under_test(
self.repo_manager)
affected_fuzz_targets.remove_unaffected_fuzz_targets(
- self.config.project_name, self.workspace.out, changed_files,
+ self.clusterfuzz_deployment, self.workspace.out, changed_files,
self.image_repo_path)
return True
diff --git a/infra/cifuzz/build_fuzzers_test.py b/infra/cifuzz/build_fuzzers_test.py
index b88552301..2d5c5dc74 100644
--- a/infra/cifuzz/build_fuzzers_test.py
+++ b/infra/cifuzz/build_fuzzers_test.py
@@ -142,6 +142,8 @@ class BuildFuzzersIntegrationTest(unittest.TestCase):
def tearDown(self):
self.tmp_dir_obj.cleanup()
+ # @mock.patch('clusterfuzz_deployment.ClusterFuzzLite.get_coverage',
+ # return_value=None)
def test_external_github_project(self):
"""Tests building fuzzers from an external project on Github."""
project_name = 'external-project'
diff --git a/infra/cifuzz/clusterfuzz_deployment.py b/infra/cifuzz/clusterfuzz_deployment.py
index 360f75287..e076eb19e 100644
--- a/infra/cifuzz/clusterfuzz_deployment.py
+++ b/infra/cifuzz/clusterfuzz_deployment.py
@@ -18,9 +18,10 @@ import sys
import urllib.error
import urllib.request
+import filestore
import filestore_utils
-
import http_utils
+import get_coverage
# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -69,6 +70,14 @@ class BaseClusterFuzzDeployment:
"""Uploads the corpus for |target_name| to filestore."""
raise NotImplementedError('Child class must implement method.')
+ def upload_coverage(self):
+ """Uploads the coverage report to the filestore."""
+ raise NotImplementedError('Child class must implement method.')
+
+ def get_coverage(self, repo_path):
+ """Returns the project coverage object for the project."""
+ raise NotImplementedError('Child class must implement method.')
+
def make_empty_corpus_dir(self, target_name):
"""Makes an empty corpus directory for |target_name| in |parent_dir| and
returns the path to the directory."""
@@ -81,6 +90,7 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
"""Class representing a deployment of ClusterFuzzLite."""
BASE_BUILD_NAME = 'cifuzz-build-'
+ COVERAGE_NAME = 'coverage'
def __init__(self, config, workspace):
super().__init__(config, workspace)
@@ -166,6 +176,24 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment):
except Exception as error: # pylint: disable=broad-except
logging.error('Failed to upload crashes. Error: %s.', error)
+ 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.')
+
+ def get_coverage(self, repo_path):
+ """Returns the project coverage object for the project."""
+ try:
+ if not self.filestore.download_coverage(
+ self.COVERAGE_NAME, self.workspace.clusterfuzz_coverage):
+ return None
+ return get_coverage.FilesystemCoverage(
+ repo_path, self.workspace.clusterfuzz_coverage)
+ except (get_coverage.CoverageError, filestore.FilestoreError):
+ return None
+
class OSSFuzz(BaseClusterFuzzDeployment):
"""The OSS-Fuzz ClusterFuzz deployment."""
@@ -254,6 +282,17 @@ class OSSFuzz(BaseClusterFuzzDeployment):
logging.warning('Failed to download corpus for %s.', target_name)
return corpus_dir
+ def upload_coverage(self):
+ """Noop Implementation of upload_coverage_report."""
+ logging.info('Not uploading coverage report because on OSS-Fuzz.')
+
+ def get_coverage(self, repo_path):
+ """Returns the project coverage object for the project."""
+ try:
+ return get_coverage.OSSFuzzCoverage(repo_path, self.config.project_name)
+ except get_coverage.CoverageError:
+ return None
+
class NoClusterFuzzDeployment(BaseClusterFuzzDeployment):
"""ClusterFuzzDeployment implementation used when there is no deployment of
@@ -282,6 +321,16 @@ class NoClusterFuzzDeployment(BaseClusterFuzzDeployment):
logging.info(
'Not downloading latest build because no ClusterFuzz deployment.')
+ def upload_coverage(self):
+ """Noop Implementation of upload_coverage."""
+ logging.info(
+ 'Not uploading coverage report because no ClusterFuzz deployment.')
+
+ def get_coverage(self, repo_path):
+ """Noop Implementation of get_coverage."""
+ logging.info(
+ 'Not getting project coverage because no ClusterFuzz deployment.')
+
def get_clusterfuzz_deployment(config, workspace):
"""Returns object reprsenting deployment of ClusterFuzz used by |config|."""
diff --git a/infra/cifuzz/docker.py b/infra/cifuzz/docker.py
index c14ecbcc6..752f9c017 100644
--- a/infra/cifuzz/docker.py
+++ b/infra/cifuzz/docker.py
@@ -124,6 +124,16 @@ class Workspace:
return os.path.join(self.workspace, 'cifuzz-prev-build')
@property
+ def clusterfuzz_coverage(self):
+ """The directory where builds from ClusterFuzz are stored."""
+ return os.path.join(self.workspace, 'cifuzz-prev-coverage')
+
+ @property
+ def coverage_report(self):
+ """The directory where coverage reports generated by cifuzz are put."""
+ return os.path.join(self.workspace, 'cifuzz-coverage')
+
+ @property
def corpora(self):
"""The directory where corpora from ClusterFuzz are stored."""
return os.path.join(self.workspace, 'cifuzz-corpus')
diff --git a/infra/cifuzz/filestore/__init__.py b/infra/cifuzz/filestore/__init__.py
index 0acb4b9ca..ab240c832 100644
--- a/infra/cifuzz/filestore/__init__.py
+++ b/infra/cifuzz/filestore/__init__.py
@@ -36,3 +36,7 @@ class BaseFilestore:
def download_latest_build(self, name, dst_directory):
"""Downloads the latest build with |name| to |dst_directory|."""
raise NotImplementedError('Child class must implement method.')
+
+ def download_coverage(self, dst_directory):
+ """Downloads the latest project coverage report."""
+ raise NotImplementedError('Child class must implement method.')
diff --git a/infra/cifuzz/filestore/github_actions/__init__.py b/infra/cifuzz/filestore/github_actions/__init__.py
index fbcbb1d76..56d6c2b00 100644
--- a/infra/cifuzz/filestore/github_actions/__init__.py
+++ b/infra/cifuzz/filestore/github_actions/__init__.py
@@ -81,3 +81,7 @@ class GithubActionsFilestore(filestore.BaseFilestore):
def download_latest_build(self, name, dst_directory):
"""Downloads latest build with name |name| to |dst_directory|."""
return self._download_artifact(name, dst_directory)
+
+ def download_coverage(self, name, dst_directory):
+ """Downloads the latest project coverage report."""
+ return self._download_artifact(name, dst_directory)
diff --git a/infra/cifuzz/filestore/github_actions/github_api.py b/infra/cifuzz/filestore/github_actions/github_api.py
index 32e1b3925..7d186b401 100644
--- a/infra/cifuzz/filestore/github_actions/github_api.py
+++ b/infra/cifuzz/filestore/github_actions/github_api.py
@@ -20,6 +20,8 @@ import sys
import requests
+import filestore
+
# pylint: disable=wrong-import-position,import-error
sys.path.append(
@@ -68,11 +70,11 @@ def _get_items(url, headers):
params = {'per_page': _MAX_ITEMS_PER_PAGE, 'page': str(page_counter)}
response = _do_get_request(url, params=params, headers=headers)
response_json = response.json()
-
if not response.status_code == 200:
# Check that request was successful.
logging.error('Request to %s failed. Code: %d. Response: %s',
response.request.url, response.status_code, response_json)
+ raise filestore.FilestoreError('Github API request failed.')
if total_num_items == float('inf'):
# Set proper total_num_items
diff --git a/infra/cifuzz/generate_coverage_report.py b/infra/cifuzz/generate_coverage_report.py
index 9a5b8e7a8..6c90ffada 100644
--- a/infra/cifuzz/generate_coverage_report.py
+++ b/infra/cifuzz/generate_coverage_report.py
@@ -23,7 +23,8 @@ def run_coverage_command(workspace, config):
docker_args, _ = docker.get_base_docker_run_args(workspace, config.sanitizer,
config.language)
docker_args += [
- '-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t',
+ '-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-e',
+ f'COVERAGE_OUTPUT_DIR={workspace.coverage_report}', '-t',
docker.BASE_RUNNER_TAG, 'coverage'
]
return helper.docker_run(docker_args)
diff --git a/infra/cifuzz/generate_coverage_report_test.py b/infra/cifuzz/generate_coverage_report_test.py
index 250d95946..62a4c8c5a 100644
--- a/infra/cifuzz/generate_coverage_report_test.py
+++ b/infra/cifuzz/generate_coverage_report_test.py
@@ -39,7 +39,8 @@ class TestRunCoverageCommand(unittest.TestCase):
f'SANITIZER={SANITIZER}', '-e', 'FUZZING_LANGUAGE=c++', '-e',
'OUT=/workspace/build-out', '-v',
f'{workspace.workspace}:{workspace.workspace}', '-e',
- 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t',
+ 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-e',
+ f'COVERAGE_OUTPUT_DIR={workspace.coverage_report}', '-t',
'gcr.io/oss-fuzz-base/base-runner', 'coverage'
]
diff --git a/infra/cifuzz/get_coverage.py b/infra/cifuzz/get_coverage.py
index 9a179c59d..f674e9553 100644
--- a/infra/cifuzz/get_coverage.py
+++ b/infra/cifuzz/get_coverage.py
@@ -15,27 +15,51 @@
import logging
import os
import sys
-import json
-import urllib.error
-import urllib.request
+
+import http_utils
# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import utils
-# The path to get project's latest report json file.
-LATEST_REPORT_INFO_PATH = 'oss-fuzz-coverage/latest_report_info/'
+# The path to get OSS-Fuzz project's latest report json file.`
+OSS_FUZZ_LATEST_COVERAGE_INFO_PATH = 'oss-fuzz-coverage/latest_report_info/'
-class OssFuzzCoverageGetter:
- """Gets coverage data for a project from OSS-Fuzz."""
+# pylint: disable=too-few-public-methods
+class CoverageError(Exception):
+ """Exceptions for project coverage."""
+
- def __init__(self, project_name, repo_path):
- """Constructor for OssFuzzCoverageGetter. Callers should check that
- fuzzer_stats_url is initialized."""
- self.project_name = project_name
+class BaseCoverage:
+ """Gets coverage data for a project."""
+
+ def __init__(self, repo_path):
self.repo_path = _normalize_repo_path(repo_path)
- self.fuzzer_stats_url = _get_fuzzer_stats_dir_url(self.project_name)
+
+ def get_files_covered_by_target(self, target):
+ """Returns a list of source files covered by the specific fuzz target.
+
+ Args:
+ target: The name of the fuzz target whose coverage is requested.
+
+ Returns:
+ A list of files that the fuzz targets covers or None.
+ """
+ raise NotImplementedError('Child class must implement method.')
+
+
+class OSSFuzzCoverage(BaseCoverage):
+ """Gets coverage data for a project from OSS-Fuzz."""
+
+ def __init__(self, repo_path, oss_fuzz_project_name):
+ """Constructor for OSSFuzzCoverage."""
+ super().__init__(repo_path)
+ self.oss_fuzz_project_name = oss_fuzz_project_name
+ self.fuzzer_stats_url = _get_oss_fuzz_fuzzer_stats_dir_url(
+ self.oss_fuzz_project_name)
+ if self.fuzzer_stats_url is None:
+ raise CoverageError('Could not get latest coverage.')
def get_target_coverage_report(self, target):
"""Get the coverage report for a specific fuzz target.
@@ -50,7 +74,7 @@ class OssFuzzCoverageGetter:
return None
target_url = utils.url_join(self.fuzzer_stats_url, target + '.json')
- return get_json_from_url(target_url)
+ return http_utils.get_json_from_url(target_url)
def get_files_covered_by_target(self, target):
"""Gets a list of source files covered by the specific fuzz target.
@@ -89,32 +113,13 @@ class OssFuzzCoverageGetter:
return affected_file_list
-def is_file_covered(file_cov):
- """Returns whether the file is covered."""
- return file_cov['summary']['regions']['covered']
-
-
-def get_coverage_per_file(target_cov):
- """Returns the coverage per file within |target_cov|."""
- return target_cov['data'][0]['files']
-
-
-def _normalize_repo_path(repo_path):
- """Normalizes and returns |repo_path| to make sure cases like /src/curl and
- /src/curl/ are both handled."""
- repo_path = os.path.normpath(repo_path)
- if not repo_path.endswith('/'):
- repo_path += '/'
- return repo_path
-
-
-def _get_latest_cov_report_info(project_name):
+def _get_oss_fuzz_latest_cov_report_info(oss_fuzz_project_name):
"""Gets and returns a dictionary containing the latest coverage report info
for |project|."""
latest_report_info_url = utils.url_join(utils.GCS_BASE_URL,
- LATEST_REPORT_INFO_PATH,
- project_name + '.json')
- latest_cov_info = get_json_from_url(latest_report_info_url)
+ OSS_FUZZ_LATEST_COVERAGE_INFO_PATH,
+ oss_fuzz_project_name + '.json')
+ latest_cov_info = http_utils.get_json_from_url(latest_report_info_url)
if latest_cov_info is None:
logging.error('Could not get the coverage report json from url: %s.',
latest_report_info_url)
@@ -122,16 +127,17 @@ def _get_latest_cov_report_info(project_name):
return latest_cov_info
-def _get_fuzzer_stats_dir_url(project_name):
- """Gets latest coverage report info for a specific OSS-Fuzz project from GCS.
+def _get_oss_fuzz_fuzzer_stats_dir_url(oss_fuzz_project_name):
+ """Gets latest coverage report info for a specific OSS-Fuzz project from
+ GCS.
Args:
- project_name: The name of the relevant OSS-Fuzz project.
+ oss_fuzz_project_name: The name of the project.
Returns:
The projects coverage report info in json dict or None on failure.
"""
- latest_cov_info = _get_latest_cov_report_info(project_name)
+ latest_cov_info = _get_oss_fuzz_latest_cov_report_info(oss_fuzz_project_name)
if not latest_cov_info:
return None
@@ -145,25 +151,40 @@ def _get_fuzzer_stats_dir_url(project_name):
return fuzzer_stats_dir_url
-def get_json_from_url(url):
- """Gets a json object from a specified HTTP URL.
+class FilesystemCoverage(BaseCoverage):
+ """Class that gets a project's coverage from the filesystem."""
- Args:
- url: The url of the json to be downloaded.
+ def __init__(self, repo_path, project_coverage_dir):
+ super().__init__(repo_path)
+ self.project_coverage_dir = project_coverage_dir
- Returns:
- A dictionary deserialized from JSON or None on failure.
- """
- try:
- response = urllib.request.urlopen(url)
- except urllib.error.HTTPError:
- logging.error('HTTP error with url %s.', url)
- return None
+ def get_files_covered_by_target(self, target):
+ """Returns a list of source files covered by the specific fuzz target.
- try:
- # read().decode() fixes compatibility issue with urllib response object.
- result_json = json.loads(response.read().decode())
- except (ValueError, TypeError, json.JSONDecodeError) as err:
- logging.error('Loading json from url %s failed with: %s.', url, str(err))
- return None
- return result_json
+ Args:
+ target: The name of the fuzz target whose coverage is requested.
+
+ Returns:
+ A list of files that the fuzz targets covers or None.
+ """
+ # TODO(jonathanmetzman): Implement this.
+ raise NotImplementedError('Implementation TODO.')
+
+
+def is_file_covered(file_cov):
+ """Returns whether the file is covered."""
+ return file_cov['summary']['regions']['covered']
+
+
+def get_coverage_per_file(target_cov):
+ """Returns the coverage per file within |target_cov|."""
+ return target_cov['data'][0]['files']
+
+
+def _normalize_repo_path(repo_path):
+ """Normalizes and returns |repo_path| to make sure cases like /src/curl and
+ /src/curl/ are both handled."""
+ repo_path = os.path.normpath(repo_path)
+ if not repo_path.endswith('/'):
+ repo_path += '/'
+ return repo_path
diff --git a/infra/cifuzz/get_coverage_test.py b/infra/cifuzz/get_coverage_test.py
index bc77ff0a6..da3e59ad6 100644
--- a/infra/cifuzz/get_coverage_test.py
+++ b/infra/cifuzz/get_coverage_test.py
@@ -17,6 +17,8 @@ import json
import unittest
from unittest import mock
+import pytest
+
import get_coverage
# pylint: disable=protected-access
@@ -36,10 +38,10 @@ with open(os.path.join(TEST_DATA_PATH,
PROJECT_COV_INFO = json.loads(cov_file_handle.read())
-class GetFuzzerStatsDirUrlTest(unittest.TestCase):
- """Tests _get_fuzzer_stats_dir_url."""
+class GetOssFuzzFuzzerStatsDirUrlTest(unittest.TestCase):
+ """Tests _get_oss_fuzz_fuzzer_stats_dir_url."""
- @mock.patch('get_coverage.get_json_from_url',
+ @mock.patch('http_utils.get_json_from_url',
return_value={
'fuzzer_stats_dir':
'gs://oss-fuzz-coverage/systemd/fuzzer_stats/20210303'
@@ -50,7 +52,7 @@ class GetFuzzerStatsDirUrlTest(unittest.TestCase):
NOTE: This test relies on the PROJECT_NAME repo's coverage report.
The "example" project was not used because it has no coverage reports.
"""
- result = get_coverage._get_fuzzer_stats_dir_url(PROJECT_NAME)
+ result = get_coverage._get_oss_fuzz_fuzzer_stats_dir_url(PROJECT_NAME)
(url,), _ = mocked_get_json_from_url.call_args
self.assertEqual(
'https://storage.googleapis.com/oss-fuzz-coverage/'
@@ -63,22 +65,23 @@ class GetFuzzerStatsDirUrlTest(unittest.TestCase):
def test_get_invalid_project(self):
"""Tests that passing a bad project returns None."""
- self.assertIsNone(get_coverage._get_fuzzer_stats_dir_url('not-a-proj'))
+ self.assertIsNone(
+ get_coverage._get_oss_fuzz_fuzzer_stats_dir_url('not-a-proj'))
-class GetTargetCoverageReportTest(unittest.TestCase):
- """Tests get_target_coverage_report."""
+class OSSFuzzCoverageGetTargetCoverageReportTest(unittest.TestCase):
+ """Tests OSSFuzzCoverage.get_target_coverage_report."""
def setUp(self):
- with mock.patch('get_coverage._get_latest_cov_report_info',
+ with mock.patch('get_coverage._get_oss_fuzz_latest_cov_report_info',
return_value=PROJECT_COV_INFO):
- self.coverage_getter = get_coverage.OssFuzzCoverageGetter(
- PROJECT_NAME, REPO_PATH)
+ self.oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(
+ REPO_PATH, PROJECT_NAME)
- @mock.patch('get_coverage.get_json_from_url', return_value={})
+ @mock.patch('http_utils.get_json_from_url', return_value={})
def test_valid_target(self, mocked_get_json_from_url):
"""Tests that a target's coverage report can be downloaded and parsed."""
- self.coverage_getter.get_target_coverage_report(FUZZ_TARGET)
+ self.oss_fuzz_coverage.get_target_coverage_report(FUZZ_TARGET)
(url,), _ = mocked_get_json_from_url.call_args
self.assertEqual(
'https://storage.googleapis.com/oss-fuzz-coverage/'
@@ -87,35 +90,35 @@ class GetTargetCoverageReportTest(unittest.TestCase):
def test_invalid_target(self):
"""Tests that passing an invalid target coverage report returns None."""
self.assertIsNone(
- self.coverage_getter.get_target_coverage_report(INVALID_TARGET))
+ self.oss_fuzz_coverage.get_target_coverage_report(INVALID_TARGET))
- @mock.patch('get_coverage._get_latest_cov_report_info', return_value=None)
- def test_invalid_project_json(self, _):
+ @mock.patch('get_coverage._get_oss_fuzz_latest_cov_report_info',
+ return_value=None)
+ def test_invalid_project_json(self, _): # pylint: disable=no-self-use
"""Tests an invalid project JSON results in None being returned."""
- coverage_getter = get_coverage.OssFuzzCoverageGetter(
- PROJECT_NAME, REPO_PATH)
- self.assertIsNone(coverage_getter.get_target_coverage_report(FUZZ_TARGET))
+ with pytest.raises(get_coverage.CoverageError):
+ get_coverage.OSSFuzzCoverage(REPO_PATH, PROJECT_NAME)
-class GetFilesCoveredByTargetTest(unittest.TestCase):
- """Tests get_files_covered_by_target."""
+class OSSFuzzCoverageGetFilesCoveredByTargetTest(unittest.TestCase):
+ """Tests OSSFuzzCoverage.get_files_covered_by_target."""
def setUp(self):
- with mock.patch('get_coverage._get_latest_cov_report_info',
+ with mock.patch('get_coverage._get_oss_fuzz_latest_cov_report_info',
return_value=PROJECT_COV_INFO):
- self.coverage_getter = get_coverage.OssFuzzCoverageGetter(
- PROJECT_NAME, REPO_PATH)
+ self.oss_fuzz_coverage = get_coverage.OSSFuzzCoverage(
+ REPO_PATH, PROJECT_NAME)
def test_valid_target(self):
"""Tests that covered files can be retrieved from a coverage report."""
with open(os.path.join(TEST_DATA_PATH,
- FUZZ_TARGET_COV_JSON_FILENAME),) as file_handle:
+ FUZZ_TARGET_COV_JSON_FILENAME)) as file_handle:
fuzzer_cov_info = json.loads(file_handle.read())
- with mock.patch(
- 'get_coverage.OssFuzzCoverageGetter.get_target_coverage_report',
- return_value=fuzzer_cov_info):
- file_list = self.coverage_getter.get_files_covered_by_target(FUZZ_TARGET)
+ with mock.patch('get_coverage.OSSFuzzCoverage.get_target_coverage_report',
+ return_value=fuzzer_cov_info):
+ file_list = self.oss_fuzz_coverage.get_files_covered_by_target(
+ FUZZ_TARGET)
curl_files_list_path = os.path.join(TEST_DATA_PATH,
'example_curl_file_list.json')
@@ -126,7 +129,7 @@ class GetFilesCoveredByTargetTest(unittest.TestCase):
def test_invalid_target(self):
"""Tests passing invalid fuzz target returns None."""
self.assertIsNone(
- self.coverage_getter.get_files_covered_by_target(INVALID_TARGET))
+ self.oss_fuzz_coverage.get_files_covered_by_target(INVALID_TARGET))
class IsFileCoveredTest(unittest.TestCase):
@@ -163,29 +166,30 @@ class IsFileCoveredTest(unittest.TestCase):
self.assertFalse(get_coverage.is_file_covered(file_coverage))
-class GetLatestCovReportInfo(unittest.TestCase):
- """Tests that _get_latest_cov_report_info works as intended."""
+class GetOssFuzzLatestCovReportInfo(unittest.TestCase):
+ """Tests that _get_oss_fuzz_latest_cov_report_info works as
+ intended."""
PROJECT = 'project'
LATEST_REPORT_INFO_URL = ('https://storage.googleapis.com/oss-fuzz-coverage/'
'latest_report_info/project.json')
@mock.patch('logging.error')
- @mock.patch('get_coverage.get_json_from_url', return_value={'coverage': 1})
- def test_get_latest_cov_report_info(self, mocked_get_json_from_url,
- mocked_error):
- """Tests that _get_latest_cov_report_info works as intended."""
- result = get_coverage._get_latest_cov_report_info(self.PROJECT)
+ @mock.patch('http_utils.get_json_from_url', return_value={'coverage': 1})
+ def test_get_oss_fuzz_latest_cov_report_info(self, mocked_get_json_from_url,
+ mocked_error):
+ """Tests that _get_oss_fuzz_latest_cov_report_info works as intended."""
+ result = get_coverage._get_oss_fuzz_latest_cov_report_info(self.PROJECT)
self.assertEqual(result, {'coverage': 1})
mocked_error.assert_not_called()
mocked_get_json_from_url.assert_called_with(self.LATEST_REPORT_INFO_URL)
@mock.patch('logging.error')
- @mock.patch('get_coverage.get_json_from_url', return_value=None)
- def test_get_latest_cov_report_info_fail(self, _, mocked_error):
- """Tests that _get_latest_cov_report_info works as intended when we can't
- get latest report info."""
- result = get_coverage._get_latest_cov_report_info('project')
+ @mock.patch('http_utils.get_json_from_url', return_value=None)
+ def test_get_oss_fuzz_latest_cov_report_info_fail(self, _, mocked_error):
+ """Tests that _get_oss_fuzz_latest_cov_report_info works as intended when we
+ can't get latest report info."""
+ result = get_coverage._get_oss_fuzz_latest_cov_report_info('project')
self.assertIsNone(result)
mocked_error.assert_called_with(
'Could not get the coverage report json from url: %s.',
diff --git a/infra/cifuzz/http_utils.py b/infra/cifuzz/http_utils.py
index 5d7b16359..931183593 100644
--- a/infra/cifuzz/http_utils.py
+++ b/infra/cifuzz/http_utils.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utility module for HTTP."""
+import json
import logging
import os
import sys
@@ -71,6 +72,23 @@ def download_url(*args, **kwargs):
return False
+def get_json_from_url(url):
+ """Gets a json object from a specified HTTP URL.
+
+ Args:
+ url: The url of the json to be downloaded.
+
+ Returns:
+ A dictionary deserialized from JSON or None on failure.
+ """
+ response = requests.get(url)
+ try:
+ return response.json()
+ except (ValueError, TypeError, json.JSONDecodeError) as err:
+ logging.error('Loading json from url %s failed with: %s.', url, str(err))
+ return None
+
+
@retry.wrap(_DOWNLOAD_URL_RETRIES, _DOWNLOAD_URL_BACKOFF)
def _download_url(url, filename, headers=None):
"""Downloads the file located at |url|, using HTTP to |filename|.
diff --git a/infra/cifuzz/run_fuzzers_test.py b/infra/cifuzz/run_fuzzers_test.py
index 9e6ddf8c5..dfbee8581 100644
--- a/infra/cifuzz/run_fuzzers_test.py
+++ b/infra/cifuzz/run_fuzzers_test.py
@@ -371,8 +371,8 @@ class CoverageReportIntegrationTest(unittest.TestCase):
TEST_DATA_PATH, 'example_coverage_report_summary.json')
with open(expected_summary_path) as file_handle:
expected_summary = json.loads(file_handle.read())
- actual_summary_path = os.path.join(workspace, 'build-out', 'report',
- 'linux', 'summary.json')
+ actual_summary_path = os.path.join(workspace, 'cifuzz-coverage',
+ 'report', 'linux', 'summary.json')
with open(actual_summary_path) as file_handle:
actual_summary = json.loads(file_handle.read())
self.assertEqual(expected_summary, actual_summary)