Quick start

One schema, one generated file, one command. Then a t/fuzz.t that runs every schema you keep in the repository, so make test and CI pick up new ones without editing anything.

1. Write a schema

Create t/conf in your module and put one file in it. The file names a module and a function, and describes the arguments and the return value. This example covers a plain function with one argument:

# t/conf/abs.yml
---
module: builtin
function: abs

config:
  test_undef: no
  test_empty: no
  test_nuls: no

input:
  number:
    type: number
    position: 0

output:
  type: number
  min: 0

module: builtin means "a Perl built-in, not a module to load". For your own code you would write module: My::Widget and function: validate. output declares what a successful return looks like; add _STATUS: DIES to say the routine must croak instead. The schema reference lists every key.

2. Generate and run

fuzz-harness-generator -r t/conf/abs.yml

-r generates the harness, runs it through prove and prints the result. You should see something like:

Generated: /tmp/hK3s9aB2.t
Result: PASS
Files=1, Tests=47,  1 wallclock secs

Drop -r to write the file where you can read it. The harness is ordinary Perl and is meant to be inspected:

fuzz-harness-generator -o t/abs_fuzz.t t/conf/abs.yml
prove -l t/abs_fuzz.t

Inside you will find the static corpus, the typed random generator, the boundary cases derived from min/max, and one run_test() helper that calls the routine, checks for the expected exception, and validates the return value.

3. Make prove run it

Regenerating a file on every test run is unnecessary, and committing generated tests is noisy. The usual pattern is a single t/fuzz.t that reads every schema in t/conf and asks the generator to run it in a subprocess:

# t/fuzz.t
use strict;
use warnings;
use FindBin qw($Bin);
use IPC::Run3;
use Test::Needs 'App::Test::Generator';
use Test::Most;

my $dirname = "$Bin/conf";

if(-d $dirname && opendir(my $dh, $dirname)) {
	while(my $filename = readdir($dh)) {
		next if $filename eq '.' || $filename eq '..' || $filename =~ /\.swp$/;

		my $filepath = "$dirname/$filename";
		next unless -f $filepath;

		my ($stdout, $stderr);
		run3 [$^X, 'fuzz-harness-generator', '-r', $filepath], undef, \$stdout, \$stderr;

		ok($? == 0, "Generated test script exits successfully: $filename");
		ok($stdout =~ /^Result: PASS/ms, "$filename passes") if $? == 0;
		diag($stderr) if length($stderr);
	}
	closedir($dh);
}

done_testing();

Note the two deliberate details: the generator is invoked through $^X rather than by name, and run3 keeps stdout separate, so a failing harness is reported as a failing test rather than as a crash.

4. Extract schemas from an existing module

Writing the first schema by hand is the slow part. Point extract-schemas at a .pm file and it will infer one schema per routine, from the signature, the validation calls, the POD and any formal =head4 input block:

extract-schemas lib/App/Test/Generator/Sample/Module.pm --output-dir schemas
fuzz-harness-generator -r schemas/greet.yml

Read the generated YAML before trusting it: inference is heuristic, and a schema is only as good as the type and constraint information the source makes available. The extraction page explains the confidence levels and what makes a schema high-confidence.

5. Keep the useful ones

Move the schemas worth keeping into t/conf/ and delete the rest. From then on the harnesses, the mutation runs and the dashboard all follow that one directory:

mkdir -p t/conf
mv schemas/validate.yml schemas/normalise.yml t/conf/
prove -l t/fuzz.t

Two things then come almost free. A test dashboard can be generated from the same run, and surviving mutants can be written back into t/conf/ as new boundary cases.

6. Run it on every push

The distribution ships two GitHub Actions workflows that are designed to be copied into another repository:

deploy-workflows --target ~/src/My-Widget
cd ~/src/My-Widget
git add .github/workflows
git commit -m 'Add test dashboard and mutation workflows'

They run the fuzz suite, mutation testing and the dashboard on a schedule and commit the results back to a gh_pages branch. If you only want the fuzzing part, copy the "generate fuzz tests and run them" steps from dashboard.yml and skip the rest.

Keep the seeds

Fuzzing is random by default, which is what makes it find things — and also what makes a failure hard to reproduce. When a run fails, set seed: in the schema so the generated harness calls srand() with that value, and raise iterations: if the failure needs a longer hunt.

← Install Schema reference →