Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package os-autoinst for openSUSE:Factory checked in at 2026-08-19 18:00:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/os-autoinst (Old) and /work/SRC/openSUSE:Factory/.os-autoinst.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "os-autoinst" Wed Aug 19 18:00:24 2026 rev:617 rq:1372000 version:5.1787055635.69d1218 Changes: -------- --- /work/SRC/openSUSE:Factory/os-autoinst/os-autoinst.changes 2026-08-05 17:49:09.551431473 +0200 +++ /work/SRC/openSUSE:Factory/.os-autoinst.new.1258/os-autoinst.changes 2026-08-19 18:02:47.635629264 +0200 @@ -1,0 +2,12 @@ +Tue Aug 18 12:20:46 UTC 2026 - [email protected] + +- Update to version 5.1787055635.69d1218: + * feat: validate domain is gone after undefine in svirt + * fix: report detailed stderr for failed virsh define + * build(deps): bump codecov/codecov-action in the all-actions group (#3082) + * docs: Document rootless container example in README + * feat: Add custom dev container Dockerfile + * feat(testapi): support become_user helper + * docs: Improve architecture documentation + +------------------------------------------------------------------- Old: ---- os-autoinst-5.1785768930.71d56f4.obscpio New: ---- os-autoinst-5.1787055635.69d1218.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ os-autoinst-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.Hpp0Ix/_old 2026-08-19 18:02:49.133682994 +0200 +++ /var/tmp/diff_new_pack.Hpp0Ix/_new 2026-08-19 18:02:49.136683102 +0200 @@ -18,7 +18,7 @@ %define short_name os-autoinst-devel Name: %{short_name}-test -Version: 5.1785768930.71d56f4 +Version: 5.1787055635.69d1218 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ os-autoinst-openvswitch-test.spec ++++++ --- /var/tmp/diff_new_pack.Hpp0Ix/_old 2026-08-19 18:02:49.173684429 +0200 +++ /var/tmp/diff_new_pack.Hpp0Ix/_new 2026-08-19 18:02:49.175684500 +0200 @@ -19,7 +19,7 @@ %define name_ext -test %define short_name os-autoinst-openvswitch Name: %{short_name}%{?name_ext} -Version: 5.1785768930.71d56f4 +Version: 5.1787055635.69d1218 Release: 0 Summary: test package for %{short_name} License: GPL-2.0-or-later ++++++ os-autoinst-test.spec ++++++ --- /var/tmp/diff_new_pack.Hpp0Ix/_old 2026-08-19 18:02:49.208685684 +0200 +++ /var/tmp/diff_new_pack.Hpp0Ix/_new 2026-08-19 18:02:49.210685756 +0200 @@ -19,7 +19,7 @@ %define name_ext -test %define short_name os-autoinst Name: %{short_name}%{?name_ext} -Version: 5.1785768930.71d56f4 +Version: 5.1787055635.69d1218 Release: 0 Summary: test package for os-autoinst License: GPL-2.0-or-later ++++++ os-autoinst.spec ++++++ --- /var/tmp/diff_new_pack.Hpp0Ix/_old 2026-08-19 18:02:49.263687657 +0200 +++ /var/tmp/diff_new_pack.Hpp0Ix/_new 2026-08-19 18:02:49.270687908 +0200 @@ -17,7 +17,7 @@ Name: os-autoinst -Version: 5.1785768930.71d56f4 +Version: 5.1787055635.69d1218 Release: 0 Summary: OS-level test automation License: GPL-2.0-or-later ++++++ os-autoinst-5.1785768930.71d56f4.obscpio -> os-autoinst-5.1787055635.69d1218.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/author-tests.yaml new/os-autoinst-5.1787055635.69d1218/.github/workflows/author-tests.yaml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/author-tests.yaml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/author-tests.yaml 2026-08-18 14:20:35.000000000 +0200 @@ -12,7 +12,7 @@ container: image: registry.opensuse.org/devel/openqa/containers/os-autoinst_dev steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: make test-local run: | git config --global --add safe.directory '*' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/checklist.yml new/os-autoinst-5.1787055635.69d1218/.github/workflows/checklist.yml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/checklist.yml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/checklist.yml 2026-08-18 14:20:35.000000000 +0200 @@ -10,9 +10,9 @@ name: Checklist job steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Checklist - uses: wyozi/contextual-qa-checklist-action@master + uses: wyozi/contextual-qa-checklist-action@dd4b6b51d85a59f537b7ca9e2f162164627ec811 # master with: gh-token: ${{ secrets.GITHUB_TOKEN }} input-file: .github/checklist.yml diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci.yml new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci.yml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci.yml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci.yml 2026-08-18 14:20:35.000000000 +0200 @@ -11,11 +11,11 @@ contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Static checks, unit tests and integration tests run: tools/container_run_ci - name: Upload coverage to Codecov - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7 with: # should not be necessary for public repos, but might help avoid sporadic upload token errors token: ${{ secrets.CODECOV_TOKEN }} @@ -24,7 +24,7 @@ verbose: true - run: | tar cvf coverage.tar build/cover_db - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage path: coverage.tar @@ -35,6 +35,6 @@ contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Test our container definitions run: tools/static_check_containers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci_extended.yml new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci_extended.yml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci_extended.yml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci_extended.yml 2026-08-18 14:20:35.000000000 +0200 @@ -14,6 +14,6 @@ contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Test our container definitions run: tools/test_containers diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/commit-message-checker.yml new/os-autoinst-5.1787055635.69d1218/.github/workflows/commit-message-checker.yml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/commit-message-checker.yml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/commit-message-checker.yml 2026-08-18 14:20:35.000000000 +0200 @@ -12,4 +12,4 @@ check-commit-message: permissions: contents: read - uses: os-autoinst/os-autoinst-common/.github/workflows/base-commit-message-checker.yml@master + uses: os-autoinst/os-autoinst-common/.github/workflows/base-commit-message-checker.yml@e76072d144ca3313002006ff4ee8d4e5a7529aa9 # master diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/obs-helper.yaml new/os-autoinst-5.1787055635.69d1218/.github/workflows/obs-helper.yaml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/obs-helper.yaml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/obs-helper.yaml 2026-08-18 14:20:35.000000000 +0200 @@ -12,7 +12,7 @@ contents: read issues: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: debug run: | env | sort diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/.github/workflows/openqa_fullstack.yml new/os-autoinst-5.1787055635.69d1218/.github/workflows/openqa_fullstack.yml --- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/openqa_fullstack.yml 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/openqa_fullstack.yml 2026-08-18 14:20:35.000000000 +0200 @@ -12,6 +12,7 @@ env: # Set to 1 to temporarily ignore warnings PERL_TEST_WARNINGS_ONLY_REPORT_WARNINGS: 0 + OPENQA_FULLSTACK_TEMP_DIR: /tmp/test-results/fullstack TEST_USER: testuser jobs: @@ -22,7 +23,7 @@ container: image: registry.opensuse.org/devel/openqa/containers/opensuse/openqa_ci:latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Create additional user to avoid initdb error "cannot be run as root" run: useradd "$TEST_USER" - name: Make project folder user-writable @@ -34,6 +35,16 @@ - name: Build os-autoinst run: sudo -u "$TEST_USER" make symlinks - name: Run unit tests - run: sudo -u "$TEST_USER" make -C ../openQA test-fullstack + run: | + sudo -u "$TEST_USER" mkdir -p "$OPENQA_FULLSTACK_TEMP_DIR" + sudo -u "$TEST_USER" make -C ../openQA test-fullstack OPENQA_FULLSTACK_TEMP_DIR="$OPENQA_FULLSTACK_TEMP_DIR" env: STABILITY_TEST: ${{ github.event.inputs.stability_test }} + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: ${{ always() }} + with: + name: Test results + path: | + /tmp/test-results/ + !/tmp/test-results/**/*.iso + include-hidden-files: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/OpenQA/Exceptions.pm new/os-autoinst-5.1787055635.69d1218/OpenQA/Exceptions.pm --- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Exceptions.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Exceptions.pm 2026-08-18 14:20:35.000000000 +0200 @@ -28,6 +28,9 @@ 'OpenQA::Exception::TestapiError' => { description => 'A testapi function failed' }, + 'OpenQA::Exception::TestapiUsageError' => { + description => 'A testapi function failed because of wrong usage' + }, ); 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Interface.pm new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Interface.pm --- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Interface.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Interface.pm 2026-08-18 14:20:35.000000000 +0200 @@ -9,7 +9,7 @@ # -> increment on every change of such APIs # -> never move that variable to another place (when refactoring) # because it may be accessed by the tests itself -our $version = 58; ## no critic (Variables::ProhibitPackageVars) +our $version = 59; ## no critic (Variables::ProhibitPackageVars) # major version of the (web socket) API relevant to the developer mode # -> increment when making non-backward compatible changes to that API diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Runner.pm new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Runner.pm --- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Runner.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Runner.pm 2026-08-18 14:20:35.000000000 +0200 @@ -50,6 +50,9 @@ $io_select->add($ch->cmd_srv_fd); $io_select->add($ch->backend_out_fd); + my @interleaved_cmds; + myjsonrpc::set_interleaved_command_handler(\@interleaved_cmds); + while ($self->loop) { my ($ready_for_read, $ready_for_write, $exceptions) = IO::Select::select($io_select, undef, $io_select, $ch->timeout); for my $readable (@$ready_for_read) { @@ -57,6 +60,7 @@ $self->_read_response($rsp, $readable); last unless defined $rsp; } + $self->_read_response(@$_) for splice @interleaved_cmds; $ch->check_asserted_screen if defined($ch->tags); } $ch->stop_command_processing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/README.md new/os-autoinst-5.1787055635.69d1218/README.md --- old/os-autoinst-5.1785768930.71d56f4/README.md 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/README.md 2026-08-18 14:20:35.000000000 +0200 @@ -365,6 +365,24 @@ (e.g. `os-autoinst-distri-opensuse-deps`) must be provided by that container image rather than the worker host. +For example, you can run an openQA test using your custom `os-autoinst` branch and the +pre-built `osado-dev-container` image (which already bundles `os-autoinst-distri-opensuse-deps`). + +Note: You must point `CASEDIR` and `NEEDLES_DIR` to remote Git repositories because the +rootless container starts with a clean environment and does not mount local worker directories: + +```sh +openqa-clone-job --skip-chained-deps --within-instance <target_job_url> \ + _GROUP=0 \ + BUILD+=-custom_suffix \ + TEST+=-custom_suffix \ + CASEDIR=https://github.com/os-autoinst/os-autoinst-distri-opensuse.git \ + NEEDLES_DIR=https://github.com/os-autoinst/os-autoinst-needles-opensuse.git \ + OS_AUTOINST_GIT_REPO=https://github.com/<your_username>/os-autoinst.git \ + OS_AUTOINST_GIT_BRANCH=<your_test_branch> \ + OS_AUTOINST_CONTAINER_IMAGE=registry.opensuse.org/devel/openqa/containers/osado-dev-container:latest +``` + ### Advanced testing with custom test-distribution dependencies If you are testing changes against tests that require custom dependencies not @@ -384,7 +402,7 @@ TEST+=-custom_suffix \ CASEDIR=https://github.com/<your_username>/os-autoinst-distri-opensuse.git#<your_test_branch> \ WORKER_CLASS=<suitable_worker_class> \ - ISOTOVIDEO="p=/var/lib/openqa/cache/podman && mkdir -p \$p/run \$p/config \$p/data && env HOME=\$p XDG_CONFIG_HOME=\$p/config XDG_DATA_HOME=\$p/data XDG_RUNTIME_DIR=\$p/run podman --root \$p/data/containers/storage --runroot \$p/run/containers --storage-opt ignore_chown_errors=true --cgroup-manager=cgroupfs --events-backend=file run --init --rm --entrypoint \"\" --device /dev/kvm -v \$(pwd):/pool -w /pool -v /var/lib/openqa/cache:/var/lib/openqa/cache registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest sh -c 'zypper -n in os-autoinst-distri-opensuse-deps && rm -rf os-autoinst && git clone --branch=<your_engine_branch> --depth=1 https://github.com/<your_username>/os-autoinst.git && make -C os-autoinst symlinks && os-autoinst/isotovideo -d'" + ISOTOVIDEO="c=/var/lib/openqa/cache/podman_storage && p=\$(pwd)/podman_tmp && mkdir -p \$c \$p/run && env HOME=\$p XDG_RUNTIME_DIR=\$p/run podman --root \$c --runroot \$p/run/containers --storage-opt ignore_chown_errors=true --cgroup-manager=cgroupfs --events-backend=file run --init --rm --entrypoint \"\" --device /dev/kvm -v \$(pwd):/pool -w /pool -v /var/lib/openqa/cache:/var/lib/openqa/cache registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest sh -c 'zypper -n in os-autoinst-distri-opensuse-deps && rm -rf os-autoinst && git clone --branch=<your_engine_branch> --depth=1 https://github.com/<your_username>/os-autoinst.git && make -C os-autoinst symlinks && os-autoinst/isotovideo -d'" ``` # Running isotovideo as CI check diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/backend/baseclass.pm new/os-autoinst-5.1787055635.69d1218/backend/baseclass.pm --- old/os-autoinst-5.1785768930.71d56f4/backend/baseclass.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/backend/baseclass.pm 2026-08-18 14:20:35.000000000 +0200 @@ -139,6 +139,7 @@ $console->backend($self); } + myjsonrpc::set_interleaved_command_handler($self->{interleaved_cmds} = []); $self->run_capture_loop; bmwqemu::diag('management process exit at ' . POSIX::strftime('%F %T', gmtime)); # uncoverable statement @@ -302,6 +303,10 @@ # next time last; } + + # handle interleaved commands (commands received while waiting for the reply to another command) + $self->_handle_cmd($_->[0]) for splice @{$self->{interleaved_cmds}}; + return 1; } @@ -633,24 +638,23 @@ _exit(0); # uncoverable statement } +sub _handle_cmd ($self, $cmd) { + die 'no command in ' . Dumper($cmd) unless $cmd->{cmd}; + my $rsp = ($self->handle_command($cmd) // 0); + my $response = {rsp => $rsp}; + if (ref $rsp eq 'HASH' && $rsp->{postponed}) { + $self->{_postponed_cmd_token} = $cmd->{json_cmd_token}; + } elsif ($self->{rsppipe}) { # the command might have closed it + $response->{json_cmd_token} = $cmd->{json_cmd_token}; + myjsonrpc::send_json($self->{rsppipe}, $response); + } +} + # this is called for all sockets ready to read from sub check_socket ($self, $fh, $write = undef) { return 0 unless $self->{cmdpipe} && $fh == $self->{cmdpipe}; return 1 if $write; - my $cmd = myjsonrpc::read_json($self->{cmdpipe}); - if ($cmd->{cmd}) { - my $rsp = ($self->handle_command($cmd) // 0); - my $response = {rsp => $rsp}; - if (ref $rsp eq 'HASH' && $rsp->{postponed}) { - $self->{_postponed_cmd_token} = $cmd->{json_cmd_token}; - } elsif ($self->{rsppipe}) { # the command might have closed it - $response->{json_cmd_token} = $cmd->{json_cmd_token}; - myjsonrpc::send_json($self->{rsppipe}, $response); - } - } - else { - die 'no command in ' . Dumper($cmd); - } + $self->_handle_cmd($_) for myjsonrpc::read_json($self->{cmdpipe}, undef, 1); return 1; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/backend/svirt.pm new/os-autoinst-5.1787055635.69d1218/backend/svirt.pm --- old/os-autoinst-5.1785768930.71d56f4/backend/svirt.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/backend/svirt.pm 2026-08-18 14:20:35.000000000 +0200 @@ -78,8 +78,17 @@ sub do_stop_vm_svirt ($self) { my $vmname = $self->vmname; - $self->run_ssh_cmd(virsh() . " destroy $vmname"); - $self->run_ssh_cmd(virsh() . " undefine --snapshots-metadata $vmname"); + + # Only attempt cleanup if the domain actually exists on the host + if ($self->run_ssh_cmd(virsh() . " dominfo $vmname 2>/dev/null") == 0) { + $self->run_ssh_cmd(virsh() . " destroy $vmname"); + $self->run_ssh_cmd(virsh() . " undefine --snapshots-metadata $vmname"); + + # Verify the domain is actually gone to prevent cryptic UUID clashes later + if ($self->run_ssh_cmd(virsh() . " dominfo $vmname 2>/dev/null") == 0) { + bmwqemu::fctwarn("Domain '$vmname' still exists after undefine attempt. The QEMU process is likely stuck (D-state) on the host."); + } + } } sub do_stop_vm ($self, @) { @@ -121,7 +130,7 @@ sub is_shutdown_cmd_hyperv ($vmname) { qq{powershell -Command "if (\$(Get-VM -VMName $vmname \| Where-Object {\$_.state -eq 'Off'})) { exit 1 } else { exit 0 }"} } sub is_shutdown_cmd_svirt ($vmname) { - return '! ' . virsh() . " dominfo $vmname | grep -w 'shut off'"; + return '! ' . virsh() . " dominfo $vmname 2>/dev/null | grep -w 'shut off'"; } sub is_shutdown ($self, @) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/consoles/sshVirtsh.pm new/os-autoinst-5.1787055635.69d1218/consoles/sshVirtsh.pm --- old/os-autoinst-5.1785768930.71d56f4/consoles/sshVirtsh.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/consoles/sshVirtsh.pm 2026-08-18 14:20:35.000000000 +0200 @@ -300,8 +300,15 @@ return; } +sub _free_lock_forcefully ($self) { + bmwqemu::diag('Lock is still held. Attempting active recovery by force-killing lingering processes for ' . $self->name); + $self->run_cmd(sprintf "%s destroy '%s'", backend::svirt::virsh, $self->name); + $self->run_cmd(sprintf "pkill -9 -f '%s'", $self->name); +} + sub _do_create_disk ($self, $file, $size, $args = undef) { my $bucket = 5; + my $active_recovery_attempted = 0; my @cmd = "qemu-img create '$file' -f qcow2"; push @cmd, $args->{additional_args} if $args->{additional_args}; push @cmd, $size; @@ -311,7 +318,16 @@ my ($ret, $stdout, $stderr) = $self->run_cmd((join ' ', @cmd), wantarray => 1); if (($stderr // '') =~ /lock/i) { $bucket--; - die 'Too many attempts to create disk' unless $bucket; + if (!$bucket) { + if (!$active_recovery_attempted) { + $self->_free_lock_forcefully; + $active_recovery_attempted = 1; + $bucket = 2; # allow 2 more attempts after active recovery + } + else { + die 'Too many attempts to create disk'; + } + } bmwqemu::diag("Resource is still not free, waiting a bit more. $bucket attempts left"); sleep 5; next; @@ -476,7 +492,7 @@ # $args->{size} is expected to be e.g. '20G' but internally we need it as integer my $size = ($args->{size} // 0) =~ tr/G//dr; # expected value in Bytes - my (undef, $json) = $self->run_cmd("qemu-img info --output=json $args->{file}", wantarray => 1); + my (undef, $json) = $self->run_cmd("qemu-img info --force-share --output=json $args->{file}", wantarray => 1); my $image_vsize = decode_json($json)->{'virtual-size'}; $size = (($size * 1024 * 1024 * 1024) <= $image_vsize) ? $image_vsize : $size . 'G'; $self->_do_create_disk($file, $size, {additional_args => "-F qcow2 -b '$basedir/$file_basename'"}); @@ -615,7 +631,12 @@ $self->backend->do_stop_vm_svirt() if $args{pre_cleanup}; # define the new domain - $self->run_cmd(backend::svirt::virsh() . " define $xmlfilename") && die 'virsh define failed'; + my ($stdout, $stderr); + ($ret, $stdout, $stderr) = $self->run_cmd(backend::svirt::virsh() . " define $xmlfilename", wantarray => 1); + if ($ret) { + $stderr =~ s/\s+$// if $stderr; + die 'virsh define failed: ' . ($stderr || "exit code $ret"); + } if ($self->vmm_family eq 'vmware') { my $vmx = sprintf '/vmfs/volumes/%s/openQA/%s.vmx', $bmwqemu::vars{VMWARE_DATASTORE} // 'datastore1', $self->name; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Containerfile new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Containerfile --- old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Containerfile 1970-01-01 01:00:00.000000000 +0100 +++ new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Containerfile 2026-08-18 14:20:35.000000000 +0200 @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: MIT +#!BuildTag: osado-dev-container:latest opensuse/osado-dev-container:latest opensuse/osado-dev-container:%PKG_VERSION% opensuse/osado-dev-container:%PKG_VERSION%.%RELEASE% +# hadolint ignore=DL3007 +FROM opensuse/os-autoinst-dev:latest +RUN zypper in -y -C os-autoinst-distri-opensuse-deps && zypper clean -a diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Dockerfile new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Dockerfile --- old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Dockerfile 1970-01-01 01:00:00.000000000 +0100 +++ new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Dockerfile 2026-08-19 18:02:50.877745547 +0200 @@ -0,0 +1 @@ +symbolic link to Containerfile diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/_service new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/_service --- old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/_service 1970-01-01 01:00:00.000000000 +0100 +++ new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/_service 2026-08-18 14:20:35.000000000 +0200 @@ -0,0 +1,22 @@ +<services> + <service name="obs_scm"> + <param name="url">https://github.com/os-autoinst/os-autoinst</param> + <param name="scm">git</param> + <param name="revision">master</param> + <param name="versionprefix">5</param> + <param name="versionformat">%ct.%h</param> + <param name="subdir">container/osado-dev-container</param> + <param name="extract">Dockerfile</param> + <param name="changesgenerate">enable</param> + <param name="changesauthor">[email protected]</param> + </service> + <service mode="buildtime" name="kiwi_metainfo_helper"/> + <service mode="buildtime" name="docker_label_helper"/> + <service mode="buildtime" name="replace_using_package_version"> + <param name="file">Dockerfile</param> + <param name="regex">%PKG_VERSION%</param> + <param name="parse-version">patch</param> + <param name="package">os-autoinst-distri-opensuse-deps</param> + </service> +</services> + diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/distribution.pm new/os-autoinst-5.1787055635.69d1218/distribution.pm --- old/os-autoinst-5.1785768930.71d56f4/distribution.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/distribution.pm 2026-08-18 14:20:35.000000000 +0200 @@ -98,6 +98,15 @@ $self->script_run('cd /tmp'); } +sub become_user ($self, $user) { + die "Invalid username: $user" unless $user =~ /^[a-zA-Z0-9_.-]+$/; + $self->script_run("su - $user", 0); + $self->invalidate_serial_marker_hook(); + my $console = testapi::current_console() // 'sut'; + my $level = $self->{_serial_marker_level}->{$console} // 1; + $self->install_serial_marker_hook($level); +} + =head2 disable_key_repeat disable_key_repeat() @@ -118,6 +127,10 @@ sub _handle_cmd_typing_error ($cmd, $args) { ($args->{check_typing_cmd} // 1 ? \&croak : \&fctwarn)->("typing command '$cmd' timed out") } +# _OANM: openQA No Marker, skips the shell hook's done marker. Applied on +# serial terminals or when C<$force> is set (e.g. manual serial redirection). +sub _no_marker_prefix ($cmd, $force = 0) { ($force || testapi::is_serial_terminal) ? "_OANM=1; $cmd" : $cmd } + =head2 script_run script_run($cmd [, timeout => $timeout] [, output => $output] [,quiet => $quiet] [,max_interval => $max_interval]) @@ -177,9 +190,11 @@ testapi::query_isotovideo('backend_clear_serial_buffer', {}); testapi::type_string "$cmd\n", max_interval => $args{max_interval}; $self->_check_sudo_password(\%args); - my $res = testapi::wait_serial(qr/OA:DONE-[0-9a-f]{4}-(\d+)-/, timeout => $args{timeout}, quiet => $args{quiet}, record_command => $cmd, internal_marker => 1, capture_name => 'Exit code'); + my $fp = $self->sut_marker($cmd); + my $regex = qr/OA:DONE-[0-9a-f]{4}-(\d+)-\Q$fp\E/; + my $res = testapi::wait_serial($regex, timeout => $args{timeout}, quiet => $args{quiet}, record_command => $cmd, internal_marker => 1, capture_name => 'Exit code'); return undef unless $res; - return ($res =~ /OA:DONE-[0-9a-f]{4}-(\d+)-/)[0]; + return ($res =~ $regex)[0]; } $str = testapi::hashed_string('SR' . $cmd . $args{timeout}); $wait_pattern = qr/$str-(\d+)-/; @@ -189,8 +204,7 @@ } else { my $marker = "; echo $str-\$?-" . ($args{output} ? "Comment: $args{output}" : ''); - # _OANM: openQA No Marker, skips printing the hook's done marker - my $final_cmd = $skip_pretty ? "_OANM=1; $cmd" : $cmd; + my $final_cmd = _no_marker_prefix($cmd, $skip_pretty); if (testapi::is_serial_terminal) { testapi::type_string "$final_cmd$marker", max_interval => $args{max_interval}; testapi::wait_serial($final_cmd . $marker, no_regex => 1, quiet => 1, buffer_size => (length $final_cmd) + 128, internal_marker => 1) @@ -235,7 +249,7 @@ testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet => 1); } - $cmd = "( $cmd )"; + $cmd = _no_marker_prefix("( $cmd )"); testapi::type_string $cmd; my $str = testapi::hashed_string('SR' . $cmd); my $marker = "& echo $str-\$!-" . ($args{output} ? "Comment: $args{output}" : ''); @@ -350,7 +364,7 @@ # might encounter on the serial device depending on how it is used in the # SUT my $shell_cmd = testapi::is_serial_terminal() ? 'bash -oe pipefail' : 'bash -eox pipefail'; - my $run_script = "echo $marker; $shell_cmd $script_path ; echo SCRIPT_FINISHED$marker-\$?-"; + my $run_script = _no_marker_prefix("echo $marker; $shell_cmd $script_path ; echo SCRIPT_FINISHED$marker-\$?-"); if (testapi::is_serial_terminal) { testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet => 1); testapi::type_string "$run_script\n"; @@ -432,7 +446,8 @@ sut_marker($cmd) Generate a unique marker string for a command to be used for synchronization -with the SUT. Used primarily for internal testing. +with the SUT. Canonical command fingerprint used for level-3 marker +correlation. =cut @@ -452,27 +467,42 @@ Install shell hooks (like PROMPT_COMMAND) into the SUT to emit synchronization markers to serial. +B<Persistence implications:> the hook is appended to the SUT's C<~/.bashrc> and +C<~/.profile> and therefore persists across shells and across jobs that boot the +same qcow image. Once installed, every prompt of every shell sourcing those +files emits C<OA:DONE>/C<OA:START> to C</dev/$testapi::serialdev> (i.e. +C<serial0.txt>), including login/C<exec $SHELL>/C<su> shells and non-serial +terminal (VNC/tty) consoles. This is by design (C<serial0.txt> is the control +channel for these markers) but means C<serial0.txt> cannot be kept completely +clean by patching individual command invocations. To fully suppress the hook, +disable the feature via C<PRETTY_SERIAL_MARKER=0> for the whole cluster, +including the parent job that creates the qcow so no hook is ever baked in. See +also L</detect_serial_marker_capability> and C<doc/backend_vars.md>. + =cut sub install_serial_marker_hook ($self, $level) { return undef if $level < 2; my $dev = "/dev/$testapi::serialdev"; - my $func; - # _oap: openQA prompt hook function - # _OANM: openQA No Marker (skip hook done marker) - # _OAM: openQA Marker (custom marker string) - if ($level == 3) { - $func = qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;else c=\$(fc -ln -1 2>/dev/null);printf "OA:DONE-%04x-%d-%s\\nOA:START\\n" \$RANDOM \$r "\${c#\${c%%[![:space:]]*}}">$dev;fi;}}; - } - else { - $func = qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;elif [ -n "\$_OAM" ];then echo "\$_OAM-\$r-">$dev;unset _OAM;fi;echo "OA:START">$dev;}}; - } + # Shell hook tokens: _oap=prompt hook, _OANM=skip-done-marker, _OAM=custom + # marker (grepped in ~/.bashrc below to detect an already-installed hook). + my $func = { + 3 => qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;else c=\$(HISTTIMEFORMAT= history 1);c=\${c#*[0-9] };c=\${c#\${c%%[![:space:]]*}};c=\${c%\${c##*[![:space:]]}};l=\${#c};t=\$c;[ \$l -ge 4 ]&&t=\${c: -4};printf "OA:DONE-%04x-%d-OA:%s%d%s\\nOA:START\\n" \$RANDOM \$r "\${c:0:4}" \$l "\$t">$dev;fi;}}, + 2 => qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;elif [ -n "\$_OAM" ];then echo "\$_OAM-\$r-">$dev;unset _OAM;fi;echo "OA:START">$dev;}}, + }->{$level}; my $pc = 'PROMPT_COMMAND=_oap'; + # Version tag: bump whenever the emitted marker format changes so a stale + # hook persisted in ~/.bashrc by an older os-autoinst is replaced instead of + # kept (a presence-only `grep _oap` guard would re-source the old format and + # emit markers the current wait_serial regex never matches). + my $tag = '_OAPV=3'; + # Consolidate installation and persistence into a single typed line to minimize VNC overhead. # We append to both ~/.bashrc and ~/.profile to cover both interactive and login shells. - # Sourcing ~/.bashrc then activates the hook in the current session. - testapi::type_string "grep -q _oap ~/.bashrc 2>/dev/null||{ echo '$func;$pc'|tee -a ~/.bashrc ~/.profile>/dev/null;};. ~/.bashrc\n"; + # If the current version tag is absent, strip any stale hook lines first, then + # (re)install. Sourcing ~/.bashrc then activates the hook in the current session. + testapi::type_string "grep -q '$tag' ~/.bashrc 2>/dev/null||{ sed -i '/_oap/d;/$tag/d' ~/.bashrc ~/.profile 2>/dev/null;echo '$tag;$func;$pc'|tee -a ~/.bashrc ~/.profile>/dev/null;};. ~/.bashrc\n"; my $console = testapi::current_console() // 'sut'; $self->{_serial_marker_hook_installed}->{$console} = 1; @@ -518,6 +548,14 @@ delete $self->{_serial_marker_hook_installed}->{$console}; } +# Disable any pre-existing PROMPT_COMMAND hook (e.g. inherited from ~/.bashrc) +# so it cannot pollute serial0.txt with markers. On a serial terminal we wait +# for the prompt to consume the command echo. +sub _disable_inherited_prompt_hook ($self, $wait_prompt = 0) { + testapi::type_string "unset PROMPT_COMMAND\n"; + testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet => 1) if $wait_prompt && $self->{serial_term_prompt}; +} + =head2 get_pretty_serial_marker get_pretty_serial_marker() @@ -547,7 +585,7 @@ # If we are turning it OFF, we MUST tell the SUT to stop sending markers # to avoid polluting the fallback mode. - testapi::type_string "unset PROMPT_COMMAND\n" if !$value; + $self->_disable_inherited_prompt_hook if !$value; $self->reset_serial_marker(); } @@ -587,6 +625,16 @@ - 2: Basic bash (PROMPT_COMMAND support) - 3: Advanced bash (PROMPT_COMMAND + history/fc support) +Level 1 is returned without installing any hook when C<PRETTY_SERIAL_MARKER> is +disabled or when the active console is a serial terminal (C<is_serial_terminal>). +On a serial terminal command markers already appear on that terminal's log +(C<serial_terminal.txt>), so no hook is baked in and nothing is written to +C<serial0.txt> from that console. Consequently, residual C<OA:DONE>/C<OA:START> +markers seen in C<serial0.txt> for serial-terminal-centric tests originate from +the VNC/tty consoles those tests still use (where the hook I<is> installed), not +from the serial terminal itself. To eliminate them entirely, disable the feature +cluster-wide with C<PRETTY_SERIAL_MARKER=0>; see L</install_serial_marker_hook>. + =cut sub detect_serial_marker_capability ($self) { @@ -601,7 +649,10 @@ my $level = 1; my $pretty = $self->get_pretty_serial_marker(); my $serial_term = testapi::is_serial_terminal(); - return $self->{_serial_marker_level}->{$console} = $level if !$pretty || $serial_term; + if (!$pretty || $serial_term) { + $self->_disable_inherited_prompt_hook(1) if $serial_term; + return $self->{_serial_marker_level}->{$console} = $level; + } testapi::type_string "echo \"BASH:\$BASH_VERSION:\" > /dev/$testapi::serialdev\n"; my $out = testapi::wait_serial(qr/BASH:([^:]*):/, 10); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/doc/architecture.md new/os-autoinst-5.1787055635.69d1218/doc/architecture.md --- old/os-autoinst-5.1785768930.71d56f4/doc/architecture.md 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/doc/architecture.md 2026-08-18 14:20:35.000000000 +0200 @@ -5,11 +5,13 @@ Once everything is running, the process tree looks like this: * **isotovideo**: spawns further processes, IO-loop for passing commands (main occupation), cleanup - relevant files: `isotovideo`, `needle.pm` (initial needle scan) + relevant files: `isotovideo`, `driver.pm`, `OpenQA/Isotovideo/Runner.pm`, + `OpenQA/Isotovideo/CommandHandler.pm`, `needle.pm` (initial needle scan) * **backend**: spawns and handles backend (eg. qemu), receives commands from isotovideo IO-loop, handles the VNC connections, makes regular screenshots - relevant files: `baseclass.pm` and derived, `console.pm` and derived, `needle.pm` (reloading, matching), `cv.pm`, `ppmclibs/*` + relevant files: `baseclass.pm` and derived, `console.pm` and derived, `needle.pm` (reloading, + matching), `cv.pm`, `ppmclibs/*` * **qemu** (for instance) @@ -30,4 +32,29 @@ * **isotovideo** starts everything and passes commands between the other processes. * All processes have an IO loop except **autotest**. The latter mainly executes the test code and everything else reacts to it. -* The command server is accessed by the openQA worker and livehandler. +* The command server is accessed by the openQA worker and livehandler and the SUT via + `autoinst_url`. + +### Details about IPC between main processes +* **isotovideo** + * in `OpenQA::Isotovideo::Runner`: tokenless `read_json` from **autotest**, **backend**, + **command server** + * in `driver`: `send_json` and `read_json` pair *with* tokens to send/receive to/from + **backend** + * in `OpenQA::Isotovideo::CommandHandler`: `send_json` to send messages to **command server** + * in `OpenQA::Isotovideo::CommandHandler`: `send_json` to send messages to **backend** +* **autotest** + * in `autotest`: `send_json` and `read_json` pair *with* tokens to send/receive to/from + **isotovideo** +* **backend** + * in `baseclass:::check_socket`: tokenless `read_json` and optional `send_json` pair to handle + commands from **isotovideo** and send back result + * uses the multi-flag of `read_json` to handle multiple commands at once + * in `baseclass`: `send_json` in various places to send messages unrequested to **isotovideo** +* **command server** + * in `commands`: tokenless `read_json` to handle commands from **isotovideo** + * uses the multi-flag of `read_json` to handle multiple commands at once + * in `commands::isotovideo_command`: `send_json` and `read_json` pair *without* tokens to + send messages to **isotovideo** and wait for a response + * in `OpenQA::Commands`: `send_json` to send messages from ws clients to **isotovideo** (which + might handle them directly or forward them to the backend) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/doc/backend_vars.md new/os-autoinst-5.1787055635.69d1218/doc/backend_vars.md --- old/os-autoinst-5.1785768930.71d56f4/doc/backend_vars.md 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/doc/backend_vars.md 2026-08-18 14:20:35.000000000 +0200 @@ -41,7 +41,7 @@ | PAUSE_ON_NEXT_COMMAND | boolean | 0 | Pause test execution on the next test API command. Same notes as for `PAUSE_AT` apply. | | PAUSE_ON_FAILURE | boolean | 0 | Pause test execution on a test failure (instead of invoking the post-fail hook and terminating). Same notes as for `PAUSE_AT` apply. | | _QUIET_SCRIPT_CALLS | boolean | 0 | Add quiet flag to all the calls to script_run, script_output and validate_script_output. It will omit all the squares "wait_serial expected" on the Details view of the test case. This option might be useful for serial terminal tests. | -| PRETTY_SERIAL_MARKER | boolean | 1 | Enable "pretty" serial markers. When enabled, os-autoinst attempts to automatically detect SUT shell capabilities (like bash PROMPT_COMMAND and fc history) to forward command exit codes to serial without typing them visibly over VNC. Falls back to regular serial markers if no advanced shell features are detected. | +| PRETTY_SERIAL_MARKER | boolean | 1 | Enable "pretty" serial markers. When enabled, os-autoinst attempts to automatically detect SUT shell capabilities (like bash PROMPT_COMMAND and fc history) to forward command exit codes to serial without typing them visibly over VNC. Falls back to regular serial markers if no advanced shell features are detected. Implication: the detection installs a persistent PROMPT_COMMAND hook into the SUT's `~/.bashrc` and `~/.profile`, which then emits `OA:DONE`/`OA:START` markers to `/dev/$SERIALDEV` (i.e. `serial0.txt`) on every prompt of every shell that sources those files (including login/`exec $SHELL`/`su` and non-serial-terminal VNC consoles). Because the hook is persisted, it survives into later jobs booting the same qcow. To keep `serial0.txt` completely clean (e.g. for tests that parse it as a data channel or that run mostly via the virtio serial terminal where markers already appear in `serial_terminal.txt`), set `PRETTY_SERIAL_MARKER=0` for th e whole cluster, i.e. on both the parent job that creates the qcow image (so no hook is ever baked in) and all downstream jobs. Note that on the virtio serial terminal itself the hook is never installed; the residual `serial0.txt` markers in such tests originate from the VNC/tty consoles they still use. | | _WAIT_STILL_SCREEN_ON_HERE_DOC_INPUT | float | 0 | If this value is greater then 0, it is used by `wait_still_screen` before starting to write the script into the here document used in `testapi::script_output()` function (see: poo#60566). By default this depends on the backend. | | AUTOINST_URL_HOSTNAME | string | | hostname or IP address of host running the autoinst webserver endpoint, defaults to the local IP address within the qemu network for the qemu backend or the `WORKER_HOSTNAME` otherwise. | | UPLOAD_METER | boolean | 0 | Display curl progress meter in `upload_logs()` and `upload_assets()` test API functions. | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/myjsonrpc.pm new/os-autoinst-5.1787055635.69d1218/myjsonrpc.pm --- old/os-autoinst-5.1785768930.71d56f4/myjsonrpc.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/myjsonrpc.pm 2026-08-18 14:20:35.000000000 +0200 @@ -14,8 +14,11 @@ use constant DEBUG_JSON => $ENV{PERL_MYJSONRPC_DEBUG} || 0; use constant READ_BUFFER => $ENV{PERL_MYJSONRPC_BYTES} || 8_000_000; -# hash for keeping state -my $sockets; +my $interleaved_command_handler; # handler to deal with commands received while waiting for another reply + +sub set_interleaved_command_handler ($handler_or_array) { + $interleaved_command_handler = ref $handler_or_array eq 'ARRAY' ? sub (@args) { push @$handler_or_array, \@args } : $handler_or_array; +} sub _syswrite ($to_fd, $json, $length = undef, $offset = undef) { syswrite $to_fd, $json, $length, $offset } @@ -60,49 +63,65 @@ } # utility function -sub read_json ($socket, $cmd_token = undef, $multi = undef) { - my $cjx = Cpanel::JSON::XS->new->utf8; +my %RESULTS; +my %CJX; + +sub _extract_result_for_cmd_token ($results, $cmd_token, $multi) { + return undef if $multi; + my $i = 0; + for my $hash (@$results) { + if ($cmd_token && ($hash->{json_cmd_token} || '') ne $cmd_token) { + ++$i; + next; + } + return splice @$results, $i, 1; + } + return undef; +} +sub read_json ($socket, $cmd_token = undef, $multi = undef) { my $fd = fileno $socket; bmwqemu::diag("read_json($fd)") if is_debug(); - if (exists $sockets->{$fd}) { - # start with the trailing text from previous call - my $buffer = delete $sockets->{$fd}; - $cjx->incr_parse($buffer); - } + # return excess result from previous invocation + my $results = $RESULTS{$fd} //= []; + my $single_result = _extract_result_for_cmd_token($results, $cmd_token, $multi); + return $single_result if defined $single_result; + return splice @$results if $multi && @$results; + + my $cjx = $CJX{$fd} //= Cpanel::JSON::XS->new->utf8; my $s = IO::Select->new(); $s->add($socket); - my @results; - # the goal here is to find the end of the next valid JSON - and don't # add more data to it. As the backend sends things unasked, we might # run into the next message otherwise while (1) { my $hash = $cjx->incr_parse(); - # remember the trailing text if ($hash) { - $sockets->{$fd} = $cjx->incr_text(); bmwqemu::diag(sprintf 'read_json(%d) json_cmd_token=%s', $fd, $hash->{json_cmd_token} // 'no-token') if is_debug(); if ($hash->{QUIT}) { bmwqemu::diag('received magic close'); - push @results, undef; + push @$results, undef; + last; + } + if ($cmd_token && ($hash->{json_cmd_token} || '') ne $cmd_token) { + $interleaved_command_handler ? $interleaved_command_handler->($hash, $socket) : (push @$results, $hash); + next; + } + else { + push @$results, $hash; + # parse all lines from buffer + next if $multi; last; } - confess 'ERROR: the token does not match - questions and answers not in the right order' if $cmd_token && ($hash->{json_cmd_token} || '') ne $cmd_token; # uncoverable statement - push @results, $hash; - # parse all lines from buffer - next if $multi; - last; } - elsif ($multi and @results) { + elsif ($multi and @$results) { # read at least one item in list context last; } # wait for next read - handle_read_error($fd) until (my @res = $s->can_read); my $qbuffer; @@ -110,7 +129,8 @@ $cjx->incr_parse($qbuffer); } - return $multi ? @results : $results[0]; + return splice @$results if $multi; + return _extract_result_for_cmd_token($results, $cmd_token, $multi); } ################################################################### diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/03-testapi.t new/os-autoinst-5.1787055635.69d1218/t/03-testapi.t --- old/os-autoinst-5.1785768930.71d56f4/t/03-testapi.t 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/t/03-testapi.t 2026-08-18 14:20:35.000000000 +0200 @@ -309,6 +309,9 @@ }); testapi::assert_screen_change(sub { }, 42); is $received_timeout, 42, 'timeout forwarded to wait_screen_change'; + + $mock_wsc->redefine(wait_screen_change => 0); + throws_ok { testapi::assert_screen_change(sub { }, 42) } qr/assert_screen_change failed to detect a screen change/, 'expected error message for no screen change'; }; is $autotest::current_test->{dents}, 0, 'no soft failures so far'; @@ -323,15 +326,21 @@ always_explain $details unless $details_ok; $mock_bmwqemu->noop('log_call'); -require distribution; -testapi::set_distribution(distribution->new()); -$autotest::last_milestone = {}; -select_console('a-console'); -is console('a-console')->{console}, 'a-console'; -is_deeply $autotest::activated_consoles, ['a-console'], 'Current console is activated'; -is is_serial_terminal, 0, 'Not a serial terminal'; -is current_console, 'a-console', 'Current console is the a-console'; -is console('b-console')->{console}, 'b-console', 'new console created on the fly'; +subtest console => sub { + require distribution; + testapi::set_distribution(distribution->new()); + $autotest::last_milestone = {}; + select_console('a-console'); + is console('a-console')->{console}, 'a-console'; + is_deeply $autotest::activated_consoles, ['a-console'], 'Current console is activated'; + is is_serial_terminal, 0, 'Not a serial terminal'; + is current_console, 'a-console', 'Current console is the a-console'; + is console('b-console')->{console}, 'b-console', 'new console created on the fly'; + + $mod2->redefine(query_isotovideo => {error => 'on purpose'}); + throws_ok { select_console('dummy') } qr/on purpose/, 'select_console dies as expected'; + $mod2->unmock('query_isotovideo'); +}; subtest 'script_run' => sub { local $bmwqemu::vars{PRETTY_SERIAL_MARKER} = 0; @@ -406,7 +415,7 @@ is assert_script_run('true', max_interval => 1), undef, 'nothing happens on success (slow typing)'; is_deeply $cmds, [ { - text => 'true; echo XXX-$?-', + text => '_OANM=1; true; echo XXX-$?-', cmd => 'backend_type_string', max_interval => 1 }, @@ -659,7 +668,7 @@ upload_logs '/var/log/messages'; is_deeply $cmds, [ { - text => 'curl --form upload=@/var/log/messages --form upname=basetest-messages http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-', + text => '_OANM=1; curl --form upload=@/var/log/messages --form upname=basetest-messages http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-', cmd => 'backend_type_string', max_interval => 250 }, @@ -673,7 +682,7 @@ upload_logs '/var/log/messages', failok => 1; is_deeply $cmds, [ { - text => 'curl --form upload=@/var/log/messages --form upname=basetest-messages --max-time 90 http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-', + text => '_OANM=1; curl --form upload=@/var/log/messages --form upname=basetest-messages --max-time 90 http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-', cmd => 'backend_type_string', max_interval => 250 }, @@ -696,7 +705,7 @@ is_deeply $cmds, [ { cmd => 'backend_type_string', - text => 'sudo rm /boot/grub/menu.lst; echo XXX-$?-', + text => '_OANM=1; sudo rm /boot/grub/menu.lst; echo XXX-$?-', max_interval => 250 }, { @@ -921,7 +930,7 @@ } qr/output not validating/, 'Die on output not match for regex'; throws_ok { validate_script_output('script', ['Invalid parameter']) - } qr/coderef or regexp/, 'Die on invalid parameter'; + } qr/Invalid use.*coderef or regexp/, 'Die on invalid parameter'; throws_ok { validate_script_output('script', qr/error/, fail_message => 'foo bar') } qr/foo bar/, 'Die on output not match'; @@ -979,6 +988,10 @@ 'assert_still_screen forwards arguments to wait_still_screen'; $fake_timeout = 1; ok !wait_still_screen, 'falsy return value on timeout'; + + $testapi->redefine(wait_still_screen => 0); + throws_ok { assert_still_screen; } qr/assert_still_screen failed to detect a still screen/, + 'expeected error message for no still screen'; }; subtest 'test console::console argument settings' => sub { @@ -1294,6 +1307,10 @@ lives_ok { force_soft_failure('boo#42') } 'can call force_soft_failure'; +subtest 'get_var' => sub { + throws_ok { get_required_var 'NOT HERE' } qr/Could not retrieve required variable NOT HERE/, 'get_required_var dies with expected message'; +}; + subtest 'set_var' => sub { $cmds = []; lives_ok { set_var('FOO', 'BAR', reload_needles => 1) } 'can call set_var with reload_needles'; @@ -1366,6 +1383,7 @@ like get_test_data('console.ref.json'), qr/area/, 'get_test_data can be called'; $fake_exit = 0; lives_ok { become_root } 'become_root can be called'; +lives_ok { become_user('geeko') } 'become_user can be called'; lives_ok { hold_key('ret') } 'hold_key can be called'; lives_ok { release_key('ret') } 'release_key can be called'; lives_ok { reset_consoles } 'reset_consoles can be called'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/05-distribution.t new/os-autoinst-5.1787055635.69d1218/t/05-distribution.t --- old/os-autoinst-5.1785768930.71d56f4/t/05-distribution.t 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/t/05-distribution.t 2026-08-18 14:20:35.000000000 +0200 @@ -53,24 +53,27 @@ return $wait_serial_res; }); $mock_testapi->redefine(is_serial_terminal => 1); + my $oanm_len = length '_OANM=1; '; $d->script_run('short_command'); - # script_run calls wait_serial three times when on a serial - # console, the call we want to check - which actually types the - # command - is the second - my $cmdcall = $wait_serial_calls[1]; - is $cmdcall->{buffer_size}, 141, 'appropriate buffer size used for short command'; + # First script_run also detects marker capability, which types an extra + # 'unset PROMPT_COMMAND' and waits for the prompt, so the call that types + # the command is the third + my $cmdcall = $wait_serial_calls[2]; + is $cmdcall->{buffer_size}, 141 + $oanm_len, 'appropriate buffer size used for short command (base + _OANM prefix)'; + # marker capability is now cached, so no 'unset PROMPT_COMMAND' wait + # precedes this call: the command is at index 1 @wait_serial_calls = (); $d->script_run('long_command' x 512); $cmdcall = $wait_serial_calls[1]; - is $cmdcall->{buffer_size}, 6272, 'appropriate buffer size used for long command'; + is $cmdcall->{buffer_size}, 6272 + $oanm_len, 'appropriate buffer size used for long command (base + _OANM prefix)'; $wait_serial_res = 0; @wait_serial_calls = (); - throws_ok { $d->script_run('foo') } qr/typing command 'foo' timed out/, 'timeout while typing command handled'; + throws_ok { $d->script_run('foo') } qr/typing command '_OANM=1; foo' timed out/, 'timeout while typing command handled'; @wait_serial_calls = (); combined_like { $d->script_run('foo', check_typing_cmd => 0) } - qr/typing command 'foo' timed out/, 'timeout while typing command just logged when opted-out'; + qr/typing command '_OANM=1; foo' timed out/, 'timeout while typing command just logged when opted-out'; }; subtest 'pretty_serial_marker' => sub { @@ -78,6 +81,7 @@ my $mock_testapi = Test::MockModule->new('testapi'); my $mock_bmwqemu = Test::MockModule->new('bmwqemu'); $mock_bmwqemu->noop('log_call'); + $mock_bmwqemu->noop('diag'); my $typed_string = ''; $mock_testapi->redefine(query_isotovideo => sub { }); $mock_testapi->redefine(type_string => sub { $typed_string .= $_[0] }); @@ -101,7 +105,7 @@ $mock_testapi->redefine(wait_serial => sub ($regexp, @) { return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ $regexp; return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp; - return 'OA:DONE-abcd-0-foo'; + return 'OA:DONE-abcd-0-OA:foo3foo'; }); $d->{_serial_marker_level} = {}; @@ -130,13 +134,13 @@ $mock_testapi->redefine(is_serial_terminal => sub { 1 }); $typed_string = ''; $d->script_run('foo'); - like $typed_string, qr/foo; echo SR.*-.*-\n/, 'Level 1 uses classic marker on serial terminal'; + like $typed_string, qr/_OANM=1; foo; echo SR.*-.*-\n/, 'Level 1 uses classic marker on serial terminal'; $mock_testapi->redefine(wait_serial => sub ($pat, %) { return 0 if $pat =~ /foo; echo SR.*-\$\?-/; return 'SRfoo-0-'; }); - throws_ok { $d->script_run('foo') } qr/typing command 'foo' timed out/, 'typing error handled in Level 1'; + throws_ok { $d->script_run('foo') } qr/typing command '_OANM=1; foo' timed out/, 'typing error handled in Level 1'; }; subtest 'serial_marker_reinstall_cached_level' => sub { @@ -150,7 +154,7 @@ $d->invalidate_serial_marker_hook('test-console'); is $d->detect_serial_marker_capability(), 2, 'Returns cached level 2'; - like $typed, qr/grep -q _oap.*\. ~\/\.bashrc/, 'Calls install_serial_marker_hook (types consolidated setup with sourcing)'; + like $typed, qr/grep -q '_OAPV=\d+'.*\. ~\/\.bashrc/, 'Calls install_serial_marker_hook (types consolidated setup with sourcing)'; ok $d->{_serial_marker_hook_installed}->{'test-console'}, 'Hook marked as installed'; }; @@ -171,11 +175,14 @@ $mock_testapi->redefine(wait_serial => sub ($regexp, @) { return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ $regexp; return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp; - return 'OA:DONE-abcd-0-'; + my $regexp_str = "$regexp"; + my ($fp) = grep { (my $c = $regexp_str) =~ s/\\//g; $c =~ /\Q$_\E/ } + map { $d->sut_marker($_) } qw(foo bar baz qux); + return "OA:DONE-abcd-0-$fp"; }); $d->script_run('foo'); - like $typed_string, qr/grep -q _oap.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 'Initial install'; + like $typed_string, qr/grep -q '_OAPV=\d+'.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 'Initial install'; $typed_string = ''; # Simulate console selection (e.g. after reboot/login) @@ -191,7 +198,7 @@ $d->reset_serial_marker('test-console'); $typed_string = ''; $d->script_run('baz'); - like $typed_string, qr/grep -q _oap.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 'Re-detect and re-install after resetting the serial marker'; + like $typed_string, qr/grep -q '_OAPV=\d+'.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 'Re-detect and re-install after resetting the serial marker'; like $typed_string, qr/baz\n/, 'Command typed after re-installation'; # Case 3: select_console triggers reset @@ -217,6 +224,61 @@ } }; +subtest 'level3_marker_correlation' => sub { + my $d = distribution->new; + my $mock_testapi = Test::MockModule->new('testapi'); + my $mock_bmwqemu = Test::MockModule->new('bmwqemu'); + $mock_bmwqemu->noop('log_call'); + $testapi::serialdev = 'ttyS0'; + + my $typed = ''; + $mock_testapi->redefine(type_string => sub { $typed .= $_[0] }); + $d->install_serial_marker_hook(3); + like $typed, qr'c=\$\(HISTTIMEFORMAT= history 1\);c=\$\{c#\*\[0-9\] \}', + 'level-3 shell hook captures the just-run command via in-memory history (no fc off-by-one)'; + like $typed, qr'l=\$\{#c\};t=\$c;\[ \$l -ge 4 \]&&t=\$\{c: -4\};printf.*OA:DONE', + 'level-3 shell hook emits the compact head4+len+tail4 command fingerprinting script'; + unlike $typed, qr'fc -ln -1', + 'level-3 shell hook no longer uses fc -ln -1 (off-by-one lag emitting the previous command marker one prompt late)'; + + $d->{_serial_marker_level}->{'test-console'} = 3; + $d->{_serial_marker_hook_installed}->{'test-console'} = 1; + $mock_testapi->redefine(current_console => sub { 'test-console' }); + $mock_testapi->redefine(query_isotovideo => sub { }); + + my @regexes_seen; + $mock_testapi->redefine(wait_serial => sub ($regexp, @) { + push @regexes_seen, $regexp; + return 'OA:DONE-1234-0-OA:curl11logs'; + }); + + my $exit_code = $d->script_run('curl --logs'); + is $exit_code, 0, 'Level 3 script_run returns correct exit code on successful command match'; + is scalar(@regexes_seen), 1, 'Only wait_serial for the anchored fingerprint is called when match succeeds'; + like $regexes_seen[0], qr/OA:DONE-\[0-9a-f\]\{4\}-\(\\d\+\)-OA(?:\\:|:)curl11logs/, + 'wait_serial matches the exact head4+len+tail4 command fingerprint of curl --logs'; + + my $fp = $d->sut_marker('curl --logs'); + my $regex = qr/OA:DONE-[0-9a-f]{4}-(\d+)-\Q$fp\E/; + my $stale_systemctl = "OA:DONE-aaaa-1-OA:syst15_ctl\n"; + my $stale_tar = "OA:DONE-bbbb-2-OA:tar_7_tar\n"; + my $correct_curl = "OA:DONE-cccc-0-OA:curl11logs\n"; + + unlike $stale_systemctl, $regex, 'Stale unconsumed systemctl markers from other commands are ignored by the curl regex'; + unlike $stale_tar, $regex, 'Stale unconsumed tar markers from other commands are ignored by the curl regex'; + like $correct_curl, $regex, 'The target curl marker matches the anchored regex perfectly'; + + my ($extracted_exit) = ($correct_curl =~ $regex); + is $extracted_exit, 0, 'Exit code is correctly extracted from the fingerprinted marker'; + + @regexes_seen = (); + $mock_testapi->redefine(wait_serial => sub ($regexp, @) { push @regexes_seen, $regexp; return undef }); + $exit_code = $d->script_run('curl --logs'); + is $exit_code, undef, 'Anchored match miss fails closed with undef instead of an unreliable generic fallback'; + is scalar(@regexes_seen), 1, 'No second generic wait_serial is issued, preventing stale-marker mismatch and doubled timeout'; + like $regexes_seen[0], qr/OA:DONE-\[0-9a-f\]\{4\}-\(\\d\+\)-OA(?:\\:|:)curl11logs/, 'The single wait_serial call uses the anchored fingerprint'; +}; + subtest 'set expected serial and autoinst failures' => sub { my $d = distribution->new; my @failures = ( @@ -296,7 +358,7 @@ # First install $d->install_serial_marker_hook(3); - like $typed, qr/grep -q _oap.*\. ~\/\.bashrc/, 'Types consolidated setup with persistence and sourcing'; + like $typed, qr/grep -q '_OAPV=\d+'.*\. ~\/\.bashrc/, 'Types consolidated setup with persistence and sourcing'; ok $d->{_serial_marker_hook_persistent}->{'test-console'}, 'Persistence marked'; # Invalidate hook but keep persistence @@ -306,6 +368,19 @@ like $typed, qr/\. ~\/\.bashrc/, 'Types setup again (with sourcing) when invalidated'; }; +subtest 'serial_marker_hook_version_migration' => sub { + my $d = distribution->new; + my $mock_testapi = Test::MockModule->new('testapi'); + $mock_testapi->redefine(current_console => sub { 'test-console' }); + my $typed = ''; + $mock_testapi->redefine(type_string => sub { $typed .= $_[0] }); + $d->install_serial_marker_hook(3); + like $typed, qr/grep -q '_OAPV=\d+'/, 'Guard keys on a version tag, not mere _oap presence'; + like $typed, qr/sed -i '[^']*_oap[^']*' ~\/\.bashrc ~\/\.profile/, + 'Stale hook lines are stripped before reinstall so an old-format _oap persisted by a previous os-autoinst is replaced'; + like $typed, qr/echo '_OAPV=\d+;_oap\(\)/, 'The version tag is persisted together with the hook definition'; +}; + subtest 'serial_terminal_redirection_guard' => sub { my $d = distribution->new; my $mock_testapi = Test::MockModule->new('testapi'); @@ -391,6 +466,26 @@ like $typed, qr/_oap/, 'hook is successfully re-installed on the next command after cached status is explicitly invalidated'; }; +subtest 'become_user' => sub { + my $d = distribution->new; + my $mock_testapi = Test::MockModule->new('testapi'); + my $mock_bmwqemu = Test::MockModule->new('bmwqemu'); + $mock_bmwqemu->noop('log_call'); + my $typed = ''; + $mock_testapi->redefine(query_isotovideo => sub { }); + $mock_testapi->redefine(type_string => sub { $typed .= $_[0] }); + $mock_testapi->redefine(is_serial_terminal => sub { 0 }); + $mock_testapi->redefine(current_console => sub { 'test-console' }); + $mock_testapi->redefine(get_var => sub { $_[0] eq 'PRETTY_SERIAL_MARKER' ? 1 : undef }); + $testapi::serialdev = 'ttyS0'; + $mock_testapi->redefine(wait_serial => undef); + $d->{_serial_marker_level}->{'test-console'} = 3; + $d->become_user('geeko'); + like $typed, qr/su - geeko/, 'become_user types su command to switch session user'; + like $typed, qr/_oap/, 'become_user automatically reinstalls the shell synchronization hook after invalidation'; + throws_ok { $d->become_user('geeko; rm -rf /') } qr/Invalid username/, 'invalid username is rejected'; +}; + done_testing; 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/22-svirt.t new/os-autoinst-5.1787055635.69d1218/t/22-svirt.t --- old/os-autoinst-5.1785768930.71d56f4/t/22-svirt.t 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/t/22-svirt.t 2026-08-18 14:20:35.000000000 +0200 @@ -117,8 +117,10 @@ like shift @$cmds, qr/cat > \/t <<.*username=u.*password=p.*auth-esx-h/s, 'config written'; my $s = 'virsh -c esx://u@h/?no_verify=1\\&authfile=/t '; my @expected = ( + $s . ' dominfo openQA-SUT-1 2>/dev/null', $s . ' destroy openQA-SUT-1', $s . ' undefine --snapshots-metadata openQA-SUT-1', + $s . ' dominfo openQA-SUT-1 2>/dev/null', $s . ' define /var/lib/libvirt/images/openQA-SUT-1.xml', 'echo \'bios.bootDelay = "10000"\' >> /vmfs/volumes/datastore1/openQA/openQA-SUT-1.vmx', 'test -e /vmfs/volumes/datastore1/openQA/openQA-SUT-1.nvram', @@ -321,6 +323,22 @@ ]); }; +subtest 'starting VMware console with define failure' => sub { + for my $tc ( + {ret => 1, stderr => "some define error\n", expected => qr/virsh define failed: some define error/}, + {ret => 2, stderr => undef, expected => qr/virsh define failed: exit code 2/} + ) { + my $mocks = _mock_svirt_vmware([], []); + $mocks->{console}->redefine( + run_cmd => sub ($self, $cmd, %args) { + return ($tc->{ret}, undef, $tc->{stderr}) if $cmd =~ /define/ && $args{wantarray}; + return 0; + } + ); + throws_ok { $svirt_console->define_and_start } $tc->{expected}; + } +}; + subtest 'SSH credentials' => sub { set_var('VIRSH_GUEST', 'foo321'); @@ -668,15 +686,43 @@ }; subtest 'family svirt-xen-hvm create=1 error handling' => sub { - @ssh_cmd_return = ([1, '', 'lock'], [1, '', 'lock'], [1, '', 'lock'], [1, '', 'lock'], [1, '', 'lock']); + # 5 failures, active recovery (destroy + pkill), 2 more failures -> dies + @ssh_cmd_return = ( + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [0, '', ''], # virsh destroy + [0, '', ''], # pkill + [1, '', 'lock'], + [1, '', 'lock'], + ); my $dev_id = 'dev_id_005'; my $exp_file = $svirt->name . $dev_id . '.img'; - throws_ok { $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id}) } qr/Too many attempts to create disk/, 'Died after 5 retry attempts'; + throws_ok { $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id}) } qr/Too many attempts to create disk/, 'died after 7 total attempts with active recovery'; + + # 5 failures, active recovery, then succeeds on 6th attempt + @ssh_cmd_return = ( + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [1, '', 'lock'], + [0, '', ''], # virsh destroy + [0, '', ''], # pkill + [0, '', ''], # succeeds + ); + @last_ssh_commands = (); + $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id}); + is $last_ssh_commands[-2], "pkill -9 -f 'openQA-SUT-1'", 'active recovery ran pkill'; + like $last_ssh_commands[-3], qr/destroy 'openQA-SUT-1'/, 'active recovery ran virsh destroy'; + is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f qcow2 88G", 'triggered img creation, after active recovery'; @ssh_cmd_return = ([1, '', 'lock'], [1, '', 'lock'], [1, '', 'lock'], [1, '', 'lock'], [0, '', '']); $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id}); - is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f qcow2 88G", 'Triggered img creation, after 4 errors'; + is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f qcow2 88G", 'triggered img creation, after 4 errors'; @ssh_cmd_return = ([0, '', ''], [0, '', ''], [0, '', ''], [0, '', ''], [0, '', '']); @@ -729,6 +775,7 @@ size => 12 }); like $last_ssh_commands[0], qr%^rsync.*--partial.*/my/path/to/this/file/$file.*$basedir/$file%, 'Use rsync to copy file'; + is $last_ssh_commands[1], "qemu-img info --force-share --output=json /my/path/to/this/file/$file", 'qemu-img info uses --force-share'; is $last_ssh_commands[-1], "qemu-img create '${basedir}openQA-SUT-1$dev_id.img' -f qcow2 -F qcow2 -b '$basedir/$file' 12G", 'Used image size > backingfile size'; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/24-myjsonrpc.t new/os-autoinst-5.1787055635.69d1218/t/24-myjsonrpc.t --- old/os-autoinst-5.1785768930.71d56f4/t/24-myjsonrpc.t 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/t/24-myjsonrpc.t 2026-08-18 14:20:35.000000000 +0200 @@ -66,6 +66,44 @@ dies_ok { myjsonrpc::send_json($child, $send1) } 'myjsonrpc: remote end terminated connection, stopping'; }; +subtest 'handling interleaved commands' => sub { + my ($sub_child, $sub_isotovideo); + socketpair $sub_child, $sub_isotovideo, AF_UNIX, SOCK_STREAM, PF_UNSPEC; + $sub_child->autoflush(1); + $sub_isotovideo->autoflush(1); + + my $msg_a = {a => 1, json_cmd_token => 'token-A'}; + my $msg_b = {b => 2, json_cmd_token => 'token-B'}; + my $msg_c = {c => 3, json_cmd_token => 'token-C'}; + my $msg_d = {d => 4, json_cmd_token => 'token-D'}; + + subtest 'interleaved_command_handler is not set' => sub { + myjsonrpc::send_json($sub_child, $msg_c); + myjsonrpc::send_json($sub_child, $msg_d); + myjsonrpc::send_json($sub_child, $msg_b); + myjsonrpc::send_json($sub_child, $msg_a); + my $read_a = myjsonrpc::read_json($sub_isotovideo, 'token-A'); + is_deeply $read_a, $msg_a, 'read_json with token-A returns msg_a'; + my $read_b = myjsonrpc::read_json($sub_isotovideo, 'token-B'); + is_deeply $read_b, $msg_b, 'subsequent read_json call returns msg_b from cached results'; + my @read_c_d = myjsonrpc::read_json($sub_isotovideo, undef, 1); + is_deeply \@read_c_d, [$msg_c, $msg_d], 'subsequent multi read_json call returns msg_c and msg_d from cached results'; + }; + subtest 'interleaved_command_handler is set' => sub { + my @interleaved; + myjsonrpc::set_interleaved_command_handler(\@interleaved); + myjsonrpc::send_json($sub_child, $msg_b); + myjsonrpc::send_json($sub_child, $msg_a); + my $read_a = myjsonrpc::read_json($sub_isotovideo, 'token-A'); + is_deeply $read_a, $msg_a, 'read_json with token-A returns msg_a'; + is_deeply \@interleaved, [[$msg_b, $sub_isotovideo]], 'message msg_b captured'; + }; + + myjsonrpc::set_interleaved_command_handler(undef); + close $sub_child; + close $sub_isotovideo; +}; + my $io_select_mock = Test::MockModule->new('IO::Select'); $io_select_mock->redefine(can_read => undef); throws_ok { myjsonrpc::read_json($isotovideo) } qr/Illegal seek/, 'error exception raised when reading is aborted'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/29-backend-svirt.t new/os-autoinst-5.1787055635.69d1218/t/29-backend-svirt.t --- old/os-autoinst-5.1785768930.71d56f4/t/29-backend-svirt.t 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/t/29-backend-svirt.t 2026-08-18 14:20:35.000000000 +0200 @@ -54,6 +54,7 @@ # silence some log output for cleaner tests $bmwqemu_mock->noop('diag'); $bmwqemu_mock->noop('log_call'); + $bmwqemu_mock->noop('fctwarn'); redefine_ssh; $backend->{need_delete_log} = 1; ok $backend->do_start_vm, 'can start vm'; @@ -80,6 +81,7 @@ # silence some log output for cleaner tests $bmwqemu_mock->noop('diag'); $bmwqemu_mock->noop('log_call'); + $bmwqemu_mock->noop('fctwarn'); redefine_ssh; $backend->{need_delete_log} = 1; ok $backend->do_start_vm, 'can start vm'; @@ -104,6 +106,7 @@ # silence some log output for cleaner tests $bmwqemu_mock->noop('diag'); $bmwqemu_mock->noop('log_call'); + $bmwqemu_mock->noop('fctwarn'); redefine_ssh; ok $backend->do_start_vm, 'can start vm'; is $backend->can_handle({function => 'snapshots'})->{ret}, 1, 'can handle snapshots'; @@ -126,4 +129,29 @@ is $virsh, 'virsh my_vmm', 'correct output from virsh()'; }; +subtest 'do_stop_vm_svirt validation' => sub { + my $backend = backend::svirt->new; + my $bmwqemu_mock = Test::MockModule->new('bmwqemu'); + $bmwqemu_mock->noop('diag'); + $bmwqemu_mock->noop('log_call'); + + subtest 'triggers fctwarn when the domain still exists after undefining' => sub { + my @warn_log; + $bmwqemu_mock->redefine(fctwarn => sub { push @warn_log, @_ }); + $run_ssh_cmd_mock->redefine(run_ssh_cmd => 0); + $backend->do_stop_vm_svirt; + like "@warn_log", qr/still exists/, 'logged fctwarn contains still exists warning'; + }; + + subtest 'does not trigger fctwarn when the domain is successfully gone' => sub { + my @warn_log; + $bmwqemu_mock->redefine(fctwarn => sub { push @warn_log, @_ }); + $run_ssh_cmd_mock->redefine(run_ssh_cmd => sub ($self, $cmd, %args) { + return $cmd =~ /dominfo/ ? 1 : 0; + }); + $backend->do_stop_vm_svirt; + is scalar(@warn_log), 0, 'no warning was logged'; + }; +}; + done_testing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/testapi.pm new/os-autoinst-5.1787055635.69d1218/testapi.pm --- old/os-autoinst-5.1785768930.71d56f4/testapi.pm 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/testapi.pm 2026-08-18 14:20:35.000000000 +0200 @@ -63,7 +63,7 @@ wait_screen_change assert_screen_change wait_still_screen assert_still_screen wait_serial record_soft_failure record_info force_soft_failure - become_root x11_start_program ensure_installed eject_cd disconnect_usb power + become_root become_user x11_start_program ensure_installed eject_cd disconnect_usb power switch_network save_memory_dump freeze_vm resume_vm save_storage @@ -244,7 +244,7 @@ sub record_info ($title, $output = undef, %nargs) { $nargs{result} //= 'ok'; - die 'unsupported $result \'' . $nargs{result} . '\'' unless _is_valid_result($nargs{result}); + OpenQA::Exception::TestapiUsageError->throw(error => "unsupported \$result '$nargs{result}'") unless _is_valid_result($nargs{result}); $output //= ''; bmwqemu::log_call(title => $title, output => $output, %nargs); $autotest::current_test->record_resultfile($title, $output, %nargs); @@ -375,8 +375,8 @@ } sub _check_or_assert ($mustmatch, $check, %args) { - die 'no tags specified' if (!$mustmatch || (ref $mustmatch eq 'ARRAY' && scalar @$mustmatch == 0)); - die 'current_test undefined' unless $autotest::current_test; + OpenQA::Exception::TestapiUsageError->throw(error => 'no tags specified') if (!$mustmatch || (ref $mustmatch eq 'ARRAY' && scalar @$mustmatch == 0)); + OpenQA::Exception::TestapiUsageError->throw(error => 'current_test undefined') unless $autotest::current_test; $args{timeout} = bmwqemu::scale_timeout($args{timeout}); @@ -645,7 +645,8 @@ # wait_screen_change uses prototype which expects code block as an argument # This resolves compile time issues my ($coderef, @args) = @_; - wait_screen_change(\&{$coderef}, @args) or die 'assert_screen_change failed to detect a screen change'; + wait_screen_change(\&{$coderef}, @args) or + OpenQA::Exception::TestapiError->throw(error => 'assert_screen_change failed to detect a screen change'); } @@ -700,7 +701,7 @@ =cut sub assert_still_screen (@args) { - wait_still_screen(@args) or die 'assert_still_screen failed to detect a still screen'; + wait_still_screen(@args) or OpenQA::Exception::TestapiError->throw(error => 'assert_still_screen failed to detect a still screen'); } =head1 test variable access @@ -727,7 +728,7 @@ =cut sub get_required_var ($var) { - return $bmwqemu::vars{$var} // croak "Could not retrieve required variable $var"; + return $bmwqemu::vars{$var} // OpenQA::Exception::TestapiError->throw(error => "Could not retrieve required variable $var"); } =head2 set_var @@ -1008,7 +1009,7 @@ bmwqemu::log_call(cmd => $cmd, %args); my $ret = $distri->script_run($cmd, %args); - croak("command '$cmd' timed out") if $args{timeout} > 0 && !defined $ret; + OpenQA::Exception::TestapiError->throw(error => "command '$cmd' timed out") if $args{timeout} > 0 && !defined $ret; return $ret; } @@ -1238,13 +1239,13 @@ $script, $check, $output; } else { - croak 'Invalid use of validate_script_output(), second arg must be a coderef or regexp'; + OpenQA::Exception::TestapiError->throw(error => 'Invalid use of validate_script_output(), second arg must be a coderef or regexp'); } $autotest::current_test->record_resultfile( $title, $message, result => $res, ); - croak $fail_message if $res eq 'fail'; + OpenQA::Exception::TestapiError->throw(error => $fail_message) if $res eq 'fail'; return 0; } @@ -1260,6 +1261,20 @@ sub become_root () { $distri->become_root } +=head2 become_user + + become_user($user); + +Switch the current shell session's user to C<$user> (e.g. via C<su - $user>) and +properly handle openQA serial marker synchronization hook invalidation so subsequent +commands run cleanly under the new user. + +I<The implementation is distribution specific and not always available.> + +=cut + +sub become_user ($user) { $distri->become_user($user) } + =head2 ensure_installed ensure_installed $package; @@ -1439,7 +1454,7 @@ } if ($wait_still && !wait_still_screen(stilltime => $wait_still, timeout => $wait_timeout, similarity_level => $wait_sim_level)) { - die "wait_still_screen timed out after ${wait_timeout}s!"; + OpenQA::Exception::TestapiError->throw(error => "wait_still_screen timed out after ${wait_timeout}s!"); } } } @@ -1606,7 +1621,7 @@ } # If neither coordinates nor a needle is provided, report an error and quit. else { - die "The starting point of the drag was not correctly provided. Either provide the 'startx' and 'starty' coordinates, or a needle marking the starting point."; + OpenQA::Exception::TestapiUsageError->throw(error => "The starting point of the drag was not correctly provided. Either provide the 'startx' and 'starty' coordinates, or a needle marking the starting point."); } # Repeat the same for endpoint coordinates or needles. @@ -1620,7 +1635,7 @@ ($endx, $endy) = _calculate_clickpoint($end_matched_needle); } else { - die "The ending point of the drag was not correctly provided. Either provide the 'endx' and 'endy' coordinates, or a needle marking the end point."; + OpenQA::Exception::TestapiUsageError->throw(error => "The ending point of the drag was not correctly provided. Either provide the 'endx' and 'endy' coordinates, or a needle marking the end point."); } # Get the button variable. If no button has been provided, assume the "left" button. my $button = $args{button} // 'left'; @@ -1704,7 +1719,7 @@ $testapi_console_proxies{$testapi_console} = backend::console_proxy->new($testapi_console); } my $ret = query_isotovideo('backend_select_console', {testapi_console => $testapi_console}); - die $ret->{error} if $ret->{error}; + OpenQA::Exception::TestapiError->throw(error => $ret->{error}) if $ret->{error}; $autotest::selected_console = $testapi_console; if ($ret->{activated}) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/tools/test_containers new/os-autoinst-5.1787055635.69d1218/tools/test_containers --- old/os-autoinst-5.1785768930.71d56f4/tools/test_containers 2026-08-03 16:55:30.000000000 +0200 +++ new/os-autoinst-5.1787055635.69d1218/tools/test_containers 2026-08-18 14:20:35.000000000 +0200 @@ -7,5 +7,12 @@ echo "$0 => Testing container $i" tag=${i,,} $cre build -t "$tag" -f "$i" "$(dirname "$i")"/ + if grep -q "BuildTag:" "$i"; then + # shellcheck disable=SC2013 + for t in $(sed -n 's/.*BuildTag: //p' "$i"); do + [[ "$t" == *%* ]] && continue + $cre tag "$tag" "$t" + done + fi $cre run --rm "$tag" --help done ++++++ os-autoinst.obsinfo ++++++ --- /var/tmp/diff_new_pack.Hpp0Ix/_old 2026-08-19 18:02:51.334761939 +0200 +++ /var/tmp/diff_new_pack.Hpp0Ix/_new 2026-08-19 18:02:51.339762118 +0200 @@ -1,5 +1,5 @@ name: os-autoinst -version: 5.1785768930.71d56f4 -mtime: 1785768930 -commit: 71d56f4caac7a7cbb21f22654bce7a7dbe4cc779 +version: 5.1787055635.69d1218 +mtime: 1787055635 +commit: 69d12181d071203c9ce43249351c5f5ac6760a3e
