diff --git a/infra/cifuzz/affected_fuzz_targets_test.py b/infra/cifuzz/affected_fuzz_targets_test.py index d697f4d3f..96d6df7aa 100644 --- a/infra/cifuzz/affected_fuzz_targets_test.py +++ b/infra/cifuzz/affected_fuzz_targets_test.py @@ -30,15 +30,15 @@ EXAMPLE_PROJECT = 'example' EXAMPLE_FILE_CHANGED = 'test.txt' -TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), - 'test_data') +TEST_DATA_OUT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'test_data', 'build-out') class RemoveUnaffectedFuzzTargets(unittest.TestCase): """Tests remove_unaffected_fuzzers.""" - TEST_FUZZER_1 = os.path.join(TEST_DATA_PATH, 'out', 'example_crash_fuzzer') - TEST_FUZZER_2 = os.path.join(TEST_DATA_PATH, 'out', 'example_nocrash_fuzzer') + TEST_FUZZER_1 = os.path.join(TEST_DATA_OUT_PATH, 'example_crash_fuzzer') + TEST_FUZZER_2 = os.path.join(TEST_DATA_OUT_PATH, 'example_nocrash_fuzzer') # yapf: disable @parameterized.parameterized.expand([ diff --git a/infra/cifuzz/build_fuzzers.py b/infra/cifuzz/build_fuzzers.py index 4254067d3..763659749 100644 --- a/infra/cifuzz/build_fuzzers.py +++ b/infra/cifuzz/build_fuzzers.py @@ -50,10 +50,9 @@ class Builder: # pylint: disable=too-many-instance-attributes def __init__(self, config, ci_system): self.config = config self.ci_system = ci_system - self.out_dir = os.path.join(config.workspace, 'out') - os.makedirs(self.out_dir, exist_ok=True) - self.work_dir = os.path.join(config.workspace, 'work') - os.makedirs(self.work_dir, exist_ok=True) + self.workspace = docker.Workspace(config) + self.workspace.initialize_dir(self.workspace.out) + self.workspace.initialize_dir(self.workspace.work) self.image_repo_path = None self.host_repo_path = None self.repo_manager = None @@ -75,13 +74,14 @@ class Builder: # pylint: disable=too-many-instance-attributes """Moves the source code we want to fuzz into the project builder and builds the fuzzers from that source code. Returns True on success.""" docker_args, docker_container = docker.get_base_docker_run_args( - self.out_dir, self.config.sanitizer, self.config.language) + self.workspace, self.config.sanitizer, self.config.language) if not docker_container: docker_args.extend( _get_docker_build_fuzzers_args_not_container(self.host_repo_path)) if self.config.sanitizer == 'memory': - docker_args.extend(_get_docker_build_fuzzers_args_msan(self.work_dir)) + docker_args.extend( + _get_docker_build_fuzzers_args_msan(self.workspace.work)) self.handle_msan_prebuild(docker_container) docker_args.extend([ @@ -111,7 +111,7 @@ class Builder: # pylint: disable=too-many-instance-attributes """Post-build step for MSAN builds. Patches the build to use MSAN libraries.""" helper.docker_run([ - '--volumes-from', container, '-e', f'WORK={self.work_dir}', + '--volumes-from', container, '-e', f'WORK={self.workspace.work}', docker.MSAN_LIBS_BUILDER_TAG, 'patch_build.py', '/out' ]) @@ -121,7 +121,7 @@ class Builder: # pylint: disable=too-many-instance-attributes logging.info('Copying MSAN libs.') helper.docker_run([ '--volumes-from', container, docker.MSAN_LIBS_BUILDER_TAG, 'bash', '-c', - f'cp -r /msan {self.work_dir}' + f'cp -r /msan {self.workspace.work}' ]) def build(self): @@ -146,7 +146,7 @@ class Builder: # pylint: disable=too-many-instance-attributes changed_files = self.ci_system.get_changed_code_under_test( self.repo_manager) affected_fuzz_targets.remove_unaffected_fuzz_targets( - self.config.project_name, self.out_dir, changed_files, + self.config.project_name, self.workspace.out, changed_files, self.image_repo_path) return True @@ -178,7 +178,7 @@ def build_fuzzers(config): return builder.build() -def check_fuzzer_build(out_dir, +def check_fuzzer_build(workspace, sanitizer, language, allowed_broken_targets_percentage=None): @@ -191,14 +191,15 @@ def check_fuzzer_build(out_dir, Returns: True if fuzzers are correct. """ - if not os.path.exists(out_dir): - logging.error('Invalid out directory: %s.', out_dir) + if not os.path.exists(workspace.out): + logging.error('Invalid out directory: %s.', workspace.out) return False - if not os.listdir(out_dir): - logging.error('No fuzzers found in out directory: %s.', out_dir) + if not os.listdir(workspace.out): + logging.error('No fuzzers found in out directory: %s.', workspace.out) return False - docker_args, _ = docker.get_base_docker_run_args(out_dir, sanitizer, language) + docker_args, _ = docker.get_base_docker_run_args(workspace, sanitizer, + language) if allowed_broken_targets_percentage is not None: docker_args += [ '-e', @@ -216,7 +217,7 @@ def check_fuzzer_build(out_dir, def _get_docker_build_fuzzers_args_not_container(host_repo_path): """Returns arguments to the docker build arguments that are needed to use - |host_out_dir| when the host of the OSS-Fuzz builder container is not + |host_repo_path| when the host of the OSS-Fuzz builder container is not another container.""" return ['-v', f'{host_repo_path}:{host_repo_path}'] diff --git a/infra/cifuzz/build_fuzzers_entrypoint.py b/infra/cifuzz/build_fuzzers_entrypoint.py index 04f562068..2ab7aa361 100644 --- a/infra/cifuzz/build_fuzzers_entrypoint.py +++ b/infra/cifuzz/build_fuzzers_entrypoint.py @@ -13,11 +13,11 @@ # limitations under the License. """Builds a specific OSS-Fuzz project's fuzzers for CI tools.""" import logging -import os import sys import build_fuzzers import config_utils +import docker # pylint: disable=c-extension-no-member # pylint gets confused because of the relative import of cifuzz. @@ -71,15 +71,13 @@ def main(): config.project_name, config.commit_sha, config.pr_ref) return returncode - out_dir = os.path.join(config.workspace, 'out') - if not config.bad_build_check: # If we've gotten to this point and we don't need to do bad_build_check, # then the build has succeeded. returncode = 0 # yapf: disable elif build_fuzzers.check_fuzzer_build( - out_dir, + docker.Workspace(config), config.sanitizer, config.language, allowed_broken_targets_percentage=config.allowed_broken_targets_percentage diff --git a/infra/cifuzz/build_fuzzers_test.py b/infra/cifuzz/build_fuzzers_test.py index 35194f754..b88552301 100644 --- a/infra/cifuzz/build_fuzzers_test.py +++ b/infra/cifuzz/build_fuzzers_test.py @@ -136,7 +136,7 @@ class BuildFuzzersIntegrationTest(unittest.TestCase): def setUp(self): self.tmp_dir_obj = tempfile.TemporaryDirectory() self.workspace = self.tmp_dir_obj.name - self.out_dir = os.path.join(self.workspace, 'out') + self.out_dir = os.path.join(self.workspace, 'build-out') test_helpers.patch_environ(self) def tearDown(self): @@ -268,29 +268,32 @@ class CheckFuzzerBuildTest(unittest.TestCase): def setUp(self): self.tmp_dir_obj = tempfile.TemporaryDirectory() - self.test_files_path = os.path.join(self.tmp_dir_obj.name, 'test_files') - shutil.copytree(TEST_DATA_PATH, self.test_files_path) + workspace_path = os.path.join(self.tmp_dir_obj.name, 'workspace') + self.workspace = test_helpers.create_workspace(workspace_path) + shutil.copytree(TEST_DATA_PATH, workspace_path) def tearDown(self): self.tmp_dir_obj.cleanup() def test_correct_fuzzer_build(self): """Checks check_fuzzer_build function returns True for valid fuzzers.""" - test_fuzzer_dir = os.path.join(self.test_files_path, 'out') self.assertTrue( - build_fuzzers.check_fuzzer_build(test_fuzzer_dir, self.SANITIZER, + build_fuzzers.check_fuzzer_build(self.workspace, self.SANITIZER, self.LANGUAGE)) - def test_not_a_valid_fuzz_path(self): - """Tests that False is returned when a bad path is given.""" + def test_not_a_valid_path(self): + """Tests that False is returned when a nonexistent path is given.""" + workspace = test_helpers.create_workspace('not/a/valid/path') self.assertFalse( - build_fuzzers.check_fuzzer_build('not/a/valid/path', self.SANITIZER, + build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER, self.LANGUAGE)) - def test_not_a_valid_fuzzer(self): - """Checks a directory that exists but does not have fuzzers is False.""" + def test_no_valid_fuzzers(self): + """Tests that False is returned when an empty directory is given.""" + with tempfile.TemporaryDirectory() as tmp_dir: + workspace = test_helpers.create_workspace(tmp_dir) self.assertFalse( - build_fuzzers.check_fuzzer_build(self.test_files_path, self.SANITIZER, + build_fuzzers.check_fuzzer_build(workspace, self.SANITIZER, self.LANGUAGE)) @mock.patch('helper.docker_run') @@ -298,8 +301,7 @@ class CheckFuzzerBuildTest(unittest.TestCase): """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running docker if passed to check_fuzzer_build.""" mocked_docker_run.return_value = 0 - test_fuzzer_dir = os.path.join(TEST_DATA_PATH, 'out') - build_fuzzers.check_fuzzer_build(test_fuzzer_dir, + build_fuzzers.check_fuzzer_build(self.workspace, self.SANITIZER, self.LANGUAGE, allowed_broken_targets_percentage='0') diff --git a/infra/cifuzz/clusterfuzz_deployment.py b/infra/cifuzz/clusterfuzz_deployment.py index eb9f835af..798913bdb 100644 --- a/infra/cifuzz/clusterfuzz_deployment.py +++ b/infra/cifuzz/clusterfuzz_deployment.py @@ -30,13 +30,11 @@ import utils class BaseClusterFuzzDeployment: """Base class for ClusterFuzz deployments.""" - CORPUS_DIR_NAME = 'cifuzz-corpus' - BUILD_DIR_NAME = 'cifuzz-latest-build' - - def __init__(self, config): + def __init__(self, config, workspace): self.config = config + self.workspace = workspace - def download_latest_build(self, parent_dir): + def download_latest_build(self): """Downloads the latest build from ClusterFuzz. Returns: @@ -44,47 +42,37 @@ class BaseClusterFuzzDeployment: """ raise NotImplementedError('Child class must implement method.') - def upload_latest_build(self, build_dir): + def upload_latest_build(self): """Uploads the latest build to the filestore. Returns: True on success. """ raise NotImplementedError('Child class must implement method.') - def download_corpus(self, target_name, parent_dir): - """Downloads the corpus for |target_name| from ClusterFuzz to a subdirectory - of |parent_dir|. + def download_corpus(self, target_name): + """Downloads the corpus for |target_name| from ClusterFuzz. Returns: A path to where the OSS-Fuzz build was stored, or None if it wasn't. """ raise NotImplementedError('Child class must implement method.') - def upload_crashes(self, crashes_dir): + def upload_crashes(self): """Uploads crashes in |crashes_dir| to filestore.""" raise NotImplementedError('Child class must implement method.') - def get_target_corpus_dir(self, target_name, parent_dir): - """Returns the path to the corpus dir for |target_name| within - |parent_dir|.""" - return os.path.join(self.get_corpus_dir(parent_dir), target_name) + def get_target_corpus_dir(self, target_name): + """Returns the path to the corpus dir for |target_name|.""" + return os.path.join(self.workspace.corpora, target_name) - def get_corpus_dir(self, parent_dir): - """Returns the path to the corpus dir within |parent_dir|.""" - return os.path.join(parent_dir, self.CORPUS_DIR_NAME) - - def get_build_dir(self, parent_dir): - """Returns the path to the build dir for within |parent_dir|.""" - return os.path.join(parent_dir, self.BUILD_DIR_NAME) - - def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument - """Uploads the corpus for |target_name| in |corpus_dir| to filestore.""" + def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument + """Uploads the corpus for |target_name| to filestore.""" raise NotImplementedError('Child class must implement method.') - def make_empty_corpus_dir(self, target_name, parent_dir): + def make_empty_corpus_dir(self, target_name): """Makes an empty corpus directory for |target_name| in |parent_dir| and returns the path to the directory.""" - corpus_dir = self.get_target_corpus_dir(target_name, parent_dir) + corpus_dir = self.get_target_corpus_dir(target_name) os.makedirs(corpus_dir, exist_ok=True) return corpus_dir @@ -94,35 +82,35 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment): BASE_BUILD_NAME = 'cifuzz-build-' - def __init__(self, config): - super().__init__(config) + def __init__(self, config, workspace): + super().__init__(config, workspace) self.filestore = filestore_utils.get_filestore(self.config) - def download_latest_build(self, parent_dir): - build_dir = self.get_build_dir(parent_dir) - if os.path.exists(build_dir): + def download_latest_build(self): + if os.path.exists(self.workspace.clusterfuzz_build): # This path is necessary because download_latest_build can be called # multiple times.That is the case because it is called only when we need # to see if a bug is novel, i.e. until we want to check a bug is novel we # don't want to waste time calling this, but therefore this method can be # called if multiple bugs are found. - return build_dir + return self.workspace.clusterfuzz_build - os.makedirs(build_dir, exist_ok=True) + os.makedirs(self.workspace.clusterfuzz_build, exist_ok=True) build_name = self._get_build_name() try: - if self.filestore.download_latest_build(build_name, build_dir): - return build_dir + if self.filestore.download_latest_build(build_name, + self.workspace.clusterfuzz_build): + return self.workspace.clusterfuzz_build except Exception as err: # pylint: disable=broad-except logging.error('Could not download latest build because of: %s.', err) return None - def download_corpus(self, target_name, parent_dir): - corpus_dir = self.make_empty_corpus_dir(target_name, parent_dir) + def download_corpus(self, target_name): + corpus_dir = self.make_empty_corpus_dir(target_name) logging.debug('ClusterFuzzLite: downloading corpus for %s to %s.', - target_name, parent_dir) + target_name, corpus_dir) corpus_name = self._get_corpus_name(target_name) try: self.filestore.download_corpus(corpus_name, corpus_dir) @@ -142,8 +130,9 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment): """Returns the name of the crashes artifact.""" return 'crashes' - def upload_corpus(self, target_name, corpus_dir): + def upload_corpus(self, target_name): """Upload the corpus produced by |target_name| in |corpus_dir|.""" + corpus_dir = self.get_target_corpus_dir(target_name) logging.info('Uploading corpus in %s for %s.', corpus_dir, target_name) name = self._get_corpus_name(target_name) try: @@ -152,25 +141,28 @@ class ClusterFuzzLite(BaseClusterFuzzDeployment): logging.error('Failed to upload corpus for target: %s. Error: %s.', target_name, error) - def upload_latest_build(self, build_dir): - logging.info('Uploading latest build in %s.', build_dir) + def upload_latest_build(self): + logging.info('Uploading latest build in %s.', + self.workspace.clusterfuzz_build) build_name = self._get_build_name() try: - return self.filestore.upload_directory(build_name, build_dir) + return self.filestore.upload_directory(build_name, + self.workspace.clusterfuzz_build) except Exception as error: # pylint: disable=broad-except - logging.error('Failed to upload latest build: %s. Error: %s.', build_dir, - error) + logging.error('Failed to upload latest build: %s. Error: %s.', + self.workspace.clusterfuzz_build, error) - def upload_crashes(self, crashes_dir): - if not os.listdir(crashes_dir): - logging.info('No crashes in %s. Not uploading.', crashes_dir) + def upload_crashes(self): + if not os.listdir(self.workspace.artifacts): + logging.info('No crashes in %s. Not uploading.', self.workspace.artifacts) return crashes_artifact_name = self._get_crashes_artifact_name() - logging.info('Uploading crashes in %s', crashes_dir) + logging.info('Uploading crashes in %s', self.workspace.artifacts) try: - self.filestore.upload_directory(crashes_artifact_name, crashes_dir) + self.filestore.upload_directory(crashes_artifact_name, + self.workspace.artifacts) except Exception as error: # pylint: disable=broad-except logging.error('Failed to upload crashes. Error: %s.', error) @@ -202,19 +194,18 @@ class OSSFuzz(BaseClusterFuzzDeployment): return None return response.read().decode() - def download_latest_build(self, parent_dir): + def download_latest_build(self): """Downloads the latest OSS-Fuzz build from GCS. Returns: A path to where the OSS-Fuzz build was stored, or None if it wasn't. """ - build_dir = self.get_build_dir(parent_dir) - if os.path.exists(build_dir): + if os.path.exists(self.workspace.clusterfuzz_build): # This function can be called multiple times, don't download the build # again. - return build_dir + return self.workspace.clusterfuzz_build - os.makedirs(build_dir, exist_ok=True) + os.makedirs(self.workspace.clusterfuzz_build, exist_ok=True) latest_build_name = self.get_latest_build_name() if not latest_build_name: @@ -224,30 +215,31 @@ class OSSFuzz(BaseClusterFuzzDeployment): self.CLUSTERFUZZ_BUILDS, self.config.project_name, latest_build_name) - if http_utils.download_and_unpack_zip(oss_fuzz_build_url, build_dir): - return build_dir + if http_utils.download_and_unpack_zip(oss_fuzz_build_url, + self.workspace.clusterfuzz_build): + return self.workspace.clusterfuzz_build return None - def upload_latest_build(self, build_dir): # pylint: disable=no-self-use,unused-argument + def upload_latest_build(self): # pylint: disable=no-self-use """Noop Impelementation of upload_latest_build.""" logging.info('Not uploading latest build because on OSS-Fuzz.') - def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument + def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument """Noop Impelementation of upload_corpus.""" logging.info('Not uploading corpus because on OSS-Fuzz.') - def upload_crashes(self, crashes_dir): # pylint: disable=no-self-use,unused-argument + def upload_crashes(self): # pylint: disable=no-self-use """Noop Impelementation of upload_crashes.""" logging.info('Not uploading crashes because on OSS-Fuzz.') - def download_corpus(self, target_name, parent_dir): + def download_corpus(self, target_name): """Downloads the latest OSS-Fuzz corpus for the target. Returns: The local path to to corpus or None if download failed. """ - corpus_dir = self.make_empty_corpus_dir(target_name, parent_dir) + corpus_dir = self.make_empty_corpus_dir(target_name) project_qualified_fuzz_target_name = target_name qualified_name_prefix = self.config.project_name + '_' if not target_name.startswith(qualified_name_prefix): @@ -267,38 +259,38 @@ class NoClusterFuzzDeployment(BaseClusterFuzzDeployment): """ClusterFuzzDeployment implementation used when there is no deployment of ClusterFuzz to use.""" - def upload_latest_build(self, build_dir): # pylint: disable=no-self-use,unused-argument + def upload_latest_build(self): # pylint: disable=no-self-use """Noop Impelementation of upload_latest_build.""" logging.info('Not uploading latest build because no ClusterFuzz ' 'deployment.') - def upload_corpus(self, target_name, corpus_dir): # pylint: disable=no-self-use,unused-argument + def upload_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument """Noop Impelementation of upload_corpus.""" logging.info('Not uploading corpus because no ClusterFuzz deployment.') - def upload_crashes(self, crashes_dir): # pylint: disable=no-self-use,unused-argument + def upload_crashes(self): # pylint: disable=no-self-use """Noop Impelementation of upload_crashes.""" logging.info('Not uploading crashes because no ClusterFuzz deployment.') - def download_corpus(self, target_name, parent_dir): # pylint: disable=no-self-use,unused-argument + def download_corpus(self, target_name): # pylint: disable=no-self-use,unused-argument """Noop Impelementation of download_corpus.""" logging.info('Not downloading corpus because no ClusterFuzz deployment.') - return self.make_empty_corpus_dir(target_name, parent_dir) + return self.make_empty_corpus_dir(target_name) - def download_latest_build(self, parent_dir): # pylint: disable=no-self-use,unused-argument + def download_latest_build(self): # pylint: disable=no-self-use """Noop Impelementation of download_latest_build.""" logging.info( 'Not downloading latest build because no ClusterFuzz deployment.') -def get_clusterfuzz_deployment(config): +def get_clusterfuzz_deployment(config, workspace): """Returns object reprsenting deployment of ClusterFuzz used by |config|.""" if (config.platform == config.Platform.INTERNAL_GENERIC_CI or config.platform == config.Platform.INTERNAL_GITHUB): logging.info('Using OSS-Fuzz as ClusterFuzz deployment.') - return OSSFuzz(config) + return OSSFuzz(config, workspace) if config.platform == config.Platform.EXTERNAL_GENERIC_CI: logging.info('Not using a ClusterFuzz deployment.') - return NoClusterFuzzDeployment(config) + return NoClusterFuzzDeployment(config, workspace) logging.info('Using ClusterFuzzLite as ClusterFuzz deployment.') - return ClusterFuzzLite(config) + return ClusterFuzzLite(config, workspace) diff --git a/infra/cifuzz/clusterfuzz_deployment_test.py b/infra/cifuzz/clusterfuzz_deployment_test.py index 3c7e27598..c03cd1e51 100644 --- a/infra/cifuzz/clusterfuzz_deployment_test.py +++ b/infra/cifuzz/clusterfuzz_deployment_test.py @@ -22,6 +22,7 @@ from pyfakefs import fake_filesystem_unittest import clusterfuzz_deployment import config_utils +import docker import test_helpers # NOTE: This integration test relies on @@ -31,15 +32,19 @@ EXAMPLE_PROJECT = 'example' # An example fuzzer that triggers an error. EXAMPLE_FUZZER = 'example_crash_fuzzer' -OUT_DIR = '/out' -EXPECTED_LATEST_BUILD_PATH = os.path.join(OUT_DIR, 'cifuzz-latest-build') +WORKSPACE = '/workspace' +EXPECTED_LATEST_BUILD_PATH = os.path.join(WORKSPACE, 'cifuzz-prev-build') def _create_config(**kwargs): """Creates a config object and then sets every attribute that is a key in |kwargs| to the corresponding value. Asserts that each key in |kwargs| is an attribute of Config.""" - defaults = {'is_github': True, 'project_name': EXAMPLE_PROJECT} + defaults = { + 'is_github': True, + 'project_name': EXAMPLE_PROJECT, + 'workspace': WORKSPACE, + } for default_key, default_value in defaults.items(): if default_key not in kwargs: kwargs[default_key] = default_value @@ -49,7 +54,8 @@ def _create_config(**kwargs): def _create_deployment(**kwargs): config = _create_config(**kwargs) - return clusterfuzz_deployment.get_clusterfuzz_deployment(config) + workspace = docker.Workspace(config) + return clusterfuzz_deployment.get_clusterfuzz_deployment(config, workspace) class OSSFuzzTest(fake_filesystem_unittest.TestCase): @@ -61,10 +67,11 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase): @mock.patch('http_utils.download_and_unpack_zip', return_value=True) def test_download_corpus(self, mocked_download_and_unpack_zip): - """Tests that download_corpus works for a valid project.""" - result = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR) + """Tests that we can download a corpus for a valid project.""" + result = self.deployment.download_corpus(EXAMPLE_FUZZER) self.assertIsNotNone(result) - expected_corpus_dir = os.path.join(OUT_DIR, 'cifuzz-corpus', EXAMPLE_FUZZER) + expected_corpus_dir = os.path.join(self.deployment.workspace.corpora, + EXAMPLE_FUZZER) expected_url = ('https://storage.googleapis.com/example-backup.' 'clusterfuzz-external.appspot.com/corpus/libFuzzer/' 'example_crash_fuzzer/public.zip') @@ -75,8 +82,9 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase): def test_download_corpus_fail(self, _): """Tests that when downloading fails, an empty corpus directory is still returned.""" - corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR) - self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer') + corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER) + self.assertEqual(corpus_path, + '/workspace/cifuzz-corpus/example_crash_fuzzer') self.assertEqual(os.listdir(corpus_path), []) def test_get_latest_build_name(self): @@ -86,12 +94,11 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase): self.assertTrue('address' in latest_build_name) @parameterized.parameterized.expand([ - ('upload_latest_build', ('build',), + ('upload_latest_build', tuple(), 'Not uploading latest build because on OSS-Fuzz.'), - ('upload_corpus', ('target', 'corpus_dir'), + ('upload_corpus', ('target',), 'Not uploading corpus because on OSS-Fuzz.'), - ('upload_crashes', ('crashes_dir',), - 'Not uploading crashes because on OSS-Fuzz.'), + ('upload_crashes', tuple(), 'Not uploading crashes because on OSS-Fuzz.'), ]) def test_noop_methods(self, method, method_args, expected_message): """Tests that certain methods are noops for OSS-Fuzz.""" @@ -104,7 +111,7 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase): def test_download_latest_build(self, mocked_download_and_unpack_zip): """Tests that downloading the latest build works as intended under normal circumstances.""" - self.assertEqual(self.deployment.download_latest_build(OUT_DIR), + self.assertEqual(self.deployment.download_latest_build(), EXPECTED_LATEST_BUILD_PATH) expected_url = ('https://storage.googleapis.com/clusterfuzz-builds/example/' 'example-address-202008030600.zip') @@ -115,7 +122,7 @@ class OSSFuzzTest(fake_filesystem_unittest.TestCase): def test_download_latest_build_fail(self, _): """Tests that download_latest_build returns None when it fails to download a build.""" - self.assertIsNone(self.deployment.download_latest_build(OUT_DIR)) + self.assertIsNone(self.deployment.download_latest_build()) class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase): @@ -130,8 +137,9 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase): return_value=True) def test_download_corpus(self, mocked_download_corpus): """Tests that download_corpus works for a valid project.""" - result = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR) - expected_corpus_dir = os.path.join(OUT_DIR, 'cifuzz-corpus', EXAMPLE_FUZZER) + result = self.deployment.download_corpus(EXAMPLE_FUZZER) + expected_corpus_dir = os.path.join(WORKSPACE, 'cifuzz-corpus', + EXAMPLE_FUZZER) self.assertEqual(result, expected_corpus_dir) mocked_download_corpus.assert_called_with('corpus-example_crash_fuzzer', expected_corpus_dir) @@ -141,8 +149,9 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase): def test_download_corpus_fail(self, _): """Tests that when downloading fails, an empty corpus directory is still returned.""" - corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR) - self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer') + corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER) + self.assertEqual(corpus_path, + '/workspace/cifuzz-corpus/example_crash_fuzzer') self.assertEqual(os.listdir(corpus_path), []) @mock.patch( @@ -151,7 +160,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase): def test_download_latest_build(self, mocked_download_latest_build): """Tests that downloading the latest build works as intended under normal circumstances.""" - self.assertEqual(self.deployment.download_latest_build(OUT_DIR), + self.assertEqual(self.deployment.download_latest_build(), EXPECTED_LATEST_BUILD_PATH) expected_artifact_name = 'cifuzz-build-address' mocked_download_latest_build.assert_called_with(expected_artifact_name, @@ -163,7 +172,7 @@ class ClusterFuzzLiteTest(fake_filesystem_unittest.TestCase): def test_download_latest_build_fail(self, _): """Tests that download_latest_build returns None when it fails to download a build.""" - self.assertIsNone(self.deployment.download_latest_build(OUT_DIR)) + self.assertIsNone(self.deployment.download_latest_build()) class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase): @@ -173,26 +182,30 @@ class NoClusterFuzzDeploymentTest(fake_filesystem_unittest.TestCase): self.setUpPyfakefs() config = test_helpers.create_run_config(project_name=EXAMPLE_PROJECT, build_integration_path='/', + workspace=WORKSPACE, is_github=False) - self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment(config) + workspace = docker.Workspace(config) + self.deployment = clusterfuzz_deployment.get_clusterfuzz_deployment( + config, workspace) @mock.patch('logging.info') def test_download_corpus(self, mocked_info): """Tests that download corpus returns the path to the empty corpus directory.""" - corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER, OUT_DIR) - self.assertEqual(corpus_path, '/out/cifuzz-corpus/example_crash_fuzzer') + corpus_path = self.deployment.download_corpus(EXAMPLE_FUZZER) + self.assertEqual(corpus_path, + '/workspace/cifuzz-corpus/example_crash_fuzzer') mocked_info.assert_called_with( 'Not downloading corpus because no ClusterFuzz deployment.') @parameterized.parameterized.expand([ - ('upload_latest_build', ('build',), + ('upload_latest_build', tuple(), 'Not uploading latest build because no ClusterFuzz deployment.'), - ('upload_corpus', ('target', 'corpus_dir'), + ('upload_corpus', ('target',), 'Not uploading corpus because no ClusterFuzz deployment.'), - ('upload_crashes', ('crashes_dir',), + ('upload_crashes', tuple(), 'Not uploading crashes because no ClusterFuzz deployment.'), - ('download_latest_build', ('parent_dir',), + ('download_latest_build', tuple(), 'Not downloading latest build because no ClusterFuzz deployment.') ]) def test_noop_methods(self, method, method_args, expected_message): @@ -222,10 +235,12 @@ class GetClusterFuzzDeploymentTest(unittest.TestCase): return_value=platform, new_callable=mock.PropertyMock): with mock.patch('filestore_utils.get_filestore', return_value=None): - config = test_helpers.create_run_config() + config = _create_config() + workspace = docker.Workspace(config) + self.assertIsInstance( - clusterfuzz_deployment.get_clusterfuzz_deployment(config), - expected_deployment_cls) + clusterfuzz_deployment.get_clusterfuzz_deployment( + config, workspace), expected_deployment_cls) if __name__ == '__main__': diff --git a/infra/cifuzz/docker.py b/infra/cifuzz/docker.py index 233e67dfb..c14ecbcc6 100644 --- a/infra/cifuzz/docker.py +++ b/infra/cifuzz/docker.py @@ -53,36 +53,77 @@ def delete_images(images): utils.execute(['docker', 'builder', 'prune', '-f']) -def get_base_docker_run_args(out_dir, sanitizer='address', language='c++'): +def get_base_docker_run_args(workspace, sanitizer='address', language='c++'): """Returns arguments that should be passed to every invocation of 'docker run'.""" docker_args = _DEFAULT_DOCKER_RUN_ARGS.copy() docker_args += [ - '-e', - f'SANITIZER={sanitizer}', - '-e', - f'FUZZING_LANGUAGE={language}', + '-e', f'SANITIZER={sanitizer}', '-e', f'FUZZING_LANGUAGE={language}', + '-e', 'OUT=' + workspace.out ] docker_container = utils.get_container_name() if docker_container: - docker_args += ['--volumes-from', docker_container, '-e', 'OUT=' + out_dir] + # Don't map specific volumes if in a docker container, it breaks when + # running a sibling container. + docker_args += ['--volumes-from', docker_container] else: - docker_args += get_args_mapping_host_path_to_container(out_dir, '/out') + docker_args += _get_args_mapping_host_path_to_container(workspace.workspace) return docker_args, docker_container -def get_base_docker_run_command(out_dir, sanitizer='address', language='c++'): +def get_base_docker_run_command(workspace, sanitizer='address', language='c++'): """Returns part of the command that should be used everytime 'docker run' is invoked.""" docker_args, docker_container = get_base_docker_run_args( - out_dir, sanitizer, language) + workspace, sanitizer, language) command = _DEFAULT_DOCKER_RUN_COMMAND.copy() + docker_args return command, docker_container -def get_args_mapping_host_path_to_container(host_path, container_path=None): +def _get_args_mapping_host_path_to_container(host_path, container_path=None): """Get arguments to docker run that will map |host_path| a path on the host to a path in the container. If |container_path| is specified, that path is mapped to. If not, then |host_path| is mapped to itself in the container.""" + # WARNING: Do not use this function when running in production (and + # --volumes-from) is used for mapping volumes. It will break production. container_path = host_path if container_path is None else container_path return ['-v', f'{host_path}:{container_path}'] + + +class Workspace: + """Class representing the workspace directory.""" + + def __init__(self, config): + self.workspace = config.workspace + + def initialize_dir(self, directory): # pylint: disable=no-self-use + """Creates directory if it doesn't already exist, otherwise does nothing.""" + os.makedirs(directory, exist_ok=True) + + @property + def out(self): + """The out directory used for storing the fuzzer build built by + build_fuzzers.""" + # Don't use 'out' because it needs to be used by artifacts. + return os.path.join(self.workspace, 'build-out') + + @property + def work(self): + """The directory used as the work directory for the fuzzer build/run.""" + return os.path.join(self.workspace, 'work') + + @property + def artifacts(self): + """The directory used to store artifacts for download by CI-system users.""" + # This is hardcoded by a lot of clients, so we need to use this. + return os.path.join(self.workspace, 'out', 'artifacts') + + @property + def clusterfuzz_build(self): + """The directory where builds from ClusterFuzz are stored.""" + return os.path.join(self.workspace, 'cifuzz-prev-build') + + @property + def corpora(self): + """The directory where corpora from ClusterFuzz are stored.""" + return os.path.join(self.workspace, 'cifuzz-corpus') diff --git a/infra/cifuzz/docker_test.py b/infra/cifuzz/docker_test.py index 74fcd47ab..7146c8c8c 100644 --- a/infra/cifuzz/docker_test.py +++ b/infra/cifuzz/docker_test.py @@ -15,10 +15,13 @@ import unittest from unittest import mock +import config_utils import docker CONTAINER_NAME = 'example-container' -OUT_DIR = '/example-out' +config = config_utils.RunFuzzersConfig() +config.workspace = '/workspace' +WORKSPACE = docker.Workspace(config) SANITIZER = 'example-sanitizer' LANGUAGE = 'example-language' @@ -57,14 +60,26 @@ class GetBaseDockerRunArgsTest(unittest.TestCase): """Tests that get_base_docker_run_args works as intended when inside a container.""" docker_args, docker_container = docker.get_base_docker_run_args( - OUT_DIR, SANITIZER, LANGUAGE) + WORKSPACE, SANITIZER, LANGUAGE) self.assertEqual(docker_container, CONTAINER_NAME) expected_docker_args = [] expected_docker_args = [ - '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', - 'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e', - f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}', - '--volumes-from', CONTAINER_NAME, '-e', f'OUT={OUT_DIR}' + '--cap-add', + 'SYS_PTRACE', + '-e', + 'FUZZING_ENGINE=libfuzzer', + '-e', + 'ARCHITECTURE=x86_64', + '-e', + 'CIFUZZ=True', + '-e', + f'SANITIZER={SANITIZER}', + '-e', + f'FUZZING_LANGUAGE={LANGUAGE}', + '-e', + f'OUT={WORKSPACE.out}', + '--volumes-from', + CONTAINER_NAME, ] self.assertEqual(docker_args, expected_docker_args) @@ -73,13 +88,14 @@ class GetBaseDockerRunArgsTest(unittest.TestCase): """Tests that get_base_docker_run_args works as intended when not inside a container.""" docker_args, docker_container = docker.get_base_docker_run_args( - OUT_DIR, SANITIZER, LANGUAGE) + WORKSPACE, SANITIZER, LANGUAGE) self.assertEqual(docker_container, None) expected_docker_args = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e', - f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}', '-v', - f'{OUT_DIR}:/out' + f'SANITIZER={SANITIZER}', '-e', f'FUZZING_LANGUAGE={LANGUAGE}', '-e', + f'OUT={WORKSPACE.out}', '-v', + f'{WORKSPACE.workspace}:{WORKSPACE.workspace}' ] self.assertEqual(docker_args, expected_docker_args) @@ -92,12 +108,13 @@ class GetBaseDockerRunCommandTest(unittest.TestCase): """Tests that get_base_docker_run_args works as intended when not inside a container.""" docker_args, docker_container = docker.get_base_docker_run_command( - OUT_DIR, SANITIZER, LANGUAGE) + WORKSPACE, SANITIZER, LANGUAGE) self.assertEqual(docker_container, None) expected_docker_command = [ 'docker', 'run', '--rm', '--privileged', '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e', f'SANITIZER={SANITIZER}', '-e', - f'FUZZING_LANGUAGE={LANGUAGE}', '-v', f'{OUT_DIR}:/out' + f'FUZZING_LANGUAGE={LANGUAGE}', '-e', f'OUT={WORKSPACE.out}', '-v', + f'{WORKSPACE.workspace}:{WORKSPACE.workspace}' ] self.assertEqual(docker_args, expected_docker_command) diff --git a/infra/cifuzz/fuzz_target.py b/infra/cifuzz/fuzz_target.py index c8d1c6861..178868feb 100644 --- a/infra/cifuzz/fuzz_target.py +++ b/infra/cifuzz/fuzz_target.py @@ -33,7 +33,7 @@ logging.basicConfig( # Use a fixed seed for determinism. Use len_control=0 since we don't have enough # time fuzzing for len_control to make sense (probably). -LIBFUZZER_OPTIONS = '-seed=1337 -len_control=0' +LIBFUZZER_OPTIONS = ['-seed=1337', '-len_control=0'] # The number of reproduce attempts for a crash. REPRODUCE_ATTEMPTS = 10 @@ -54,25 +54,25 @@ class ReproduceError(Exception): """Error for when we can't attempt to reproduce a crash.""" -class FuzzTarget: +class FuzzTarget: # pylint: disable=too-many-instance-attributes """A class to manage a single fuzz target. Attributes: 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. - out_dir: The location of where output artifacts are stored. + workspace: The workspace for storing things related to fuzzing. """ # pylint: disable=too-many-arguments - def __init__(self, target_path, duration, out_dir, clusterfuzz_deployment, + def __init__(self, target_path, duration, workspace, clusterfuzz_deployment, config): """Represents a single fuzz target. 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. + workspace: The path used for storing things needed for fuzzing. clusterfuzz_deployment: The object representing the ClusterFuzz deployment. config: The config of this project. @@ -80,7 +80,7 @@ class FuzzTarget: self.target_path = target_path self.target_name = os.path.basename(self.target_path) self.duration = int(duration) - self.out_dir = out_dir + self.workspace = workspace self.clusterfuzz_deployment = clusterfuzz_deployment self.config = config self.latest_corpus_path = None @@ -92,17 +92,13 @@ class FuzzTarget: FuzzResult namedtuple with stacktrace and testcase if applicable. """ logging.info('Running fuzzer: %s.', self.target_name) - command, _ = docker.get_base_docker_run_command(self.out_dir, + command, _ = docker.get_base_docker_run_command(self.workspace, self.config.sanitizer, self.config.language) - # TODO(metzman): Stop using /out for artifacts and corpus. Use another - # directory. # If corpus can be downloaded use it for fuzzing. self.latest_corpus_path = self.clusterfuzz_deployment.download_corpus( - self.target_name, self.out_dir) - command += docker.get_args_mapping_host_path_to_container( - self.latest_corpus_path) + self.target_name) command += ['-e', 'CORPUS_DIR=' + self.latest_corpus_path] command += [ @@ -110,9 +106,13 @@ class FuzzTarget: '-c' ] - options = LIBFUZZER_OPTIONS + ' -max_total_time=' + str(self.duration) + options = LIBFUZZER_OPTIONS.copy() + [ + f'-max_total_time={self.duration}', + # Make sure libFuzzer artifact files don't pollute $OUT. + f'-artifact_prefix={self.workspace.artifacts}/' + ] + options = ' '.join(options) run_fuzzer_command = f'run_fuzzer {self.target_name} {options}' - command.append(run_fuzzer_command) logging.info('Running command: %s', ' '.join(command)) @@ -134,7 +134,7 @@ class FuzzTarget: # Crash was discovered. logging.info('Fuzzer %s, ended before timeout.', self.target_name) - testcase = self.get_testcase(stderr) + testcase = get_testcase(stderr) if not testcase: logging.error(b'No testcase found in stacktrace: %s.', stderr) return FuzzResult(None, None, self.latest_corpus_path) @@ -186,14 +186,13 @@ class FuzzTarget: Raises: ReproduceError if we can't attempt to reproduce the crash. """ - if not os.path.exists(target_path): raise ReproduceError(f'Target {target_path} not found.') os.chmod(target_path, stat.S_IRWXO) command, container = docker.get_base_docker_run_command( - self.out_dir, self.config.sanitizer, self.config.language) + self.workspace, self.config.sanitizer, self.config.language) if container: command += ['-e', f'TESTCASE={testcase}'] else: @@ -256,8 +255,7 @@ class FuzzTarget: can't be reproduced on an older ClusterFuzz build of the target.""" if not os.path.exists(testcase): raise ReproduceError('Testcase %s not found.' % testcase) - clusterfuzz_build_dir = self.clusterfuzz_deployment.download_latest_build( - self.out_dir) + clusterfuzz_build_dir = self.clusterfuzz_deployment.download_latest_build() if not clusterfuzz_build_dir: # Crash is reproducible on PR build and we can't test on a recent # ClusterFuzz/OSS-Fuzz build. @@ -284,18 +282,17 @@ class FuzzTarget: 'Code change (pr/commit) introduced crash.') return True - def get_testcase(self, error_bytes): - """Gets the file from a fuzzer run stacktrace. - Args: - error_bytes: The bytes containing the output from the fuzzer. +def get_testcase(stderr_bytes): + """Gets the file from a fuzzer run stacktrace. - Returns: - The path to the testcase or None if not found. - """ - # TODO(metzman): Stop parsing and use libFuzzers' artifact_prefix option - # instead. - match = re.search(rb'\bTest unit written to \.\/([^\s]+)', error_bytes) - if match: - return os.path.join(self.out_dir, match.group(1).decode('utf-8')) - return None + Args: + stderr_bytes: The bytes containing the output from the fuzzer. + + Returns: + The path to the testcase or None if not found. + """ + match = re.search(rb'\bTest unit written to (.+)', stderr_bytes) + if match: + return match.group(1).decode('utf-8') + return None diff --git a/infra/cifuzz/fuzz_target_test.py b/infra/cifuzz/fuzz_target_test.py index b8a9d142a..c760f9fc1 100644 --- a/infra/cifuzz/fuzz_target_test.py +++ b/infra/cifuzz/fuzz_target_test.py @@ -23,6 +23,7 @@ import parameterized from pyfakefs import fake_filesystem_unittest import clusterfuzz_deployment +import docker import fuzz_target import test_helpers @@ -44,7 +45,11 @@ def _create_config(**kwargs): """Creates a config object and then sets every attribute that is a key in |kwargs| to the corresponding value. Asserts that each key in |kwargs| is an attribute of Config.""" - defaults = {'is_github': True, 'project_name': EXAMPLE_PROJECT} + defaults = { + 'is_github': True, + 'project_name': EXAMPLE_PROJECT, + 'workspace': '/workspace' + } for default_key, default_value in defaults.items(): if default_key not in kwargs: kwargs[default_key] = default_value @@ -54,7 +59,8 @@ def _create_config(**kwargs): def _create_deployment(**kwargs): config = _create_config(**kwargs) - return clusterfuzz_deployment.get_clusterfuzz_deployment(config) + workspace = docker.Workspace(config) + return clusterfuzz_deployment.get_clusterfuzz_deployment(config, workspace) # TODO(metzman): Use patch from test_libs/helpers.py in clusterfuzz so that we @@ -66,13 +72,14 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase): def setUp(self): """Sets up example fuzz target to test is_reproducible method.""" self.fuzz_target_name = 'fuzz-target' - self.out_dir = '/example/outdir' - self.fuzz_target_path = os.path.join(self.out_dir, self.fuzz_target_name) - self.testcase_path = '/testcase' deployment = _create_deployment() + self.workspace = deployment.workspace + self.fuzz_target_path = os.path.join(self.workspace.out, + self.fuzz_target_name) + self.testcase_path = '/testcase' self.test_target = fuzz_target.FuzzTarget(self.fuzz_target_path, fuzz_target.REPRODUCE_ATTEMPTS, - self.out_dir, deployment, + self.workspace, deployment, deployment.config) def test_reproducible(self, _): @@ -87,8 +94,8 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase): 'docker', 'run', '--rm', '--privileged', '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e', 'SANITIZER=' + self.test_target.config.sanitizer, - '-e', 'FUZZING_LANGUAGE=' + self.test_target.config.language, - '--volumes-from', 'container', '-e', 'OUT=' + self.out_dir, '-e', + '-e', 'FUZZING_LANGUAGE=' + self.test_target.config.language, '-e', + 'OUT=' + self.workspace.out, '--volumes-from', 'container', '-e', 'TESTCASE=' + self.testcase_path, '-t', 'gcr.io/oss-fuzz-base/base-runner', 'reproduce', self.fuzz_target_name, '-runs=100' @@ -136,32 +143,24 @@ class IsReproducibleTest(fake_filesystem_unittest.TestCase): class GetTestCaseTest(unittest.TestCase): """Tests get_testcase.""" - def setUp(self): - """Sets up example fuzz target to test get_testcase method.""" - deployment = _create_deployment() - self.test_target = fuzz_target.FuzzTarget('/example/path', 10, - '/example/outdir', deployment, - deployment.config) - def test_valid_error_string(self): """Tests that get_testcase returns the correct testcase give an error.""" testcase_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_data', 'example_crash_fuzzer_output.txt') with open(testcase_path, 'rb') as test_fuzz_output: - parsed_testcase = self.test_target.get_testcase(test_fuzz_output.read()) - self.assertEqual( - parsed_testcase, - '/example/outdir/crash-ad6700613693ef977ff3a8c8f4dae239c3dde6f5') + parsed_testcase = fuzz_target.get_testcase(test_fuzz_output.read()) + self.assertEqual(parsed_testcase, + './crash-ad6700613693ef977ff3a8c8f4dae239c3dde6f5') def test_invalid_error_string(self): """Tests that get_testcase returns None with a bad error string.""" - self.assertIsNone(self.test_target.get_testcase(b'')) - self.assertIsNone(self.test_target.get_testcase(b' Example crash string.')) + self.assertIsNone(fuzz_target.get_testcase(b'')) + self.assertIsNone(fuzz_target.get_testcase(b' Example crash string.')) def test_encoding(self): """Tests that get_testcase accepts bytes and returns a string.""" fuzzer_output = b'\x8fTest unit written to ./crash-1' - result = self.test_target.get_testcase(fuzzer_output) + result = fuzz_target.get_testcase(fuzzer_output) self.assertTrue(isinstance(result, str)) diff --git a/infra/cifuzz/generate_coverage_report.py b/infra/cifuzz/generate_coverage_report.py index 1916131bd..9a5b8e7a8 100644 --- a/infra/cifuzz/generate_coverage_report.py +++ b/infra/cifuzz/generate_coverage_report.py @@ -18,9 +18,9 @@ import helper import docker -def run_coverage_command(out_dir, config): +def run_coverage_command(workspace, config): """Runs the coverage command in base-runner to generate a coverage report.""" - docker_args, _ = docker.get_base_docker_run_args(out_dir, config.sanitizer, + docker_args, _ = docker.get_base_docker_run_args(workspace, config.sanitizer, config.language) docker_args += [ '-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t', @@ -29,18 +29,18 @@ def run_coverage_command(out_dir, config): return helper.docker_run(docker_args) -def download_corpora(out_dir, fuzz_target_paths, clusterfuzz_deployment): - """Downloads corpora to |out_dir| for the fuzz targets in |fuzz_target_paths| - using clusterfuzz_deployment| to download corpora from ClusterFuzz/OSS-Fuzz""" +def download_corpora(fuzz_target_paths, clusterfuzz_deployment): + """Downloads corpora for fuzz targets in |fuzz_target_paths| using + clusterfuzz_deployment| to download corpora from ClusterFuzz/OSS-Fuzz.""" # TODO(metzman): Download to /corpus dir. for target_path in fuzz_target_paths: target = os.path.basename(target_path) - clusterfuzz_deployment.download_corpus(target, out_dir) + clusterfuzz_deployment.download_corpus(target) -def generate_coverage_report(fuzz_target_paths, out_dir, clusterfuzz_deployment, - config): +def generate_coverage_report(fuzz_target_paths, workspace, + clusterfuzz_deployment, config): """Generates a coverage report using Clang's source based coverage.""" - download_corpora(out_dir, fuzz_target_paths, clusterfuzz_deployment) - run_coverage_command(out_dir, config) + download_corpora(fuzz_target_paths, clusterfuzz_deployment) + run_coverage_command(workspace, config) # TODO(metzman): Upload this build to the filestore. diff --git a/infra/cifuzz/generate_coverage_report_test.py b/infra/cifuzz/generate_coverage_report_test.py index 461cd7ca3..250d95946 100644 --- a/infra/cifuzz/generate_coverage_report_test.py +++ b/infra/cifuzz/generate_coverage_report_test.py @@ -30,17 +30,20 @@ class TestRunCoverageCommand(unittest.TestCase): @mock.patch('helper.docker_run') def test_run_coverage_command(self, mocked_docker_run): # pylint: disable=no-self-use """Tests that run_coverage_command works as intended.""" + config = test_helpers.create_run_config(project_name=PROJECT, + sanitizer=SANITIZER) + workspace = test_helpers.create_workspace() expected_docker_args = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'ARCHITECTURE=x86_64', '-e', 'CIFUZZ=True', '-e', - f'SANITIZER={SANITIZER}', '-e', 'FUZZING_LANGUAGE=c++', '-v', - f'{OUT_DIR}:/out', '-e', 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', - '-t', 'gcr.io/oss-fuzz-base/base-runner', 'coverage' + f'SANITIZER={SANITIZER}', '-e', 'FUZZING_LANGUAGE=c++', '-e', + 'OUT=/workspace/build-out', '-v', + f'{workspace.workspace}:{workspace.workspace}', '-e', + 'COVERAGE_EXTRA_ARGS=', '-e', 'HTTP_PORT=', '-t', + 'gcr.io/oss-fuzz-base/base-runner', 'coverage' ] - config = test_helpers.create_run_config(project_name=PROJECT, - sanitizer=SANITIZER) - generate_coverage_report.run_coverage_command(OUT_DIR, config) + generate_coverage_report.run_coverage_command(workspace, config) mocked_docker_run.assert_called_with(expected_docker_args) @@ -51,10 +54,7 @@ class DownloadCorporaTest(unittest.TestCase): """Tests that download_corpora works as intended.""" clusterfuzz_deployment = mock.Mock() fuzz_target_paths = ['/path/to/fuzzer1', '/path/to/fuzzer2'] - expected_calls = [ - mock.call('fuzzer1', OUT_DIR), - mock.call('fuzzer2', OUT_DIR) - ] - generate_coverage_report.download_corpora(OUT_DIR, fuzz_target_paths, + expected_calls = [mock.call('fuzzer1'), mock.call('fuzzer2')] + generate_coverage_report.download_corpora(fuzz_target_paths, clusterfuzz_deployment) clusterfuzz_deployment.download_corpus.assert_has_calls(expected_calls) diff --git a/infra/cifuzz/run_fuzzers.py b/infra/cifuzz/run_fuzzers.py index 6b787fcc7..a50e43a4c 100644 --- a/infra/cifuzz/run_fuzzers.py +++ b/infra/cifuzz/run_fuzzers.py @@ -20,6 +20,7 @@ import sys import time import clusterfuzz_deployment +import docker import fuzz_target import generate_coverage_report import stack_parser @@ -42,16 +43,17 @@ class BaseFuzzTargetRunner: def __init__(self, config): self.config = config + self.workspace = docker.Workspace(config) self.clusterfuzz_deployment = ( - clusterfuzz_deployment.get_clusterfuzz_deployment(self.config)) + clusterfuzz_deployment.get_clusterfuzz_deployment( + self.config, self.workspace)) + # Set by the initialize method. - self.out_dir = None self.fuzz_target_paths = None - self.crashes_dir = None def get_fuzz_targets(self): - """Returns fuzz targets in out_dir.""" - return utils.get_fuzz_targets(self.out_dir) + """Returns fuzz targets in out directory.""" + return utils.get_fuzz_targets(self.workspace.out) def initialize(self): """Initialization method. Must be called before calling run_fuzz_targets. @@ -69,24 +71,23 @@ class BaseFuzzTargetRunner: self.config.fuzz_seconds) return False - self.out_dir = os.path.join(self.config.workspace, 'out') - if not os.path.exists(self.out_dir): - logging.error('Out directory: %s does not exist.', self.out_dir) + if not os.path.exists(self.workspace.out): + logging.error('Out directory: %s does not exist.', self.workspace.out) return False - self.crashes_dir = os.path.join(self.out_dir, 'artifacts') - if not os.path.exists(self.crashes_dir): - os.mkdir(self.crashes_dir) - elif (not os.path.isdir(self.crashes_dir) or os.listdir(self.crashes_dir)): + if not os.path.exists(self.workspace.artifacts): + os.makedirs(self.workspace.artifacts) + elif (not os.path.isdir(self.workspace.artifacts) or + os.listdir(self.workspace.artifacts)): logging.error('Artifacts path: %s exists and is not an empty directory.', - self.crashes_dir) + self.workspace.artifacts) return False self.fuzz_target_paths = self.get_fuzz_targets() logging.info('Fuzz targets: %s', self.fuzz_target_paths) if not self.fuzz_target_paths: logging.error('No fuzz targets were found in out directory: %s.', - self.out_dir) + self.workspace.out) return False return True @@ -110,11 +111,11 @@ class BaseFuzzTargetRunner: |fuzz_target|.""" artifact_name = (f'{target.target_name}-{self.config.sanitizer}-' f'{artifact_name}') - return os.path.join(self.crashes_dir, artifact_name) + return os.path.join(self.workspace.artifacts, artifact_name) def create_fuzz_target_obj(self, target_path, run_seconds): """Returns a fuzz target object.""" - return fuzz_target.FuzzTarget(target_path, run_seconds, self.out_dir, + return fuzz_target.FuzzTarget(target_path, run_seconds, self.workspace, self.clusterfuzz_deployment, self.config) def run_fuzz_targets(self): @@ -173,19 +174,19 @@ class CoverageTargetRunner(BaseFuzzTargetRunner): raise NotImplementedError('Not implemented for CoverageTargetRunner.') def get_fuzz_targets(self): - """Returns fuzz targets in out_dir.""" + """Returns fuzz targets in out directory.""" # We only want fuzz targets from the root because during the coverage build, # a lot of the image's filesystem is copied into /out for the purpose of # generating coverage reports. # TOOD(metzman): Figure out if top_level_only should be the only behavior # for this function. - return utils.get_fuzz_targets(self.out_dir, top_level_only=True) + return utils.get_fuzz_targets(self.workspace.out, top_level_only=True) def run_fuzz_targets(self): """Generates a coverage report. Always returns False since it never finds any bugs.""" generate_coverage_report.generate_coverage_report( - self.fuzz_target_paths, self.out_dir, self.clusterfuzz_deployment, + self.fuzz_target_paths, self.workspace, self.clusterfuzz_deployment, self.config) return False @@ -238,38 +239,20 @@ class BatchFuzzTargetRunner(BaseFuzzTargetRunner): def run_fuzz_targets(self): result = super().run_fuzz_targets() - self.clusterfuzz_deployment.upload_crashes(self.crashes_dir) + self.clusterfuzz_deployment.upload_crashes() # We want to upload the build to the filestore after we do batch fuzzing. - # There are some problems with this. First, we don't want to upload the - # build before fuzzing, because if we download the latest build, we will - # consider the build we just uploaded to be the latest even though it - # shouldn't be (we really intend to download the build before the curent - # one. - # Second, the out directory is mounted into the runnner container and is - # used to pass the runner corpora, old builds and for the runner to pass the - # host testcases. Thus, we will upload the build after fuzzing. But before - # we zip the out directory we will remove these extra things that are now in - # out. + # There are some is a problem with this. We don't want to upload the build + # before fuzzing, because if we download the latest build, we will consider + # the build we just uploaded to be the latest even though it shouldn't be + # (we really intend to download the build before the curent one. # TODO(metzman): We should really be uploading latest build in build_fuzzers # before we remove unaffected fuzzers. Otherwise, we can lose fuzzers. This # is probably more of a theoretical concern since in batch fuzzing, there is # no code change and thus no fuzzers that are removed, but it's inelegant to # put this here. - # TODO(metzman): Don't pollute self.out_dir like this. - - for directory in [ - self.clusterfuzz_deployment.get_corpus_dir(self.out_dir), - # This is the directory of the ClusterFuzz build, not the build we just - # did. - self.clusterfuzz_deployment.get_build_dir(self.out_dir), - self.crashes_dir, - ]: - if os.path.exists(directory): - shutil.rmtree(directory) - - self.clusterfuzz_deployment.upload_latest_build(self.out_dir) + self.clusterfuzz_deployment.upload_latest_build() return result diff --git a/infra/cifuzz/run_fuzzers_test.py b/infra/cifuzz/run_fuzzers_test.py index 83f18d517..9e6ddf8c5 100644 --- a/infra/cifuzz/run_fuzzers_test.py +++ b/infra/cifuzz/run_fuzzers_test.py @@ -121,7 +121,7 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): expected_error_args = ('Fuzz_seconds argument must be greater than 1, ' 'but was: %s.', fuzz_seconds) with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') + out_path = os.path.join(tmp_dir, 'build-out') os.mkdir(out_path) with mock.patch('utils.get_fuzz_targets') as mocked_get_fuzz_targets: mocked_get_fuzz_targets.return_value = [ @@ -134,16 +134,17 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): def test_initialize_no_out_dir(self): """Tests initialize fails with no out dir.""" with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') + out_path = os.path.join(tmp_dir, 'build-out') expected_error_args = ('Out directory: %s does not exist.', out_path) self._test_initialize_fail(expected_error_args, workspace=tmp_dir) def test_initialize_nonempty_artifacts(self): """Tests initialize with a file artifacts path.""" with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') + out_path = os.path.join(tmp_dir, 'build-out') os.mkdir(out_path) - artifacts_path = os.path.join(out_path, 'artifacts') + os.makedirs(os.path.join(tmp_dir, 'out')) + artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts') with open(artifacts_path, 'w') as artifacts_handle: artifacts_handle.write('fake') expected_error_args = ( @@ -154,8 +155,9 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): def test_initialize_bad_artifacts(self): """Tests initialize with a non-empty artifacts path.""" with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') - artifacts_path = os.path.join(out_path, 'artifacts') + out_path = os.path.join(tmp_dir, 'build-out') + os.mkdir(out_path) + artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts') os.makedirs(artifacts_path) artifact_path = os.path.join(artifacts_path, 'artifact') with open(artifact_path, 'w') as artifact_handle: @@ -172,8 +174,9 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): """Tests initialize with an empty artifacts dir.""" mocked_get_fuzz_targets.return_value = ['fuzz-target'] with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') - artifacts_path = os.path.join(out_path, 'artifacts') + out_path = os.path.join(tmp_dir, 'build-out') + os.mkdir(out_path) + artifacts_path = os.path.join(tmp_dir, 'out', 'artifacts') os.makedirs(artifacts_path) runner = self._create_runner(workspace=tmp_dir) self.assertTrue(runner.initialize()) @@ -187,17 +190,17 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): """Tests initialize with no artifacts dir (the expected setting).""" mocked_get_fuzz_targets.return_value = ['fuzz-target'] with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') - os.makedirs(out_path) + out_path = os.path.join(tmp_dir, 'build-out') + os.mkdir(out_path) runner = self._create_runner(workspace=tmp_dir) self.assertTrue(runner.initialize()) mocked_log_error.assert_not_called() - self.assertTrue(os.path.isdir(os.path.join(out_path, 'artifacts'))) + self.assertTrue(os.path.isdir(os.path.join(tmp_dir, 'out', 'artifacts'))) def test_initialize_no_fuzz_targets(self): """Tests initialize with no fuzz targets.""" with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') + out_path = os.path.join(tmp_dir, 'build-out') os.makedirs(out_path) expected_error_args = ('No fuzz targets were found in out directory: %s.', out_path) @@ -205,18 +208,21 @@ class BaseFuzzTargetRunnerTest(unittest.TestCase): def test_get_fuzz_target_artifact(self): """Tests that get_fuzz_target_artifact works as intended.""" - runner = self._create_runner() - crashes_dir = 'crashes-dir' - runner.crashes_dir = crashes_dir - artifact_name = 'artifact-name' - target = mock.MagicMock() - target_name = 'target_name' - target.target_name = target_name - fuzz_target_artifact = runner.get_fuzz_target_artifact( - target, artifact_name) - expected_fuzz_target_artifact = ( - 'crashes-dir/target_name-address-artifact-name') - self.assertEqual(fuzz_target_artifact, expected_fuzz_target_artifact) + with tempfile.TemporaryDirectory() as tmp_dir: + runner = self._create_runner(workspace=tmp_dir) + crashes_dir = 'crashes-dir' + runner.crashes_dir = crashes_dir + artifact_name = 'artifact-name' + target = mock.MagicMock() + target_name = 'target_name' + target.target_name = target_name + + fuzz_target_artifact = runner.get_fuzz_target_artifact( + target, artifact_name) + expected_fuzz_target_artifact = os.path.join( + tmp_dir, 'out', 'artifacts', 'target_name-address-artifact-name') + + self.assertEqual(fuzz_target_artifact, expected_fuzz_target_artifact) class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): @@ -233,7 +239,7 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): mocked_get_fuzz_targets): """Tests that run_fuzz_targets quits on the first crash it finds.""" workspace = 'workspace' - out_path = os.path.join(workspace, 'out') + out_path = os.path.join(workspace, 'build-out') self.fs.create_dir(out_path) config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS, workspace=workspace, @@ -253,7 +259,8 @@ class CiFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): magic_mock.target_name = 'target1' mocked_create_fuzz_target_obj.return_value = magic_mock self.assertTrue(runner.run_fuzz_targets()) - self.assertIn('target1-address-testcase', os.listdir(runner.crashes_dir)) + self.assertIn('target1-address-testcase', + os.listdir(runner.workspace.artifacts)) self.assertEqual(mocked_run_fuzz_target.call_count, 1) @@ -265,11 +272,11 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): def setUp(self): self.setUpPyfakefs() - self.out_dir = os.path.join(self.WORKSPACE, 'out') - self.fs.create_dir(self.out_dir) - self.testcase1 = os.path.join(self.WORKSPACE, 'testcase-aaa') + out_dir = os.path.join(self.WORKSPACE, 'build-out') + self.fs.create_dir(out_dir) + self.testcase1 = os.path.join(out_dir, 'testcase-aaa') self.fs.create_file(self.testcase1) - self.testcase2 = os.path.join(self.WORKSPACE, 'testcase-bbb') + self.testcase2 = os.path.join(out_dir, 'testcase-bbb') self.fs.create_file(self.testcase2) self.config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS, workspace=self.WORKSPACE, @@ -317,32 +324,9 @@ class BatchFuzzTargetRunnerTest(fake_filesystem_unittest.TestCase): self, mocked_upload_crashes, mocked_upload_latest_build, _): """Tests that run_fuzz_targets uploads crashes and builds correctly.""" runner = run_fuzzers.BatchFuzzTargetRunner(self.config) + # TODO(metzman): Don't rely on this failing gracefully. runner.initialize() - expected_crashes_dir = 'workspace/out/artifacts' - - def mock_upload_crashes(crashes_dir): - self.assertEqual(crashes_dir, expected_crashes_dir) - # Ensure it wasn't deleted first. - self.assertTrue(os.path.exists(crashes_dir)) - - mocked_upload_crashes.side_effect = mock_upload_crashes - - expected_out_dir = 'workspace/out' - expected_build_dir = 'workspace/out/cifuzz-latest-build' - expected_corpus_dir = 'workspace/out/cifuzz-corpus' - self.fs.create_dir(expected_build_dir) - self.fs.create_dir(expected_corpus_dir) - - def mock_upload_latest_build(out_dir): - self.assertEqual(out_dir, expected_out_dir) - # Ensure these were deleted before this function call. - self.assertFalse(os.path.exists(expected_crashes_dir)) - self.assertFalse(os.path.exists(expected_build_dir)) - self.assertFalse(os.path.exists(expected_corpus_dir)) - - mocked_upload_latest_build.side_effect = mock_upload_latest_build - self.assertFalse(runner.run_fuzz_targets()) self.assertEqual(mocked_upload_crashes.call_count, 1) self.assertEqual(mocked_upload_latest_build.call_count, 1) @@ -362,7 +346,6 @@ class CoverageReportIntegrationTest(unittest.TestCase): generation.""" with tempfile.TemporaryDirectory() as workspace: - out_dir = os.path.join(workspace, 'out') try: # Do coverage build. build_config = test_helpers.create_build_config( @@ -388,8 +371,8 @@ class CoverageReportIntegrationTest(unittest.TestCase): TEST_DATA_PATH, 'example_coverage_report_summary.json') with open(expected_summary_path) as file_handle: expected_summary = json.loads(file_handle.read()) - actual_summary_path = os.path.join(out_dir, 'report', 'linux', - 'summary.json') + actual_summary_path = os.path.join(workspace, 'build-out', 'report', + 'linux', 'summary.json') with open(actual_summary_path) as file_handle: actual_summary = json.loads(file_handle.read()) self.assertEqual(expected_summary, actual_summary) @@ -427,8 +410,6 @@ class RunAddressFuzzersIntegrationTest(RunFuzzerIntegrationTestMixin, project_name=EXAMPLE_PROJECT) result = run_fuzzers.run_fuzzers(config) self.assertEqual(result, run_fuzzers.RunFuzzersResult.BUG_FOUND) - build_dir = os.path.join(workspace, 'out', self.BUILD_DIR_NAME) - self.assertNotEqual(0, len(os.listdir(build_dir))) @mock.patch('fuzz_target.FuzzTarget.is_reproducible', side_effect=[True, True]) @@ -438,18 +419,15 @@ class RunAddressFuzzersIntegrationTest(RunFuzzerIntegrationTestMixin, workspace = os.path.join(tmp_dir, 'workspace') shutil.copytree(TEST_DATA_PATH, workspace) config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS, - workspace=TEST_DATA_PATH, + workspace=workspace, project_name=EXAMPLE_PROJECT) result = run_fuzzers.run_fuzzers(config) self.assertEqual(result, run_fuzzers.RunFuzzersResult.NO_BUG_FOUND) - build_dir = os.path.join(TEST_DATA_PATH, 'out', self.BUILD_DIR_NAME) - self.assertTrue(os.path.exists(build_dir)) - self.assertNotEqual(0, len(os.listdir(build_dir))) def test_invalid_build(self): """Tests run_fuzzers with an invalid ASAN build.""" with tempfile.TemporaryDirectory() as tmp_dir: - out_path = os.path.join(tmp_dir, 'out') + out_path = os.path.join(tmp_dir, 'build-out') os.mkdir(out_path) config = test_helpers.create_run_config(fuzz_seconds=FUZZ_SECONDS, workspace=tmp_dir, diff --git a/infra/cifuzz/test_data/out/example_crash_fuzzer b/infra/cifuzz/test_data/build-out/example_crash_fuzzer similarity index 100% rename from infra/cifuzz/test_data/out/example_crash_fuzzer rename to infra/cifuzz/test_data/build-out/example_crash_fuzzer diff --git a/infra/cifuzz/test_data/out/example_nocrash_fuzzer b/infra/cifuzz/test_data/build-out/example_nocrash_fuzzer similarity index 100% rename from infra/cifuzz/test_data/out/example_nocrash_fuzzer rename to infra/cifuzz/test_data/build-out/example_nocrash_fuzzer diff --git a/infra/cifuzz/test_data/memory/out/curl_fuzzer_memory b/infra/cifuzz/test_data/memory/build-out/curl_fuzzer_memory similarity index 100% rename from infra/cifuzz/test_data/memory/out/curl_fuzzer_memory rename to infra/cifuzz/test_data/memory/build-out/curl_fuzzer_memory diff --git a/infra/cifuzz/test_data/undefined/out/curl_fuzzer_undefined b/infra/cifuzz/test_data/undefined/build-out/curl_fuzzer_undefined similarity index 100% rename from infra/cifuzz/test_data/undefined/out/curl_fuzzer_undefined rename to infra/cifuzz/test_data/undefined/build-out/curl_fuzzer_undefined diff --git a/infra/cifuzz/test_helpers.py b/infra/cifuzz/test_helpers.py index 3d20bf468..c5eea2de9 100644 --- a/infra/cifuzz/test_helpers.py +++ b/infra/cifuzz/test_helpers.py @@ -20,6 +20,7 @@ import tempfile from unittest import mock import config_utils +import docker def _create_config(config_cls, **kwargs): @@ -48,6 +49,13 @@ def create_run_config(**kwargs): return _create_config(config_utils.RunFuzzersConfig, **kwargs) +def create_workspace(workspace_path='/workspace'): + """Returns a workspace located at |workspace_path| ('/workspace' by + default).""" + config = create_run_config(workspace=workspace_path) + return docker.Workspace(config) + + def patch_environ(testcase_obj, env=None): """Patch environment.""" if env is None: diff --git a/infra/utils_test.py b/infra/utils_test.py index aa6ec7ba7..3ce405523 100644 --- a/infra/utils_test.py +++ b/infra/utils_test.py @@ -24,7 +24,7 @@ import helper EXAMPLE_PROJECT = 'example' TEST_OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), - 'cifuzz', 'test_data', 'out') + 'cifuzz', 'test_data', 'build-out') class IsFuzzTargetLocalTest(unittest.TestCase):