Group
Extension

App-Test-Generator/lib/App/Test/Generator/Template.pm

package App::Test::Generator::Template;

use strict;
use warnings;
use autodie qw(:all);

use utf8;
use Data::Section::Simple;

our $VERSION = '0.16';

=head1 NAME

App::Test::Generator::Template - Template for the test file generated by App::Test::Generator

=head1 VERSION

Version 0.16

=head1 SYNOPSIS

The template for the test file generated by App::Test::Generator.

=head1 METHODS

  get_data_section($template_file)

Returns a reference to the template.
The only value for C<$template_file>, for now, is test.tt.

=cut

sub get_data_section
{
	if($_[0] && ($_[0] eq __PACKAGE__)) {
		shift;
	}

	return \Data::Section::Simple::get_data_section($_[0]);
}

1;

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

Portions of this module's initial design and documentation were created with the
assistance of L<ChatGPT|https://openai.com/> (GPT-5), with final curation
and authorship by Nigel Horne.

=cut

__DATA__

@@ test.tt
#!/usr/bin/env perl

use strict;
use warnings;

use utf8;
use open qw(:std :encoding(UTF-8));	# https://github.com/nigelhorne/App-Test-Generator/issues/1

use Data::Dumper;
use Data::Random qw(:all);
use Data::Random::String;
use Data::Random::String::Matches 0.02;
use Data::Random::Structure;
use Test::Most;
use Test::Returns 0.02;

if($^O ne 'MSWin32') {
	close(STDIN);
	open(STDIN, '<', '/dev/null');
}

# TODO: add more, and remove magic numbers
# perhaps allow them to be configurable?
use constant {
	PROB_LOWERCASE => 0.72,
	PROB_EDGE_CASE => 0.4,
};

[% setup_code %]

[% IF module %]
	diag('[% module %]->[% function %] test case created by https://github.com/nigelhorne/App-Test-Generator');
[% ELSE %]
	diag('[% function %] test case created by https://github.com/nigelhorne/App-Test-Generator');
[% END %]

# Edge-case maps injected from config (optional)
my %edge_cases = (
[% edge_cases_code %]
);
my @edge_case_array = (
[% edge_case_array_code %]
);
my %type_edge_cases = (
[% type_edge_cases_code %]
);
my %config = (
[% config_code %]
);

# Seed for reproducible fuzzing (if provided)
[% seed_code %]

my %input = (
[% input_code %]
);

my %output = (
[% output_code %]
);

my %transforms = (
[% transforms_code %]
);

# Candidates for regex comparisons
my @candidate_good = ('123', 'abc', 'A1B2', '0');
my @candidate_bad = (
	"😊",	# emoji
	"123",	# full-width digits
	"١٢٣",	# Arabic digits
	'..',	# regex metachars
	"a\nb",	# newline in middle
	"é",	# E acute
	'x' x 5000,	# huge string

	# Added later if the configuration says so
	# '',	# empty
	# undef,	# undefined
	# "\0",	# null byte
);

# --- Fuzzer helpers ---
sub _pick_from {
	my $arrayref = $_[0];
	return undef unless $arrayref && ref $arrayref eq 'ARRAY' && @$arrayref;
	return $arrayref->[ int(rand(scalar @$arrayref)) ];
}

sub rand_ascii_str {
	my $len = shift || int(rand(10)) + 1;
	# join '', map { chr(97 + int(rand(26))) } 1..$len;
	return Data::Random::String->create_random_string(length => $len, contains => 'alphanumeric');
}

my @unicode_codepoints = (
	0x00A9,	# ©
	0x00AE,	# ®
	0x03A9,	# Ω
	0x20AC,	# €
	0x2013,	# – (en-dash)
	0x0301,	# combining acute accent
	0x0308,	# combining diaeresis
	0x1F600,	# 😀 (emoji)
	0x1F62E,	# 😮
	0x1F4A9,	# 💩 (yes)
);

# Tests for matches or nomatch
my @regex_tests = (
	'match123',
	'nope',
	'/fullpath',
	'/',
	'/etc/passwd',
	'../../etc/passwd',
	"/etc/passwd\0",
	"D:\\dos_path",
	"I:\\",
);

sub rand_unicode_char {
	my $cp = $unicode_codepoints[ int(rand(@unicode_codepoints)) ];
	return chr($cp);
}

# Generate a string: mostly ASCII, sometimes unicode, sometimes nul bytes or combining marks
sub rand_str
{
	my $len = shift || int(rand(10)) + 1;

	my @chars;
	for (1..$len) {
		my $r = rand();
		if ($r < PROB_LOWERCASE) {
			push @chars, chr(97 + int(rand(26)));	# a-z
		} elsif ($r < 0.88) {
			push @chars, chr(65 + int(rand(26)));	# A-Z
		} elsif ($r < 0.95) {
			push @chars, chr(48 + int(rand(10)));	# 0-9
		} elsif ($r < 0.975) {
			push @chars, rand_unicode_char();	# occasional emoji/marks
		} elsif($config{'test_nuls'}) {
			push @chars, chr(0);	# nul byte injection
		} else {
			push @chars, chr(97 + int(rand(26)));	# a-z
		}
	}
	# Occasionally prepend/append a combining mark to produce combining sequences
	if (rand() < 0.08) {
		unshift @chars, chr(0x0301);
	}
	if (rand() < 0.08) {
		push @chars, chr(0x0308);
	}
	return join('', @chars);
}

# Random character either upper or lower case
# sub rand_char
# {
	# return rand_chars(set => 'all', min => 1, max => 1);

	# my $char = '';
	# my $upper_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
	# my $lower_chars = 'abcdefghijklmnopqrstuvwxyz';
	# my $combined_chars = $upper_chars . $lower_chars;

	# # Generate a random index between 0 and the length of the string minus 1
	# my $rand_index = int(rand(length($combined_chars)));

	# # Get the character at that index
	# return substr($combined_chars, $rand_index, 1);
# }

# Integer generator: mix typical small ints with large limits
sub rand_int {
	my $r = rand();
	if ($r < 0.75) {
		return int(rand(200)) - 100;	# -100 .. 100 (usual)
	} elsif ($r < 0.9) {
		return int(rand(2**31)) - 2**30;	# 32-bit-ish
	} elsif ($r < 0.98) {
		return (int(rand(2**63)) - 2**62);	# 64-bit-ish
	} else {
		# very large/suspicious values
		return 2**63 - 1;
	}
}
sub rand_bool { rand() > 0.5 ? 1 : 0 }

# Number generator (floating), includes tiny/huge floats
sub rand_num {
	my $r = rand();
	if ($r < 0.7) {
		return (rand() * 200 - 100);	# -100 .. 100
	} elsif ($r < 0.9) {
		return (rand() * 1e12) - 5e11;	# large-ish
	} elsif ($r < 0.98) {
		return (rand() * 1e308) - 5e307;	# very large floats
	} else {
		return 1e-308 * (rand() * 1000);	# tiny float, subnormal-like
	}
}

sub rand_arrayref {
	my $len = shift || int(rand(3)) + 1; # small arrays

	return Data::Random::Structure->new(max_elements => $len, max_depth => 1)->generate_array();

	# return [ map { rand_str() } 1..$len ];
}

sub rand_hashref {
	my $len = shift || int(rand(3)) + 1; # small hashes

	return Data::Random::Structure->new(max_elements => $len, max_depth => 1)->generate_hash();

	# my %h;
	# for (1..$len) {
		# $h{rand_str(3)} = rand_str(5);
	# }
	# return \%h;
}

sub rand_email
{
	my $len = shift || int(rand(10));
	my $l;
	my @name;
	my @tlds = qw(com org net edu gov io co uk de fr);

	for($l = 0; $l < $len; $l++) {
		push @name, pack('c', (int(rand 26))+97);
	}
	push @name, '@';
	$len = rand(10);
	for($l = 0; $l < $len; $l++) {
		push @name, pack('c', (int(rand 26))+97);
	}
	push @name, '.';
	$len = rand($#tlds+1);
	push @name, $tlds[$len];
	return join('', @name);
}

sub fuzz_inputs
{
	my @cases;

	# Are any options manadatory?
	my $all_optional = 1;
	my %mandatory_strings;	# List of mandatory strings to be added to all tests, always put at start so it can be overwritten
	my %mandatory_objects;
	my %mandatory_numbers;
	my $class_simple_loaded;
	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		if((ref($spec) eq 'HASH') && (!$spec->{optional})) {
			$all_optional = 0;
			if($spec->{'type'} eq 'string') {
				local $config{'test_undef'} = 0;
				local $config{'test_nuls'} = 0;
				local $config{'test_empty'} = 0;
				$mandatory_strings{$field} = rand_ascii_str();
			} elsif($spec->{'type'} eq 'object') {
				my $method = $spec->{'can'};
				if(!$class_simple_loaded) {
					require_ok('Class::Simple');
					eval {
						Class::Simple->import();
						$class_simple_loaded = 1;
					};
				}
				my $obj = new_ok('Class::Simple');
				$obj->$method(1);
				$mandatory_objects{$field} = $obj;
				$config{'dedup'} = 0;	# FIXME:	Can't yet dedup with class method calls
			} elsif(($spec->{'type'} eq 'float') || ($spec->{'type'} eq 'number')) {
				my $min = $spec->{'min'};
				my $max = $spec->{'max'};
				my $number;
				if(defined($min)) {
					$number = rand($min);
				} else {
					$number = rand(100000);
				}
				if(defined($max)) {
					if($number > $max) {
						$number = $max;
					}
				}
				$mandatory_numbers{$field} = $number;
			} else {
				die 'TODO: type = ', $spec->{'type'};
			}
		}
	}
	my %mandatory_args = (%mandatory_strings, %mandatory_objects, %mandatory_numbers);

	if(($all_optional) || ((scalar keys %input) > 1)) {
		# Basic test cases
		if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
			# our %input = ( type => 'string' );
			my $type = $input{'type'};

			foreach my $field(keys %input) {
				if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can'))) {
					die("TODO: handle schema keyword '$field'");
				}
			}

			if ($type eq 'string') {
				# Is hello allowed?
				if(!defined($input{'memberof'}) || (grep { $_ eq 'hello' } @{$input{'memberof'}})) {
					if(defined($input{'notmemberof'}) && (grep { $_ eq 'hello' } @{$input{'notmemberof'}})) {
						push @cases, { _input => 'hello', _STATUS => 'DIES' };
					} else {
						push @cases, { _input => 'hello' };
					}
				} elsif(defined($input{'memberof'}) && !defined($input{'max'})) {
					# Data::Random
					push @cases, { _input => (rand_set(set => $input{'memberof'}, size => 1))[0] }
				} else {
					if((!defined($input{'min'})) || ($input{'min'} >= 1)) {
						push @cases, { _input => '0' } if(!defined($input{'memberof'}));
					}
					if(defined($input{'notmemberof'}) || (!grep { $_ eq 'hello' } @{$input{'memberof'}})) {
						push @cases, { _input => 'hello' };
					} else {
						push @cases, { _input => 'hello', _STATUS => 'DIES' };
					}
				}
				push @cases, { _input => '' } if((!exists($input{'min'})) || ($input{'min'} == 0));
				# push @cases, { $field => "emoji \x{1F600}" };
				push @cases, { _input => "\0null" } if($config{'test_nuls'});
			} else {
				die "TODO: type $type";
			}
		} else {
			# our %input = ( str => { type => 'string' } );
			foreach my $arg_name (keys %input) {
				my $spec = $input{$arg_name} || {};
				my $type = lc((!ref($spec)) ? $spec : $spec->{type}) || 'string';

				foreach my $field(keys %{$spec}) {
					if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'memberof', 'position'))) {
						diag(__LINE__, ": TODO: handle schema keyword '$field'");
					}
				}

				# --- Type-based seeds ---
				if(($type eq 'number') || ($type eq 'float')) {
					push @cases, @{_generate_float_cases($arg_name, $spec, \%mandatory_args)};
				}
				elsif ($type eq 'integer') {
					# Probably duplicated below, but here as well just in case
					push @cases, @{_generate_integer_cases($arg_name, $spec, \%mandatory_args)};
				} elsif ($type eq 'string') {
					# Is hello allowed?
					if(my $re = $spec->{matches}) {
						if(ref($re) ne 'Regexp') {
							$re = qr/$re/;
						}
						my $random_string;
						if($spec->{'max'}) {
							$random_string = Data::Random::String::Matches->create_random_string({ length => $spec->{'max'}, regex => $re });
						} elsif($spec->{'min'}) {
							$random_string = Data::Random::String::Matches->create_random_string({ length => $spec->{'min'}, regex => $re });
						} else {
							$random_string = Data::Random::String::Matches->create_random_string({ regex => $re });
						}
						foreach my $str('hello', $random_string) {
							if($str =~ $re) {
								if(!defined($spec->{'memberof'}) || (grep { $_ eq $str } @{$spec->{'memberof'}})) {
									if(defined($spec->{'notmemberof'}) && (grep { $_ eq $str } @{$spec->{'notmemberof'}})) {
										push @cases, { %mandatory_args, ( $arg_name => $str, _STATUS => 'DIES' ) };
									} else {
										push @cases, { %mandatory_args, ( $arg_name => $str ) };
									}
								} elsif(defined($spec->{'memberof'}) && !defined($spec->{'max'})) {
									# Data::Random
									push @cases, { %mandatory_args, ( _input => (rand_set(set => $spec->{'memberof'}, size => 1))[0] ) }
								} else {
									push @cases, { %mandatory_args, ( $arg_name => $str, _STATUS => 'DIES' ) };
								}
							} else {
								push @cases, { %mandatory_args, ( $arg_name => $str, _STATUS => 'DIES' ) };
							}
						}
					} else {
						if(!defined($spec->{'memberof'}) || (grep { $_ eq 'hello' } @{$spec->{'memberof'}})) {
							if(defined($spec->{'notmemberof'}) && (grep { $_ eq 'hello' } @{$spec->{'notmemberof'}})) {
								push @cases, { %mandatory_args, ( $arg_name => 'hello', _LINE => __LINE__, _STATUS => 'DIES' ) };
							} else {
								push @cases, { %mandatory_args, ( $arg_name => 'hello' ) };
							}
						} else {
							push @cases, { %mandatory_args, ( $arg_name => 'hello', _LINE => __LINE__, _STATUS => 'DIES' ) };
						}
					}
					if((!exists($spec->{min})) || ($spec->{min} == 0)) {
						# '' should die unless it's in the memberof list
						if(defined($spec->{'memberof'}) && (!grep { $_ eq '' } @{$spec->{'memberof'}})) {
							push @cases, { %mandatory_args, ( $arg_name => '', _NAME => $arg_name, _STATUS => 'DIES' ) }
						} elsif(defined($spec->{'memberof'}) && !defined($spec->{'max'})) {
							# Data::Random
							push @cases, { %mandatory_args, _input => (rand_set(set => $spec->{'memberof'}, size => 1))[0] }
						} else {
							push @cases, { %mandatory_args, ( $arg_name => '', _NAME => $arg_name ) } if((!exists($spec->{min})) || ($spec->{min} == 0));
						}
					}
					# push @cases, { $arg_name => "emoji \x{1F600}" };
					push @cases, { %mandatory_args, ( $arg_name => "\0null" ) } if($config{'test_nuls'} && (!(defined $spec->{memberof})) && !defined($spec->{matches}));

					unless(defined($spec->{memberof}) || defined($spec->{matches})) {
						# --- min/max string/array boundaries ---
						if (defined $spec->{min}) {
							my $len = $spec->{min};
							push @cases, { %mandatory_args, ( $arg_name => 'a' x ($len - 1), _STATUS => 'DIES' ) } if($len > 0);
							push @cases, { %mandatory_args, ( $arg_name => 'a' x $len ) };
							push @cases, { %mandatory_args, ( $arg_name => 'a' x ($len + 1) ) };
						}
						if (defined $spec->{max}) {
							my $len = $spec->{max};
							push @cases, { %mandatory_args, ( $arg_name => 'a' x ($len - 1) ) };
							push @cases, { %mandatory_args, ( $arg_name => 'a' x $len ) };
							push @cases, { %mandatory_args, ( $arg_name => 'a' x ($len + 1), _STATUS => 'DIES' ) };
						}
					}
				}
				elsif ($type eq 'boolean') {
					push @cases, { %mandatory_args, ( $arg_name => 0 ) };
					push @cases, { %mandatory_args, ( $arg_name => 1 ) };
					push @cases, { %mandatory_args, ( $arg_name => 'true' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'false' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'off' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'on' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'yes' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'no' ) };
					push @cases, { %mandatory_args, ( $arg_name => 'bletch', _STATUS => 'DIES' ) };
					push @cases, { %mandatory_args, ( $arg_name => -1, _STATUS => 'DIES' ) };
					push @cases, { %mandatory_args, ( $arg_name => 2, _STATUS => 'DIES' ) };
				}
				elsif ($type eq 'hashref') {
					push @cases, { $arg_name => { a => 1 } };
					push @cases, { $arg_name => [], _STATUS => 'DIES' };
				}
				elsif ($type eq 'arrayref') {
					push @cases, { $arg_name => [1,2] };
					push @cases, { $arg_name => { a => 1 }, _STATUS => 'DIES' };
				}

				# --- matches (regex) ---
				if (defined $spec->{matches}) {
					my $regex = $spec->{matches};
					for my $string(@regex_tests) {
						if($string =~ $regex) {
							push @cases, { %mandatory_args, ( $arg_name => $string ) };
						} else {
							push @cases, { %mandatory_args, ( $arg_name => $string, _STATUS => 'DIES' ) };
						}
					}
				}

				# --- nomatch (regex) ---
				if (defined $spec->{nomatch}) {
					my $regex = $spec->{nomatch};
					for my $string(@regex_tests) {
						if($string =~ $regex) {
							push @cases, { %mandatory_args, ( $arg_name => $string, _STATUS => 'DIES' ) };
						} else {
							push @cases, { %mandatory_args, ( $arg_name => $string ) };
						}
					}
				}

				# --- memberof ---
				if (defined $spec->{memberof}) {
					my @set = @{ $spec->{memberof} };
					push @cases, { %mandatory_args, ( $arg_name => $set[0] ) } if @set;
					push @cases, { %mandatory_args, ( $arg_name => '_not_in_set_', _STATUS => 'DIES' ) };
				}

				# --- notmemberof ---
				if (defined $spec->{notmemberof}) {
					my @set = @{ $spec->{notmemberof} };
					push @cases, { %mandatory_args, ( $arg_name => $set[0], _STATUS => 'DIES' ) } if @set;
					push @cases, { %mandatory_args, ( $arg_name => '_not_in_set_' ) };
				}
			}
		}
	}

	# Optional deduplication
	# my %seen;
	# @cases = grep { !$seen{join '|', %$_}++ } @cases;

	# Random data test cases
	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') {
					$case_input = rand_int() + $input{'min'};
				} 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' } );
			foreach my $field (keys %input) {
				my $spec = $input{$field} || {};
				foreach my $field(keys %{$spec}) {
					if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'position', 'semantic'))) {
						diag(__LINE__, ": TODO: handle schema keyword '$field'");
					}
				}
			}
			for (1..[% iterations_code %]) {
				my %case_input = (%mandatory_args);
				foreach my $field (keys %input) {
					my $spec = $input{$field} || {};
					next if $spec->{'memberof'};	# Memberof data is created below
					my $type = $spec->{type} || 'string';

					# 1) Sometimes pick a field-specific edge-case
					if (exists $edge_cases{$field} && rand() < PROB_EDGE_CASE) {
						$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});
						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'");
							}
						} else {
							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 {
								$case_input{$field} = rand_int() + $min;
							}
						} elsif(exists($spec->{min})) {
							# min == 0
							if(my $max = $spec->{'max'}) {
								$case_input{$field} = int(rand($max + 1));
							} else {
								$case_input{$field} = abs(rand_int());
							}
						} else {
							$case_input{$field} = rand_int();
						}
					}
					elsif ($type eq 'boolean') {
						$case_input{$field} = rand_bool();
					}
					elsif ($type eq 'number') {
						if(my $min = $spec->{min}) {
							$case_input{$field} = rand_num() + $min;
						} else {
							$case_input{$field} = rand_num();
						}
					}
					elsif ($type eq 'arrayref') {
						$case_input{$field} = rand_arrayref();
					}
					elsif ($type eq 'hashref') {
						$case_input{$field} = rand_hashref();
					} elsif($config{'test_undef'}) {
						$case_input{$field} = undef;
					}

					# 4) occasionally drop optional fields
					if ($spec->{optional} && rand() < 0.25) {
						delete $case_input{$field};
					}
				}
				push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
			}
		}
	}

	# edge-cases
	if($config{'test_undef'}) {
		if($all_optional) {
			push @cases, {};
		} 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, { };	# 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
	push @cases, { map { $_ => $mandatory_strings{$_} } keys %input, %mandatory_objects } if($config{'test_undef'});

	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
	# TODO: For hashref and arrayref, if there's a $spec->{schema} field, use that for the data that's being generated
	if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
		# our %input = ( type => 'string' );
		my $type = $input{type};
		if (exists $input{memberof} && ref $input{memberof} eq 'ARRAY' && @{$input{memberof}}) {
			# Generate edge cases for memberof inside values
			foreach my $val (@{$input{memberof}}) {
				push @cases, { _input => $val };
			}
			# outside value
			my $outside;
			if(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) {
				$outside = (sort { $a <=> $b } @{$input{memberof}})[-1] + 1;
			} else {
				$outside = 'INVALID_MEMBEROF';
			}
			push @cases, { _input => $outside, _STATUS => 'DIES' };
		} else {
			# Generate edge cases for min/max
			if($type eq 'integer') {
				push @cases, @{_generate_integer_cases('_input', \%input, \%mandatory_args)};
			} elsif(($type eq 'number') || ($type eq 'float')) {
				push @cases, @{_generate_float_cases('_input', \%input, \%mandatory_args)};
			} elsif ($type eq 'string') {
				if (defined $input{min}) {
					my $len = $input{min};
					push @cases, { _input => 'a' x ($len + 1) };	# just inside
					if($len == 0) {
						push @cases, { _input => '' } if($config{'test_empty'});
					} else {
						# outside
						push @cases, { _input => 'a' x $len };	# border
						push @cases, { _input => 'a' x ($len - 1), _STATUS => 'DIES' };
					}
					if($len >= 1) {
						# Test checking of 'defined'/'exists' rather than if($string)
						push @cases, { %mandatory_args, ( _input => '0', _LINE => __LINE__ ) };
					} else {
						push @cases, { _input => '0', _STATUS => 'DIES' }
					}
				} else {
					push @cases, { _input => '', _LINE => __LINE__ } if($config{'test_empty'});	# No min, empty string should be allowable
				}
				if (defined $input{max}) {
					my $len = $input{max};
					push @cases, { %mandatory_args, ( _input => 'a' x ($len - 1) ) };	# just inside
					push @cases, { %mandatory_args, ( _input => 'a' x $len ) };	# border
					push @cases, { %mandatory_args, ( _input => 'a' x ($len + 1), _STATUS => 'DIES' ) }; # outside
				}
				if(defined $input{matches}) {
					my $re = $input{matches};

					# --- Positive controls ---
					foreach my $val (@candidate_good) {
						if ($val =~ $re) {
							push @cases, { %mandatory_args, ( _input => $val ) };
							last; # one good match is enough
						}
					}

					# --- Negative controls ---
					foreach my $val (@candidate_bad) {
						if(!defined($val)) {
							push @cases, { _input => undef, _STATUS => 'DIES' };
						} elsif ($val !~ $re) {
							push @cases, { _input => $val, _STATUS => 'DIES' };
						}
					}
					push @cases, { _input => undef, _STATUS => 'DIES' } if($config{'test_undef'});
					push @cases, { _input => "\0", _STATUS => 'DIES' } if($config{'test_nuls'});
				}
				if(defined $input{nomatch}) {
					my $re = $input{nomatch};

					# --- Positive controls ---
					foreach my $val (@candidate_good) {
						if ($val !~ $re) {
							push @cases, { %mandatory_args, ( _input => $val ) };
							last; # one good match is enough
						}
					}

					# --- Negative controls ---
					foreach my $val (@candidate_bad) {
						if ($val =~ $re) {
							push @cases, { _input => $val, _STATUS => 'DIES' };
						}
					}
				}
			} elsif ($type eq 'arrayref') {
				if (defined $input{min}) {
					my $len = $input{min};
					push @cases, { _input => [ (1) x ($len + 1) ] };	# just inside
					push @cases, { _input => [ (1) x $len ] };	# border
					push @cases, { _input => [ (1) x ($len - 1) ], _STATUS => 'DIES' } if $len > 0; # outside
				} else {
					push @cases, { _input => [] } if($config{'test_empty'});	# No min, empty array should be allowable
				}
				if (defined $input{max}) {
					my $len = $input{max};
					push @cases, { _input => [ (1) x ($len - 1) ] };	# just inside
					push @cases, { _input => [ (1) x $len ] };	# border
					push @cases, { _input => [ (1) x ($len + 1) ], _STATUS => 'DIES' }; # outside
				}
			} elsif ($type eq 'hashref') {
				if (defined $input{min}) {
					my $len = $input{min};
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len + 1) } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len - 1) }, _STATUS => 'DIES' } if $len > 0;
				} else {
					push @cases, { _input => {} } if($config{'test_empty'});	# No min, empty hash should be allowable
				}
				if (defined $input{max}) {
					my $len = $input{max};
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len - 1) } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len + 1) }, _STATUS => 'DIES' };
				}
			} elsif ($type eq 'boolean') {
				if (exists $input{memberof} && ref $input{memberof} eq 'ARRAY') {
					# memberof already defines allowed booleans
					foreach my $val (@{$input{memberof}}) {
						push @cases, { _input => $val };
					}
				} else {
					# basic boolean edge cases
					push @cases,
						{ _input => 0 },
						{ _input => 1 },
						{ _input => 'off' },
						{ _input => 'on' },
						{ _input => 'false' },
						{ _input => 'true' },
						{ _input => 'yes' },
						{ _input => 'no' },
						{ _input => 2, _STATUS => 'DIES' },	# invalid boolean
						{ _input => -1, _STATUS => 'DIES' },	# invalid boolean
						{ _input => [ 3 ], _STATUS => 'DIES' },	# invalid boolean
						{ _input => { 'abc' => 'xyz' }, _STATUS => 'DIES' },	# invalid boolean
						{ _input => 'plugh', _STATUS => 'DIES' };	# invalid boolean
					push @cases, { _input => undef, _STATUS => 'DIES' } if($config{'test_undef'});
				}
			}

			# Test all edge cases
			foreach my $edge(@edge_case_array) {
				push @cases, { _input => $edge };
			}
		}
	} else {
		# our %input = ( str => { type => 'string' } );
		push @cases, @{generate_tests(\%input, \%mandatory_args)};
	}

	if($config{'dedup'}) {
		return _dedup_cases(\@cases);
	}

	# use Data::Dumper;
	# die(Dumper(@cases));

	return \@cases;
}

# Functions to generate test cases
sub _generate_integer_cases {
	my ($arg_name, $spec, $mandatory_args) = @_;
	my @cases;

	if((!defined $spec->{min}) || ($spec->{min} <= -1)) {
		push @cases, { %{$mandatory_args}, ( $arg_name => -1, _LINE => __LINE__ ) };
	}
	if((!defined $spec->{min}) || ($spec->{min} <= 42)) {
		push @cases, { %{$mandatory_args}, ( $arg_name => 42 ) };
	}

	[% IF module %]
		# Send wrong data type - builtins aren't good at checking this
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => "test string in integer field $arg_name", _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => 3.14, _STATUS => 'DIES' ) },	# Float
			{ %{$mandatory_args}, ( $arg_name => 'xyz', _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 - 1, _STATUS => 'DIES' ) },
			{ %{$mandatory_args}, ( $arg_name => $min, _LINE => __LINE__ ) },	# border
			{ %{$mandatory_args}, ( $arg_name => $min + 1 ) };	# just inside

		if(!defined $spec->{max}) {
			push @cases, { %{$mandatory_args}, ( $arg_name => $min + rand_int() ) };
			if($min == 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_int()) ) };	# Any positive integer
			}
		}
	}
	if (defined $spec->{max}) {
		my $max = $spec->{max};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $max - 1 ) },
			{ %{$mandatory_args}, ( $arg_name => $max ) },
			{ %{$mandatory_args}, ( $arg_name => $max + 1, _STATUS => 'DIES' ) };

		if(defined $spec->{min}) {
			# Test 0 if it's in range
			push @cases, { %{$mandatory_args}, ( $arg_name => 0 ) } if($spec->{'min'} >= 0);
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => $max - rand_int() ) };
			if($max == 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_int()) * -1 ) };	# Any negative integer
			}
		}
	} elsif(!defined $spec->{min}) {
		# Can take any number, so give it one
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => rand_int() ) },
			{ %{$mandatory_args}, ( $arg_name => 0) };	# 0 is in range
	}

	return \@cases;
}

sub _generate_float_cases {
	my ($arg_name, $spec, $mandatory_args) = @_;
	my @cases;

	if((!defined $spec->{min}) || ($spec->{min} <= -0.1)) {
		push @cases, { %{$mandatory_args}, ( $arg_name => -0.1, _LINE => __LINE__ ) };
	}
	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 integer field $arg_name", _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => 'abc', _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' ) },
			{ %{$mandatory_args}, ( $arg_name => $min, _LINE => __LINE__ ) },	# border
			{ %{$mandatory_args}, ( $arg_name => $min + 0.001 ) };	# just inside

		if(!defined $spec->{max}) {
			push @cases, { %{$mandatory_args}, ( $arg_name => $min + rand_num() ) };
			if($min == 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_num()) ) };	# Any positive number
			}
		}
	}
	if (defined $spec->{max}) {
		my $max = $spec->{max};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $max - 0.000001 ) },
			{ %{$mandatory_args}, ( $arg_name => $max ) },
			{ %{$mandatory_args}, ( $arg_name => $max + 0.000001, _STATUS => 'DIES' ) };

		if(defined $spec->{min}) {
			# Test 0 if it's in range
			push @cases, { %{$mandatory_args}, ( $arg_name => 0 ) } if($spec->{'min'} >= 0);
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => $max - rand_num() ) };
			if($max == 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_num()) * -0.00000001 ) };	# Any negative number
			}
		}
	} elsif(!defined $spec->{min}) {
		# Can take any number, so give it some
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => rand_num() ) },
			{ %{$mandatory_args}, ( $arg_name => 1.23 ) },
			{ %{$mandatory_args}, ( $arg_name => -42.1 ) },
			{ %{$mandatory_args}, ( $arg_name => 0) };	# 0 is in range
	}

	return \@cases;
}

# dedup, fuzzing can easily generate repeats
# FIXME: I don't think this catches them all
# FIXME: Handle cases with Class::Simple calls
sub _dedup_cases
{
	my $cases = shift;

	require JSON::MaybeXS;
	JSON::MaybeXS->import();

	my %seen;
	my @rc = grep {
		my $dump = encode_json($_);
		!$seen{$dump}++
	} @{$cases};

	return \@rc;
}

sub generate_tests
{
	my $input = $_[0];
	my %mandatory_args = %{$_[1]};

	my @cases;

	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		my $type = $spec->{type} || 'string';

		if (exists $spec->{memberof} && ref $spec->{memberof} eq 'ARRAY' && @{$spec->{memberof}}) {
			# Generate edge cases for memberof
			# inside values
			foreach my $val (@{$spec->{memberof}}) {
				push @cases, { %mandatory_args, ( $field => $val ) };
			}
			# outside value
			my $outside;
			if ($type eq 'integer' || $type eq 'number') {
				$outside = (sort { $a <=> $b } @{$spec->{memberof}})[-1] + 1;
			} else {
				$outside = 'INVALID_MEMBEROF';
			}
			push @cases, { %mandatory_args, ( $field => $outside, _STATUS => 'DIES' ) };
		} else {
			# Generate edge cases for min/max
			if($type eq 'integer') {
				push @cases, @{_generate_integer_cases($field, $spec, \%mandatory_args)};
			} elsif(($type eq 'number') || ($type eq 'float')) {
				push @cases, @{_generate_float_cases($field, $spec, \%mandatory_args)};
			} elsif($type eq 'string') {
				if (defined $spec->{min}) {
					my $len = $spec->{min};
					if(my $re = $spec->{matches}) {
						for my $count ($len + 1, $len, $len - 1) {
							next if ($count < 0);
							my $str = rand_str($count);
							if($str =~ $re) {
								push @cases, { %mandatory_args, ( $field => $str ) };
							} else {
								push @cases, { %mandatory_args, ( $field => $str, _STATUS => 'DIES' ) };
							}
						}
					} else {
						push @cases, { %mandatory_args, ( $field => 'a' x ($len + 1) ) };	# just inside
						push @cases, { %mandatory_args, ( $field => 'a' x $len ) };	# border
						if($len > 0) {
							if(($len > 1) || $config{'test_empty'}) {
								# outside
								push @cases, { %mandatory_args, ( $field => 'a' x ($len - 1), _STATUS => 'DIES' ) };
							}
							if($len <= 1) {
								push @cases, { %mandatory_args, ( $field => '9' ) };
								push @cases, { %mandatory_args, ( $field => '' ) } if($len == 0);
							}
						} else {
							push @cases, { %mandatory_args, ( $field => '' ) } if($config{'test_empty'});	# min == 0, empty string should be allowable
							# Don't confuse if() with if(defined())
							push @cases, { %mandatory_args, ( $field => '0', _STATUS => 'DIES' ) };
						}
					}
				} else {
					push @cases, { %mandatory_args, ( $field => '' ) } if($config{'test_empty'});	# No min, empty string should be allowable
				}
				if (defined $spec->{max}) {
					my $len = $spec->{max};
					if((!defined($spec->{min})) || ($spec->{min} != $len)) {
						if(my $re = $spec->{matches}) {
							for my $count ($len - 1, $len, $len + 1) {
								my $str = rand_str($count);
								if($str =~ $re) {
									if($count > $len) {
										push @cases, { %mandatory_args, ( $field => $str, _LINE => __LINE__, _STATUS => 'DIES' ) };
									} else {
										push @cases, { %mandatory_args, ( $field => $str, _LINE => __LINE__ ) };
									}
								} else {
									push @cases, { %mandatory_args, ( $field => $str, _STATUS => 'DIES', _LINE => __LINE__ ) };
								}
							}
						} else {
							push @cases, { %mandatory_args, ( $field => 'a' x ($len - 1), _LINE => __LINE__ ) };	# just inside
							push @cases, { %mandatory_args, ( $field => 'a' x $len, _LINE => __LINE__ ) };	# border
							push @cases, { %mandatory_args, ( $field => 'a' x ($len + 1), _LINE => __LINE__, _STATUS => 'DIES' ) }; # outside
						}
					}
				} else {
					if(exists($spec->{'min'})) {
						push @cases, { %mandatory_args, ( $field => 'a' x (($spec->{'min'} + 1) * 1_000), _LINE => __LINE__ ) };
					} else {
						push @cases, { %mandatory_args, ( $field => 'a' x 10_000, _LINE => __LINE__ ) };
					}
				}

				if(defined $spec->{matches}) {
					my $re = $spec->{matches};

					# --- Positive controls ---
					foreach my $val (@candidate_good) {
						if ($val =~ $re) {
							push @cases, { %mandatory_args, ( $field => $val ) };
							last; # one good match is enough
						}
					}

					# --- Negative controls ---
					foreach my $val (@candidate_bad) {
						if(!defined($val)) {
							push @cases, { _input => undef, _STATUS => 'DIES' } if($config{'test_undef'});
						} elsif ($val !~ $re) {
							push @cases, { _input => $val, _STATUS => 'DIES' };
						}
					}
					push @cases, { $field => undef, _STATUS => 'DIES' } if($config{'test_undef'});
					push @cases, { $field => "\0", _STATUS => 'DIES' } if($config{'test_nuls'});
				}
				if(defined $spec->{nomatch}) {
					my $re = $spec->{nomatch};

					# --- Positive controls ---
					foreach my $val (@candidate_good) {
						if ($val !~ $re) {
							push @cases, { %mandatory_args, ( $field => $val ) };
							last; # one good match is enough
						}
					}

					# --- Negative controls ---
					foreach my $val (@candidate_bad) {
						if ($val =~ $re) {
							push @cases, { $field => $val, _STATUS => 'DIES' };
						}
					}
				}
				# Send wrong data type
				push @cases, { %mandatory_args, ( $field => [], _STATUS => 'DIES', _LINE => __LINE__ ) } if($config{'test_empty'});
				push @cases, { %mandatory_args, ( $field => {}, _STATUS => 'DIES', _LINE => __LINE__ ) } if($config{'test_empty'});
			} elsif ($type eq 'arrayref') {
				if (defined $spec->{min}) {
					my $len = $spec->{min};
					push @cases, { $field => [ (1) x ($len + 1) ] };	# just inside
					push @cases, { $field => [ (1) x $len ] };	# border
					push @cases, { $field => [ (1) x ($len - 1) ], _STATUS => 'DIES' } if $len > 0; # outside
				} else {
					push @cases, { $field => [] } if($config{'test_empty'});	# No min, empty array should be allowable
				}
				if (defined $spec->{max}) {
					my $len = $spec->{max};
					push @cases, { $field => [ (1) x ($len - 1) ] };	# just inside
					push @cases, { $field => [ (1) x $len ] };	# border
					push @cases, { $field => [ (1) x ($len + 1) ], _STATUS => 'DIES' }; # outside
				}
			} elsif ($type eq 'hashref') {
				if (defined $spec->{min}) {
					my $len = $spec->{min};
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. ($len + 1) } };
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. ($len - 1) }, _STATUS => 'DIES' } if $len > 0;
				} else {
					push @cases, { $field => {} } if($config{'test_empty'});	# No min, empty hash should be allowable
				}
				if (defined $spec->{max}) {
					my $len = $spec->{max};
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. ($len - 1) } };
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { $field => { map { "k$_" => 1 }, 1 .. ($len + 1) }, _STATUS => 'DIES' };
				}
			} elsif ($type eq 'boolean') {
				if (exists $spec->{memberof} && ref $spec->{memberof} eq 'ARRAY') {
					# memberof already defines allowed booleans
					foreach my $val (@{$spec->{memberof}}) {
						push @cases, { %mandatory_args, ( $field => $val ) };
					}
				} else {
					# basic boolean edge cases
					push @cases,
						{ %mandatory_args, ( $field => 0 ) },
						{ %mandatory_args, ( $field => 1 ) },
						{ %mandatory_args, ( $field => 'false' ) },
						{ %mandatory_args, ( $field => 'true' ) },
						{ %mandatory_args, ( $field => 'off' ) },
						{ %mandatory_args, ( $field => 'on' ) },
						{ %mandatory_args, ( $field => 'yes' ) },
						{ %mandatory_args, ( $field => 'no' ) },
						{ %mandatory_args, ( $field => 2, _STATUS => 'DIES' ) },	# invalid boolean
						{ %mandatory_args, ( $field => -1, _STATUS => 'DIES' ) },	# invalid boolean
						{ %mandatory_args, ( $field => 'xyzzy', _STATUS => 'DIES' ) };	# invalid boolean

					push @cases, { %mandatory_args, ( $field => undef, _STATUS => 'DIES' ) } if($config{'test_undef'});
					push @cases, { %mandatory_args, ( $field => '', _STATUS => 'DIES' ) } if($config{'test_empty'});
				}
			}
		}

		# case_sensitive tests for memberof
		if (defined $spec->{memberof} && exists $spec->{case_sensitive}) {
			if (!$spec->{case_sensitive}) {
				# Generate mixed-case versions of memberof values
				foreach my $val (@{$spec->{memberof}}) {
					push @cases, { %mandatory_args, ( $field => uc($val) ) },
						{ %mandatory_args, ( $field => lc($val) ) },
						{ %mandatory_args, ( $field => ucfirst(lc($val)) ) };
				}
			}
		}

		# Add notmemberof tests
		if (defined $spec->{notmemberof}) {
			my @blacklist = @{$spec->{notmemberof}};
			# Each blacklisted value should die
			foreach my $val (@blacklist) {
				push @cases, { %mandatory_args, ( $field => $val, _STATUS => 'DIES' ) };
			}
			# Non-blacklisted value should pass
			push @cases, { %mandatory_args, ( $field => '_not_in_blacklist_' ) };
		}

		# TODO:	How do we generate tests for cross-field validation?
	}

	return \@cases;
}

sub populate_positions
{
	my $input = shift;

	my $rc;
	foreach my $arg (keys %{$input}) {
		my $spec = $input->{$arg} || {};
		if(((ref($spec)) eq 'HASH') && defined($spec->{'position'})) {
			$rc->{$arg} = $spec->{'position'};
		} else {
			if($rc) {
				::diag("$arg is missing a position parameter in its schema");
			}
			return;	# All must be defined
		}
	}

	return $rc;
}

sub run_test
{
	my($case, $input, $output, $positions) = @_;

	if($ENV{'TEST_VERBOSE'}) {
		diag('input: ', Dumper($input));
	}

	my $name = delete local $case->{'_NAME'};
	my $result;
	my $mess;
	if(defined($input) && !ref($input)) {
		if($name) {
			$mess = "[% function %]($name = '$input') %s";
		} else {
			$mess = "[% function %]('$input') %s";
		}
	} elsif(defined($input)) {
		my @alist = ();
		if($positions) {
			# Positional args
			foreach my $key (keys %{$input}) {
				if(($key ne '_STATUS') && ($key ne '_NAME')) {
					if(exists($positions->{$key})) {
						$alist[$positions->{$key}] = delete $input->{$key};
					} else {
						diag("Lost position number for $key");
					}
				}
			}
			@alist = grep { defined $_ } @alist;	# Undefs will cause not enough args to be sent, which is a nice test
			$input = join(', ', @alist);
		} else {
			# Named args
			foreach my $key (sort keys %{$input}) {
				if($key ne '_STATUS') {
					if(defined($input->{$key})) {
						push @alist, "'$key' => '$input->{$key}'";
					} else {
						push @alist, "'$key' => undef";
					}
				}
			}
		}
		my $args = join(', ', @alist);
		$args =~ s/%/%%/g;
		$mess = "[% function %]($args) %s";
	} else {
		$mess = "[% function %] %s";
	}

	if(my $status = (delete $case->{'_STATUS'} || $output->{'_STATUS'})) {
		if($status eq 'DIES') {
			dies_ok { [% call_code %] } sprintf($mess, 'dies');
			ok(!defined($result));
			return;	# There should be no output to validate
		} elsif($status eq 'WARNS') {
			warnings_exist { [% call_code %] } qr/./, sprintf($mess, 'warns');
		} else {
			lives_ok { [% call_code %] } sprintf($mess, 'survives');
		}
	} else {
		lives_ok { [% call_code %] } sprintf($mess, 'survives');
	}

	delete local $output->{'_STATUS'};

	if(scalar keys %{$output}) {
		if($ENV{'TEST_VERBOSE'}) {
			diag('result: ', Dumper($result));
		}
		returns_ok($result, $output, 'output validates');
	}
}

my $positions = populate_positions(\%input);

diag('Run Fuzz Tests') if($ENV{'TEST_VERBOSE'});

foreach my $case (@{fuzz_inputs()}) {
	# my %params;
	# lives_ok { %params = get_params(\%input, %$case) } 'Params::Get input check';
	# lives_ok { validate_strict(\%input, %params) } 'Params::Validate::Strict input check';

	my $input;
	if((ref($case) eq 'HASH') && exists($case->{'_input'})) {
		$input = $case->{'_input'};
	} else {
		$input = $case;
	}

	if(my $line = (delete $case->{'_LINE'} || delete $input{'_LINE'})) {
		diag("Test case from line number $line") if($ENV{'TEST_VERBOSE'});
	}

	{
		# local %ENV;
		run_test($case, $input, \%output, $positions);
		# delete $ENV{'LANG'};
		# delete $ENV{'LC_ALL'};
		# run_test($case, $input, \%output, $positions);
		# $ENV{'LANG'} = 'fr_FR.utf8';
		# $ENV{'LC_ALL'} = 'fr_FR.utf8';
		# run_test($case, $input, \%output, $positions);
	}
}

diag('Run ', scalar(keys %transforms), ' transform tests') if($ENV{'TEST_VERBOSE'});
# diag('-' x 60);

# Build the foundation - which is a basic test with sensible defaults in the field
foreach my $transform (keys %transforms) {
	my $foundation;	# basic set of data with every field filled in with a sensible default value

	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		my $type = $spec->{type} || 'string';

		if(($type eq 'number') || ($type eq 'float')) {
			if(defined $spec->{min}) {
				if(defined $spec->{max}) {
					$foundation->{$field} = $spec->{max};	# border
				} else {
					$foundation->{$field} = rand_num() + $spec->{'min'};
				}
			} else {
				if(defined $spec->{max}) {
					$foundation->{$field} = $spec->{max};	# border
				} else {
					$foundation->{$field} = -0.1;	# No min, so -0.1 should be allowable
				}
			}
		} elsif($type eq 'string') {
			if(defined $spec->{min} && $spec->{min} > 0) {
				$foundation->{$field} = 'a' x $spec->{min};
			} elsif(defined $spec->{max} && $spec->{max} > 0) {
				$foundation->{$field} = 'b' x $spec->{max};
			} else {
				$foundation->{$field} = 'test_value';
			}
		} elsif ($type eq 'integer') {
			if (defined $spec->{min}) {
				$foundation->{$field} = $spec->{min};
			} elsif (defined $spec->{max}) {
				$foundation->{$field} = rand_int() + $spec->{max};
			} else {
				$foundation->{$field} = rand_int();
			}
		} elsif ($type eq 'boolean') {
			$foundation->{$field} = 1;
		} elsif ($type eq 'arrayref') {
			$foundation->{$field} = rand_arrayref(defined($spec->{'min'}) ? $spec->{'min'} : ($spec->{'max'} // 5));
		} elsif ($type eq 'hashref') {
			$foundation->{$field} = { key => 'value' };
		} else {
			die("TODO: transform type $type for foundation");
		}
	}

	# The foundation should work
	my $case = { _NAME => "basic $transform test", _LINE => __LINE__ };
	my $positions = populate_positions(\%input);
	run_test($case, $foundation, \%output, $positions);

	# Generate transform tests
	# Don't generate invalid data, that's all already done,
	#	this is about verifying the transorms
	my @tests;
	diag("tests for transform $transform") if($ENV{'TEST_VERBOSE'});

	# Now modify the foundation with test code

	# BUILD CODE TO CALL FUNCTION
	# CALL FUNCTION
	# CHECK STATUS CORRECT
	# IF STATUS EQ LIVES
	#   CHECK OUTPUT USING returns_ok
	# FI

	my $transform_input = $transforms{$transform}{'input'} || {};

	foreach my $field (keys %input) {
		my $spec = $transform_input->{$field} || {};
		my $type = $spec->{type} || 'string';

		# If there's a specific value, test that exact value
		if (exists $spec->{value}) {
			push @tests, {
				%{$foundation},
				$field => $spec->{value},
				_LINE => __LINE__
				# _DESCRIPTION => "$transform_name: $field=$spec->{value}"
			};
			next;
		}

		# Generate edge cases based on type and contraints
		if($type eq 'integer') {
			push @tests, @{_generate_integer_cases($field, $spec, $foundation)};
		} elsif(($type eq 'number') || ($type eq 'float')) {
			push @tests, @{_generate_float_cases($field, $spec, $foundation)};
		} elsif($type eq 'string') {
			if(defined $spec->{min}) {
				push @tests, { %{$foundation}, ( $field => rand_str($spec->{min} + 1) ) };	# just inside
				push @tests, { %{$foundation}, ( $field => rand_str($spec->{min}) ) };	# border
			} else {
				push @tests, { %{$foundation}, ( $field => rand_str() ) };
			}
			if(defined $spec->{max}) {
				push @tests, { %{$foundation}, ( $field => rand_str($spec->{max} - 1) ) };	# just inside
				if((defined $spec->{min}) && ($spec->{'min'} != $spec->{'max'})) {
					push @tests, { %{$foundation}, ( $field => rand_str($spec->{max}) ) };	# border
				}
			}
		} elsif($type eq 'boolean') {
			push @tests, { %{$foundation}, ( $field => 1 ) }, { %{$foundation}, ( $field => 0 ) };
		} elsif ($type eq 'arrayref') {
			if(defined $spec->{min}) {
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{min} + 1) ) };	# just inside
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{min}) ) };	# border
			} else {
				push @tests, { %{$foundation}, ( $field => rand_arrayref() ) };
			}
			if(defined $spec->{max}) {
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{max} - 1) ) };	# just inside
				if((defined $spec->{min}) && ($spec->{'min'} != $spec->{'max'})) {
					push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{max}) ) };	# border
				}
			}
		} else {
			die("TODO: transform type $type for test case");
		}
	}

	if($config{'dedup'}) {
		@tests = @{_dedup_cases(\@tests)};
	}

	{
		# local %ENV;
		my $transform_output = $transforms{$transform}{'output'} || {};
		foreach my $test(@tests) {
			if(my $line = (delete $test->{'_LINE'} || delete $input{'_LINE'})) {
				diag("Test case from line number $line") if($ENV{'TEST_VERBOSE'});
			}
			run_test({ _NAME => $transform }, $test, $transform_output, $positions);
			# delete $ENV{'LANG'};
			# delete $ENV{'LC_ALL'};
			# run_test({ _NAME => $transform }, $test, \%output, $positions);
			# $ENV{'LANG'} = 'de_DE.utf8';
			# $ENV{'LC_ALL'} = 'de_DE.utf8';
			# run_test({ _NAME => $transform }, $test, \%output, $positions);
		}
	}
}

[% 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();
__END__


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.