Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package openQA for openSUSE:Factory checked in at 2026-11-04 19:44:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Wed Nov 4 19:44:23 2026 rev:903 rq:1394758 version:5.1793722468.aa0c30fc Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-11-03 17:56:56.321765628 +0100 +++ /work/SRC/openSUSE:Factory/.openQA.new.3759290/openQA.changes 2026-11-04 19:44:30.965040926 +0100 @@ -1,0 +2,38 @@ +Tue Nov 03 16:14:56 UTC 2026 - [email protected] + +- Update to version 5.1793722468.aa0c30fc: + * docs: Mention the `max_connections` PostgreSQL setting + +------------------------------------------------------------------- +Tue Nov 03 05:54:25 UTC 2026 - [email protected] + +- Update to version 5.1793682485.ae04aa33: + * chore(deps): Dependency cron 2026-11-03 + +------------------------------------------------------------------- +Tue Nov 03 02:54:27 UTC 2026 - [email protected] + +- Update to version 5.1793674456.f48b3512: + * fix(auth): log stray OpenID callbacks as info + +------------------------------------------------------------------- +Mon Nov 02 18:54:24 UTC 2026 - [email protected] + +- Update to version 5.1793643862.95842f6a: + * feat(worker): Add upload inactivity timeout + * feat(worker): Add upload timeout via MAX_UPLOAD_TIME + +------------------------------------------------------------------- +Mon Nov 02 18:02:36 UTC 2026 - [email protected] + +- Update to version 5.1793641808.5ee26040: + * test(scalability): Avoid running dummy isotovideo in parallel + * test(scalability): Fix sporadic test failure + +------------------------------------------------------------------- +Mon Nov 02 16:54:26 UTC 2026 - [email protected] + +- Update to version 5.1793635054.fe4b39a3: + * feat(docs): add Ecosystem guide + +------------------------------------------------------------------- Old: ---- openQA-5.1793632686.cbcf1318.obscpio New: ---- openQA-5.1793722468.aa0c30fc.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.hTaxCs/_old 2026-11-04 19:44:33.781158805 +0100 +++ /var/tmp/diff_new_pack.hTaxCs/_new 2026-11-04 19:44:33.783158888 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1793632686.cbcf1318 +Version: 5.1793722468.aa0c30fc Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.hTaxCs/_old 2026-11-04 19:44:33.841161316 +0100 +++ /var/tmp/diff_new_pack.hTaxCs/_new 2026-11-04 19:44:33.843161400 +0100 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1793632686.cbcf1318 +Version: 5.1793722468.aa0c30fc Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.hTaxCs/_old 2026-11-04 19:44:33.891163409 +0100 +++ /var/tmp/diff_new_pack.hTaxCs/_new 2026-11-04 19:44:33.892163451 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1793632686.cbcf1318 +Version: 5.1793722468.aa0c30fc Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.hTaxCs/_old 2026-11-04 19:44:33.960166298 +0100 +++ /var/tmp/diff_new_pack.hTaxCs/_new 2026-11-04 19:44:33.962166381 +0100 @@ -104,7 +104,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1793632686.cbcf1318 +Version: 5.1793722468.aa0c30fc Release: 0 Summary: Framework for automated system-level testing (web-frontend, scheduler and tools) Group: Development/Tools/Other ++++++ openQA-5.1793632686.cbcf1318.obscpio -> openQA-5.1793722468.aa0c30fc.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/Contributing.md new/openQA-5.1793722468.aa0c30fc/docs/Contributing.md --- old/openQA-5.1793632686.cbcf1318/docs/Contributing.md 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/Contributing.md 2026-11-03 17:14:28.000000000 +0100 @@ -109,6 +109,10 @@ about the complete project but without a guarantee for correctness: <https://deepwiki.com/os-autoinst/openQA> +The surrounding tools, public instances, learning resources, and community +channels are listed in the [Ecosystem guide](Ecosystem.md#ecosystem), which is +also the place to add your own instance, tool, blog, or video. + ## Technologies Most in openQA, from `os-autoinst` to the web frontend and from the tests to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/Ecosystem.md new/openQA-5.1793722468.aa0c30fc/docs/Ecosystem.md --- old/openQA-5.1793632686.cbcf1318/docs/Ecosystem.md 1970-01-01 01:00:00.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/Ecosystem.md 2026-11-03 17:14:28.000000000 +0100 @@ -0,0 +1,100 @@ +<a id="ecosystem"></a> + +# Ecosystem + +## Introduction + +openQA is more than a single tool. The core is two repositories, +[openQA](https://github.com/os-autoinst/openQA) (web UI, scheduler, worker) +and [os-autoinst](https://github.com/os-autoinst/os-autoinst) (the backend +that runs the system under test). The ecosystem is everything built around +them: the many other projects in the +[os-autoinst organization](https://github.com/os-autoinst), public instances, +learning resources, and a community. This page is a discovery guide for users +and a place for contributors to add their instance, project, blog, or video +with a pull request. + +## Related projects + +Beyond the core repos, the +[os-autoinst organization](https://github.com/os-autoinst) hosts many more +projects, e.g. the test distributions and needles: + +- [os-autoinst-distri-opensuse](https://github.com/os-autoinst/os-autoinst-distri-opensuse): + the openSUSE and SLE tests, conducted on + [openqa.opensuse.org](https://openqa.opensuse.org). +- [os-autoinst-distri-example](https://github.com/os-autoinst/os-autoinst-distri-example): + a template to start writing tests for a new operating system. +- [os-autoinst-needles-opensuse](https://github.com/os-autoinst/os-autoinst-needles-opensuse): + reference images not already included in the test distribution. + +The list is intentionally short: the organization contains many more projects, +so browse it directly rather than enumerating them here, which would quickly +go stale. + +Third-party tools live outside the organization, e.g. +[Haystack](https://gitlab.gnome.org/jwharm/Haystack) +([Flathub](https://flathub.org/en/apps/app.drey.Haystack)), a GNOME app to +edit openQA needles. + +## Public instances + +Anyone can run an openQA instance, so this is a non-exhaustive list of known +public instances, not an authoritative registry. openQA is one part of each +project's test infrastructure, not necessarily its primary quality gateway. +Operators who prefer not to be listed can request removal. + +- [openqa.opensuse.org](https://openqa.opensuse.org): openSUSE. +- [openqa.suse.de](https://openqa.suse.de): SUSE SLE (private). +- [Fedora](https://openqa.fedoraproject.org/): Fedora. +- [AlmaLinux](http://openqa.almalinux.org/): AlmaLinux. +- [Debian](https://openqa.debian.net/): Debian. +- [Qubes OS](https://openqa.qubes-os.org/): Qubes OS. +- [Endless OS](https://openqa.endlessm.com/): Endless OS. +- [GNOME](https://openqa.gnome.org): the GNOME project. +- KDE: see the + [KDE blog post on openQA testing in KDE Linux](https://blogs.kde.org/2026-07-13/openqa-testing-in-kde-linux/). +- openSUSE KDE contributors: + [group 23](https://openqa.opensuse.org/group_overview/23). +- openSUSE GNOME contributors: + [group 35](https://openqa.opensuse.org/group_overview/35). +- OBS developers: + [parent group 7](https://openqa.opensuse.org/parent_group_overview/7#grouped_by_build). +- wicked developers: + [wicked-ci](https://gitlab.suse.de/wicked-maintainers/wicked-ci#openqa). +- openQA-in-openQA (the team's own tests): + [group 24](https://openqa.opensuse.org/group_overview/24). + +See also the +[openSUSE:OpenQA/Partners](https://en.opensuse.org/openSUSE:OpenQA/Partners) +page. + +## Learning resources + +- [openqa-bites](https://openqa-bites.github.io/): stories and tutorials from + an openQA developer. +- [Liv's blog](https://liv.pink/): guides and hints from an openQA developer. +- [EuroLinux on openQA](https://en.euro-linux.com/blog/openqa-or-how-we-test-eurolinux/): + how EuroLinux tests with openQA. +- [Codethink on automated Linux kernel testing](https://www.codethink.co.uk/articles/2021/automated-linux-kernel-testing/). +- [SUSE QE Tools Workshop](https://youtube.com/playlist?list=PL6TdzZgl22k7GD62L292tn3dBTg1JPmVF): + a playlist covering many topics. +- [Conference presentations on media.ccc.de](https://media.ccc.de/search/?q=openQA). +- [Various videos on YouTube](https://www.youtube.com/results?search_query=openQA). +- [Linux Inlaws podcast featuring openQA](https://archive.org/download/LI_S02E16_OpenQA__0D80/LI_S02E16_OpenQA_.mp3). +- [Testing in a Box: Streamlining Embedded Systems Testing](https://www.youtube.com/watch?v=Q2Y1YQ8V5gk). + +## Community and contributing + +- The [openQAv3 project](https://progress.opensuse.org/projects/openqav3) in + openSUSE's project management tool coordinates the development roadmap. +- The QA testing monthly call: + [GNOME QA testing monthly call](https://gitlab.gnome.org/GNOME/openqa-tests/-/wikis/QA-testing-monthly-call). +- The official openSUSE presence on + [Reddit](https://www.reddit.com/r/opensuse/) and openQA on + [X/Twitter by openQAhq](https://twitter.com/openQAhq). + +If you are contributing to openQA by running an instance, writing a tool, a +post, a video, or a podcast, add a link to the relevant section above with a +pull request to +[`docs/Ecosystem.md`](https://github.com/os-autoinst/openQA/blob/master/docs/Ecosystem.md). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/GettingStarted.md new/openQA-5.1793722468.aa0c30fc/docs/GettingStarted.md --- old/openQA-5.1793632686.cbcf1318/docs/GettingStarted.md 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/GettingStarted.md 2026-11-03 17:14:28.000000000 +0100 @@ -40,6 +40,8 @@ existing instance see the [Users Guide](UsersGuide.md#usersguide). More advanced topics can be found in other documents. All documents are also available in the [official repository](https://github.com/os-autoinst/openQA). +For the surrounding tools, public instances, learning resources, and community +see the [Ecosystem guide](Ecosystem.md#ecosystem). ## Architecture @@ -188,8 +190,9 @@ via `record_soft_failure` (from os-autoinst) or a job failure has been ignored explicitly via a [job label](UsersGuide.md#labels). -- **timeout_exceeded** The job was aborted because `MAX_JOB_TIME` or `MAX_SETUP_TIME` - has been exceeded, see [Changing timeout](WritingTests.md#changing_timeouts) for details. +- **timeout_exceeded** The job was aborted because `MAX_JOB_TIME`, `MAX_SETUP_TIME`, + `MAX_UPLOAD_TIME`, or `UPLOAD_INACTIVITY_TIMEOUT` has been exceeded, see + [Changing timeout](WritingTests.md#changing_timeouts) for details. - **skipped** Dependencies failed so the job was not started. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/Installing.md new/openQA-5.1793722468.aa0c30fc/docs/Installing.md --- old/openQA-5.1793632686.cbcf1318/docs/Installing.md 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/Installing.md 2026-11-03 17:14:28.000000000 +0100 @@ -556,6 +556,11 @@ value format can be found in the [DBD::Pg documentation](https://metacpan.org/pod/DBD::Pg#DBI-Class-Methods). +No specific PostgreSQL configuration is required. The default connection limit +of 100 is sufficient for most openQA instances. If you connect additional +tooling like Telegraf, you might need to increase `max_connections` in +`/var/lib/pgsql/data/postgresql.conf`, though. + #### Example for connecting to local PostgreSQL database ```ini diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/WritingTests.md new/openQA-5.1793722468.aa0c30fc/docs/WritingTests.md --- old/openQA-5.1793632686.cbcf1318/docs/WritingTests.md 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/WritingTests.md 2026-11-03 17:14:28.000000000 +0100 @@ -498,10 +498,15 @@ The download of assets, synchronization of tests and other setup tasks do **not** count into `MAX_JOB_TIME`. However, the setup time is limited by default -to one hour. This can be changed by setting `MAX_SETUP_TIME`. The variable -`TIMEOUT_SCALE` allows to scale `MAX_JOB_TIME` and timeouts within the backend, -for example the [test API](#api). This is supposed to be set within the worker -settings on slow worker hosts. It has no influence on the video setting. +to one hour. This can be changed by setting `MAX_SETUP_TIME`. Similarly, the +teardown and upload phase is limited by default to 4 hours, which can be +changed by setting `MAX_UPLOAD_TIME`. Additionally, an inactivity timeout of +by default 20 minutes applies during upload, which can be changed via +`UPLOAD_INACTIVITY_TIMEOUT`. The variable `TIMEOUT_SCALE` allows to scale +`MAX_JOB_TIME`, `MAX_UPLOAD_TIME`, `UPLOAD_INACTIVITY_TIMEOUT`, and timeouts +within the backend, for example the [test API](#api). This is supposed to be +set within the worker settings on slow worker hosts. It has no influence on the +video setting. ### Enabling UEFI with QEMU diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/docs/index.md new/openQA-5.1793722468.aa0c30fc/docs/index.md --- old/openQA-5.1793632686.cbcf1318/docs/index.md 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/docs/index.md 2026-11-03 17:14:28.000000000 +0100 @@ -7,6 +7,7 @@ include::Pitfalls.md[] include::Networking.md[] include::Contributing.md[] +include::Ecosystem.md[] include::Branding.md[] include::ContainerizedSetup.md[] include::PrivacyPolicy.md[] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/lib/OpenQA/Constants.pm new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/Constants.pm --- old/openQA-5.1793632686.cbcf1318/lib/OpenQA/Constants.pm 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/Constants.pm 2026-11-03 17:14:28.000000000 +0100 @@ -93,6 +93,12 @@ # The max. time the job setup (asset caching, test syncing) is allowed to take before the worker stops it. use constant DEFAULT_MAX_SETUP_TIME => ONE_HOUR; +# The max. time uploading assets and logs is allowed to take before the worker stops it. +use constant DEFAULT_MAX_UPLOAD_TIME => 4 * ONE_HOUR; + +# The max. time an upload is allowed to be inactive before the worker aborts it. +use constant DEFAULT_UPLOAD_INACTIVITY_TIMEOUT => 20 * ONE_MINUTE; + # The smallest time difference of database timestamps we usually distinguish in seconds # note: PostgreSQL actually provides a higher accuracy for the timestamp type. However, # the automatic timestamp handling provided by DBIx only stores whole seconds. The @@ -135,7 +141,7 @@ MAX_TIMER MIN_TIMER DEFAULT_MAX_JOB_TIME DEFAULT_MAX_SETUP_TIME - DEFAULT_UPLOAD_CHUNK_SIZE API_KEY_COMMENT_MAX_LENGTH + DEFAULT_MAX_UPLOAD_TIME DEFAULT_UPLOAD_INACTIVITY_TIMEOUT DEFAULT_UPLOAD_CHUNK_SIZE API_KEY_COMMENT_MAX_LENGTH DB_TIMESTAMP_ACCURACY VIDEO_FILE_NAME_START VIDEO_FILE_NAME_REGEX FRAGMENT_REGEX diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/lib/OpenQA/WebAPI/Auth/OpenID.pm new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/WebAPI/Auth/OpenID.pm --- old/openQA-5.1793632686.cbcf1318/lib/OpenQA/WebAPI/Auth/OpenID.pm 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/WebAPI/Auth/OpenID.pm 2026-11-03 17:14:28.000000000 +0100 @@ -111,11 +111,17 @@ $csr->handle_server_response( not_openid => sub () { my $op_uri = $params{'openid.op_endpoint'}; - my $msg - = $op_uri - ? "OpenID provider '$op_uri' returned invalid data on a login attempt" - : 'OpenID response returned no provider endpoint'; - $err_handler->('Failed to login', $msg); + if ($op_uri) { + $err_handler->('Failed to login', "OpenID provider '$op_uri' returned invalid data on a login attempt"); + } + elsif (grep { /^openid\./ } keys %params) { + $err_handler->('Failed to login', 'OpenID response returned no provider endpoint'); + } + else { + # A callback hit without any openid.* parameters is a stray or scanner request, not a failed login + $c->app->log->info('OpenID: Response callback invoked without OpenID parameters'); + return (error => 0); + } }, setup_needed => sub ($setup_url) { # Redirect the user to $setup_url diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/lib/OpenQA/Worker/Job.pm new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/Worker/Job.pm --- old/openQA-5.1793632686.cbcf1318/lib/OpenQA/Worker/Job.pm 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/lib/OpenQA/Worker/Job.pm 2026-11-03 17:14:28.000000000 +0100 @@ -5,8 +5,8 @@ use Mojo::Base 'Mojo::EventEmitter', -signatures; use OpenQA::Constants qw(DEFAULT_MAX_JOB_TIME DEFAULT_UPLOAD_CHUNK_SIZE DEFAULT_MAX_SETUP_TIME WORKER_COMMAND_ABORT - WORKER_COMMAND_QUIT WORKER_COMMAND_CANCEL WORKER_COMMAND_OBSOLETE WORKER_SR_BROKEN WORKER_SR_SETUP_FAILURE - WORKER_SR_API_FAILURE WORKER_SR_TIMEOUT WORKER_SR_DONE WORKER_SR_DIED DOWNLOAD_PERMISSION); + DEFAULT_MAX_UPLOAD_TIME DEFAULT_UPLOAD_INACTIVITY_TIMEOUT WORKER_COMMAND_QUIT WORKER_COMMAND_CANCEL WORKER_COMMAND_OBSOLETE + WORKER_SR_BROKEN WORKER_SR_SETUP_FAILURE WORKER_SR_API_FAILURE WORKER_SR_TIMEOUT WORKER_SR_DONE WORKER_SR_DIED DOWNLOAD_PERMISSION); use OpenQA::Jobs::Constants; use OpenQA::Worker::Engines::isotovideo; use OpenQA::Worker::Isotovideo::Client; @@ -98,7 +98,8 @@ } sub DESTROY ($self) { - $self->_remove_timer unless ${^GLOBAL_PHASE} eq 'DESTRUCT'; + $self->_remove_timer([qw(_upload_results_timer _timeout_timer _upload_timeout_timer _upload_inactivity_timer)]) + unless ${^GLOBAL_PHASE} eq 'DESTRUCT'; } sub _remove_timer ($self, $timer_names = undef) { @@ -120,6 +121,7 @@ $event_data->{job} = $self; $event_data->{status} = $status; $self->{_status} = $status; + $self->_remove_timer([qw(_upload_timeout_timer _upload_inactivity_timer)]) if $status eq 'stopped'; $self->emit(status_changed => $event_data); } @@ -194,11 +196,19 @@ sub _compute_timeouts ($job_settings) { my $max_job_time = $job_settings->{MAX_JOB_TIME}; my $max_setup_time = $job_settings->{MAX_SETUP_TIME}; + my $max_upload_time = $job_settings->{MAX_UPLOAD_TIME}; + my $upload_inactivity_timeout = $job_settings->{UPLOAD_INACTIVITY_TIMEOUT}; my $timeout_scale = $job_settings->{TIMEOUT_SCALE}; $max_job_time = DEFAULT_MAX_JOB_TIME unless looks_like_number $max_job_time; $max_setup_time = DEFAULT_MAX_SETUP_TIME unless looks_like_number $max_setup_time; - $max_job_time *= $timeout_scale if looks_like_number $timeout_scale; - return ($max_job_time, $max_setup_time); + $max_upload_time = DEFAULT_MAX_UPLOAD_TIME unless looks_like_number $max_upload_time; + $upload_inactivity_timeout = DEFAULT_UPLOAD_INACTIVITY_TIMEOUT unless looks_like_number $upload_inactivity_timeout; + if (looks_like_number $timeout_scale) { + $max_job_time *= $timeout_scale; + $max_upload_time *= $timeout_scale; + $upload_inactivity_timeout *= $timeout_scale; + } + return ($max_job_time, $max_setup_time, $max_upload_time, $upload_inactivity_timeout); } sub _handle_timeout ($self, $engine = undef) { @@ -219,6 +229,53 @@ $self->{_timeout_timer} = $loop->timer($timeout => sub { $self->_handle_timeout($engine) }); } +sub _set_upload_timeout ($self, $timeout = undef) { + $timeout //= $self->{_max_upload_time} // (_compute_timeouts($self->settings // {}))[2]; + my $loop = Mojo::IOLoop->singleton; + if (my $current_timer = $self->{_upload_timeout_timer}) { $loop->remove($current_timer) } + $self->{_upload_timeout_timer} = $loop->timer($timeout => sub { $self->_handle_upload_timeout }); +} + +sub _set_upload_inactivity_timeout ($self, $timeout = undef) { + $timeout //= $self->{_upload_inactivity_timeout} // (_compute_timeouts($self->settings // {}))[3]; + my $loop = Mojo::IOLoop->singleton; + if (my $current_timer = $self->{_upload_inactivity_timer}) { $loop->remove($current_timer) } + $self->{_upload_inactivity_timer} = $loop->timer($timeout => sub { $self->_handle_upload_inactivity_timeout }); +} + +sub _reset_upload_inactivity_timeout ($self) { + return unless $self->status eq 'stopping'; + $self->_set_upload_inactivity_timeout; +} + +sub _terminate_upload_due_to_timeout ($self, $flag, $message) { + my $job_id = $self->id; + log_warning("Job $job_id stopped because $message"); + $self->_remove_timer([qw(_upload_results_timer _timeout_timer _upload_timeout_timer _upload_inactivity_timer)]); + $self->{$flag} = 1; + if (my $proc = delete $self->{_upload_subprocess}) { + if (my $pid = $proc->pid) { + kill 'KILL', $pid; + } + } + if (my $proc = delete $self->{_upload_images_subprocess}) { + if (my $pid = $proc->pid) { + kill 'KILL', $pid; + } + } + $self->conclude_upload_if_ongoing; + $self->_stop_step_5_finalize(WORKER_SR_TIMEOUT, {result => TIMEOUT_EXCEEDED}); +} + +sub _handle_upload_timeout ($self) { + $self->_terminate_upload_due_to_timeout(_upload_timeout => 'upload exceeded MAX_UPLOAD_TIME'); +} + +sub _handle_upload_inactivity_timeout ($self) { + $self->_terminate_upload_due_to_timeout( + _upload_inactivity_timeout_exceeded => 'upload exceeded UPLOAD_INACTIVITY_TIMEOUT'); +} + sub start ($self) { my $id = $self->id; my $info = $self->info; @@ -263,7 +320,10 @@ $ENV{OPENQA_SHAREDIR} = $client->working_directory; # stop setup if timeout has been exceeded - my ($max_job_time, $max_setup_time) = _compute_timeouts($job_settings); + my ($max_job_time, $max_setup_time, $max_upload_time, $upload_inactivity_timeout) + = _compute_timeouts($job_settings); + $self->{_max_upload_time} = $max_upload_time; + $self->{_upload_inactivity_timeout} = $upload_inactivity_timeout; $self->_set_timeout($max_setup_time); # start isotovideo @@ -337,6 +397,8 @@ $self->_set_status(stopping => {reason => $reason}); $self->_remove_timer(['_timeout_timer']); + $self->_set_upload_timeout; + $self->_set_upload_inactivity_timeout; $self->_stop_step_2_post_status( $reason, @@ -365,7 +427,10 @@ json => { status => {uploading => 1, worker_id => $client->worker_id}, }, - callback => $callback, + callback => sub (@args) { + $self->_reset_upload_inactivity_timeout; + $callback->(@args); + }, ); } @@ -374,7 +439,12 @@ # skip if isotovideo not running anymore (e.g. when isotovideo just exited on its own) return Mojo::IOLoop->next_tick($callback) unless $self->is_backend_running; - $self->isotovideo_client->stop_gracefully($reason, $callback); + $self->isotovideo_client->stop_gracefully( + $reason, + sub (@args) { + $self->_reset_upload_inactivity_timeout; + $callback->(@args); + }); } sub _redact_file ($file_path, $file_name) { @@ -398,8 +468,10 @@ # upload logs and assets return Mojo::IOLoop->next_tick(sub { $self->_stop_step_5_1_upload($reason, $callback) }) if $reason eq WORKER_COMMAND_QUIT || $reason eq WORKER_COMMAND_ABORT || $reason eq WORKER_SR_API_FAILURE; - Mojo::IOLoop->subprocess( - sub { + my $upload_subprocess = $self->{_upload_subprocess} = Mojo::IOLoop->subprocess( + sub ($subprocess) { + local $self->{_current_subprocess} = $subprocess; + # upload ulogs my @uploaded_logfiles = glob "$pooldir/ulogs/*"; for my $file (@uploaded_logfiles) { @@ -413,6 +485,7 @@ $reason = WORKER_SR_API_FAILURE; last; } + $subprocess->progress('file'); } # upload assets created by successful jobs @@ -429,6 +502,7 @@ asset => $dir, ); last unless ($upload_result = $self->_upload_log_file_or_asset(\%upload_parameter)); + $subprocess->progress('file'); } if (!$upload_result) { $reason = WORKER_SR_API_FAILURE; @@ -461,14 +535,19 @@ $reason = WORKER_SR_API_FAILURE; last; } + $subprocess->progress('file'); } return $reason; }, sub { my ($subprocess, $err, $reason) = @_; + delete $self->{_upload_subprocess}; + return if $self->{_upload_timeout} || $self->{_upload_inactivity_timeout_exceeded}; + $self->_reset_upload_inactivity_timeout; log_error("Upload subprocess error: $err") if $err; $self->_stop_step_5_1_upload($reason // WORKER_SR_API_FAILURE, $callback); }); + $upload_subprocess->on(progress => sub { $self->_reset_upload_inactivity_timeout }); } sub _stop_step_5_1_upload ($self, $reason, $callback) { @@ -478,11 +557,13 @@ $self->emit(uploading_logs_and_assets_concluded => {}); # ensure no asynchronous "side tasks" are started anymore automatically at this point $self->_remove_timer; + $self->_reset_upload_inactivity_timeout; # postpone any further actions until a possibly ongoing result upload has been concluded $self->_invoke_after_result_upload(sub { $self->_stop_step_5_2_upload($reason, $callback); }); } sub _stop_step_5_2_upload ($self, $reason, $callback) { + $self->_reset_upload_inactivity_timeout; # do final status upload for selected reasons my $job_id = $self->id; @@ -556,8 +637,12 @@ sub _format_reason ($self, $state, $result, $reason) { # format stop reasons from the worker itself - return 'timeout: ' . ($self->{_engine} ? 'test execution exceeded MAX_JOB_TIME' : 'setup exceeded MAX_SETUP_TIME') - if $reason eq WORKER_SR_TIMEOUT; + if ($reason eq WORKER_SR_TIMEOUT) { + return 'timeout: upload exceeded UPLOAD_INACTIVITY_TIMEOUT' if $self->{_upload_inactivity_timeout_exceeded}; + return 'timeout: upload exceeded MAX_UPLOAD_TIME' if $self->{_upload_timeout}; + return 'timeout: ' + . ($self->{_engine} ? 'test execution exceeded MAX_JOB_TIME' : 'setup exceeded MAX_SETUP_TIME'); + } return "$self->{_setup_error_category}: $self->{_setup_error}" if $reason eq WORKER_SR_SETUP_FAILURE; if ($reason eq WORKER_SR_API_FAILURE) { my $last_client_error = $self->client->last_error; @@ -704,6 +789,7 @@ } sub _conclude_upload ($self, $callback = undef, $result = {}) { + $self->_reset_upload_inactivity_timeout if $self->status eq 'stopping'; my $existing_callback = delete $self->{_upload_results_callback}; $callback //= $existing_callback; $self->{_is_uploading_results} = 0; @@ -832,6 +918,7 @@ \%status, sub { my ($status_post_res) = @_; + $self->_reset_upload_inactivity_timeout; # handle error occurred when posting the status if (!$status_post_res) { @@ -922,8 +1009,14 @@ } sub _upload_results_step_2_upload_images ($self, $callback) { - Mojo::IOLoop->subprocess(sub { $self->_upload_results_step_2_1_upload_images }, - sub ($subprocess, $error, @args) { $self->_upload_results_step_2_2_upload_images($callback, $error) }); + $self->{_upload_images_subprocess} = Mojo::IOLoop->subprocess( + sub { $self->_upload_results_step_2_1_upload_images }, + sub ($subprocess, $error, @args) { + delete $self->{_upload_images_subprocess}; + return if $self->{_upload_timeout} || $self->{_upload_inactivity_timeout_exceeded}; + $self->_reset_upload_inactivity_timeout; + $self->_upload_results_step_2_2_upload_images($callback, $error); + }); } sub _upload_results_step_3_finalize ($self, $upload_up_to, $callback) { @@ -1006,6 +1099,7 @@ my $retries = $global_settings->{UPLOAD_RETRIES} // 10; my $local_upload = $global_settings->{LOCAL_UPLOAD} // 1; my $ua = $self->client->ua; + my $subprocess = $self->{_current_subprocess}; my @channels_worker_only = ('worker'); my @channels_both = qw(autoinst worker); my $error; @@ -1016,6 +1110,7 @@ 'upload_local.prepare' => sub ($upload) { log_info("$filename: local upload (no chunks needed)", channels => \@channels_worker_only, default => 1); chmod DOWNLOAD_PERMISSION, $file; + $subprocess->progress('local') if $subprocess; }); $ua->upload->once( 'upload_chunk.prepare' => sub ($upload, $pieces) { @@ -1037,6 +1132,7 @@ channels => ($show_in_autoinst_log ? \@channels_both : \@channels_worker_only), default => 1 ); + $subprocess->progress('chunk') if $subprocess; }); my $response_cb = sub ($upload, $tx, $retries) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/t/03-auth-openid.t new/openQA-5.1793722468.aa0c30fc/t/03-auth-openid.t --- old/openQA-5.1793632686.cbcf1318/t/03-auth-openid.t 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/t/03-auth-openid.t 2026-11-03 17:14:28.000000000 +0100 @@ -34,13 +34,32 @@ my $params = Test::MockObject->new->set_always(pairs => ['openid.op_endpoint', 'https://www.opensuse.org/openid/']); my $req = Test::MockObject->new->set_always(params => $params)->set_always(url => $url); -my $log = Test::MockObject->new->set_true('error'); +my $log = Test::MockObject->new->set_true('error')->set_true('debug'); my $app = Test::MockObject->new->set_always(config => {})->set_always(log => $log); $c->set_always(return_page => undef)->set_always(req => $req)->set_always(app => $app)->set_true('flash'); Test::MockObject->new->set_always(params => $params)->set_always(url => $url); is +OpenQA::WebAPI::Auth::OpenID::auth_response($c), 0, 'can call auth_response'; $c->app->log->called_ok('error', 'an error was logged for call without proper config'); +my $empty_params = Test::MockObject->new->set_always(pairs => []); +$req->set_always(params => $empty_params); +$log->set_true('info'); +$log->clear; +$c->clear; +is +OpenQA::WebAPI::Auth::OpenID::auth_response($c), 0, 'can call auth_response without OpenID parameters'; +$c->app->log->called_ok('info', 'info logged when response invoked without OpenID parameters'); +ok !$c->app->log->called('error'), 'no error logged for direct visit without OpenID parameters'; +ok !$c->called('flash'), 'no flash message shown for direct visit without OpenID parameters'; + +my $missing_endpoint_params = Test::MockObject->new->set_always(pairs => ['openid.foo', 'bar']); +$req->set_always(params => $missing_endpoint_params); +$log->clear; +$c->clear; +is +OpenQA::WebAPI::Auth::OpenID::auth_response($c), 0, + 'can call auth_response with OpenID parameters lacking endpoint'; +$c->app->log->called_ok('error', 'an error was logged when OpenID response lacks provider endpoint'); +$c->called_ok('flash', 'flash message shown when OpenID response lacks provider endpoint'); + my $mock_openid_consumer = Test::MockModule->new('Net::OpenID::Consumer'); $mock_openid_consumer->redefine( handle_server_response => sub ($self, %res_handlers) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/t/24-worker-jobs.t new/openQA-5.1793722468.aa0c30fc/t/24-worker-jobs.t --- old/openQA-5.1793632686.cbcf1318/t/24-worker-jobs.t 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/t/24-worker-jobs.t 2026-11-03 17:14:28.000000000 +0100 @@ -28,9 +28,10 @@ use Mojo::UserAgent; use Mojo::URL; use Mojo::IOLoop; -use OpenQA::Constants qw(DEFAULT_MAX_JOB_TIME DEFAULT_MAX_SETUP_TIME DEFAULT_UPLOAD_CHUNK_SIZE WORKER_COMMAND_CANCEL - WORKER_COMMAND_QUIT WORKER_COMMAND_OBSOLETE WORKER_SR_SETUP_FAILURE WORKER_SR_TIMEOUT WORKER_EC_ASSET_FAILURE - WORKER_EC_CACHE_FAILURE WORKER_SR_API_FAILURE WORKER_SR_DIED WORKER_SR_DONE WORKER_SR_BROKEN); +use OpenQA::Constants qw(DEFAULT_MAX_JOB_TIME DEFAULT_MAX_SETUP_TIME DEFAULT_MAX_UPLOAD_TIME + DEFAULT_UPLOAD_INACTIVITY_TIMEOUT DEFAULT_UPLOAD_CHUNK_SIZE WORKER_COMMAND_CANCEL WORKER_COMMAND_QUIT + WORKER_COMMAND_OBSOLETE WORKER_SR_SETUP_FAILURE WORKER_SR_TIMEOUT WORKER_EC_ASSET_FAILURE WORKER_EC_CACHE_FAILURE + WORKER_SR_API_FAILURE WORKER_SR_DIED WORKER_SR_DONE WORKER_SR_BROKEN); use OpenQA::Worker::Job; use OpenQA::Worker::Settings; use OpenQA::Test::FakeWebSocketTransaction; @@ -1432,23 +1433,38 @@ # uploaded results for might still be in progress and still needs to be considered on further uploads. }; -subtest 'computing max job time and max setup time' => sub { +subtest 'computing timeouts' => sub { my %settings; - my ($max_job_time, $max_setup_time) = OpenQA::Worker::Job::_compute_timeouts(\%settings); + my ($max_job_time, $max_setup_time, $max_upload_time, $upload_inactivity_timeout) + = OpenQA::Worker::Job::_compute_timeouts(\%settings); is $max_job_time, DEFAULT_MAX_JOB_TIME, 'default scenario: default max job time)'; is $max_setup_time, DEFAULT_MAX_SETUP_TIME, 'default scenario: default max setup time)'; + is $max_upload_time, DEFAULT_MAX_UPLOAD_TIME, 'default scenario: default max upload time'; + is $upload_inactivity_timeout, DEFAULT_UPLOAD_INACTIVITY_TIMEOUT, + 'default scenario: default upload inactivity timeout'; $settings{MAX_JOB_TIME} = sprintf '%d', DEFAULT_MAX_JOB_TIME / 2; $settings{MAX_SETUP_TIME} = sprintf '%d', DEFAULT_MAX_SETUP_TIME / 3; - ($max_job_time, $max_setup_time) = OpenQA::Worker::Job::_compute_timeouts(\%settings); + $settings{MAX_UPLOAD_TIME} = sprintf '%d', DEFAULT_MAX_UPLOAD_TIME / 4; + $settings{UPLOAD_INACTIVITY_TIMEOUT} = sprintf '%d', DEFAULT_UPLOAD_INACTIVITY_TIMEOUT / 5; + ($max_job_time, $max_setup_time, $max_upload_time, $upload_inactivity_timeout) + = OpenQA::Worker::Job::_compute_timeouts(\%settings); is $max_job_time, DEFAULT_MAX_JOB_TIME / 2, 'short scenario: max job time taken from settings'; is $max_setup_time, DEFAULT_MAX_SETUP_TIME / 3, 'short scenario: max setup time taken from settings'; + is $max_upload_time, DEFAULT_MAX_UPLOAD_TIME / 4, 'short scenario: max upload time taken from settings'; + is $upload_inactivity_timeout, DEFAULT_UPLOAD_INACTIVITY_TIMEOUT / 5, + 'short scenario: upload inactivity timeout taken from settings'; $settings{TIMEOUT_SCALE} = '4'; delete $settings{MAX_SETUP_TIME}; - ($max_job_time, $max_setup_time) = OpenQA::Worker::Job::_compute_timeouts(\%settings); + delete $settings{MAX_UPLOAD_TIME}; + delete $settings{UPLOAD_INACTIVITY_TIMEOUT}; + ($max_job_time, $max_setup_time, $max_upload_time, $upload_inactivity_timeout) + = OpenQA::Worker::Job::_compute_timeouts(\%settings); is $max_job_time, DEFAULT_MAX_JOB_TIME * 2, 'max job time scaled'; is $max_setup_time, DEFAULT_MAX_SETUP_TIME, 'max setup time not scaled'; + is $max_upload_time, DEFAULT_MAX_UPLOAD_TIME * 4, 'max upload time scaled'; + is $upload_inactivity_timeout, DEFAULT_UPLOAD_INACTIVITY_TIMEOUT * 4, 'upload inactivity timeout scaled'; is_deeply [sort keys %settings], [qw(MAX_JOB_TIME TIMEOUT_SCALE)], 'no extra settings added so far'; }; @@ -1465,6 +1481,88 @@ 'timeout logged'; }; +subtest 'handling upload timeout' => sub { + my ($job, $event_data) = OpenQA::Worker::Job->new($worker, $client, {id => 2, URL => $engine_url}); + $job->_set_status(stopping => {reason => WORKER_SR_DONE}); + $job->on(status_changed => sub ($job, $data) { $event_data = $data }); + + my $proc = Test::MockObject->new->set_always(pid => 1_234_567); + $job->{_upload_subprocess} = $proc; + my $img_proc = Test::MockObject->new->set_always(pid => 1_234_568); + $job->{_upload_images_subprocess} = $img_proc; + $job->{_is_uploading_results} = 1; + + combined_like { + $job->_handle_upload_timeout; + wait_until_job_status_ok($job, 'stopped'); + } + qr/stopped because upload exceeded MAX_UPLOAD_TIME/, 'upload timeout logged'; + + is $job->status, 'stopped', 'job is stopped'; + is $event_data->{reason}, WORKER_SR_TIMEOUT, 'stop reason is timeout'; + ok !defined $job->{_upload_subprocess}, 'upload subprocess cleaned up'; + ok !defined $job->{_upload_images_subprocess}, 'upload images subprocess cleaned up'; + ok !$job->is_uploading_results, 'ongoing upload concluded'; + is $job->_format_reason(undef, TIMEOUT_EXCEEDED, WORKER_SR_TIMEOUT), + 'timeout: upload exceeded MAX_UPLOAD_TIME', 'format reason for upload timeout'; + + subtest 'test inactivity timeout' => sub { + ($job, $event_data) = OpenQA::Worker::Job->new($worker, $client, {id => 3, URL => $engine_url}); + $job->_set_status(stopping => {reason => WORKER_SR_DONE}); + $job->on(status_changed => sub ($job, $data) { $event_data = $data }); + $job->{_upload_subprocess} = Test::MockObject->new->set_always(pid => 1_234_569); + $job->{_is_uploading_results} = 1; + + combined_like { + $job->_handle_upload_inactivity_timeout; + wait_until_job_status_ok($job, 'stopped'); + } + qr/stopped because upload exceeded UPLOAD_INACTIVITY_TIMEOUT/, 'upload inactivity timeout logged'; + + is $job->status, 'stopped', 'job is stopped'; + is $event_data->{reason}, WORKER_SR_TIMEOUT, 'stop reason is timeout'; + ok !defined $job->{_upload_subprocess}, 'upload subprocess cleaned up'; + ok !$job->is_uploading_results, 'ongoing upload concluded'; + is $job->_format_reason(undef, TIMEOUT_EXCEEDED, WORKER_SR_TIMEOUT), + 'timeout: upload exceeded UPLOAD_INACTIVITY_TIMEOUT', 'format reason for upload inactivity timeout'; + }; + + subtest 'test reset inactivity timeout' => sub { + my $job4 = OpenQA::Worker::Job->new($worker, $client, {id => 4, URL => $engine_url}); + $job4->_set_status(stopping => {reason => WORKER_SR_DONE}); + $job4->_set_upload_inactivity_timeout(100); + my $old_timer = $job4->{_upload_inactivity_timer}; + ok $old_timer, 'inactivity timer set'; + $job4->_reset_upload_inactivity_timeout; + my $new_timer = $job4->{_upload_inactivity_timer}; + ok $new_timer, 'inactivity timer reset'; + isnt $new_timer, $old_timer, 'timer replaced with new one'; + $job4->_set_status(stopped => {}); + ok !defined $job4->{_upload_inactivity_timer}, 'timer removed when stopped'; + }; + + subtest 'progress reporting' => sub { + $job_mock->redefine(_stop_step_5_1_upload => sub ($self, $reason, $callback) { $callback->() }); + $job_mock->redefine(_upload_log_file_or_asset => 1); + + my $job = OpenQA::Worker::Job->new($worker, $client, {id => 100, URL => $engine_url}); + my $callback = sub ($res = {}) { $job->_set_status(stopped => $res) }; + my $fake_process; + $job->_set_status(stopping => {reason => WORKER_SR_DONE}); + path($worker->pool_directory, 'ulogs')->make_path->child('foo')->touch; + combined_like { + $job->_stop_step_4_upload(test => $callback); + wait_until_job_status_ok($job, 'stopped'); + } + qr/Result: test/, 'result logged'; + ok $fake_process = OpenQA::Test::Utils::last_fake_process, 'process created' or return; + is_deeply $fake_process->progress_args, [(['file']) x 4], 'process reported' + or always_explain $fake_process->progress_args; + + $job_mock->unmock($_) for qw(_stop_step_5_1_upload _upload_log_file_or_asset); + }; +}; + subtest 'ignoring known images and files' => sub { my $job = OpenQA::Worker::Job->new($worker, $client, {id => 1, URL => $engine_url}); $job->{_images_to_send} = {md5sum1 => 1, md5sum2 => 1}; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/t/dummy-isotovideo.sh new/openQA-5.1793722468.aa0c30fc/t/dummy-isotovideo.sh --- old/openQA-5.1793632686.cbcf1318/t/dummy-isotovideo.sh 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/t/dummy-isotovideo.sh 2026-11-03 17:14:28.000000000 +0100 @@ -2,6 +2,11 @@ set -euo pipefail echo "dummy isotovideo started with arguments: $*" +if [[ "$*" =~ --version ]]; then + echo "dummy isotovideo interface v0" + exit 0 +fi + echo "arguments: $*" > autoinst-log.txt # create fake test results so the web UI thinks we ran at least one test module successfully and considers @@ -9,7 +14,8 @@ mkdir -p testresults echo '[{"category":"dummy","name":"fake","script":"none","flags":{}}]' > testresults/test_order.json echo '{"dents":0,"details":[],"result":"ok"}' > testresults/result-fake.json -echo '{"current_test":"fake","status":"finished","test_execution_paused":0}' > autoinst-status.json +echo '{"current_test":"fake","status":"finished","test_execution_paused":0}' > autoinst-status.json.tmp +mv autoinst-status.json{.tmp,} # also add a vars.json file for less distracting error messages in the test log echo '{"ARCH":"x86_64","NAME":"fake-test"}' > vars.json diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/t/lib/OpenQA/Test/Utils.pm new/openQA-5.1793722468.aa0c30fc/t/lib/OpenQA/Test/Utils.pm --- old/openQA-5.1793632686.cbcf1318/t/lib/OpenQA/Test/Utils.pm 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/t/lib/OpenQA/Test/Utils.pm 2026-11-03 17:14:28.000000000 +0100 @@ -595,19 +595,33 @@ return $dest; } +my @FAKE_PROCESSES; + +package Test::FakeSubprocess { + use Mojo::Base 'Mojo::EventEmitter', -signatures; + has pid => 12345; + has progress_args => sub { [] }; + sub progress ($self, @args) { push @{$self->progress_args}, [@args]; $self->emit(progress => @args) } +} + sub mock_io_loop (%args) { my $io_loop_mock = Test::MockModule->new('Mojo::IOLoop'); $io_loop_mock->redefine( # avoid forking to prevent coverage analysis from slowing down the test significantly subprocess => sub ($io_loop, $function, $callback) { + my $proc = Test::FakeSubprocess->new; my @result; my $error = ''; - try { @result = $function->() } + push @FAKE_PROCESSES, $proc; + try { @result = $function->($proc) } catch ($e) { $error = $e } - $io_loop->next_tick(sub { $callback->(undef, $error, @result) }); + $io_loop->next_tick(sub { $callback->($proc, $error, @result) }); + return $proc; }) if $args{subprocess}; return $io_loop_mock; } +sub last_fake_process () { pop @FAKE_PROCESSES } + sub assume_all_assets_exist () { OpenQA::Schema->singleton->resultset('Assets')->search({})->update({size => 0}) } sub schedule_iso ($t, $args, $status = 200, $query_params = {}, $msg = undef) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1793632686.cbcf1318/tools/ci/autoinst.sha new/openQA-5.1793722468.aa0c30fc/tools/ci/autoinst.sha --- old/openQA-5.1793632686.cbcf1318/tools/ci/autoinst.sha 2026-11-02 16:18:06.000000000 +0100 +++ new/openQA-5.1793722468.aa0c30fc/tools/ci/autoinst.sha 2026-11-03 17:14:28.000000000 +0100 @@ -1 +1 @@ -5.044000-d65739f82b142e3ea7c26d7b873ec5b80361ca8c \ No newline at end of file +5.044000-90d5c840a113cf5cc3d3d1618a217771f5e6f4ba \ No newline at end of file ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.hTaxCs/_old 2026-11-04 19:44:50.594862604 +0100 +++ /var/tmp/diff_new_pack.hTaxCs/_new 2026-11-04 19:44:50.599862813 +0100 @@ -1,5 +1,5 @@ name: openQA -version: 5.1793632686.cbcf1318 -mtime: 1793632686 -commit: cbcf131875f9e4964fe8e0270befea922e00cb24 +version: 5.1793722468.aa0c30fc +mtime: 1793722468 +commit: aa0c30fcbc07ef6ddb3746ad0bd3a83336b09a48
