[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.
+
+![FuzzBlocker_before](img/fuzzblocker_before.png)
+
+-   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:
+
+![FuzzBlocker_after](img/fuzzblocker_after.png)
+
 ### 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()