App-Test-Generator
view release on metacpan or search on metacpan
t/function.t view on Meta::CPAN
};
}
# Allow access to private helpers via the package namespace
BEGIN {
use_ok('App::Test::Generator');
use_ok('App::Test::Generator::Mutator');
use_ok('App::Test::Generator::Mutant');
use_ok('App::Test::Generator::Exporter::YAML');
use_ok('App::Test::Generator::Analyzer::Return');
use_ok('App::Test::Generator::Analyzer::ReturnMeta');
use_ok('App::Test::Generator::Analyzer::Complexity');
use_ok('App::Test::Generator::Analyzer::SideEffect');
use_ok('App::Test::Generator::Planner::Mock');
use_ok('App::Test::Generator::Planner::Fixture');
use_ok('App::Test::Generator::Planner::Grouping');
use_ok('App::Test::Generator::Planner::Isolation');
use_ok('App::Test::Generator::Planner');
use_ok('App::Test::Generator::LCSAJ::Coverage');
use_ok('App::Test::Generator::LCSAJ');
use_ok('App::Test::Generator::Mutation::Base');
use_ok('App::Test::Generator::Mutation::ConditionalInversion');
use_ok('App::Test::Generator::Mutation::NumericBoundary');
use_ok('App::Test::Generator::Mutation::ReturnUndef');
use_ok('App::Test::Generator::Mutation::BooleanNegation');
use_ok('App::Test::Generator::TestStrategy');
use_ok('App::Test::Generator::Model::Method');
use_ok('App::Test::Generator::Sample::Module');
use_ok('Devel::App::Test::Generator::LCSAJ::Runtime');
use_ok('App::Test::Generator::Emitter::Perl');
use_ok('App::Test::Generator::Template');
use_ok('App::Test::Generator::SchemaExtractor');
}
# --------------------------------------------------
# Constants used across multiple subtests to avoid
# magic literals and make intent clear
# --------------------------------------------------
Readonly my $EMPTY_STRING => '';
Readonly my $UNDEF_LITERAL => 'undef';
# --------------------------------------------------
# Mirrors of the boundary constants private to
# App::Test::Generator::Sample::Module, so its
# boundary tests below avoid magic numbers without
# requiring the module to export its internals
# --------------------------------------------------
Readonly my $SAMPLE_MIN_EMAIL_LEN => 5;
Readonly my $SAMPLE_MAX_EMAIL_LEN => 254;
Readonly my $SAMPLE_MIN_BIRTH_YEAR => 1900;
Readonly my $SAMPLE_MIN_NAME_LEN => 1;
Readonly my $SAMPLE_MAX_NAME_LEN => 50;
Readonly my $SAMPLE_MIN_SCORE => 0.0;
Readonly my $SAMPLE_MAX_SCORE => 100.0;
Readonly my $SAMPLE_PASS_THRESHOLD => 60.0;
# ==================================================================
# perl_sq
# --------------------------------------------------
# White-box tests for the low-level single-quote
# string escaper used by perl_quote and q_wrap
# ==================================================================
subtest 'perl_sq' => sub {
# Access the private function directly via the package namespace
my $fn = \&App::Test::Generator::perl_sq;
# Undef input returns empty string, not 'undef'
is($fn->(undef), $EMPTY_STRING, 'undef returns empty string');
# Plain ASCII string passes through unchanged
is($fn->('hello'), 'hello', 'plain ASCII unchanged');
# Apostrophe must be escaped so it does not break the surrounding
# single-quoted string literal in the generated test
is($fn->("it's"), "it\\'s", 'apostrophe escaped');
# Backslash must be escaped first so later substitutions
# do not double-escape already-escaped sequences
is($fn->('a\\b'), 'a\\\\b', 'backslash escaped');
# Control characters are converted to their two-char sequences
is($fn->("a\nb"), 'a\\nb', 'newline escaped');
is($fn->("a\rb"), 'a\\rb', 'carriage return escaped');
is($fn->("a\tb"), 'a\\tb', 'tab escaped');
is($fn->("a\fb"), 'a\\fb', 'form feed escaped');
# NUL byte is converted to \0 for double-quoted context
is($fn->("a\0b"), 'a\\0b', 'NUL byte escaped');
# Both apostrophe and backslash in the same string
is($fn->("a\\'b"), "a\\\\\\'b", 'backslash and apostrophe together');
done_testing();
};
# ==================================================================
# perl_quote
# --------------------------------------------------
# Tests for the top-level value quoter that produces
# Perl source-code literals for any scalar type
# ==================================================================
subtest 'perl_quote' => sub {
my $fn = \&App::Test::Generator::perl_quote;
# Undef always produces the bare word 'undef'
is($fn->(undef), $UNDEF_LITERAL, 'undef produces undef literal');
# YAML boolean strings must round-trip to Perl boolean constants
is($fn->('true'), '!!1', 'true produces !!1');
is($fn->('false'), '!!0', 'false produces !!0');
# Integers are emitted unquoted for numeric comparison
is($fn->(0), '0', 'zero unquoted');
is($fn->(42), '42', 'positive integer unquoted');
is($fn->(-1), '-1', 'negative integer unquoted');
# Floats are emitted unquoted
is($fn->(3.14), '3.14', 'float unquoted');
# Plain strings are single-quoted
is($fn->('hello'), "'hello'", 'string single-quoted');
# Strings containing apostrophes have them escaped
is($fn->("it's"), "'it\\'s'", 'apostrophe in string escaped');
# Arrayrefs are recursively quoted with brackets
is($fn->([1, 2, 3]), '[ 1, 2, 3 ]', 'arrayref recursively quoted');
# Nested arrayrefs recurse correctly
is($fn->([1, [2, 3]]), '[ 1, [ 2, 3 ] ]', 'nested arrayref quoted');
# Arrayref containing undef produces undef literal in the output
is($fn->([undef, 1]), "[ $UNDEF_LITERAL, 1 ]", 'arrayref with undef element');
# Regexp objects are rendered as qr{} with modifiers
my $re = qr/foo/i;
like($fn->($re), qr/qr\{foo\}i/, 'Regexp rendered as qr{}');
# Regexp without modifiers has no trailing flags
my $re2 = qr/bar/;
like($fn->($re2), qr/qr\{bar\}/, 'Regexp without modifiers');
done_testing();
};
# ==================================================================
# q_wrap
# --------------------------------------------------
# Tests for the string wrapper that chooses the most
# readable q{} delimiter form
# ==================================================================
subtest 'q_wrap' => sub {
my $fn = \&App::Test::Generator::q_wrap;
# Undef returns empty single-quoted string â q_wrap is a
# string quoter, not a value serialiser, so undef means
# no string value rather than the Perl literal 'undef'
is($fn->(undef), "''", 'undef returns empty single-quoted string');
# Plain string uses the preferred q{} bracket form
is($fn->('hello'), 'q{hello}', 'plain string uses q{}');
# String containing { forces a different bracket pair
my $with_brace = 'a{b';
unlike($fn->($with_brace), qr/^q\{/, 'string with { avoids q{}');
# String containing all bracket pairs falls back to single chars
my $all_brackets = '{([<>])}';
my $result = $fn->($all_brackets);
like($result, qr/^q./, 'all-bracket string still uses q form');
# Empty string produces empty q form
is($fn->($EMPTY_STRING), 'q{}', 'empty string produces q{}');
# String with apostrophe â q_wrap avoids needing to escape it
# by choosing a delimiter that is not an apostrophe
my $apos = "it's";
my $wrapped = $fn->($apos);
unlike($wrapped, qr/\\'/, 'apostrophe not escaped in q_wrap output');
done_testing();
};
# ==================================================================
# render_fallback
# --------------------------------------------------
# Tests for the Data::Dumper-based catch-all renderer
# ==================================================================
subtest 'render_fallback' => sub {
my $fn = \&App::Test::Generator::render_fallback;
# Undef produces the literal string 'undef'
is($fn->(undef), $UNDEF_LITERAL, 'undef produces undef literal');
# Integer scalars pass through Dumper in terse mode
my $scalar_result = $fn->(42);
is($scalar_result, '42', 'integer scalar');
# Hashrefs are rendered as Perl hash literals with braces
my $hash_result = $fn->({ a => 1 });
like($hash_result, qr/\{/, 'hashref renders with braces');
like($hash_result, qr/'a'/, 'hashref key present');
# No trailing newline â Dumper adds one and we strip it
unlike($fn->({ a => 1 }), qr/\n$/, 'no trailing newline');
# Arrayrefs render with square brackets
my $arr_result = $fn->([1, 2]);
like($arr_result, qr/\[/, 'arrayref renders with brackets');
done_testing();
};
# ==================================================================
# render_args_hash
# --------------------------------------------------
# Tests for the flat hashref renderer used for output
# specs and constructor argument lists
# ==================================================================
subtest 'render_args_hash' => sub {
my $fn = \&App::Test::Generator::render_args_hash;
# Undef input returns empty string
is($fn->(undef), $EMPTY_STRING, 'undef returns empty string');
# Non-hash input returns empty string
is($fn->([1, 2]), $EMPTY_STRING, 'arrayref returns empty string');
# Empty hash returns empty string
is($fn->({}), $EMPTY_STRING, 'empty hash returns empty string');
# 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'");
t/function.t view on Meta::CPAN
},
'analyze() report matches documented hashref shape',
);
is($clean_report->{stability_score}, 100, 'clean schema scores full stability');
is($clean_report->{consistency_score}, 100, 'clean schema scores full consistency');
is_deeply($clean_report->{risk_flags}, [], 'clean schema raises no risk flags');
# A boolean return with no other risk stays clamped at 100 -- the
# bonus is documented as a no-op here, not an over-100 value silently
# clamped down, so this also guards against the clamp being removed
my $boolean_report = $analyser->analyze({ output => { type => 'boolean' } });
is($boolean_report->{stability_score}, 100, 'boolean bonus is a no-op when already at 100');
# Combine the implicit-undef penalty with the boolean bonus to prove
# the bonus does take effect once stability has actually been reduced
my $combined_report = $analyser->analyze({
output => {
type => 'boolean',
_error_handling => { implicit_undef => 1 },
},
});
is(
$combined_report->{stability_score},
100 - $PENALTY_IMPLICIT_UNDEF_STABILITY + $BONUS_BOOLEAN_STABILITY,
'boolean bonus applies on top of an already-reduced stability score',
);
is_deeply($combined_report->{risk_flags}, ['implicit_error_return'], 'implicit_error_return flag recorded');
diag("combined report: stability=$combined_report->{stability_score} consistency=$combined_report->{consistency_score}")
if $ENV{TEST_VERBOSE};
# risk_flags is a fresh arrayref per call with no back-reference
# to the schema or the analyser, so no cycle should be detectable
memory_cycle_ok($combined_report, 'analyze() report has no reference cycles');
};
# ==================================================================
# App::Test::Generator::Analyzer::Complexity
# --------------------------------------------------
# t/Analyzer-Complexity.t already covers analyze()'s
# public behaviour in depth (branching, exceptions,
# nesting, classification, string/comment stripping
# indirectly). What it does not do is call the private
# helper _strip_strings_and_comments directly -- the
# skill explicitly requires testing internal helpers in
# isolation, so that is the focus here, plus the
# Test::Returns schema-shape check on analyze()'s report.
# ==================================================================
subtest 'Analyzer::Complexity - _strip_strings_and_comments isolated behaviour' => sub {
my $fn = \&App::Test::Generator::Analyzer::Complexity::_strip_strings_and_comments;
# Double- and single-quoted string contents are removed entirely,
# including any keyword-like text inside them
is($fn->(q{my $x = "if this then that";}), q{my $x = ;}, 'double-quoted string contents removed');
is($fn->(q{my $x = 'unless this';}), q{my $x = ;}, 'single-quoted string contents removed');
# Trailing # comments are blanked from the matched line onward
is($fn->("my \$x = 1; # if true do this\n"), "my \$x = 1; \n", 'trailing comment removed');
# Escaped quote characters inside a string do not terminate the
# match early -- this is the (?:[^"\\]|\\.) escape-aware alternation
is($fn->(q{my $x = "she said \"hi\"";}), q{my $x = ;}, 'escaped quotes inside string do not break stripping');
# A body with no strings or comments passes through unchanged
is($fn->('if ($x) { return 1; }'), 'if ($x) { return 1; }', 'plain code is unchanged');
# Empty input returns empty output, not undef or a die
is($fn->(''), '', 'empty string input returns empty string');
done_testing();
};
subtest 'Analyzer::Complexity - new and analyze report shape' => sub {
my $analyser = App::Test::Generator::Analyzer::Complexity->new();
isa_ok($analyser, 'App::Test::Generator::Analyzer::Complexity', 'new() returns correct class');
my $method = { body => 'sub foo { if ($x) { return 1; } return 0; }' };
my $report = $analyser->analyze($method);
returns_ok(
$report,
{
type => 'hashref',
schema => {
cyclomatic_score => { type => 'integer' },
branching_points => { type => 'integer' },
early_returns => { type => 'integer' },
exception_paths => { type => 'integer' },
nesting_depth => { type => 'integer' },
complexity_level => { type => 'string' },
},
},
'analyze() report matches documented hashref shape',
);
diag("complexity report: $report->{complexity_level} (score=$report->{cyclomatic_score})")
if $ENV{TEST_VERBOSE};
# The report hashref holds only plain scalars, so no cycle is possible;
# this guards against a future change that embeds $method or $self in it
memory_cycle_ok($report, 'analyze() report has no reference cycles');
# Method argument missing a body key entirely must not die --
# documented via the "//= ''" default in the source
lives_ok { $analyser->analyze({}) } 'analyze() tolerates a method hashref with no body key';
done_testing();
};
# ==================================================================
# App::Test::Generator::Analyzer::SideEffect
# --------------------------------------------------
# t/Analyzer-SideEffect.t already covers analyze()'s
# public flags and purity classification thoroughly,
# including string/comment false-positive avoidance.
# This module ships its own copy of
# _strip_strings_and_comments (duplicated-by-design
# from Analyzer::Complexity per CLAUDE.md's "shared-by-
# duplication helper" convention elsewhere in this
# codebase) -- exercised directly here since it has not
# been called by its fully-qualified name anywhere else.
# ==================================================================
t/function.t view on Meta::CPAN
subtest 'Devel::App::Test::Generator::LCSAJ::Runtime::_write_results - writes a well-formed per-PID JSON file' => sub {
my $orig_cwd = getcwd();
my $tmp = File::Temp::tempdir(CLEANUP => 1);
local %Devel::App::Test::Generator::LCSAJ::Runtime::HITS = (
'lib/Foo/Bar.pm' => { 10 => 3, 12 => 1 },
);
eval {
chdir $tmp or die "cannot chdir to $tmp: $!";
Devel::App::Test::Generator::LCSAJ::Runtime::_write_results();
};
my $err = $@;
chdir $orig_cwd or croak "cannot chdir back to $orig_cwd: $!";
is($err, '', '_write_results() does not die for a normal, writable target') or diag($err);
my $out_file = "$tmp/cover_html/lcsaj_hits/hits_$$.json";
ok(-e $out_file, 'a per-PID JSON file is written under cover_html/lcsaj_hits, named with the current PID');
no autodie qw(open);
open my $fh, '<', $out_file or croak "cannot read back $out_file: $!";
local $/;
my $content = <$fh>;
close $fh;
my $decoded = decode_json($content);
is_deeply(
$decoded,
{ 'lib/Foo/Bar.pm' => { 10 => 3, 12 => 1 } },
'the written JSON round-trips back to the exact %HITS structure that was serialised'
);
memory_cycle_ok(\%Devel::App::Test::Generator::LCSAJ::Runtime::HITS, '%HITS holds no circular references');
};
subtest 'Devel::App::Test::Generator::LCSAJ::Runtime::_write_results - croaks with the documented message when open() fails' => sub {
my $orig_cwd = getcwd();
my $tmp = File::Temp::tempdir(CLEANUP => 1);
local %Devel::App::Test::Generator::LCSAJ::Runtime::HITS = (
'lib/Foo/Bar.pm' => { 10 => 1 },
);
eval {
chdir $tmp or die "cannot chdir to $tmp: $!";
# Pre-create the exact target path as a directory, not a file --
# open() for write against a directory portably fails on every
# platform this module supports, without relying on chmod-based
# permission tricks that behave differently when run as root
my $out_file = "cover_html/lcsaj_hits/hits_$$.json";
make_path($out_file);
throws_ok { Devel::App::Test::Generator::LCSAJ::Runtime::_write_results() }
qr/^Cannot write \Q$out_file\E: /,
'_write_results() croaks with the exact documented message when open() fails';
};
my $err = $@;
chdir $orig_cwd or croak "cannot chdir back to $orig_cwd: $!";
is($err, '', 'no unexpected exception escaped the eval wrapper') or diag($err);
};
subtest 'Devel::App::Test::Generator::LCSAJ::Runtime - BEGIN-time LCSAJ_TARGETS parsing' => sub {
# %TARGET is populated once, at compile time, from $ENV{LCSAJ_TARGETS}.
# That BEGIN block cannot be re-run against this already-loaded module,
# so it is exercised by loading a fresh copy of the module in a child
# process with the env var pre-set, then reading back %TARGET -- a
# single bounded perl -e invocation, not a recursive test/prove run
my $targets = '/build/blib/lib/Foo.pm:/home/user/proj/lib/Bar.pm:' . "\n";
local $ENV{LCSAJ_TARGETS} = $targets;
# List-form system() under capture_merged(), not qx{}/backticks --
# qx{} always goes through a shell, and a shell-quoted '...' -e
# argument that works under sh/bash is invalid syntax for
# cmd.exe on Windows (single quotes are not its quoting
# character), which previously made the child perl process fail
# to parse its own -e script.
my @inc_args = map { ('-I', $_) } @INC;
my $code = 'print join(q{,}, sort keys %Devel::App::Test::Generator::LCSAJ::Runtime::TARGET)';
my ($output, $exit) = capture_merged {
system($^X, @inc_args, '-MDevel::App::Test::Generator::LCSAJ::Runtime', '-e', $code);
};
is($exit, 0, 'the child process exits cleanly') or diag($output);
is(
$output,
'lib/Bar.pm,lib/Foo.pm',
'LCSAJ_TARGETS entries are normalised (blib/lib and lib stripped) and stray trailing newlines are removed'
);
};
# Clear %HITS so this module's END block (_write_results) does not write
# a stray hits_$$.json file into the real project tree when this test
# script itself exits
%Devel::App::Test::Generator::LCSAJ::Runtime::HITS = ();
# --------------------------------------------------
# App::Test::Generator::Emitter::Perl
#
# t/Emitter-Perl.t and t/Emitter-Perl_unit.t already exhaustively cover
# every emitted code block and every plan-flag combination, but use only
# loose (unanchored) regex matches on croak messages and never use
# Test::Mockingbird, Test::Returns, or Test::Memory::Cycle. These
# subtests add: exact (anchored) croak-message assertions, dispatch-only
# isolation of _emit_method_tests() via mocking every _emit_*_test sub
# (so the dispatch logic is verified independently of what those subs
# actually emit), the never-before-tested predicate_test flag, the
# never-before-tested case of a method entirely absent from %schema,
# and returns_ok/memory_cycle_ok coverage.
# --------------------------------------------------
subtest 'Emitter::Perl::new - exact croak messages' => sub {
throws_ok { App::Test::Generator::Emitter::Perl->new(plans => {}, package => 'Foo') }
qr/^schema required at /, 'missing schema croaks with the exact documented message';
throws_ok { App::Test::Generator::Emitter::Perl->new(schema => {}, package => 'Foo') }
qr/^plans required at /, 'missing plans croaks with the exact documented message';
throws_ok { App::Test::Generator::Emitter::Perl->new(schema => {}, plans => {}) }
qr/^package required at /, 'missing package croaks with the exact documented message';
my $bad_package = "Evil'); system('touch /tmp/pwned'); #";
throws_ok { App::Test::Generator::Emitter::Perl->new(schema => {}, plans => {}, package => $bad_package) }
qr/^package '\Q$bad_package\E' is not a valid Perl package name at /,
t/function.t view on Meta::CPAN
package => 'My::Module',
);
my $code = $emitter->emit();
returns_ok($code, { type => 'string' }, 'emit() returns a string');
like($code, qr/done_testing\(\);\s*\z/, 'emit() output ends with the done_testing() footer');
memory_cycle_ok($emitter, 'emit() leaves the emitter free of circular references');
};
# ==================================================================
# App::Test::Generator::Template
#
# This module's own package code is a single thin wrapper,
# get_data_section(), around Data::Section::Simple::get_data_section().
# Everything else in the .pm file is __DATA__ template text (test.tt)
# rendered into *generated* downstream test files at runtime -- those
# embedded helper subs (rand_str, fuzz_inputs, etc.) never belong to the
# App::Test::Generator::Template namespace, so they are out of scope
# here; t/app.t exercises them indirectly by rendering and running the
# generated harnesses. t/Template.t and t/Template_unit.t already cover
# both call styles and the unknown-template/undef-argument cases by
# value, so this section adds only what those files do not: a mocked
# dispatch-isolation test pinning down exactly which arguments reach
# Data::Section::Simple::get_data_section() for each call style, plus
# Test::Returns/Test::Memory::Cycle coverage.
# ==================================================================
subtest 'Template::get_data_section - dispatch isolation via mocked Data::Section::Simple' => sub {
my @captured;
Test::Mockingbird::mock(
'Data::Section::Simple::get_data_section',
sub { @captured = @_; return 'mocked-content'; },
);
@captured = ();
App::Test::Generator::Template->get_data_section('test.tt');
is_deeply(\@captured, ['test.tt'],
'class-method call strips the leading class name before delegating, passing on only the template name');
@captured = ();
App::Test::Generator::Template::get_data_section('test.tt');
is_deeply(\@captured, ['test.tt'],
'plain function call passes the template name through unchanged');
@captured = ();
App::Test::Generator::Template::get_data_section();
is_deeply(\@captured, [undef],
'a no-argument call still forwards a single undef element, since $_[0] is read (not shifted) when it is not the package name');
Test::Mockingbird::unmock('Data::Section::Simple::get_data_section');
};
subtest 'Template::get_data_section - return value and memory-cycle safety' => sub {
my $result = App::Test::Generator::Template->get_data_section('test.tt');
returns_ok($result, { type => 'scalarref' }, 'get_data_section() returns a scalarref');
memory_cycle_ok($result, 'get_data_section() result is free of circular references');
};
# ==================================================================
# App::Test::Generator::_assert_identifier
#
# Security-critical guard (see CLAUDE.md): every module/function/
# transform/field name spliced unescaped into generated test source
# passes through here first. No existing test file (t/Generator.t,
# t/Generator_unit.t) exercises this function at all, so this section
# is full by-value coverage rather than a value-add narrowing.
# ==================================================================
subtest 'Generator::_assert_identifier - accepts well-formed identifiers' => sub {
my $fn = \&App::Test::Generator::_assert_identifier;
is($fn->('foo', 'name'), 'foo', 'a plain bareword identifier is returned unchanged');
is($fn->('_private', 'name'), '_private', 'a leading underscore is accepted');
is($fn->('Foo123', 'name'), 'Foo123', 'letters and digits after the first character are accepted');
};
subtest 'Generator::_assert_identifier - package => 1 additionally allows "::" separators' => sub {
my $fn = \&App::Test::Generator::_assert_identifier;
is($fn->('Foo::Bar', 'module', package => 1), 'Foo::Bar',
'a "::"-separated package name is accepted when package => 1');
is($fn->('DB::DB', 'function', package => 1), 'DB::DB',
'a fully-qualified sub name such as DB::DB is accepted when package => 1');
throws_ok { $fn->('Foo::Bar', 'module') } qr/^App::Test::Generator: module 'Foo::Bar' is not a valid Perl identifier at /,
'without package => 1, a "::"-separated name is rejected with the exact documented message';
};
subtest 'Generator::_assert_identifier - croaks with the exact message for missing or empty names' => sub {
my $fn = \&App::Test::Generator::_assert_identifier;
throws_ok { $fn->(undef, 'module') } qr/^App::Test::Generator: module is missing or empty at /,
'undef name croaks with the exact documented message';
throws_ok { $fn->('', 'function') } qr/^App::Test::Generator: function is missing or empty at /,
'empty-string name croaks with the exact documented message';
};
subtest 'Generator::_assert_identifier - rejects injection payloads even with package => 1' => sub {
my $fn = \&App::Test::Generator::_assert_identifier;
for my $payload ("Foo'; system('rm -rf /'); '", 'Foo::Bar()', 'Foo::Bar; 1', 'Foo Bar', '1Foo') {
throws_ok { $fn->($payload, 'module', package => 1) }
qr/^App::Test::Generator: module '\Q$payload\E' is not a valid Perl identifier at /,
"payload '$payload' is rejected even with package => 1";
}
};
subtest 'Generator::_assert_identifier - return value shape' => sub {
my $fn = \&App::Test::Generator::_assert_identifier;
returns_ok($fn->('foo', 'name'), { type => 'string' }, '_assert_identifier() returns a plain string on success');
};
# ==================================================================
# App::Test::Generator::_perl_quote - circular-reference depth guard
#
# The depth > 100 croak in _perl_quote() is unreachable through any
# single-level call from the public perl_quote() wrapper (which always
# starts at depth 0), but is reachable legitimately by feeding
# perl_quote() a sufficiently deep arrayref-of-arrayrefs -- no need to
# call the private _perl_quote() directly with a fabricated depth.
# ==================================================================
subtest 'Generator::perl_quote - croaks on a structure nested past the recursion limit' => sub {
Readonly my $DEPTH_BEYOND_LIMIT => 101;
t/function.t view on Meta::CPAN
my $info = $e->_detect_external_object_dependency(q{ my $ua = LWP::UserAgent->new; $ua->get($url); });
is_deeply($info->{uses_objects}, ['LWP::UserAgent'], 'the class of the object variable is inferred from its assignment');
};
subtest 'SchemaExtractor::_detect_external_object_dependency - returns undef for an undef or dependency-free body' => sub {
my $e = bless {}, 'App::Test::Generator::SchemaExtractor';
is($e->_detect_external_object_dependency(undef), undef, 'an undef method body returns undef immediately');
is($e->_detect_external_object_dependency('return 1;'), undef, 'a body with no external object usage returns undef');
};
subtest 'SchemaExtractor::_extract_default_value - extracts a default from each of several common assignment idioms' => sub {
my $e = bless {}, 'App::Test::Generator::SchemaExtractor';
is($e->_extract_default_value('timeout', '$timeout = $timeout || 30;'), 30, 'Pattern 1: $param = $param || value');
is($e->_extract_default_value('x', '$x //= 5;'), 5, 'Pattern 2: $param //= value');
is($e->_extract_default_value('x', q{$x ||= 'foo';}), 'foo', 'Pattern 5: $param ||= value, with quotes stripped');
is($e->_extract_default_value('x', '$x = 1;'), undef, 'a plain assignment with none of the eight idioms returns undef');
};
subtest 'SchemaExtractor::_extract_default_value - returns undef for missing param or code' => sub {
my $e = bless {}, 'App::Test::Generator::SchemaExtractor';
is($e->_extract_default_value(undef, '$x = 1;'), undef, 'undef param returns undef');
is($e->_extract_default_value('x', undef), undef, 'undef code returns undef');
is($e->_extract_default_value('x', ''), undef, 'empty code returns undef');
};
subtest 'SchemaExtractor::_compile_signature_isolated: empty stdout and empty stderr -> return undef without croaking' => sub {
# Regression: when the child subprocess is killed by SIGKILL (e.g. the
# kernel OOM killer) before it writes anything, $stdout and $stderr are
# both empty strings. Before the fix, decode_json('') croaked with
# "malformed JSON string" and the exception propagated through the
# Test::Builder subtest boundary, killing the test process with exit 255.
#
# Strategy: mock open3 in the consuming namespace so the "child" returns
# empty filehandles without spawning a real process. waitpid($$, 0) on
# our own PID returns -1 immediately (not our child), which is harmless.
my $e = bless { allow_signature_exec => 1, verbose => 0 }, 'App::Test::Generator::SchemaExtractor';
my $empty = '';
open my $fake_rdr, '<', \$empty or die;
open my $fake_err, '<', \$empty or die;
no warnings 'redefine';
local *App::Test::Generator::SchemaExtractor::open3 = sub {
$_[0] = do { open my $fh, '>', \my $buf; $fh }; # writable stdin for child
$_[1] = $fake_rdr; # stdout: empty
$_[2] = $fake_err; # stderr: empty
return $$; # our own PID â waitpid returns -1 quickly
};
my $result;
lives_ok { $result = $e->_compile_signature_isolated('myfunc', '(positional => [])') }
'does not croak when child produces nothing on stdout or stderr';
is($result, undef, 'returns undef for silent subprocess exit');
};
subtest 'SchemaExtractor::_compile_signature_isolated: non-JSON stdout -> return undef without croaking' => sub {
# Regression: decode_json() was called unconditionally on $stdout; when
# the child wrote a Perl warning or truncated output instead of valid JSON,
# the croak escaped Test::Builder's subtest eval and killed the process.
#
# Strategy: mock open3 so the "child" stdout returns garbage text.
my $e = bless { allow_signature_exec => 1, verbose => 0 }, 'App::Test::Generator::SchemaExtractor';
my $garbage = 'NOT VALID JSON AT ALL }{[]';
open my $fake_rdr, '<', \$garbage or die;
my $empty = '';
open my $fake_err, '<', \$empty or die;
no warnings 'redefine';
local *App::Test::Generator::SchemaExtractor::open3 = sub {
$_[0] = do { open my $fh, '>', \my $buf; $fh };
$_[1] = $fake_rdr;
$_[2] = $fake_err;
return $$;
};
my $result;
lives_ok { $result = $e->_compile_signature_isolated('myfunc', '(positional => [])') }
'does not croak when child stdout is non-JSON';
is($result, undef, 'returns undef for non-JSON subprocess output');
};
subtest 'SchemaExtractor::_log - prints to stdout only when verbose is true' => sub {
my $e = bless { verbose => 1 }, 'App::Test::Generator::SchemaExtractor';
my $stdout = capture_stdout { $e->_log('hello world') };
is($stdout, "hello world\n", 'message is printed with a trailing newline when verbose is set');
$e->{verbose} = 0;
$stdout = capture_stdout { $e->_log('should not appear') };
is($stdout, '', 'nothing is printed when verbose is false');
};
done_testing();
( run in 1.622 second using v1.01-cache-2.11-cpan-788537b7465 )