From 76d8dfdd14a4f39ea0b49ff4910d4deecb5d437e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:48:40 -0300 Subject: [PATCH] test(otherpkgs): move sandbox checks to BATS --- xCAT-test/bats/otherpkgs_upgrade_scope.bats | 239 ++++++++++++++++++++ xCAT-test/unit/otherpkgs_upgrade_scope.t | 222 ------------------ 2 files changed, 239 insertions(+), 222 deletions(-) create mode 100644 xCAT-test/bats/otherpkgs_upgrade_scope.bats delete mode 100644 xCAT-test/unit/otherpkgs_upgrade_scope.t diff --git a/xCAT-test/bats/otherpkgs_upgrade_scope.bats b/xCAT-test/bats/otherpkgs_upgrade_scope.bats new file mode 100644 index 000000000..d924f932b --- /dev/null +++ b/xCAT-test/bats/otherpkgs_upgrade_scope.bats @@ -0,0 +1,239 @@ +#!/usr/bin/env bats + +load 'helpers/shell_source' + +setup() +{ + [ "$(uname -s)" = Linux ] || skip 'otherpkgs filesystem isolation requires Linux' + local utility executable + local utilities=(bash sh basename dirname cat cp expr grep ls mkdir rm uname wc) + local bwrap + bwrap=$(PATH=/usr/bin:/bin type -P bwrap) || { + echo 'Install bubblewrap to run the otherpkgs test' >&2 + return 1 + } + postscripts=$(repo_path xCAT/postscripts) + fixture="$BATS_TEST_TMPDIR/fixture" + mkdir -p "$fixture/bin" + sandbox=(env -i PATH=/usr/bin:/bin LC_ALL=C "$bwrap" + --unshare-all --die-with-parent --new-session + --ro-bind / / --tmpfs /etc --tmpfs /usr/bin --tmpfs /tmp + --proc /proc --dev /dev --setenv PATH /usr/bin --setenv LC_ALL C) + if PATH=/usr/bin:/bin type -P coreutils >/dev/null; then + utilities+=(coreutils) + fi + for utility in "${utilities[@]}"; do + executable=$(PATH=/usr/bin:/bin type -P "$utility") || { + echo "Required utility is unavailable: $utility" >&2 + return 1 + } + sandbox+=(--ro-bind "$executable" "/usr/bin/$utility") + done + run "${sandbox[@]}" /usr/bin/sh -c 'test ! -e /etc/os-release' + if [ "$status" -ne 0 ]; then + echo "Cannot isolate otherpkgs: $output" >&2 + return 1 + fi +} + +run_case() +{ + local manager=$1 scenario=$2 + local verbose='' remote='' mounted='' repoonly='' multiple='' empty='' + local upgrade_status=0 install_status=0 + case "$scenario" in + http) verbose=1; remote=1 ;; + mounted) mounted=1 ;; + upgrade_failure) upgrade_status=17; verbose=1 ;; + install_failure) install_status=23; verbose=1 ;; + repoonly) repoonly=1; remote=1 ;; + multiple) multiple=1; verbose=1 ;; + empty) remote=1; empty=1 ;; + esac + printf '#!/usr/bin/sh\nexit 0\n' >"$fixture/bin/logger" + printf '#!/usr/bin/sh\nexit 1\n' >"$fixture/bin/dpkg" + printf '#!/usr/bin/sh\n[ "$*" = --version ]\n' >"$fixture/bin/rpm" + if [ "$mounted" ]; then + printf '#!/usr/bin/sh\nprintf "%%s\\n" "package-server:/install on /install type nfs (rw)"\n' >"$fixture/bin/mount" + else + printf '#!/usr/bin/sh\nexit 0\n' >"$fixture/bin/mount" + fi + cat >"$fixture/bin/$manager" <<'SH' +#!/usr/bin/sh +printf '%s\t' "${0##*/}" "SCOPE_ENV=${SCOPE_ENV:-}" "$@" >> /tmp/fixture/commands +printf '\n' >> /tmp/fixture/commands +sequence=$(wc -l < /tmp/fixture/commands) +mkdir "/tmp/fixture/repos.$sequence" +cp /etc/yum.repos.d/*.repo "/tmp/fixture/repos.$sequence/" 2>/dev/null || : +for argument do + case "$argument" in + upgrade) printf '%s\n' upgrade-result; exit "$UPGRADE_STATUS" ;; + install) printf '%s\n' install-result; exit "$INSTALL_STATUS" ;; + esac +done +exit 0 +SH + chmod +x "$fixture/bin/"* + + local otherpkgdir=/install/other packages=alpha/tool-one,beta/tool-two + local list_count=1 + [ ! "$remote" ] || otherpkgdir=https://packages.example.invalid/extra,/install/other + [ ! "$empty" ] || packages= + [ ! "$multiple" ] || list_count=2 + local command=("${sandbox[@]}" + --bind "$fixture" /tmp/fixture + --ro-bind "$postscripts" /tmp/postscripts --chdir /tmp/fixture) + local tool + for tool in logger dpkg rpm mount "$manager"; do + command+=(--ro-bind "$fixture/bin/$tool" "/usr/bin/$tool") + done + command+=( + --setenv OSVER rhel9 --setenv ARCH x86_64 --setenv UPDATENODE 1 + --setenv NFSSERVER package-server --setenv HTTPPORT 80 + --setenv INSTALLDIR /install --setenv OTHERPKGDIR "$otherpkgdir" + --setenv OTHERPKGS_INDEX "$list_count" --setenv OTHERPKGS1 "$packages" + --setenv ENVLIST1 SCOPE_ENV=first --setenv VERBOSE "$verbose" + --setenv UPGRADE_STATUS "$upgrade_status" --setenv INSTALL_STATUS "$install_status") + if [ "$multiple" ]; then + command+=(--setenv OTHERPKGS2 gamma/tool-three --setenv ENVLIST2 SCOPE_ENV=second) + fi + local runner + runner=$(cat <<'SH' +/usr/bin/bash /tmp/postscripts/otherpkgs "$@" +status=$? +mkdir /tmp/fixture/final-repos +cp /etc/yum.repos.d/*.repo /tmp/fixture/final-repos/ 2>/dev/null || : +exit "$status" +SH + ) + command+=(/usr/bin/sh -c "$runner" otherpkgs-test) + [ ! "$repoonly" ] || command+=(--repoonly) + + run "${command[@]}" + if [ "$status" -ne "$((upgrade_status + install_status))" ]; then + echo "$output" >&2 + return 1 + fi + [ -f "$fixture/commands" ] + + local line operand sequence=0 + local arguments=() paths=() + : >"$fixture/transactions" + while IFS= read -r line; do + sequence=$((sequence + 1)) + IFS=$'\t' read -r -a arguments <<<"$line" + operand= + for operand in "${arguments[@]:2}"; do + [[ "$operand" = -* ]] || break + done + case "$operand" in clean|list) continue ;; esac + printf '%s\n' "$line" >>"$fixture/transactions" + paths=(alpha beta) + [ ! "$empty" ] || paths=() + if [ "$multiple" ] && [ "${arguments[1]}" = SCOPE_ENV=second ]; then + paths=(gamma) + fi + check_repositories "$fixture/repos.$sequence" "${paths[@]}" + done <"$fixture/commands" + + : >"$fixture/expected-transactions" + : >"$fixture/expected-printed" + if [ ! "$repoonly" ]; then + expect_transaction first -y '--disablerepo=*' '--enablerepo=xcat-otherpkgs*' upgrade + if [ ! "$empty" ]; then + expect_transaction first -y install tool-one tool-two + fi + if [ "$multiple" ]; then + expect_transaction second -y '--disablerepo=*' '--enablerepo=xcat-otherpkgs*' upgrade + expect_transaction second -y install tool-three + fi + fi + diff -u "$fixture/expected-transactions" "$fixture/transactions" + + : >"$fixture/printed" + while IFS= read -r line; do + [[ "$line" = SCOPE_ENV=* ]] || continue + read -r -a arguments <<<"$line" + printf '%s\t' "${arguments[@]}" >>"$fixture/printed" + printf '\n' >>"$fixture/printed" + done <<<"$output" + diff -u "$fixture/expected-printed" "$fixture/printed" + + paths=(alpha beta) + [ ! "$empty" ] || paths=() + [ ! "$multiple" ] || paths=(gamma) + check_repositories "$fixture/final-repos" "${paths[@]}" + if [ "$verbose" ] && [ ! "$repoonly" ]; then + [[ $'\n'"$output"$'\n' = *$'\nupgrade-result\n'* ]] + if [ ! "$empty" ]; then + [[ $'\n'"$output"$'\n' = *$'\ninstall-result\n'* ]] + fi + fi +} + +expect_transaction() +{ + local label=$1 + shift + printf '%s\t' "$manager" "SCOPE_ENV=$label" "$@" >>"$fixture/expected-transactions" + printf '\n' >>"$fixture/expected-transactions" + if [ "$verbose" ]; then + printf '%s\t' "SCOPE_ENV=$label" "$manager" "$@" >>"$fixture/expected-printed" + printf '\n' >>"$fixture/expected-printed" + fi +} + +check_repositories() +{ + local directory=$1 + shift + local base=http://package-server:80 index=0 pkgpath file + [ ! "$mounted" ] || base=file:// + { + printf '%s\t%s\t1\t0\n' xCAT-rhel9-path0 "$base/install/rhel9/x86_64/BaseOS" + printf '%s\t%s\t1\t0\n' xCAT-rhel9-path1 "$base/install/rhel9/x86_64/AppStream" + if [ "$remote" ]; then + printf '%s\t%s\t1\t0\n' xcat-otherpkgs0 https://packages.example.invalid/extra + index=1 + fi + for pkgpath do + printf '%s\t%s\t1\t0\n' "xcat-otherpkgs$index" "$base/install/other/$pkgpath" + index=$((index + 1)) + done + } | LC_ALL=C sort >"$fixture/expected-repos" + : >"$fixture/repos" + for file in "$directory/"*.repo; do + [ -f "$file" ] || continue + awk ' + /^\[[^]]+\]$/ { sections = sections substr($0, 2, length($0) - 2) "\t" } + /^(baseurl|enabled|gpgcheck)=/ { + key = substr($0, 1, index($0, "=") - 1) + value = substr($0, index($0, "=") + 1) + sub(/[[:space:]]+$/, "", value) + values[key] = values[key] value "\t" + } + END { + record = sections values["baseurl"] values["enabled"] values["gpgcheck"] + sub(/\t$/, "", record) + print record + } + ' "$file" >>"$fixture/repos" + done + LC_ALL=C sort "$fixture/repos" >"$fixture/sorted-repos" + diff -u "$fixture/expected-repos" "$fixture/sorted-repos" +} + +@test "dnf: HTTP and local repositories" { run_case dnf http; } +@test "dnf: mounted repositories" { run_case dnf mounted; } +@test "dnf: upgrade failure" { run_case dnf upgrade_failure; } +@test "dnf: install failure" { run_case dnf install_failure; } +@test "dnf: repository-only mode" { run_case dnf repoonly; } +@test "dnf: separate package lists" { run_case dnf multiple; } +@test "dnf: remote repository without installs" { run_case dnf empty; } +@test "yum: HTTP and local repositories" { run_case yum http; } +@test "yum: mounted repositories" { run_case yum mounted; } +@test "yum: upgrade failure" { run_case yum upgrade_failure; } +@test "yum: install failure" { run_case yum install_failure; } +@test "yum: repository-only mode" { run_case yum repoonly; } +@test "yum: separate package lists" { run_case yum multiple; } +@test "yum: remote repository without installs" { run_case yum empty; } diff --git a/xCAT-test/unit/otherpkgs_upgrade_scope.t b/xCAT-test/unit/otherpkgs_upgrade_scope.t deleted file mode 100644 index 933564809..000000000 --- a/xCAT-test/unit/otherpkgs_upgrade_scope.t +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use Capture::Tiny qw(capture_merged); -use File::Glob qw(bsd_glob); -use File::Path qw(make_path); -use File::Slurper qw(read_text write_text); -use File::Spec; -use File::Temp qw(tempdir); -use FindBin; -use lib "$FindBin::Bin/../lib"; -use Test::More; -use Text::ParseWords qw(shellwords); - -use XCAT::Test::File qw(repo_path); - -plan skip_all => 'otherpkgs filesystem isolation requires Linux' - unless $^O eq 'linux'; - -my $temporary_root = File::Spec->tmpdir(); -local %ENV = ( PATH => '/usr/bin:/bin', LC_ALL => 'C' ); -my $command_utils = repo_path('perl-xCAT/xCAT/CommandUtils.pm'); -require $command_utils; -my $bwrap = xCAT::CommandUtils::find_executable('bwrap'); -die "Install bubblewrap to run the otherpkgs test\n" unless $bwrap; -my $postscripts = repo_path('xCAT/postscripts'); -my @utilities = qw(bash sh basename dirname cat cp expr grep ls mkdir rm uname wc); -push @utilities, 'coreutils' if xCAT::CommandUtils::find_executable('coreutils'); -my @sandbox = ( - $bwrap, '--unshare-all', '--die-with-parent', '--new-session', - '--ro-bind', '/', '/', '--tmpfs', '/etc', '--tmpfs', '/usr/bin', - '--tmpfs', '/tmp', '--proc', '/proc', '--dev', '/dev', - '--setenv', 'PATH', '/usr/bin', '--setenv', 'LC_ALL', 'C', -); -for my $utility (@utilities) { - my $source = xCAT::CommandUtils::find_executable($utility); - die "Required utility is unavailable: $utility\n" unless $source; - push @sandbox, '--ro-bind', $source, "/usr/bin/$utility"; -} - -my ( $probe_output, $probe_status ) = capture_merged { - system( @sandbox, '/usr/bin/sh', '-c', 'test ! -e /etc/os-release' ); -}; -die "Cannot isolate otherpkgs: $probe_output" if $probe_status; - -for my $manager (qw(dnf yum)) { - for my $case ( - { name => 'HTTP and local repositories', verbose => 1, remote => 1 }, - { name => 'mounted repositories', mounted => 1 }, - { name => 'upgrade failure', upgrade_status => 17, verbose => 1 }, - { name => 'install failure', install_status => 23, verbose => 1 }, - { name => 'repository-only mode', repoonly => 1, remote => 1 }, - { name => 'separate package lists', multiple => 1, verbose => 1 }, - { name => 'remote repository without installs', remote => 1, empty => 1 }, - ) - { - subtest "$manager: $case->{name}" => sub { - run_case( $manager, $case ); - }; - } -} - -done_testing(); - -sub run_case { - my ( $manager, $case ) = @_; - my $fixture = tempdir( DIR => $temporary_root, CLEANUP => 1 ); - make_path("$fixture/bin"); - write_command( "$fixture/bin/logger", "exit 0\n" ); - write_command( "$fixture/bin/dpkg", "exit 1\n" ); - write_command( "$fixture/bin/rpm", '[ "$*" = --version ]' . "\n" ); - write_command( - "$fixture/bin/mount", - $case->{mounted} - ? "printf '%s\\n' 'package-server:/install on /install type nfs (rw)'\n" - : "exit 0\n" - ); - write_command( "$fixture/bin/$manager", <<'SH' ); -printf '%s\t' "${0##*/}" "SCOPE_ENV=${SCOPE_ENV:-}" "$@" >> /tmp/fixture/commands -printf '\n' >> /tmp/fixture/commands -sequence=$(wc -l < /tmp/fixture/commands) -mkdir "/tmp/fixture/repos.$sequence" -cp /etc/yum.repos.d/*.repo "/tmp/fixture/repos.$sequence/" 2>/dev/null || : -for argument do - case "$argument" in - upgrade) - printf '%s\n' upgrade-result - exit "$UPGRADE_STATUS" - ;; - install) - printf '%s\n' install-result - exit "$INSTALL_STATUS" - ;; - esac -done -exit 0 -SH - - my %environment = ( - OSVER => 'rhel9', ARCH => 'x86_64', UPDATENODE => 1, - NFSSERVER => 'package-server', HTTPPORT => 80, INSTALLDIR => '/install', - OTHERPKGDIR => '/install/other', OTHERPKGS_INDEX => 1, - OTHERPKGS1 => $case->{empty} ? '' : 'alpha/tool-one,beta/tool-two', - ENVLIST1 => 'SCOPE_ENV=first', VERBOSE => $case->{verbose} ? 1 : '', - UPGRADE_STATUS => $case->{upgrade_status} || 0, - INSTALL_STATUS => $case->{install_status} || 0, - ); - $environment{OTHERPKGDIR} = - 'https://packages.example.invalid/extra,/install/other' if $case->{remote}; - if ( $case->{multiple} ) { - @environment{qw(OTHERPKGS_INDEX OTHERPKGS1 OTHERPKGS2 ENVLIST2)} = - ( 2, 'alpha/tool-one,beta/tool-two', 'gamma/tool-three', 'SCOPE_ENV=second' ); - } - - my @command = ( - @sandbox, '--bind', $fixture, '/tmp/fixture', - '--ro-bind', $postscripts, '/tmp/postscripts', '--chdir', '/tmp/fixture', - ); - for my $tool (qw(logger dpkg rpm mount), $manager) { - push @command, '--ro-bind', "$fixture/bin/$tool", "/usr/bin/$tool"; - } - for my $key ( sort keys %environment ) { - push @command, '--setenv', $key, $environment{$key}; - } - push @command, '/usr/bin/sh', '-c', <<'SH', 'otherpkgs-test'; -/usr/bin/bash /tmp/postscripts/otherpkgs "$@" -status=$? -mkdir /tmp/fixture/final-repos -cp /etc/yum.repos.d/*.repo /tmp/fixture/final-repos/ 2>/dev/null || : -exit "$status" -SH - push @command, '--repoonly' if $case->{repoonly}; - - my ( $output, $status ) = capture_merged { system(@command) }; - is( $status, ( $case->{upgrade_status} || $case->{install_status} || 0 ) << 8, - 'the postscript returns the package-manager status' ) or diag($output); - ok( -f "$fixture/commands", 'the real postscript reaches the package manager' ); - return unless -f "$fixture/commands"; - - my @calls = map { [ split /\t/ ] } split /\n/, read_text("$fixture/commands"); - my @transactions; - for my $index ( 0 .. $#calls ) { - my $call = $calls[$index]; - my @operands = grep { !/^-/ } @{$call}[ 2 .. $#{$call} ]; - next if @operands && ( $operands[0] eq 'clean' || $operands[0] eq 'list' ); - push @transactions, [ $index + 1, $call ]; - } - my @expected; - my @groups = $case->{multiple} - ? ( [ first => qw(tool-one tool-two) ], [ second => 'tool-three' ] ) - : ( [ first => ( $case->{empty} ? () : qw(tool-one tool-two) ) ] ); - unless ( $case->{repoonly} ) { - for my $group (@groups) { - my ( $label, @packages ) = @{$group}; - push @expected, - [ $manager, "SCOPE_ENV=$label", '-y', '--disablerepo=*', - '--enablerepo=xcat-otherpkgs*', 'upgrade' ]; - push @expected, [ $manager, "SCOPE_ENV=$label", '-y', 'install', @packages ] - if @packages; - } - } - is_deeply( [ map { $_->[1] } @transactions ], \@expected, - 'only upgrades are repository-scoped; installs retain dependency repositories' ); - - my @printed = map { [ shellwords($_) ] } - grep { /^SCOPE_ENV=/ } split /\n/, $output; - my @expected_printed = $case->{verbose} - ? map { [ $_->[1], $_->[0], @{$_}[ 2 .. $#{$_} ] ] } @expected : (); - is_deeply( \@printed, \@expected_printed, - 'verbose commands describe the executed transactions and quiet mode omits them' ); - - for my $transaction (@transactions) { - my ( $sequence, $call ) = @{$transaction}; - my @paths = $case->{multiple} - ? ( $call->[1] eq 'SCOPE_ENV=first' ? qw(alpha beta) : 'gamma' ) - : ( $case->{empty} ? () : qw(alpha beta) ); - check_repositories( "$fixture/repos.$sequence", $case, \@paths ); - } - my @final_paths = $case->{multiple} ? ('gamma') - : $case->{empty} ? () : qw(alpha beta); - check_repositories( "$fixture/final-repos", $case, \@final_paths ); - if ( $case->{verbose} && !$case->{repoonly} ) { - like( $output, qr/^upgrade-result$/m, 'upgrade output reaches the caller' ); - like( $output, qr/^install-result$/m, 'install output reaches the caller' ) - unless $case->{empty}; - } -} - -sub check_repositories { - my ( $directory, $case, $paths ) = @_; - my $base = $case->{mounted} ? 'file://' : 'http://package-server:80'; - my @expected = ( - [ 'xCAT-rhel9-path0', "$base/install/rhel9/x86_64/BaseOS", '1' ], - [ 'xCAT-rhel9-path1', "$base/install/rhel9/x86_64/AppStream", '1' ], - ); - my $index = 0; - push @expected, [ 'xcat-otherpkgs' . $index++, - 'https://packages.example.invalid/extra', '1' ] if $case->{remote}; - push @expected, [ 'xcat-otherpkgs' . $index++, "$base/install/other/$_", '1' ] - for @{$paths}; - my @actual; - for my $file ( bsd_glob("$directory/*.repo") ) { - my $contents = read_text($file); - my @sections = $contents =~ /^\[([^\]\n]+)\]$/mg; - my @urls = $contents =~ /^baseurl=(.*?)\s*$/mg; - my @enabled = $contents =~ /^enabled=(.*?)\s*$/mg; - my @gpgcheck = $contents =~ /^gpgcheck=(.*?)\s*$/mg; - push @actual, [ @sections, @urls, @enabled, @gpgcheck ]; - } - is_deeply( - [ sort { $a->[0] cmp $b->[0] } @actual ], - [ map { [ @{$_}, '0' ] } sort { $a->[0] cmp $b->[0] } @expected ], - 'generated repositories match the upgrade scope and retain the OS repositories' - ); -} - -sub write_command { - my ( $file, $body ) = @_; - write_text( $file, "#!/usr/bin/sh\n$body" ); - chmod 0755, $file or die "Cannot make $file executable: $!"; -}