mirror of
https://github.com/webmin/webmin.git
synced 2026-10-03 02:40:39 +01:00
386 lines
12 KiB
Perl
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();
|