diff --git a/.evergreen/scripts/run_tests.py b/.evergreen/scripts/run_tests.py index b0273f1d80..9c3cdf13f5 100644 --- a/.evergreen/scripts/run_tests.py +++ b/.evergreen/scripts/run_tests.py @@ -139,15 +139,26 @@ def handle_fuzz() -> None: targets = ["fuzz_decode", "fuzz_decode_all", "fuzz_is_valid"] if SUB_TEST_NAME == "regression": + # Run the committed reproducers plus the seed corpus. # Skip dotfiles (pathlib globbing matches them). inputs = sorted( str(p) for p in (ROOT / "fuzz" / "regression").glob("*") if not p.name.startswith(".") ) if not inputs: raise RuntimeError("No fuzz regression inputs found in fuzz/regression/") - # Each reproducer is passed as its own file argument, so libFuzzer - # runs each exactly once; the -atheris_runs budget equals the file - # count so atheris exits cleanly after the last one. + corpus_dir = os.environ.get("FUZZ_CORPUS_DIR", str(ROOT / "fuzz-scratch" / "corpus")) + # Fail fast on a missing or empty corpus; libFuzzer would silently + # skip it. + corpus_files = sorted( + str(p) for p in Path(corpus_dir).glob("*") if not p.name.startswith(".") + ) + if not corpus_files: + raise RuntimeError( + f"No fuzz seed corpus found in {corpus_dir}; was setup_tests.py run?" + ) + inputs.extend(corpus_files) + # Each input runs exactly once without random fuzzing; the -atheris_runs + # budget equals the file count so atheris exits cleanly after the last. runs = len(inputs) for target in targets: run_command( diff --git a/.evergreen/scripts/setup_tests.py b/.evergreen/scripts/setup_tests.py index dbf87d28d5..e21a346480 100644 --- a/.evergreen/scripts/setup_tests.py +++ b/.evergreen/scripts/setup_tests.py @@ -530,8 +530,9 @@ def handle_test_env() -> None: else: TEST_ARGS = f"test/performance/async_perf_test.py {TEST_ARGS}" - if test_name == "fuzz" and sub_test_name != "regression": - # Build the seed corpus from the vendored spec data. + if test_name == "fuzz": + # Build the seed corpus from the vendored spec data. Regression runs + # run it too. scratch_dir = ROOT / "fuzz-scratch" shutil.rmtree(scratch_dir, ignore_errors=True) corpus_dir = scratch_dir / "corpus" diff --git a/fuzz/README.md b/fuzz/README.md index ab05d6096f..8069c0586c 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -47,6 +47,9 @@ just setup-tests fuzz regression just run-tests ``` +On every PR, the `fuzz-regression` task runs the committed reproducers and the full generated seed +corpus. + Or without the justfile: ```bash @@ -114,5 +117,5 @@ fuzz_decode.py Fuzz target: bson.decode() fuzz_decode_all.py Fuzz target: bson.decode_all() fuzz_is_valid.py Fuzz target: bson.is_valid() fuzz_shared.py Code shared by the fuzz targets -regression/ Known bug reproducers (no corpus setup required) +regression/ Known bug reproducers; run with the seed corpus on PRs ``` diff --git a/fuzz/fuzz_shared.py b/fuzz/fuzz_shared.py index 30bc47e373..255cc9a004 100755 --- a/fuzz/fuzz_shared.py +++ b/fuzz/fuzz_shared.py @@ -57,6 +57,10 @@ def normalize(obj: object) -> object: return [normalize(v) for v in obj] if isinstance(obj, float) and math.isnan(obj): return "nan" + if type(obj).__name__ == "Regex": + # int() canonicalizes the C extension's plain int flags and pure + # Python's re.RegexFlag. + return f"Regex:{obj.pattern!r}:{int(obj.flags)}" return f"{type(obj).__name__}:{obj!r}" diff --git a/fuzz/regression/decode__regex_flags_repr b/fuzz/regression/decode__regex_flags_repr new file mode 100644 index 0000000000..cfb1d7a917 Binary files /dev/null and b/fuzz/regression/decode__regex_flags_repr differ