diff options
-rw-r--r-- | docs/getting-started/continuous_integration.md | 5 | ||||
-rw-r--r-- | infra/cifuzz/actions/build_fuzzers/action.yml | 4 | ||||
-rw-r--r-- | infra/cifuzz/actions/build_fuzzers/build_fuzzers_entrypoint.py | 4 | ||||
-rw-r--r-- | infra/cifuzz/actions/run_fuzzers/action.yml | 4 | ||||
-rw-r--r-- | infra/cifuzz/actions/run_fuzzers/run_fuzzers_entrypoint.py | 6 | ||||
-rw-r--r-- | infra/cifuzz/cifuzz.py | 6 | ||||
-rw-r--r-- | infra/cifuzz/cifuzz_test.py | 89 | ||||
-rw-r--r-- | infra/cifuzz/example_main.yml | 3 | ||||
-rw-r--r-- | infra/cifuzz/fuzz_target.py | 131 | ||||
-rw-r--r-- | infra/cifuzz/fuzz_target_test.py | 121 |
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() |