App-Test-Generator
view release on metacpan or search on metacpan
t/integration.t view on Meta::CPAN
sub foo {
my $x = shift;
my $y = $x + 1;
return $y;
}
1;
END
my $branching_src = <<'END';
package Branching;
sub foo {
my $x = shift;
if($x > 0) { return $x; }
if($x < 0) { return -$x; }
return 0;
}
1;
END
my $tmpdir = tempdir(CLEANUP => 1);
require Cwd;
my $orig = Cwd::cwd();
chdir $tmpdir or die $!;
open my $fh1, '>', 'Linear.pm' or die $!;
print $fh1 $linear_src;
close $fh1;
open my $fh2, '>', 'Branching.pm' or die $!;
print $fh2 $branching_src;
close $fh2;
my $lin_paths = App::Test::Generator::LCSAJ->generate('Linear.pm', 'lin_out');
my $br_paths = App::Test::Generator::LCSAJ->generate('Branching.pm', 'br_out');
chdir $orig;
ok(scalar @{$br_paths} > scalar @{$lin_paths},
'branching code produces more LCSAJ paths than linear code');
};
# ==================================================================
# PIPELINE 5: CoverageGuidedFuzzer -> corpus round-trip
# ==================================================================
subtest 'CoverageGuidedFuzzer: run -> save_corpus -> load_corpus -> run' => sub {
my $tmpdir = tempdir(CLEANUP => 1);
my $corpus_file = File::Spec->catfile($tmpdir, 'corpus.json');
my $call_count = 0;
my $target = sub {
my $input = shift;
$call_count++;
die "too long\n" if defined($input) && length($input) > 50;
return length($input // '');
};
# First run
my $f1 = App::Test::Generator::CoverageGuidedFuzzer->new(
schema => { input => { type => 'string', max => 100 } },
target_sub => $target,
iterations => 10,
seed => 42,
);
if($ENV{EXTENDED_TESTING}) {
my $r1 = $f1->run();
is($r1->{total_iterations}, 10, 'first run: 10 iterations completed');
}
# Save corpus
lives_ok(sub { $f1->save_corpus($corpus_file) },
'save_corpus() lives after run');
ok(-f $corpus_file, 'corpus file written');
# Load into second fuzzer
my $f2 = App::Test::Generator::CoverageGuidedFuzzer->new(
schema => { input => { type => 'string', max => 100 } },
target_sub => $target,
iterations => 5,
seed => 99,
);
# Second run
if($ENV{EXTENDED_TESTING}) {
lives_ok(sub { $f2->load_corpus($corpus_file) }, 'load_corpus() lives');
ok(scalar @{$f2->corpus()} > 0, 'corpus loaded into second fuzzer');
my $r2 = $f2->run();
is($r2->{total_iterations}, 5, 'second run: 5 iterations completed');
ok($call_count > 0, 'target sub called across both runs');
}
};
subtest 'CoverageGuidedFuzzer: bugs list entries are well-formed' => sub {
my $target = sub {
my $input = shift;
die "trigger\n" if defined($input) && $input eq 'TRIGGER';
return 1;
};
my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
schema => { input => { type => 'string', min => 1, max => 20 } },
target_sub => $target,
iterations => 30,
seed => 42,
);
if($ENV{EXTENDED_TESTING}) {
lives_ok(sub { $f->run() }, 'run() lives');
}
for my $bug (@{$f->bugs()}) {
ok(exists $bug->{input}, 'bug entry has input key');
ok(exists $bug->{error}, 'bug entry has error key');
ok(defined $bug->{error}, 'bug error is defined');
}
ok(1, 'bug list iteration completed');
};
# ==================================================================
# PIPELINE 6: Generator with various schema configurations
# ==================================================================
subtest 'Generator: integer input/output schema produces compilable test' => sub {
my $schema = _make_schema(function => 'add', input => 'integer', output => 'integer');
my $tmpdir = tempdir(CLEANUP => 1);
my $outfile = File::Spec->catfile($tmpdir, 'add.t');
capture(sub { App::Test::Generator->generate($schema, $outfile) });
ok(-f $outfile, 'test file written for integer schema');
is(system($^X, '-c', $outfile), 0, 'generated test compiles');
};
subtest 'Generator: boolean output schema produces compilable test' => sub {
my $schema = _make_schema(function => 'is_positive', input => 'number', output => 'boolean');
my $tmpdir = tempdir(CLEANUP => 1);
my $outfile = File::Spec->catfile($tmpdir, 'bool.t');
capture(sub { App::Test::Generator->generate($schema, $outfile) });
ok(-f $outfile, 'test file written for boolean schema');
is(system($^X, '-c', $outfile), 0, 'generated test compiles');
};
subtest 'Generator: same seed produces reproducible output' => sub {
my $s1 = _make_schema(function => 'my_func', input => 'string',
output => 'string', extra => 'seed: 12345');
my $s2 = _make_schema(function => 'my_func', input => 'string',
output => 'string', extra => 'seed: 12345');
my ($out1) = capture(sub { App::Test::Generator->generate($s1) });
my ($out2) = capture(sub { App::Test::Generator->generate($s2) });
is($out1, $out2, 'same seed produces identical output');
};
subtest 'Generator: different seeds produce different output' => sub {
my $s1 = _make_schema(function => 'my_func', input => 'string',
output => 'string', extra => 'seed: 1');
my $s2 = _make_schema(function => 'my_func', input => 'string',
output => 'string', extra => 'seed: 2');
my ($out1) = capture(sub { App::Test::Generator->generate($s1) });
my ($out2) = capture(sub { App::Test::Generator->generate($s2) });
isnt($out1, $out2, 'different seeds produce different output');
};
subtest 'Generator: iterations config controls iteration count in output' => sub {
my $schema = _make_schema(function => 'my_func', input => 'string',
output => 'string', extra => 'iterations: 99');
my ($out) = capture(sub { App::Test::Generator->generate($schema) });
like($out, qr/99/, 'iteration count 99 appears in generated output');
};
# ==================================================================
# PIPELINE 7: SchemaExtractor strict_pod validation report
# ==================================================================
subtest 'SchemaExtractor: strict_pod=1 populates validation report' => sub {
my ($pm, $tmpdir) = _make_sample_module();
my $extractor = App::Test::Generator::SchemaExtractor->new(
input_file => $pm,
strict_pod => 1,
);
my $schemas = $extractor->extract_all(no_write => 1);
my $report = $extractor->generate_pod_validation_report($schemas);
ok(defined $report, 'report is defined');
ok(length($report) > 0, 'report is non-empty');
ok(
$report =~ /All methods passed/i || $report =~ /Validation Report/i,
'report is either all-passed or a validation report',
);
};
subtest 'SchemaExtractor -> generate_pod_validation_report: injected errors appear' => sub {
my ($pm, $tmpdir) = _make_sample_module();
my $extractor = App::Test::Generator::SchemaExtractor->new(
input_file => $pm,
);
my $schemas = $extractor->extract_all(no_write => 1);
# Inject errors into two methods that we know were extracted
my @methods = sort keys %{$schemas};
SKIP: {
skip 'fewer than two methods extracted', 1 unless scalar @methods >= 2;
my ($m1, $m2) = @methods[0, 1];
$schemas->{$m1}{_pod_validation_errors} = ['param mismatch'];
$schemas->{$m1}{_pod_disagreement} = 1;
$schemas->{$m2}{_pod_validation_errors} = ['return type unclear'];
$schemas->{$m2}{_pod_disagreement} = 1;
my $report = $extractor->generate_pod_validation_report($schemas);
like($report, qr/\Q$m1\E/, "$m1 appears in report");
like($report, qr/\Q$m2\E/, "$m2 appears in report");
}
};
# ==================================================================
# PIPELINE 8: Full stack â SchemaExtractor write -> Generator read
# ==================================================================
subtest 'Full stack: SchemaExtractor write -> Generator read round-trip' => sub {
my ($pm, $tmpdir) = _make_sample_module();
my $out_dir = File::Spec->catdir($tmpdir, 'schemas');
mkdir $out_dir or die $!;
my $extractor = App::Test::Generator::SchemaExtractor->new(
input_file => $pm,
output_dir => $out_dir,
t/integration.t view on Meta::CPAN
package Sample::Branchy;
use strict;
use warnings;
sub classify {
my ($self, $n) = @_;
if($n > 0) { return 'positive'; }
if($n < 0) { return 'negative'; }
return 'zero';
}
1;
END_PM
close $fh_b;
require Cwd;
my $orig = Cwd::cwd();
chdir $tmpdir_a or die $!;
my $mutator_a = App::Test::Generator::Mutator->new(
file => File::Spec->catfile('lib', 'Sample', 'Calculator.pm'), lib_dir => 'lib',
);
my @mutants_a_pass1 = eval { $mutator_a->generate_mutants() };
chdir $orig;
chdir $tmpdir_b or die $!;
my $mutator_b = App::Test::Generator::Mutator->new(
file => File::Spec->catfile('lib', 'Sample', 'Branchy.pm'), lib_dir => 'lib',
);
my @mutants_b = eval { $mutator_b->generate_mutants() };
chdir $orig;
# Re-run mutator_a after mutator_b has been constructed and used,
# to confirm mutator_a's own results are unaffected by mutator_b.
chdir $tmpdir_a or die $!;
my @mutants_a_pass2 = eval { $mutator_a->generate_mutants() };
chdir $orig;
ok(scalar @mutants_a_pass1 > 0, 'mutator_a produced mutants');
ok(scalar @mutants_b > 0, 'mutator_b produced mutants');
is(scalar @mutants_a_pass1, scalar @mutants_a_pass2,
'mutator_a is deterministic and unaffected by mutator_b running in between');
for my $m (@mutants_a_pass1) {
unlike($m->line_content // '', qr/classify|positive|negative/,
"mutator_a mutant does not reference mutator_b's source");
}
for my $m (@mutants_b) {
unlike($m->line_content // '', qr/precision|Calculator/,
"mutator_b mutant does not reference mutator_a's source");
}
};
subtest 'Concurrency: independent CoverageGuidedFuzzer instances do not share corpus or bug state' => sub {
my $calls_a = 0;
my $target_a = sub { $calls_a++; die "A trigger\n" if ($_[0] // '') eq 'A_BUG'; return 1; };
my $calls_b = 0;
my $target_b = sub { $calls_b++; die "B trigger\n" if ($_[0] // '') eq 'B_BUG'; return 1; };
my $f_a = App::Test::Generator::CoverageGuidedFuzzer->new(
schema => { input => { type => 'string', min => 1, max => 10 } },
target_sub => $target_a, iterations => $FUZZ_SMALL_ITER, seed => $FUZZ_SEED_A,
);
my $f_b = App::Test::Generator::CoverageGuidedFuzzer->new(
schema => { input => { type => 'string', min => 1, max => 10 } },
target_sub => $target_b, iterations => $FUZZ_SMALL_ITER, seed => $FUZZ_SEED_B,
);
if($ENV{EXTENDED_TESTING}) {
# Interleave: run A, then B, then A again, so any shared
# package-level state would show up as cross-contamination.
# total_iterations accumulates per-instance across run() calls
# (stats live in $self, initialised once in new()), so fuzzer
# A's second run is expected to report 2x iterations â what
# matters for this test is that fuzzer B's run in between adds
# nothing to fuzzer A's count.
my $r_a1 = $f_a->run();
my $r_b = $f_b->run();
my $r_a2 = $f_a->run();
is($r_a1->{total_iterations}, $FUZZ_SMALL_ITER, 'fuzzer A first run completed its own iteration count');
is($r_b->{total_iterations}, $FUZZ_SMALL_ITER, 'fuzzer B run completed its own iteration count');
is($r_a2->{total_iterations}, $FUZZ_SMALL_ITER * 2, 'fuzzer A accumulates only its own iterations, unaffected by fuzzer B running between');
ok($calls_a > 0, 'target_a was actually called by fuzzer A');
ok($calls_b > 0, 'target_b was actually called by fuzzer B');
for my $bug (@{ $f_a->bugs() }) {
unlike($bug->{error} // '', qr/B trigger/, 'fuzzer A bug list never contains fuzzer B errors');
}
for my $bug (@{ $f_b->bugs() }) {
unlike($bug->{error} // '', qr/A trigger/, 'fuzzer B bug list never contains fuzzer A errors');
}
} else {
ok(1, 'EXTENDED_TESTING not set â skipping fuzzer run, construction only');
}
};
# ==================================================================
# PIPELINE 14: Optional dependency fallback â BSD::Resource missing
#
# CLAUDE.md documents BSD::Resource as loaded via a runtime require
# inside SchemaExtractor's _compile_signature_isolated (not a
# Makefile.PL PREREQ_PM, Unix-only, best-effort rlimit). This pipeline
# proves the documented fallback: extraction must succeed identically
# whether or not the module is installed.
# ==================================================================
subtest 'SchemaExtractor: signature_for extraction degrades gracefully without BSD::Resource' => sub {
# The fixture module below is only ever compiled by the isolated
# perl -T subprocess spawned from _compile_signature_isolated()
# (PIPELINE 14 sets allow_signature_exec => 1), so the dependency
# on Type::Params/Types::Common is real, not just text inside this
# process. Test::Without::Module cannot simulate "missing" for it
# (its @INC hook does not propagate to the spawned subprocess), so
# unlike BSD::Resource below, this is a hard skip, not a fallback
# under test.
test_needs('Type::Params', 'Types::Common');
my $module_src = <<'END_MODULE';
package TestModule::SignatureFor;
use Types::Standard qw(Num);
use Type::Params qw(-sigs);
signature_for add_numbers => (
method => 1,
positional => [ Num, Num ],
returns => Num,
);
sub add_numbers ( $self, $first, $second ) {
return $first + $second;
}
1;
END_MODULE
my $tmpdir = tempdir(CLEANUP => 1);
my $module_file = File::Spec->catfile($tmpdir, 'SignatureFor.pm');
open my $fh, '>', $module_file or die $!;
print $fh $module_src;
close $fh;
my $extract_with = sub {
my $extractor = App::Test::Generator::SchemaExtractor->new(
( run in 0.487 second using v1.01-cache-2.11-cpan-f03e8824b8d )