App-Test-Generator

 view release on metacpan or  search on metacpan

t/extended_tests.t  view on Meta::CPAN

	my ($out) = capture(sub {
		eval { App::Test::Generator->generate($path) };
	});
	is($@, '', 'transforms schema does not croak');
};

subtest 'Generator: _valid_type returns 1 for all supported types' => sub {
	for my $type (qw(string boolean integer number float hashref arrayref object int bool)) {
		is(App::Test::Generator::_valid_type($type), 1,
			"'$type' is a valid type");
	}
};

subtest 'Generator: _valid_type returns 0 for unknown types' => sub {
	for my $type (qw(unknown blob xml json list map set)) {
		is(App::Test::Generator::_valid_type($type), 0,
			"'$type' is not a valid type");
	}
};

subtest 'Generator: _valid_type returns 0 for undef' => sub {
	is(App::Test::Generator::_valid_type(undef), 0, 'undef -> 0');
};

subtest 'Generator: _has_positions returns 0 for empty hashref' => sub {
	is(App::Test::Generator::_has_positions({}), 0, 'empty -> 0');
};

subtest 'Generator: _has_positions returns 0 for undef' => sub {
	is(App::Test::Generator::_has_positions(undef), 0, 'undef -> 0');
};

subtest 'Generator: _has_positions returns 1 when position present' => sub {
	is(App::Test::Generator::_has_positions({
		x => { type => 'string', position => 0 }
	}), 1, 'position present -> 1');
};

subtest 'Generator: _has_positions returns 0 when no position' => sub {
	is(App::Test::Generator::_has_positions({
		x => { type => 'string' }
	}), 0, 'no position -> 0');
};

subtest 'Generator: _has_positions returns 0 for scalar value spec' => sub {
	is(App::Test::Generator::_has_positions({
		x => 'string'
	}), 0, 'scalar spec -> 0');
};

# ==================================================================
# CoverageGuidedFuzzer — branch coverage
# ==================================================================

subtest 'CoverageGuidedFuzzer: mutate handles all scalar types' => sub {
	use_ok('App::Test::Generator::CoverageGuidedFuzzer');

	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);

	# Exercise _mutate with each scalar type
	for my $val (42, 3.14, 'hello', '', undef) {
		lives_ok(
			sub {
				# Access _mutate directly
				App::Test::Generator::CoverageGuidedFuzzer::_mutate($f, $val)
			},
			defined($val) ? "mutate('$val') lives" : 'mutate(undef) lives',
		);
	}
};

subtest 'CoverageGuidedFuzzer: mutate handles arrayref' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'arrayref' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	my $result;
	lives_ok(
		sub {
			$result = App::Test::Generator::CoverageGuidedFuzzer::_mutate($f, [1, 2, 3])
		},
		'mutate([1,2,3]) lives',
	);
	is(ref($result), 'ARRAY', 'mutated arrayref is still an arrayref');
};

subtest 'CoverageGuidedFuzzer: mutate handles hashref' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'hashref' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	my $result;
	lives_ok(
		sub {
			$result = App::Test::Generator::CoverageGuidedFuzzer::_mutate($f, { a => 1, b => 2 })
		},
		'mutate({a=>1}) lives',
	);
	is(ref($result), 'HASH', 'mutated hashref is still a hashref');
};

subtest 'CoverageGuidedFuzzer: mutate passes blessed ref through unchanged' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	my $obj    = bless {}, 'FakeClass';
	my $result = App::Test::Generator::CoverageGuidedFuzzer::_mutate($f, $obj);
	is($result, $obj, 'blessed ref passed through unchanged');
};

subtest 'CoverageGuidedFuzzer: _rand_int returns a numeric value' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'integer', min => 5, max => 10 } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	for (1..20) {
		my $val = App::Test::Generator::CoverageGuidedFuzzer::_rand_int(
			$f, { min => 5, max => 10 }
		);
		ok(looks_like_number($val), "_rand_int returns numeric value (got $val)");
	}
};

subtest 'CoverageGuidedFuzzer: _rand_num returns value within bounds' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'number' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	for (1..10) {
		my $val = App::Test::Generator::CoverageGuidedFuzzer::_rand_num(
			$f, { min => 0, max => 1 }
		);
		ok($val >= 0 && $val <= 1,
			"_rand_num($val) within [0, 1]");
	}
};

subtest 'CoverageGuidedFuzzer: _validate_value correctly validates types' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);

	# integer
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, 42, { type => 'integer' }), 1, 'integer 42: valid');
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, 3.14, { type => 'integer' }), 0, 'float 3.14: invalid integer');

	# number
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, '3.14', { type => 'number' }), 1, 'string 3.14: valid number');

	# boolean
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, '1', { type => 'boolean' }), 1, '"1": valid boolean');
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, '2', { type => 'boolean' }), 0, '"2": invalid boolean');

	# string with min/max
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, 'hi', { type => 'string', min => 1, max => 10 }), 1, 'string in range: valid');
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, '', { type => 'string', min => 1 }), 0, 'empty string below min: invalid');

	# arrayref
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, [1,2], { type => 'arrayref' }), 1, 'arrayref: valid');
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, 'str', { type => 'arrayref' }), 0, 'string: invalid arrayref');

	# hashref
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, {a=>1}, { type => 'hashref' }), 1, 'hashref: valid');
};

subtest 'CoverageGuidedFuzzer: _validate_value returns 0 for undef' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { 1 },
		iterations => 0,
		seed       => 42,
	);
	is(App::Test::Generator::CoverageGuidedFuzzer::_validate_value(
		$f, undef, { type => 'string' }), 0, 'undef: always invalid');
};

# ==================================================================
# Stateful tests — verify state accumulates correctly across calls
# ==================================================================

subtest 'CoverageGuidedFuzzer: corpus accumulates across multiple run() calls' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { length($_[0] // '') },
		iterations => 5,
		seed       => 42,
	);
	$f->run();
	my $size1 = scalar @{$f->corpus()};
	$f->run();
	my $size2 = scalar @{$f->corpus()};
	ok($size2 >= $size1, 'corpus grows or stays same across runs');
};

subtest 'CoverageGuidedFuzzer: stats accumulate across run() calls' => sub {
	my $f = App::Test::Generator::CoverageGuidedFuzzer->new(
		schema     => { input => { type => 'string' } },
		target_sub => sub { 1 },
		iterations => 5,
		seed       => 42,
	);
	my $r1 = $f->run();
	my $r2 = $f->run();
	ok($r2->{total_iterations} >= $r1->{total_iterations},
		'total_iterations increases across runs');
};

subtest 'Mutator: generate_mutants is idempotent — same results on two calls' => sub {
	my $tmpdir = tempdir(CLEANUP => 1);
	my $lib    = File::Spec->catdir($tmpdir, 'lib');
	mkdir $lib or die $!;
	my $pm = File::Spec->catfile($lib, 'Idempotent.pm');
	open my $fh, '>', $pm or die $!;
	print $fh "package Idempotent;\nsub foo { if(\$x > 0) { return 1; } return 0; }\n1;\n";
	close $fh;

	my $mutator = App::Test::Generator::Mutator->new(
		file    => $pm,
		lib_dir => $lib,
	);
	my @m1 = $mutator->generate_mutants();
	my @m2 = $mutator->generate_mutants();
	is(scalar @m1, scalar @m2, 'generate_mutants count is idempotent');
};

subtest 'Planner: plan_all is idempotent — same results on two calls' => sub {
	my $p = App::Test::Generator::Planner->new(
		schemas => {
			foo => { accessor => { type => 'get' }, output => {} },
			bar => { output => { type => 'boolean' } },
		},
		package => 'Foo',
	);
	my $plan1 = $p->plan_all();
	my $plan2 = $p->plan_all();
	is_deeply($plan1, $plan2, 'plan_all is idempotent');
};

done_testing();



( run in 1.530 second using v1.01-cache-2.11-cpan-71847e10f99 )