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
