From 0c0803d83a29dd2e5b51e1a9bf4a7970f5ffb40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:03:05 -0300 Subject: [PATCH] test(utils): reuse and cover executable lookup --- .../autotest/testcase/probe/xcatproble_list | 2 +- .../integration/dhcp_kea_config_validation.t | 24 +- .../dhcp_kea_control_agent_smoke.t | 20 +- xCAT-test/unit/command_utils.t | 215 ++++++++++++++++++ xCAT-test/unit/dhcp_backend_selection.t | 8 + xCAT-test/unit/dhcp_kea_plugin_intent.t | 9 + xCAT-test/unit/dhcp_kea_renderer.t | 9 + xCAT-test/unit/probe_utils_dhcp_reply.t | 1 + xCAT-test/unit/probe_utils_netplan.t | 1 + xCAT-test/unit/probe_utils_netplan_get.t | 1 + xCAT-test/unit/probe_utils_tcp_listener.t | 9 + xCAT-test/unit/xcat_probe_package_payload.t | 20 ++ 12 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 xCAT-test/unit/command_utils.t diff --git a/xCAT-test/autotest/testcase/probe/xcatproble_list b/xCAT-test/autotest/testcase/probe/xcatproble_list index e5471aae9..faf9eec1e 100644 --- a/xCAT-test/autotest/testcase/probe/xcatproble_list +++ b/xCAT-test/autotest/testcase/probe/xcatproble_list @@ -5,7 +5,7 @@ cmd:mkdir -p /tmp/xcatprobe_l cmd:xcatprobe -l check:rc==0 check:output=~Supported sub commands are: -cmd:for module in GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done +cmd:for module in CommandUtils.pm GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm; do test -r "/opt/xcat/probe/lib/perl/xCAT/$module" || exit 1; done check:rc==0 cmd:xcatprobe -l|grep -v "Supported sub commands are" |awk '/^[[:graph:]]/ {print $1}'|sort > /tmp/xcatprobe_l/subcmd_from_xcatprobe_l cmd:ls -l /opt/xcat/probe/subcmds/ |awk '/^-/ {print $9}'|sort > /tmp/xcatprobe_l/subcmd_under_subcmds_dir diff --git a/xCAT-test/integration/dhcp_kea_config_validation.t b/xCAT-test/integration/dhcp_kea_config_validation.t index 7c1408394..5afecaedc 100644 --- a/xCAT-test/integration/dhcp_kea_config_validation.t +++ b/xCAT-test/integration/dhcp_kea_config_validation.t @@ -8,9 +8,10 @@ use File::Temp qw/tempfile/; use JSON (); use Test::More; +use xCAT::CommandUtils; use xCAT::DHCP::Backend::Kea; -my $kea_dhcp4 = command_path('kea-dhcp4'); +my $kea_dhcp4 = xCAT::CommandUtils::find_executable('kea-dhcp4'); plan skip_all => 'kea-dhcp4 is not installed' unless $kea_dhcp4; my $validation_dir = validation_temp_dir($kea_dhcp4); @@ -106,7 +107,7 @@ ok( !$result->{error}, 'generated Kea DHCPv4 config validates with kea-dhcp4 -t' unlink $path; SKIP: { - skip 'kea-dhcp6 is not installed', 1 unless command_path('kea-dhcp6'); + skip 'kea-dhcp6 is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-dhcp6'); my $dhcp6_json = $backend->render_dhcp6_config( { interfaces => ['*'], @@ -150,7 +151,7 @@ SKIP: { } SKIP: { - skip 'kea-dhcp-ddns is not installed', 1 unless command_path('kea-dhcp-ddns'); + skip 'kea-dhcp-ddns is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-dhcp-ddns'); my $ddns_json = $backend->render_ddns_config( { 'tsig-keys' => [ @@ -180,7 +181,7 @@ SKIP: { } SKIP: { - skip 'kea-ctrl-agent is not installed', 1 unless command_path('kea-ctrl-agent'); + skip 'kea-ctrl-agent is not installed', 1 unless xCAT::CommandUtils::find_executable('kea-ctrl-agent'); my $ctrl_agent_json = $backend->render_ctrl_agent_config( { dhcp6 => 1, @@ -195,21 +196,6 @@ SKIP: { } done_testing(); -sub command_path { - my ($command) = @_; - - foreach my $dir ( split /:/, $ENV{PATH} || '' ) { - next unless $dir; - return "$dir/$command" if -x "$dir/$command"; - } - - foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) { - return $path if -x $path; - } - - return; -} - sub validation_temp_dir { my ($kea_dhcp4) = @_; diff --git a/xCAT-test/integration/dhcp_kea_control_agent_smoke.t b/xCAT-test/integration/dhcp_kea_control_agent_smoke.t index 5f9473349..df9726940 100644 --- a/xCAT-test/integration/dhcp_kea_control_agent_smoke.t +++ b/xCAT-test/integration/dhcp_kea_control_agent_smoke.t @@ -11,14 +11,15 @@ use POSIX qw/WNOHANG _exit setgid setuid/; use Test::More; use Time::HiRes qw/sleep time/; +use xCAT::CommandUtils; use xCAT::DHCP::Backend::Kea; plan skip_all => 'set XCAT_KEA_LIVE_SMOKE=1 to run live Kea daemon smoke test' unless $ENV{XCAT_KEA_LIVE_SMOKE}; plan skip_all => 'live Kea daemon smoke test must run as root' unless $> == 0; -my $kea_dhcp4 = command_path('kea-dhcp4'); -my $kea_ctrl = command_path('kea-ctrl-agent'); +my $kea_dhcp4 = xCAT::CommandUtils::find_executable('kea-dhcp4'); +my $kea_ctrl = xCAT::CommandUtils::find_executable('kea-ctrl-agent'); plan skip_all => 'kea-dhcp4 and kea-ctrl-agent are required' unless $kea_dhcp4 && $kea_ctrl; @@ -192,21 +193,6 @@ SKIP: { stop_daemons(\%children); done_testing(); -sub command_path { - my ($command) = @_; - - foreach my $dir ( split /:/, $ENV{PATH} || '' ) { - next unless $dir; - return "$dir/$command" if -x "$dir/$command"; - } - - foreach my $path ( "/usr/sbin/$command", "/usr/bin/$command", "/sbin/$command", "/bin/$command" ) { - return $path if -x $path; - } - - return; -} - sub start_daemon { my ( $account, $command, $log, @args ) = @_; my $pid = fork(); diff --git a/xCAT-test/unit/command_utils.t b/xCAT-test/unit/command_utils.t new file mode 100644 index 000000000..724e52516 --- /dev/null +++ b/xCAT-test/unit/command_utils.t @@ -0,0 +1,215 @@ +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; + +use Cwd qw/getcwd/; +use File::Path qw/make_path/; +use File::Temp qw/tempdir/; +use Test::More; + +use xCAT::CommandUtils; + +my $command_utils_source = "$FindBin::Bin/../../perl-xCAT/xCAT/CommandUtils.pm"; +open( my $source_fh, '<', $command_utils_source ) + or die "Unable to read $command_utils_source: $!"; +my $source = do { local $/; <$source_fh> }; +close($source_fh) or die "Unable to close $command_utils_source: $!"; +like( + $source, + qr/my \@SYSTEM_FALLBACK_DIRS = qw\(\s*\/usr\/sbin\s*\/usr\/bin\s*\/sbin\s*\/bin\s*\);/s, + 'the built-in system fallback order matches the replaced callers' +); + +my $root = tempdir(CLEANUP => 1); +my $first_dir = "$root/first"; +my $second_dir = "$root/second"; +my $fallback_dir = "$root/fallback"; +make_path( $first_dir, $second_dir, $fallback_dir, "$root/relative" ); + +sub write_executable { + my ($path) = @_; + open( my $fh, '>', $path ) or die "Unable to write $path: $!"; + print {$fh} "#!/bin/sh\nexit 0\n"; + close($fh) or die "Unable to close $path: $!"; + chmod 0755, $path or die "Unable to make $path executable: $!"; + return $path; +} + +my $first_tool = write_executable("$first_dir/xcat-command-utils-tool"); +my $second_tool = write_executable("$second_dir/xcat-command-utils-tool"); + +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-tool', + path => "$first_dir:$second_dir", + fallback_dirs => [], + ), + $first_tool, + 'PATH entries are searched in order' +); + +chmod 0644, $first_tool or die "Unable to remove execute permission from $first_tool: $!"; +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-tool', + path => "$first_dir:$second_dir", + fallback_dirs => [], + ), + $second_tool, + 'non-executable candidates are skipped' +); + +{ + local $ENV{PATH} = $second_dir; + is( + xCAT::CommandUtils::find_executable('xcat-command-utils-tool'), + $second_tool, + 'the process PATH is used by default' + ); + is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-tool', + path => $first_dir, + fallback_dirs => [], + ), + undef, + 'an explicit path overrides the process PATH' + ); +} + +my $fallback_tool = write_executable("$fallback_dir/xcat-command-utils-fallback"); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-fallback', + path => '', + fallback_dirs => [ '', $fallback_dir ], + ), + $fallback_tool, + 'custom fallback directories are searched after PATH and ignore empty entries' +); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-fallback', + path => '', + fallback_dirs => [], + ), + undef, + 'fallback lookup can be disabled' +); +is( + xCAT::CommandUtils::find_executable( + 'bin', + path => '', + fallback_dirs => [''], + ), + undef, + 'empty fallback entries are ignored instead of matching executable root directories' +); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-tool', + path => '', + fallback_dirs => [$first_dir], + ), + undef, + 'non-executable fallback candidates are skipped' +); + +my $fallback_order_first = write_executable("$first_dir/xcat-command-utils-fallback-order"); +write_executable("$second_dir/xcat-command-utils-fallback-order"); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-fallback-order', + path => '', + fallback_dirs => [ $first_dir, $second_dir ], + ), + $fallback_order_first, + 'custom fallback directories are searched in order' +); + +my $path_collision = write_executable("$second_dir/xcat-command-utils-collision"); +write_executable("$fallback_dir/xcat-command-utils-collision"); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-collision', + path => $second_dir, + fallback_dirs => [$fallback_dir], + ), + $path_collision, + 'PATH matches take precedence over fallback matches' +); + +my $system_shell = xCAT::CommandUtils::find_executable( 'sh', path => '' ); +my ($expected_system_shell) = grep { -x "$_/sh" } qw(/usr/sbin /usr/bin /sbin /bin); +is( + $system_shell, + defined($expected_system_shell) ? "$expected_system_shell/sh" : undef, + 'standard system directories are searched by default' +); + +my $original_dir = getcwd(); +chdir($root) or die "Unable to enter $root: $!"; +is( + xCAT::CommandUtils::find_executable( + 'bin', + path => ":$second_dir", + fallback_dirs => [], + ), + undef, + 'empty PATH entries are ignored instead of matching executable root directories' +); + +write_executable("$root/relative/xcat-command-utils-relative"); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-relative', + path => 'relative', + fallback_dirs => [], + ), + 'relative/xcat-command-utils-relative', + 'relative PATH entries preserve the existing candidate path shape' +); + +make_path("$root/0"); +write_executable("$root/0/xcat-command-utils-zero-entry"); +is( + xCAT::CommandUtils::find_executable( + 'xcat-command-utils-zero-entry', + path => "0:$second_dir", + fallback_dirs => [], + ), + undef, + 'a PATH component named zero retains the existing falsy-entry behavior' +); +chdir($original_dir) or die "Unable to restore $original_dir: $!"; + +my $marker = "$root/shell-was-invoked"; +my $literal_name = 'xcat-command-utils;touch shell-was-invoked'; +my $literal_tool = write_executable("$second_dir/$literal_name"); +chdir($root) or die "Unable to enter $root for shell-safety lookup: $!"; +is( + xCAT::CommandUtils::find_executable( + $literal_name, + path => $second_dir, + fallback_dirs => [], + ), + $literal_tool, + 'command names are treated as filesystem paths without shell interpretation' +); +ok( !-e $marker, 'executable lookup never invokes a shell' ); +chdir($original_dir) or die "Unable to restore $original_dir: $!"; + +is( + xCAT::CommandUtils::find_executable( undef, path => $second_dir ), + undef, + 'an undefined command is not searched' +); +is( + xCAT::CommandUtils::find_executable( '', path => $second_dir ), + undef, + 'an empty command is not searched' +); + +done_testing(); diff --git a/xCAT-test/unit/dhcp_backend_selection.t b/xCAT-test/unit/dhcp_backend_selection.t index f6450cf21..179be060d 100644 --- a/xCAT-test/unit/dhcp_backend_selection.t +++ b/xCAT-test/unit/dhcp_backend_selection.t @@ -307,4 +307,12 @@ is( 'forced Kea succeeds when available' ); +{ + local $ENV{PATH} = ''; + ok( + xCAT::DHCP::Backend::_command_exists('sh'), + 'backend availability retains the standard system-directory fallback' + ); +} + done_testing(); diff --git a/xCAT-test/unit/dhcp_kea_plugin_intent.t b/xCAT-test/unit/dhcp_kea_plugin_intent.t index 839de3bec..fc2b1b0a4 100644 --- a/xCAT-test/unit/dhcp_kea_plugin_intent.t +++ b/xCAT-test/unit/dhcp_kea_plugin_intent.t @@ -162,6 +162,15 @@ foreach my $case (@sysconfig_policy_cases) { close($ip_fh); chmod 0755, $fake_ip; + { + local $ENV{PATH} = $tmpdir; + is( + xCAT_plugin::dhcp::kea_command_path('ip'), + $fake_ip, + 'DHCP route command lookup retains the first executable PATH match' + ); + } + no warnings 'redefine'; local *xCAT_plugin::dhcp::kea_command_path = sub { my ($command) = @_; diff --git a/xCAT-test/unit/dhcp_kea_renderer.t b/xCAT-test/unit/dhcp_kea_renderer.t index e9b31c708..295cd329d 100644 --- a/xCAT-test/unit/dhcp_kea_renderer.t +++ b/xCAT-test/unit/dhcp_kea_renderer.t @@ -580,6 +580,15 @@ FAKE_KEA close($fake_kea_fh) or die "Unable to close fake Kea command: $!"; chmod 0755, $fake_kea_dhcp4 or die "Unable to make fake Kea command executable: $!"; +{ + local $ENV{PATH} = $unit_dir; + is( + xCAT::DHCP::Backend::Kea::_command_path('kea-dhcp4-build-report'), + $fake_kea_dhcp4, + 'Kea command lookup retains the first executable PATH match' + ); +} + my $command_socket_backend = xCAT::DHCP::Backend::Kea->new(kea_dhcp4_command => $fake_kea_dhcp4); is( $command_socket_backend->control_socket_path('kea4-ctrl-socket'), '/xcat-test-command-run/kea/kea4-ctrl-socket', 'socket path comes from the Kea build-report command' ); diff --git a/xCAT-test/unit/probe_utils_dhcp_reply.t b/xCAT-test/unit/probe_utils_dhcp_reply.t index 4d432a0d4..69f55120b 100644 --- a/xCAT-test/unit/probe_utils_dhcp_reply.t +++ b/xCAT-test/unit/probe_utils_dhcp_reply.t @@ -3,6 +3,7 @@ use strict; use warnings; use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; use Test::More; diff --git a/xCAT-test/unit/probe_utils_netplan.t b/xCAT-test/unit/probe_utils_netplan.t index c31a5b733..46521e830 100644 --- a/xCAT-test/unit/probe_utils_netplan.t +++ b/xCAT-test/unit/probe_utils_netplan.t @@ -3,6 +3,7 @@ use strict; use warnings; use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; use File::Slurper qw(write_text); diff --git a/xCAT-test/unit/probe_utils_netplan_get.t b/xCAT-test/unit/probe_utils_netplan_get.t index 8a7068897..a20f57c25 100644 --- a/xCAT-test/unit/probe_utils_netplan_get.t +++ b/xCAT-test/unit/probe_utils_netplan_get.t @@ -3,6 +3,7 @@ use strict; use warnings; use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; use File::Temp qw(tempdir); diff --git a/xCAT-test/unit/probe_utils_tcp_listener.t b/xCAT-test/unit/probe_utils_tcp_listener.t index 07d14913e..6942653a5 100644 --- a/xCAT-test/unit/probe_utils_tcp_listener.t +++ b/xCAT-test/unit/probe_utils_tcp_listener.t @@ -3,6 +3,7 @@ use strict; use warnings; use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; use lib "$FindBin::Bin/../../xCAT-probe/lib/perl"; use Test::More; @@ -49,6 +50,14 @@ ok(probe_utils::_tcp_listener_output_has_port($netstat_output, 80, qr/httpd|apac ok(!probe_utils::_tcp_listener_output_has_port($netstat_output, 0), 'invalid port is rejected'); ok(!probe_utils::_tcp_listener_output_has_port($netstat_output, 'http'), 'non-numeric port is rejected'); +{ + local $ENV{PATH} = ''; + ok( + !probe_utils::_command_available('sh'), + 'probe command discovery retains PATH-only lookup without system fallbacks' + ); +} + { no warnings 'redefine'; my @commands; diff --git a/xCAT-test/unit/xcat_probe_package_payload.t b/xCAT-test/unit/xcat_probe_package_payload.t index 0db35af2b..26659bcdf 100644 --- a/xCAT-test/unit/xcat_probe_package_payload.t +++ b/xCAT-test/unit/xcat_probe_package_payload.t @@ -9,11 +9,14 @@ use File::Spec; use File::Temp qw(tempdir); use FindBin; use lib "$FindBin::Bin/../lib"; +use lib "$FindBin::Bin/../../build-utils/lib"; use Test::More; +use XCAT::BuildUtils qw(XCAT_PROBE_HELPERS); use XCAT::Test::File qw(repo_path slurp_repo_file); my @helpers = qw( + CommandUtils.pm GlobalDef.pm NetworkUtils.pm ServiceNodeUtils.pm @@ -26,6 +29,9 @@ my @affected_subcommands = qw( ); my $builder = slurp_repo_file('buildrpms.pl'); +my $debian_builder = slurp_repo_file('build-ubunturepo'); +my $installed_probe_test = + slurp_repo_file('xCAT-test/autotest/testcase/probe/xcatproble_list'); my $rpm_spec = slurp_repo_file('xCAT-probe/xCAT-probe.spec'); my $debian_control = slurp_repo_file('xCAT-probe/debian/control'); like($builder, qr/sub prepare_xcat_probe_source_tar\b/, 'RPM builder has dedicated xCAT-probe source preparation'); @@ -65,6 +71,20 @@ for my $helper (@helpers) { my $source = repo_path(File::Spec->catfile('perl-xCAT', 'xCAT', $helper)); ok(-f $source, "$helper source exists"); like($builder, qr/^\s*\Q$helper\E\s*$/m, "RPM builder stages $helper"); + ok( + scalar(grep { $_ eq $helper } XCAT_PROBE_HELPERS), + "the shared builder helper list carries $helper" + ); + like( + $debian_builder, + qr{cp -f [^\n]*/perl-xCAT/xCAT/\Q$helper\E\s+[^\n]*/lib/perl/xCAT/}, + "Debian builder stages $helper" + ); + like( + $installed_probe_test, + qr/cmd:for module in [^;]*\b\Q$helper\E\b[^;]*; do test -r/, + "installed probe payload checks $helper" + ); } my $tmpdir = tempdir(CLEANUP => 1);