view release on metacpan or search on metacpan
bin/fuzz-harness-generator view on Meta::CPAN
# Purpose: Format a scalar input value as a Perl
# literal string suitable for embedding
# directly in generated test source code.
#
# Entry: $input - the input value to format.
# May be undef, a numeric string,
# or an arbitrary string.
#
# Exit: Returns a Perl literal string:
# 'undef' if $input is undef
# bare number if $input looks numeric
# single-quoted string otherwise, with
# backslashes and single quotes escaped.
#
# Side effects: None.
#
# Notes: Only scalar inputs are handled â corpus
# entries with arrayref or hashref inputs
# are not currently supported and will be
# formatted as a single-quoted string of
# the stringified reference, which will
doc/SchemaExtractor.pm view on Meta::CPAN
```bash
perl demo_extractor.pl
```
This creates a sample module, extracts schemas, and validates the results.
## How It Works
### 1. POD Analysis
The extractor looks for parameter documentation in POD:
```perl
=head2 validate_email($email)
=head3 INPUT
$email - string (5-254 chars), email address
Returns: 1 if valid
=cut
lib/App/Test/Generator.pm view on Meta::CPAN
use Config::Abstraction 0.36;
use Data::Dumper;
use Data::Section::Simple;
use File::Basename qw(basename);
use File::Spec;
use Module::Load::Conditional qw(check_install can_load);
use Params::Get;
use Params::Validate::Strict 0.30;
use Readonly;
use Readonly::Values::Boolean;
use Scalar::Util qw(looks_like_number);
use re 'regexp_pattern';
use Template;
use YAML::XS qw(LoadFile);
use Exporter 'import';
our @EXPORT_OK = qw(generate);
our $VERSION = '0.33';
lib/App/Test/Generator.pm view on Meta::CPAN
}
if(my $invalid = $hints->{invalid}) {
carp('TODO: handle yamltest_hints->invalid');
}
}
# If the schema says the type is numeric, normalize
if ($schema->{type} && $schema->{type} =~ /^(integer|number|float)$/) {
for (@edge_case_array) {
next unless defined $_;
$_ += 0 if Scalar::Util::looks_like_number($_);
}
}
# Load relationships from the schema if present and well-formed.
# SchemaExtractor may set this to undef or an empty arrayref when
# no relationships were detected, so guard both existence and type.
my @relationships;
if(exists($schema->{relationships}) && ref($schema->{relationships}) eq 'ARRAY') {
@relationships = @{$schema->{relationships}};
}
lib/App/Test/Generator.pm view on Meta::CPAN
# Unknown type â warn and skip rather than emitting broken code
} else {
carp "Unknown relationship type '$type', skipping";
}
}
# Dedup the edge cases
my %seen;
@edge_case_array = grep {
my $key = defined($_) ? (Scalar::Util::looks_like_number($_) ? "N:$_" : "S:$_") : 'U';
!$seen{$key}++;
} @edge_case_array;
# Sort the edge cases to keep it consistent across runs
@edge_case_array = sort {
return -1 if !defined $a;
return 1 if !defined $b;
my $na = Scalar::Util::looks_like_number($a);
my $nb = Scalar::Util::looks_like_number($b);
return $a <=> $b if $na && $nb;
return -1 if $na;
return 1 if $nb;
return $a cmp $b;
} @edge_case_array;
# render edge case maps for inclusion in the .t
my $edge_cases_code = render_arrayref_map(\%edge_cases);
my $type_edge_cases_code = render_arrayref_map(\%type_edge_cases);
lib/App/Test/Generator.pm view on Meta::CPAN
return $re;
}
# Hashrefs and other reference types fall through
# to render_fallback which uses Data::Dumper
return render_fallback($v);
}
# Numeric values are emitted unquoted so the generated
# test performs numeric rather than string comparison
return looks_like_number($v) ? $v : "'" . perl_sq($v) . "'";
}
# --------------------------------------------------
# _generate_transform_properties
#
# Convert a hashref of transform
# specifications into an arrayref of
# LectroTest property definition hashrefs,
# one per transform. Each hashref contains
# all the information needed by
lib/App/Test/Generator.pm view on Meta::CPAN
$output_spec //= {};
# --------------------------------------------------
# Property 1: Output range constraints (numeric)
# --------------------------------------------------
if(_is_numeric_transform($input_spec, $output_spec)) {
if(defined($output_spec->{'min'})) {
my $min = $output_spec->{'min'};
push @properties, {
name => 'min_constraint',
code => "defined(\$result) && looks_like_number(\$result) && \$result >= $min",
};
}
if(defined($output_spec->{'max'})) {
my $max = $output_spec->{'max'};
push @properties, {
name => 'max_constraint',
code => "defined(\$result) && looks_like_number(\$result) && \$result <= $max",
};
}
# Heuristic: transforms named 'positive' (case-insensitive)
# imply a non-negative result constraint
if($transform_name =~ /$TRANSFORM_POSITIVE_PATTERN/i) {
push @properties, {
name => 'non_negative',
code => "defined(\$result) && looks_like_number(\$result) && \$result >= 0",
};
}
}
# --------------------------------------------------
# Property 2: Specific value output
# --------------------------------------------------
if(defined($output_spec->{'value'})) {
my $expected = $output_spec->{'value'};
lib/App/Test/Generator.pm view on Meta::CPAN
# Property 4: Type preservation
# --------------------------------------------------
if(_same_type($input_spec, $output_spec)) {
my $type = _get_dominant_type($output_spec);
# Only emit a numeric_type check for numeric types â
# string and other types have no equivalent simple check
if($type eq 'number' || $type eq 'integer' || $type eq 'float') {
push @properties, {
name => 'numeric_type',
code => 'looks_like_number($result)',
};
}
}
# --------------------------------------------------
# Property 5: Definedness
# --------------------------------------------------
# Emit a defined() check for all transforms except those
# whose output type is explicitly 'undef' â those are
# expected to return nothing
lib/App/Test/Generator.pm view on Meta::CPAN
$prop_desc = $prop_def->{'description'} || "Custom property: $prop_name";
unless($prop_code) {
carp "Custom property '$prop_name' missing 'code' field, skipping";
next;
}
# Sanity-check: code must contain at least a variable
# reference or a word character to be meaningful
unless($prop_code =~ /\$/ || $prop_code =~ /\w+/) {
carp "Custom property '$prop_name' code looks invalid: $prop_code";
next;
}
} else {
# Neither string nor hashref â unrecognised definition type
carp 'Invalid property definition: ', render_fallback($prop_def);
next;
}
push @properties, {
lib/App/Test/Generator/Mutation/NumericBoundary.pm view on Meta::CPAN
# in the document copy it receives at test time
transform => sub {
my $doc = $_[0];
my $ops = $doc->find('PPI::Token::Operator') || [];
for my $op (@{$ops}) {
next unless $op->line_number == $line;
next unless $op->column_number == $col;
next unless $op->content eq $original;
# Safety check â do not mutate if this looks like
# a readline operator (<$fh>) rather than a numeric
# comparison. A readline < is immediately followed
# by a symbol token starting with $
my $next_sib = $op->next_sibling;
if($next_sib && $next_sib->isa('PPI::Token::Symbol')) {
last;
}
$op->set_content($change);
last;
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
use App::Test::Generator::Analyzer::ReturnMeta;
use App::Test::Generator::Analyzer::SideEffect;
use Carp qw(carp croak);
use PPI;
use Pod::Simple::Text;
use File::Basename;
use File::Path qw(make_path);
use Params::Get;
use Safe;
use Scalar::Util qw(looks_like_number);
use YAML::XS;
use IPC::Open3;
use JSON::MaybeXS qw(encode_json decode_json);
use Readonly;
use Symbol qw(gensym);
# --------------------------------------------------
# Confidence score thresholds for input and output analysis
# --------------------------------------------------
Readonly my $CONFIDENCE_HIGH_THRESHOLD => 60;
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
next unless $k && $v;
my $keyname = $k->content;
my $value = $v->can('content') ? $v->content : undef;
$value =~ s/^['"]|['"]$//g if defined $value;
if ($keyname eq 'type') {
$param{type} = lc($value);
} elsif ($keyname eq 'optional') {
$param{optional} = $value ? 1 : 0;
} elsif ($keyname =~ /^(min|max)$/ && looks_like_number($value)) {
$param{$keyname} = 0 + $value;
} elsif ($keyname eq 'matches') {
$param{matches} = qr/$value/;
}
}
$param{type} //= 'string';
$param{optional} //= 0;
$result{$key} = \%param;
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
# Positive
elsif ($constraint =~ /positive/i) {
$param->{min} = 1 if $param->{type} && $param->{type} eq 'integer';
$param->{min} = 0.01 if $param->{type} && $param->{type} eq 'number';
}
# Non-negative
elsif ($constraint =~ /non-negative/i) {
$param->{min} = 0;
} elsif($constraint =~ /(.+)?\s(.+)/) {
my ($op, $val) = ($1, $2);
if(looks_like_number($val)) {
if ($op eq '<') {
$param->{max} = $val - 1;
} elsif ($op eq '<=') {
$param->{max} = $val;
} elsif ($op eq '>') {
$param->{min} = $val + 1;
} elsif ($op eq '>=') {
$param->{min} = $val;
}
}
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
$p->{semantic} = 'filepath';
$self->_log(" ADVANCED: $param manipulated as file path");
return;
}
# Path validation patterns
# Only match a literal path assigned or defaulted to this variable
if(defined $p->{_default} && $p->{_default} =~ m{^([A-Za-z]:\\|/|\./|\.\./)}) {
$p->{type} = 'string';
$p->{semantic} = 'filepath';
$self->_log(" ADVANCED: $param default looks like a path");
return;
}
# IO::File detection
if ($code =~ /\$$param\s*->\s*isa\s*\(\s*['"]IO::File['"]\s*\)/ ||
$code =~ /IO::File\s*->\s*new\s*\(\s*\$$param/) {
$p->{type} = 'object';
$p->{isa} = 'IO::File';
$p->{semantic} = 'filehandle';
$self->_log(" ADVANCED: $param is IO::File object");
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
# $x :Type,
# $y = default
# ) { }
# Try to match signature after attributes
# Look for the parameter list - it's the last (...) before the opening brace
# that contains sigils ($, %, @)
if ($code =~ /sub\s+\w+\s*(?::\w+(?:\([^)]*\))?\s*)*\(((?:[^()]|\([^)]*\))*)\)\s*\{/s) {
my $potential_sig = $1;
# Check if this looks like parameters (has sigils)
if ($potential_sig =~ /[\$\%\@]/) {
$self->_log(" SIG: Found modern signature: ($potential_sig)");
$self->_parse_modern_signature($params, $potential_sig);
return;
}
}
# Traditional Style 1: my ($self, $arg1, $arg2) = @_;
if ($code =~ /my\s*\(\s*([^)]+)\)\s*=\s*\@_/s) {
my $sig = $1;
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
}
$self->_log(" CODE: $param length constraint $op $val");
}
# Numeric range checks (only if NOT part of error guard)
if (
!$guarded
&& $code =~ /\$$param\s*([<>]=?)\s*([+-]?(?:\d+\.?\d*|\.\d+))/
) {
my ($op, $val) = ($1, $2);
$p->{type} ||= looks_like_number($val) ? 'number' : 'integer';
if ($op eq '<' || $op eq '<=') {
# Only set max if it tightens the range
my $max = ($op eq '<') ? $val - 1 : $val;
$p->{max} = $max if !defined($p->{max}) || $max < $p->{max};
} elsif ($op eq '>' || $op eq '>=') {
my $min = ($op eq '>') ? $val + 1 : $val;
$p->{min} = $min if !defined($p->{min}) || $min > $p->{min};
}
}
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
}
# Extract default values with the new method
my $default_value = $self->_extract_default_value($param, $code);
if (defined $default_value && !exists $p->{_default}) {
$p->{optional} = 1;
$p->{_default} = $default_value;
# Try to infer type from default value if not already set
unless ($p->{type}) {
if (looks_like_number($default_value)) {
$p->{type} = $default_value =~ /\./ ? 'number' : 'integer';
} elsif (ref($default_value) eq 'ARRAY') {
$p->{type} = 'arrayref';
} elsif (ref($default_value) eq 'HASH') {
$p->{type} = 'hashref';
} elsif ($default_value eq 'undef') {
$p->{type} = 'scalar'; # undef can be any scalar
} elsif (defined $default_value && !ref($default_value)) {
$p->{type} = 'string';
}
lib/App/Test/Generator/Template.pm view on Meta::CPAN
if((!defined $spec->{min}) || ($spec->{min} <= 43.56)) {
push @cases, { %{$mandatory_args}, ( $arg_name => 43.56 ) };
}
[% IF module %]
# Send wrong data type - builtins aren't good at checking this
push @cases,
{ %{$mandatory_args}, ( $arg_name => "test string in float field $arg_name", _STATUS => 'DIES', _LINE => __LINE__ ) },
{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) },
{ %{$mandatory_args}, ( $arg_name => \42.1, _STATUS => 'DIES' ) }, # Scalar ref
# NaN and Inf are valid according to looks_like_number() so we
# cannot assume they die
# { %{$mandatory_args}, ( $arg_name => "NaN", _STATUS => 'DIES' ) },
{ %{$mandatory_args}, ( $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ ) };
[% END %]
# min/max numeric boundaries
if (defined $spec->{min}) {
my $min = $spec->{min};
push @cases,
{ %{$mandatory_args}, ( $arg_name => $min - 0.001, _STATUS => 'DIES' ) },
lib/App/Test/Generator/Template.pm view on Meta::CPAN
return $foundation;
}
[% IF use_properties %]
# ============================================================
# Property-Based Transform Tests (Test::LectroTest)
# ============================================================
use Test::LectroTest::Compat;
use Test::LectroTest::Generator qw(:common);
use Scalar::Util qw(looks_like_number);
diag('Run property-based transform tests') if($ENV{'TEST_VERBOSE'});
[% transform_properties_code %]
[% END %]
[% corpus_code %]
done_testing();
t/cli-extract-schemas.t view on Meta::CPAN
}
# --------------------------------------------------------------------
# --help
# --------------------------------------------------------------------
{
my ($exit, $out, $err) = run_cmd($script, '--help');
is($exit, 0, '--help exits cleanly');
like($out, qr/Usage:/i, '--help output looks correct' );
}
# --------------------------------------------------------------------
# Missing input file
# --------------------------------------------------------------------
{
my ($exit, $out, $err) = run_cmd($script);
isnt($exit, 0, 'missing input file exits non-zero');
t/cli-fuzz-harness-generator.t view on Meta::CPAN
my ($stdout, $stderr);
run3([$^X, @cmd], \undef, \$stdout, \$stderr);
my $exit = $? >> 8;
return ($exit, $stdout // '', $stderr // '');
}
# --help
{
my ($exit, $out, $err) = run_cmd($script, '--help');
is($exit, 0, '--help exits cleanly');
like($out, qr/Usage:/i, '--help output looks correct');
}
# --version
{
my ($exit, $out, $err) = run_cmd($script, '--version');
is($exit, 0, '--version exits cleanly');
like($out, qr/\d+\.\d+/, '--version prints version');
}
# --dry-run
t/generate.t view on Meta::CPAN
use open qw(:std :encoding(UTF-8));
my $conf_file = 't/conf/app_generator.yml';
ok(-e $conf_file, 'config file exists: $conf_file');
# Generate into a scalar
{
local *STDOUT;
open STDOUT, '>', \my $output;
App::Test::Generator->generate($conf_file);
like($output, qr/use Test::Most;/, 'output looks like a test file');
}
dies_ok { App::Test::Generator->generate() } 'Dies when not given an argument';
like $@, qr/^Usage: /;
done_testing();
t/schema_input.t view on Meta::CPAN
# Create a minimal test module to extract schema from
# ------------------------------------------------------------------
my $module = File::Spec->catfile($dir, 'TestSchema.pm');
open my $mod_fh, '>', $module or die $!;
print {$mod_fh} <<'EOF';
package TestSchema;
# This is the package that will be tested
use Scalar::Util qw(looks_like_number);
sub add {
if($_[0] && ($_[0] eq __PACKAGE__)) {
shift;
}
my ($a, $b) = @_;
die 'missing a' unless defined($a);
die 'not numeric' unless looks_like_number($a);
die 'not numeric' if defined($b) && !looks_like_number($b);
return $a + ($b // 0);
}
1;
EOF
close $mod_fh;
ok(-e $module, 'Test module created');
# ------------------------------------------------------------------