App-GHGen

 view release on metacpan or  search on metacpan

t/integration.t  view on Meta::CPAN

	my @trig_b = grep { $_->{type} eq 'cost' && $_->{message} =~ /triggers/ } @issues_b;
	ok(@trig_a > 0, 'Problem workflow (A) has broad-trigger issue');
	is(scalar @trig_b, 0, 'Clean workflow (B) has no broad-trigger issue');

	# Clean workflow must report zero issues in total.
	is(scalar @issues_b, 0, 'Clean workflow (B) has zero issues overall');

	diag("Issues A: " . scalar(@issues_a) . "  Issues B: " . scalar(@issues_b))
		if $ENV{TEST_VERBOSE};
};

# ---------------------------------------------------------------------------
# Subtest 14: Reporter::estimate_savings vs CostEstimator::estimate_savings
# Both functions share a name but document different return-value contracts.
# Verify each conforms to its own POD spec when given the same input.
subtest 'Reporter vs CostEstimator estimate_savings: distinct return contracts' => sub {
	my @issues = (
		{
			type     => 'performance',
			severity => 'medium',
			message  => 'No dependency caching found - increases build times and costs',
		},
		{
			type     => 'cost',
			severity => 'low',
			message  => 'No concurrency group - old runs continue when superseded',
		},
	);

	# Reporter::estimate_savings: { minutes => Int, cost => Int }
	my $reporter_s = App::GHGen::Reporter::estimate_savings(\@issues);
	ok(defined $reporter_s->{minutes},            'Reporter: minutes key present');
	ok(defined $reporter_s->{cost},               'Reporter: cost key present');
	ok(!exists $reporter_s->{details},            'Reporter: no details key (simpler contract)');
	ok(!exists $reporter_s->{percentage},         'Reporter: no percentage key');
	cmp_ok($reporter_s->{minutes}, '>', 0,        'Reporter: nonzero savings for caching issue');

	# CostEstimator::estimate_savings: { minutes, cost (Str), percentage, details ArrayRef }
	my $estimator_s = App::GHGen::CostEstimator::estimate_savings(\@issues);
	ok(defined $estimator_s->{minutes},              'CostEstimator: minutes key present');
	ok(defined $estimator_s->{cost},                 'CostEstimator: cost key present');
	ok(exists  $estimator_s->{details},              'CostEstimator: details key present');
	ok(ref $estimator_s->{details} eq 'ARRAY',       'CostEstimator: details is an ArrayRef');
	ok(exists  $estimator_s->{percentage},           'CostEstimator: percentage key present');
	cmp_ok($estimator_s->{minutes}, '>', 0,          'CostEstimator: nonzero savings');

	diag("Reporter: $reporter_s->{minutes} min  CostEstimator: $estimator_s->{minutes} min")
		if $ENV{TEST_VERBOSE};
};

# ---------------------------------------------------------------------------
# Subtest 15: get_cache_suggestion per ecosystem (Analyzer cross-module check)
# For each ecosystem the Analyzer understands, build a minimal workflow that
# triggers that detection and verify the cache path in the suggestion.
subtest 'Analyzer::get_cache_suggestion: correct cache path per ecosystem' => sub {
	# These constants come directly from the FORMAL SPECIFICATION in the POD.
	my @cases = (
		{
			ecosystem => 'npm',
			steps     => [{ run => 'npm install' }],
			path_frag => '~/.npm',
		},
		{
			ecosystem => 'pip',
			steps     => [{ run => 'pip install -r requirements.txt' }],
			path_frag => '~/.cache/pip',
		},
		{
			ecosystem => 'cargo',
			steps     => [{ run => 'cargo build' }],
			path_frag => '~/.cargo',
		},
		{
			ecosystem => 'bundler',
			steps     => [{ run => 'bundle install' }],
			path_frag => 'vendor/bundle',
		},
	);

	for my $case (@cases) {
		my $wf         = { jobs => { build => { steps => $case->{steps} } } };
		my $suggestion = get_cache_suggestion($wf);
		like($suggestion, qr/\Q$case->{path_frag}\E/,
			"$case->{ecosystem}: suggestion contains path '$case->{path_frag}'");
		diag("$case->{ecosystem}: $suggestion") if $ENV{TEST_VERBOSE};
	}

	# Unknown ecosystem (make install) must return the generic guidance message.
	my $unk_wf   = { jobs => { build => { steps => [{ run => 'make install' }] } } };
	my $unk_hint = get_cache_suggestion($unk_wf);
	like($unk_hint, qr/dependency manager/i,
		'Unknown ecosystem returns generic guidance per POD');
};

# ---------------------------------------------------------------------------
# Subtest 16: Test::Without::Module -- YAML::XS absence causes load error in Fixer
# Fixer has a hard `use YAML::XS` dependency.  Blocking it in a child process
# must produce a non-zero exit and a recognizable "Can't locate" error.
subtest 'Test::Without::Module: YAML::XS absence causes load error for Fixer' => sub {
	my $script = q(
		use Test::Without::Module 'YAML::XS';
		require App::GHGen::Fixer;
	);
	my ($out, $err);
	run3([$^X, '-Ilib', '-e', $script], \undef, \$out, \$err);
	my $exit = $? >> 8;

	cmp_ok($exit, '!=', 0, 'Child process exits non-zero when YAML::XS is blocked');
	like("$out$err", qr/Can't locate YAML/i,
		'Error output mentions the missing YAML::XS module');
};

# ---------------------------------------------------------------------------
# Subtest 17: Test::Without::Module -- Path::Tiny absence causes load error in Analyzer
# Analyzer has a hard `use Path::Tiny` dependency.  Blocking it must fail.
subtest 'Test::Without::Module: Path::Tiny absence causes load error for Analyzer' => sub {
	my $script = q(
		use Test::Without::Module 'Path::Tiny';
		require App::GHGen::Analyzer;
	);
	my ($out, $err);
	run3([$^X, '-Ilib', '-e', $script], \undef, \$out, \$err);
	my $exit = $? >> 8;

	cmp_ok($exit, '!=', 0, 'Child process exits non-zero when Path::Tiny is blocked');
	like("$out$err", qr/Can't locate Path/i,
		'Error output mentions the missing Path::Tiny module');
};

done_testing();



( run in 2.500 seconds using v1.01-cache-2.11-cpan-b16cb0d3907 )