Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package os-autoinst for openSUSE:Factory 
checked in at 2026-06-23 17:41:13
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/os-autoinst (Old)
 and      /work/SRC/openSUSE:Factory/.os-autoinst.new.1956 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "os-autoinst"

Tue Jun 23 17:41:13 2026 rev:606 rq:1361268 version:5.1782140090.fe34efb

Changes:
--------
--- /work/SRC/openSUSE:Factory/os-autoinst/os-autoinst.changes  2026-06-22 
17:28:34.967955437 +0200
+++ /work/SRC/openSUSE:Factory/.os-autoinst.new.1956/os-autoinst.changes        
2026-06-23 17:44:04.116997841 +0200
@@ -2 +2 @@
-Fri Jun 19 16:56:43 UTC 2026 - [email protected]
+Mon Jun 22 14:54:59 UTC 2026 - [email protected]
@@ -4 +4,4 @@
-- Update to version 5.1781875657.efde779:
+- Update to version 5.1782140090.fe34efb:
+  * fix: exclude virt-firmware on older Leap ppc64
+  * style: enforce signatures in anonymous subroutines
+  * fix: stop deepening when repo is no longer shallow
@@ -6,5 +8,0 @@
-  * test: assert Level 3 pretty serial markers
-  * fix: fallback pretty marker console to 'sut'
-  * test: fix color test resilience to NO_COLOR
-  * fix: stop QMP wait hangs on early QEMU exits
-  * fix: stop autodie unlink crashes in qemu backend
@@ -11,0 +10 @@
+  * refactor: improve t/01-test_needle.t structure and style

Old:
----
  os-autoinst-5.1781875657.efde779.obscpio

New:
----
  os-autoinst-5.1782140090.fe34efb.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ os-autoinst-devel-test.spec ++++++
--- /var/tmp/diff_new_pack.jgGmXI/_old  2026-06-23 17:44:08.025134036 +0200
+++ /var/tmp/diff_new_pack.jgGmXI/_new  2026-06-23 17:44:08.041134594 +0200
@@ -18,7 +18,7 @@
 
 %define         short_name os-autoinst-devel
 Name:           %{short_name}-test
-Version:        5.1781875657.efde779
+Version:        5.1782140090.fe34efb
 Release:        0
 Summary:        Test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ os-autoinst-openvswitch-test.spec ++++++
--- /var/tmp/diff_new_pack.jgGmXI/_old  2026-06-23 17:44:08.353145467 +0200
+++ /var/tmp/diff_new_pack.jgGmXI/_new  2026-06-23 17:44:08.381146443 +0200
@@ -19,7 +19,7 @@
 %define name_ext -test
 %define         short_name os-autoinst-openvswitch
 Name:           %{short_name}%{?name_ext}
-Version:        5.1781875657.efde779
+Version:        5.1782140090.fe34efb
 Release:        0
 Summary:        test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ os-autoinst-test.spec ++++++
--- /var/tmp/diff_new_pack.jgGmXI/_old  2026-06-23 17:44:08.733158710 +0200
+++ /var/tmp/diff_new_pack.jgGmXI/_new  2026-06-23 17:44:08.753159407 +0200
@@ -19,7 +19,7 @@
 %define name_ext -test
 %define         short_name os-autoinst
 Name:           %{short_name}%{?name_ext}
-Version:        5.1781875657.efde779
+Version:        5.1782140090.fe34efb
 Release:        0
 Summary:        test package for os-autoinst
 License:        GPL-2.0-or-later

++++++ os-autoinst.spec ++++++
--- /var/tmp/diff_new_pack.jgGmXI/_old  2026-06-23 17:44:09.061170142 +0200
+++ /var/tmp/diff_new_pack.jgGmXI/_new  2026-06-23 17:44:09.081170838 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           os-autoinst
-Version:        5.1781875657.efde779
+Version:        5.1782140090.fe34efb
 Release:        0
 Summary:        OS-level test automation
 License:        GPL-2.0-or-later
@@ -39,7 +39,7 @@
 # The following line is generated from dependencies.yaml
 %define build_requires %build_base_requires cmake ninja
 # The following line is generated from dependencies.yaml
-%define main_requires git-core iproute2 iputils jq openssh-clients 
perl(B::Deparse) perl(Carp) perl(Carp::Always) perl(Config) 
perl(Cpanel::JSON::XS) perl(Crypt::DES) perl(Cwd) perl(Data::Dumper) 
perl(Digest::MD5) perl(DynaLoader) perl(English) perl(Errno) 
perl(Exception::Class) perl(Exporter) perl(ExtUtils::testlib) perl(Fcntl) 
perl(Feature::Compat::Try) perl(File::Basename) perl(File::Find) 
perl(File::Map) perl(File::Path) perl(File::Temp) perl(File::Which) 
perl(File::chdir) perl(IO::Handle) perl(IO::Scalar) perl(IO::Select) 
perl(IO::Socket) perl(IO::Socket::INET) perl(IO::Socket::UNIX) perl(IPC::Open3) 
perl(IPC::Run::Debug) perl(IPC::System::Simple) perl(JSON::Validator) 
perl(List::MoreUtils) perl(List::Util) perl(Mojo::IOLoop::ReadWriteProcess) >= 
0.26 perl(Mojo::JSON) perl(Mojo::Log) perl(Mojo::URL) perl(Mojo::UserAgent) 
perl(Mojolicious) >= 9.340.0 perl(Mojolicious::Lite) perl(Net::DBus) 
perl(Net::Domain) perl(Net::IP) perl(Net::SNMP) perl(Net::SSH2) perl(POSIX) 
perl(Scalar::U
 til) perl(Socket) perl(Socket::MsgHdr) perl(Term::ANSIColor) 
perl(Thread::Queue) perl(Time::HiRes) perl(Time::Moment) perl(Time::Seconds) 
perl(XML::LibXML) perl(XML::SemanticDiff) perl(YAML::PP) perl(YAML::XS) 
perl(autodie) perl(base) perl(constant) perl(integer) perl(strict) 
perl(version) perl(warnings) perl-base rsync sshpass virt-firmware
+%define main_requires git-core iproute2 iputils jq openssh-clients 
perl(B::Deparse) perl(Carp) perl(Carp::Always) perl(Config) 
perl(Cpanel::JSON::XS) perl(Crypt::DES) perl(Cwd) perl(Data::Dumper) 
perl(Digest::MD5) perl(DynaLoader) perl(English) perl(Errno) 
perl(Exception::Class) perl(Exporter) perl(ExtUtils::testlib) perl(Fcntl) 
perl(Feature::Compat::Try) perl(File::Basename) perl(File::Find) 
perl(File::Map) perl(File::Path) perl(File::Temp) perl(File::Which) 
perl(File::chdir) perl(IO::Handle) perl(IO::Scalar) perl(IO::Select) 
perl(IO::Socket) perl(IO::Socket::INET) perl(IO::Socket::UNIX) perl(IPC::Open3) 
perl(IPC::Run::Debug) perl(IPC::System::Simple) perl(JSON::Validator) 
perl(List::MoreUtils) perl(List::Util) perl(Mojo::IOLoop::ReadWriteProcess) >= 
0.26 perl(Mojo::JSON) perl(Mojo::Log) perl(Mojo::URL) perl(Mojo::UserAgent) 
perl(Mojolicious) >= 9.340.0 perl(Mojolicious::Lite) perl(Net::DBus) 
perl(Net::Domain) perl(Net::IP) perl(Net::SNMP) perl(Net::SSH2) perl(POSIX) 
perl(Scalar::U
 til) perl(Socket) perl(Socket::MsgHdr) perl(Term::ANSIColor) 
perl(Thread::Queue) perl(Time::HiRes) perl(Time::Moment) perl(Time::Seconds) 
perl(XML::LibXML) perl(XML::SemanticDiff) perl(YAML::PP) perl(YAML::XS) 
perl(autodie) perl(base) perl(constant) perl(integer) perl(strict) 
perl(version) perl(warnings) perl-base rsync sshpass
 # all requirements needed by the tests, do not require on this in the package
 # itself or any sub-packages
 # SLE is missing spell check requirements
@@ -137,6 +137,15 @@
 # For unbuffered output of Perl testsuite, especially when running it on OBS 
so timestamps in the log are actually useful
 BuildRequires:  expect
 Requires:       %main_requires
+# For dynamically generating UEFI/secureboot assets, not available in all
+# OS+arch combinations
+%if 0%{?suse_version} > 0 && 0%{?suse_version} < 1600
+%ifnarch ppc64 ppc64le
+Requires:       virt-firmware
+%endif
+%else
+Requires:       virt-firmware
+%endif
 %if %{with ocr}
 Recommends:     tesseract-ocr
 %endif

++++++ os-autoinst-5.1781875657.efde779.obscpio -> 
os-autoinst-5.1782140090.fe34efb.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/OpenQA/Isotovideo/Utils.pm 
new/os-autoinst-5.1782140090.fe34efb/OpenQA/Isotovideo/Utils.pm
--- old/os-autoinst-5.1781875657.efde779/OpenQA/Isotovideo/Utils.pm     
2026-06-19 15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/OpenQA/Isotovideo/Utils.pm     
2026-06-22 16:54:50.000000000 +0200
@@ -198,13 +198,15 @@
     # * 
https://stackoverflow.com/questions/18515488/how-to-check-if-the-commit-exists-in-a-git-repository-by-its-sha-1
     # * 
https://stackoverflow.com/questions/26135216/why-isnt-there-a-git-clone-specific-commit-option
     bmwqemu::diag "Fetching more remote objects to ensure availability of 
'$branch'";
-    while (qx[git -C $local_path cat-file -e $branch^{commit} 2>&1] =~ /Not a 
valid object/) {
+    my $branch_not_found_err = "Could not find '$branch' in complete history 
in cloned Git repository \"$dir\"";
+    while (qx{git -C "$local_path" cat-file -e "$branch^{commit}" 2>&1} =~ 
/Not a valid object/) {
+        die $branch_not_found_err if qx{git -C "$local_path" rev-parse 
--is-shallow-repository 2>&1} =~ /^false/m;
         $clone_depth *= 2;
-        @out = qx[git -C $local_path fetch --progress --depth=$clone_depth 
2>&1];
+        @out = qx{git -C "$local_path" fetch --progress --depth=$clone_depth 
2>&1};
         $handle_output->($?, @out);
-        die "Could not find '$branch' in complete history in cloned Git 
repository \"$dir\"" if grep { /remote: Total 0/ } @out;
+        die $branch_not_found_err if grep { /remote: Total 0/ } @out;
     }
-    @out = qx{git -C $local_path checkout $branch};
+    @out = qx{git -C "$local_path" checkout "$branch"};
     bmwqemu::diag "@out" if @out;
     die "Unable to check out branch '$branch' in cloned Git repository 
\"$dir\"" unless $? == 0;
     return 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/backend/driver.pm 
new/os-autoinst-5.1782140090.fe34efb/backend/driver.pm
--- old/os-autoinst-5.1781875657.efde779/backend/driver.pm      2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/backend/driver.pm      2026-06-22 
16:54:50.000000000 +0200
@@ -50,8 +50,7 @@
         blocking_stop => 1,
         separate_err => 0,
         subreaper => 1,
-        code => sub {
-            my $process = shift;
+        code => sub ($process) {
             $0 = "$0: backend";
 
             open STDOUT, '>&', $STDOUTPARENT;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/backend/qemu.pm 
new/os-autoinst-5.1782140090.fe34efb/backend/qemu.pm
--- old/os-autoinst-5.1781875657.efde779/backend/qemu.pm        2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/backend/qemu.pm        2026-06-22 
16:54:50.000000000 +0200
@@ -460,11 +460,10 @@
     $self->save_console_snapshots($vmname);
 
     my $snapshot = $self->{proc}->snapshot_conf->add_snapshot($vmname);
-    $bdc->for_each_drive(sub {
+    $bdc->for_each_drive(sub ($drive) {
             local $Data::Dumper::Indent = 0;
             local $Data::Dumper::Terse = 1;
             local $Data::Dumper::Sortkeys = 1;
-            my $drive = shift;
 
             my $overlay = $bdc->add_snapshot_to_drive($drive, $snapshot);
             my $req = {execute => 'blockdev-snapshot-sync',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/commands.pm 
new/os-autoinst-5.1782140090.fe34efb/commands.pm
--- old/os-autoinst-5.1781875657.efde779/commands.pm    2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/commands.pm    2026-06-22 
16:54:50.000000000 +0200
@@ -264,8 +264,7 @@
     # Use same log format as isotovideo
     app->log->format(\&bmwqemu::log_format_callback);
     # process json messages from isotovideo
-    Mojo::IOLoop->singleton->reactor->io($isotovideo => sub {
-            my ($reactor, $writable) = @_;
+    Mojo::IOLoop->singleton->reactor->io($isotovideo => sub ($reactor, 
$writable) {
 
             my @isotovideo_responses = myjsonrpc::read_json($isotovideo, 
undef, 1);
             my $clients = app->defaults('clients');
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/container/os-autoinst_dev/Dockerfile 
new/os-autoinst-5.1782140090.fe34efb/container/os-autoinst_dev/Dockerfile
--- old/os-autoinst-5.1781875657.efde779/container/os-autoinst_dev/Dockerfile   
2026-06-19 15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/container/os-autoinst_dev/Dockerfile   
2026-06-22 16:54:50.000000000 +0200
@@ -68,7 +68,6 @@
        sudo \
        tesseract-ocr \
        tesseract-ocr-traineddata-english \
-       virt-firmware \
        which \
        xorg-x11-Xvnc \
        xterm \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/dependencies.yaml 
new/os-autoinst-5.1782140090.fe34efb/dependencies.yaml
--- old/os-autoinst-5.1781875657.efde779/dependencies.yaml      2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/dependencies.yaml      2026-06-22 
16:54:50.000000000 +0200
@@ -196,7 +196,6 @@
 main_requires:
   git-core:
   openssh-clients:
-  virt-firmware:
   perl(Cwd):
   perl(B::Deparse):
   perl(Carp):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/dist/rpm/os-autoinst.spec 
new/os-autoinst-5.1782140090.fe34efb/dist/rpm/os-autoinst.spec
--- old/os-autoinst-5.1781875657.efde779/dist/rpm/os-autoinst.spec      
2026-06-19 15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/dist/rpm/os-autoinst.spec      
2026-06-22 16:54:50.000000000 +0200
@@ -39,7 +39,7 @@
 # The following line is generated from dependencies.yaml
 %define build_requires %build_base_requires cmake ninja
 # The following line is generated from dependencies.yaml
-%define main_requires git-core iproute2 iputils jq openssh-clients 
perl(B::Deparse) perl(Carp) perl(Carp::Always) perl(Config) 
perl(Cpanel::JSON::XS) perl(Crypt::DES) perl(Cwd) perl(Data::Dumper) 
perl(Digest::MD5) perl(DynaLoader) perl(English) perl(Errno) 
perl(Exception::Class) perl(Exporter) perl(ExtUtils::testlib) perl(Fcntl) 
perl(Feature::Compat::Try) perl(File::Basename) perl(File::Find) 
perl(File::Map) perl(File::Path) perl(File::Temp) perl(File::Which) 
perl(File::chdir) perl(IO::Handle) perl(IO::Scalar) perl(IO::Select) 
perl(IO::Socket) perl(IO::Socket::INET) perl(IO::Socket::UNIX) perl(IPC::Open3) 
perl(IPC::Run::Debug) perl(IPC::System::Simple) perl(JSON::Validator) 
perl(List::MoreUtils) perl(List::Util) perl(Mojo::IOLoop::ReadWriteProcess) >= 
0.26 perl(Mojo::JSON) perl(Mojo::Log) perl(Mojo::URL) perl(Mojo::UserAgent) 
perl(Mojolicious) >= 9.340.0 perl(Mojolicious::Lite) perl(Net::DBus) 
perl(Net::Domain) perl(Net::IP) perl(Net::SNMP) perl(Net::SSH2) perl(POSIX) 
perl(Scalar::U
 til) perl(Socket) perl(Socket::MsgHdr) perl(Term::ANSIColor) 
perl(Thread::Queue) perl(Time::HiRes) perl(Time::Moment) perl(Time::Seconds) 
perl(XML::LibXML) perl(XML::SemanticDiff) perl(YAML::PP) perl(YAML::XS) 
perl(autodie) perl(base) perl(constant) perl(integer) perl(strict) 
perl(version) perl(warnings) perl-base rsync sshpass virt-firmware
+%define main_requires git-core iproute2 iputils jq openssh-clients 
perl(B::Deparse) perl(Carp) perl(Carp::Always) perl(Config) 
perl(Cpanel::JSON::XS) perl(Crypt::DES) perl(Cwd) perl(Data::Dumper) 
perl(Digest::MD5) perl(DynaLoader) perl(English) perl(Errno) 
perl(Exception::Class) perl(Exporter) perl(ExtUtils::testlib) perl(Fcntl) 
perl(Feature::Compat::Try) perl(File::Basename) perl(File::Find) 
perl(File::Map) perl(File::Path) perl(File::Temp) perl(File::Which) 
perl(File::chdir) perl(IO::Handle) perl(IO::Scalar) perl(IO::Select) 
perl(IO::Socket) perl(IO::Socket::INET) perl(IO::Socket::UNIX) perl(IPC::Open3) 
perl(IPC::Run::Debug) perl(IPC::System::Simple) perl(JSON::Validator) 
perl(List::MoreUtils) perl(List::Util) perl(Mojo::IOLoop::ReadWriteProcess) >= 
0.26 perl(Mojo::JSON) perl(Mojo::Log) perl(Mojo::URL) perl(Mojo::UserAgent) 
perl(Mojolicious) >= 9.340.0 perl(Mojolicious::Lite) perl(Net::DBus) 
perl(Net::Domain) perl(Net::IP) perl(Net::SNMP) perl(Net::SSH2) perl(POSIX) 
perl(Scalar::U
 til) perl(Socket) perl(Socket::MsgHdr) perl(Term::ANSIColor) 
perl(Thread::Queue) perl(Time::HiRes) perl(Time::Moment) perl(Time::Seconds) 
perl(XML::LibXML) perl(XML::SemanticDiff) perl(YAML::PP) perl(YAML::XS) 
perl(autodie) perl(base) perl(constant) perl(integer) perl(strict) 
perl(version) perl(warnings) perl-base rsync sshpass
 # all requirements needed by the tests, do not require on this in the package
 # itself or any sub-packages
 # SLE is missing spell check requirements
@@ -137,6 +137,15 @@
 # For unbuffered output of Perl testsuite, especially when running it on OBS 
so timestamps in the log are actually useful
 BuildRequires:  expect
 Requires:       %main_requires
+# For dynamically generating UEFI/secureboot assets, not available in all
+# OS+arch combinations
+%if 0%{?suse_version} > 0 && 0%{?suse_version} < 1600
+%ifnarch ppc64 ppc64le
+Requires:       virt-firmware
+%endif
+%else
+Requires:       virt-firmware
+%endif
 %if %{with ocr}
 Recommends:     tesseract-ocr
 %endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/01-test_needle.t 
new/os-autoinst-5.1782140090.fe34efb/t/01-test_needle.t
--- old/os-autoinst-5.1781875657.efde779/t/01-test_needle.t     2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/01-test_needle.t     2026-06-22 
16:54:50.000000000 +0200
@@ -28,120 +28,111 @@
       or always_explain "actual similarity: $similarity, expected similarity: 
$expected_similarity";
 }
 
+sub needle_init () {
+    my $ret;
+    stderr_like { $ret = needle::init } qr/loaded.*needles/, 'log output for 
needle init';
+    return $ret;
+}
+
 throws_ok(
-    sub {
-        needle->new('foo.json');
-    },
+    sub { needle->new('foo.json') },
     qr{needles not initialized}s,
     'died when constructing needle without prior call to needle::init()'
 );
 
-subtest 'needle JSON file not under needle directory' => sub {
-    my $misc_needles_dir = Cwd::cwd;
-    needle::set_needles_dir($misc_needles_dir);
-    my $invalid_json_path = 'invalid/path/to/file.json';
-    throws_ok {
-        needle->new($invalid_json_path);
-    } qr/Needle $invalid_json_path is not under needle directory 
$misc_needles_dir/,
-      'throws error when needle JSON file is not under needle directory';
-};
-
-subtest 'handle broken JSON file' => sub {
-    my $sandbox = tempdir(CLEANUP => 1);
-    needle::set_needles_dir($sandbox);
-    my $broken_json_path = path($sandbox, 'broken.json');
-
-    $broken_json_path->spew('{ "tags": ["test');
-
-    like warning {
-        my $needle = needle->new($broken_json_path->basename);
-        is $needle, undef, 'needle object not created with broken JSON';
-    }, qr/broken json.*broken\.json/, 'warning shown for broken JSON file';
-};
-
-subtest 'handle tags duplicates' => sub {
-    my $sandbox = tempdir(CLEANUP => 1);
-    needle::set_needles_dir($sandbox);
-    my $tag_json_path = path($sandbox, 'tag.json');
-    my $tag_png_path = path($sandbox, 'tag.png');
-
-    $tag_png_path->spew('foobar');
-    $tag_json_path->spew('{"area": [{"x" : 123, "y" : 456}],"tags": ["tag1", 
"tag1"]}');
-
-    my $needle;
-    combined_like { $needle = needle->new($tag_json_path->basename) } 
qr/\[debug\].*tag contains tag1 twice/, 'tag contains tag1 twice';
-    is $needle->has_tag('tag1'), 1, 'tag found';
-};
+subtest 'needle file location and validation' => sub {
+    subtest 'needle JSON file not under needle directory' => sub {
+        my $misc_needles_dir = Cwd::cwd;
+        needle::set_needles_dir($misc_needles_dir);
+        my $invalid_json_path = 'invalid/path/to/file.json';
+        throws_ok {
+            needle->new($invalid_json_path);
+        } qr/Needle $invalid_json_path is not under needle directory 
$misc_needles_dir/,
+          'throws error when needle JSON file is not under needle directory';
+    };
 
-subtest 'handle invalid click point' => sub {
-    my $sandbox = tempdir(CLEANUP => 1);
-    needle::set_needles_dir($sandbox);
-    my $invalid_click_point_json_path = path($sandbox, 
'invalid-click-point.json');
+    subtest 'handle broken JSON file' => sub {
+        my $sandbox = tempdir(CLEANUP => 1);
+        needle::set_needles_dir($sandbox);
+        my $broken_json_path = path($sandbox, 'broken.json');
+        $broken_json_path->spew('{ "tags": ["test');
+
+        like warning {
+            my $needle = needle->new($broken_json_path->basename);
+            is $needle, undef, 'needle object not created with broken JSON';
+        }, qr/broken json.*broken\.json/, 'warning shown for broken JSON file';
+    };
 
-    $invalid_click_point_json_path->spew('{"area": [{"click_point": 
"invalid"}]}');
+    subtest 'handle tags duplicates' => sub {
+        my $sandbox = tempdir(CLEANUP => 1);
+        needle::set_needles_dir($sandbox);
+        my $tag_json_path = path($sandbox, 'tag.json');
+        my $tag_png_path = path($sandbox, 'tag.png');
+        $tag_png_path->spew('foobar');
+        $tag_json_path->spew('{"area": [{"x" : 123, "y" : 456}],"tags": 
["tag1", "tag1"]}');
+
+        my $needle;
+        combined_like { $needle = needle->new($tag_json_path->basename) } 
qr/\[debug\].*tag contains tag1 twice/, 'tag contains tag1 twice';
+        ok $needle->has_tag('tag1'), 'tag found';
+    };
 
-    like warning {
-        my $needle = needle->new($invalid_click_point_json_path->basename);
-        is $needle, undef, 'needle object not created with invalid click 
point';
-    }, qr/invalid-click-point\.json has an area with invalid click point/, 
'warning shown for invalid click point';
+    subtest 'handle invalid click point' => sub {
+        my $sandbox = tempdir(CLEANUP => 1);
+        needle::set_needles_dir($sandbox);
+        my $invalid_click_point_json_path = path($sandbox, 
'invalid-click-point.json');
+        $invalid_click_point_json_path->spew('{"area": [{"click_point": 
"invalid"}]}');
+
+        like warning {
+            my $needle = needle->new($invalid_click_point_json_path->basename);
+            is $needle, undef, 'needle object not created with invalid click 
point';
+        }, qr/invalid-click-point\.json has an area with invalid click point/, 
'warning shown for invalid click point';
+    };
 };
 
-sub needle_init () {
-    my $ret;
-    stderr_like { $ret = needle::init } qr/loaded.*needles/, 'log output for 
needle init';
-    return $ret;
-}
-
 cv::init();
 require tinycv;
 
-my ($res, $needle, $img1, $cand);
-
 my $data_dir = dirname(__FILE__) . '/data/';
 my $misc_needles_dir = abs_path(dirname(__FILE__)) . '/misc_needles/';
-
 $bmwqemu::vars{NEEDLES_DIR} = $data_dir;
 needle_init;
 
-$img1 = tinycv::read($data_dir . 'bootmenu.test.png');
-$needle = needle->new('bootmenu.ref.json');
-
-is $needle->has_tag('inst-bootmenu'), 1, 'tag found';
-is $needle->has_tag('foobar'), 0, 'tag not found';
-
-is $needle->has_property('glossy'), 1, 'property found';
-is $needle->has_property('dull'), 0, 'property not found';
-
-$res = $img1->search($needle);
-
-ok defined $res, 'match with exclude area';
-
-($res, $cand) = $img1->search($needle);
-ok defined $res, 'match in array context';
-ok $res->{ok}, 'match in array context ok == 1';
-ok $res->{area}->[-1]->{result} eq 'ok', 'match in array context result == ok';
-ok !defined $cand, 'candidates must be undefined';
-
-$needle = needle->new('bootmenu-fail.ref.json');
-$res = $img1->search($needle);
-ok !defined $res, 'no match';
-
-($res, $cand) = $img1->search($needle);
-ok !defined $res, 'no match in array context';
-ok defined $cand && ref $cand eq 'ARRAY', 'candidates must be array';
-
-$img1 = tinycv::read($data_dir . 'reclaim_space_delete_btn-20160823.test.png');
-$needle = needle->new('reclaim_space_delete_btn-20160823.ref.json');
-
-$res = $img1->search($needle, 0, 0);
-is $res->{area}->[0]->{x}, 108, 'found area is the original one';
-$res = $img1->search($needle, 0, 0.9);
-is $res->{area}->[0]->{x}, 108, 'found area is the original one too';
-
-$img1 = tinycv::read($data_dir . 'kde.test.png');
-$needle = needle->new('kde.ref.json');
-$res = $img1->search($needle);
-ok !defined $res, 'no match with different art';
+subtest 'needle properties and simple search' => sub {
+    my $img = tinycv::read($data_dir . 'bootmenu.test.png');
+    my $needle = needle->new('bootmenu.ref.json');
+
+    ok $needle->has_tag('inst-bootmenu'), 'tag found';
+    ok !$needle->has_tag('foobar'), 'tag not found';
+    ok $needle->has_property('glossy'), 'property found';
+    ok !$needle->has_property('dull'), 'property not found';
+
+    my $res = $img->search($needle);
+    ok defined $res, 'match with exclude area';
+
+    my ($res_ctx, $cand) = $img->search($needle);
+    ok defined $res_ctx, 'match in array context';
+    ok $res_ctx->{ok}, 'match in array context ok == 1';
+    is $res_ctx->{area}->[-1]->{result}, 'ok', 'match in array context result 
== ok';
+    ok !defined $cand, 'candidates must be undefined';
+
+    $needle = needle->new('bootmenu-fail.ref.json');
+    $res = $img->search($needle);
+    ok !defined $res, 'no match';
+
+    ($res, $cand) = $img->search($needle);
+    ok !defined $res, 'no match in array context';
+    ok defined $cand && ref $cand eq 'ARRAY', 'candidates must be array';
+};
+
+subtest 'search with parameters' => sub {
+    my $img = tinycv::read($data_dir . 
'reclaim_space_delete_btn-20160823.test.png');
+    my $needle = needle->new('reclaim_space_delete_btn-20160823.ref.json');
+
+    my $res = $img->search($needle, 0, 0);
+    is $res->{area}->[0]->{x}, 108, 'found area is the original one';
+    $res = $img->search($needle, 0, 0.9);
+    is $res->{area}->[0]->{x}, 108, 'found area is the original one too';
+};
 
 subtest 'handle failure to load image' => sub {
     my $needle_with_png = needle->new('kde.ref.json');
@@ -164,406 +155,314 @@
       'needle with missing PNG skipped';
 };
 
-$img1 = tinycv::read($data_dir . 'console.test.png');
-$needle = needle->new('console.ref.json');
-($res, $cand) = $img1->search($needle);
-ok !defined $res, 'no match different console screenshots';
-subtest 'candidate is almost true' => sub {
-    my $areas = $cand->[0]->{area};
-    _cmp_similarity $areas->[0], 0.945;
-    is_deeply $areas, [{h => 160, w => 645, x => 190, y => 285, result => 
'fail'}], 'coordinates/result';
-};
-
-$img1 = tinycv::read($data_dir . 'instdetails.test.png');
-$needle = needle->new('instdetails.ref.json');
-$res = $img1->search($needle);
-ok !defined $res, 'no match different perform installation tabs';
-
-# Check that if the margin is missing from JSON, is set in the hash
-$img1 = tinycv::read($data_dir . 'uefi.test.png');
-$needle = needle->new('uefi.ref.json');
-ok $needle->{area}->[0]->{margin} == 50, 'search margin have the default 
value';
-$res = $img1->search($needle);
-ok !defined $res, 'no found a match for an small margin';
-
-# Check that if the margin is set in JSON, is set in the hash
-$img1 = tinycv::read($data_dir . 'uefi.test.png');
-$needle = needle->new('uefi-margin.ref.json');
-ok $needle->{area}->[0]->{margin} == 100, 'search margin have the defined 
value';
-$res = $img1->search($needle);
-ok defined $res, 'found match for a large margin';
-ok $res->{area}->[0]->{x} == 378 && $res->{area}->[0]->{y} == 221, 'mach area 
coordinates';
-
-# This test fails in internal SLE system
-$img1 = tinycv::read($data_dir . 'glibc_i686.test.png');
-$needle = needle->new('glibc_i686.ref.json');
-$res = $img1->search($needle);
-ok !defined $res, 'no found a match for an small margin';
-# We emulate 'assert_screen "needle", 3'
-my $timeout = 3;
-for (my $n = 0; $n < $timeout; $n++) {
-    my $search_ratio = 1.0 - ($timeout - $n) / ($timeout);
-    $res = $img1->search($needle, 0, $search_ratio);
-}
-ok defined $res, 'found match after timeout';
-
-$img1 = tinycv::read($data_dir . 'zypper_ref.test.png');
-$needle = needle->new('zypper_ref.ref.json');
-ok $needle->{area}->[0]->{margin} == 300, 'search margin have the default 
value';
-$res = $img1->search($needle);
-ok defined $res, 'found a match for 300 margin';
-
-needle_init;
-
-my @alltags = sort keys %needle::tags;
-my @needles = @{needle::tags('none') || []};
-is @needles, 3, 'three needles found';
-for my $n (@needles) {
-    $n->unregister();
-}
+subtest 'candidate analysis' => sub {
+    my $img = tinycv::read($data_dir . 'console.test.png');
+    my $needle = needle->new('console.ref.json');
+    my ($res, $cand) = $img->search($needle);
+    ok !defined $res, 'no match different console screenshots';
+    subtest 'candidate is almost true' => sub {
+        my $areas = $cand->[0]->{area};
+        _cmp_similarity $areas->[0], 0.945;
+        is_deeply $areas, [{h => 160, w => 645, x => 190, y => 285, result => 
'fail'}], 'coordinates/result';
+    };
+};
 
-@needles = @{needle::tags('none') || []};
-is @needles, 0, 'no needles after unregister';
+subtest 'margin specifications' => sub {
+    my $img = tinycv::read($data_dir . 'uefi.test.png');
+    subtest 'default margin from JSON' => sub {
+        my $needle = needle->new('uefi.ref.json');
+        is $needle->{area}->[0]->{margin}, 50, 'search margin has the default 
value';
+        my $res = $img->search($needle);
+        ok !defined $res, 'no match for small margin';
+    };
 
-for my $n (needle::all()) {
-    $n->unregister();
-}
+    subtest 'explicit margin from JSON' => sub {
+        my $needle = needle->new('uefi-margin.ref.json');
+        is $needle->{area}->[0]->{margin}, 100, 'search margin has the defined 
value';
+        my $res = $img->search($needle);
+        ok defined $res, 'found match for a large margin';
+        is $res->{area}->[0]->{x}, 378, 'match area x coordinates';
+        is $res->{area}->[0]->{y}, 221, 'match area y coordinates';
+    };
+};
 
-is_deeply \%needle::tags, {}, 'no tags registered';
+subtest 'search timeout emulation' => sub {
+    my $img = tinycv::read($data_dir . 'glibc_i686.test.png');
+    my $needle = needle->new('glibc_i686.ref.json');
+    my $res = $img->search($needle);
+    ok !defined $res, 'no match with strict similarity';
+
+    my $timeout = 3;
+    for (my $n = 0; $n < $timeout; $n++) {
+        my $search_ratio = 1.0 - ($timeout - $n) / ($timeout);
+        $res = $img->search($needle, 0, $search_ratio);
+    }
+    ok defined $res, 'found match after timeout';
+};
 
-for my $n (needle::all()) {
-    $n->register();
-}
+subtest 'data-driven needle search cases' => sub {
+    my @cases = (
+        {png => 'kde.test.png', json => 'kde.ref.json', match => 0, desc => 
'no match with different art'},
+        {png => 'zypper_ref.test.png', json => 'zypper_ref.ref.json', match => 
1, desc => 'found a match for 300 margin'},
+        {png => 'screenlock.test.png', json => 'screenlock.ref.json', match => 
1, desc => 'match screenlock'},
+        {png => 'desktop-at-first-boot-kde-without-greeter-20140926.test.png', 
json => 'desktop-at-first-boot-kde-without-greeter-20140926.json', match => 0, 
desc => 'KDE clearly not ready'},
+        {png => 'yast2_lan-hostname-tab-20140630.test.png', json => 
'yast2_lan-hostname-tab-20140630.json', match => 1, desc => 'hostname is 
different'},
+        {png => 'desktop_mainmenu-gnomesled-sles12.test.png', json => 
'desktop_mainmenu-gnomesled-sles12.json', match => 0, desc => 'the mixer has a 
hover effect'},
+        {png => 'inst-video-typed-sles12b9.test.png', json => 
'inst-video-typed-sles12b9.json', match => 0, desc => 'the contrast is just too 
different'},
+        {png => 'displaymanager-sle12.test.png', json => 
'displaymanager-sle12.json', match => 0, desc => 'the headline is completely 
different'},
+        {png => 'inst-welcome-20140902.test.png', json => 
'inst-welcome-20140902.json', match => 1, desc => 'match welcome'},
+        {png => 'confirmlicense-sle12.test.png', json => 
'confirmlicense-sle12.json', match => 1, desc => 'license to confirm'},
+        {png => 'desktop-runner-20140523.test.png', json => 
'desktop-runner-20140523.json', match => 1, desc => 'just some dark shade'},
+        {png => 'accept-ssh-host-key.test.png', json => 
'accept-ssh-host-key.json', match => 0, desc => 'no match for blinking cursor'},
+        {png => 'xorg_vt-Xorg-20140729.test.png', json => 
'xorg_vt-Xorg-20140729.json', match => 0, desc => 'the y goes into the line'},
+        {png => 'select_patterns.test.png', json => 'select_patterns.json', 
match => 0, desc => 'the green mark is unselected'},
+        {png => 'other-desktop-dvd-20140904.test.png', json => 
'other-desktop-dvd-20140904.json', match => 0, desc => "the hot keys don't 
match"},
+    );
+
+    for my $case (@cases) {
+        my $img = tinycv::read($data_dir . $case->{png});
+        my $needle = needle->new($case->{json});
+        my $res = $img->search($needle);
+        ok $case->{match} ? defined $res : !defined $res, $case->{desc};
+    }
+};
 
-is_deeply \@alltags, [sort keys %needle::tags], 'all tags restored';
+subtest 'special case: kde unselected' => sub {
+    my $needle = needle->new('kde-unselected-20141211.json');
+    my $img = tinycv::read($data_dir . 'kde-unselected-20141211.test.png');
+    my $res = $img->search($needle);
+    ok defined $res, 'match kde is not selected';
+    is $res->{area}->[-1]->{w}, 17, 'click area width';
+    is $res->{area}->[-1]->{h}, 12, 'click area height';
+    is $res->{area}->[-1]->{y}, 260, 'click area y';
+    is $res->{area}->[-1]->{x}, 313, 'click area x';
+};
+
+subtest 'complex candidates' => sub {
+    my @complex_cases = (
+        {
+            png => 'xterm-started-20141204.test.png',
+            json => 'xterm-started-20141204.json',
+            ratio => 0.7,
+            expect_area => {x => 127, w => 39, y => 76, h => 18, result => 
'fail'},
+            similarity => 0.9058,
+            desc => 'xterm on GNOME is more blurry'
+        },
+        {
+            png => 'inst-rescuesystem-20141027.test.png',
+            json => 'inst-rescuesystem-20141027.json',
+            expect_area => {x => 245, w => 312, result => 'fail', y => 219, h 
=> 36},
+            similarity => 0,
+            desc => 'different text in rescue system'
+        },
+        {
+            png => 'ooffice-save-prompt-gnome-20160713.test.png',
+            json => 'ooffice-save-prompt-gnome-20160713.json',
+            expect_area => {x => 273, w => 483, result => 'fail', y => 323, h 
=> 133},
+            similarity => 0,
+            desc => 'font rendering changed in ooffice'
+        }
+    );
+
+    for my $case (@complex_cases) {
+        my $img = tinycv::read($data_dir . $case->{png});
+        my $needle = needle->new($case->{json});
+        my ($res, $cand) = $img->search($needle, 0, $case->{ratio} // 0);
+        ok !defined $res, $case->{desc};
+        my $area = $cand->[0]->{area}->[-1];
+        is $area->{x}, $case->{expect_area}->{x}, "$case->{desc} candidate x";
+        is $area->{y}, $case->{expect_area}->{y}, "$case->{desc} candidate y";
+        _cmp_similarity $area, $case->{similarity} if defined 
$case->{similarity};
+    }
+};
 
-$img1 = tinycv::read($data_dir . 'user_settings-1.png');
-my $img2 = tinycv::read($data_dir . 'user_settings-2.png');
-ok $img1->similarity($img2) > 53, 'similarity is too small';
-
-$img1 = tinycv::read($data_dir . 'screenlock.test.png');
-$needle = needle->new('screenlock.ref.json');
-$res = $img1->search($needle);
-
-ok defined $res, 'match screenlock';
-
-$img1 = tinycv::read($data_dir . 
'desktop-at-first-boot-kde-without-greeter-20140926.test.png');
-$needle = 
needle->new('desktop-at-first-boot-kde-without-greeter-20140926.json');
-$res = $img1->search($needle);
-ok !defined $res, 'KDE clearly not ready';
-
-$img1 = tinycv::read($data_dir . 'yast2_lan-hostname-tab-20140630.test.png');
-$needle = needle->new('yast2_lan-hostname-tab-20140630.json');
-$res = $img1->search($needle);
-
-ok defined $res, 'hostname is different';
-
-$img1 = tinycv::read($data_dir . 'desktop_mainmenu-gnomesled-sles12.test.png');
-$needle = needle->new('desktop_mainmenu-gnomesled-sles12.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'the mixer has a hover effect';
-
-$img1 = tinycv::read($data_dir . 'inst-video-typed-sles12b9.test.png');
-$needle = needle->new('inst-video-typed-sles12b9.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'the contrast is just too different';
-
-$img1 = tinycv::read($data_dir . 'xterm-started-20141204.test.png');
-$needle = needle->new('xterm-started-20141204.json');
-($res, $cand) = $img1->search($needle, 0, 0.7);
-
-ok !defined $res, 'xterm on GNOME is more blurry';
-subtest 'we find the xterm though' => sub {
-    my $area = $cand->[0]->{area}->[1];
-    _cmp_similarity $area, 0.905881691408007;
-    is_deeply $area, {x => 127, w => 39, y => 76, h => 18, result => 'fail'}, 
'coordinates/result';
-};
-
-$img1 = tinycv::read($data_dir . 'pkcon-proceed-prompt-20141205.test.png');
-$needle = needle->new('pkcon-proceed-prompt-20141205.json');
-($res, $cand) = $img1->search($needle, 0, 0.7);
-
-ok !defined $res, 'the prompt is the same to the human eye, but it differs in 
shades of gray';
-# the value varies between 92.9 and 92.8 dependending on used libraries
-$cand->[0]->{area}->[0]->{similarity} = 
int($cand->[0]->{area}->[0]->{similarity} * 100 + 0.5);
-is_deeply
-  $cand->[0]->{area},
-  [
-    {
-        similarity => 93,
-        x => 17,
-        w => 237,
-        result => 'fail',
-        y => 326,
-        h => 10
-
-    },
-  ],
-  'offered for needle recreation though'
-  ;
-
-$img1 = tinycv::read($data_dir . 'displaymanager-sle12.test.png');
-$needle = needle->new('displaymanager-sle12.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'the headline is completely different';
-
-$img1 = tinycv::read($data_dir . 'inst-rescuesystem-20141027.test.png');
-$needle = needle->new('inst-rescuesystem-20141027.json');
-($res, $cand) = $img1->search($needle);
-is_deeply
-  $cand->[0]->{area},
-  [
-    {
-        similarity => '0',
-        x => 245,
-        w => 312,
-        result => 'fail',
-        y => 219,
-        h => 36
+subtest 'needle registration and tagging' => sub {
+    needle_init;
+    my @alltags = sort keys %needle::tags;
+    my @needles = @{needle::tags('none') || []};
+    is scalar @needles, 3, 'three needles found for tag "none"';
+
+    for my $n (@needles) { $n->unregister() }
+    is scalar @{needle::tags('none') || []}, 0, 'no needles after unregister';
+
+    for my $n (needle::all()) { $n->unregister() }
+    is_deeply \%needle::tags, {}, 'no tags registered';
+
+    for my $n (needle::all()) { $n->register() }
+    is_deeply [sort keys %needle::tags], \@alltags, 'all tags restored';
+
+    subtest 'test tags method with multiple tags' => sub {
+        my $tag1 = 'tag1';
+        my $tag2 = 'tag2';
+        my $tag3 = 'tag3';
+        needle::set_needles_dir($misc_needles_dir);
+
+        needle->new($_) for qw(test_tag1.json test_tag2.json test_tag3.json);
+        $_->register() for needle::all();
+
+        my $result = needle::tags($tag1);
+        is scalar @$result, 2, 'two needles found for tag1';
+        $result = needle::tags($tag2);
+        is scalar @$result, 2, 'two needles found for tag2';
+        $result = needle::tags("$tag1 $tag2");
+        is scalar @$result, 1, 'one needle found with tag1 and tag2';
+        $result = needle::tags("$tag2 $tag3");
+        is scalar @$result, 0, 'no needle found with tag2 and tag3';
+        $result = needle::tags($tag3);
+        is scalar @$result, 1, 'one needle found for tag3';
+        $result = needle::tags('nonexistent');
+        is scalar @$result, 0, 'no needles found for nonexistent tag';
+
+        # Restore original state
+        $bmwqemu::vars{NEEDLES_DIR} = $data_dir;
+        needle_init;
+    };
+};
 
-    }
-  ],
-  'candidate total fail, but not at 0x0'
-  ;
-
-ok !defined $res, 'different text';
-
-$needle = needle->new('ooffice-save-prompt-gnome-20160713.json');
-$img1 = tinycv::read($data_dir . 
'ooffice-save-prompt-gnome-20160713.test.png');
-($res, $cand) = $img1->search($needle);
-
-ok !defined $res, 'font rendering changed';
-is_deeply
-  $cand->[0]->{area},
-  [
-    {
-        similarity => '0',
-        x => 273,
-        w => 483,
-        result => 'fail',
-        y => 323,
-        h => 133
+subtest 'similarity and image cache' => sub {
+    my $img1 = tinycv::read($data_dir . 'user_settings-1.png');
+    my $img2 = tinycv::read($data_dir . 'user_settings-2.png');
+    ok $img1->similarity($img2) > 53, 'similarity between user settings 
images';
+
+    needle::clean_image_cache(0);
+    is needle::image_cache_size, 0, 'image cache completely cleaned';
+
+    my $needle = needle->new('other-desktop-dvd-20140904.json');
+    $needle->{png} = $data_dir . 'other-desktop-dvd-20140904.test.png';
+    my $cached_img = $needle->get_image;
+    ok defined $cached_img, 'image returned';
+    is needle::image_cache_size, 1, 'cache size increased';
+    is $needle->get_image, $cached_img, 'cached image returned on next call';
+
+    my $img_area = $needle->get_image($needle->{area}->[0]);
+    ok $img_area != $cached_img, 'different image returned when get_image with 
area';
+
+    my $json_hash = $needle->TO_JSON;
+    is $json_hash->{name}, 'other-desktop-dvd-20140904', 'TO_JSON 
serialization';
+
+    my $other_needle = needle->new('xorg_vt-Xorg-20140729.json');
+    $other_needle->{png} = $data_dir . 'xorg_vt-Xorg-20140729.test.png';
+    my $other_img = $other_needle->get_image;
+    ok $other_img != $cached_img, 'different image returned for other needle 
instance';
+    is needle::image_cache_size, 2, 'cache size increased to 2';
+
+    needle::clean_image_cache(1);
+    is needle::image_cache_size, 1, 'cleaning cache to keep 1 image';
+    is $other_needle->get_image, $other_img, 'most recently used cached image 
still exists';
+    ok $needle->get_image != $cached_img, 'old cached image was deleted';
+};
+
+subtest 'initialization variants' => sub {
+    subtest 'default_needles_dir' => sub {
+        local $bmwqemu::vars{PRODUCTDIR} = '/tmp/foo';
+        is needle::default_needles_dir(), '/tmp/foo/needles', 'default needles 
dir correct';
+    };
 
-    }
-  ],
-  'candidate total fail, but position still good'
-  ;
-
-
-$img1 = tinycv::read($data_dir . 'inst-welcome-20140902.test.png');
-$needle = needle->new('inst-welcome-20140902.json');
-$res = $img1->search($needle);
-
-ok defined $res, 'match welcome';
-
-$img1 = tinycv::read($data_dir . 'confirmlicense-sle12.test.png');
-$needle = needle->new('confirmlicense-sle12.json');
-$res = $img1->search($needle);
-
-ok defined $res, 'license to confirm';
-
-$img1 = tinycv::read($data_dir . 'desktop-runner-20140523.test.png');
-$needle = needle->new('desktop-runner-20140523.json');
-$res = $img1->search($needle);
-
-ok defined $res, 'just some dark shade';
-
-$img1 = tinycv::read($data_dir . 'accept-ssh-host-key.test.png');
-$needle = needle->new('accept-ssh-host-key.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'no match for blinking cursor';
-
-$img1 = tinycv::read($data_dir . 'xorg_vt-Xorg-20140729.test.png');
-$needle = needle->new('xorg_vt-Xorg-20140729.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'the y goes into the line';
-
-$needle = needle->new('kde-unselected-20141211.json');
-$img1 = tinycv::read($data_dir . 'kde-unselected-20141211.test.png');
-$res = $img1->search($needle);
-
-ok defined $res, 'match kde is not selected';
-
-# make sure the last area is the click area
-is $res->{area}->[-1]->{w}, 17;
-is $res->{area}->[-1]->{h}, 12;
-is $res->{area}->[-1]->{y}, 260;
-is $res->{area}->[-1]->{x}, 313;
-
-$img1 = tinycv::read($data_dir . 'select_patterns.test.png');
-$needle = needle->new('select_patterns.json');
-$res = $img1->search($needle);
-
-ok !defined $res, 'the green mark is unselected';
-
-$img1 = tinycv::read($data_dir . 'other-desktop-dvd-20140904.test.png');
-$needle = needle->new('other-desktop-dvd-20140904.json');
-$res = $img1->search($needle);
-
-ok !defined $res, "the hot keys don't match";
-
-# match comparison tests
-# note it's important that the workaround needle sort alphabetically
-# *AFTER* the imperfect needle, so it doesn't win 'by accident'
-my $perfect = needle->new('login_sddm.ref.perfect.json');
-my $imperfect = needle->new('login_sddm.ref.imperfect.json');
-my $workaround = needle->new('login_sddm.ref.workaround.imperfect.json');
-
-# test that a perfect non-workaround match is preferred to imperfect
-# non-workaround and workaround matches
-$img1 = tinycv::read($data_dir . 'login_sddm.test.png');
-$res = $img1->search([$perfect, $imperfect, $workaround], 0.9, 0);
-is $res->{needle}->{name}, 'login_sddm.ref.perfect', 'perfect match should 
win';
-
-# test that when two equal matches fight and one is a workaround, that
-# one wins
-$res = $img1->search([$imperfect, $workaround], 0.9, 0);
-is $res->{needle}->{name}, 'login_sddm.ref.workaround.imperfect', 'workaround 
match should win';
-
-# test caching via needle->get_image
-needle::clean_image_cache(0);
-is needle::image_cache_size, 0, 'image cache completely cleaned';
-$needle = needle->new('other-desktop-dvd-20140904.json');
-$needle->{png} = $data_dir . 'other-desktop-dvd-20140904.test.png';
-$img1 = $needle->get_image;
-ok defined $img1, 'image returned';
-is needle::image_cache_size, 1, 'cache size increased';
-is $needle->get_image, $img1, 'cached image returned on next call';
-is needle::image_cache_size, 1, 'cache size not further increased';
-my $img_area = $needle->get_image($needle->{area}->[0]);
-ok $img_area != $img1, 'different image returned for when get_image with area';
-my $other_needle = needle->new('xorg_vt-Xorg-20140729.json');
-$other_needle->{png} = $data_dir . 'xorg_vt-Xorg-20140729.test.png';
-$img2 = $other_needle->get_image;
-ok $img2 != $img1, 'different image returned for other needle instance';
-is needle::image_cache_size, 2, 'cache size increased';
-needle::clean_image_cache(2);
-is needle::image_cache_size, 2, 'cleaning cache to keep only 2 images should 
not affect cache size';
-is $other_needle->get_image, $img2, 'cached image still returned';
-is $needle->get_image, $img1, 'cached image still returned';
-needle::clean_image_cache(1);
-ok $other_needle->get_image != $img2, 'cleaning cache to keep 1 image deleted 
$img2';
-is $needle->get_image, $img1, 'cleaning cache to keep 1 image kept $img1';
-$img2 = $other_needle->get_image;    # make $img2 the most recently used
-needle::clean_image_cache(1);
-is $other_needle->get_image, $img2, 'cleaning cache to keep 1 image kept 
$img2';
-ok $needle->get_image != $img1, 'cleaning cache to keep 1 image deleted $img1';
-is $needle->{file}, 'other-desktop-dvd-20140904.json', 'needle json path is 
relative to needles dir';
-
-subtest 'needle::init accepts custom NEEDLES_DIR within working directory and 
otherwise falls back to "$bmwqemu::vars{PRODUCTDIR}/needles"' => sub {
-    # create temporary working directory and a needle directory within it
-    my $temp_working_dir = tempdir(CLEANUP => 1);
-    my $needles_dir = $bmwqemu::vars{NEEDLES_DIR} = 
"$temp_working_dir/some-needle-repo";
-    make_path("$needles_dir/subdir");
-    for my $extension (qw(json png)) {
-        path($misc_needles_dir, 
"click-point.$extension")->copy_to("$needles_dir/subdir/foo.$extension");
-    }
+    subtest 'custom NEEDLES_DIR within working directory' => sub {
+        my $temp_working_dir = tempdir(CLEANUP => 1);
+        my $needles_dir = "$temp_working_dir/some-needle-repo";
+        make_path("$needles_dir/subdir");
+        for my $ext (qw(json png)) {
+            path($misc_needles_dir, 
"click-point.$ext")->copy_to("$needles_dir/subdir/foo.$ext");
+        }
 
-    subtest 'custom NEEDLES_DIR used when within working directory' => sub {
-        note "using working directory $temp_working_dir";
+        local $bmwqemu::vars{NEEDLES_DIR} = $needles_dir;
+        my $orig_cwd = Cwd::cwd;
         chdir $temp_working_dir;
-        $bmwqemu::vars{NEEDLES_DIR} = $needles_dir;
         is needle_init, $needles_dir, 'custom needle dir accepted';
-
-        ok $needle = needle->new('subdir/foo.json'), 'needle object created 
with needle from working directory';
+        is needle::needles_dir(), $needles_dir, 'needles_dir returns current 
needle dir';
+        my $needle = needle->new('subdir/foo.json');
         is $needle->{file}, 'subdir/foo.json', 'file path relative to needle 
directory';
         is $needle->{png}, "$needles_dir/subdir/foo.png", 'absolute image path 
assigned';
+        chdir $orig_cwd;
+    };
+
+    subtest 'clarify error message when needles directory does not exist' => 
sub {
+        local $bmwqemu::vars{CASEDIR} = '/tmp/foo';
+        local $bmwqemu::vars{PRODUCTDIR} = '/tmp/boo/products/boo';
+        local $bmwqemu::vars{NEEDLES_DIR} = undef;
+        throws_ok { needle::init } qr/Can't init needles from 
\/tmp\/boo\/products\/boo\/needles at.*/, 'do not combine CASEDIR when the 
default needles directory is an absolute path';
+
+        local $bmwqemu::vars{PRODUCTDIR} = 'boo/products/boo';
+        throws_ok { needle::init } qr/Can't init needles from 
boo\/products\/boo\/needles;.*\/tmp\/foo\/boo\/products\/boo\/needles/, 
'combine CASEDIR when the default needles directory is a relative path';
     };
 };
 
 subtest 'click point' => sub {
     needle::set_needles_dir($misc_needles_dir);
 
-    my $needle = needle->new('click-point.json');
-    is_deeply $needle->{area}->[0]->{click_point}, {xpos => 2, ypos => 4}, 
'click point parsed';
+    my $click_point_1 = 
needle->new('click-point.json')->{area}->[0]->{click_point};
+    is_deeply $click_point_1, {xpos => 2, ypos => 4}, 'click point parsed';
+    my $click_point_2 = 
needle->new('click-point-center.json')->{area}->[0]->{click_point};
+    is_deeply $click_point_2, 'center', 'click point "center" parsed';
+
+    my $multi = needle->new('click-point-multiple-ids.json');
+    is_deeply $multi->{area}->[0]->{click_point}, {xpos => 2, ypos => 4, id => 
'first'}, 'first click point parsed';
+    is_deeply $multi->{area}->[1]->{click_point}, {xpos => 1, ypos => 3, id => 
'second'}, 'second click point parsed';
+
+    for my $bad (qw(click-point-multiple click-point-multiple-mixed-1 
click-point-multiple-mixed-2)) {
+        like warning {
+            my $bad_needle = needle->new("$bad.json");
+            is $bad_needle, undef, "bad click point in $bad";
+        }, qr/has more than one area with a click point/, "warning for $bad";
+    }
 
-    $needle = needle->new('click-point-center.json');
-    is_deeply $needle->{area}->[0]->{click_point}, 'center', 'click point 
"center" parsed';
+    subtest 'click point copying in search' => sub {
+        my $sandbox = tempdir(CLEANUP => 1);
+        my $json_path = path($sandbox, 'click-point-test.json');
+        $json_path->spew('{"area": [{"xpos": 0, "ypos": 0, "width": 10, 
"height": 10, "click_point": {"xpos": 2, "ypos": 4}}], "tags": ["test"]}');
+        path($sandbox, 'click-point-test.png')->spew('dummy');
+
+        my $orig_needles_dir = needle::needles_dir();
+        needle::set_needles_dir($sandbox);
+        my $needle = needle->new('click-point-test.json');
+        $needle->{png} = $data_dir . 'bootmenu.test.png';
+
+        my $img = tinycv::read($data_dir . 'bootmenu.test.png');
+        my $res = $img->search($needle);
+        ok defined $res, 'click-point-test needle matched';
+        is_deeply $res->{area}->[0]->{click_point}, {xpos => 2, ypos => 4}, 
'click point copied to search result';
 
-    $needle = needle->new('click-point-multiple-ids.json');
-    is_deeply $needle->{area}->[0]->{click_point}, {xpos => 2, ypos => 4, id 
=> 'first'}, 'first click point parsed';
-    is_deeply $needle->{area}->[1]->{click_point}, {xpos => 1, ypos => 3, id 
=> 'second'}, 'second click point parsed';
-
-    like warning {
-        $needle = needle->new('click-point-multiple.json');
-    }, qr/click-point-multiple\.json has more than one area with a click point 
without assigning IDs to each/, 'warning shown';
-    is_deeply $needle, undef, 'multiple click points without IDs not accepted';
-
-    like warning {
-        $needle = needle->new('click-point-multiple-mixed-1.json');
-    }, qr/click-point-multiple-mixed-1\.json has more than one area with a 
click point without assigning IDs to each/, 'warning shown';
-    is_deeply $needle, undef, 'multiple click points without IDs not accepted';
-
-    like warning {
-        $needle = needle->new('click-point-multiple-mixed-2.json');
-    }, qr/click-point-multiple-mixed-2\.json has more than one area with a 
click point without assigning IDs to each/, 'warning shown';
-    is_deeply $needle, undef, 'multiple click points without IDs not accepted';
+        needle::set_needles_dir($orig_needles_dir);
+    };
 };
 
 subtest 'workaround property' => sub {
     needle::set_needles_dir($misc_needles_dir);
+    my $w_str = needle->new('check-workaround-bsc1234567-20190522.json');
+    my $w_hash = needle->new('check-workaround-hash-20190522.json');
+    my $no_w = needle->new('click-point-center.json');
 
-    my $workaround_string_needle = 
needle->new('check-workaround-bsc1234567-20190522.json');
-    my $workaround_hash_needle = 
needle->new('check-workaround-hash-20190522.json');
-    my $no_workaround_needle = needle->new('click-point-center.json');
-    my $mix_workaround_string_needle = 
needle->new('check-workaround-mix-bsc987321-20190617.json');
-    my $mix_workaround_hash_needle = 
needle->new('check-workaround-hash-mix-20190617.json');
-
-    ok $workaround_string_needle->has_property('workaround'), 'workaround 
property found when it is recorded in string';
-    ok $workaround_hash_needle->has_property('workaround'), 'workaround 
property found when it is recorded in hash';
-    ok $mix_workaround_string_needle->has_property('workaround'), 'workaround 
property found in mixed properties';
-    ok $mix_workaround_hash_needle->has_property('workaround'), 'workaround 
property found in mixed properties';
-    ok $no_workaround_needle->has_property('glossy'), 'glossy property found';
-    ok !$no_workaround_needle->has_property('workaround'), 'workaround 
property not found';
-    ok !$workaround_string_needle->has_property('glossy'), 'glossy property 
not found';
-    ok !$workaround_hash_needle->has_property('glossy'), 'glossy property not 
found';
-
-    is $workaround_string_needle->get_property_value('workaround'), 
'bsc#1234567', 'get correct value when workaround is recorded in string';
-    is $workaround_hash_needle->get_property_value('workaround'), 
'bsc#7654321: this is a test about workaround.', 'get ccorrect value when 
workaround is recorded in hash';
-    is $mix_workaround_string_needle->get_property_value('workaround'), 
'bsc#987321', 'workaround value is correct';
-    is $mix_workaround_hash_needle->get_property_value('workaround'), 
'bsc#123789: This is a test for workaround property', 'workaround value is 
correct';
-    is $workaround_hash_needle->get_property_value('test'), undef, 'no test 
value';
-    is $no_workaround_needle->get_property_value('workaround'), undef, 'no 
workaround property';
-    is $no_workaround_needle->get_property_value('glossy'), undef, 'glossy 
property is a string, has no value';
-};
-
-subtest 'clarify error message when needles directory does not exist' => sub {
-    $bmwqemu::vars{CASEDIR} = '/tmp/foo';
-    $bmwqemu::vars{PRODUCTDIR} = '/tmp/boo/products/boo';
-    $bmwqemu::vars{NEEDLES_DIR} = undef;
-    throws_ok { needle::init } qr/Can't init needles from 
\/tmp\/boo\/products\/boo\/needles at.*/, 'do not combine CASEDIR when the 
default needles directory is an absolute path';
-
-    $bmwqemu::vars{PRODUCTDIR} = 'boo/products/boo';
-    throws_ok { needle::init } qr/Can't init needles from 
boo\/products\/boo\/needles;.*\/tmp\/foo\/boo\/products\/boo\/needles/, 
'combine CASEDIR when the default needles directory is a relative path';
-};
-
-subtest 'test tags method' => sub {
-    my $tag1 = 'tag1';
-    my $tag2 = 'tag2';
-    my $tag3 = 'tag3';
-    needle::set_needles_dir($misc_needles_dir);
+    ok $w_str->has_property('workaround'), 'workaround property found in 
string';
+    ok $w_hash->has_property('workaround'), 'workaround property found in 
hash';
+    ok !$no_w->has_property('workaround'), 'no workaround property';
+
+    is $w_str->get_property_value('workaround'), 'bsc#1234567', 'value from 
string';
+    is $w_hash->get_property_value('workaround'), 'bsc#7654321: this is a test 
about workaround.', 'value from hash';
+    is $w_hash->get_property_value('test'), undef, 'no test value';
+    is $no_w->get_property_value('workaround'), undef, 'no workaround 
property';
+    is $no_w->get_property_value('glossy'), undef, 'glossy property is a 
string, has no value';
+};
+
+subtest 'match comparison and workaround preference' => sub {
+    needle::set_needles_dir($data_dir);
+    my $perfect = needle->new('login_sddm.ref.perfect.json');
+    my $imperfect = needle->new('login_sddm.ref.imperfect.json');
+    my $workaround = needle->new('login_sddm.ref.workaround.imperfect.json');
+
+    my $img = tinycv::read($data_dir . 'login_sddm.test.png');
+
+    my ($res_arr, $cand_arr) = $img->search([$perfect, $imperfect, 
$workaround], 0.9, 0);
+    is $res_arr->{needle}->{name}, 'login_sddm.ref.perfect', 'perfect match 
should win in array context';
 
-    needle->new($_) for qw(test_tag1.json test_tag2.json test_tag3.json);
+    my $res_sc = $img->search([$perfect, $imperfect, $workaround], 0.9, 0);
+    is $res_sc->{needle}->{name}, 'login_sddm.ref.perfect', 'perfect match 
should win in scalar context';
 
-    $_->register() for needle::all();
+    my $res_workaround = $img->search([$imperfect, $workaround], 0.9, 0);
+    is $res_workaround->{needle}->{name}, 
'login_sddm.ref.workaround.imperfect', 'workaround match should win';
 
-    my $result = needle::tags($tag1);
-    is scalar @$result, 2, 'two needles found for tag1';
-    $result = needle::tags($tag2);
-    is scalar @$result, 2, 'two needles found for tag2';
-    $result = needle::tags("$tag1 $tag2");
-    is scalar @$result, 1, 'one needle found with tag1 and tag2';
-    $result = needle::tags("$tag2 $tag3");
-    is scalar @$result, 0, 'no needle found with tag2 and tag3';
-    $result = needle::tags($tag3);
-    is scalar @$result, 1, 'one needle found for tag3';
-    $result = needle::tags('nonexistent');
-    is scalar @$result, 0, 'no needles found for nonexistent tag';
+    my $res_tie = $img->search([$imperfect, $imperfect], 0.9, 0);
+    ok defined $res_tie, 'tie breaker alphabetical names';
 };
 
 done_testing();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/05-distribution.t 
new/os-autoinst-5.1782140090.fe34efb/t/05-distribution.t
--- old/os-autoinst-5.1781875657.efde779/t/05-distribution.t    2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/05-distribution.t    2026-06-22 
16:54:50.000000000 +0200
@@ -84,8 +84,7 @@
     $mock_testapi->redefine(get_var => sub { $_[0] eq 'PRETTY_SERIAL_MARKER' ? 
1 : undef });
     $testapi::serialdev = 'ttyS0';
 
-    $mock_testapi->redefine(wait_serial => sub {
-            my ($regexp) = @_;
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return undef if ref($regexp) eq 'Regexp' && $regexp =~ /FC/;
             return 'SRfoo-0-';
@@ -95,8 +94,7 @@
     $d->script_run('foo');
     like $typed_string, qr/export __OA_MARK=.*; foo\n/, 'Level 2 uses export 
marker';
 
-    $mock_testapi->redefine(wait_serial => sub {
-            my ($regexp) = @_;
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp;
             return 'OA:DONE-abcd-0-foo';
@@ -130,7 +128,7 @@
     $d->script_run('foo');
     like $typed_string, qr/foo; echo SR.*-.*-\n/, 'Level 1 uses classic marker 
on serial terminal';
 
-    $mock_testapi->redefine(wait_serial => sub ($pat, %args) {
+    $mock_testapi->redefine(wait_serial => sub ($pat, %) {
             return 0 if $pat =~ /foo; echo SR.*-\$\?-/;
             return 'SRfoo-0-';
     });
@@ -167,8 +165,7 @@
     $testapi::serialdev = 'ttyS0';
 
     # Initial detection (Level 3)
-    $mock_testapi->redefine(wait_serial => sub {
-            my ($regexp) = @_;
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp;
             return 'OA:DONE-abcd-0-';
@@ -324,8 +321,7 @@
     $mock_testapi->redefine(query_isotovideo => sub { });
     $mock_bmwqemu->redefine(diag => sub { $diag_msg .= $_[0] });
     $mock_bmwqemu->redefine(log_call => sub { });
-    $mock_testapi->redefine(wait_serial => sub {
-            my ($regexp) = @_;
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp;
             return 'OA:DONE-abcd-0-';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/t/10-virtio_terminal.t 
new/os-autoinst-5.1782140090.fe34efb/t/10-virtio_terminal.t
--- old/os-autoinst-5.1781875657.efde779/t/10-virtio_terminal.t 2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/10-virtio_terminal.t 2026-06-22 
16:54:50.000000000 +0200
@@ -84,8 +84,7 @@
     my $size = 1024;
     $file_mock->redefine(slurp => sub { return 65_536; });
     $vterminal_mock->redefine('get_pipe_sz', sub { return 1024; });
-    $vterminal_mock->redefine('set_pipe_sz', sub {
-            my ($self, $fd, $newsize) = @_;
+    $vterminal_mock->redefine('set_pipe_sz', sub ($self, $fd, $newsize) {
             return if ($newsize > 2048);
             return $size = $newsize;
     });
@@ -104,8 +103,7 @@
     is $size, 1024, "Size didn't changed";
 
     $size = 1024;
-    $vterminal_mock->redefine('set_pipe_sz', sub {
-            my ($self, $fd, $newsize) = @_;
+    $vterminal_mock->redefine('set_pipe_sz', sub ($self, $fd, $newsize) {
             return $size = $newsize;
     });
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/14-isotovideo.t 
new/os-autoinst-5.1782140090.fe34efb/t/14-isotovideo.t
--- old/os-autoinst-5.1781875657.efde779/t/14-isotovideo.t      2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/14-isotovideo.t      2026-06-22 
16:54:50.000000000 +0200
@@ -120,7 +120,7 @@
         my $state = decode_json($base_state->slurp);
         if (is ref $state, 'HASH', 'state file contains object') {
             is $state->{component}, 'isotovideo', 'state file contains 
component';
-            like $state->{msg}, qr/Unable to clone Git repository/, 'state 
file contains error message';
+            like $state->{msg}, qr/(?:Unable to clone Git repository|Could not 
find 'foo' in complete history)/, 'state file contains error message';
         }
     };
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/17-basetest.t 
new/os-autoinst-5.1782140090.fe34efb/t/17-basetest.t
--- old/os-autoinst-5.1781875657.efde779/t/17-basetest.t        2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/17-basetest.t        2026-06-22 
16:54:50.000000000 +0200
@@ -124,8 +124,7 @@
     });
     my $basetest = basetest->new('installation');
     my $message;
-    $mock_basetest->redefine(record_resultfile => sub {
-            my ($self, $title, $output, %nargs) = @_;
+    $mock_basetest->redefine(record_resultfile => sub ($self, $title, $output, 
%nargs) {
             $message = $output;
     });
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/t/19-isotovideo-command-processing.t 
new/os-autoinst-5.1782140090.fe34efb/t/19-isotovideo-command-processing.t
--- old/os-autoinst-5.1781875657.efde779/t/19-isotovideo-command-processing.t   
2026-06-19 15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/19-isotovideo-command-processing.t   
2026-06-22 16:54:50.000000000 +0200
@@ -28,8 +28,7 @@
 
 # mock the json rpc
 my $rpc_mock = Test::MockModule->new('myjsonrpc');
-$rpc_mock->redefine(send_json => sub {
-        my ($fd, $cmd) = @_;
+$rpc_mock->redefine(send_json => sub ($fd, $cmd) {
         if (!defined($fd) || ($fd != $cmd_srv_fd && $fd != $backend_fd && $fd 
!= $answer_fd)) {
             fail 'invalid file descriptor passed to send_json: ' . ($fd ? $fd 
: 'undef');    # uncoverable statement
             return;    # uncoverable statement
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/22-svirt.t 
new/os-autoinst-5.1782140090.fe34efb/t/22-svirt.t
--- old/os-autoinst-5.1781875657.efde779/t/22-svirt.t   2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/22-svirt.t   2026-06-22 
16:54:50.000000000 +0200
@@ -103,10 +103,10 @@
     my $backend_mock = Test::MockModule->new('backend::svirt');
     my $console_mock = Test::MockModule->new('consoles::sshVirtsh');
     my $tmp_mock = Test::MockModule->new('File::Temp');
-    $console_mock->redefine(run_cmd => sub ($self, $cmd, %args) { push 
@$cmds_ref, $cmd; 0 });
-    $console_mock->redefine(get_cmd_output => sub ($self, $cmd, %args) { push 
@$cmds_ref, $cmd; 0 });
-    $backend_mock->redefine(run_ssh_cmd => sub ($self, $cmd, %args) { push 
@$cmds_ref, $cmd; 0 });
-    $backend_mock->redefine(run_ssh => sub ($self, $cmd, %args) { push 
@$ssh_cmds_ref, $cmd; (undef, $chan_mock) });
+    $console_mock->redefine(run_cmd => sub ($self, $cmd, %) { push @$cmds_ref, 
$cmd; 0 });
+    $console_mock->redefine(get_cmd_output => sub ($self, $cmd, %) { push 
@$cmds_ref, $cmd; 0 });
+    $backend_mock->redefine(run_ssh_cmd => sub ($self, $cmd, %) { push 
@$cmds_ref, $cmd; 0 });
+    $backend_mock->redefine(run_ssh => sub ($self, $cmd, %) { push 
@$ssh_cmds_ref, $cmd; (undef, $chan_mock) });
     $backend_mock->redefine(start_serial_grab => 1);
     $console_mock->redefine(get_ssh_credentials => sub { (hostname => 'foo', 
username => 'root', password => '123') });
     $tmp_mock->redefine(tempfile => sub { (undef, '/t') });
@@ -442,10 +442,8 @@
     my $test_log_cnt = 0;
     my $grep_return = 1;
     my @deleted_logs;
-    $module->redefine(run_ssh_cmd => sub {
-            my $self = shift;
-            @LAST_ = @_;
-            my $cmd = shift;
+    $module->redefine(run_ssh_cmd => sub ($self, $cmd, @args) {
+            @LAST_ = ($cmd, @args);
             return !!($test_log_cnt > 0 ? --$test_log_cnt : 0) if ($cmd =~ 
m/^test -e/);
             return $grep_return if ($cmd =~ m/^grep -q/);
             push @deleted_logs, ($cmd =~ /(\S+)$/) if ($cmd =~ / && rm /);
@@ -454,8 +452,7 @@
     });
 
     my $run_ssh_expect = '$a';
-    $module->redefine(run_ssh => sub {
-            my ($self, $cmd, %args) = @_;
+    $module->redefine(run_ssh => sub ($self, $cmd, %) {
             like $cmd, qr/$run_ssh_expect/, "run_ssh() command is like 
qr/$run_ssh_expect/";
             return ('A', 'B');
     });
@@ -545,8 +542,7 @@
     $console_mock->redefine(which => 1);
 
     my $mock_baseclass = Test::MockModule->new('backend::baseclass');
-    $mock_baseclass->redefine('run_ssh_cmd' => sub {
-            my ($self, $cmd, %args) = @_;
+    $mock_baseclass->redefine('run_ssh_cmd' => sub ($self, $cmd, %args) {
             push @last_ssh_commands, $cmd;
             push @last_ssh_args, [%args];
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/23-baseclass.t 
new/os-autoinst-5.1782140090.fe34efb/t/23-baseclass.t
--- old/os-autoinst-5.1781875657.efde779/t/23-baseclass.t       2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/23-baseclass.t       2026-06-22 
16:54:50.000000000 +0200
@@ -107,15 +107,13 @@
     my @timeouts = ();
     my $net_ssh2 = Test::MockModule->new('Net::SSH2');
     my @agent;
-    $net_ssh2->redefine(new => sub {
-            my ($class, %opts) = @_;
+    $net_ssh2->redefine(new => sub ($class, %opts) {
             my $self = Test::MockObject->new();
             my $id = $self->{my_custom_id} = bmwqemu::random_string(32);
             die 'Identifier not unique' if exists $ssh_obj_data->{$id};
             $ssh_obj_data->{$id} = $self;
 
-            $self->mock(connect => sub {
-                    my ($self, $hostname, $port) = @_;
+            $self->mock(connect => sub ($self, $hostname, $port) {
                     return 0 if $ssh_connect_error;
                     is $hostname, $ssh_expect->{hostname}, 'Connect to correct 
hostname';
                     # if unspecified, default to port 22
@@ -126,20 +124,17 @@
                     return 1;
             });
             $self->mock(hostname => sub { return 
$ssh_obj_data->{refaddr(shift)}->{hostname} });
-            $self->mock(auth => sub {
-                    my ($self, %args) = @_;
+            $self->mock(auth => sub ($self, %args) {
                     is $args{username}, $ssh_expect->{username}, 'Correct 
username for ssh connection';
                     is $args{password}, $ssh_expect->{password}, 'Correct 
password for ssh connection';
                     return 1;
             });
             $self->mock(auth_agent => sub { push @agent, [@_]; return 1 });
-            $self->mock(auth_ok => sub {
-                    my $self = shift;
+            $self->mock(auth_ok => sub ($self) {
                     $self->{connected} = !!$ssh_auth_ok;
                     return $ssh_auth_ok;
             });
-            $self->mock(blocking => sub {
-                    my ($self, $v) = @_;
+            $self->mock(blocking => sub ($self, $v = undef) {
                     $self->{blocking} = $v if defined $v;
                     return $self->{blocking};
             });
@@ -148,8 +143,7 @@
                     return 1;
             });
             $self->mock(error => sub { wantarray ? @net_ssh2_error : 
($net_ssh2_error[0] // 0) });
-            $self->mock(sock => sub {
-                    my $self = shift;
+            $self->mock(sock => sub ($self) {
                     unless ($self->{sock}) {
                         my $mock_sock = Test::MockObject->new();
                         $mock_sock->{ssh} = $self;
@@ -157,14 +151,12 @@
                     }
                     return $self->{sock};
             });
-            $self->mock(channel => sub {
-                    my $self = shift;
+            $self->mock(channel => sub ($self) {
                     die 'Not connected' unless ($self->{connected});
                     return $fail_on_channel_call = undef if 
$fail_on_channel_call;
                     my $mock_channel = Test::MockObject->new();
                     $mock_channel->{ssh} = $self;
-                    $mock_channel->mock(exec => sub {
-                            my ($self, $cmd) = @_;
+                    $mock_channel->mock(exec => sub ($self, $cmd) {
                             $self->{cmd} = $cmd;
                             $self->{eof} = 0;
                             return 1 unless $cmd =~ /^(echo|test)/;
@@ -321,7 +313,7 @@
         $baseclass->truncate_serial_file();
         my $expect_output = "FOO$/" x 
backend::baseclass::SSH_SERIAL_READ_BUFFER_SIZE;
         my $channel_read_string = $expect_output;
-        $chan->mock(read => sub {
+        $chan->mock(read => sub {    # no:style:signatures
                 my ($self, undef, $max) = @_;
                 return unless (defined $channel_read_string);
                 $max //= backend::baseclass::SSH_SERIAL_READ_BUFFER_SIZE;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/25-spvm.t 
new/os-autoinst-5.1782140090.fe34efb/t/25-spvm.t
--- old/os-autoinst-5.1781875657.efde779/t/25-spvm.t    2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/25-spvm.t    2026-06-22 
16:54:50.000000000 +0200
@@ -14,8 +14,7 @@
 subtest 'SSH credentials in spvm' => sub {
     my $expected_credentials = {username => 'root', password => 'foo', 
hostname => 'my_foo_hostname'};
     my $mock_spvm = Test::MockModule->new('backend::spvm');
-    $mock_spvm->mock(run_ssh_cmd => sub {
-            my ($self, $cmd, %args) = @_;
+    $mock_spvm->mock(run_ssh_cmd => sub ($self, $cmd, %args) {
             for my $k (keys %{$expected_credentials}) {
                 is $args{$k}, $expected_credentials->{$k}, "Correct $k 
parameter";
             }
@@ -40,8 +39,7 @@
 
 subtest 'PowerVM power actions' => sub {
     my $mock_spvm = Test::MockModule->new('backend::spvm');
-    $mock_spvm->redefine('run_cmd', sub {
-            my ($self, $cmd) = @_;
+    $mock_spvm->redefine('run_cmd', sub ($self, $cmd) {
             return $cmd;
     });
     my $spvm = backend::spvm->new();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/t/30-mmapi.t 
new/os-autoinst-5.1782140090.fe34efb/t/30-mmapi.t
--- old/os-autoinst-5.1781875657.efde779/t/30-mmapi.t   2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/30-mmapi.t   2026-06-22 
16:54:50.000000000 +0200
@@ -61,12 +61,10 @@
 # setup a fake server
 my $mock_srv = Mojolicious->new;
 $mock_srv->log->unsubscribe('message')->on(
-    message => sub {
-        my ($log, $level, @lines) = @_;
+    message => sub ($log, $level, @lines) {
         note "[$level] " . join "\n", @lines, '';
     });
-$mock_srv->helper(render_mutex => sub {
-        my ($self, %args) = @_;
+$mock_srv->helper(render_mutex => sub ($self, %) {
         my $name = $self->param('name') // '';
         return $self->render(status => 200, text => 'ok') if $name eq 
'lucky_lock';
         return $self->render(status => 404, text => 'error') if $name eq 
'prone_lock';
@@ -76,8 +74,7 @@
 my $routes = $mock_srv->routes;
 my $fake_api = $routes->any('/api/v1');
 my $wait_for_children_state;
-$fake_api->get('/mm/children' => sub {
-        my ($self) = @_;
+$fake_api->get('/mm/children' => sub ($self) {
         if ($wait_for_children_state) {
             return $self->render(json => {jobs => {1 => 'scheduled'}}) if 
$wait_for_children_state->{interations_left}--;
             return $self->render(json => {jobs => {1 => 
$wait_for_children_state->{state}}});
@@ -85,8 +82,7 @@
         return $self->render(status => 403, text => 'not authorized') if 
($self->tx->req->headers->header('X-API-JobToken') || '') ne 'fake-jobtoken';
         return $self->render(json => {jobs => [1, 2, 3]});
 });
-$fake_api->get('/mm/children/#state' => sub {
-        my ($self) = @_;
+$fake_api->get('/mm/children/#state' => sub ($self) {
         return $self->render(status => 404, json => {jobs => []}) if 
$self->stash('state') ne 'some-state';
         return $self->render(json => {jobs => [1]});
 });
@@ -102,22 +98,19 @@
 });
 $fake_api->post('/mutex' => sub { shift->render_mutex });
 $fake_api->post('/mutex/foo' => sub { shift->render(json => {some => 'mutex'}) 
});
-$fake_api->post('/mutex/#name' => sub {
-        my ($self) = @_;
+$fake_api->post('/mutex/#name' => sub ($self) {
         my $name = $self->param('name') // '';
         my $action = $self->param('action') // '';
         return $self->render(status => 200, text => 'ok') if ($name eq 
'lockable' && $action eq 'lock') || ($name eq 'unlockable' && $action eq 
'unlock');
         return $self->render_mutex;
 });
-$fake_api->post('/barrier' => sub {
-        my ($self) = @_;
+$fake_api->post('/barrier' => sub ($self) {
         my $name = $self->param('name') // '';
         my $tasks = $self->param('tasks') // '';
         return $self->render(status => 200, text => 'ok') if $name eq 
'lucky_barrier' && $tasks eq '41';
         return $self->render_mutex;
 });
-$fake_api->post('/barrier/#name' => sub {
-        my ($self) = @_;
+$fake_api->post('/barrier/#name' => sub ($self) {
         state $counter = 0;
         my $name = $self->param('name') // '';
         return $self->render(status => (++$counter % 3 == 0) == 0 ? 200 : 409, 
text => 'ok') if $name eq 'unblocked_next';
@@ -127,8 +120,7 @@
         return $self->render(status => 200, text => 'ok') if $name eq 
'check_dead_job_barrier' && ($self->param('check_dead_job') // '' eq '1');
         return $self->render_mutex;
 });
-$fake_api->delete('/barrier/#name' => sub {
-        my ($self) = @_;
+$fake_api->delete('/barrier/#name' => sub ($self) {
         my $name = $self->param('name') // '';
         return $self->render(status => 200, text => 'ok') if $name eq 
'deletable';
         return $self->render_mutex;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1781875657.efde779/t/32-console_proxy.t 
new/os-autoinst-5.1782140090.fe34efb/t/32-console_proxy.t
--- old/os-autoinst-5.1781875657.efde779/t/32-console_proxy.t   2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/t/32-console_proxy.t   2026-06-22 
16:54:50.000000000 +0200
@@ -33,8 +33,7 @@
 $console->mock('ret_list', sub { qw(a b c d); });
 $console->mock('ret_list_empty', sub { return; });
 $console->mock('ret_die', sub { die '!!Urgs!!'; });
-$console->mock('check_args', sub {
-        my ($self, @args) = @_;
+$console->mock('check_args', sub ($self, @args) {
         is_deeply \@args, $console_check_args, 'Got expected (' . join(',', 
@args) . ') arguments';
 });
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1781875657.efde779/xt/01-style.t 
new/os-autoinst-5.1782140090.fe34efb/xt/01-style.t
--- old/os-autoinst-5.1781875657.efde779/xt/01-style.t  2026-06-19 
15:27:37.000000000 +0200
+++ new/os-autoinst-5.1782140090.fe34efb/xt/01-style.t  2026-06-22 
16:54:50.000000000 +0200
@@ -20,7 +20,7 @@
 is qx{git grep -I -l '\\<spurt\\>' ':!xt/01-style.t'}, '', 'No deprecated 
"Mojo::File::spurt", use "spew" instead';
 is qx{git grep -I -l '^use testapi' backend/ consoles/}, '', 'No backend or 
console files use external facing testapi';
 is qx[git grep -l -e '^\\s*sub \\S\\+ [^(]\\+' --and --not -e 'sub [(\{]' 
--and --not -e 'sub \\S\\+\\s*[:(]' --and --not -e 'sub \\S\\+;' --and --not -e 
'# no:style:signatures' ':!external/' ':!t/48-testmodules-style.t'], '', 'All 
files use sub signatures everywhere (nameless and in-place definitions still 
allowed)';
-is qx[git grep -l -P 'sub\\s*\\{\\s*my\\s*\\(?\\\$' t/], '', 'Anonymous subs 
in tests should use signatures instead of manual unpacking of @_';
+is qx(perl -0777 -ne 'print "\$ARGV\n" if 
/sub\\s*\\{(?!\\s*#\\s*no:style:signatures)[^{}]*?\\bmy\\s+[^=]+=\\s*(?:\\x40_|shift\\b)/s'
 \$(git ls-files | grep -E '\\.(t|pm)\$' | grep -v '^external/')), '', 'All 
files use signatures in anonymous subroutines (no manual @_ unpacking or 
shift)';
 is qx{git grep -L '^#!.*perl' t/**.t}, '', 'All test files have shebang';
 is qx{git ls-files -s t/**.t | grep -v ^1007}, '', 'All test modules are 
executable';
 is qx{git grep -l '^use POSIX;'}, '', 'Use of bare POSIX import is 
discouraged, see https://perldoc.perl.org/POSIX';

++++++ os-autoinst.obsinfo ++++++
--- /var/tmp/diff_new_pack.jgGmXI/_old  2026-06-23 17:44:14.417356800 +0200
+++ /var/tmp/diff_new_pack.jgGmXI/_new  2026-06-23 17:44:14.421356940 +0200
@@ -1,5 +1,5 @@
 name: os-autoinst
-version: 5.1781875657.efde779
-mtime: 1781875657
-commit: efde77941e6607ce3412d53f4c4762f93909b801
+version: 5.1782140090.fe34efb
+mtime: 1782140090
+commit: fe34efb51586bd82c830ab3660764b2ac1ea4402
 

Reply via email to