2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-05 04:27:55 +00:00
Files
xcat-core/xCAT-test/unit/buildrpms_source_only.t
T
Daniel Hilst 63c9ef8a1f fix(build): stop builddebs.pl leaving the checkout unbuildable and dirty
A second run in the same checkout died once the release string moved:

  dpkg-genbuildinfo: error: cannot fstat file ../xcat_..._ppc64el.deb

debian/files accumulates one line per artifact and survives `dh_clean -d`,
which removes directories only. The next build's dpkg-genchanges reads the
stale entries and fstats artifacts collect_debs already moved away. The old
shell builder deleted debian/files explicitly; that step was not carried
over. --force does not help, since it only wipes the output repository.

Three further ways the build did not put the tree back as it found it:

xCAT/postscripts/{bmcsetup,getipmi} are TRACKED files that the xCAT build
rewrites from the genesis sources. They were recorded as created, so
cleanup deleted them from the checkout. They are claimed now.

Restoring a claimed file lost its mode: File::Copy::copy does not carry
permissions, so an executable came back 100644 with identical content --
visible only as a git mode change. backup_file/restore_file record and
reapply it.

debian/*.substvars are rewritten in place and several are tracked; they are
claimed too. debhelper's .debhelper/ and *.debhelper.log are never tracked
and are removed with the rest of the residue.

clean_debian_residue runs from collect_debs, which is OUTSIDE
with_prepared_tree -- the restore has already happened by then. That is why
it must not remove *.substvars: doing so would delete the tracked ones it
just put back. The claim mechanism handles those instead.

Verified on xcat-master-ub: two consecutive full 14-package builds in one
checkout both succeed, and a build against a git checkout now leaves zero
tracked files modified or deleted (was five, two of them deleted). The
remaining untracked residue -- pods/, share/, pod2htmd.tmp and the
substvars of packages that do not track one -- is inherited from
build-ubunturepo and unchanged here.

Also covers the guard that gives this PR its name: deleting
`return if $opts{source_only}` from buildall left buildrpms_source_only.t
green, so nothing checked that --source-only skips the binary rebuild. It
does now, and the mutation reddens two assertions. Same for the changelog
trailer: making its substitution global left the suite green because only
the older header was asserted, not its author and date.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
2026-09-02 12:39:44 -03:00

168 lines
6.5 KiB
Perl

#!/usr/bin/env perl
# buildrpms.pl --source-only: build the source rpms and stop.
#
# buildrpms.pl cannot be loaded -- it runs mkdir/git/read_text at file scope and
# expects a working tree -- so the routines whose behaviour changed are lifted out
# with a regex and eval'd into a scratch package with their collaborators stubbed,
# per the repo's code standard. The CLI contract is exercised by running the real
# program.
use strict;
use warnings;
use Cwd qw(getcwd);
use File::Copy ();
use File::Slurper qw(read_text);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use lib "$FindBin::Bin/../lib";
use Test::More;
use XCAT::Test::File qw(repo_path);
my $builder = repo_path('buildrpms.pl');
plan skip_all => 'buildrpms.pl not found' unless -r $builder;
my $source = read_text($builder);
# ---------------------------------------------------------------- extraction --
# Lift the two routines that decide what a source-only run publishes. BAIL_OUT
# rather than skip: if the extraction stops matching, this file would silently
# cover nothing.
my %routine;
for my $name (qw(index_repo write_repo_metadata_dir buildall)) {
my ($body) = $source =~ /\n(sub \Q$name\E \{.*?\n\})\n/s;
BAIL_OUT("could not extract $name from buildrpms.pl") unless $body;
$routine{$name} = $body;
}
our @CREATEREPO;
our @METADATA_WRITTEN;
our @STAGES;
{
package Scratch;
no warnings 'redefine';
# Collaborators the lifted code calls. Each records instead of acting.
sub createrepo_dir { push @main::CREATEREPO, $_[0]; }
# The four stages buildall drives. Each records that it was reached.
sub createmockconfig { push @main::STAGES, 'createmockconfig'; }
sub buildsources { push @main::STAGES, 'buildsources'; }
sub buildspkgs { push @main::STAGES, 'buildspkgs'; }
sub buildpkgs { push @main::STAGES, 'buildpkgs'; }
}
# %opts lives in the scratch package and is set directly. Aliasing it to a hash in
# main and localising that does NOT work: local swaps the container, so the lifted
# code keeps reading the hash the glob pointed at before.
my $harness = join "\n",
'package Scratch;',
'use strict; use warnings;',
'our %opts;',
'sub say { }',
# write_repo_metadata_dir does real work past the guard; stop it there so the
# test observes the guard and nothing else.
$routine{index_repo},
$routine{buildall},
($routine{write_repo_metadata_dir} =~ s/(return if \$opts\{source_only\};).*\n\}\z/$1\n push \@main::METADATA_WRITTEN, \$repodir;\n return 1;\n}/sr),
'1;';
eval $harness or BAIL_OUT("could not evaluate the extracted routines: $@");
sub run_index {
my (%args) = @_;
%Scratch::opts = (source_only => $args{source_only});
local @CREATEREPO = ();
my $dir = tempdir(CLEANUP => 1);
mkdir File::Spec->catdir($dir, 'SRPMS');
Scratch::index_repo($dir);
return [map { $_ eq $dir ? 'BINARY' : 'SRPMS' } @CREATEREPO];
}
# ------------------------------------------------------------------ behaviour --
is_deeply( run_index(source_only => 0), ['BINARY', 'SRPMS'],
'a normal run indexes the binary repository and the srpms' );
is_deeply( run_index(source_only => 1), ['SRPMS'],
'a source-only run indexes only the srpms' );
# The reason for the guard: re-indexing a directory with no binaries in it would
# replace working metadata with metadata for an empty repository.
ok( !grep({ $_ eq 'BINARY' } @{ run_index(source_only => 1) }),
'a source-only run leaves the binary metadata alone' );
{
%Scratch::opts = (source_only => 1);
local @METADATA_WRITTEN = ();
my $dir = tempdir(CLEANUP => 1);
Scratch::write_repo_metadata_dir($dir);
is_deeply( \@METADATA_WRITTEN, [],
'a source-only run emits no .repo file for packages it did not build' );
}
{
%Scratch::opts = (source_only => 0);
local @METADATA_WRITTEN = ();
my $dir = tempdir(CLEANUP => 1);
Scratch::write_repo_metadata_dir($dir);
is_deeply( \@METADATA_WRITTEN, [$dir],
'a normal run still emits the repository metadata' );
}
# ------------------------------------------------------------------ the build --
# The point of the option: the source rpm is built and the binary rebuild is not.
# Without this, removing the guard from buildall leaves every other assertion in
# this file green -- the repository ones only observe what index_repo does.
sub stages_for {
my ($source_only) = @_;
%Scratch::opts = (source_only => $source_only);
local @STAGES = ();
Scratch::buildall('xCAT-vlan', 'alma+epel-9-x86_64');
return [@STAGES];
}
is_deeply( stages_for(0),
[qw(createmockconfig buildsources buildspkgs buildpkgs)],
'a normal run builds the source rpm and then rebuilds it into binaries' );
is_deeply( stages_for(1),
[qw(createmockconfig buildsources buildspkgs)],
'a source-only run stops once the source rpm exists' );
ok( !grep( { $_ eq 'buildpkgs' } @{ stages_for(1) } ),
'and never enters the binary rebuild, which is the expensive half' );
# ------------------------------------------------------------------- the CLI --
# Run the real program. --source-only and --merge-core-repos are different modes:
# one builds, the other assembles trees that are already built.
#
# Run it from a copy, never from the checkout: before it looks at @ARGV,
# buildrpms.pl rewrites the tracked Gitinfo in its working directory and creates
# $HOME/rpmbuild. Running it in place left the developer's tree dirty and reached
# into their home for a test that only exercises argument parsing. Version is
# staged because the same file-scope code reads it and dies without it.
my $sandbox = tempdir(CLEANUP => 1);
for my $needed (qw(buildrpms.pl Version)) {
my $from = repo_path($needed);
BAIL_OUT("$needed is missing from the repository") unless -r $from;
File::Copy::copy($from, File::Spec->catfile($sandbox, $needed))
or BAIL_OUT("could not stage $needed: $!");
}
my $cwd = getcwd();
chdir $sandbox or BAIL_OUT("cannot chdir to the sandbox: $!");
local $ENV{HOME} = $sandbox;
my $out = qx($^X buildrpms.pl --source-only --merge-core-repos 2>&1);
my $rc = $? >> 8;
chdir $cwd;
# 2 is usage()'s exit code, but perl also exits 2 when compilation aborts, so the
# status alone does not say the option check ran -- it passed in CI while
# buildrpms.pl could not even load Parallel::ForkManager. The message below is
# what distinguishes the two; this only pins the code usage() is meant to use.
is( $rc, 2, 'combining --source-only with --merge-core-repos is refused' );
like( $out, qr/--source-only and --merge-core-repos/,
'and the refusal names both options' );
done_testing();