Files
webmin/t/software-yum.t

386 lines
12 KiB
Perl

#!/usr/local/bin/perl
use strict;
use warnings;
use Test::More;
use File::Basename qw(dirname);
use File::Spec;
use Cwd qw(abs_path);
our (%config, %packages, %text);
our ($yum_command, $supports_dnf_versionlock, $dnf_version,
$update_system_error);
sub has_command
{
return $_[0] eq 'dnf' ? '/usr/bin/dnf' : undef;
}
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..'));
chdir($root) or die "chdir($root): $!";
do './software/yum-lib.pl' or die $@ || $!;
{
no warnings qw(once redefine);
local *clean_language = sub { };
local *reset_environment = sub { };
local *backquote_command = sub {
return $_[0] =~ /--version/ ? "4.14.0\n" :
" versionlock control package version locks\n";
};
local $dnf_version;
local $supports_dnf_versionlock;
is(get_dnf_version(), 4, 'detects DNF 4');
ok(supports_update_system_holds(),
'detects the DNF 4 versionlock command');
is(update_system_hold_flags(), '--disableplugin=versionlock',
'uses the DNF 4 held-update discovery option');
}
{
no warnings qw(once redefine);
local *clean_language = sub { };
local *reset_environment = sub { };
local *backquote_command = sub {
return $_[0] =~ /--version/ ? "dnf5 version 5.4.2.1\n" :
" versionlock Manage versionlock configuration\n";
};
local $dnf_version;
local $supports_dnf_versionlock;
is(get_dnf_version(), 5, 'detects DNF 5');
ok(supports_update_system_holds(),
'detects the DNF 5 versionlock command');
is(update_system_hold_flags(), '--setopt=disable_excludes=*',
'uses the DNF 5 held-update discovery option');
}
{
no warnings qw(once redefine);
local *clean_language = sub { };
local *reset_environment = sub { };
local *backquote_command = sub {
return $_[0] =~ /--version/ ? "4.14.0\n" :
"No such command: versionlock\n";
};
local $dnf_version;
local $supports_dnf_versionlock;
ok(!supports_update_system_holds(),
'hides holds when DNF does not expose versionlock');
}
{
no warnings qw(once redefine);
my $output =
"bash-0:5.1.8-8.el9.*\n".
"python3-*\n".
"0:python3-pip-21.2.3-8.el9.*\n".
"!blocked-0:2.0-1.el9.*\n";
local *supports_update_system_holds = sub { return 1; };
local *get_dnf_version = sub { return 4; };
local *clean_language = sub { };
local *reset_environment = sub { };
local *open_execute_command = sub {
my ($fh) = @_;
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
or die "open simulated DNF 4 locks: $!";
};
is_deeply([ list_update_system_holds() ], [ qw(bash python3-pip) ],
'lists DNF 4 exact locks without raw patterns or excludes');
}
{
no warnings qw(once redefine);
my $output =
"# Added by 'dnf versionlock add nano'\n".
"Package name: nano\n".
"evr = 8.7.1-2.fc44\n\n".
"Package name: coreutils\n".
"evr != 9.10-4.fc44\n\n".
"Package name: bash\n".
"evr = 5.3.0-2.fc44\n".
"arch = aarch64\n\n".
"Package name: python3-*\n".
"evr = 3.14.0-1.fc44\n\n".
"Package name: nano\n".
"evr > 9\n";
local *supports_update_system_holds = sub { return 1; };
local *get_dnf_version = sub { return 5; };
local *clean_language = sub { };
local *reset_environment = sub { };
local *open_execute_command = sub {
my ($fh) = @_;
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
or die "open simulated DNF 5 locks: $!";
};
is_deeply([ list_update_system_holds() ], [ ],
'skips DNF 5 names with duplicate, custom or glob lock rules');
$output = "Package name: nano\nevr = 8.7.1-2.fc44\n";
is_deeply([ list_update_system_holds() ], [ 'nano' ],
'lists a simple DNF 5 exact lock by package name');
}
{
no warnings qw(once redefine);
is(update_system_hold_spec_name('bash-0:5.1.8-8.el9.*'), 'bash',
'decodes DNF 4 name-epoch-version entries');
is(update_system_hold_spec_name('0:python3-pip-21.2.3-8.el9.*'),
'python3-pip', 'decodes legacy DNF 4 epoch-name entries');
is(update_system_hold_spec_name('python3-*'), undef,
'does not treat a raw pattern as an exact lock');
is(update_system_hold_spec_name('!bash-0:5.1.8-8.el9.*'), undef,
'does not treat an exclude as an exact lock');
is(update_system_nevra_name('nano-0:8.7.1-2.fc44'), 'nano',
'extracts a DNF 5 package name from transaction output');
is(update_system_nevra_name(
'webmin-virtualmin-support-2:4.3.202602061237-1.noarch'),
'webmin-virtualmin-support', 'extracts a hyphenated NEVRA name');
is(update_system_nevra_name('nano-8.7.1-2.fc44.aarch64'), 'nano',
'extracts a package name when no epoch is printed');
}
{
no warnings qw(once redefine);
my @commands;
local *supports_update_system_holds = sub { return 1; };
local *unique = sub {
my %seen;
return grep { !$seen{$_}++ } @_;
};
local *clean_language = sub { };
local *reset_environment = sub { };
local *trim = sub {
my ($value) = @_;
$value =~ s/^\s+|\s+$//g;
return $value;
};
local *list_update_system_holds = sub { return ('bash'); };
local *execute_command_logged = sub {
my ($command, undef, $stdout) = @_;
push(@commands, $command);
$$stdout = '';
return 0;
};
is(update_system_hold([ 'bash', 'bash', 'coreutils' ], 1), undef,
'adds DNF version locks successfully');
is(update_system_hold([ 'bash', 'coreutils' ], 0), undef,
'removes only package names reported as exactly locked');
is_deeply(\@commands,
[ '/usr/bin/dnf -q versionlock add bash',
'/usr/bin/dnf -q versionlock add coreutils',
'/usr/bin/dnf -q versionlock delete bash' ],
'uses name-based add and delete commands on both DNF generations');
}
{
no warnings qw(once redefine);
my $dnf_output =
"[3/6] Upgrading bash-0:5.3.0-2.fc44 100% | 1.0 MiB/s | 1.0 MiB | 00m01s\n";
my $executed_command;
my @lock_actions;
local *update_system_hold_flags = sub { return 'held-update'; };
local *append_architectures = sub { return @_; };
local *package_info = sub { return ('bash'); };
local *update_system_repo = sub { return undef; };
local *additional_log = sub { };
local *html_escape = sub { return $_[0]; };
local *text = sub { return $_[0]; };
local *unique = sub {
my %seen;
return grep { !$seen{$_}++ } @_;
};
local *delete_update_system_holds = sub {
my ($packages, $removed) = @_;
push(@lock_actions, [ 'delete', [ @$packages ] ]);
push(@$removed, 'bash');
return undef;
};
local *restore_update_system_holds = sub {
my ($packages) = @_;
push(@lock_actions, [ 'restore', [ @$packages ] ]);
return undef;
};
local *open_execute_command = sub {
my ($fh, $command) = @_;
$executed_command = $command;
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$dnf_output)
or die "open simulated DNF install: $!";
};
my $printed = '';
open(my $stdout, '>', \$printed) or die "open captured stdout: $!";
local *STDOUT = $stdout;
$? = 0;
my @installed = update_system_install('bash', { }, 1, 'held-update');
is_deeply(\@installed, [ 'bash' ],
'returns a DNF 5 package updated while held');
unlike($executed_command, qr/held-update|disableplugin|disable_excludes/,
'runs the transaction without a global versionlock bypass');
is_deeply(\@lock_actions,
[ [ 'delete', [ 'bash' ] ], [ 'restore', [ 'bash' ] ] ],
'temporarily unlocks and then re-locks only the selected package');
}
{
no warnings qw(once redefine);
my $dnf_output =
"bash.aarch64 5.1.8-10.el9 baseos\n".
"coreutils.aarch64 8.32-40.el9 baseos\n";
my $dnf_stderr = '';
my $dnf_status = 100;
my @commands;
local *supports_update_system_holds = sub { return 1; };
local *list_update_system_holds = sub { return ('bash'); };
local *set_yum_security_field = sub { };
local *get_dnf_version = sub { return 4; };
local *transname = sub { return 'simulated-stderr-file'; };
local *read_file_contents = sub { return $dnf_stderr; };
local *execute_command = sub {
my ($command, $stdin, $stdout) = @_;
push(@commands, $command);
$$stdout = $dnf_output;
return $dnf_status << 8;
};
my @normal = update_system_updates(0);
is_deeply([ map { $_->{'name'} } @normal ], [ 'coreutils' ],
'DNF 4 regular updates exclude held packages');
is($update_system_error, undef, 'a successful check reports no error');
my @with_holds = update_system_updates(1);
is_deeply([ map { $_->{'name'} } @with_holds ],
[ 'bash', 'coreutils' ], 'DNF 4 held-update query includes locks');
like($commands[1], qr/--disableplugin=versionlock check-update/,
'DNF 4 uses its plugin bypass for held-update discovery');
@commands = ( );
local *get_dnf_version = sub { return 5; };
@with_holds = update_system_updates(1);
like($commands[0], qr/--setopt=disable_excludes=\\\* check-update/,
'DNF 5 disables excludes for held-update discovery');
ok($with_holds[0]->{'held'}, 'marks a DNF 5 locked update as held');
$dnf_output = '';
$dnf_status = 0;
is_deeply([ update_system_updates(0) ], [ ],
'DNF exit status 0 means no updates');
is($update_system_error, undef, 'no updates is not reported as an error');
$dnf_output = "Some repo 0.0 B/s | 0 B 00:00\n";
$dnf_stderr =
"Errors during downloading metadata for repository 'x':\n".
"Error: Failed to download metadata for repo 'x'\n";
$dnf_status = 1;
is_deeply([ update_system_updates(0) ], [ ],
'a failed check returns no updates');
is($update_system_error,
"Errors during downloading metadata for repository 'x':\n".
"Error: Failed to download metadata for repo 'x'",
'a failed check reports the DNF error output');
$dnf_stderr = '';
update_system_updates(0);
is($update_system_error, 'Some repo 0.0 B/s | 0 B 00:00',
'a failed check without error output reports its normal output');
$dnf_output = '';
update_system_updates(0);
is($update_system_error, '/usr/bin/dnf check-update exited with status 1',
'a silent failure reports the exit status');
$dnf_status = 137;
update_system_updates(0);
is($update_system_error, '/usr/bin/dnf check-update exited with status 137',
'a killed check is reported as a failure');
}
{
no warnings qw(once redefine);
my $dnf_output =
"Last metadata expiration check: 0:10:00 ago.\n".
"Dependencies resolved.\n".
"================================================================\n".
" Package Architecture Version Repository Size\n".
"================================================================\n".
"Upgrading:\n".
" tzdata noarch 2026c-1.fc44 updates 497 k\n".
" vim-minimal aarch64 2:9.1.2000-2.fc44 updates 647 k\n".
"Installing dependencies:\n".
" wbt-virtual-server-theme\n".
" noarch 21.20-1 virtualmin 2.5 M\n".
"Removing dependent packages:\n".
" oldpkg noarch 1.0-1 system 1 k\n".
"\nTransaction Summary:\n".
"Upgrade 2 Packages\n";
my $command;
my @lock_actions;
my $dnf_major = 4;
local *update_system_hold_flags = sub {
return '--setopt=disable_excludes=*';
};
local *get_dnf_version = sub { return $dnf_major; };
local *open_execute_command = sub {
my ($fh, $cmd) = @_;
$command = $cmd;
no strict 'refs';
open(ref($fh) ? $fh : \*{$fh}, '<', \$dnf_output)
or die "open simulated DNF transaction: $!";
};
local *clean_language = sub { };
local *reset_environment = sub { };
local *trim = sub {
my ($value) = @_;
$value =~ s/^\s+|\s+$//g;
return $value;
};
local *unique = sub {
my %seen;
return grep { !$seen{$_}++ } @_;
};
local *delete_update_system_holds = sub {
my ($packages, $removed) = @_;
push(@lock_actions, [ 'delete', [ @$packages ] ]);
push(@$removed, @$packages);
return undef;
};
local *restore_update_system_holds = sub {
my ($packages) = @_;
push(@lock_actions, [ 'restore', [ @$packages ] ]);
return undef;
};
my @ops = update_system_operations('tzdata vim-minimal');
like($command, qr{^/usr/bin/dnf --assumeno install },
'uses a simulated install for DNF 4 operations');
unlike($command, qr/disableplugin|disable_excludes/,
'keeps versionlock active for regular operations');
is_deeply([ map { $_->{'name'} } @ops ],
[ qw(tzdata vim-minimal wbt-virtual-server-theme) ],
'parses install and upgrade rows from a DNF transaction table');
$dnf_major = 5;
@ops = update_system_operations('tzdata vim-minimal',
'--setopt=disable_excludes=*');
like($command, qr{^/usr/bin/dnf --assumeno upgrade },
'uses a simulated upgrade for installed packages on DNF 5');
unlike($command, qr/disableplugin|disable_excludes/,
'does not pass the discovery-only bypass to a transaction preview');
is_deeply(\@lock_actions,
[ [ 'delete', [ qw(tzdata vim-minimal) ] ],
[ 'restore', [ qw(tzdata vim-minimal) ] ] ],
'temporarily unlocks selected packages for a held-update preview');
is_deeply([ map { $_->{'name'} } @ops ],
[ qw(tzdata vim-minimal wbt-virtual-server-theme) ],
'parses the held-package preview after restoring its locks');
is($ops[1]->{'epoch'}, '2', 'splits epochs from preview versions');
is($ops[2]->{'arch'}, 'noarch', 'handles wrapped package names');
}
done_testing();