aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--docs/getting-started/continuous_integration.md5
-rw-r--r--infra/cifuzz/actions/build_fuzzers/action.yml4
-rw-r--r--infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py4
-rw-r--r--infra/cifuzz/actions/run_fuzzers/action.yml4
-rw-r--r--infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py6
-rw-r--r--infra/cifuzz/cifuzz.py6
-rw-r--r--infra/cifuzz/cifuzz_test.py89
-rw-r--r--infra/cifuzz/example_main.yml3
-rw-r--r--infra/cifuzz/fuzz_target.py131
-rw-r--r--infra/cifuzz/fuzz_target_test.py121
10 files changed, 306 insertions, 67 deletions
diff --git a/docs/getting-started/continuous_integration.md b/docs/getting-started/continuous_integration.md
index 76564611..7e36346d 100644
--- a/docs/getting-started/continuous_integration.md
+++ b/docs/getting-started/continuous_integration.md
@@ -32,7 +32,7 @@ You can integrate CIFuzz into your project using the following steps:
1. Create a `workflows` directory inside of your `.github` directory.
1. Copy the example [`main.yml`](https://github.com/google/oss-fuzz/blob/master/infra/cifuzz/example_main.yml)
file over from the OSS-Fuzz repository to the `workflows` directory.
-1. Change the `project-name` value in `main.yml` from `example` to the name of your OSS-Fuzz project. It is **very important** that you use your OSS-Fuzz project name which is case sensitive. This name
+1. Change the `oss-fuzz-project-name` value in `main.yml` from `example` to the name of your OSS-Fuzz project. It is **very important** that you use your OSS-Fuzz project name which is case sensitive. This name
is the name of your project's subdirectory in the [`projects`](https://github.com/google/oss-fuzz/tree/master/projects) directory of OSS-Fuzz.
Your directory structure should look like the following:
@@ -56,11 +56,12 @@ jobs:
- name: Build Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
- project-name: 'example'
+ oss-fuzz-project-name: 'example'
dry-run: false
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
+ oss-fuzz-project-name: 'example'
fuzz-time: 600
dry-run: false
- name: Upload Crash
diff --git a/infra/cifuzz/actions/build_fuzzers/action.yml b/infra/cifuzz/actions/build_fuzzers/action.yml
index cea4c9f4..e9fb31ea 100644
--- a/infra/cifuzz/actions/build_fuzzers/action.yml
+++ b/infra/cifuzz/actions/build_fuzzers/action.yml
@@ -2,7 +2,7 @@
name: 'build-fuzzers'
description: "Builds an OSS-Fuzz project's fuzzers."
inputs:
- project-name:
+ oss-fuzz-project-name:
description: 'Name of the corresponding OSS-Fuzz project.'
required: true
dry-run:
@@ -12,5 +12,5 @@ runs:
using: 'docker'
image: 'Dockerfile'
env:
- PROJECT_NAME: ${{ inputs.project-name }}
+ OSS_FUZZ_PROJECT_NAME: ${{ inputs.oss-fuzz-project-name }}
DRY_RUN: ${{ inputs.dry-run}}
diff --git a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
index 92d795a3..9c0a982b 100644
--- a/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
+++ b/infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py
@@ -37,7 +37,7 @@ def main():
the directory: ${GITHUB_WORKSPACE}/out
Required environment variables:
- PROJECT_NAME: The name of OSS-Fuzz project.
+ OSS_FUZZ_PROJECT_NAME: The name of OSS-Fuzz project.
GITHUB_REPOSITORY: The name of the Github repo that called this script.
GITHUB_SHA: The commit SHA that triggered this script.
GITHUB_REF: The pull request reference that triggered this script.
@@ -47,7 +47,7 @@ def main():
Returns:
0 on success or 1 on Failure.
"""
- oss_fuzz_project_name = os.environ.get('PROJECT_NAME')
+ oss_fuzz_project_name = os.environ.get('OSS_FUZZ_PROJECT_NAME')
github_repo_name = os.path.basename(os.environ.get('GITHUB_REPOSITORY'))
pr_ref = os.environ.get('GITHUB_REF')
commit_sha = os.environ.get('GITHUB_SHA')
diff --git a/infra/cifuzz/actions/run_fuzzers/action.yml b/infra/cifuzz/actions/run_fuzzers/action.yml
index c4ce0e49..ca40c4fe 100644
--- a/infra/cifuzz/actions/run_fuzzers/action.yml
+++ b/infra/cifuzz/actions/run_fuzzers/action.yml
@@ -2,6 +2,9 @@
name: 'run-fuzzers'
description: 'Runs fuzz target binaries for a specified length of time.'
inputs:
+ oss-fuzz-project-name:
+ description: 'The OSS-Fuzz project name.'
+ required: true
fuzz-seconds:
description: 'The total time allotted for fuzzing in seconds.'
required: true
@@ -13,5 +16,6 @@ runs:
using: 'docker'
image: 'Dockerfile'
env:
+ OSS_FUZZ_PROJECT_NAME: ${{ inputs.oss-fuzz-project-name }}
FUZZ_SECONDS: ${{ inputs.fuzz-seconds }}
DRY_RUN: ${{ inputs.dry-run}}
diff --git a/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py b/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py
index 70a32f30..5aa83e5a 100644
--- a/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py
+++ b/infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py
@@ -46,13 +46,14 @@ def main():
FUZZ_SECONDS: The length of time in seconds that fuzzers are to be run.
GITHUB_WORKSPACE: The shared volume directory where input artifacts are.
DRY_RUN: If true, no failures will surface.
+ OSS_FUZZ_PROJECT_NAME: The name of the relevant OSS-Fuzz project.
Returns:
0 on success or 1 on Failure.
"""
fuzz_seconds = int(os.environ.get('FUZZ_SECONDS', 600))
workspace = os.environ.get('GITHUB_WORKSPACE')
-
+ oss_fuzz_project_name = os.environ.get('OSS_FUZZ_PROJECT_NAME')
# Check if failures should not be reported.
dry_run = (os.environ.get('DRY_RUN').lower() == 'true')
@@ -73,7 +74,8 @@ def main():
logging.error('This script needs to be run in the Github action context.')
return error_code
# Run the specified project's fuzzers from the build.
- run_status, bug_found = cifuzz.run_fuzzers(fuzz_seconds, workspace)
+ run_status, bug_found = cifuzz.run_fuzzers(fuzz_seconds, workspace,
+ oss_fuzz_project_name)
if not run_status:
logging.error('Error occured while running in workspace %s.', workspace)
return error_code
diff --git a/infra/cifuzz/cifuzz.py b/infra/cifuzz/cifuzz.py
index ab7b2cfe..6742f86f 100644
--- a/infra/cifuzz/cifuzz.py
+++ b/infra/cifuzz/cifuzz.py
@@ -154,13 +154,14 @@ def build_fuzzers(project_name,
return True
-def run_fuzzers(fuzz_seconds, workspace):
+def run_fuzzers(fuzz_seconds, workspace, project_name):
"""Runs all fuzzers for a specific OSS-Fuzz project.
Args:
fuzz_seconds: The total time allotted for fuzzing.
workspace: The location in a shared volume to store a git repo and build
artifacts.
+ project_name: The name of the relevant OSS-Fuzz project.
Returns:
(True if run was successful, True if bug was found).
@@ -188,7 +189,8 @@ def run_fuzzers(fuzz_seconds, workspace):
# Run fuzzers for alotted time.
for fuzzer_path in fuzzer_paths:
target = fuzz_target.FuzzTarget(fuzzer_path, fuzz_seconds_per_target,
- out_dir)
+ out_dir, project_name)
+
test_case, stack_trace = target.fuzz()
if not test_case or not stack_trace:
logging.info('Fuzzer %s, finished running.', target.target_name)
diff --git a/infra/cifuzz/cifuzz_test.py b/infra/cifuzz/cifuzz_test.py
index c62a95a6..27c96d90 100644
--- a/infra/cifuzz/cifuzz_test.py
+++ b/infra/cifuzz/cifuzz_test.py
@@ -114,7 +114,7 @@ class BuildFuzzersIntegrationTest(unittest.TestCase):
class RunFuzzersIntegrationTest(unittest.TestCase):
"""Test build_fuzzers function in the cifuzz module."""
- def test_valid(self):
+ def test_new_bug_found(self):
"""Test run_fuzzers with a valid build."""
with tempfile.TemporaryDirectory() as tmp_dir:
out_path = os.path.join(tmp_dir, 'out')
@@ -126,16 +126,48 @@ class RunFuzzersIntegrationTest(unittest.TestCase):
tmp_dir,
commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523'))
self.assertTrue(os.path.exists(os.path.join(out_path, 'do_stuff_fuzzer')))
- run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir)
- self.assertTrue(run_success)
- self.assertTrue(bug_found)
- def test_invlid_build(self):
+ # Setting the first return value to True, then the second to False to
+ # emulate a bug existing in the current PR but not on the downloaded
+ # OSS-Fuzz build.
+ with unittest.mock.patch.object(fuzz_target.FuzzTarget,
+ 'is_reproducible',
+ side_effect=[True, False]):
+ run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir, EXAMPLE_PROJECT)
+ build_dir = os.path.join(tmp_dir, 'out', 'oss_fuzz_latest')
+ self.assertTrue(os.path.exists(build_dir))
+ self.assertNotEqual(0, len(os.listdir(build_dir)))
+ self.assertTrue(run_success)
+ self.assertTrue(bug_found)
+
+ def test_old_bug_found(self):
+ """Test run_fuzzers with a bug found in OSS-Fuzz before."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ out_path = os.path.join(tmp_dir, 'out')
+ os.mkdir(out_path)
+ self.assertTrue(
+ cifuzz.build_fuzzers(
+ EXAMPLE_PROJECT,
+ 'oss-fuzz',
+ tmp_dir,
+ commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523'))
+ self.assertTrue(os.path.exists(os.path.join(out_path, 'do_stuff_fuzzer')))
+ with unittest.mock.patch.object(fuzz_target.FuzzTarget,
+ 'is_reproducible',
+ side_effect=[True, True]):
+ run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir, EXAMPLE_PROJECT)
+ build_dir = os.path.join(tmp_dir, 'out', 'oss_fuzz_latest')
+ self.assertTrue(os.path.exists(build_dir))
+ self.assertNotEqual(0, len(os.listdir(build_dir)))
+ self.assertTrue(run_success)
+ self.assertFalse(bug_found)
+
+ def test_invalid_build(self):
"""Test run_fuzzers with an invalid build."""
with tempfile.TemporaryDirectory() as tmp_dir:
out_path = os.path.join(tmp_dir, 'out')
os.mkdir(out_path)
- run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir)
+ run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir, EXAMPLE_PROJECT)
self.assertFalse(run_success)
self.assertFalse(bug_found)
@@ -144,13 +176,14 @@ class RunFuzzersIntegrationTest(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp_dir:
out_path = os.path.join(tmp_dir, 'out')
os.mkdir(out_path)
- run_success, bug_found = cifuzz.run_fuzzers(0, tmp_dir)
+ run_success, bug_found = cifuzz.run_fuzzers(0, tmp_dir, EXAMPLE_PROJECT)
self.assertFalse(run_success)
self.assertFalse(bug_found)
def test_invalid_out_dir(self):
"""Tests run_fuzzers with an invalid out directory."""
- run_success, bug_found = cifuzz.run_fuzzers(5, 'not/a/valid/path')
+ run_success, bug_found = cifuzz.run_fuzzers(5, 'not/a/valid/path',
+ EXAMPLE_PROJECT)
self.assertFalse(run_success)
self.assertFalse(bug_found)
@@ -184,45 +217,5 @@ class ParseOutputUnitTest(unittest.TestCase):
self.assertEqual(len(os.listdir(tmp_dir)), 0)
-class ReproduceIntegrationTest(unittest.TestCase):
- """Test that only reproducible bugs are reported by CIFuzz."""
-
- def test_reproduce_true(self):
- """Checks CIFuzz reports an error when a crash is reproducible."""
- with tempfile.TemporaryDirectory() as tmp_dir:
- out_path = os.path.join(tmp_dir, 'out')
- os.mkdir(out_path)
- self.assertTrue(
- cifuzz.build_fuzzers(
- EXAMPLE_PROJECT,
- 'oss-fuzz',
- tmp_dir,
- commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523'))
- with unittest.mock.patch.object(fuzz_target.FuzzTarget,
- 'is_reproducible',
- return_value=True):
- run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir)
- self.assertTrue(run_success)
- self.assertTrue(bug_found)
-
- def test_reproduce_false(self):
- """Checks CIFuzz doesn't report an error when a crash isn't reproducible."""
- with tempfile.TemporaryDirectory() as tmp_dir:
- out_path = os.path.join(tmp_dir, 'out')
- os.mkdir(out_path)
- self.assertTrue(
- cifuzz.build_fuzzers(
- EXAMPLE_PROJECT,
- 'oss-fuzz',
- tmp_dir,
- commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523'))
- with unittest.mock.patch.object(fuzz_target.FuzzTarget,
- 'is_reproducible',
- return_value=False):
- run_success, bug_found = cifuzz.run_fuzzers(5, tmp_dir)
- self.assertTrue(run_success)
- self.assertFalse(bug_found)
-
-
if __name__ == '__main__':
unittest.main()
diff --git a/infra/cifuzz/example_main.yml b/infra/cifuzz/example_main.yml
index 588dde82..ca69c744 100644
--- a/infra/cifuzz/example_main.yml
+++ b/infra/cifuzz/example_main.yml
@@ -7,11 +7,12 @@ jobs:
- name: Build Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
- project-name: 'example'
+ oss-fuzz-project-name: 'example'
dry-run: false
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
+ oss-fuzz-project-name: 'example'
fuzz-time: 600
dry-run: false
- name: Upload Crash
diff --git a/infra/cifuzz/fuzz_target.py b/infra/cifuzz/fuzz_target.py
index 50d4f1d0..d49c0981 100644
--- a/infra/cifuzz/fuzz_target.py
+++ b/infra/cifuzz/fuzz_target.py
@@ -14,9 +14,12 @@
"""A module to handle running a fuzz target for a specified amount of time."""
import logging
import os
+import posixpath
import re
import subprocess
import sys
+import urllib.request
+import zipfile
# pylint: disable=wrong-import-position
# pylint: disable=import-error
@@ -30,9 +33,20 @@ logging.basicConfig(
LIBFUZZER_OPTIONS = '-seed=1337 -len_control=0'
+# Location of google cloud storage for latest OSS-Fuzz builds.
+GCS_BASE_URL = 'https://storage.googleapis.com/clusterfuzz-builds'
+
# The number of reproduce attempts for a crash.
REPRODUCE_ATTEMPTS = 10
+# The name to store the latest OSS-Fuzz build at.
+BUILD_ARCHIVE_NAME = 'oss_fuzz_latest.zip'
+
+# The get request for the latest version of a project's build.
+VERSION_STRING = '{project_name}-{sanitizer}-latest.version'
+
+SANITIZER = 'address'
+
class FuzzTarget:
"""A class to manage a single fuzz target.
@@ -41,20 +55,26 @@ class FuzzTarget:
target_name: The name of the fuzz target.
duration: The length of time in seconds that the target should run.
target_path: The location of the fuzz target binary.
+ project_name: The name of the relevant OSS-Fuzz project.
"""
- def __init__(self, target_path, duration, out_dir):
+ def __init__(self, target_path, duration, out_dir, project_name=None):
"""Represents a single fuzz target.
+ Note: project_name should be none when the fuzzer being run is not
+ associated with a specific OSS-Fuzz project.
+
Args:
target_path: The location of the fuzz target binary.
duration: The length of time in seconds the target should run.
out_dir: The location of where the output from crashes should be stored.
+ project_name: The name of the relevant OSS-Fuzz project.
"""
self.target_name = os.path.basename(target_path)
self.duration = duration
self.target_path = target_path
self.out_dir = out_dir
+ self.project_name = project_name
def fuzz(self):
"""Starts the fuzz target run for the length of time specified by duration.
@@ -95,23 +115,23 @@ class FuzzTarget:
if not test_case:
logging.error('No test case found in stack trace. %s.', sys.stderr)
return None, None
- if self.is_reproducible(test_case):
+ if self.check_reproducibility_and_regression(test_case):
return test_case, err_str
- logging.error('A crash was found but it was not reproducible.')
return None, None
- def is_reproducible(self, test_case):
+ def is_reproducible(self, test_case, target_path):
"""Checks if the test case reproduces.
Args:
test_case: The path to the test case to be tested.
+ target_path: The path to the fuzz target to be tested
Returns:
True if crash is reproducible.
"""
command = [
'docker', 'run', '--rm', '--privileged', '-v',
- '%s:/out' % os.path.dirname(self.target_path), '-v',
+ '%s:/out' % target_path, '-v',
'%s:/testcase' % test_case, '-t', 'gcr.io/oss-fuzz-base/base-runner',
'reproduce', self.target_name, '-runs=100'
]
@@ -121,6 +141,43 @@ class FuzzTarget:
return True
return False
+ def check_reproducibility_and_regression(self, test_case):
+ """Checks if a crash is reproducible, and if it is, whether it's a new
+ regression that cannot be reproduced with the latest OSS-Fuzz build.
+
+ NOTE: If no project is specified the crash is assumed introduced
+ by the pull request if it is reproducible.
+
+ Args:
+ test_case: The path to the test_case that triggered the crash.
+
+ Returns:
+ True if the crash was introduced by the current pull request.
+ """
+ reproducible_in_pr = self.is_reproducible(test_case,
+ os.path.dirname(self.target_path))
+ if not self.project_name:
+ return reproducible_in_pr
+
+ if not reproducible_in_pr:
+ logging.info(
+ 'Failed to reproduce the crash using the obtained test case.')
+ return False
+
+ oss_fuzz_build_dir = self.download_oss_fuzz_build()
+ if not oss_fuzz_build_dir:
+ return False
+
+ reproducible_in_oss_fuzz = self.is_reproducible(test_case,
+ oss_fuzz_build_dir)
+
+ if reproducible_in_pr and not reproducible_in_oss_fuzz:
+ logging.info('The crash is reproducible. The crash doesn\'t reproduce ' \
+ 'on old builds. This pull request probably introduced the crash.')
+ return True
+ logging.info('The crash is reproducible without the current pull request.')
+ return False
+
def get_test_case(self, error_string):
"""Gets the file from a fuzzer run stack trace.
@@ -134,3 +191,67 @@ class FuzzTarget:
if match:
return os.path.join(self.out_dir, match.group(1))
return None
+
+ def get_lastest_build_version(self):
+ """Gets the latest OSS-Fuzz build version for a projects' fuzzers.
+
+ Returns:
+ A string with the latest build version or None.
+ """
+ if not self.project_name:
+ return None
+
+ version = VERSION_STRING.format(project_name=self.project_name,
+ sanitizer=SANITIZER)
+ version_url = url_join(GCS_BASE_URL, self.project_name, version)
+ try:
+ response = urllib.request.urlopen(version_url)
+ except urllib.error.HTTPError:
+ logging.error(
+ 'Error getting the lastest build version for %s from url %s.',
+ self.project_name, version_url)
+ return None
+ return response.read().decode()
+
+ def download_oss_fuzz_build(self):
+ """Downloads the latest OSS-Fuzz build from GCS.
+
+ Returns:
+ A path to where the OSS-Fuzz build is located, or None.
+ """
+ if not os.path.exists(self.out_dir):
+ logging.error('Out directory %s does not exist.', self.out_dir)
+ return None
+ if not self.project_name:
+ return None
+ build_dir = os.path.join(self.out_dir, 'oss_fuzz_latest', self.project_name)
+ if os.path.exists(os.path.join(build_dir, self.target_name)):
+ return build_dir
+ os.makedirs(build_dir, exist_ok=True)
+ latest_build_str = self.get_lastest_build_version()
+ if not latest_build_str:
+ return None
+
+ oss_fuzz_build_url = url_join(GCS_BASE_URL, self.project_name,
+ latest_build_str)
+ try:
+ urllib.request.urlretrieve(oss_fuzz_build_url, BUILD_ARCHIVE_NAME)
+ except urllib.error.HTTPError:
+ logging.error('Unable to download build from: %s.', oss_fuzz_build_url)
+ return None
+ with zipfile.ZipFile(BUILD_ARCHIVE_NAME, 'r') as zip_file:
+ zip_file.extractall(build_dir)
+ os.remove(BUILD_ARCHIVE_NAME)
+ return build_dir
+
+
+def url_join(*argv):
+ """Joins URLs together using the posix join method.
+
+ Args:
+ argv: Sections of a URL to be joined.
+
+ Returns:
+ Joined URL.
+ """
+ return posixpath.join(*argv)
diff --git a/infra/cifuzz/fuzz_target_test.py b/infra/cifuzz/fuzz_target_test.py
index 9656e55f..d770b13b 100644
--- a/infra/cifuzz/fuzz_target_test.py
+++ b/infra/cifuzz/fuzz_target_test.py
@@ -15,6 +15,7 @@
import os
import sys
+import tempfile
import unittest
import unittest.mock
@@ -44,14 +45,18 @@ class IsReproducibleUnitTest(unittest.TestCase):
all_success_mock = unittest.mock.Mock()
all_success_mock.side_effect = test_all_success
utils.execute = all_success_mock
- self.assertTrue(self.test_target.is_reproducible('/fake/path/to/testcase'))
+ self.assertTrue(
+ self.test_target.is_reproducible('/fake/path/to/testcase',
+ '/fake/target'))
self.assertEqual(1, all_success_mock.call_count)
test_one_success = [(0, 0, 0)] * 9 + [(0, 0, 1)]
one_success_mock = unittest.mock.Mock()
one_success_mock.side_effect = test_one_success
utils.execute = one_success_mock
- self.assertTrue(self.test_target.is_reproducible('/fake/path/to/testcase'))
+ self.assertTrue(
+ self.test_target.is_reproducible('/fake/path/to/testcase',
+ '/fake/target'))
self.assertEqual(10, one_success_mock.call_count)
def test_with_not_reproducible(self):
@@ -60,7 +65,9 @@ class IsReproducibleUnitTest(unittest.TestCase):
all_fail_mock = unittest.mock.Mock()
all_fail_mock.side_effect = test_all_fail
utils.execute = all_fail_mock
- self.assertFalse(self.test_target.is_reproducible('/fake/path/to/testcase'))
+ self.assertFalse(
+ self.test_target.is_reproducible('/fake/path/to/testcase',
+ '/fake/target'))
class GetTestCaseUnitTest(unittest.TestCase):
@@ -87,5 +94,113 @@ class GetTestCaseUnitTest(unittest.TestCase):
self.assertIsNone(self.test_target.get_test_case(' Example crash string.'))
+class CheckReproducibilityAndRegressionUnitTest(unittest.TestCase):
+ """Test check_reproducibility_and_regression function fuzz_target module."""
+
+ def setUp(self):
+ """Sets up dummy fuzz target to test is_reproducible method."""
+ self.test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ '/example/outdir', 'example')
+
+ def test_with_valid_crash(self):
+ """Checks to make sure a valid crash returns true."""
+ with unittest.mock.patch.object(
+ fuzz_target.FuzzTarget, 'is_reproducible',
+ side_effect=[True, False]), tempfile.TemporaryDirectory() as tmp_dir:
+ self.test_target.out_dir = tmp_dir
+ self.assertTrue(
+ self.test_target.check_reproducibility_and_regression(
+ '/example/crash/testcase'))
+
+ def test_with_invalid_crash(self):
+ """Checks to make sure an invalid crash returns false."""
+ with unittest.mock.patch.object(fuzz_target.FuzzTarget,
+ 'is_reproducible',
+ side_effect=[True, True]):
+ self.assertFalse(
+ self.test_target.check_reproducibility_and_regression(
+ '/example/crash/testcase'))
+
+ with unittest.mock.patch.object(fuzz_target.FuzzTarget,
+ 'is_reproducible',
+ side_effect=[False, True]):
+ self.assertFalse(
+ self.test_target.check_reproducibility_and_regression(
+ '/example/crash/testcase'))
+
+ with unittest.mock.patch.object(fuzz_target.FuzzTarget,
+ 'is_reproducible',
+ side_effect=[False, False]):
+ self.assertFalse(
+ self.test_target.check_reproducibility_and_regression(
+ '/example/crash/testcase'))
+
+
+class GetLatestBuildVersionUnitTest(unittest.TestCase):
+ """Test the get_latest_build_version function in the fuzz_target module."""
+
+ def test_get_valid_project(self):
+ """Checks the latest build can be retrieved from gcs."""
+ test_target = fuzz_target.FuzzTarget('/example/path', 10, '/example/outdir',
+ 'example')
+ latest_build = test_target.get_lastest_build_version()
+ self.assertIsNotNone(latest_build)
+ self.assertTrue(latest_build.endswith('.zip'))
+ self.assertTrue('address' in latest_build)
+
+ def test_get_invalid_project(self):
+ """Checks the latest build will return None when project doesn't exist."""
+ test_target = fuzz_target.FuzzTarget('/example/path', 10, '/example/outdir',
+ 'not-a-proj')
+ self.assertIsNone(test_target.get_lastest_build_version())
+ test_target = fuzz_target.FuzzTarget('/example/path', 10, '/example/outdir')
+ self.assertIsNone(test_target.get_lastest_build_version())
+
+
+class DownloadOSSFuzzBuildDirIntegrationTests(unittest.TestCase):
+ """Test the download_oss_fuzz_build in function in the fuzz_target module."""
+
+ def test_single_download(self):
+ """Checks that the build directory was only downloaded once."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ tmp_dir, 'example')
+ latest_version = test_target.get_lastest_build_version()
+ with unittest.mock.patch.object(
+ fuzz_target.FuzzTarget,
+ 'get_lastest_build_version',
+ return_value=latest_version) as mock_build_version:
+ for _ in range(5):
+ oss_fuzz_build_path = test_target.download_oss_fuzz_build()
+ self.assertEqual(1, mock_build_version.call_count)
+ self.assertIsNotNone(oss_fuzz_build_path)
+ self.assertTrue(os.listdir(oss_fuzz_build_path))
+
+ def test_get_valid_project(self):
+ """Checks the latest build can be retrieved from gcs."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ tmp_dir, 'example')
+ oss_fuzz_build_path = test_target.download_oss_fuzz_build()
+ self.assertIsNotNone(oss_fuzz_build_path)
+ self.assertTrue(os.listdir(oss_fuzz_build_path))
+
+ def test_get_invalid_project(self):
+ """Checks the latest build will return None when project doesn't exist."""
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ tmp_dir)
+ self.assertIsNone(test_target.download_oss_fuzz_build())
+ test_target = fuzz_target.FuzzTarget('/example/do_stuff_fuzzer', 10,
+ tmp_dir, 'not-a-proj')
+ self.assertIsNone(test_target.download_oss_fuzz_build())
+
+ def test_invalid_build_dir(self):
+ """Checks the download will return 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())
+
+
if __name__ == '__main__':
unittest.main()