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 )