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
 

Reply via email to