Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openQA for openSUSE:Factory checked in at 2026-06-22 17:27:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.1956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Mon Jun 22 17:27:35 2026 rev:855 rq:1360717 version:5.1781884690.e39cc969 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-06-19 17:17:54.402918373 +0200 +++ /work/SRC/openSUSE:Factory/.openQA.new.1956/openQA.changes 2026-06-22 17:28:17.155334940 +0200 @@ -1,0 +2,13 @@ +Fri Jun 19 16:54:50 UTC 2026 - [email protected] + +- Update to version 5.1781884690.e39cc969: + * test: Make parallel execution of the fullstack test more reliable + * ci: disable Mergify interactive queue controls in PR comments + * feat(worker): enable direct execution with podman + * fix(worker): add --init and --rm flags for containerized engine + * feat(worker): support containerized os-autoinst worker engine + * test: refactor archive download tests for maintainability + * feat: implement category-specific ZIP downloads for jobs + * chore(deps): Dependency cron 2026-06-19 + +------------------------------------------------------------------- Old: ---- openQA-5.1781832185.5ddf5343.obscpio New: ---- openQA-5.1781884690.e39cc969.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:18.847393883 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:18.851394022 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1781832185.5ddf5343 +Version: 5.1781884690.e39cc969 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:18.879394997 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:18.883395137 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1781832185.5ddf5343 +Version: 5.1781884690.e39cc969 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:18.915396252 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:18.919396391 +0200 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1781832185.5ddf5343 +Version: 5.1781884690.e39cc969 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:18.947397366 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:18.947397366 +0200 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1781832185.5ddf5343 +Version: 5.1781884690.e39cc969 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:18.975398341 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:18.979398481 +0200 @@ -104,7 +104,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1781832185.5ddf5343 +Version: 5.1781884690.e39cc969 Release: 0 Summary: Framework for automated system-level testing (web-frontend, scheduler and tools) Group: Development/Tools/Other ++++++ openQA-5.1781832185.5ddf5343.obscpio -> openQA-5.1781884690.e39cc969.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/.mergify.yml new/openQA-5.1781884690.e39cc969/.mergify.yml --- old/openQA-5.1781832185.5ddf5343/.mergify.yml 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/.mergify.yml 2026-06-19 17:58:10.000000000 +0200 @@ -61,3 +61,4 @@ queue_rules: [] merge_queue: status_comments: none + queue_controls_comment: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/docs/WritingTests.md new/openQA-5.1781884690.e39cc969/docs/WritingTests.md --- old/openQA-5.1781832185.5ddf5343/docs/WritingTests.md 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/docs/WritingTests.md 2026-06-19 17:58:10.000000000 +0200 @@ -726,6 +726,16 @@ test variable setting `ISOTOVIDEO=podman run --pull=always --rm -it registry.example.org/my/container/isotovideo /usr/bin/isotovideo -d` +Alternatively, you can run the worker engine inside a rootless Podman +container by setting `OS_AUTOINST_GIT_REPO` to a Git repository URL. +openQA will construct a Podman command to clone, build, and execute +`os-autoinst` from that repository. +The following optional test variables are supported: +* `OS_AUTOINST_GIT_BRANCH`: The branch to clone (defaults to `master`). +* `OS_AUTOINST_CONTAINER_IMAGE`: A custom container image to use (defaults to + `registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest`). + `OS_AUTOINST_CONTAINER_IMAGE` requires `OS_AUTOINST_GIT_REPO` to be set. + ### Automatic retries of jobs You might encounter flaky openQA tests that fail sporadically. The best way to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Archive.pm new/openQA-5.1781884690.e39cc969/lib/OpenQA/Archive.pm --- old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Archive.pm 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/lib/OpenQA/Archive.pm 2026-06-19 17:58:10.000000000 +0200 @@ -13,6 +13,8 @@ use Feature::Compat::Try; use Fcntl qw(:flock); +use constant VALID_CATEGORIES => qw(all resultfiles ulogs assets); + sub archive_cache_dir () { return $ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR} if $ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR}; my $config = OpenQA::App->singleton->config->{job_details_archive}; @@ -34,11 +36,15 @@ // 80; } -sub create_job_archive ($job) { +sub job_archive_filename ($job_id, $category = 'all') { + return $category eq 'all' ? "job_$job_id.zip" : "job_${job_id}_$category.zip"; +} + +sub create_job_archive ($job, $category = 'all') { my $job_id = $job->id; my $cache_dir = path(archive_cache_dir()); $cache_dir->make_path unless -d $cache_dir; - my $archive_name = "job_$job_id.zip"; + my $archive_name = job_archive_filename($job_id, $category); my $archive_path = $cache_dir->child($archive_name); my $lock_path = $cache_dir->child("$archive_name.lock"); open my $lock_fh, '>', $lock_path->to_string @@ -48,20 +54,39 @@ close $lock_fh; return $archive_path; } - log_info "Creating archive for job $job_id at $archive_path"; + log_info "Creating archive for job $job_id (category: $category) at $archive_path"; # Archive::Zip does not keep all member data in memory. When using addFile or # addTree, it only remembers the filenames. writeToFileNamed then streams # the content from the original files to the output zip. my $zip = Archive::Zip->new(); - if (my $res_dir = $job->result_dir) { - log_debug "Adding results from $res_dir to archive"; - $zip->addTree($res_dir, 'testresults/') if -d $res_dir; + my $res_dir = $job->result_dir; + + if ($res_dir && -d $res_dir) { + if ($category eq 'all') { + log_debug "Adding results from $res_dir to archive"; + $zip->addTree($res_dir, 'testresults/'); + } + elsif ($category eq 'resultfiles') { + for my $file (@{$job->test_resultfile_list}) { + my $full_path = path($res_dir)->child($file); + $zip->addFile($full_path->to_string, "testresults/$file") if -e $full_path; + } + } + elsif ($category eq 'ulogs') { + my $ulogs_dir = path($res_dir)->child('ulogs'); + for my $file (@{$job->test_uploadlog_list}) { + my $full_path = $ulogs_dir->child($file); + $zip->addFile($full_path->to_string, "testresults/ulogs/$file") if -e $full_path; + } + } } - my $assets = $job->jobs_assets; - while (my $ja = $assets->next) { - my $asset = $ja->asset; - my $disk_file = $asset->disk_file; - if ($disk_file && -e $disk_file) { + + if ($category eq 'all' || $category eq 'assets') { + my $assets = $job->jobs_assets; + while (my $ja = $assets->next) { + my $asset = $ja->asset; + my $disk_file = $asset->disk_file; + next unless $disk_file && -e $disk_file; log_debug 'Adding asset ' . $asset->name . ' to archive'; my $zip_path = $asset->type . '/' . $asset->name; if (-d $disk_file) { @@ -72,6 +97,7 @@ } } } + cleanup_cache(); my $temp_path = $cache_dir->child($archive_name . '.tmp.' . random_hex(8)); my $status = $zip->writeToFileNamed($temp_path->to_string); @@ -100,7 +126,8 @@ sub _perform_cache_cleanup ($cache_dir) { my ($available, $total) = check_df($cache_dir->to_string); - my $archives = $cache_dir->list->grep(sub { $_->basename =~ /^job_\d+\.zip$/ })->map( + my $category_regex = join '|', VALID_CATEGORIES; + my $archives = $cache_dir->list->grep(sub { $_->basename =~ /^job_\d+(?:_(?:$category_regex))?\.zip$/ })->map( sub { my $stat = $_->stat; {path => $_, mtime => $stat->mtime, size => $stat->size}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Task/Job/CreateZipArchive.pm new/openQA-5.1781884690.e39cc969/lib/OpenQA/Task/Job/CreateZipArchive.pm --- old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Task/Job/CreateZipArchive.pm 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/lib/OpenQA/Task/Job/CreateZipArchive.pm 2026-06-19 17:58:10.000000000 +0200 @@ -12,7 +12,7 @@ $app->minion->add_task(create_zip_archive => \&_create_zip_archive); } -sub _create_zip_archive ($minion_job, $job_id) { +sub _create_zip_archive ($minion_job, $job_id, $category = 'all') { my $app = $minion_job->app; # avoid running too many archive generation jobs in parallel @@ -26,7 +26,7 @@ return undef; } try { - OpenQA::Archive::create_job_archive($job); + OpenQA::Archive::create_job_archive($job, $category); $minion_job->finish; } catch ($e) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/lib/OpenQA/WebAPI/Controller/File.pm new/openQA-5.1781884690.e39cc969/lib/OpenQA/WebAPI/Controller/File.pm --- old/openQA-5.1781832185.5ddf5343/lib/OpenQA/WebAPI/Controller/File.pm 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/lib/OpenQA/WebAPI/Controller/File.pm 2026-06-19 17:58:10.000000000 +0200 @@ -160,21 +160,24 @@ sub archive ($self) { return $self->reply->not_found unless my $job = $self->schema->resultset('Jobs')->find($self->param('testid')); + my $category = $self->param('category') // 'all'; + my %valid = map { $_ => 1 } OpenQA::Archive::VALID_CATEGORIES; + return $self->reply->not_found unless $valid{$category}; my $job_id = $job->id; - my $archive_name = "job_$job_id.zip"; + my $archive_name = OpenQA::Archive::job_archive_filename($job_id, $category); my $cache_dir = path(OpenQA::Archive::archive_cache_dir()); my $archive_path = $cache_dir->child($archive_name); return $self->_redirect_to_archive($archive_path) if -e $archive_path; try { if (my $minion = $self->app->can('minion') ? $self->app->minion : undef) { - $self->app->log->info("Enqueuing create_zip_archive for job $job_id"); + $self->app->log->info("Enqueuing create_zip_archive for job $job_id (category: $category)"); $minion->enqueue( - create_zip_archive => [$job_id], - {notes => {job_id => $job_id}, priority => -10, expire => 3600}); + create_zip_archive => [$job_id, $category], + {notes => {job_id => $job_id, category => $category}, priority => -10, expire => 3600}); } else { # Fallback for environments without a fully-functional Minion (e.g. tests) - return $self->_redirect_to_archive(OpenQA::Archive::create_job_archive($job)); + return $self->_redirect_to_archive(OpenQA::Archive::create_job_archive($job, $category)); } } catch ($e) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Worker/Engines/isotovideo.pm new/openQA-5.1781884690.e39cc969/lib/OpenQA/Worker/Engines/isotovideo.pm --- old/openQA-5.1781832185.5ddf5343/lib/OpenQA/Worker/Engines/isotovideo.pm 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/lib/OpenQA/Worker/Engines/isotovideo.pm 2026-06-19 17:58:10.000000000 +0200 @@ -7,7 +7,7 @@ use OpenQA::JobSettings; use OpenQA::Log qw(log_error log_info log_debug log_warning get_channel_handle format_settings); use OpenQA::Utils - qw(asset_type_from_setting base_host locate_asset looks_like_url_with_scheme effective_distri testcasedir productdir needledir); + qw(asset_type_from_setting base_host locate_asset looks_like_url_with_scheme effective_distri testcasedir productdir needledir prjdir); use POSIX qw(:sys_wait_h strftime uname _exit); use Mojo::JSON 'encode_json'; # booleans use Cpanel::JSON::XS (); @@ -15,7 +15,7 @@ use File::Spec::Functions qw(abs2rel catdir file_name_is_absolute); use File::Basename qw(basename fileparse); use Errno; -use Cwd 'abs_path'; +use Cwd qw(abs_path getcwd); use OpenQA::CacheService::Client; use OpenQA::CacheService::Request; use Time::HiRes 'sleep'; @@ -477,8 +477,8 @@ # them in the spawned process as it does not belong to openQA code local $ENV{PERL5OPT} = ''; # Allow to override isotovideo executable with an arbitrary - # command line based on a config option - exec $job_settings->{ISOTOVIDEO} ? $job_settings->{ISOTOVIDEO} : ('perl', $isotovideo, '-d'); + # command line based on a config option or environment variables + exec _construct_isotovideo_cmd($job_settings, $isotovideo); die "exec failed: $!\n"; # uncoverable statement }); $child->on( @@ -521,4 +521,45 @@ return undef; } +sub _construct_isotovideo_cmd ($job_settings, $isotovideo) { + if (my $custom_cmd = $job_settings->{ISOTOVIDEO}) { + return $custom_cmd; + } + + if ($job_settings->{OS_AUTOINST_CONTAINER_IMAGE} && !$job_settings->{OS_AUTOINST_GIT_REPO}) { + die +"OS_AUTOINST_CONTAINER_IMAGE requires OS_AUTOINST_GIT_REPO. To run a custom container command, use ISOTOVIDEO instead.\n"; + } + + if (my $repo = $job_settings->{OS_AUTOINST_GIT_REPO}) { + my $branch = $job_settings->{OS_AUTOINST_GIT_BRANCH} // 'master'; + my $image = $job_settings->{OS_AUTOINST_CONTAINER_IMAGE} + // 'registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest'; + + my $podman_dir = prjdir() . '/cache/podman'; + my @cmd = ( + 'env', + "HOME=$podman_dir", + 'podman', + '--root', "$podman_dir/data/containers/storage", + '--runroot', "$podman_dir/run/containers", + '--storage-opt', 'ignore_chown_errors=true', + '--cgroup-manager=cgroupfs', + '--events-backend=file', + 'run', + '--init', + '--rm', + '--entrypoint', '', + '--device', '/dev/kvm', + '-v', getcwd() . ':/pool', + '-w', '/pool', + $image, + 'sh', '-c', "git clone --branch=$branch --depth=1 $repo && make -C os-autoinst && os-autoinst/isotovideo -d" + ); + return @cmd; + } + + return ('perl', $isotovideo, '-d'); +} + 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/t/24-worker-engine.t new/openQA-5.1781884690.e39cc969/t/24-worker-engine.t --- old/openQA-5.1781832185.5ddf5343/t/24-worker-engine.t 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/t/24-worker-engine.t 2026-06-19 17:58:10.000000000 +0200 @@ -25,7 +25,7 @@ use OpenQA::Test::FakeWorker; use Mojo::File qw(path tempdir); use Mojo::JSON 'decode_json'; -use OpenQA::Utils qw(testcasedir productdir needledir locate_asset base_host); +use OpenQA::Utils qw(testcasedir productdir needledir locate_asset base_host prjdir); use Cwd qw(getcwd); use Mojo::Util 'scope_guard'; use File::Copy::Recursive qw(dircopy); @@ -580,4 +580,75 @@ qr|Using cgroup /sys/fs/cgroup/.*/42|, 'use of cgroup logged'; }; +subtest '_construct_isotovideo_cmd' => sub { + my $isotovideo = '/usr/bin/isotovideo'; + + subtest 'default command' => sub { + my $settings = {}; + my @cmd = OpenQA::Worker::Engines::isotovideo::_construct_isotovideo_cmd($settings, $isotovideo); + is_deeply \@cmd, ['perl', '/usr/bin/isotovideo', '-d'], 'returns default execution array'; + }; + + subtest 'custom ISOTOVIDEO override' => sub { + my $settings = {ISOTOVIDEO => 'some custom command'}; + my @cmd = OpenQA::Worker::Engines::isotovideo::_construct_isotovideo_cmd($settings, $isotovideo); + is_deeply \@cmd, ['some custom command'], 'returns custom command string'; + }; + + subtest 'containerized with OS_AUTOINST_GIT_REPO' => sub { + my $settings = {OS_AUTOINST_GIT_REPO => 'https://github.com/foo/os-autoinst.git'}; + my @cmd = OpenQA::Worker::Engines::isotovideo::_construct_isotovideo_cmd($settings, $isotovideo); + is scalar(@cmd), 26, 'returns a list of execution arguments'; + my $podman_dir = prjdir() . '/cache/podman'; + is $cmd[0], 'env', 'uses env'; + is $cmd[1], "HOME=$podman_dir", 'sets correct home directory under openqa cache'; + is $cmd[2], 'podman', 'runs podman'; + is $cmd[3], '--root', 'uses --root'; + is $cmd[4], "$podman_dir/data/containers/storage", 'sets correct root directory'; + is $cmd[5], '--runroot', 'uses --runroot'; + is $cmd[6], "$podman_dir/run/containers", 'sets correct run root directory'; + is $cmd[7], '--storage-opt', 'sets storage-opt'; + is $cmd[8], 'ignore_chown_errors=true', 'ignores chown errors'; + is $cmd[9], '--cgroup-manager=cgroupfs', 'uses cgroupfs manager'; + is $cmd[10], '--events-backend=file', 'uses file events backend'; + is $cmd[11], 'run', 'runs the container'; + is $cmd[12], '--init', 'runs with --init for signal handling'; + is $cmd[13], '--rm', 'runs with --rm for auto-cleanup'; + is $cmd[14], '--entrypoint', 'specifies entrypoint'; + is $cmd[15], '', 'clears entrypoint'; + is $cmd[16], '--device', 'adds device'; + is $cmd[17], '/dev/kvm', 'passes kvm'; + is $cmd[18], '-v', 'mounts a volume'; + is $cmd[19], getcwd() . ':/pool', 'mounts getcwd() to pool'; + is $cmd[20], '-w', 'sets working dir flag'; + is $cmd[21], '/pool', 'sets working dir'; + is $cmd[22], 'registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest', 'uses default image'; + is $cmd[23], 'sh', 'executes sh'; + is $cmd[24], '-c', 'runs shell command'; + like $cmd[25], qr/git clone --branch=master --depth=1 https:\/\/github.com\/foo\/os-autoinst\.git/, + 'clones master branch by default'; + }; + + subtest 'containerized with OS_AUTOINST_GIT_BRANCH and custom image' => sub { + my $settings = { + OS_AUTOINST_GIT_REPO => 'https://github.com/foo/os-autoinst.git', + OS_AUTOINST_GIT_BRANCH => 'my_feature', + OS_AUTOINST_CONTAINER_IMAGE => 'my_custom_image:latest' + }; + my @cmd = OpenQA::Worker::Engines::isotovideo::_construct_isotovideo_cmd($settings, $isotovideo); + is scalar(@cmd), 26, 'returns a list of execution arguments'; + is $cmd[22], 'my_custom_image:latest', 'uses custom image'; + like $cmd[25], qr/git clone --branch=my_feature --depth=1 https:\/\/github.com\/foo\/os-autoinst\.git/, + 'clones custom branch'; + }; + + subtest 'containerized without OS_AUTOINST_GIT_REPO' => sub { + my $settings = {OS_AUTOINST_CONTAINER_IMAGE => 'my_custom_image:latest'}; + throws_ok { + OpenQA::Worker::Engines::isotovideo::_construct_isotovideo_cmd($settings, $isotovideo); + } + qr/OS_AUTOINST_CONTAINER_IMAGE requires OS_AUTOINST_GIT_REPO/, 'dies with helpful error message'; + }; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/t/60-archive-download.t new/openQA-5.1781884690.e39cc969/t/60-archive-download.t --- old/openQA-5.1781832185.5ddf5343/t/60-archive-download.t 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/t/60-archive-download.t 2026-06-19 17:58:10.000000000 +0200 @@ -34,7 +34,7 @@ my $cache_dir = path($ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR}); $cache_dir->make_path; -subtest 'Archive download' => sub { +subtest 'ZIP archive download and categorization' => sub { my $job = $schema->resultset('Jobs')->create( { DISTRI => 'archtest', @@ -45,47 +45,56 @@ state => 'done', result => 'passed', }); - $job->create_result_dir; - $job->update({result_dir => $job->result_dir}); - my $res_dir_str = $job->result_dir; - die 'result_dir is not set' unless $res_dir_str; - my $res_dir = path($res_dir_str); - $res_dir->make_path; - $res_dir->child('details-test.json')->spew('{"test": "data"}'); + my $res_dir = path($job->create_result_dir); + $res_dir->child('vars.json')->spew('{"test": "data"}'); $res_dir->child('ulogs')->make_path->child('test.log')->spew('log data'); - my $asset = $schema->resultset('Assets')->create( - { - type => 'iso', - name => 'test.iso', - }); - $schema->resultset('JobsAssets')->create( - { - job_id => $job->id, - asset_id => $asset->id, - }); - my $asset_path = path(assetdir(), 'iso', 'test.iso'); - $asset_path->dirname->make_path; - $asset_path->spew('iso data'); + my $asset = $schema->resultset('Assets')->create({type => 'iso', name => 'test.iso'}); + $schema->resultset('JobsAssets')->create({job_id => $job->id, asset_id => $asset->id}); + path(assetdir(), 'iso', 'test.iso')->dirname->make_path->child('test.iso')->spew('iso data'); + $t->get_ok('/tests/' . $job->id . '/archive')->status_is(302) ->header_is('Location' => '/login?return_page=%2Ftests%2F' . $job->id . '%2Farchive'); $t->get_ok('/'); - ok $case->login($t, 'admin'), 'Logged in as admin'; - $t->get_ok('/tests/' . $job->id . '/archive')->status_is(302)->header_like('Location' => qr|/archives/job_|); - my $archive_url = $t->tx->res->headers->location; - $t->get_ok('/tests/' . $job->id . '/downloads_ajax')->status_is(200) - ->element_exists('a[title="Download all test results and assets as a ZIP archive"]'); - $t->get_ok($archive_url)->status_is(200)->content_type_is('application/zip') - ->header_is('Content-Disposition' => 'attachment; filename=job_' . $job->id . '.zip;'); - my $zip_content = $t->tx->res->body; - my $zip_file = $tmp->child('downloaded.zip'); - $zip_file->spew($zip_content); - my $zip = Archive::Zip->new(); - is $zip->read($zip_file->to_string), AZ_OK, 'Zip is valid'; - ok $zip->memberNamed('testresults/details-test.json'), 'Contains test results'; - ok $zip->memberNamed('testresults/ulogs/test.log'), 'Contains ulogs'; - ok $zip->memberNamed('iso/test.iso'), 'Contains assets'; - is $zip->contents('testresults/details-test.json'), '{"test": "data"}', 'Result content is correct'; - is $zip->contents('iso/test.iso'), 'iso data', 'Asset content is correct'; + ok $case->login($t, 'admin'), 'Admin login succeeds'; + + my @test_cases = ( + { + category => 'all', + expected => [qw(testresults/vars.json testresults/ulogs/test.log iso/test.iso)], + }, + { + category => 'resultfiles', + expected => ['testresults/vars.json'], + unexpected => [qw(testresults/ulogs/test.log iso/test.iso)], + }, + { + category => 'ulogs', + expected => ['testresults/ulogs/test.log'], + unexpected => [qw(testresults/vars.json iso/test.iso)], + }, + { + category => 'assets', + expected => ['iso/test.iso'], + unexpected => [qw(testresults/vars.json testresults/ulogs/test.log)], + }, + ); + + for my $tc (@test_cases) { + my $cat = $tc->{category}; + my $url = '/tests/' . $job->id . '/archive' . ($cat eq 'all' ? '' : "?category=$cat"); + my $suffix = $cat eq 'all' ? '' : "_$cat"; + + $t->get_ok($url)->status_is(302)->header_like('Location' => qr|/archives/job_@{[ $job->id ]}${suffix}\.zip|); + $t->get_ok($t->tx->res->headers->location)->status_is(200)->content_type_is('application/zip'); + + my $zip = Archive::Zip->new(); + my $content = $t->tx->res->body; + open my $fh, '<', \$content; + is $zip->read($fh), AZ_OK, "ZIP archive for category '$cat' is structurally valid"; + + ok $zip->memberNamed($_), "Category '$cat' archive correctly includes: $_" for @{$tc->{expected}}; + ok !$zip->memberNamed($_), "Category '$cat' archive correctly excludes: $_" for @{$tc->{unexpected} // []}; + } }; subtest 'Archive with large files' => sub { @@ -101,32 +110,29 @@ }); my $res_dir = path($job->create_result_dir); my $large_file = $res_dir->child('large.bin'); - # 50MB is enough to test without taking too long my $fh = $large_file->open('>'); - for (1 .. 50 * 1024) { - print $fh 'A' x 1024; - } + print $fh 'A' x 1024 for 1 .. 50 * 1024; $fh->close; my $archive_path = OpenQA::Archive::create_job_archive($job); - ok -e $archive_path, 'Archive with large file created'; + ok -e $archive_path, 'Archive with 50MB file created successfully'; my $zip = Archive::Zip->new(); - is $zip->read($archive_path->to_string), AZ_OK, 'Large zip is valid'; + is $zip->read($archive_path->to_string), AZ_OK, 'Large ZIP archive is readable'; my $member = $zip->memberNamed('testresults/large.bin'); - ok $member, 'Contains large file'; - is $member->uncompressedSize, 50 * 1024 * 1024, 'Size is correct'; + ok $member, 'ZIP archive contains the large file'; + is $member->uncompressedSize, 50 * 1024 * 1024, 'Archived file size matches expected 50MB'; }; subtest 'Archive caching' => sub { my $job_id = $schema->resultset('Jobs')->first->id; my $cache_file = path($ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR})->child("job_$job_id.zip"); - ok -e $cache_file, 'Archive is cached'; + ok -e $cache_file, 'Initial archive exists in cache'; my $mtime = $cache_file->stat->mtime; utime $mtime - 10, $mtime - 10, $cache_file->to_string; $mtime = $cache_file->stat->mtime; $t->get_ok('/tests/' . $job_id . '/archive')->status_is(302); $t->get_ok($t->tx->res->headers->location)->status_is(200); - is $cache_file->stat->mtime, $mtime, 'Cached file was reused'; + is $cache_file->stat->mtime, $mtime, 'Archive request reuses existing cached file without regeneration'; }; subtest 'Hide "Download All" button when no content' => sub { @@ -155,17 +161,20 @@ my $orig_env = $ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR}; delete $ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR}; is OpenQA::Archive::archive_cache_dir(), $app->config->{job_details_archive}->{job_details_archive_cache_dir}, - 'Cache dir from config'; - is OpenQA::Archive::get_cache_limit(), 1024 * 1024 * 1024, 'Limit in bytes'; - is OpenQA::Archive::get_min_free_percentage(), 20, 'Min free percentage'; - is OpenQA::Archive::get_watermark_percentage(), 50, 'Watermark percentage'; - ok OpenQA::Archive::is_cache_limit_exceeded(2 * 1024 * 1024 * 1024, 100, 1000), 'Limit exceeded by size'; - ok OpenQA::Archive::is_cache_limit_exceeded(100, 10, 100), 'Limit exceeded by free percentage'; - ok !OpenQA::Archive::is_cache_limit_exceeded(100, 30, 100), 'Limit not exceeded'; + 'Archive cache directory follows application config'; + is OpenQA::Archive::get_cache_limit(), 1024 * 1024 * 1024, 'Cache size limit is correctly converted to bytes'; + is OpenQA::Archive::get_min_free_percentage(), 20, 'Minimum free space percentage follows config'; + is OpenQA::Archive::get_watermark_percentage(), 50, 'Cleanup watermark percentage follows config'; + ok OpenQA::Archive::is_cache_limit_exceeded(2 * 1024 * 1024 * 1024, 100, 1000), + 'Cache limit is exceeded when current size is over threshold'; + ok OpenQA::Archive::is_cache_limit_exceeded(100, 10, 100), + 'Cache limit is exceeded when free disk space is below minimum'; + ok !OpenQA::Archive::is_cache_limit_exceeded(100, 30, 100), + 'Cache limit is not exceeded when within safety thresholds'; delete $app->config->{job_details_archive}->{job_details_archive_cache_dir}; - like OpenQA::Archive::archive_cache_dir(), qr|/webui/cache/archives$|, 'Default cache dir'; + like OpenQA::Archive::archive_cache_dir(), qr|/webui/cache/archives$|, 'Fallback to default cache directory works'; delete $app->config->{job_details_archive}; - is OpenQA::Archive::get_cache_limit(), 5 * 1024 * 1024 * 1024, 'Default limit'; + is OpenQA::Archive::get_cache_limit(), 5 * 1024 * 1024 * 1024, 'Default cache limit is 5GB'; $ENV{OPENQA_JOB_DETAILS_ARCHIVE_CACHE_DIR} = $orig_env; $app->config->{job_details_archive} = $orig_config; }; @@ -188,7 +197,7 @@ my @initial = $cache_dir->list->grep(sub { $_->basename =~ /^job_\d+\.zip$/ })->each; OpenQA::Archive::cleanup_cache(); my @remaining = $cache_dir->list->grep(sub { $_->basename =~ /^job_\d+\.zip$/ })->each; - ok scalar(@remaining) < scalar(@initial), 'Some archives were removed during cleanup'; + ok scalar(@remaining) < scalar(@initial), 'Archive cache is rotated after exceeding limit'; $app->config->{job_details_archive} = $orig_config; }; @@ -210,7 +219,8 @@ $mock_assets_rs->mock(next => sub { shift @assets }); $mock_job->set_always(jobs_assets => $mock_assets_rs); my $archive_path = OpenQA::Archive::create_job_archive($mock_job); - ok -e $archive_path, 'Archive created with directory asset'; + ok -e $archive_path, 'Archive is created correctly when an asset is a directory'; + my $mock_asset_missing = Test::MockObject->new; $mock_asset_missing->set_always(disk_file => $tmp->child('nonexistent_asset')->to_string); $mock_asset_missing->set_always(name => 'missing_asset'); @@ -221,9 +231,10 @@ $mock_assets_rs->mock(next => sub { shift @assets_missing }); $mock_job->set_always(id => 7890); my $archive_path_missing = OpenQA::Archive::create_job_archive($mock_job); - ok -e $archive_path_missing, 'Archive created even with missing asset file'; + ok -e $archive_path_missing, 'Archive creation succeeds even if an asset file is missing from disk'; my $archive_path2 = OpenQA::Archive::create_job_archive($mock_job); - is $archive_path2->to_string, $archive_path_missing->to_string, 'Returned existing archive'; + is $archive_path2->to_string, $archive_path_missing->to_string, + 'Archive request returns existing file if already present in cache'; }; subtest 'Create archive failure' => sub { @@ -238,33 +249,33 @@ path($file)->spew('dummy'); return AZ_IO_ERROR; }); - throws_ok { OpenQA::Archive::create_job_archive($mock_job) } qr/Failed to create archive/, 'Throws on zip failure'; + throws_ok { OpenQA::Archive::create_job_archive($mock_job) } qr/Failed to create archive/, + 'Archive creation throws error on ZIP library failure'; }; -subtest 'CreateZipArchive task' => sub { +subtest 'CreateZipArchive Minion task' => sub { require OpenQA::Task::Job::CreateZipArchive; - my $mock_minion_job = Test::MockObject->new; - $mock_minion_job->set_always(app => $app); + my $mock_minion_job = Test::MockObject->new->set_always(app => $app)->set_true('finish')->set_true('fail'); my $mock_schema_obj = Test::MockObject->new; my $mock_rs = Test::MockObject->new; $mock_schema_obj->set_always(resultset => $mock_rs); $mock_rs->set_always(find => undef); + my $mock_app_module = Test::MockModule->new('OpenQA::WebAPI'); $mock_app_module->mock(schema => sub { $mock_schema_obj }); - $mock_minion_job->set_true('finish'); OpenQA::Task::Job::CreateZipArchive::_create_zip_archive($mock_minion_job, 4567); - $mock_minion_job->called_ok('finish', 'Finished with job not found message'); - my $mock_job = Test::MockObject->new; - $mock_job->set_always(id => 4567); + $mock_minion_job->called_ok('finish', 'Task finishes gracefully if job is not found in database'); + + my $mock_job = Test::MockObject->new->set_always(id => 4567); $mock_rs->set_always(find => $mock_job); my $mock_archive_module = Test::MockModule->new('OpenQA::Archive'); $mock_archive_module->mock(create_job_archive => sub { path('/tmp/dummy.zip') }); OpenQA::Task::Job::CreateZipArchive::_create_zip_archive($mock_minion_job, 4567); - $mock_minion_job->called_ok('finish', 'Finished successfully'); + $mock_minion_job->called_ok('finish', 'Task completes successfully after archive generation'); + $mock_archive_module->mock(create_job_archive => sub { die 'creation error' }); - $mock_minion_job->set_true('fail'); OpenQA::Task::Job::CreateZipArchive::_create_zip_archive($mock_minion_job, 4567); - $mock_minion_job->called_ok('fail', 'Failed correctly on error'); + $mock_minion_job->called_ok('fail', 'Task fails correctly when archive generation logic throws an exception'); # Verify create_zip_archive_limit config is respected my $mock_minion = Test::MockModule->new('Minion'); @@ -296,7 +307,7 @@ $mock_minion->unmock_all; }; -subtest 'Controller extra tests' => sub { +subtest 'Controller archive endpoint' => sub { my $job = $schema->resultset('Jobs')->create( { DISTRI => 'archtest', @@ -309,7 +320,8 @@ }); $t->get_ok('/archives/..%2fetc%2fpasswd')->status_is(404); $t->get_ok('/archives/nonexistent.zip')->status_is(404); - my $mock_minion = Test::MockObject->new; + + my $mock_minion = Test::MockObject->new->set_true('enqueue'); my $mock_app_module = Test::MockModule->new('OpenQA::WebAPI'); $mock_app_module->mock(minion => sub { $mock_minion }); my $orig_can = $app->can('can'); @@ -321,15 +333,71 @@ $mock_minion->set_true('enqueue'); $case->login($t, 'admin'); - $t->get_ok('/tests/' . $job->id . '/archive')->status_is(200)->content_like(qr/Preparing Archive for Job/); - $mock_minion->called_ok('enqueue', 'Minion job enqueued'); + $mock_minion->called_ok('enqueue', 'Archive generation is enqueued in Minion'); $mock_minion->clear; $t->get_ok('/tests/' . $job->id . '/archive')->status_is(200); - $mock_minion->called_ok('enqueue', 'Minion job enqueued again'); + $mock_minion->called_ok('enqueue', 'Repeated archive request re-enqueues generation task'); $mock_minion->mock(enqueue => sub { die 'Enqueue failed' }); $t->get_ok('/tests/' . $job->id . '/archive')->status_is(500)->content_is('Internal Server Error'); }; +subtest 'Category specific archives' => sub { + my $job = $schema->resultset('Jobs')->create( + { + DISTRI => 'archtest', + VERSION => '1.0', + FLAVOR => 'test', + ARCH => 'x86_64', + TEST => 'category_test', + state => 'done', + result => 'passed', + }); + my $res_dir = path($job->create_result_dir); + $res_dir->child('vars.json')->spew('{}'); # COMMON_RESULT_FILES includes vars.json + $res_dir->child('ulogs')->make_path->child('ulog.txt')->spew('ulog data'); + my $asset = $schema->resultset('Assets')->create({type => 'iso', name => 'asset.iso'}); + $schema->resultset('JobsAssets')->create({job_id => $job->id, asset_id => $asset->id}); + path(assetdir(), 'iso', 'asset.iso')->dirname->make_path->child('asset.iso')->spew('asset data'); + + $case->login($t, 'admin'); + + # Test resultfiles category + $t->get_ok('/tests/' . $job->id . '/archive?category=resultfiles')->status_is(302) + ->header_like('Location' => qr|/archives/job_\d+_resultfiles.zip|); + $t->get_ok($t->tx->res->headers->location)->status_is(200); + my $zip_file = $tmp->child('resultfiles.zip'); + $zip_file->spew($t->tx->res->body); + my $zip = Archive::Zip->new(); + is $zip->read($zip_file->to_string), AZ_OK, 'Resultfiles zip is valid'; + ok $zip->memberNamed('testresults/vars.json'), 'Contains result file'; + ok !$zip->memberNamed('testresults/ulogs/ulog.txt'), 'Does not contain ulog'; + ok !$zip->memberNamed('iso/asset.iso'), 'Does not contain asset'; + + # Test ulogs category + $t->get_ok('/tests/' . $job->id . '/archive?category=ulogs')->status_is(302) + ->header_like('Location' => qr|/archives/job_\d+_ulogs.zip|); + $t->get_ok($t->tx->res->headers->location)->status_is(200); + $zip_file = $tmp->child('ulogs.zip'); + $zip_file->spew($t->tx->res->body); + $zip = Archive::Zip->new(); + is $zip->read($zip_file->to_string), AZ_OK, 'Ulogs zip is valid'; + ok !$zip->memberNamed('testresults/vars.json'), 'Does not contain result file'; + ok $zip->memberNamed('testresults/ulogs/ulog.txt'), 'Contains ulog'; + ok !$zip->memberNamed('iso/asset.iso'), 'Does not contain asset'; + + # Test assets category + $t->get_ok('/tests/' . $job->id . '/archive?category=assets')->status_is(302) + ->header_like('Location' => qr|/archives/job_\d+_assets.zip|); + $t->get_ok($t->tx->res->headers->location)->status_is(200); + $zip_file = $tmp->child('assets.zip'); + $zip_file->spew($t->tx->res->body); + $zip = Archive::Zip->new(); + is $zip->read($zip_file->to_string), AZ_OK, 'Assets zip is valid'; + ok !$zip->memberNamed('testresults/vars.json'), 'Does not contain result file'; + ok !$zip->memberNamed('testresults/ulogs/ulog.txt'), 'Does not contain ulog'; + ok $zip->memberNamed('iso/asset.iso'), 'Contains asset'; +}; + done_testing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/t/data/workers.ini new/openQA-5.1781884690.e39cc969/t/data/workers.ini --- old/openQA-5.1781832185.5ddf5343/t/data/workers.ini 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/t/data/workers.ini 2026-06-19 17:58:10.000000000 +0200 @@ -7,6 +7,6 @@ # the value set to 0 prevents jobs from being blocked in ci CRITICAL_LOAD_AVG_THRESHOLD = 0 -[1-99] +[1-1024] WORKER_CLASS = qemu_i386,qemu_x86_64 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/t/full-stack.t new/openQA-5.1781884690.e39cc969/t/full-stack.t --- old/openQA-5.1781832185.5ddf5343/t/full-stack.t 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/t/full-stack.t 2026-06-19 17:58:10.000000000 +0200 @@ -304,6 +304,7 @@ my $cache_location = path($ENV{OPENQA_BASEDIR}, 'cache')->make_path; ok -e $cache_location, 'Setting up Cache directory'; +my $max_instances = MAX_WORKER_INSTANCES; path($ENV{OPENQA_CONFIG})->child('workers.ini')->spew(<<"EOC"); [global] CACHEDIRECTORY = $cache_location @@ -313,7 +314,7 @@ # Ensure fullstack tests run even under high load. CRITICAL_LOAD_AVG_THRESHOLD = 0 -[1-99] +[1-$max_instances] WORKER_CLASS = qemu_i386,qemu_x86_64 [http://localhost:$mojoport] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/t/lib/OpenQA/Test/Utils.pm new/openQA-5.1781884690.e39cc969/t/lib/OpenQA/Test/Utils.pm --- old/openQA-5.1781832185.5ddf5343/t/lib/OpenQA/Test/Utils.pm 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/t/lib/OpenQA/Test/Utils.pm 2026-06-19 17:58:10.000000000 +0200 @@ -38,7 +38,11 @@ use Feature::Compat::Try; use Time::HiRes 'sleep'; -use constant MAX_WORKER_INSTANCES => 64; +use constant MAX_WORKER_INSTANCES => 1024; +# That is the highest power of two that still leaves os-autoinst enough headroom +# to compute port numbers. This means there is only a < 0.1 % chance of running +# into a conflict when running two fullstack tests in parallel and we still avoid +# exceeding the maximum port number. BEGIN { if (!$ENV{MOJO_HOME}) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/templates/webapi/test/downloads.html.ep new/openQA-5.1781884690.e39cc969/templates/webapi/test/downloads.html.ep --- old/openQA-5.1781832185.5ddf5343/templates/webapi/test/downloads.html.ep 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/templates/webapi/test/downloads.html.ep 2026-06-19 17:58:10.000000000 +0200 @@ -8,11 +8,23 @@ % } % if(@$resultfiles) { - <h5>Result files</h5> + <h5>Result files + % if (current_user) { + %= link_to url_for('test_archive', testid => $job->id)->query(category => 'resultfiles') => (class => 'btn btn-outline-primary btn-sm ms-2', title => 'Download result files as a ZIP archive', rel => 'nofollow') => begin + <i class="fa fa-download"></i> Download (ZIP) + % end + % } + </h5> %= include 'test/result_file_list', resultfiles => $resultfiles, is_userfile => 0 % } % if (@$ulogs) { - <h6>Uploaded logs</h6> + <h6>Uploaded logs + % if (current_user) { + %= link_to url_for('test_archive', testid => $job->id)->query(category => 'ulogs') => (class => 'btn btn-outline-primary btn-sm ms-2', title => 'Download uploaded logs as a ZIP archive', rel => 'nofollow') => begin + <i class="fa fa-download"></i> Download (ZIP) + % end + % } + </h6> %= include 'test/result_file_list', resultfiles => $ulogs, is_userfile => 1 % } @@ -38,6 +50,12 @@ % } % } % if (length(content('asset_box'))) { - <h5>Assets</h5> + <h5>Assets + % if (current_user) { + %= link_to url_for('test_archive', testid => $job->id)->query(category => 'assets') => (class => 'btn btn-outline-primary btn-sm ms-2', title => 'Download assets as a ZIP archive', rel => 'nofollow') => begin + <i class="fa fa-download"></i> Download (ZIP) + % end + % } + </h5> <ul id="asset-list"><%= content 'asset_box' %></ul> % } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1781832185.5ddf5343/tools/ci/autoinst.sha new/openQA-5.1781884690.e39cc969/tools/ci/autoinst.sha --- old/openQA-5.1781832185.5ddf5343/tools/ci/autoinst.sha 2026-06-19 03:23:05.000000000 +0200 +++ new/openQA-5.1781884690.e39cc969/tools/ci/autoinst.sha 2026-06-19 17:58:10.000000000 +0200 @@ -1 +1 @@ -22ced0db810357e853386e7dc6aed23ce4ed5b68 \ No newline at end of file +0fb10771665c8db2173a5097c1dbdc837464c9d7 \ No newline at end of file ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.vHObut/_old 2026-06-22 17:28:31.119821388 +0200 +++ /var/tmp/diff_new_pack.vHObut/_new 2026-06-22 17:28:31.135821946 +0200 @@ -1,5 +1,5 @@ name: openQA -version: 5.1781832185.5ddf5343 -mtime: 1781832185 -commit: 5ddf53437214d6869d82cdbbf990149b6b4da672 +version: 5.1781884690.e39cc969 +mtime: 1781884690 +commit: e39cc96968d08eb9d9691acd620da8ac8a483ccb
