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.
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.