diff options
22 files changed, 717 insertions, 251 deletions
diff --git a/infra/base-images/base-builder/detect_repo_test.py b/infra/base-images/base-builder/detect_repo_test.py index 4886522ac..96f4c9ec3 100644 --- a/infra/base-images/base-builder/detect_repo_test.py +++ b/infra/base-images/base-builder/detect_repo_test.py @@ -44,20 +44,19 @@ class DetectRepoIntegrationTest(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp_dir: # Construct example repo's to check for commits. - for example_repo in test_repos.TEST_REPOS: - repo_manager.RepoManager(example_repo.git_url, tmp_dir) - self.check_with_repo(example_repo.git_url, - example_repo.git_repo_name, + for test_repo in test_repos.TEST_REPOS: + repo_manager.clone_repo_and_get_manager(test_repo.git_url, tmp_dir) + self.check_with_repo(test_repo.git_url, + test_repo.git_repo_name, tmp_dir, - commit=example_repo.old_commit) + commit=test_repo.old_commit) def test_infer_main_repo_from_name(self): """Tests that the main project repo can be inferred from a repo name.""" - with tempfile.TemporaryDirectory() as tmp_dir: - for example_repo in test_repos.TEST_REPOS: - repo_manager.RepoManager(example_repo.git_url, tmp_dir) - self.check_with_repo(example_repo.git_url, example_repo.git_repo_name, + for test_repo in test_repos.TEST_REPOS: + repo_manager.clone_repo_and_get_manager(test_repo.git_url, tmp_dir) + self.check_with_repo(test_repo.git_url, test_repo.git_repo_name, tmp_dir) def check_with_repo(self, repo_origin, repo_name, tmp_dir, commit=None): diff --git a/infra/build_fuzzers.Dockerfile b/infra/build_fuzzers.Dockerfile index 49e438575..49c0d2c16 100644 --- a/infra/build_fuzzers.Dockerfile +++ b/infra/build_fuzzers.Dockerfile @@ -17,7 +17,6 @@ FROM gcr.io/oss-fuzz-base/cifuzz-base -# Copies your code file from action repository to the container COPY cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py /opt/build_fuzzers_entrypoint.py # Python file to execute when the docker container starts up diff --git a/infra/build_specified_commit.py b/infra/build_specified_commit.py index 6c3e3cc23..a4ff70f7a 100644 --- a/infra/build_specified_commit.py +++ b/infra/build_specified_commit.py @@ -143,8 +143,7 @@ def copy_src_from_docker(project_name, host_dir): return src_dir -@retry.wrap(_IMAGE_BUILD_TRIES, 2, - 'infra.build_specified_commit._build_image_with_retries') +@retry.wrap(_IMAGE_BUILD_TRIES, 2) def _build_image_with_retries(project_name): """Build image with retries.""" return helper.build_image_impl(project_name) diff --git a/infra/cifuzz/actions/build_fuzzers/action.yml b/infra/cifuzz/actions/build_fuzzers/action.yml index 20420eb37..2919db40e 100644 --- a/infra/cifuzz/actions/build_fuzzers/action.yml +++ b/infra/cifuzz/actions/build_fuzzers/action.yml @@ -14,6 +14,12 @@ inputs: sanitizer: description: 'The sanitizer to build the fuzzers with.' default: 'address' + project-src-path: + description: "The path to the project's source code checkout." + required: false + build-integration-path: + description: "The path to the the project's build integration." + required: false runs: using: 'docker' image: '../../../build_fuzzers.Dockerfile' @@ -22,3 +28,5 @@ runs: DRY_RUN: ${{ inputs.dry-run}} ALLOWED_BROKEN_TARGETS_PERCENTAGE: ${{ inputs.allowed-broken-targets-percentage}} SANITIZER: ${{ inputs.sanitizer }} + PROJECT_SRC_PATH: ${{ inputs.project-src-path }} + BUILD_INTEGRATION_PATH: ${{ inputs.build-integration-path }} diff --git a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py index 689862c04..5d467e7b4 100644 --- a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py +++ b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py @@ -27,6 +27,32 @@ logging.basicConfig( level=logging.DEBUG) +def get_pr_ref(event_path): + """Returns the PR ref from |event_path|.""" + with open(event_path, encoding='utf-8') as file_handle: + event = json.load(file_handle) + return 'refs/pull/{0}/merge'.format(event['pull_request']['number']) + + +def get_project_src_path(workspace): + """Returns the manually checked out path of the project's source if specified + or None.""" + # TODO(metzman): Get rid of MANUAL_SRC_PATH when Skia switches to + # project_src_path. + path = os.getenv('PROJECT_SRC_PATH', os.getenv('MANUAL_SRC_PATH')) + if not path: + logging.debug('No PROJECT_SRC_PATH.') + return path + + logging.debug('PROJECT_SRC_PATH set.') + if os.path.isabs(path): + return path + + # If |src| is not absolute, assume we are running in GitHub actions. + # TODO(metzman): Don't make this assumption. + return os.path.join(workspace, path) + + def main(): """Build OSS-Fuzz project's fuzzers for CI tools. This script is used to kick off the Github Actions CI tool. It is the @@ -51,52 +77,54 @@ def main(): Returns: 0 on success or 1 on failure. """ - oss_fuzz_project_name = os.environ.get('OSS_FUZZ_PROJECT_NAME') - github_repo_name = os.path.basename(os.environ.get('GITHUB_REPOSITORY')) - commit_sha = os.environ.get('GITHUB_SHA') - event = os.environ.get('GITHUB_EVENT_NAME') - workspace = os.environ.get('GITHUB_WORKSPACE') - sanitizer = os.environ.get('SANITIZER').lower() + oss_fuzz_project_name = os.getenv('OSS_FUZZ_PROJECT_NAME') + github_repo_name = os.path.basename(os.getenv('GITHUB_REPOSITORY')) + commit_sha = os.getenv('GITHUB_SHA') + event = os.getenv('GITHUB_EVENT_NAME') + workspace = os.getenv('GITHUB_WORKSPACE') + sanitizer = os.getenv('SANITIZER').lower() + project_src_path = get_project_src_path(workspace) + build_integration_path = os.getenv('BUILD_INTEGRATION_PATH') + allowed_broken_targets_percentage = os.getenv( + 'ALLOWED_BROKEN_TARGETS_PERCENTAGE') # Check if failures should not be reported. - dry_run = (os.environ.get('DRY_RUN').lower() == 'true') - - # The default return code when an error occurs. - returncode = 1 + dry_run = os.getenv('DRY_RUN').lower() == 'true' if dry_run: # Sets the default return code on error to success. returncode = 0 + else: + # The default return code when an error occurs. + returncode = 1 if not workspace: - logging.error('This script needs to be run in the Github action context.') - return returncode - - if event == 'push' and not cifuzz.build_fuzzers(oss_fuzz_project_name, - github_repo_name, - workspace, - commit_sha=commit_sha, - sanitizer=sanitizer): - logging.error('Error building fuzzers for project %s with commit %s.', - oss_fuzz_project_name, commit_sha) + logging.error('This script needs to be run within Github actions.') return returncode if event == 'pull_request': - event_path = os.environ.get('GITHUB_EVENT_PATH') - with open(event_path, encoding='utf-8') as file_handle: - event = json.load(file_handle) - pr_ref = 'refs/pull/{0}/merge'.format(event['pull_request']['number']) - if not cifuzz.build_fuzzers(oss_fuzz_project_name, - github_repo_name, - workspace, - pr_ref=pr_ref, - sanitizer=sanitizer): - logging.error( - 'Error building fuzzers for project %s with pull request %s.', - oss_fuzz_project_name, pr_ref) - return returncode + event_path = os.getenv('GITHUB_EVENT_PATH') + pr_ref = get_pr_ref(event_path) + else: + pr_ref = None + + if not cifuzz.build_fuzzers(oss_fuzz_project_name, + github_repo_name, + workspace, + commit_sha=commit_sha, + pr_ref=pr_ref, + sanitizer=sanitizer, + project_src_path=project_src_path, + build_integration_path=build_integration_path): + logging.error( + 'Error building fuzzers for project %s (commit: %s, pr_ref: %s).', + oss_fuzz_project_name, commit_sha, pr_ref) + return returncode out_dir = os.path.join(workspace, 'out') - if cifuzz.check_fuzzer_build(out_dir, sanitizer=sanitizer): + if cifuzz.check_fuzzer_build( + out_dir, + sanitizer=sanitizer, + allowed_broken_targets_percentage=allowed_broken_targets_percentage): returncode = 0 return returncode diff --git a/infra/cifuzz/cifuzz-base/Dockerfile b/infra/cifuzz/cifuzz-base/Dockerfile index e006c2b48..0aee3b2cf 100644 --- a/infra/cifuzz/cifuzz-base/Dockerfile +++ b/infra/cifuzz/cifuzz-base/Dockerfile @@ -35,4 +35,4 @@ RUN apt-get update && apt-get install docker-ce docker-ce-cli containerd.io -y ENV OSS_FUZZ_ROOT=/opt/oss-fuzz ADD . ${OSS_FUZZ_ROOT} -RUN rm -rf ${OSS_FUZZ_ROOT}/infra +RUN rm -rf ${OSS_FUZZ_ROOT}/infra
\ No newline at end of file diff --git a/infra/cifuzz/cifuzz.py b/infra/cifuzz/cifuzz.py index 479a4e07c..69e92a4c0 100644 --- a/infra/cifuzz/cifuzz.py +++ b/infra/cifuzz/cifuzz.py @@ -34,6 +34,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import build_specified_commit import helper import repo_manager +import retry import utils # From clusterfuzz: src/python/crash_analysis/crash_analyzer.py @@ -77,28 +78,273 @@ logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG) +_IMAGE_BUILD_TRIES = 3 +_IMAGE_BUILD_BACKOFF = 2 -def checkout_specified_commit(build_repo_manager, pr_ref, commit_sha): + +def checkout_specified_commit(repo_manager_obj, pr_ref, commit_sha): """Checks out the specified commit or pull request using - build_repo_manager.""" + |repo_manager_obj|.""" try: if pr_ref: - build_repo_manager.checkout_pr(pr_ref) + repo_manager_obj.checkout_pr(pr_ref) else: - build_repo_manager.checkout_commit(commit_sha) + repo_manager_obj.checkout_commit(commit_sha) except (RuntimeError, ValueError): logging.error( 'Can not check out requested state %s. ' 'Using current repo state', pr_ref or commit_sha) +@retry.wrap(_IMAGE_BUILD_TRIES, _IMAGE_BUILD_BACKOFF) +def build_external_project_docker_image(project_name, project_src, + build_integration_path): + """Builds the project builder image for an external (non-OSS-Fuzz) project. + Returns True on success.""" + dockerfile_path = os.path.join(build_integration_path, 'Dockerfile') + tag = 'gcr.io/oss-fuzz/{project_name}'.format(project_name=project_name) + command = ['-t', tag, '-f', dockerfile_path, project_src] + return helper.docker_build(command) + + +def fix_git_repo_for_diff(repo_dir): + """Fixes git repos cloned by the "checkout" action so that diffing works on + them.""" + command = [ + 'git', 'symbolic-ref', 'refs/remotes/origin/HEAD', + 'refs/remotes/origin/master' + ] + return utils.execute(command, location=repo_dir) + + +def check_project_src_path(project_src_path): + """Returns True if |project_src_path| exists.""" + if not os.path.exists(project_src_path): + logging.error( + 'PROJECT_SRC_PATH: %s does not exist. ' + 'Are you mounting it correctly?', project_src_path) + return False + return True + + +# pylint: disable=too-many-arguments + + +class BaseBuilder: # pylint: disable=too-many-instance-attributes + """Base class for fuzzer builders.""" + + def __init__(self, + project_name, + project_repo_name, + workspace, + sanitizer, + host_repo_path=None): + self.project_name = project_name + self.project_repo_name = project_repo_name + self.workspace = workspace + self.out_dir = os.path.join(workspace, 'out') + os.makedirs(self.out_dir, exist_ok=True) + self.sanitizer = sanitizer + self.host_repo_path = host_repo_path + self.image_repo_path = None + self.repo_manager = None + + def build_image_and_checkout_src(self): + """Builds the project builder image and checkout source code for the patch + we want to fuzz (if necessary). Returns True on success. + Must be implemented by child classes.""" + raise NotImplementedError('Child class must implement method') + + def build_fuzzers(self): + """Moves the source code we want to fuzz into the project builder and builds + the fuzzers from that source code. Returns True on success.""" + image_src_path = os.path.dirname(self.image_repo_path) + command = get_common_docker_args(self.sanitizer) + container = utils.get_container_name() + + if container: + command.extend(['-e', 'OUT=' + self.out_dir, '--volumes-from', container]) + rm_path = os.path.join(self.image_repo_path, '*') + + bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( + rm_path, self.host_repo_path, image_src_path) + else: + # TODO(metzman): Figure out if we can eliminate this branch. + command.extend([ + '-e', 'OUT=' + '/out', '-v', + '%s:%s' % (self.host_repo_path, self.image_repo_path), '-v', + '%s:%s' % (self.out_dir, '/out') + ]) + bash_command = 'compile' + + command.extend([ + 'gcr.io/oss-fuzz/' + self.project_name, + '/bin/bash', + '-c', + ]) + command.append(bash_command) + logging.info('Building with %s sanitizer.', self.sanitizer) + if helper.docker_run(command): + # docker_run returns nonzero on failure. + logging.error('Building fuzzers failed.') + return False + return True + + def build(self): + """Builds the image, checkouts the source (if needed), builds the fuzzers + and then removes the unaffectted fuzzers. Returns True on success.""" + methods = [ + self.build_image_and_checkout_src, self.build_fuzzers, + self.remove_unaffected_fuzzers + ] + for method in methods: + if not method(): + return False + return True + + def remove_unaffected_fuzzers(self): + """Removes the fuzzers unaffected by the patch.""" + fix_git_repo_for_diff(self.host_repo_path) + remove_unaffected_fuzzers(self.project_name, self.out_dir, + self.repo_manager.get_git_diff(), + self.image_repo_path) + return True + + +class ExternalGithubBuilder(BaseBuilder): + """Class for building non-OSS-Fuzz projects on GitHub Actions.""" + + def __init__(self, project_name, project_repo_name, workspace, sanitizer, + project_src_path, build_integration_path): + + super().__init__(project_name, + project_repo_name, + workspace, + sanitizer, + host_repo_path=project_src_path) + self.build_integration_path = os.path.join(self.host_repo_path, + build_integration_path) + logging.info('build_integration_path %s, project_src_path %s.', + self.build_integration_path, self.host_repo_path) + self.image_repo_path = os.path.join('/src', project_repo_name) + + def build_image_and_checkout_src(self): + """Builds the project builder image for a non-OSS-Fuzz project. Sets the + repo manager. Does not checkout source code since external projects are + expected to bring their own source code to CIFuzz. Returns True on + success.""" + logging.info('Building external project.') + if not build_external_project_docker_image( + self.project_name, self.host_repo_path, self.build_integration_path): + logging.error('Failed to build external project.') + return False + self.repo_manager = repo_manager.RepoManager(self.host_repo_path) + return True + + +class InternalGithubBuilder(BaseBuilder): + """Class for building OSS-Fuzz projects on GitHub actions.""" + + def __init__(self, project_name, project_repo_name, workspace, sanitizer, + commit_sha, pr_ref): + # Validate inputs. + assert pr_ref or commit_sha + + super().__init__(project_name, project_repo_name, workspace, sanitizer) + + self.commit_sha = commit_sha + self.pr_ref = pr_ref + + def build_image_and_checkout_src(self): + """Builds the project builder image for a non-OSS-Fuzz project. Sets the + repo manager and host_repo_path. Checks out source code of project with + patch under test. Returns True on success.""" + logging.info('Building OSS-Fuzz project on Github Actions.') + # detect_main_repo builds the image as a side effect. + inferred_url, self.image_repo_path = ( + build_specified_commit.detect_main_repo( + self.project_name, repo_name=self.project_repo_name)) + + if not inferred_url or not self.image_repo_path: + logging.error('Could not detect repo from project %s.', self.project_name) + return False + + git_workspace = os.path.join(self.workspace, 'storage') + os.makedirs(git_workspace, exist_ok=True) + + # Checkout project's repo in the shared volume. + self.repo_manager = repo_manager.clone_repo_and_get_manager( + inferred_url, git_workspace, repo_name=self.project_repo_name) + + self.host_repo_path = self.repo_manager.repo_dir + + checkout_specified_commit(self.repo_manager, self.pr_ref, self.commit_sha) + return True + + +class InternalGenericCiBuilder(BaseBuilder): + """Class for building fuzzers for an OSS-Fuzz project using on a platform + other than GitHub actions.""" + + def __init__(self, project_name, project_repo_name, workspace, sanitizer, + project_src_path): + super().__init__(project_name, + project_repo_name, + workspace, + sanitizer, + host_repo_path=project_src_path) + + def build_image_and_checkout_src(self): + """Builds the project builder image for a non-OSS-Fuzz project. Sets the + repo manager. Does not checkout source code since external projects are + expected to bring their own source code to CIFuzz. Returns True on + success.""" + logging.info('Building OSS-Fuzz project.') + # detect_main_repo builds the image as a side effect. + _, self.image_repo_path = (build_specified_commit.detect_main_repo( + self.project_name, repo_name=self.project_repo_name)) + + if not self.image_repo_path: + logging.error('Could not detect repo from project %s.', self.project_name) + return False + + # Checkout project's repo in the shared volume. + self.repo_manager = repo_manager.RepoManager(self.host_repo_path) + return True + + +def get_builder(project_name, project_repo_name, workspace, pr_ref, commit_sha, + sanitizer, project_src_path, build_integration_path): + """Determines what kind of build is being requested using the arguments + provided and instantiates and returns the correct builder object.""" + if build_integration_path and project_src_path: + # Non-OSS-Fuzz projects must bring their own source and their own build + # integration (which is relative to that source). + return ExternalGithubBuilder(project_name, project_repo_name, workspace, + sanitizer, project_src_path, + build_integration_path) + + if project_src_path: + # Builds of OSS-Fuzz projects not hosted on Github must bring their own + # source since the checkout logic CIFuzz implements is github-specific. + # TODO(metzman): Consider moving Github-actions builds of OSS-Fuzz projects + # to this system to reduce implementation complexity. + return InternalGenericCiBuilder(project_name, project_repo_name, workspace, + sanitizer, project_src_path) + + return InternalGithubBuilder(project_name, project_repo_name, workspace, + sanitizer, commit_sha, pr_ref) + + def build_fuzzers( # pylint: disable=too-many-arguments,too-many-locals project_name, project_repo_name, workspace, pr_ref=None, commit_sha=None, - sanitizer='address'): + sanitizer='address', + project_src_path=None, + build_integration_path=None): """Builds all of the fuzzers for a specific OSS-Fuzz project. Args: @@ -113,91 +359,15 @@ def build_fuzzers( # pylint: disable=too-many-arguments,too-many-locals Returns: True if build succeeded or False on failure. """ - # Validate inputs. - assert pr_ref or commit_sha - if not os.path.exists(workspace): - logging.error('Invalid workspace: %s.', workspace) + # Do some quick validation. + if project_src_path and not check_project_src_path(project_src_path): return False - logging.info("Using %s sanitizer.", sanitizer) - - out_dir = os.path.join(workspace, 'out') - os.makedirs(out_dir, exist_ok=True) - - # Build Fuzzers using docker run. - inferred_url, project_builder_repo_path = ( - build_specified_commit.detect_main_repo(project_name, - repo_name=project_repo_name)) - if not inferred_url or not project_builder_repo_path: - logging.error('Could not detect repo from project %s.', project_name) - return False - project_repo_name = os.path.basename(project_builder_repo_path) - src_in_project_builder = os.path.dirname(project_builder_repo_path) - - manual_src_path = os.getenv('MANUAL_SRC_PATH') - if manual_src_path: - if not os.path.exists(manual_src_path): - logging.error( - 'MANUAL_SRC_PATH: %s does not exist. ' - 'Are you mounting it correctly?', manual_src_path) - return False - # This is the path taken outside of GitHub actions. - git_workspace = os.path.dirname(manual_src_path) - else: - git_workspace = os.path.join(workspace, 'storage') - os.makedirs(git_workspace, exist_ok=True) - - # Checkout projects repo in the shared volume. - build_repo_manager = repo_manager.RepoManager(inferred_url, - git_workspace, - repo_name=project_repo_name) - - if not manual_src_path: - checkout_specified_commit(build_repo_manager, pr_ref, commit_sha) - - command = [ - '--cap-add', - 'SYS_PTRACE', - '-e', - 'FUZZING_ENGINE=' + DEFAULT_ENGINE, - '-e', - 'SANITIZER=' + sanitizer, - '-e', - 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE, - '-e', - 'CIFUZZ=True', - '-e', - 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support. - ] - container = utils.get_container_name() - if container: - command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] - bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( - os.path.join(src_in_project_builder, project_repo_name, '*'), - os.path.join(git_workspace, project_repo_name), src_in_project_builder) - else: - command += [ - '-e', 'OUT=' + '/out', '-v', - '%s:%s' % (os.path.join(git_workspace, project_repo_name), - os.path.join(src_in_project_builder, project_repo_name)), - '-v', - '%s:%s' % (out_dir, '/out') - ] - bash_command = 'compile' - - command.extend([ - 'gcr.io/oss-fuzz/' + project_name, - '/bin/bash', - '-c', - ]) - command.append(bash_command) - if helper.docker_run(command): - logging.error('Building fuzzers failed.') - return False - remove_unaffected_fuzzers(project_name, out_dir, - build_repo_manager.get_git_diff(), - project_builder_repo_path) - return True + # Get the builder and then build the fuzzers. + builder = get_builder(project_name, project_repo_name, workspace, pr_ref, + commit_sha, sanitizer, project_src_path, + build_integration_path) + return builder.build() def run_fuzzers( # pylint: disable=too-many-arguments,too-many-locals @@ -267,7 +437,27 @@ def run_fuzzers( # pylint: disable=too-many-arguments,too-many-locals return True, False -def check_fuzzer_build(out_dir, sanitizer='address'): +def get_common_docker_args(sanitizer): + """Returns a list of common docker arguments.""" + return [ + '--cap-add', + 'SYS_PTRACE', + '-e', + 'FUZZING_ENGINE=' + DEFAULT_ENGINE, + '-e', + 'SANITIZER=' + sanitizer, + '-e', + 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE, + '-e', + 'CIFUZZ=True', + '-e', + 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support. + ] + + +def check_fuzzer_build(out_dir, + sanitizer='address', + allowed_broken_targets_percentage=None): """Checks the integrity of the built fuzzers. Args: @@ -284,24 +474,8 @@ def check_fuzzer_build(out_dir, sanitizer='address'): logging.error('No fuzzers found in out directory: %s.', out_dir) return False - command = [ - '--cap-add', - 'SYS_PTRACE', - '-e', - 'FUZZING_ENGINE=' + DEFAULT_ENGINE, - '-e', - 'SANITIZER=' + sanitizer, - '-e', - 'ARCHITECTURE=' + DEFAULT_ARCHITECTURE, - '-e', - 'CIFUZZ=True', - '-e', - 'FUZZING_LANGUAGE=c++', # FIXME: Add proper support. - ] + command = get_common_docker_args(sanitizer) - # Set ALLOWED_BROKEN_TARGETS_PERCENTAGE in docker if specified by user. - allowed_broken_targets_percentage = os.getenv( - 'ALLOWED_BROKEN_TARGETS_PERCENTAGE') if allowed_broken_targets_percentage is not None: command += [ '-e', @@ -316,6 +490,7 @@ def check_fuzzer_build(out_dir, sanitizer='address'): command += ['-v', '%s:/out' % out_dir] command.extend(['-t', 'gcr.io/oss-fuzz-base/base-runner', 'test_all.py']) exit_code = helper.docker_run(command) + logging.info('check fuzzer build exit code: %d', exit_code) if exit_code: logging.error('Check fuzzer build failed.') return False diff --git a/infra/cifuzz/cifuzz_test.py b/infra/cifuzz/cifuzz_test.py index e7624f549..87ce8f236 100644 --- a/infra/cifuzz/cifuzz_test.py +++ b/infra/cifuzz/cifuzz_test.py @@ -33,6 +33,7 @@ OSS_FUZZ_DIR = os.path.dirname(INFRA_DIR) import cifuzz import fuzz_target +import test_helpers # NOTE: This integration test relies on # https://github.com/google/oss-fuzz/tree/master/projects/example project. @@ -69,7 +70,7 @@ class BuildFuzzersTest(unittest.TestCase): @mock.patch('build_specified_commit.detect_main_repo', return_value=('example.com', '/path')) - @mock.patch('repo_manager.RepoManager', return_value=None) + @mock.patch('repo_manager._clone', return_value=None) @mock.patch('cifuzz.checkout_specified_commit') @mock.patch('helper.docker_run') def test_cifuzz_env_var(self, mocked_docker_run, _, __, ___): @@ -97,6 +98,26 @@ class BuildFuzzersTest(unittest.TestCase): class BuildFuzzersIntegrationTest(unittest.TestCase): """Integration tests for build_fuzzers.""" + def setUp(self): + test_helpers.patch_environ(self) + + def test_external_project(self): + """Tests building fuzzers from an external project.""" + project_name = 'external-project' + project_src_path = os.path.join(TEST_FILES_PATH, project_name) + build_integration_path = os.path.join(project_src_path, 'oss-fuzz') + with tempfile.TemporaryDirectory() as tmp_dir: + out_path = os.path.join(tmp_dir, 'out') + os.mkdir(out_path) + self.assertTrue( + cifuzz.build_fuzzers(project_name, + project_name, + tmp_dir, + project_src_path=project_src_path, + build_integration_path=build_integration_path)) + self.assertTrue( + os.path.exists(os.path.join(out_path, EXAMPLE_BUILD_FUZZER))) + def test_valid_commit(self): """Tests building fuzzers with valid inputs.""" with tempfile.TemporaryDirectory() as tmp_dir: @@ -343,14 +364,14 @@ class CheckFuzzerBuildTest(unittest.TestCase): """Checks a directory that exists but does not have fuzzers is False.""" self.assertFalse(cifuzz.check_fuzzer_build(TEST_FILES_PATH)) - @mock.patch.dict(os.environ, {'ALLOWED_BROKEN_TARGETS_PERCENTAGE': '0'}) @mock.patch('helper.docker_run') def test_allow_broken_fuzz_targets_percentage(self, mocked_docker_run): """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running - docker if it is set in the environment.""" + docker if passed to check_fuzzer_build.""" mocked_docker_run.return_value = 0 test_fuzzer_dir = os.path.join(TEST_FILES_PATH, 'out') - cifuzz.check_fuzzer_build(test_fuzzer_dir) + cifuzz.check_fuzzer_build(test_fuzzer_dir, + allowed_broken_targets_percentage='0') self.assertIn('-e ALLOWED_BROKEN_TARGETS_PERCENTAGE=0', ' '.join(mocked_docker_run.call_args[0][0])) diff --git a/infra/cifuzz/fuzz_target_test.py b/infra/cifuzz/fuzz_target_test.py index 3e8788813..8f72f32cb 100644 --- a/infra/cifuzz/fuzz_target_test.py +++ b/infra/cifuzz/fuzz_target_test.py @@ -320,9 +320,11 @@ class DownloadOSSFuzzBuildDirIntegrationTest(unittest.TestCase): def test_invalid_build_dir(self): """Tests the download returns None when out_dir doesn't exist.""" - test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10, - 'not/a/dir', 'example') - self.assertIsNone(test_target.download_oss_fuzz_build()) + with tempfile.TemporaryDirectory() as tmp_dir: + invalid_dir = os.path.join(tmp_dir, 'not/a/dir') + test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10, + invalid_dir, 'example') + self.assertIsNone(test_target.download_oss_fuzz_build()) class DownloadUrlTest(unittest.TestCase): diff --git a/infra/cifuzz/test_files/external-project/Makefile b/infra/cifuzz/test_files/external-project/Makefile new file mode 100644 index 000000000..2c1773776 --- /dev/null +++ b/infra/cifuzz/test_files/external-project/Makefile @@ -0,0 +1,44 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); + +# Simple example of a build file that nicely integrates a fuzz target +# with the rest of the project. +# +# We use 'make' as the build system, but these ideas are applicable +# to any other build system + +# By default, use our own standalone_fuzz_target_runner. +# This runner does no fuzzing, but simply executes the inputs +# provided via parameters. +# Run e.g. "make all LIB_FUZZING_ENGINE=/path/to/libFuzzer.a" +# to link the fuzzer(s) against a real fuzzing engine. +# +# OSS-Fuzz will define its own value for LIB_FUZZING_ENGINE. +LIB_FUZZING_ENGINE ?= standalone_fuzz_target_runner.o + +# Values for CC, CFLAGS, CXX, CXXFLAGS are provided by OSS-Fuzz. +# Outside of OSS-Fuzz use the ones you prefer or rely on the default values. +# Do not use the -fsanitize=* flags by default. +# OSS-Fuzz will use different -fsanitize=* flags for different builds (asan, ubsan, msan, ...) + +# You may add extra compiler flags like this: +CXXFLAGS += -std=c++11 + +all: do_stuff_fuzzer + +clean: + rm -fv *.a *.o *_fuzzer crash-* *.zip + +# Fuzz target, links against $LIB_FUZZING_ENGINE, so that +# you may choose which fuzzing engine to use. +do_stuff_fuzzer: do_stuff_fuzzer.cpp my_api.a standalone_fuzz_target_runner.o + ${CXX} ${CXXFLAGS} $< my_api.a ${LIB_FUZZING_ENGINE} -o $@ + + +# The library itself. +my_api.a: my_api.cpp my_api.h + ${CXX} ${CXXFLAGS} $< -c + ar ruv my_api.a my_api.o + +# The standalone fuzz target runner. +standalone_fuzz_target_runner.o: standalone_fuzz_target_runner.cpp diff --git a/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp new file mode 100644 index 000000000..71fa8cae2 --- /dev/null +++ b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.cpp @@ -0,0 +1,24 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "my_api.h" + +#include <string> + +// Simple fuzz target for DoStuff(). +// See http://libfuzzer.info for details. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + std::string str(reinterpret_cast<const char *>(data), size); + DoStuff(str); // Disregard the output. + return 0; +} diff --git a/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict new file mode 100644 index 000000000..224679bf4 --- /dev/null +++ b/infra/cifuzz/test_files/external-project/do_stuff_fuzzer.dict @@ -0,0 +1,6 @@ +# A dictionary for more efficient fuzzing of DoStuff(). +# If the inputs contain multi-byte tokens, list them here. +# See http://libfuzzer.info#dictionaries +"foo" +"bar" +"ouch" diff --git a/infra/cifuzz/test_files/external-project/my_api.cpp b/infra/cifuzz/test_files/external-project/my_api.cpp new file mode 100644 index 000000000..9a2c1bc1c --- /dev/null +++ b/infra/cifuzz/test_files/external-project/my_api.cpp @@ -0,0 +1,36 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Implementation of "my_api". +#include "my_api.h" + +#include <vector> + +// Do some computations with 'str', return the result. +// This function contains a bug. Can you spot it? +size_t DoStuff(const std::string &str) { + std::vector<int> Vec({0, 1, 2, 3, 4}); + size_t Idx = 0; + if (str.size() > 5) + Idx++; + if (str.find("foo") != std::string::npos) + Idx++; + if (str.find("bar") != std::string::npos) + Idx++; + if (str.find("ouch") != std::string::npos) + Idx++; + if (str.find("omg") != std::string::npos) + Idx++; + return Vec[Idx]; +} diff --git a/infra/cifuzz/test_files/external-project/my_api.h b/infra/cifuzz/test_files/external-project/my_api.h new file mode 100644 index 000000000..325aa15cc --- /dev/null +++ b/infra/cifuzz/test_files/external-project/my_api.h @@ -0,0 +1,19 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// A library that does ... stuff. +// Serves as an example of good fuzz testing and OSS-Fuzz integration. +#include <string> + +size_t DoStuff(const std::string &str); diff --git a/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile b/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile new file mode 100644 index 000000000..e9dc33031 --- /dev/null +++ b/infra/cifuzz/test_files/external-project/oss-fuzz/Dockerfile @@ -0,0 +1,22 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +FROM gcr.io/oss-fuzz-base/base-builder +RUN apt-get update && apt-get install -y make + +COPY . $SRC/external-project +WORKDIR external-project +COPY oss-fuzz/build.sh $SRC/ diff --git a/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh b/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh new file mode 100644 index 000000000..2c52ef90f --- /dev/null +++ b/infra/cifuzz/test_files/external-project/oss-fuzz/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash -eu +# Copyright 2020 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +make clean # Not strictly necessary, since we are building in a fresh dir. +make -j$(nproc) all # Build the fuzz targets. + +# Copy the fuzzer executables, zip-ed corpora, option and dictionary files to $OUT +find . -name '*_fuzzer' -exec cp -v '{}' $OUT ';' +find . -name '*_fuzzer.dict' -exec cp -v '{}' $OUT ';' # If you have dictionaries. +find . -name '*_fuzzer.options' -exec cp -v '{}' $OUT ';' # If you have custom options. diff --git a/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp b/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp new file mode 100644 index 000000000..38a0454f0 --- /dev/null +++ b/infra/cifuzz/test_files/external-project/standalone_fuzz_target_runner.cpp @@ -0,0 +1,47 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Example of a standalone runner for "fuzz targets". +// It reads all files passed as parameters and feeds their contents +// one by one into the fuzz target (LLVMFuzzerTestOneInput). +// This runner does not do any fuzzing, but allows us to run the fuzz target +// on the test corpus (e.g. "do_stuff_test_data") or on a single file, +// e.g. the one that comes from a bug report. + +#include <cassert> +#include <iostream> +#include <fstream> +#include <vector> + +// Forward declare the "fuzz target" interface. +// We deliberately keep this inteface simple and header-free. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size); + +int main(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + std::ifstream in(argv[i]); + in.seekg(0, in.end); + size_t length = in.tellg(); + in.seekg (0, in.beg); + std::cout << "Reading " << length << " bytes from " << argv[i] << std::endl; + // Allocate exactly length bytes so that we reliably catch buffer overflows. + std::vector<char> bytes(length); + in.read(bytes.data(), bytes.size()); + assert(in); + LLVMFuzzerTestOneInput(reinterpret_cast<const uint8_t *>(bytes.data()), + bytes.size()); + std::cout << "Execution successful" << std::endl; + } + return 0; +} diff --git a/infra/repo_manager.py b/infra/repo_manager.py index 71f3e7821..1fa485bc1 100644 --- a/infra/repo_manager.py +++ b/infra/repo_manager.py @@ -29,8 +29,8 @@ import shutil import utils -class BaseRepoManager: - """Base repo manager.""" +class RepoManager: + """Repo manager.""" def __init__(self, repo_dir): self.repo_dir = repo_dir @@ -200,51 +200,36 @@ class BaseRepoManager: raise RuntimeError('Error checking out commit %s' % commit) def remove_repo(self): - """Attempts to remove the git repo. """ + """Removes the git repo from disk.""" if os.path.isdir(self.repo_dir): shutil.rmtree(self.repo_dir) -class RepoManager(BaseRepoManager): - """Class to manage git repos from python. - - Attributes: - repo_url: The location of the git repo. - base_dir: The location of where the repo clone is stored locally. - repo_name: The name of the GitHub project. - repo_dir: The location of the main repo. - """ - - def __init__(self, repo_url, base_dir, repo_name=None): - """Constructs a repo manager class. +def clone_repo_and_get_manager(repo_url, base_dir, repo_name=None): + """Clones a repo and constructs a repo manager class. Args: repo_url: The github url needed to clone. base_dir: The full file-path where the git repo is located. repo_name: The name of the directory the repo is cloned to. """ - self.repo_url = repo_url - self.base_dir = base_dir - if repo_name: - self.repo_name = repo_name - else: - self.repo_name = os.path.basename(self.repo_url).replace('.git', '') - repo_dir = os.path.join(self.base_dir, self.repo_name) - super(RepoManager, self).__init__(repo_dir) + if repo_name is None: + repo_name = os.path.basename(repo_url).replace('.git', '') + repo_dir = os.path.join(base_dir, repo_name) + manager = RepoManager(repo_dir) - if not os.path.exists(self.repo_dir): - self._clone() + if not os.path.exists(repo_dir): + _clone(repo_url, base_dir, repo_name) - def _clone(self): - """Creates a clone of the repo in the specified directory. + return manager - Raises: - ValueError: when the repo is not able to be cloned. - """ - if not os.path.exists(self.base_dir): - os.makedirs(self.base_dir) - self.remove_repo() - out, _, _ = utils.execute(['git', 'clone', self.repo_url, self.repo_name], - location=self.base_dir) - if not self._is_git_repo(): - raise ValueError('%s is not a git repo' % self.repo_url) + +def _clone(repo_url, base_dir, repo_name): + """Creates a clone of the repo in the specified directory. + + Raises: + ValueError: when the repo is not able to be cloned. + """ + utils.execute(['git', 'clone', repo_url, repo_name], + location=base_dir, + check_result=True) diff --git a/infra/repo_manager_test.py b/infra/repo_manager_test.py index f3294bc2a..653a21718 100644 --- a/infra/repo_manager_test.py +++ b/infra/repo_manager_test.py @@ -13,6 +13,7 @@ # limitations under the License. """Test the functionality of the RepoManager class.""" +import contextlib import os import tempfile import unittest @@ -21,31 +22,36 @@ from unittest import mock import repo_manager import utils -OSS_FUZZ_REPO = 'https://github.com/google/oss-fuzz' +# pylint: disable=protected-access +OSS_FUZZ_REPO_URL = 'https://github.com/google/oss-fuzz' -class RepoManagerCloneTest(unittest.TestCase): - """Tests the cloning functionality of RepoManager.""" + +@contextlib.contextmanager +def get_oss_fuzz_repo(): + """Clones a temporary copy of the OSS-Fuzz repo. Returns the path to the + repo.""" + repo_name = 'oss-fuzz' + with tempfile.TemporaryDirectory() as tmp_dir: + repo_manager._clone(OSS_FUZZ_REPO_URL, tmp_dir, repo_name) + yield os.path.join(tmp_dir, repo_name) + + +class CloneIntegrationTest(unittest.TestCase): + """Tests the _clone function.""" def test_clone_valid_repo(self): """Tests the correct location of the git repo.""" - with tempfile.TemporaryDirectory() as tmp_dir: - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) - git_path = os.path.join(test_repo_manager.base_dir, - test_repo_manager.repo_name, '.git') + with get_oss_fuzz_repo() as oss_fuzz_repo: + git_path = os.path.join(oss_fuzz_repo, '.git') self.assertTrue(os.path.isdir(git_path)) - test_repo_manager.remove_repo() def test_clone_invalid_repo(self): - """Tests that constructing RepoManager with an invalid repo will fail.""" + """Tests that cloning an invalid repo will fail.""" with tempfile.TemporaryDirectory() as tmp_dir: - with self.assertRaises(ValueError): - repo_manager.RepoManager(' ', tmp_dir) - with self.assertRaises(ValueError): - repo_manager.RepoManager('not_a_valid_repo', tmp_dir) - with self.assertRaises(ValueError): - repo_manager.RepoManager('https://github.com/oss-fuzz-not-real.git', - tmp_dir) + with self.assertRaises(RuntimeError): + repo_manager._clone('https://github.com/oss-fuzz-not-real.git', tmp_dir, + 'oss-fuzz') class RepoManagerCheckoutTest(unittest.TestCase): @@ -53,23 +59,22 @@ class RepoManagerCheckoutTest(unittest.TestCase): def test_checkout_valid_commit(self): """Tests that the git checkout command works.""" - with tempfile.TemporaryDirectory() as tmp_dir: - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) commit_to_test = '04ea24ee15bbe46a19e5da6c5f022a2ffdfbdb3b' - test_repo_manager.checkout_commit(commit_to_test) - self.assertEqual(commit_to_test, test_repo_manager.get_current_commit()) + repo_man.checkout_commit(commit_to_test) + self.assertEqual(commit_to_test, repo_man.get_current_commit()) def test_checkout_invalid_commit(self): """Tests that the git checkout invalid commit fails.""" - with tempfile.TemporaryDirectory() as tmp_dir: - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) with self.assertRaises(ValueError): - test_repo_manager.checkout_commit(' ') + repo_man.checkout_commit(' ') with self.assertRaises(ValueError): - test_repo_manager.checkout_commit( - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + repo_man.checkout_commit('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') with self.assertRaises(ValueError): - test_repo_manager.checkout_commit('not-a-valid-commit') + repo_man.checkout_commit('not-a-valid-commit') class RepoManagerGetCommitListTest(unittest.TestCase): @@ -77,8 +82,8 @@ class RepoManagerGetCommitListTest(unittest.TestCase): def test_get_valid_commit_list(self): """Tests an accurate commit list can be retrieved from the repo manager.""" - with tempfile.TemporaryDirectory() as tmp_dir: - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) old_commit = '04ea24ee15bbe46a19e5da6c5f022a2ffdfbdb3b' new_commit = 'fa662173bfeb3ba08d2e84cefc363be11e6c8463' commit_list = [ @@ -87,22 +92,22 @@ class RepoManagerGetCommitListTest(unittest.TestCase): '97dee00a3c4ce95071c3e061592f5fd577dea886', '04ea24ee15bbe46a19e5da6c5f022a2ffdfbdb3b' ] - result_list = test_repo_manager.get_commit_list(new_commit, old_commit) + result_list = repo_man.get_commit_list(new_commit, old_commit) self.assertListEqual(commit_list, result_list) def test_get_invalid_commit_list(self): """Tests that the proper errors are thrown when invalid commits are passed to get_commit_list.""" - with tempfile.TemporaryDirectory() as tmp_dir: + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) old_commit = '04ea24ee15bbe46a19e5da6c5f022a2ffdfbdb3b' new_commit = 'fa662173bfeb3ba08d2e84cefc363be11e6c8463' - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) with self.assertRaises(ValueError): - test_repo_manager.get_commit_list('fakecommit', new_commit) + repo_man.get_commit_list('fakecommit', new_commit) with self.assertRaises(ValueError): - test_repo_manager.get_commit_list(new_commit, 'fakecommit') + repo_man.get_commit_list(new_commit, 'fakecommit') with self.assertRaises(RuntimeError): - test_repo_manager.get_commit_list(old_commit, new_commit) # pylint: disable=arguments-out-of-order + repo_man.get_commit_list(old_commit, new_commit) # pylint: disable=arguments-out-of-order class GitDiffTest(unittest.TestCase): @@ -110,8 +115,8 @@ class GitDiffTest(unittest.TestCase): def test_diff_exists(self): """Tests that a real diff is returned when a valid repo manager exists.""" - with tempfile.TemporaryDirectory() as tmp_dir: - repo_man = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) with mock.patch.object(utils, 'execute', return_value=('test.py\ndiff.py', None, 0)): @@ -120,16 +125,16 @@ class GitDiffTest(unittest.TestCase): def test_diff_empty(self): """Tests that None is returned when there is no difference between repos.""" - with tempfile.TemporaryDirectory() as tmp_dir: - repo_man = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) with mock.patch.object(utils, 'execute', return_value=('', None, 0)): diff = repo_man.get_git_diff() self.assertIsNone(diff) def test_error_on_command(self): """Tests that None is returned when the command errors out.""" - with tempfile.TemporaryDirectory() as tmp_dir: - repo_man = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) with mock.patch.object(utils, 'execute', return_value=('', 'Test error.', 1)): @@ -138,8 +143,8 @@ class GitDiffTest(unittest.TestCase): def test_diff_no_change(self): """Tests that None is returned when there is no difference between repos.""" - with tempfile.TemporaryDirectory() as tmp_dir: - repo_man = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) diff = repo_man.get_git_diff() self.assertIsNone(diff) @@ -149,24 +154,22 @@ class CheckoutPrIntegrationTest(unittest.TestCase): def test_pull_request_exists(self): """Tests that a diff is returned when a valid PR is checked out.""" - with tempfile.TemporaryDirectory() as tmp_dir: - repo_man = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) repo_man.checkout_pr('refs/pull/3415/merge') diff = repo_man.get_git_diff() - print(diff) self.assertCountEqual(diff, ['README.md']) def test_checkout_invalid_pull_request(self): """Tests that the git checkout invalid pull request fails.""" - with tempfile.TemporaryDirectory() as tmp_dir: - test_repo_manager = repo_manager.RepoManager(OSS_FUZZ_REPO, tmp_dir) + with get_oss_fuzz_repo() as oss_fuzz_repo: + repo_man = repo_manager.RepoManager(oss_fuzz_repo) with self.assertRaises(RuntimeError): - test_repo_manager.checkout_pr(' ') + repo_man.checkout_pr(' ') with self.assertRaises(RuntimeError): - test_repo_manager.checkout_pr( - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') + repo_man.checkout_pr('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') with self.assertRaises(RuntimeError): - test_repo_manager.checkout_pr('not/a/valid/pr') + repo_man.checkout_pr('not/a/valid/pr') if __name__ == '__main__': diff --git a/infra/retry.py b/infra/retry.py index 4205319ea..293da998e 100644 --- a/infra/retry.py +++ b/infra/retry.py @@ -35,7 +35,6 @@ def get_delay(num_try, delay, backoff): def wrap(retries, delay, - function, backoff=2, exception_type=Exception, retry_on_false=False): @@ -49,7 +48,7 @@ def wrap(retries, """Decorator for the given function.""" tries = retries + 1 is_generator = inspect.isgeneratorfunction(func) - function_with_type = function + function_with_type = func.__qualname__ if is_generator: function_with_type += ' (generator)' diff --git a/infra/run_fuzzers.Dockerfile b/infra/run_fuzzers.Dockerfile index 938cf0881..38a65259c 100644 --- a/infra/run_fuzzers.Dockerfile +++ b/infra/run_fuzzers.Dockerfile @@ -17,7 +17,6 @@ FROM gcr.io/oss-fuzz-base/cifuzz-base -# Copies your code file from action repository to the container COPY cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py /opt/run_fuzzers_entrypoint.py # Python file to execute when the docker container starts up diff --git a/infra/test_helpers.py b/infra/test_helpers.py new file mode 100644 index 000000000..5a9a4d582 --- /dev/null +++ b/infra/test_helpers.py @@ -0,0 +1,27 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains convenient helpers for writing tests.""" + +import os +from unittest import mock + + +def patch_environ(testcase_obj, env=None): + """Patch environment.""" + if env is None: + env = {} + + patcher = mock.patch.dict(os.environ, env) + testcase_obj.addCleanup(patcher.stop) + patcher.start() |