npx claudepluginhub avsm/ocaml-claude-marketplace --plugin ocaml-devThis skill uses the workspace's default tool permissions.
1. **One fuzz file per module**: `fuzz_foo.ml` tests `lib/foo.ml`. Keeps tests organized and discoverable.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
fuzz_foo.ml tests lib/foo.ml. Keeps tests organized and discoverable.encode and decode, test decode(encode(x)) = x.For standalone packages, use one fuzz file per package:
ocaml-foo/
├── lib/
├── fuzz/
│ ├── dune
│ └── fuzz_foo.ml
└── dune-project
fuzz/dune:
(executable
(name fuzz_foo)
(modules fuzz_foo)
(libraries foo crowbar))
; Quick check with Crowbar (no AFL instrumentation)
(rule
(alias fuzz)
(deps fuzz_foo.exe)
(action
(run %{exe:fuzz_foo.exe})))
; AFL-instrumented build target (use with --profile=afl)
(rule
(alias fuzz-afl)
(deps
(source_tree input)
fuzz_foo.exe)
(action
(echo "AFL fuzzer built: %{exe:fuzz_foo.exe}\n")))
Seed corpus: Create fuzz/input/ with sample inputs:
mkdir -p fuzz/input
echo -n "" > fuzz/input/empty
# Add representative samples as seed inputs
fuzz/fuzz_foo.ml:
open Crowbar
let test_parse_crash_safety buf =
ignore (Foo.parse buf);
check true
let () =
add_test ~name:"foo: parse crash safety" [ bytes ] test_parse_crash_safety
For larger projects with many modules:
(executable
(name fuzz)
(libraries crowbar borealis)
(modules
fuzz
fuzz_common
fuzz_foo
fuzz_bar))
Main entry point (fuzz/fuzz.ml):
(* Force linking of modules that register tests via side effects *)
let () =
Fuzz_common.run ();
Fuzz_foo.run ();
Fuzz_bar.run ()
Each fuzz module ends with:
let run () = ()
This ensures the module is linked and its add_test calls execute.
When writing fuzz tests, follow these conventions:
add_test callsbytes directly instead of custom generatorstruncate helper to limit input size for protocol messages() directly - no need for check true in most casesCrypto_rng_unix.use_default () at the top if crypto is used(** Fuzz tests for Foo module. *)
open Crowbar
open Fuzz_common
(** Decode - must not crash on arbitrary input. *)
let test_decode buf =
let buf = truncate buf in
let _ = Foo.decode (to_bytes buf) in
()
(** Roundtrip - valid values must round-trip. *)
let test_roundtrip buf =
let buf = truncate buf in
match Foo.decode (to_bytes buf) with
| Error _ -> ()
| Ok v ->
let encoded = Foo.encode v in
match Foo.decode encoded with
| Error _ -> fail "re-decode failed"
| Ok v' -> if v <> v' then fail "roundtrip mismatch"
(** Pretty-print - must not crash. *)
let test_pp n =
let v = Foo.of_int (n mod 4) in
let _ = Format.asprintf "%a" Foo.pp v in
()
(* All add_test calls in run function - no side effects at module init *)
let run () =
add_test ~name:"foo: decode crash safety" [ bytes ] test_decode;
add_test ~name:"foo: roundtrip" [ bytes ] test_roundtrip;
add_test ~name:"foo: pp" [ uint8 ] test_pp
Main entry point (fuzz/fuzz.ml):
(* Initialize crypto RNG if needed by any module *)
let () = Crypto_rng_unix.use_default ()
(* Register all fuzz tests *)
let () =
Fuzz_common.run ();
Fuzz_foo.run ();
Fuzz_bar.run ()
open Crowbar
open Fuzz_common
(** Decode - must not crash on arbitrary input. *)
let test_decode buf =
let buf = truncate buf in
let _ = Foo.decode (to_bytes buf) in
()
(** Decode with exceptions - must not crash. *)
let test_decode_exn buf =
let buf = truncate buf in
(try ignore (Foo.decode_exn (to_bytes buf)) with _ -> ());
()
let run () =
add_test ~name:"foo: decode crash safety" [ bytes ] test_decode;
add_test ~name:"foo: decode_exn crash safety" [ bytes ] test_decode_exn
Key points:
bytes generator for arbitrary binary input (produces string type)ignore to discard results without warnings| exception _ -> () to catch any exceptionscheck true signals test passed(** Roundtrip - valid values must round-trip. *)
let test_roundtrip buf =
let buf = truncate buf in
match Foo.decode (to_bytes buf) with
| Error _ -> () (* Invalid input is fine *)
| Ok original ->
let encoded = Foo.encode original in
match Foo.decode encoded with
| Error _ -> fail "re-decode failed"
| Ok decoded ->
if original <> decoded then fail "roundtrip mismatch"
let run () =
add_test ~name:"foo: roundtrip" [ bytes ] test_roundtrip
Key points:
(** APID roundtrip - valid values must round-trip. *)
let test_apid_roundtrip n =
match Apid.of_int n with
| None -> if n >= 0 && n <= 2047 then fail "should accept valid value"
| Some apid ->
let n' = Apid.to_int apid in
if n <> n' then fail "roundtrip mismatch"
let run () =
add_test ~name:"apid: roundtrip" [ range 2048 ] test_apid_roundtrip
(** Max valid value. *)
let test_max_valid () =
match Apid.of_int 2047 with
| None -> fail "2047 should be valid"
| Some apid -> if Apid.to_int apid <> 2047 then fail "value mismatch"
(** Min valid value. *)
let test_min_valid () =
match Apid.of_int 0 with
| None -> fail "0 should be valid"
| Some apid -> if Apid.to_int apid <> 0 then fail "value mismatch"
let run () =
add_test ~name:"apid: max_valid" [ const () ] test_max_valid;
add_test ~name:"apid: min_valid" [ const () ] test_min_valid
Key points:
[ const () ] for tests with no random input[] as generator list (causes type error)(** Values above max must be rejected. *)
let test_invalid_above n =
let invalid = 2048 + n in
match Apid.of_int invalid with
| None -> ()
| Some _ -> fail "should reject values > 2047"
(** Negative values must be rejected. *)
let test_invalid_negative n =
let invalid = -(n + 1) in
match Apid.of_int invalid with
| None -> ()
| Some _ -> fail "should reject negative values"
let run () =
add_test ~name:"apid: invalid_above" [ range 1000 ] test_invalid_above;
add_test ~name:"apid: invalid_negative" [ range 1000 ] test_invalid_negative
(** Pretty-print - must not crash. *)
let test_pp buf =
let buf = truncate buf in
match Foo.decode (to_bytes buf) with
| Error _ -> ()
| Ok v -> let _ = Format.asprintf "%a" Foo.pp v in ()
let run () =
add_test ~name:"foo: pp" [ bytes ] test_pp
(** Test valid state transitions. *)
let test_activate_pending kid algo material_buf =
let material = to_bytes material_buf in
if Bytes.length material = 0 then ()
else
let key = Key.v ~kid ~algorithm:algo ~material in
match Key.activate key with
| Error _ -> () (* May fail if material invalid *)
| Ok active_key ->
if Key.state active_key <> Key.Active then fail "wrong state"
(** Test invalid state transitions return errors. *)
let test_activate_empty_fails kid algo =
let key = Key.empty ~kid ~algorithm:algo in
match Key.activate key with
| Ok _ -> fail "should fail on Empty key"
| Error (Key.Invalid_state_transition _) -> ()
| Error _ -> fail "wrong error type"
let run () =
add_test ~name:"key: activate Pending" [ uint8; uint8; bytes ]
test_activate_pending;
add_test ~name:"key: activate Empty fails" [ uint8; uint8 ]
test_activate_empty_fails
(** Nanoseconds roundtrip. *)
let test_ns_roundtrip n =
let d = Duration.of_ns n in
let n' = Duration.to_ns d in
if n <> n' then fail "ns roundtrip mismatch"
(** Microseconds to milliseconds conversion. *)
let test_us_to_ms n =
let us = Int64.of_int n in
let d = Duration.of_us us in
let ms = Duration.to_ms d in
let expected = Int64.div us 1000L in
if ms <> expected then fail "us to ms conversion failed"
let run () =
add_test ~name:"duration: ns_roundtrip" [ int64 ] test_ns_roundtrip;
add_test ~name:"duration: us_to_ms" [ range 1000000 ] test_us_to_ms
(** Test create/exists invariant. *)
let test_create_exists name_buf =
let name = Bytes.to_string (to_bytes name_buf) in
if String.length name = 0 then ()
else
let fs = Filestore.in_memory () in
match Filestore.create fs name with
| Error _ -> ()
| Ok () ->
if not (Filestore.exists fs name) then
fail "created file should exist"
let run () =
add_test ~name:"filestore: create_exists" [ bytes ] test_create_exists
(** Common utilities for fuzz tests. *)
open Crowbar
let to_bytes buf =
let len = String.length buf in
let b = Bytes.create len in
Bytes.blit_string buf 0 b 0 len;
b
let catch_invalid_arg f =
try f () with Invalid_argument _ -> check true
let run () = ()
| Generator | Type | Use for |
|---|---|---|
bytes | string | Arbitrary binary data |
uint8 | int | 0-255 |
int8 | int | -128 to 127 |
int32 | int32 | Full int32 range |
int64 | int64 | Full int64 range |
range n | int | 0 to n-1 |
bool | bool | true/false |
const v | 'a | Fixed value (for no-input tests) |
list gen | 'a list | Lists of generated values |
option gen | 'a option | Some/None |
fuzz/
├── fuzz.ml # Main entry, links all modules
├── fuzz_common.ml # Shared utilities
├── fuzz_tc_frame.ml # Tests for lib/frames/tc_frame.ml
├── fuzz_tm_frame.ml # Tests for lib/frames/tm_frame.ml
├── fuzz_apid.ml # Tests for lib/frames/apid.ml
├── fuzz_keyid.ml # Tests for lib/sdls/keyid.ml
└── ...
Naming convention: fuzz_<module>.ml tests lib/**/<module>.ml
dune exec fuzz/fuzz.exe
# Or use the alias:
dune build @fuzz
dune build fuzz/fuzz.exe
mkdir -p fuzz/input
echo -n "" > fuzz/input/empty
afl-fuzz -m none -i fuzz/input -o _fuzz -- \
_build/default/fuzz/fuzz.exe @@
Use crow to orchestrate long-running AFL campaigns across multiple targets:
# Initialize workspace (creates dune-workspace with afl profile if needed)
crow init
# Build all fuzz targets with AFL instrumentation
dune build --profile=afl @fuzz-afl
# List discovered fuzz targets
crow list
# Start a campaign with 8 CPUs for 24 hours
crow start --cpus=8 --duration=24h
# Monitor progress
crow status
# Stop the campaign
crow stop
crow automatically:
*/fuzz/dune files with crowbar dependenciesdune-workspace with AFL profile if missinggrep -h 'add_test ~name:"' fuzz/fuzz_*.ml | \
sed 's/.*~name:"\([^"]*\)".*/\1/' | sort | uniq -d
For each module with a public API (.mli file):
decode_*, parse_*, read_*, of_* functionsencode/decode, to_*/of_* pairspp_* functions don't crashequal and compare are consistentWhen adding fuzz tests to a codebase:
(* ERROR: This expression should not be a function *)
add_test ~name:"test" [] @@ fun () -> ...
const () for no-input testsadd_test ~name:"test" [ const () ] @@ fun () -> ...
(* BAD: Only tests happy path *)
add_test ~name:"foo: decode" [ bytes ] @@ fun buf ->
let Ok v = Foo.decode (to_bytes buf) in
check true
add_test ~name:"foo: decode" [ bytes ] @@ fun buf ->
(match Foo.decode (to_bytes buf) with
| Ok _ -> ()
| Error _ -> ());
check true
(* BAD: Fails on invalid input *)
add_test ~name:"foo: roundtrip" [ bytes ] @@ fun buf ->
match Foo.decode (to_bytes buf) with
| Error _ -> fail "decode failed" (* Wrong! Invalid input is expected *)
| Ok v -> ...
add_test ~name:"foo: roundtrip" [ bytes ] @@ fun buf ->
match Foo.decode (to_bytes buf) with
| Error _ -> check true (* Invalid input is fine *)
| Ok v -> ...
When adding fuzz tests, produce:
fuzz/fuzz_<module>.ml with comprehensive testsFuzz_<module>.run () calldune build succeeds