App-Test-Generator
view release on metacpan or search on metacpan
t/function.t view on Meta::CPAN
# Single key-value pair is rendered correctly
my $result = $fn->({ type => 'string' });
like($result, qr/'type'\s*=>\s*'string'/, 'single key rendered');
# Multiple keys are sorted alphabetically for deterministic output
my $multi = $fn->({ b => 2, a => 1 });
my $a_pos = index($multi, "'a'");
my $b_pos = index($multi, "'b'");
ok($a_pos < $b_pos, 'keys sorted alphabetically');
# Numeric values are rendered unquoted
my $num = $fn->({ min => 1, max => 10 });
like($num, qr/'min'\s*=>\s*1\b/, 'numeric value unquoted');
# Regexp value is rendered as qr{} not a raw string
my $re_result = $fn->({ matches => qr/foo/ });
like($re_result, qr/qr\{foo\}/, 'Regexp rendered as qr{}');
done_testing();
};
# ==================================================================
# render_hash
# --------------------------------------------------
# Tests for the two-level hash renderer used for
# the %input specification in generated tests.
# Note: render_hash emits unquoted sub-keys (type =>)
# because it delegates sub-key rendering to
# render_args_hash which does not quote key names.
# ==================================================================
subtest 'render_hash' => sub {
my $fn = \&App::Test::Generator::render_hash;
# Undef and non-hash inputs return empty string
is($fn->(undef), $EMPTY_STRING, 'undef returns empty string');
is($fn->(42), $EMPTY_STRING, 'scalar returns empty string');
is($fn->({}), $EMPTY_STRING, 'empty hash returns empty string');
# Standard two-level hash renders the top-level key quoted
# but sub-keys are rendered unquoted by render_args_hash
my $result = $fn->({ name => { type => 'string', optional => 0 } });
like($result, qr/'name'\s*=>\s*\{/, 'top-level key rendered quoted');
like($result, qr/type\s*=>\s*'string'/, 'sub-key type rendered unquoted');
like($result, qr/optional\s*=>\s*0/, 'sub-key optional rendered unquoted');
# Scalar type shorthand is expanded to a full spec hashref
# so it renders as a proper { type => '...' } block
my $shorthand = $fn->({ arg => 'string' });
like($shorthand, qr/'arg'\s*=>\s*\{/, 'shorthand expanded to hashref');
like($shorthand, qr/type\s*=>\s*'string'/, 'shorthand type present unquoted');
# matches pattern is compiled to Regexp before rendering
# so it appears as qr{} not a raw string in the output
my $re_result = $fn->({ field => { type => 'string', matches => 'foo' } });
like($re_result, qr/qr\{/, 'matches compiled to qr{}');
# Unknown type shorthand produces a warning, not a croak
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
$fn->({ x => 'not_a_type' });
ok(@warnings, 'unknown type shorthand produces warning');
done_testing();
};
# ==================================================================
# render_arrayref_map
# --------------------------------------------------
# Tests for the hash-of-arrayrefs renderer used for
# edge_cases and type_edge_cases.
# Note: undef returns '()' but an empty hash returns ''
# â these are distinct cases with different semantics.
# ==================================================================
subtest 'render_arrayref_map' => sub {
my $fn = \&App::Test::Generator::render_arrayref_map;
# Undef returns '()' so the generated test gets a valid
# empty hash literal rather than a syntax error
is($fn->(undef), '()', 'undef returns ()');
# Empty hash returns empty string â distinct from the undef case
is($fn->({}), $EMPTY_STRING, 'empty hash returns empty string');
# Standard case renders the key with its arrayref correctly
my $result = $fn->({ name => ['', 'x' x 10] });
like($result, qr/'name'\s*=>\s*\[/, 'key rendered with arrayref');
like($result, qr/''/, 'empty string element rendered');
# Non-arrayref values are silently skipped
my $mixed = $fn->({ arr => [1, 2], scalar => 'foo' });
like($mixed, qr/'arr'/, 'arrayref value included');
unlike($mixed, qr/'scalar'/, 'scalar value skipped');
# Keys are sorted alphabetically for deterministic output
my $sorted = $fn->({ b => [2], a => [1] });
ok(index($sorted, "'a'") < index($sorted, "'b'"), 'keys sorted');
done_testing();
};
# ==================================================================
# _valid_type
# --------------------------------------------------
# Tests for the type string validator
# ==================================================================
subtest '_valid_type' => sub {
my $fn = \&App::Test::Generator::_valid_type;
# Undef is never a valid type
ok(!$fn->(undef), 'undef is not valid');
# All documented base types must be accepted
for my $type (qw(string boolean integer number float hashref arrayref object)) {
ok($fn->($type), "$type is valid");
}
# Short aliases must be accepted for compatibility with external
# tools that use the shorter forms
ok($fn->('int'), 'int alias accepted');
ok($fn->('bool'), 'bool alias accepted');
t/function.t view on Meta::CPAN
done_testing();
};
# ==================================================================
# _has_positions
# --------------------------------------------------
# Tests for the positional argument detector
# ==================================================================
subtest '_has_positions' => sub {
my $fn = \&App::Test::Generator::_has_positions;
# Undef and non-hash inputs return 0 without throwing
is($fn->(undef), 0, 'undef returns 0');
is($fn->(42), 0, 'scalar returns 0');
is($fn->({}), 0, 'empty hash returns 0');
# Hash with no position keys returns 0
my $no_pos = { a => { type => 'string' } };
is($fn->($no_pos), 0, 'no position keys returns 0');
# Hash with a position key returns 1
my $with_pos = { a => { type => 'string', position => 0 } };
is($fn->($with_pos), 1, 'position key found returns 1');
# Mixed hash â any single position key is enough to return 1
my $mixed = {
a => { type => 'string', position => 0 },
b => { type => 'integer' },
};
is($fn->($mixed), 1, 'mixed: any position key returns 1');
# Scalar spec values (shorthand) cannot carry positions
my $scalar_spec = { a => 'string' };
is($fn->($scalar_spec), 0, 'scalar spec has no position');
done_testing();
};
# ==================================================================
# _load_schema_section
# --------------------------------------------------
# Tests for the safe section extractor
# ==================================================================
subtest '_load_schema_section' => sub {
my $fn = \&App::Test::Generator::_load_schema_section;
# Missing section returns empty hashref as the safe default
my $schema = {};
is_deeply($fn->($schema, 'input', 'test.yml'), {}, 'missing section returns {}');
# Present hashref section is returned directly without copying
my $input = { name => { type => 'string' } };
$schema = { input => $input };
is_deeply($fn->($schema, 'input', 'test.yml'), $input, 'hashref section returned');
# The string 'undef' in a section means the same as absent
$schema = { output => 'undef' };
is_deeply($fn->($schema, 'output', 'test.yml'), {}, "'undef' string treated as absent");
# Wrong type croaks with a message mentioning the section name
$schema = { input => 'not_a_hash' };
throws_ok {
$fn->($schema, 'input', 'test.yml')
} qr/should be a hash/, 'wrong type croaks';
done_testing();
};
# ==================================================================
# _is_numeric_transform and _is_string_transform
# --------------------------------------------------
# Tests for the output type classifiers used by
# _detect_transform_properties
# ==================================================================
subtest '_is_numeric_transform' => sub {
my $fn = \&App::Test::Generator::_is_numeric_transform;
# All three numeric type strings must be detected
for my $type (qw(number integer float)) {
ok($fn->({}, { type => $type }), "$type output detected as numeric");
}
# Non-numeric types return 0
ok(!$fn->({}, { type => 'string' }), 'string not numeric');
ok(!$fn->({}, { type => 'boolean' }), 'boolean not numeric');
ok(!$fn->({}, {}), 'missing type not numeric');
ok(!$fn->({}, undef), 'undef output spec not numeric');
done_testing();
};
subtest '_is_string_transform' => sub {
my $fn = \&App::Test::Generator::_is_string_transform;
# Only the string 'string' is detected as a string transform
ok($fn->({}, { type => 'string' }), 'string output detected');
ok(!$fn->({}, { type => 'integer' }), 'integer not string');
ok(!$fn->({}, {}), 'missing type not string');
ok(!$fn->({}, undef), 'undef output not string');
done_testing();
};
# ==================================================================
# _get_dominant_type
# --------------------------------------------------
# Tests for the type extractor used by _same_type
# and _detect_transform_properties
# ==================================================================
subtest '_get_dominant_type' => sub {
my $fn = \&App::Test::Generator::_get_dominant_type;
# Undef and empty hash both return the default type 'string'
is($fn->(undef), 'string', 'undef returns default string');
is($fn->({}), 'string', 'empty hash returns default string');
# Flat output spec with an explicit type returns it directly
is($fn->({ type => 'integer' }), 'integer', 'flat spec type returned');
# Multi-field input spec returns the type of the first field found
my $multi = { a => { type => 'number' } };
t/function.t view on Meta::CPAN
is($config{timeout}, undef, 'timeout left undef when absent (numeric, not boolean)');
# Common string boolean representations are normalised to integers
my %bool_config = (
test_nuls => 'yes',
test_undef => 'no',
test_empty => 'true',
);
$fn->(\%bool_config);
is($bool_config{test_nuls}, 1, "'yes' normalised to 1");
is($bool_config{test_undef}, 0, "'no' normalised to 0");
is($bool_config{test_empty}, 1, "'true' normalised to 1");
# Properties field is always a hashref after normalisation
my %no_props;
$fn->(\%no_props);
is(ref($no_props{properties}), 'HASH', 'properties always a hashref');
is($no_props{properties}{enable}, 0, 'properties defaults to disabled');
# An existing properties hashref is preserved untouched
my %with_props = (properties => { enable => 1, trials => 500 });
$fn->(\%with_props);
is($with_props{properties}{enable}, 1, 'existing properties enable preserved');
is($with_props{properties}{trials}, 500, 'existing properties trials preserved');
done_testing();
};
# ==================================================================
# _validate_config
# --------------------------------------------------
# Tests for the top-level schema validator.
# Warnings from 'neither input nor output defined' are
# suppressed in tests that are only checking for lives/dies
# on structural issues unrelated to warning behaviour.
# ==================================================================
subtest '_validate_config' => sub {
my $fn = \&App::Test::Generator::_validate_config;
# Missing both module and function croaks with a clear message
throws_ok {
$fn->({})
} qr/At least one of function and module/, 'missing module and function croaks';
# Function alone is sufficient â suppress the no-input/output warning
# since we are testing structural validation not warning behaviour here
lives_ok {
local $SIG{__WARN__} = sub {};
$fn->({ function => 'foo' })
} 'function alone is sufficient';
# Module alone is sufficient â same warning suppression as above
lives_ok {
local $SIG{__WARN__} = sub {};
$fn->({ module => 'Foo' })
} 'module alone is sufficient';
# Invalid input type string croaks with a clear message
throws_ok {
local $SIG{__WARN__} = sub {};
$fn->({ function => 'foo', input => 'not_undef' })
} qr/Invalid input specification/, 'invalid input type croaks';
# The string 'undef' for input is accepted and the key is removed
my $schema = { function => 'foo', input => 'undef' };
lives_ok {
local $SIG{__WARN__} = sub {};
$fn->($schema)
} "'undef' input accepted";
ok(!exists($schema->{input}), "input key removed when set to 'undef'");
# Unknown config key croaks naming the bad key
throws_ok {
$fn->({ function => 'foo', config => { unknown_key => 1 } })
} qr/unknown config setting/, 'unknown config key croaks';
# Neither input nor output defined generates a warning not a croak
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
lives_ok {
$fn->({ function => 'foo' })
} 'missing input and output lives';
ok(@warnings, 'missing input and output warns');
done_testing();
};
# ==================================================================
# _validate_input_positions
# ==================================================================
subtest '_validate_input_positions' => sub {
my $fn = \&App::Test::Generator::_validate_input_positions;
# No positions at all is always valid
lives_ok {
$fn->({ input => { a => { type => 'string' } } })
} 'no positions is valid';
# Contiguous positions starting at 0 are valid
lives_ok {
$fn->({
input => {
a => { type => 'string', position => 0 },
b => { type => 'string', position => 1 },
}
})
} 'valid contiguous positions';
# Duplicate position numbers always croak
throws_ok {
$fn->({
input => {
a => { type => 'string', position => 0 },
b => { type => 'string', position => 0 },
}
})
} qr/Duplicate position/, 'duplicate positions croak';
# If any param has a position, all must have one â missing one croaks
throws_ok {
$fn->({
input => {
a => { type => 'string', position => 0 },
b => { type => 'string' },
}
})
} qr/missing position/, 'partial positions croak';
# A gap in the position sequence warns but does not croak
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
lives_ok {
$fn->({
input => {
a => { type => 'string', position => 0 },
b => { type => 'string', position => 2 },
}
})
} 'position gap lives';
ok(@warnings, 'position gap warns');
done_testing();
};
# ==================================================================
# _validate_input_semantics
# ==================================================================
subtest '_validate_input_semantics' => sub {
my $fn = \&App::Test::Generator::_validate_input_semantics;
# A known semantic type passes without any warnings
lives_ok {
$fn->({ input => { email => { type => 'string', semantic => 'email' } } })
} 'known semantic type lives';
# An unknown semantic type warns but does not croak
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
lives_ok {
$fn->({ input => { x => { type => 'string', semantic => 'nonsense' } } })
} 'unknown semantic lives';
ok(@warnings, 'unknown semantic warns');
# Having both enum and memberof on the same param always croaks
throws_ok {
$fn->({
input => {
x => { type => 'string', enum => ['a'], memberof => ['a'] }
}
})
} qr/both enum and memberof/, 'enum and memberof together croaks';
# memberof value must be an arrayref not a plain scalar
throws_ok {
$fn->({
input => {
x => { type => 'string', memberof => 'not_array' }
}
})
} qr/must be an arrayref/, 'non-arrayref memberof croaks';
done_testing();
};
# ==================================================================
# _detect_transform_properties
# --------------------------------------------------
# Tests for the automatic property detection logic
# ==================================================================
subtest '_detect_transform_properties' => sub {
my $fn = \&App::Test::Generator::_detect_transform_properties;
# Undef input spec always returns an empty list
my @props = $fn->('test', undef, {});
is(scalar @props, 0, 'undef input returns empty list');
# The YAML string 'undef' as input also returns empty list
@props = $fn->('test', 'undef', {});
is(scalar @props, 0, "'undef' input returns empty list");
# Numeric output with min generates a min_constraint property
@props = $fn->('test', { a => { type => 'number' } }, { type => 'number', min => 0 });
my %by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{min_constraint}, 'min generates min_constraint');
like($by_name{min_constraint}{code}, qr/>=\s*0/, 'min_constraint code checks >= 0');
# Numeric output with max generates a max_constraint property
@props = $fn->('test', { a => { type => 'number' } }, { type => 'number', max => 100 });
%by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{max_constraint}, 'max generates max_constraint');
# A transform named 'positive' gets a non_negative property added
@props = $fn->('positive', { a => { type => 'number' } }, { type => 'number' });
%by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{non_negative}, 'positive transform gets non_negative property');
# An exact value in output generates an exact_value property
@props = $fn->('test', { a => { type => 'string' } }, { type => 'string', value => 'foo' });
%by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{exact_value}, 'output value generates exact_value property');
like($by_name{exact_value}{code}, qr/'foo'/, 'exact_value code contains expected value');
# All non-undef outputs get a defined property
@props = $fn->('test', { a => { type => 'string' } }, { type => 'string' });
%by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{defined}, 'non-undef output gets defined property');
# Output type explicitly set to undef must not get a defined property
@props = $fn->('test', { a => { type => 'string' } }, { type => 'undef' });
%by_name = map { $_->{name} => $_ } @props;
ok(!exists $by_name{defined}, 'undef output type gets no defined property');
# String output with min length generates a min_length property
@props = $fn->('test', { a => { type => 'string' } }, { type => 'string', min => 3 });
%by_name = map { $_->{name} => $_ } @props;
ok(exists $by_name{min_length}, 'string min generates min_length');
( run in 0.860 second using v1.01-cache-2.11-cpan-9581c071862 )