diff options
-rwxr-xr-x | infra/base-images/base-runner/coverage | 12 | ||||
-rw-r--r-- | infra/cifuzz/affected_fuzz_targets.py | 24 | ||||
-rw-r--r-- | infra/cifuzz/affected_fuzz_targets_test.py | 17 | ||||
-rw-r--r-- | infra/cifuzz/build_fuzzers.py | 6 | ||||
-rw-r--r-- | infra/cifuzz/build_fuzzers_test.py | 2 | ||||
-rw-r--r-- | infra/cifuzz/clusterfuzz_deployment.py | 51 | ||||
-rw-r--r-- | infra/cifuzz/docker.py | 10 | ||||
-rw-r--r-- | infra/cifuzz/filestore/__init__.py | 4 | ||||
-rw-r--r-- | infra/cifuzz/filestore/github_actions/__init__.py | 4 | ||||
-rw-r--r-- | infra/cifuzz/filestore/github_actions/github_api.py | 4 | ||||
-rw-r--r-- | infra/cifuzz/generate_coverage_report.py | 3 | ||||
-rw-r--r-- | infra/cifuzz/generate_coverage_report_test.py | 3 | ||||
-rw-r--r-- | infra/cifuzz/get_coverage.py | 139 | ||||
-rw-r--r-- | infra/cifuzz/get_coverage_test.py | 86 | ||||
-rw-r--r-- | infra/cifuzz/http_utils.py | 18 | ||||
-rw-r--r-- | infra/cifuzz/run_fuzzers_test.py | 4 |
16 files changed, 257 insertions, 130 deletions
diff --git a/infra/base-images/base-runner/coverage b/infra/base-images/base-runner/coverage index 3d722809..69853891 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 f1dfe9b7..959170c3 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 96d6df7a..c32c2777 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 76365974..931784a2 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 b8855230..2d5c5dc7 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 360f7528..e076eb19 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 c14ecbcc..752f9c01 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 0acb4b9c..ab240c83 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 fbcbb1d7..56d6c2b0 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 32e1b392..7d186b40 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 9a5b8e7a..6c90ffad 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 250d9594..62a4c8c5 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 9a179c59..f674e955 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 bc77ff0a..da3e59ad 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 5d7b1635..93118359 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 9e6ddf8c..dfbee858 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) |