App-Test-Generator
view release on metacpan or search on metacpan
lib/App/Test/Generator/Template.pm view on Meta::CPAN
{ %mandatory_args, ( $arg_name => 'relative/path.txt' ) },
{ %mandatory_args, ( $arg_name => '.' ) },
{ %mandatory_args, ( $arg_name => '..' ) },
{ %mandatory_args, ( $arg_name => '../../etc/passwd', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => '/etc/passwd' . "\0", _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => '/' . ('a' x 4096), _STATUS => 'DIES' ) };
if($^O eq 'MSWin32') {
push @cases,
{ %mandatory_args, ( $arg_name => 'C:\\Users\\test\\file.txt' ) },
{ %mandatory_args, ( $arg_name => 'D:\\' ) },
{ %mandatory_args, ( $arg_name => 'relative\\path.txt' ) },
{ %mandatory_args, ( $arg_name => 'C:\\..\\..\\Windows\\System32', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => 'C:\\path\\' . "\0" . 'file', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => 'COM1', _STATUS => 'DIES' ) }, # reserved device name
{ %mandatory_args, ( $arg_name => 'NUL', _STATUS => 'DIES' ) }, # reserved device name
{ %mandatory_args, ( $arg_name => 'C:\\path\\file?.txt', _STATUS => 'DIES' ) }, # wildcard
{ %mandatory_args, ( $arg_name => 'C:\\path\\file*.txt', _STATUS => 'DIES' ) }, # wildcard
{ %mandatory_args, ( $arg_name => '\\\\server\\share\\file.txt' ) }; # UNC path
}
} elsif($semantic eq 'date_string') {
push @cases,
{ %mandatory_args, ( $arg_name => '2024-01-01' ) },
{ %mandatory_args, ( $arg_name => '1970-01-01' ) },
{ %mandatory_args, ( $arg_name => '2024-02-29' ) }, # leap day
{ %mandatory_args, ( $arg_name => '2023-02-29', _STATUS => 'DIES' ) }, # not a leap year
{ %mandatory_args, ( $arg_name => '2024-13-01', _STATUS => 'DIES' ) }, # month 13
{ %mandatory_args, ( $arg_name => '2024-00-01', _STATUS => 'DIES' ) }, # month 0
{ %mandatory_args, ( $arg_name => '01-01-2024', _STATUS => 'DIES' ) }, # wrong order
{ %mandatory_args, ( $arg_name => '2024/01/01', _STATUS => 'DIES' ) }, # wrong separator
{ %mandatory_args, ( $arg_name => 'not-a-date', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) };
} elsif($semantic eq 'iso8601_string') {
push @cases,
{ %mandatory_args, ( $arg_name => '2024-01-01T00:00:00Z' ) },
{ %mandatory_args, ( $arg_name => '2024-06-15T12:30:45Z' ) },
{ %mandatory_args, ( $arg_name => '2024-01-01T00:00:00+05:30' ) },
{ %mandatory_args, ( $arg_name => '2024-01-01' ) }, # date only - check if accepted
{ %mandatory_args, ( $arg_name => '2024-01-01T25:00:00Z', _STATUS => 'DIES' ) }, # hour 25
{ %mandatory_args, ( $arg_name => '2024-01-01T00:61:00Z', _STATUS => 'DIES' ) }, # minute 61
{ %mandatory_args, ( $arg_name => 'not-a-datetime', _STATUS => 'DIES' ) },
{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) };
} else {
diag("semantic type $semantic is not yet supported");
}
}
}
}
}
# Optional deduplication
# my %seen;
# @cases = grep { !$seen{join '|', %$_}++ } @cases;
# Random data test cases
# Uses type_edge_cases sometimes
if(scalar keys %input) {
if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
# our %input = ( type => 'string' );
my $type = $input{'type'};
for (1..[% iterations_code %]) {
my $case_input;
if (@edge_case_array && rand() < PROB_EDGE_CASE) {
# Sometimes pick a field-specific edge-case
$case_input = _pick_from(\@edge_case_array);
} elsif(exists $type_edge_cases{$type} && rand() < 0.3) {
# Sometimes pick a type-level edge-case
$case_input = _pick_from($type_edge_cases{$type});
} elsif($type eq 'string') {
if($input{matches}) {
$case_input = Data::Random::String::Matches->create_random_string({ regex => $input{'matches'} });
} else {
$case_input = rand_str();
}
} elsif($type eq 'integer') {
my $min = $input{'min'} // 0;
$case_input = int(rand_int() + $min);
# If it's takes an integer, a float should die
push @cases, { _input => $case_input + 0.1, _STATUS => 'DIES', _LINE => __LINE__ };
} elsif(($type eq 'number') || ($type eq 'float')) {
$case_input = rand_num() + $input{'min'};
} elsif($type eq 'boolean') {
$case_input = rand_bool();
} else {
die "TODO: type $type";
}
push @cases, { _input => $case_input, _STATUS => 'OK', _LINE => __LINE__ } if($case_input);
}
} else {
# our %input = ( str => { type => 'string' } );
push @cases, @{generate_tests(\%input, \%mandatory_args, _LINE => __LINE__)};
}
}
# edge-cases
if($config{'test_undef'}) {
if($all_optional) {
push @cases, { '_DESCRIPTION' => 'No args since all are optional' };
} else {
# Note that this is set on the input rather than output
push @cases, { '_STATUS' => 'DIES' }; # At least one argument is needed
}
}
if(scalar keys %input) {
push @cases, { '_STATUS' => 'DIES', map { $_ => undef } keys %input } if($config{'test_undef'});
} else {
push @cases, { '_DESCRIPTION' => 'Takes no input' }; # Takes no input
}
# If it's not in mandatory_strings it sets to 'undef' which is the idea, to test { value => undef } in the args
# _LINE has to go first or else the undef in there mucks up the hash format
push @cases, { _LINE => __LINE__, map { $_ => $mandatory_strings{$_} } keys %input, %mandatory_objects } if($config{'test_undef'} && !$positions);
push @candidate_bad, '' if($config{'test_empty'});
push @candidate_bad, undef if($config{'test_undef'});
push @candidate_bad, "\0" if($config{'test_nuls'});
# generate numeric, string, hashref and arrayref min/max edge cases
lib/App/Test/Generator/Template.pm view on Meta::CPAN
{ %{$mandatory_args}, ( $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ ) },
{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) };
}
push @cases,
{ %{$mandatory_args}, ( $arg_name => \'ref to scalar', _STATUS => 'DIES', _LINE => __LINE__ ) },
{ %{$mandatory_args}, ( $arg_name => sub { die 'boom' }, _STATUS => 'DIES', _LINE => __LINE__ ) },
{ %{$mandatory_args}, ( $arg_name => bless({}, 'Evil::Class'), _STATUS => 'DIES', _LINE => __LINE__ ) },
# { %{$mandatory_args}, ( $arg_name => [1, 2, 3], _STATUS => 'DIES', _LINE => __LINE__ ) }, # Generates false positives. Why?
{ %{$mandatory_args}, ( $arg_name => { a => 1 }, _STATUS => 'DIES', _LINE => __LINE__ ) };
[% END %]
return \@cases;
}
# dedup, fuzzing can easily generate repeats
# FIXME: I don't think this catches them all
# FIXME: Handle cases with Class::Simple calls
# FIXME: The JSON encoding fails on various data types that are sent (e.g. scalar refs, objects) so don't bother
sub _dedup_cases
{
my $cases = shift;
# Do not use JSON::MaybeXS because it will fail on non utf-8 characters
require JSON::PP;
JSON::PP->import();
eval {
my %seen;
my @rc = grep {
my $dump = encode_json($_);
!$seen{$dump}++
} @{$cases};
return \@rc;
};
# Carp::carp(__PACKAGE__, ": disabling deduping: $@");
return $cases;
}
sub generate_tests
{
my $input = $_[0];
my %mandatory_args = %{$_[1]};
my @cases;
foreach my $field (keys %input) {
my $spec = $input{$field} || {};
foreach my $field(keys %{$spec}) {
next if($field =~ /^_/); # Ignore comments
if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'position', 'memberof', 'semantic', 'isa'))) {
die("TODO: handle schema keyword '$field'");
}
}
}
# Build a test of the mandatory args
push @cases, { _input => \%mandatory_args, status => 'OK' } if(keys %mandatory_args);
for (1..[% iterations_code %]) {
# One by one change each of the mandatory fields
foreach my $field (keys %input) {
my $spec = $input{$field} || {};
next if $spec->{'memberof'}; # Memberof data is created below
my $type = $spec->{type} || 'string';
my %case_input = (%mandatory_args);
# 1) Sometimes pick a field-specific edge-case
if (exists $edge_cases{$field} && rand() < PROB_EDGE_CASE) {
push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
$case_input{$field} = _pick_from($edge_cases{$field});
next;
}
# 2) Sometimes pick a type-level edge-case
if (exists $type_edge_cases{$type} && rand() < 0.3) {
$case_input{$field} = _pick_from($type_edge_cases{$type});
push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
next;
}
# 3) Sormal random generation by type
if ($type eq 'string') {
if(my $re = $spec->{matches}) {
if(ref($re) ne 'Regexp') {
$re = qr/$re/;
}
if($spec->{'max'}) {
$case_input{$field} = Data::Random::String::Matches->create_random_string({ length => $spec->{'max'}, regex => $re });
} elsif($spec->{'min'}) {
$case_input{$field} = Data::Random::String::Matches->create_random_string({ length => $spec->{'min'}, regex => $re });
} else {
$case_input{$field} = Data::Random::String::Matches->create_random_string({ regex => $re });
}
} elsif(my $semantic = $spec->{'semantic'}) {
if($semantic eq 'email') {
$case_input{$field} = rand_email($spec->{'max'} // $spec->{'min'});
} else {
diag(__LINE__, ": TODO: handle semantic type '$semantic'");
}
} elsif(!$spec->{'memberof'}) {
if(my $min = $spec->{min}) {
$case_input{$field} = rand_str($min);
if($config{'test_empty'} && ($min == 0)) {
push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
$case_input{$field} = '';
}
} else {
$case_input{$field} = rand_str();
if($config{'test_empty'}) {
push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
$case_input{$field} = '';
}
}
}
} elsif ($type eq 'integer') {
if(my $min = $spec->{min}) {
if(my $max = $spec->{'max'}) {
$case_input{$field} = int(rand($max - $min + 1)) + $min;
} else {
( run in 0.635 second using v1.01-cache-2.11-cpan-71847e10f99 )