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-02-27 17:10:38 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.29461 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Fri Feb 27 17:10:38 2026 rev:813 rq:1335359 version:5.1772092969.74a39650 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-02-26 18:52:05.958142125 +0100 +++ /work/SRC/openSUSE:Factory/.openQA.new.29461/openQA.changes 2026-02-27 17:11:32.336169223 +0100 @@ -1,0 +2,21 @@ +Thu Feb 26 18:08:33 UTC 2026 - [email protected] + +- Update to version 5.1772092969.74a39650: + * test: Consider all of `lib/OpenQA/Task/` covered + * test: Cover handling developer session when saving needles + * test: Cover further error cases when saving needles + * fix: Fix error handling when saving needle JSON + * test: Workaround limitation of coverage tracking + * feat: Improve needle JSON validation + * test: Cover all cases of needle JSON validation + * fix: Avoid Perl warning when validating needle JSON + * refactor: Simplify code in `_delete_needles` + * test: Cover handling error when asset directory is not writable + * test: Cover skipping screenshot cleanup if still enqueued + * feat(openqa-upstreams.inc): set `max_conns` to max connection handled + * refactor: optimize and harden aggregate overview badges implementation + * refactor: improve aggregate overview badges implementation + * test: consolidate SVG badge unit tests + * feat: implement test result badges for aggregate overview queries + +------------------------------------------------------------------- Old: ---- openQA-5.1772031289.93bc2a13.obscpio New: ---- openQA-5.1772092969.74a39650.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:35.568303167 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:35.576303498 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1772031289.93bc2a13 +Version: 5.1772092969.74a39650 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:35.872315765 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:35.884316262 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1772031289.93bc2a13 +Version: 5.1772092969.74a39650 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:36.044322893 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:36.056323391 +0100 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1772031289.93bc2a13 +Version: 5.1772092969.74a39650 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:36.332334829 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:36.388337150 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1772031289.93bc2a13 +Version: 5.1772092969.74a39650 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:36.636347428 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:36.636347428 +0100 @@ -99,7 +99,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1772031289.93bc2a13 +Version: 5.1772092969.74a39650 Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later ++++++ openQA-5.1772031289.93bc2a13.obscpio -> openQA-5.1772092969.74a39650.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/codecov.yml new/openQA-5.1772092969.74a39650/codecov.yml --- old/openQA-5.1772031289.93bc2a13/codecov.yml 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/codecov.yml 2026-02-26 09:02:49.000000000 +0100 @@ -25,6 +25,7 @@ - lib/OpenQA/Utils.pm - lib/OpenQA/WebAPI/Controller/ - lib/OpenQA/Shared/ + - lib/OpenQA/Task/ tests: target: 100.0 threshold: 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/etc/nginx/vhosts.d/openqa-upstreams.inc new/openQA-5.1772092969.74a39650/etc/nginx/vhosts.d/openqa-upstreams.inc --- old/openQA-5.1772031289.93bc2a13/etc/nginx/vhosts.d/openqa-upstreams.inc 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/etc/nginx/vhosts.d/openqa-upstreams.inc 2026-02-26 09:02:49.000000000 +0100 @@ -1,6 +1,7 @@ upstream webui { zone upstream_webui 128k; - server [::1]:9526; + # max_conns should match -w (worker/prefork count) in scripts/openqa-webui-daemon + server [::1]:9526 max_conns=30; } upstream websocket { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Jobs/Constants.pm new/openQA-5.1772092969.74a39650/lib/OpenQA/Jobs/Constants.pm --- old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Jobs/Constants.pm 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/lib/OpenQA/Jobs/Constants.pm 2026-02-26 09:02:49.000000000 +0100 @@ -113,6 +113,8 @@ }, }; +use constant OVERVIEW_STATUS_PRIORITY => qw(failed not_complete softfailed running scheduled passed aborted); + # results for particular job modules use constant MODULE_RESULTS => (CANCELLED, FAILED, NONE, PASSED, RUNNING, SKIPPED, SOFTFAILED); @@ -181,6 +183,7 @@ META_RESULTS META_STATES META_MAPPING + OVERVIEW_STATUS_PRIORITY COMMON_RESULT_FILES TIMEOUT_EXCEEDED DEFAULT_JOB_PRIORITY diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Task/Needle/Delete.pm new/openQA-5.1772092969.74a39650/lib/OpenQA/Task/Needle/Delete.pm --- old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Task/Needle/Delete.pm 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/lib/OpenQA/Task/Needle/Delete.pm 2026-02-26 09:02:49.000000000 +0100 @@ -69,15 +69,13 @@ DIR: for my $dir (sort keys %$to_remove) { my $needles = $to_remove->{$dir}; # prevent multiple git tasks to run in parallel + # note: The unless-block is covered by subtest 'minion guard' in t/14-grutasks-git.t which would fail when + # placing a "die" there. The coverage tracking does not seem to work for this block, though. my $guard; unless ($guard = $app->minion->guard("git_clone_${dir}_task", 2 * ONE_HOUR)) { - push @$errors, - { - id => $_, - message => "Another git task for $dir is ongoing. Try again later.", - } - for map { $_->id } @$needles; - next; + my $msg = "Another git task for $dir is ongoing. Try again later."; # uncoverable statement + push @$errors, {id => $_->id, message => $msg} for @$needles; # uncoverable statement + next; # uncoverable statement } for my $needle (@$needles) { my $needle_id = $needle->id; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Task/Needle/Save.pm new/openQA-5.1772092969.74a39650/lib/OpenQA/Task/Needle/Save.pm --- old/openQA-5.1772031289.93bc2a13/lib/OpenQA/Task/Needle/Save.pm 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/lib/OpenQA/Task/Needle/Save.pm 2026-02-26 09:02:49.000000000 +0100 @@ -11,6 +11,7 @@ use OpenQA::Jobs::Constants; use OpenQA::Task::SignalGuard; use OpenQA::Utils; +use Mojo::File 'path'; use Mojo::JSON 'decode_json'; use Feature::Compat::Try; use Time::Seconds 'ONE_HOUR'; @@ -29,17 +30,15 @@ $e =~ s@at /usr/.*$@@; # do not print perl module reference die "syntax error: $e"; } - if (!exists $djson->{area} || !exists $djson->{area}[0]) { - die 'no area defined'; - } - if (!exists $djson->{tags} || !exists $djson->{tags}[0]) { - die 'no tag defined'; - } - my @not_ocr_area = grep { $_->{type} ne 'ocr' } @{$djson->{area}}; + die 'needle JSON is no object' unless ref $djson eq 'HASH'; + die 'no area defined' unless ref $djson->{area} eq 'ARRAY' && exists $djson->{area}[0]; + die 'no tag defined' unless ref $djson->{tags} eq 'ARRAY' && exists $djson->{tags}[0]; + + my @not_ocr_area = grep { ($_->{type} // '') ne 'ocr' } @{$djson->{area}}; die 'Cannot create a needle with only OCR areas' if scalar(@not_ocr_area) == 0; - my $areas = $djson->{area}; - foreach my $area (@$areas) { + foreach my $area (@{$djson->{area}}) { + die 'area is no object' unless ref $area eq 'HASH'; die 'area without xpos' unless exists $area->{xpos}; die 'area without ypos' unless exists $area->{ypos}; die 'area without type' unless exists $area->{type}; @@ -123,22 +122,21 @@ } # copy image - my $success = 1; + my $error; if (!($imagepath eq "$baseneedle.png") && !copy($imagepath, "$baseneedle.png")) { $app->log->error("Copy $imagepath -> $baseneedle.png failed: $!"); - $success = 0; + $error = $!; } - if ($success) { - open(my $J, '>', "$baseneedle.json") or $success = 0; - if ($success) { - print($J $needle_json); - close($J); + if (!$error) { + try { + path("$baseneedle.json")->spew($needle_json); } - else { - $app->log->error("Writing needle $baseneedle.json failed: $!"); + catch ($e) { + $app->log->error("Writing needle $baseneedle.json failed: $e"); + $error = $e; } } - return $minion_job->fail({error => "<strong>Error creating/updating needle:</strong><br>$!."}) unless $success; + return $minion_job->fail({error => "<strong>Error creating/updating needle:</strong><br>$error."}) if $error; try { _commit_needle_in_git_repo($app, $git, $needlename, $openqa_job, $commit_message); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/lib/OpenQA/WebAPI/Controller/Test.pm new/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/Test.pm --- old/openQA-5.1772031289.93bc2a13/lib/OpenQA/WebAPI/Controller/Test.pm 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI/Controller/Test.pm 2026-02-26 09:02:49.000000000 +0100 @@ -607,6 +607,10 @@ $status = 200; } + $self->_render_badge($badge_text, $badge_color, $status); +} + +sub _render_badge ($self, $badge_text, $badge_color, $status = 200) { # determine the approximate required width of the badge my $charlen = 11; my $badge_prefix_width = 85; @@ -621,6 +625,37 @@ $self->render('test/badge', format => 'svg', status => $status); } +sub _gather_overview_results ($self, %args) { + my ($search_args, $groups) = $self->compose_job_overview_search_args; + my $jobs_rs = $self->schema->resultset('Jobs'); + my $latest_job_ids = $jobs_rs->complex_query_latest_ids(%$search_args); + my $limit = $search_args->{limit}; + my $jobs = $jobs_rs->latest_jobs_from_ids($latest_job_ids, $limit); + my ($archs, $results, $aggregated, $job_ids) = $self->_prepare_job_results($jobs, $latest_job_ids, %args); + return { + search_args => $search_args, + groups => $groups, + latest_job_ids => $latest_job_ids, + limit => $limit, + archs => $archs, + results => $results, + aggregated => $aggregated, + job_ids => $job_ids, + }; +} + +sub overview_badge ($self) { + my $data = $self->_gather_overview_results(only_aggregated => 1); + my $aggregated = $data->{aggregated}; + + my $status = (grep { $aggregated->{$_} } OVERVIEW_STATUS_PRIORITY)[0] // 'none'; + + state $mapping = {not_complete => 'incomplete', aborted => 'cancelled', none => 'cancelled'}; + my $badge_color_key = $mapping->{$status} // $status; + + $self->_render_badge($status =~ tr/_/ /r, $BADGE_RESULT_COLORS{$badge_color_key}); +} + sub job_next_previous_ajax ($self) { return $self->reply->not_found unless my $main_job = $self->_get_current_job; my $main_jobid = $main_job->id; @@ -712,7 +747,8 @@ } # Take an job objects arrayref and prepare data structures for 'overview' -sub _prepare_job_results ($self, $jobs, $job_ids) { +sub _prepare_job_results ($self, $jobs, $job_ids, %args) { + my $only_aggregated = $args{only_aggregated}; my %archs; my %results; my @job_ids; @@ -727,31 +763,42 @@ unknown => 0 }; my @jobs = $jobs->all; - my $preferred_machines = _calculate_preferred_machines(\@jobs); # prefetch the number of available labels for those jobs my $schema = $self->schema; my $comment_data = $schema->resultset('Comments')->comment_data_for_jobs(\@jobs, {bugdetails => 1}); - # prefetch test suite names from job settings - my $job_settings - = $schema->resultset('JobSettings') - ->search({job_id => {-in => $job_ids}, key => {-in => [qw(JOB_DESCRIPTION TEST_SUITE_NAME)]}}); my %settings_by_job_id; - for my $js ($job_settings->all) { - $settings_by_job_id{$js->job_id}->{$js->key} = $js->value; - } + my %descriptions; + my $failed_modules_by_job = {}; + my $children_by_job = {}; + my $parents_by_job = {}; + my $preferred_machines = {}; + my %test_suite_names; + + unless ($only_aggregated) { + $preferred_machines = _calculate_preferred_machines(\@jobs); + + # prefetch test suite names from job settings + my $job_settings + = $schema->resultset('JobSettings') + ->search({job_id => {-in => $job_ids}, key => {-in => [qw(JOB_DESCRIPTION TEST_SUITE_NAME)]}}); + for my $js ($job_settings->all) { + $settings_by_job_id{$js->job_id}->{$js->key} = $js->value; + } - my %test_suite_names = map { $_->id => ($settings_by_job_id{$_->id}->{TEST_SUITE_NAME} // $_->TEST) } @jobs; + %test_suite_names = map { $_->id => ($settings_by_job_id{$_->id}->{TEST_SUITE_NAME} // $_->TEST) } @jobs; - # prefetch descriptions from test suites - my %desc_args = (in => [values %test_suite_names]); - my @descriptions - = $schema->resultset('TestSuites')->search({name => \%desc_args}, {columns => [qw(name description)]}); - my %descriptions = map { $_->name => $_->description } @descriptions; + # prefetch descriptions from test suites + my %desc_args = (in => [values %test_suite_names]); + my @descriptions + = $schema->resultset('TestSuites')->search({name => \%desc_args}, {columns => [qw(name description)]}); + %descriptions = map { $_->name => $_->description } @descriptions; + + $failed_modules_by_job = $self->_fetch_failed_modules_by_jobs($job_ids); + ($children_by_job, $parents_by_job) = $self->_fetch_dependencies_by_jobs($job_ids); + } - my $failed_modules_by_job = $self->_fetch_failed_modules_by_jobs($job_ids); - my ($children_by_job, $parents_by_job) = $self->_fetch_dependencies_by_jobs($job_ids); foreach my $job (@jobs) { my $id = $job->id; my $result = $job->overview_result( @@ -759,6 +806,9 @@ $self->param_hash('failed_modules'), $failed_modules_by_job->{$id} || [], $self->param('todo')) or next; + + next if $only_aggregated; + my $test = $job->TEST; my $flavor = $job->FLAVOR || 'sweet'; my $arch = $job->ARCH || 'noarch'; @@ -861,16 +911,20 @@ # A generic query page showing test results in a configurable matrix sub overview ($self) { - my ($search_args, $groups) = $self->compose_job_overview_search_args; + my $data = $self->_gather_overview_results; + my $search_args = $data->{search_args}; + my $groups = $data->{groups}; + my $latest_job_ids = $data->{latest_job_ids}; + my $limit = $data->{limit}; + my $archs = $data->{archs}; + my $results = $data->{results}; + my $aggregated = $data->{aggregated}; + my $job_ids = $data->{job_ids}; + my $config = OpenQA::App->singleton->config; my $distri = $search_args->{distri}; my $version = $search_args->{version}; - my $jobs_rs = $self->schema->resultset('Jobs'); - my $latest_job_ids = $jobs_rs->complex_query_latest_ids(%$search_args); - my $limit = $search_args->{limit}; # one more than actual limit so we can check whether the limit was exceeded my $exceeded_limit = @$latest_job_ids >= $limit ? $limit - 1 : 0; - my $jobs = $jobs_rs->latest_jobs_from_ids($latest_job_ids, $limit); - my ($archs, $results, $aggregated, $job_ids) = $self->_prepare_job_results($jobs, $latest_job_ids); my %counts = (%$aggregated, failed => $aggregated->{failed} || ($aggregated->{unknown} // 0)); my $aggregate_status = (first { $counts{$_} } STATUS_PRIORITY) // 'passed'; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/lib/OpenQA/WebAPI.pm new/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI.pm --- old/openQA-5.1772031289.93bc2a13/lib/OpenQA/WebAPI.pm 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/lib/OpenQA/WebAPI.pm 2026-02-26 09:02:49.000000000 +0100 @@ -127,6 +127,8 @@ $op_auth->post('/tests/clone')->name('tests_clone')->to('test#clone'); $r->get('/tests/overview' => [format => ['json', 'html']])->name('tests_overview') ->to('test#overview', format => undef); + $r->get('/tests/overview/badge' => [format => ['svg']])->name('tests_overview_badge') + ->to('test#overview_badge', format => 'svg'); $r->get('/tests/latest')->name('latest')->to('test#latest'); $r->get('/tests/latest/badge')->name('latest_test_result_badge')->to('test#latest_badge'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/script/openqa-webui-daemon new/openQA-5.1772092969.74a39650/script/openqa-webui-daemon --- old/openQA-5.1772031289.93bc2a13/script/openqa-webui-daemon 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/script/openqa-webui-daemon 2026-02-26 09:02:49.000000000 +0100 @@ -17,6 +17,7 @@ # start openQA in the background # note: Our API commands are very expensive, so the default timeouts are too tight. + # note: -w (workers/preforks) should match max_conns in /etc/nginx/vhosts.d/openqa-upstreams.inc "$openqa_dir"/openqa prefork -m "$OPENQA_WEBUI_MODE" --proxy -i 100 -H 900 -w 30 -c 1 -G 800 -P "$pid_file" "${openqa_args[@]}" & pid=$! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/t/10-overview_badge.t new/openQA-5.1772092969.74a39650/t/10-overview_badge.t --- old/openQA-5.1772031289.93bc2a13/t/10-overview_badge.t 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/t/10-overview_badge.t 2026-02-26 09:02:49.000000000 +0100 @@ -0,0 +1,39 @@ +# Copyright SUSE LLC +# SPDX-License-Identifier: GPL-2.0-or-later + +use Test::Most; +use Mojo::Base -signatures; +use Test::Mojo; +use Test::Warnings ':report_warnings'; +use FindBin; +use lib "$FindBin::Bin/lib", "$FindBin::Bin/../external/os-autoinst-common/lib"; +use OpenQA::Test::TimeLimit '10'; +use OpenQA::Test::Case; +use OpenQA::Jobs::Constants; + +my $test_case = OpenQA::Test::Case->new; +$test_case->init_data(fixtures_glob => '01-jobs.pl 05-job_modules.pl'); +my $t = Test::Mojo->new('OpenQA::WebAPI'); + +$t->get_ok('/tests/overview/badge')->status_is(200)->content_type_is('image/svg+xml')->content_like(qr/running/); +$t->get_ok('/tests/overview/badge?result=passed')->status_is(200)->content_like(qr/passed/); +$t->get_ok('/tests/overview/badge?result=none&state=scheduled')->status_is(200)->content_like(qr/scheduled/); +$t->get_ok('/tests/overview/badge?build=87.5011')->status_is(200)->content_like(qr/not complete/); +$t->get_ok('/tests/overview/badge?build=0048')->status_is(200)->content_like(qr/failed/); +$t->get_ok('/tests/overview/badge?distri=nonexistent')->status_is(200)->content_like(qr/none/); +$t->get_ok('/tests/overview/badge?groupid=1001&build=0091')->status_is(200)->content_like(qr/running/); + +subtest 'svg badge' => sub { + $t->get_ok('/tests/99927/badge')->status_is(200)->content_type_is('image/svg+xml') + ->header_is('Cache-Control' => 'max-age=0, no-cache')->element_exists('svg', 'valid svg badge'); + $t->get_ok('/tests/9992711111/badge')->status_is(404)->content_type_is('image/svg+xml')->element_exists('svg') + ->content_like(qr/404/, 'valid 404 svg badge'); + $t->get_ok('/tests/latest/badge?test=kde&machine=32bit')->status_is(200)->content_type_is('image/svg+xml') + ->element_exists('svg', 'valid latest svg badge'); + $t->app->schema->resultset('Jobs')->find(99928) + ->update({state => SCHEDULED, result => NONE, blocked_by_id => 99927}); + $t->get_ok('/tests/99928/badge')->status_is(200)->content_type_is('image/svg+xml')->element_exists('svg') + ->content_like(qr/blocked/, 'valid blocked svg badge'); +}; + +done_testing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/t/21-needles.t new/openQA-5.1772092969.74a39650/t/21-needles.t --- old/openQA-5.1772031289.93bc2a13/t/21-needles.t 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/t/21-needles.t 2026-02-26 09:02:49.000000000 +0100 @@ -12,6 +12,7 @@ use Cwd qw(abs_path); use OpenQA::Schema; require OpenQA::Test::Database; +use OpenQA::Jobs::Constants; use OpenQA::Test::TimeLimit '10'; use OpenQA::Task::Needle::Save; use OpenQA::Task::Needle::Scan; @@ -24,12 +25,14 @@ use Time::Seconds; use Test::Output 'combined_like'; use Test::Mojo; +use Test::MockObject; use Test::MockModule; use OpenQA::Utils qw(ensure_timestamp_appended find_bug_number needledir testcasedir run_cmd_with_log run_cmd_with_log_return_error); use OpenQA::Test::Utils 'setup_fullstack_temp_dir'; use Test::Warnings ':report_warnings'; use Date::Format 'time2str'; +use POSIX ':errno_h'; # Avoid using tester's ~/.gitconfig delete $ENV{HOME}; @@ -183,25 +186,91 @@ }; }; +my %needle = (area => [{xpos => 0, ypos => 0, width => 1, height => 1, type => ''}], tags => ['tag']); +my $needle_json = encode_json(\%needle); +my $needle_dir = "$FindBin::Bin/testresults/00099/00099926-opensuse-Factory-staging_e-x86_64-Build87.5011-minimalx"; +my $real_needledir = "$FindBin::Bin/testresults/00099/00099961-opensuse-13.1-DVD-x86_64-Build0091-kde"; +my $image_dir = "$FindBin::Bin/images/347/da6"; +my $openqa_job = $schema->resultset('Jobs')->create({TEST => 'foo'}); +my %save_needle_args = ( + job_id => $openqa_job->id, + user_id => 1, + needledir => $needle_dir, + needle_json => $needle_json, + imagedir => $image_dir, + imagename => '61d0c3faf37d49d33b6fc308f2.png' +); + subtest 'handling symlinks when saving needles' => sub { my $git_mock = Test::MockModule->new('OpenQA::Git'); my $used_needle_dir; $git_mock->redefine(new => sub ($class, $args) { $used_needle_dir = $args->{dir}; die 'do not actually save' }); - my %needle = (area => [{xpos => 0, ypos => 0, width => 1, height => 1, type => ''}], tags => [{}]); - my $real_needledir = "$FindBin::Bin/testresults/00099/00099961-opensuse-13.1-DVD-x86_64-Build0091-kde"; - my %args = ( - job_id => $schema->resultset('Jobs')->create({TEST => 'foo'})->id, - user_id => 1, - needledir => "$FindBin::Bin/testresults/00099/00099926-opensuse-Factory-staging_e-x86_64-Build87.5011-minimalx", - needle_json => encode_json(\%needle), - imagedir => "$FindBin::Bin/images/347/da6", - imagename => '61d0c3faf37d49d33b6fc308f2.png' - ); - throws_ok { OpenQA::Task::Needle::Save::_save_needle($t->app, undef, \%args) } qr/do not actually save/, + throws_ok { OpenQA::Task::Needle::Save::_save_needle($t->app, undef, \%save_needle_args) } qr/do not actually save/, 'save needle runs as far as needed'; is $used_needle_dir, $real_needledir, 'the real path of the needles dir is used'; }; +subtest 'error cases when saving needles' => sub { + my $fake_job = Test::MockObject->new(); + $fake_job->set_true(qw(finish fail)); + + $save_needle_args{needlename} = 'foo'; + $save_needle_args{needle_json} = '}'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(1))[1])->{error}, qr/failed to validate foo.*malformed json/i, + 'error about malformed JSON returned'; + + $save_needle_args{needle_json} = '{}'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(2))[1])->{error}, qr/no area defined/i, 'error about missing area'; + + $save_needle_args{needle_json} = '{"area": [{}]}'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(3))[1])->{error}, qr/no tag defined/i, 'error about missing tag'; + + $save_needle_args{needle_json} = '{"area": [{}], "tags":["foo"]}'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(4))[1])->{error}, qr/area without xpos/i, 'error about invalid area'; + + $save_needle_args{needle_json} = '[]'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(5))[1])->{error}, qr/needle JSON is no object/i, 'error about wrong type'; + + $save_needle_args{needle_json} = $needle_json; + $save_needle_args{imagedir} = ''; + $save_needle_args{imagedistri} = 'imagedistri'; + $save_needle_args{imageversion} = 'imageversion'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(6))[1])->{error}, qr/image.*could not be found!/i, 'error about missing imaage'; + + $save_needle_args{imagedir} = $image_dir; + $save_needle_args{needledir} = 'does/not/exist'; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(7))[1])->{error}, qr/no needle directory/i, 'error about missing needle dir'; + + my $copy_mock = Test::MockModule->new('OpenQA::Task::Needle::Save'); + $copy_mock->redefine(copy => sub ($from, $to) { $! = EACCES; 0; }); # fake copy failing with "Permission denied" + $save_needle_args{needledir} = $needle_dir; + $save_needle_args{overwrite} = 1; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(8))[1])->{error}, qr/Error.*Permission denied/i, 'error about failing copy'; + + my $file_mock = Test::MockModule->new('Mojo::File'); + $file_mock->redefine(spew => sub ($file, $data) { die "unable to write $file" }); + undef $copy_mock; + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + like + (($fake_job->call_args(9))[1])->{error}, qr/unable to write.*foo\.json/i, 'error when writing JSON handled'; +}; + +subtest 'handling developer session when saving needles' => sub { + my $fake_job = Test::MockObject->new(); + $fake_job->set_true(qw(finish)); + $openqa_job->update({state => RUNNING}); + $schema->resultset('DeveloperSessions')->create({job_id => $openqa_job->id, user_id => 1}); + OpenQA::Task::Needle::Save::_save_needle($t->app, $fake_job, \%save_needle_args); + is +(($fake_job->call_args(1))[1])->{developer_session_job_id}, $save_needle_args{job_id}, 'job ID assigned'; +}; + subtest 'controller->_determine_needles_dir_for_job' => sub { my $controller = OpenQA::WebAPI::Controller::Step->new; $controller->app($t->app); @@ -304,4 +373,6 @@ ok -f "$symlinked_path/test-rootneedle.json", 'needle only removed in database (needle still exists on disk)'; }; +END { path($real_needledir, $_)->remove for qw(foo.json foo.png) } + done_testing; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/t/42-screenshots.t new/openQA-5.1772092969.74a39650/t/42-screenshots.t --- old/openQA-5.1772031289.93bc2a13/t/42-screenshots.t 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/t/42-screenshots.t 2026-02-26 09:02:49.000000000 +0100 @@ -165,6 +165,11 @@ [[{min_screenshot_id => 1, max_screenshot_id => 4, screenshots_per_batch => 200000}]], 'limit_screenshots task with default batch size from config enqueued' or always_explain $enququed_minion_job_args; + + my $job_info = run_gru_job($app, limit_results_and_logs => [{}]); + like $job_info->{notes}->{screenshot_cleanup}, qr/skipping.*still 1.*job/i, + 'screenshot limit tasks not enqueued again' + or always_explain $job_info; }; }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/t/api/02-iso-download.t new/openQA-5.1772092969.74a39650/t/api/02-iso-download.t --- old/openQA-5.1772031289.93bc2a13/t/api/02-iso-download.t 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/t/api/02-iso-download.t 2026-02-26 09:02:49.000000000 +0100 @@ -13,7 +13,7 @@ use OpenQA::Test::TimeLimit '20'; use OpenQA::Test::Case; use OpenQA::Test::Client 'client'; -use OpenQA::Test::Utils 'schedule_iso'; +use OpenQA::Test::Utils qw(schedule_iso run_gru_job); use OpenQA::Utils 'locate_asset'; OpenQA::Test::Case->new->init_data(fixtures_glob => '01-jobs.pl 03-users.pl 04-products.pl'); @@ -66,6 +66,11 @@ my $expected_job_count = 10; +# Assume the asset directory is not writeable +my $job = run_gru_job($t->app, download_asset => ['http://foo', '/does/not/exist', 0]); +is $job->{result}, 'Cannot write to asset directory "/does/not"', 'job fails if asset directory is not writeable' + or always_explain $job; + # Schedule download of an existing ISO $rsp = schedule_iso($t, {%iso, ISO_URL => 'http://localhost/openSUSE-13.1-DVD-i586-Build0091-Media.iso'}); check_download_asset('existing ISO'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1772031289.93bc2a13/t/ui/18-tests-details.t new/openQA-5.1772092969.74a39650/t/ui/18-tests-details.t --- old/openQA-5.1772031289.93bc2a13/t/ui/18-tests-details.t 2026-02-25 15:54:49.000000000 +0100 +++ new/openQA-5.1772092969.74a39650/t/ui/18-tests-details.t 2026-02-26 09:02:49.000000000 +0100 @@ -611,17 +611,6 @@ $t->content_like(qr/scheduled.*, created.*\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d/s, 'creation date displayed'); }; -subtest 'svg badge' => sub { - $t->get_ok('/tests/99927/badge')->status_is(200)->content_type_is('image/svg+xml') - ->header_is('Cache-Control' => 'max-age=0, no-cache')->element_exists('svg', 'valid svg badge'); - $t->get_ok('/tests/9992711111/badge')->status_is(404)->content_type_is('image/svg+xml')->element_exists('svg') - ->content_like(qr/404/, 'valid 404 svg badge'); - $t->get_ok('/tests/latest/badge?test=kde&machine=32bit')->status_is(200)->content_type_is('image/svg+xml') - ->element_exists('svg', 'valid latest svg badge'); - $jobs->find(99928)->update({state => SCHEDULED, result => NONE, blocked_by_id => 99927}); - $t->get_ok('/tests/99928/badge')->status_is(200)->content_type_is('image/svg+xml')->element_exists('svg') - ->content_like(qr/blocked/, 'valid blocked svg badge'); -}; subtest 'route to latest' => sub { $t->get_ok('/tests/latest?distri=opensuse&version=13.1&flavor=DVD&arch=x86_64&test=kde&machine=64bit') ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.KRGca8/_old 2026-02-27 17:11:53.305038197 +0100 +++ /var/tmp/diff_new_pack.KRGca8/_new 2026-02-27 17:11:53.341039689 +0100 @@ -1,5 +1,5 @@ name: openQA -version: 5.1772031289.93bc2a13 -mtime: 1772031289 -commit: 93bc2a130594f802fa3c4843b4680862d01e978b +version: 5.1772092969.74a39650 +mtime: 1772092969 +commit: 74a396508c9a0468c077a85c120fbdbac5bcb04e
