[FuzzTests] Adding Script to run FuzzTests and Generate Coverage Reports (#38282)
* stop build_examples from automatically generating coverage report after building Fuzz tests with coverage
* Add support for coverage instrumentation to FuzzTests
* Adding Script to run fuzztests and generate Coverage Reports
* Adding Documentation
* restyled
* Spelling fix
* linter fixes
* Integrating two comments
* Making it non interactive by default
* adding two different modes for running the script
* Restyled by isort
* Fixing exceptions in script
* integrating comments
* show the stderr of fuzztest binary incase of error
* add function to detect missing coverage packages
---------
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/build/chip/fuzz_test.gni b/build/chip/fuzz_test.gni
index b48c318..c57feea 100644
--- a/build/chip/fuzz_test.gni
+++ b/build/chip/fuzz_test.gni
@@ -117,6 +117,13 @@
# "MAKE_BUILD_TYPE=RelWithDebug",
# ]
+ if (!defined(configs)) {
+ configs = []
+ }
+ if (use_coverage) {
+ configs += [ "${build_root}/config/compiler:coverage" ]
+ }
+
sources = invoker.test_source
output_dir = _test_output_dir
diff --git a/docs/guides/BUILDING.md b/docs/guides/BUILDING.md
index 57987a2..2466a6b 100644
--- a/docs/guides/BUILDING.md
+++ b/docs/guides/BUILDING.md
@@ -393,8 +393,17 @@
./scripts/build/build_examples.py --target linux-x64-tests-clang-pw-fuzztest build
```
-NOTE: `asan` is enabled by default in FuzzTest, so please do not add it in
-build_examples.py invocation.
+> [!NOTE]
+> `asan` is enabled by default in FuzzTest, so please do not add it in
+> `build_examples.py` invocation.
+
+> [!TIP]
+>
+> - It is possible to build `FuzzTests` with Coverage instrumentation, by
+> appending `-coverage` to the target, e.g.
+> `linux-x64-tests-clang-pw-fuzztest-coverage`
+> - Details:
+> [Coverage Report Generation](https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/fuzz_testing.md#coverage-report-generation)
Tests will be located in:
`out/linux-x64-tests-clang-pw-fuzztest/chip_pw_fuzztest/tests/` where
@@ -403,9 +412,9 @@
- Details on How To Run Fuzz Tests in
[Running FuzzTests](https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/fuzz_testing.md#running-fuzztests)
-FAQ: In the event of a build failure related to missing files or dependencies
-for pw_fuzzer, check the
-[FuzzTest FAQ](https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/fuzz_testing.md#FAQ)
+- FAQ: In the event of a build failure related to missing files or
+ dependencies for pw_fuzzer, check the
+ [FuzzTest FAQ](https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/fuzz_testing.md#FAQ)
## Build custom configuration
diff --git a/docs/testing/fuzz_testing.md b/docs/testing/fuzz_testing.md
index 7b5fd56..5ea93e5 100644
--- a/docs/testing/fuzz_testing.md
+++ b/docs/testing/fuzz_testing.md
@@ -256,6 +256,55 @@
```
+### Coverage Report Generation
+
+> [!TIP]
+>
+> Use Coverage Reports to get more insights while writing FuzzTests.
+
+1. Build FuzzTests with coverage instrumentation
+ [Building pw_fuzzer FuzzTests](https://github.com/project-chip/connectedhomeip/blob/master/docs/guides/BUILDING.md#pw_fuzzer-fuzztests).
+
+2. Run These FuzzTests using `scripts/tests/run_fuzztest_coverage.py`
+
+ - run them in `Continuous Fuzzing Mode` for as long as possible to get max
+ coverage
+
+3. The path for the HTML Coverage Report will be output after generation
+
+### Coverage Reports and Fuzz Blockers
+
+- Coverage Reports can give (FuzzTest Developers) insights and help identify
+ `Fuzz Blockers`.
+- **Fuzz Blocker**: something that prevents a fuzz test from exploring a
+ certain part of the code.
+
+#### Example of Fuzz Blocker Analysis:
+
+- Screenshot below shows how we can use a Coverage Report to identify a Fuzz
+ Blocker
+- We can see the number of executions of each line in the report.
+- Line (#2159) was not reached, in at least 129,452 executions.
+- The line (#2156) just above it is possibly a Fuzz Blocker.
+
+ - The `data.fabricId` check is always failing and it is blocking the
+ execution of the function that follows it.
+
+
+
+- Thus, we can adapt our FuzzTest in a way to be able to pass that check.
+- **Solution**: One approach will be to:
+
+ 1. Seed the Fuzzed `NOC` with a valid NOC Certificate
+ 2. Fuzz the `FabricId`
+ 3. Seed the `FabricId` using the same **valid** `FabricId` included in the
+ valid NOC Cert.
+
+- After doing this, Screenshot below shows Line #2159 is now reached; We have
+ increased our coverage and we are sure that our FuzzTest is more effective:
+
+
+
### FAQ
#### What revision should the FuzzTest and Abseil submodules be for running `pw_fuzzer` with FuzzTest?
diff --git a/docs/testing/img/fuzzblocker_after.png b/docs/testing/img/fuzzblocker_after.png
new file mode 100644
index 0000000..e17c9ed
--- /dev/null
+++ b/docs/testing/img/fuzzblocker_after.png
Binary files differ
diff --git a/docs/testing/img/fuzzblocker_before.png b/docs/testing/img/fuzzblocker_before.png
new file mode 100644
index 0000000..e0c66b0
--- /dev/null
+++ b/docs/testing/img/fuzzblocker_before.png
Binary files differ
diff --git a/scripts/build/builders/host.py b/scripts/build/builders/host.py
index 7b6b4ca..99e9257 100644
--- a/scripts/build/builders/host.py
+++ b/scripts/build/builders/host.py
@@ -365,6 +365,7 @@
self.board = board
self.extra_gn_options = []
self.build_env = {}
+ self.fuzzing_type = fuzzing_type
if enable_rpcs:
self.extra_gn_options.append('import("//with_pw_rpc.gni")')
@@ -560,7 +561,7 @@
if self.board == HostBoard.ARM64:
self.build_env['PKG_CONFIG_PATH'] = os.path.join(
self.SysRootPath('SYSROOT_AARCH64'), 'lib/aarch64-linux-gnu/pkgconfig')
- if self.app == HostApp.TESTS and self.use_coverage and self.use_clang:
+ if self.app == HostApp.TESTS and self.use_coverage and self.use_clang and self.fuzzing_type == HostFuzzingType.NONE:
# Every test is expected to have a distinct build ID, so `%m` will be
# distinct.
#
@@ -642,7 +643,7 @@
os.path.join(self.coverage_dir, 'html')], title="HTML coverage")
# coverage for clang works by having perfdata for every test run, which are in "*.profraw" files
- if self.app == HostApp.TESTS and self.use_coverage and self.use_clang:
+ if self.app == HostApp.TESTS and self.use_coverage and self.use_clang and self.fuzzing_type == HostFuzzingType.NONE:
# Clang coverage config generates "coverage/{name}.profraw" for each test indivdually
# Here we are merging ALL raw profiles into a single indexed file
diff --git a/scripts/tests/run_fuzztest_coverage.py b/scripts/tests/run_fuzztest_coverage.py
new file mode 100755
index 0000000..5570983
--- /dev/null
+++ b/scripts/tests/run_fuzztest_coverage.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env -S python3 -B
+
+# Copyright (c) 2025 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import enum
+import glob
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+from dataclasses import dataclass
+from typing import Optional
+
+import click
+import coloredlogs # type: ignore
+
+profile_output_folder = "out/profiling_fuzztest"
+coverage_report_output_folder = "out/coverage_fuzztest"
+
+
+class FuzzTestMode(enum.Enum):
+ UNIT_TEST_MODE = enum.auto()
+ CONTINUOUS_FUZZ_MODE = enum.auto()
+
+
+@dataclass
+class FuzzTestContext:
+ fuzz_test_binary_path: str
+ fuzz_test_binary_name: str
+ build_target_name: str
+ is_coverage_instrumented: bool
+ selected_fuzz_test_case: Optional[str] = None
+ coverage_output_base_name: Optional[str] = None
+ run_mode: Optional[FuzzTestMode] = None
+
+
+FUZZTEST_TOOLCHAIN_MARKER_DIR = "chip_pw_fuzztest"
+FUZZTEST_BIN_PATTERN = f"{FUZZTEST_TOOLCHAIN_MARKER_DIR}/tests/*"
+
+
+def get_build_target_from_fuzztest_path(binary_path):
+ """Extract build directory name (target name) from the given binary path, by using FUZZTEST_TOOLCHAIN_MARKER_DIR as a reference point.
+
+ This assumes the binary was built using the Pigweed FuzzTest toolchain, which GN places in a dedicated subdirectory with the toolchain name 'chip_pw_fuzztest' (as it is a secondary toolchain).
+ The build target is always the parent directory of 'chip_pw_fuzztest'.
+ """
+ path_directories = binary_path.split(os.sep)
+
+ for parent, child in zip(path_directories, path_directories[1:]):
+ if child == FUZZTEST_TOOLCHAIN_MARKER_DIR:
+ return parent
+
+ raise ValueError(
+ f"Could not deduce build target name: '{FUZZTEST_TOOLCHAIN_MARKER_DIR}' is not found in binary path: '{binary_path}'")
+
+
+def list_fuzz_test_binaries():
+ """Lists all compiled fuzz tests in the 'out' directory"""
+ build_dir = "out"
+ if not os.path.isdir(build_dir):
+ logging.error(f"Error: Build directory '{build_dir}' does not exist.")
+ return []
+
+ fuzz_tests = []
+ for file in glob.glob(f"{build_dir}/**/{FUZZTEST_BIN_PATTERN}", recursive=True):
+ if os.access(file, os.X_OK):
+ fuzz_tests.append(file)
+ return fuzz_tests
+
+
+def get_fuzz_test_cases(context):
+ """Executes the fuzz test binary (i.e. Fuzz Test Suite) with --list_fuzz_tests to print the list of all available FUZZ_TESTs (i.e. Test Cases)"""
+ try:
+ logging.debug(f"\nfuzz_test_path = {context.fuzz_test_binary_path}\n")
+
+ # workaround: we add this to supress creation of "default.profraw" which we don't need at this stage
+ env = os.environ.copy()
+ env["LLVM_PROFILE_FILE"] = os.devnull
+
+ result = subprocess.run([context.fuzz_test_binary_path, "--list_fuzz_tests"], env=env, capture_output=True, text=True)
+ output = result.stdout
+ if output:
+ return re.findall(r'test:\s*(\S+)', output)
+
+ else:
+ logging.info("No FUZZ_TESTs (TestCases) found in {context.fuzz_test_binary_path}")
+ raise ValueError(f"FuzzTest Binary outputted the following error: \n{result.stderr}\n")
+
+ except Exception as e:
+ raise Exception(f"Error executing {context.fuzz_test_binary_path}: {e}")
+
+
+def check_if_coverage_tools_detected():
+ missing = []
+ for tool in ["llvm-profdata", "llvm-cov", "genhtml"]:
+ if shutil.which(tool) is None:
+ missing.append(tool)
+
+ if missing:
+ raise Exception("Following required coverage packages not found: " + ", ".join(missing) +
+ "\nPlease either install them or source the correct environment")
+
+
+def run_fuzz_test(context):
+ """Runs the fuzz test and generates an LLVM profile file if binary is coverage instrumented"""
+
+ # Create build-specific profile folder
+ build_profile_folder = f"{profile_output_folder}/{context.build_target_name}"
+ os.makedirs(build_profile_folder, exist_ok=True)
+
+ if context.run_mode == FuzzTestMode.CONTINUOUS_FUZZ_MODE:
+ # Use the FuzzTest (Test Case) Name as the name for coverage output
+ context.coverage_output_base_name = "{}".format(context.selected_fuzz_test_case.replace('.', "_"))
+ elif context.run_mode == FuzzTestMode.UNIT_TEST_MODE:
+ # Use the FuzzTest Binary Name as the name for coverage output
+ context.coverage_output_base_name = "{}".format(context.fuzz_test_binary_name)
+
+ env = os.environ.copy()
+ if context.is_coverage_instrumented:
+ check_if_coverage_tools_detected()
+ profraw_file = f"{build_profile_folder}/{context.coverage_output_base_name}.profraw"
+ env["LLVM_PROFILE_FILE"] = profraw_file
+
+ try:
+ if context.run_mode == FuzzTestMode.UNIT_TEST_MODE:
+ subprocess.run([context.fuzz_test_binary_path, ], env=env, check=True)
+ logging.info("Fuzz Test Suite executed in Unit Test Mode.\n")
+
+ elif context.run_mode == FuzzTestMode.CONTINUOUS_FUZZ_MODE:
+ subprocess.run([context.fuzz_test_binary_path, f"--fuzz={context.selected_fuzz_test_case}"], env=env, check=True)
+
+ # in FuzzTestMode.CONTINUOUS_FUZZ_MODE, the fuzzing will run indefinitely until stopped by the user
+ except KeyboardInterrupt:
+ logging.info("\n===============\nContinuous-Mode Fuzzing Stopped")
+
+ except Exception as e:
+ raise ValueError(f"Error running fuzz test: {e}")
+
+
+def generate_coverage_report(context, output_dir_arg):
+ """Generates an HTML coverage report."""
+
+ build_profile_folder = f"{profile_output_folder}/{context.build_target_name}"
+ build_coverage_folder = f"{coverage_report_output_folder}/{context.build_target_name}"
+
+ # Create build-specific directories
+ os.makedirs(build_profile_folder, exist_ok=True)
+ os.makedirs(build_coverage_folder, exist_ok=True)
+
+ if not output_dir_arg:
+ coverage_subfolder = f"{build_coverage_folder}/{context.coverage_output_base_name}"
+ else:
+ coverage_subfolder = output_dir_arg
+
+ profraw_file = f"{build_profile_folder}/{context.coverage_output_base_name}.profraw"
+ profdata_file = f"{build_profile_folder}/{context.coverage_output_base_name}.profdata"
+ lcov_trace_file = f"{build_profile_folder}/{context.coverage_output_base_name}.info"
+
+ if not os.path.exists(profraw_file):
+ logging.error(f"Profile raw file not found: {profraw_file}")
+ return False
+
+ # Step1 Merge the profile data
+ subprocess.run(["llvm-profdata", "merge", "-sparse", profraw_file, "-o", profdata_file], check=True)
+ logging.debug(f"Profile data merged into {profdata_file}")
+
+ # Step2 Exports coverage data into lcov trace file format.
+ cmd = [
+ "llvm-cov",
+ "export",
+ "-format=lcov",
+ "--instr-profile",
+ profdata_file,
+ context.fuzz_test_binary_path
+ ]
+
+ # for -ignore-filename-regex
+ ignore_paths = [
+ "third_party/.*",
+ "/usr/include/.*",
+ "/usr/lib/.*",
+
+ ]
+ for p in ignore_paths:
+ cmd.append("-ignore-filename-regex")
+ cmd.append(p)
+
+ with open(lcov_trace_file, "w") as file:
+ subprocess.run(cmd, stdout=file, stderr=file)
+
+ logging.debug("Data exported into lcov trace format")
+
+ # Step3 Generate the coverage report
+ cmd = ["genhtml"]
+
+ errors_to_ignore = [
+ "inconsistent", "source"
+ ]
+ for e in errors_to_ignore:
+ cmd.append("--ignore-errors")
+ cmd.append(e)
+
+ flat = False
+ cmd.append("--flat" if flat else "--hierarchical")
+ cmd.append("--synthesize-missing")
+ cmd.append("--output-directory")
+ cmd.append(f"{coverage_subfolder}")
+ cmd.append(f"{lcov_trace_file}")
+
+ logging.info(f"Generating Coverage Report into: {coverage_subfolder}/index.html\n...Please wait...\n")
+ try:
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL)
+ except FileNotFoundError:
+ logging.error("genhtml not found. Please install lcov to generate the HTML coverage report")
+ return
+
+ logging.info("Coverage report Generated.")
+
+
+def run_script_in_interactive_mode():
+
+ fuzz_tests = list_fuzz_test_binaries()
+ if not fuzz_tests:
+ logging.error("No pigweed-based FuzzTests found in the 'out' directory.\n")
+ logging.info("FuzzTests can be built using build_examples.py, for example:\n\n\tpython scripts/build/build_examples.py --target linux-x64-tests-clang-pw-fuzztest-coverage build \n")
+ raise ValueError
+
+ # ==== Choose FuzzTest Binary ====
+ while True:
+ print("=" * 70 + "\n")
+ logging.info("INTERACTIVE MODE: Choose a FuzzTest Binary to Run:\n")
+ for i, fuzz_test in enumerate(fuzz_tests, start=1):
+ logging.info(f"\t{i}. {fuzz_test}")
+
+ fuzz_choice = click.prompt("Enter the number of the fuzz test binary to run", type=int)
+
+ if 1 <= fuzz_choice <= len(fuzz_tests):
+ break
+ else:
+ logging.error("Invalid choice for fuzz test binary. Please try again")
+
+ selected_fuzz = fuzz_tests[fuzz_choice - 1]
+ context = FuzzTestContext(
+ fuzz_test_binary_path=selected_fuzz,
+ fuzz_test_binary_name=selected_fuzz.split(os.sep)[-1],
+ build_target_name=get_build_target_from_fuzztest_path(selected_fuzz),
+ is_coverage_instrumented="-coverage" in get_build_target_from_fuzztest_path(selected_fuzz)
+ )
+
+ if not context.is_coverage_instrumented:
+ logging.error(
+ f"\nFuzzTest Not coverage instrumented: No coverage report will be generated for: '{context.fuzz_test_binary_path}'\n")
+ logging.error("for Coverage reports --> Build with Coverage by appending '-coverage' to target e.g.:\n\n\tpython scripts/build/build_examples.py --target linux-x64-tests-clang-pw-fuzztest-coverage build\n")
+ logging.info("Continuing...")
+
+ test_cases = get_fuzz_test_cases(context)
+
+ # ==== Choose Test Case (FUZZ_TEST) ====
+ while True:
+ print("=" * 70 + "\n")
+ logging.info("AVAILABLE TEST CASES --> Choose a number to run in 'Continuous Mode', continues until interrupted:\n")
+ for i, case in enumerate(test_cases, start=1):
+ logging.info(f"\t{i}. {case}")
+ logging.info("\nUNIT_TEST_MODE: Enter 0 to run all test cases in Unit Test Mode (just a few seconds of each FUZZ_TEST(testcase)\n")
+
+ choice = click.prompt("Enter the number of the test case to run", type=int)
+
+ if 1 <= choice <= len(test_cases):
+ context.run_mode = FuzzTestMode.CONTINUOUS_FUZZ_MODE
+ context.selected_fuzz_test_case = test_cases[choice - 1]
+ break
+
+ elif choice == 0:
+ context.run_mode = FuzzTestMode.UNIT_TEST_MODE
+ break
+
+ else:
+ logging.info("Invalid choice. Please try again")
+
+ return context
+
+
+def run_script_in_normal_mode(fuzz_test, test_case, list_test_cases, help):
+ fuzz_tests = list_fuzz_test_binaries()
+
+ if help or not fuzz_test:
+ logging.info("\nAVAILABLE FUZZTEST BINARIES in 'out' directory (each Binary can have multiple FUZZ_TESTs/TestCases): \n")
+ previous_build_target_dir = ""
+ for test in fuzz_tests:
+ build_target_dir = get_build_target_from_fuzztest_path(test)
+ if build_target_dir != previous_build_target_dir:
+ is_coverage_build = "-coverage" in build_target_dir
+ if is_coverage_build:
+ logging.info("\n----------- Coverage-instrumented FuzzTests -----------\n")
+ else:
+ logging.info("\n----------- FuzzTests without coverage-instrumentation -----------\n")
+ previous_build_target_dir = build_target_dir
+ print(f" {test}")
+ print("\n")
+ sys.exit(0)
+
+ # initialise fuzz test context
+ context = FuzzTestContext(
+ fuzz_test_binary_path=fuzz_test,
+ fuzz_test_binary_name=fuzz_test.split(os.sep)[-1],
+ build_target_name=get_build_target_from_fuzztest_path(fuzz_test),
+ is_coverage_instrumented="-coverage" in get_build_target_from_fuzztest_path(fuzz_test)
+ )
+
+ if not context.is_coverage_instrumented:
+ logging.error(
+ f"\nFuzzTest Not coverage instrumented: No coverage report will be generated for: '{context.fuzz_test_binary_path}'\n")
+ logging.error("for Coverage reports --> Build with Coverage by appending '-coverage' to target e.g.:\n\n\tpython scripts/build/build_examples.py --target linux-x64-tests-clang-pw-fuzztest-coverage build\n")
+ logging.info("Continuing...")
+
+ test_cases = get_fuzz_test_cases(context)
+
+ if (list_test_cases or not test_case) and test_cases:
+ logging.info(f"\nList of Testcases (i.e. FUZZ_TESTs) for {context.fuzz_test_binary_name}: \n")
+ for case in test_cases:
+ print(f" {case}")
+ print("\n")
+ if not test_case:
+ raise ValueError(
+ "Please use for: \n 1. CONTINUOUS_FUZZ_MODE: Use --test-case to choose a specific Testcase, run until interrupted \n 2. UNIT_TEST_MODE: '--test-case all' to run all Testcases for a few seconds")
+ sys.exit(0)
+
+ if not test_cases:
+ raise ValueError(f"No FUZZ_TESTs (TestCases) found in {fuzz_test}")
+
+ if test_case.strip().lower() == "all":
+ context.run_mode = FuzzTestMode.UNIT_TEST_MODE
+ else:
+ context.run_mode = FuzzTestMode.CONTINUOUS_FUZZ_MODE
+ context.selected_fuzz_test_case = test_case
+
+ return context
+
+
+@click.command(add_help_option=False)
+@click.option("--fuzz-test", help="Specific FuzzTest binary to run. If not provided, all available FuzzTest binaries in 'out' are listed.")
+@click.option("--test-case", help="Specific test case to run in continuous mode OR add '--test-case all' to run all Testcases for a few seconds.")
+@click.option("--list-test-cases", is_flag=True, help="List available test cases for the given FuzzTest binary and exit.")
+@click.option("--interactive", is_flag=True, help="Run Script in Interactive Mode (automatically lists FuzzTests, TestCases and allows choosing easily).")
+@click.option('--help', is_flag=True, help="Show this message and exit.")
+@click.option("--output", help="Optional directory for coverage report (auto-generated if not provided).")
+def main(fuzz_test, test_case, list_test_cases, interactive, output, help):
+
+ coloredlogs.install(
+ level="DEBUG",
+ fmt="%(message)s",
+ level_styles={
+ "debug": {"color": "cyan"},
+ "info": {"color": "green"},
+ "error": {"color": "red"},
+ },
+ )
+
+ if help or not fuzz_test:
+ logging.info("\nThis Script:")
+ logging.info("1. Runs Google FuzzTests in CONTINUOUS_FUZZ_MODE or UNIT_TEST_MODE")
+ logging.info("2. Automatically generates HTML Coverage Report if FuzzTest is coverage-instrumented.\n")
+ logging.info("WARNING: This Script is designed to work with FuzzTests built using build_examples.py")
+ print("=" * 70 + "\n")
+ ctx = click.get_current_context()
+ click.echo(ctx.get_help())
+ logging.info("\nCoverage Report Generation requires: llvm-profdata, llvm-cov, and genhtml (part of lcov package)")
+ print("\n" + "=" * 70 + "\n")
+
+ # ==== Run Script in Interactive or non-interactive mode ====
+ try:
+ if interactive:
+ context = run_script_in_interactive_mode()
+ else:
+ context = run_script_in_normal_mode(fuzz_test, test_case, list_test_cases, help)
+
+ except Exception as e:
+ logging.error(e)
+ logging.error("\nPlease Try Again.")
+ sys.exit(0)
+
+ # ==== Run FuzzTest and Generate Coverage Report ====
+ should_generate_coverage = False
+ try:
+ run_fuzz_test(context)
+ # Unit Test Mode
+ should_generate_coverage = True
+ except KeyboardInterrupt:
+ # Continuous Fuzzing Mode Stoppped by User
+ should_generate_coverage = True
+ except ValueError as e:
+ logging.error(e)
+
+ if should_generate_coverage and context.is_coverage_instrumented:
+ generate_coverage_report(context, output)
+ else:
+ logging.info("Skipping coverage report generation")
+
+
+if __name__ == "__main__":
+ main()