2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-10-19 02:00:44 +00:00
Files
xcat-dep/native/openeuler-srpm.t
Vinícius Ferrão 3f3289bc95 Test native repository locks and publisher signature preservation
Exercise mixed repository finalization and changed publisher inputs on x86_64. Check native manifest selection and use File::Slurper in the goconserver fixture.
2026-09-29 00:03:57 -03:00

397 lines
24 KiB
Perl

use strict;
use warnings;
use Cwd qw(abs_path cwd);
use File::Basename qw(dirname basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin qw($RealBin);
use JSON::PP qw(decode_json);
use Test::More;
use lib "$RealBin/../lib", "$RealBin/../t/lib";
use XCAT::BuildUtils qw(capture_command command_exists digest_file read_binary write_binary);
use XCAT::GenesisReleaseTest qw(run_capture);
use XCAT::NFSLock ();
plan skip_all => 'Linux RPM tools and user namespaces required'
unless $^O eq 'linux' && !grep { !command_exists($_) } qw(rpm rpmkeys rpmbuild createrepo_c unshare gpg gpgconf);
my $parent_pid = $$;
my $tmp = tempdir(CLEANUP => 1);
my @namespace = $> == 0 ? () : ('unshare', '--user', '--map-root-user');
plan skip_all => 'User namespace unavailable for the collector root check'
if @namespace && run_capture("$tmp/namespace.log", @namespace, 'true') != 0;
my $collector = $ENV{XCAT_TEST_COLLECTOR} // abs_path("$RealBin/../mockbuild-all.pl");
my $source = abs_path("$RealBin/../python-scp/python-scp-0.14.5-1.oe2403.src.rpm");
my $hash = '3461d2a3fe0122cac2893d8465ad1271ae21e5570a31d4402e3f887ef545a0e8';
my $arch = capture_command('uname', '-m');
plan skip_all => 'Source package closure is selected only for x86_64' if $arch ne 'x86_64';
is(digest_file($source), $hash, 'the official source RPM is pinned');
my $epoch = 1788718796;
my $target = 'openeuler-20.03sp4-x86_64';
my $key_home = "$tmp/gnupg";
my $key_name = 'source-contract@example.invalid';
make_path("$tmp/bin", "$tmp/fixture/SPECS", $key_home);
chmod 0700, $key_home;
if ($ENV{XCAT_TEST_GENESIS_SIGNING_ONLY}) {
test_genesis_signing();
done_testing();
exit;
}
is(run_capture("$tmp/key.log", 'gpg', '--homedir', $key_home, '--batch', '--pinentry-mode', 'loopback',
'--passphrase', '', '--quick-generate-key', $key_name, 'rsa2048', 'sign', '0'), 0,
'create a private ephemeral signing identity for the repository gate')
or die(read_binary("$tmp/key.log"));
END {
local $?;
run_capture("$tmp/key-cleanup.log", 'gpgconf', '--homedir', $key_home, '--kill', 'gpg-agent')
if defined($parent_pid) && $$ == $parent_pid && defined($key_home) && -d $key_home;
}
write_binary("$tmp/fixture/SPECS/python3-scp.spec", <<'SPEC');
Name: python3-scp
Version: 0.14.5
Release: 1
Summary: Collector contract fixture
License: MIT
BuildArch: noarch
%description
Collector contract fixture.
%install
mkdir -p %{buildroot}/usr/share/scp-contract
printf 'fixture\n' > %{buildroot}/usr/share/scp-contract/payload
%files
/usr/share/scp-contract
SPEC
is(run_capture("$tmp/fixture.log", 'rpmbuild', '--quiet', '-ba', '--define', "_topdir $tmp/fixture",
"$tmp/fixture/SPECS/python3-scp.spec"), 0, 'build real RPM fixtures for the command boundary')
or die(read_binary("$tmp/fixture.log"));
copy("$RealBin/fixtures/srpm-mock.pl", "$tmp/bin/mock") or die $!;
chmod 0755, "$tmp/bin/mock";
sub scenario {
my ($name, %opt) = @_;
my $root = "$tmp/$name source";
my $out = "$tmp/$name output";
my $repo = "$tmp/$name-repo";
my $selected = $opt{target} // $target;
my $manifest = $opt{packages} // 'python3-scp=0.14.5';
make_path("$root/python-scp", "$root/grub2-xcat", "$repo/openeuler20.03sp4/x86_64");
write_binary("$root/packages-manifest.conf", "[$selected]\n$manifest\n");
write_binary("$root/Gitinfo", ('a' x 40) . "-dirty-snapshot-" . ('b' x 64) . "\n");
write_binary("$root/Gitepoch", "$epoch\n");
write_binary("$root/buildrpms.pl", "die 'Genesis dry-run must not execute its child';\n");
write_binary("$root/grub2-xcat/grub2-xcat.spec", "Name: grub2-xcat\nRelease: 1\n");
write_binary("$root/grub2-xcat/mockbuild.pl", <<'PERL');
use strict;
use warnings;
use JSON::PP qw(encode_json);
open my $fh, '>>', $ENV{SCP_CALLS} or die $!;
print {$fh} encode_json({script => 'grub2-xcat', argv => \@ARGV}) . "\n";
close $fh or die $!;
exit 41;
PERL
my $input = "$root/python-scp/" . (split m{/}, $source)[-1];
copy($source, $input) or die $! unless $opt{missing};
write_binary($input, 'corrupt') if $opt{corrupt};
write_binary("$repo/openeuler20.03sp4/x86_64/sentinel", 'previous repository');
for my $cell (keys %{$opt{published} // {}}) {
make_path("$repo/$cell");
copy($opt{published}{$cell}, "$repo/$cell/python3-scp-0.14.5-1.noarch.rpm") or die $!;
}
my ($held_lock, $lock_path, $lock_before);
if ($opt{hold_cell}) {
my $cell = "$repo/$opt{hold_cell}";
make_path(dirname($cell));
$lock_path = dirname($cell) . '/.' . basename($cell) . '.lock';
$held_lock = XCAT::NFSLock->acquire($lock_path, quiet => 1);
$lock_before = read_binary("$lock_path/metadata");
}
local $ENV{PATH} = "$tmp/bin:$ENV{PATH}";
local $ENV{MOCKBUILD_ALL_MOUNTNS} = 1;
local $ENV{SCP_CALLS} = "$tmp/$name calls.jsonl";
local $ENV{SCP_BUILD_STATUS} = $opt{success} ? '0' : '43';
local $ENV{SCP_EMPTY_OUTPUT} = $opt{empty} // '';
local $ENV{SCP_MUTATE_SOURCE} = $opt{mutate} ? $input : '';
local $ENV{SCP_FIXTURE_BINARY} = "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm";
local $ENV{SCP_FIXTURE_SOURCE} = "$tmp/fixture/SRPMS/python3-scp-0.14.5-1.src.rpm";
local $ENV{HOME} = $root;
local $ENV{GNUPGHOME} = $opt{env_home} // '';
my @options = @{$opt{options} // []};
unshift @options, '--gpg-sign', '--gpg-key-name', $key_name,
($opt{default_home} ? () : ('--gpg-home', $opt{key_home} // $key_home))
if !$opt{unsigned} && $selected =~ /^openeuler-/;
my $rc = run_capture("$tmp/$name.log", @namespace, $^X, $collector,
'--repo-root', $root, '--xcat-source', $root, '--target', $selected,
'--output', $out, '--repo-dep', $repo, '--run-id', 'source-contract',
'--build-timestamp', $epoch, '--max-parallel', 1, '--parallel-builds', 1,
'--skip-genesis', '--skip-perl', '--skip-tarball', @options);
my @calls = -f $ENV{SCP_CALLS}
? map { decode_json($_) } split /\n/, read_binary($ENV{SCP_CALLS}) : ();
my $lock_after = $held_lock && -f "$lock_path/metadata" ? read_binary("$lock_path/metadata") : undef;
$held_lock->release if $held_lock;
return {rc => $rc, calls => \@calls, log => read_binary("$tmp/$name.log"), input => $input,
repo => $repo, out => $out, root => $root, lock_path => $lock_path,
lock_before => $lock_before, lock_after => $lock_after};
}
my $selected = scenario('selected');
isnt($selected->{rc}, 0, 'a failed native source rebuild fails the full owner');
my @builds = grep { grep { $_ eq '--rebuild' } @{$_->{argv}} } @{$selected->{calls}};
is(scalar @builds, 1, 'the exact native manifest selects one source rebuild');
if (@builds) {
my $call = $builds[0];
like($call->{config}, qr/\Ainclude\('\/etc\/mock\/\Q$target\E\.cfg'\)\n/,
'the source rebuild includes the exact native target');
like($call->{config}, qr/\Qconfig_opts['environment']['SOURCE_DATE_EPOCH'] = '$epoch'\E/,
'the epoch enters the mock build environment');
unlike($call->{config}, qr/epel|forcearch|bootstrap_image/, 'the overlay introduces no foreign target policy');
isnt($call->{source}, $selected->{input}, 'mock consumes a private staged source');
is($call->{sha256}, $hash, 'the staged source retains the official digest');
my %args;
for my $i (0 .. $#{$call->{argv}} - 1) { $args{$call->{argv}[$i]} = $call->{argv}[$i + 1]; }
like($args{'--uniqueext'}, qr/^mba-01-openeuler-20\.03-[0-9a-f]{8}-python3-scp$/,
'mock uses the existing bounded target, run digest and package suffix');
like($args{'--resultdir'}, qr/\Q$target\E-source-contract\/build-results\/python3-scp\z/,
'result collection remains target and package specific');
for my $macro ('use_source_date_epoch_as_buildtime 1', 'clamp_mtime_to_source_date_epoch 1', '_buildhost xcat-build') {
ok(grep($_ eq $macro, @{$call->{argv}}), "mock retains deterministic macro $macro");
}
}
like($selected->{log}, qr/required build step|every build step failed/, 'mock failure reaches the owner failure gate');
is(read_binary("$selected->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
'failed source build preserves the previous repository');
for my $case (['corrupt', 'SHA256 mismatch'], ['missing', 'Missing source RPM']) {
my $result = scenario($case->[0], $case->[0] => 1, options => ['--scrub-all-chroots']);
isnt($result->{rc}, 0, "$case->[0] selected source fails");
like($result->{log}, qr/\Q$case->[1]\E/, "$case->[0] source identifies the input failure");
is_deeply($result->{calls}, [], "$case->[0] source fails before any mock action, including scrub");
}
my $staged = scenario('immutable-stage', mutate => 1);
isnt(digest_file($staged->{input}), $hash, 'the command double changes the original after staging');
my @staged_builds = grep { exists $_->{sha256} } @{$staged->{calls}};
is(scalar @staged_builds, 1, 'the staged source reaches mock once');
is($staged_builds[0]{sha256}, $hash, 'changing the original does not change the build input') if @staged_builds;
for my $other ('openeuler-24.03sp3-x86_64', 'openeuler-24.03sp4-x86_64', 'alma+epel-9-x86_64') {
my $result = scenario("unselected-$other", target => $other, packages => 'grub2-xcat=1.0', missing => 1);
isnt($result->{rc}, 0, "$other propagates the existing script failure");
my @script = grep { ($_->{script} // '') eq 'grub2-xcat' } @{$result->{calls}};
is(scalar @script, 1, "$other keeps the existing script builder");
is(scalar(grep { exists $_->{source} } @{$result->{calls}}), 0, "$other does not select the source RPM");
unlike($result->{log}, qr/Missing source RPM|SHA256 mismatch/, "$other does not require the unselected source input");
if (@script) {
my %args = @{$script[0]{argv}};
is($args{'--mock-cfg'}, $other, "$other preserves the script target argument");
is($args{'--build-timestamp'}, "$epoch", "$other preserves the script epoch argument");
}
}
my $absent = scenario('native-manifest-absence', packages => 'grub2-xcat=1.0', missing => 1);
is(scalar(grep { exists $_->{source} } @{$absent->{calls}}), 0, '20 SP4 also requires explicit manifest selection');
unlike($absent->{log}, qr/Missing source RPM/, 'an absent native manifest entry needs no source input');
my $cross = scenario('cross', target => 'openeuler-24.03-ppc64le');
isnt($cross->{rc}, 0, 'native cross-architecture source build is rejected');
like($cross->{log}, qr/requires a ppc64le build host/, 'cross rejection names the native host requirement');
is_deeply($cross->{calls}, [], 'cross rejection precedes every build command');
my $dry = scenario('dry', unsigned => 1, options => ['--dry-run']);
is($dry->{rc}, 0, 'the selected source has a successful dry-run plan');
like($dry->{log}, qr/--rebuild.*python-scp-0\.14\.5-1\.oe2403\.src\.rpm/, 'dry run reports the source rebuild');
is_deeply($dry->{calls}, [], 'dry run executes no mock action');
ok(!-d "$dry->{out}/mockbuild-all/$target-source-contract/source-rpms", 'dry run stages no source or mock overlay');
my $restamp = scenario('restamp', options => ['--build-number', 7]);
isnt($restamp->{rc}, 0, 'restamped rebuild failure remains fatal');
my @sources = grep { exists $_->{spec} } @{$restamp->{calls}};
is(scalar @sources, 1, 'a build number first creates one native source RPM');
like($sources[0]{spec}, qr/^Release:\s+1\.snap202609061819\.7\s*$/m,
'source spec reuses the existing release suffix policy') if @sources;
my @restamped = grep { exists $_->{source} } @{$restamp->{calls}};
is(scalar @restamped, 1, 'the generated source RPM is rebuilt once');
like($restamped[0]{source}, qr/restamp-srpm\/python3-scp-0\.14\.5-1\.src\.rpm\z/,
'binary build consumes the new source RPM') if @restamped;
is(digest_file($restamp->{input}), $hash, 'Release restamping preserves the official input bytes');
my $empty = scenario('empty', success => 1, empty => 1);
isnt($empty->{rc}, 0, 'mock success without an RPM cannot close the owner build');
like($empty->{log}, qr/No binary RPMs were collected/, 'empty results reach the existing collection gate');
for my $skip (0, 1) {
my $unsigned = scenario("unsigned-$skip", unsigned => 1, success => 1,
options => ['--no-verify-repo', ($skip ? ('--skip-build', '--collect-dir', "$tmp/fixture/RPMS/noarch") : ())]);
isnt($unsigned->{rc}, 0, 'unsigned native publication is rejected even when verification is disabled');
like($unsigned->{log}, qr/openEuler repository publication requires --gpg-sign/, 'the owner reports the signing requirement');
is_deeply($unsigned->{calls}, [], 'unsigned publication fails before every mock action');
my $sentinel = "$unsigned->{repo}/openeuler20.03sp4/x86_64/sentinel";
is(-f $sentinel ? read_binary($sentinel) : '', 'previous repository', 'rejection preserves the previous repository');
my $metadata = "$unsigned->{repo}/openeuler20.03sp4/x86_64/xcat-dep.repo";
is(-f $metadata ? read_binary($metadata) : '', '', 'rejection emits no misleading native repository configuration');
}
my $collected = scenario('collection', success => 1, options => ['--no-verify-repo']);
is($collected->{rc}, 0, 'the debug collection path accepts successful RPM-producing command output')
or diag($collected->{log});
my $published = "$collected->{repo}/openeuler20.03sp4/x86_64/python3-scp-0.14.5-1.noarch.rpm";
ok(-f $published, 'native binary reaches the exact repository subdirectory');
is(capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $published),
capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm"),
'signing preserves the collected RPM header and payload digest') if -f $published;
ok(-s "$collected->{repo}/openeuler20.03sp4/x86_64/repodata/repomd.xml.key",
'signed native publication exports its configured repository key');
ok(-f "$collected->{out}/mockbuild-all/$target-source-contract/repo-src/python3-scp-0.14.5-1.src.rpm",
'the existing collector also retains the generated source RPM');
my $unsigned_fixture = "$tmp/fixture/RPMS/noarch/python3-scp-0.14.5-1.noarch.rpm";
for my $case ([$target, 'openeuler20.03sp4/x86_64', 'rh20.03sp4/x86_64'],
['alma+epel-9-x86_64', 'rh9/x86_64', 'openeuler9/x86_64']) {
my ($cell_target, $cell, $decoy) = @$case;
my $result = scenario("carry-$cell_target", missing => 1, target => $cell_target,
published => {$cell => $published, $decoy => $unsigned_fixture},
options => ['--skip-xcat-dep', '--gpg-sign', '--gpg-home', $key_home, '--gpg-key-name', $key_name]);
is($result->{rc}, 0, "$cell_target carries the signed skipped package from its own published cell")
or diag($result->{log});
my $carried = "$result->{out}/mockbuild-all/$cell_target-source-contract/repo/x86_64/python3-scp-0.14.5-1.noarch.rpm";
ok(-f $carried, "$cell_target includes the carried package in this run's repository");
is(capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $carried),
capture_command('rpm', '-qp', '--qf', '%{SIGMD5}', $published), "$cell_target preserves the carried payload") if -f $carried;
is_deeply($result->{calls}, [], "$cell_target carry-over executes no build command");
}
my $untrusted = scenario('carry-unsigned', missing => 1,
published => {'openeuler20.03sp4/x86_64' => $unsigned_fixture}, options => ['--skip-xcat-dep']);
isnt($untrusted->{rc}, 0, 'unsigned native carry-over fails before publication');
like($untrusted->{log}, qr/not signed by the configured key/, 'native carry-over reports the trust failure');
is(read_binary("$untrusted->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
'rejected native carry-over preserves the published repository');
my $wrong_cell = scenario('carry-wrong-cell', missing => 1,
published => {'rh20.03sp4/x86_64' => $published}, options => ['--skip-xcat-dep']);
isnt($wrong_cell->{rc}, 0, 'a signed package in the EL-shaped path cannot fill a native cell');
like($wrong_cell->{log}, qr/MISSING python3-scp/, 'the native gate reports the package absent from its own cell');
for my $held ('openeuler20.03sp4/x86_64', 'rh20.03sp4/x86_64') {
my $native = $held =~ /^openeuler/;
my $result = scenario($native ? 'native-lock' : 'decoy-lock', missing => 1,
hold_cell => $held, published => {'openeuler20.03sp4/x86_64' => $published},
options => ['--skip-xcat-dep', '--try-unlock-timeout', 0]);
if ($native) {
isnt($result->{rc}, 0, 'the native published cell lock excludes a second publisher');
like($result->{log}, qr/Trying to unlock \Q$result->{lock_path}\E failed/,
'the refusal names the lock beside the native published cell');
is(read_binary("$result->{repo}/openeuler20.03sp4/x86_64/sentinel"), 'previous repository',
'native lock refusal preserves the published repository');
ok(!-d "$result->{out}/mockbuild-all/$target-source-contract",
'native lock refusal precedes carry-over and collection');
} else {
is($result->{rc}, 0, 'an EL-shaped decoy lock does not block native carry-over and deployment')
or diag($result->{log});
ok(-f "$result->{repo}/openeuler20.03sp4/x86_64/python3-scp-0.14.5-1.noarch.rpm",
'the unlocked native cell receives the carried package');
my $metadata = "$result->{repo}/openeuler20.03sp4/x86_64/xcat-dep.repo";
my $config = -f $metadata ? read_binary($metadata) : '';
like($config, qr{^baseurl=.*\/openeuler20\.03sp4/x86_64$}m,
'the native repository configuration names the deployed cell');
}
is_deeply($result->{calls}, [], "$held lock case runs no package builder");
is($result->{lock_after}, $result->{lock_before}, "$held remains held by its original owner");
}
for my $name ('native-only', 'mixed', 'legacy-locked') {
my $root = "$tmp/finalize-$name";
my @native = ("$root/openeuler20.03sp4/x86_64", "$root/openeuler24.03/ppc64le");
make_path(@native);
write_binary("$_/marker", 'native repository') for @native;
my @held_cells = @native;
my ($x, $p) = ("$root/rh9/x86_64", "$root/rh9/ppc64le");
my ($xrpm, $prpm) = ('xCAT-genesis-base-x86_64-2.19.1-1.noarch.rpm',
'xCAT-genesis-base-ppc64-2.19.1-1.noarch.rpm');
if ($name ne 'native-only') {
make_path($x, $p);
copy($published, "$x/$xrpm") or die $!;
copy($published, "$p/$prpm") or die $!;
push @held_cells, $p;
push @held_cells, $x if $name eq 'legacy-locked';
}
my @held;
for my $cell (@held_cells) {
my $path = dirname($cell) . '/.' . basename($cell) . '.lock';
my $lock = XCAT::NFSLock->acquire($path, quiet => 1);
push @held, [$lock, $path, read_binary("$path/metadata")];
}
my $logfile = "$tmp/finalize-$name.log";
my $rc = run_capture($logfile, $^X, $collector, '--finalize-xcat-dep',
'--x86_64-repo', $root, '--ppc64le-repo', $root, '--finalize-arch', 'x86_64',
'--build-timestamp', $epoch, '--try-unlock-timeout', 0, '--no-verify-repo');
my $log = read_binary($logfile);
if ($name eq 'legacy-locked') {
isnt($rc, 0, 'finalization refuses a held legacy destination lock');
like($log, qr{Trying to unlock \Q$root\E/rh9/\.x86_64\.lock failed},
'finalization refuses the same sibling lock as an EL publisher');
ok(!-e "$x/$prpm", 'lock refusal precedes the legacy cross-copy');
ok(!-d "$x/repodata", 'lock refusal precedes legacy reindexing');
} else {
is($rc, 0, "$name finalization ignores held native and unselected cell locks") or diag($log);
if ($name eq 'mixed') {
ok(-f "$x/$prpm", 'selected legacy destination receives its foreign Genesis');
is(-f "$x/$prpm" ? digest_file("$x/$prpm") : undef, digest_file("$p/$prpm"),
'legacy cross-copy preserves the source RPM');
ok(-f "$x/repodata/repomd.xml", 'selected legacy destination is reindexed');
ok(!-e "$p/$xrpm", 'unselected legacy source receives no foreign Genesis');
ok(!-d "$p/repodata", 'unselected legacy source is not reindexed');
} else {
like($log, qr/openEuler.*skipping/, 'native-only finalization explains the skip');
unlike($log, qr/(?:acquired|took-over) repository cell lock/,
'native-only finalization acquires no cell lock');
}
}
for my $native (@native) {
is_deeply([glob("$native/*")], ["$native/marker"], "$name adds no native artifacts");
is(read_binary("$native/marker"), 'native repository', "$name preserves native content");
}
for my $held (@held) {
my ($lock, $path, $before) = @$held;
is(-f "$path/metadata" ? read_binary("$path/metadata") : undef, $before,
"$name preserves the existing owner of $path");
$lock->release;
}
}
my $skipped = scenario('skip-dep', missing => 1, options => ['--skip-xcat-dep', '--dry-run']);
is($skipped->{rc}, 0, 'skipping dependency builds does not require the source RPM');
is_deeply($skipped->{calls}, [], 'skip-dep executes no source action');
my $replay = scenario('replay', missing => 1, options => ['--skip-build', '--no-verify-repo',
'--collect-dir', "$tmp/fixture/RPMS/noarch"]);
is($replay->{rc}, 0, 'build-free artifact collection does not require the original source RPM');
is_deeply($replay->{calls}, [], 'build-free collection executes no source action');
my $incomplete = scenario('incomplete', success => 1, packages => "python3-scp=0.14.5\nclosure-gap=1");
isnt($incomplete->{rc}, 0, 'a successful source rebuild does not bypass the manifest gate');
like($incomplete->{log}, qr/MISSING closure-gap\b/, 'the manifest gate identifies the missing required package');
test_genesis_signing();
done_testing();
sub test_genesis_signing {
for my $home ('explicit', 'default', 'environment', 'relative-explicit', 'relative-environment', 'relative-missing') {
my $relative = File::Spec->abs2rel($key_home, cwd());
$relative .= '/not-created' if $home eq 'relative-missing';
my $environment = $home =~ /environment/ ? ($home eq 'environment' ? $key_home : $relative) : '';
my $result = scenario("genesis-$home", packages => 'xCAT-genesis-base=2.19.0',
missing => 1, default_home => ($home eq 'default' || $home =~ /environment/ ? 1 : 0), env_home => $environment,
key_home => $home =~ /^relative-/ ? $relative : $key_home,
options => ['--dry-run', '--no-skip-genesis', '--skip-xcat-dep']);
is($result->{rc}, 0, "$home keyring native Genesis planning completes") or diag($result->{log});
my ($command) = grep { /^\+ .*buildrpms\.pl/ } split /\n/, $result->{log};
my $home_path = $home eq 'default' ? "$result->{root}/.gnupg" : $key_home;
$home_path = File::Spec->rel2abs($relative, cwd()) if $home =~ /^relative-/;
like($command // '', qr/--gpg-sign --gpg-key-name '\Q$key_name\E' --gpg-home '\Q$home_path\E'/,
"$home parent signing identity reaches the Genesis child despite its private HOME");
is_deeply($result->{calls}, [], "$home Genesis planning runs no mock command");
isnt($result->{root}, cwd(), "$home child source directory differs from the parent signing directory");
like($result->{log}, qr/\(cwd: \Q$result->{root}\E\)/, "$home child command runs in its source directory");
}
my $legacy_genesis = scenario('genesis-legacy', target => 'alma+epel-9-x86_64', packages => 'xCAT-genesis-base=2.19.0',
missing => 1,
options => ['--dry-run', '--no-skip-genesis', '--skip-xcat-dep', '--gpg-sign', '--gpg-home', $key_home, '--gpg-key-name', $key_name]);
is($legacy_genesis->{rc}, 0, 'legacy signed owner still plans Genesis');
my ($legacy_command) = grep { /^\+ .*buildrpms\.pl/ } split /\n/, $legacy_genesis->{log};
like($legacy_command // '', qr/--package xCAT-genesis-base/, 'legacy plan contains the production child command');
unlike($legacy_command // '', qr/--gpg-(?:sign|home|key-name)/, 'legacy Genesis child invocation remains unchanged');
}