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-03-14 22:22:35 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/openQA (Old) and /work/SRC/openSUSE:Factory/.openQA.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "openQA" Sat Mar 14 22:22:35 2026 rev:824 rq:1338820 version:5.1773427330.0b172206 Changes: -------- --- /work/SRC/openSUSE:Factory/openQA/openQA.changes 2026-03-13 21:17:01.617849736 +0100 +++ /work/SRC/openSUSE:Factory/.openQA.new.8177/openQA.changes 2026-03-14 22:23:45.460517043 +0100 @@ -1,0 +2,27 @@ +Fri Mar 13 22:08:20 UTC 2026 - [email protected] + +- Update to version 5.1773427330.0b172206: + * fix: Display GitHub bugrefs correctly when used within a label + * refactor: Improve style in `t/39-scheduled_products-table.t` + * feat: improve responsiveness of tabs in job details view + * ci(helm): override pullPolicy when install helm via ct + * test: Consider everything under `lib/OpenQA/Schema` covered + * test: Cover generating Gravatar URLs + * test: Cover handling failed job cancellation when scheduling iso + * feat: add zypper clean command in base container + * test: Cover computing Git log diff + * test: Cover adding logs to result file list + * refactor: Simplify and slightly improve `create_asset` + * refactor: Simplify error handling in function for appending job logs + * test: Cover setting and deleting job properties + * test: Cover error handling when duplicating jobs + * test: Mark error handling in `_hashref` as uncoverable + * test: Cover remaining lines of `JobModules.pm` + * refactor: Remove unused function `locked_by_jobs` + * test: Cover removing test suite defaults + * test: Cover rendering description of parent job group + * test: Consider everything under `lib/OpenQA/Script/` covered + * test: Cover `openqa_baseurl` used by clone script + * test: Cover handling unexpected return code in clone script + +------------------------------------------------------------------- Old: ---- openQA-5.1773333964.ffc5eff5.obscpio New: ---- openQA-5.1773427330.0b172206.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ openQA-client-test.spec ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:23:48.296634531 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:23:48.304634862 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-client Name: %{short_name}-test -Version: 5.1773333964.ffc5eff5 +Version: 5.1773427330.0b172206 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-devel-test.spec ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:23:48.604647291 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:23:48.612647622 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-devel Name: %{short_name}-test -Version: 5.1773333964.ffc5eff5 +Version: 5.1773427330.0b172206 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA-test.spec ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:23:48.872658394 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:23:48.880658725 +0100 @@ -18,7 +18,7 @@ %define short_name openQA Name: %{short_name}-test -Version: 5.1773333964.ffc5eff5 +Version: 5.1773427330.0b172206 Release: 0 Summary: Test package for openQA License: GPL-2.0-or-later ++++++ openQA-worker-test.spec ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:23:49.116668502 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:23:49.116668502 +0100 @@ -18,7 +18,7 @@ %define short_name openQA-worker Name: %{short_name}-test -Version: 5.1773333964.ffc5eff5 +Version: 5.1773427330.0b172206 Release: 0 Summary: Test package for %{short_name} License: GPL-2.0-or-later ++++++ openQA.spec ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:23:49.196671816 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:23:49.200671982 +0100 @@ -99,7 +99,7 @@ %define devel_requires %devel_no_selenium_requires chromedriver Name: openQA -Version: 5.1773333964.ffc5eff5 +Version: 5.1773427330.0b172206 Release: 0 Summary: The openQA web-frontend, scheduler and tools License: GPL-2.0-or-later ++++++ openQA-5.1773333964.ffc5eff5.obscpio -> openQA-5.1773427330.0b172206.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/assets/javascripts/render.js new/openQA-5.1773427330.0b172206/assets/javascripts/render.js --- old/openQA-5.1773333964.ffc5eff5/assets/javascripts/render.js 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/assets/javascripts/render.js 2026-03-13 19:42:10.000000000 +0100 @@ -212,7 +212,53 @@ return E('tr', [component, result, links], {id: rowid}); } -function renderModuleTable(container, response) { +// Default batch size for chunked rendering to balance responsiveness and overhead +const DEFAULT_BATCH_SIZE = 50; + +/** + * Process an array of items in batches using requestAnimationFrame for cooperative multitasking. + * This prevents blocking the UI thread during long-running operations. + * + * @param {Array} items - Array of items to process + * @param {Function} processor - Function called for each item with (item, index) as arguments + * @param {Object} options - Processing options + * @param {number} [options.batchSize=50] - Number of items to process per frame + * @param {Function} [options.shouldContinue=() => true] - Callback returning boolean to control interruption + * @returns {Promise<boolean>} Promise resolving to true if completed, false if interrupted + * @throws {Error} If processor throws an error during execution + */ +function batchProcess(items, processor, options = {}) { + const {batchSize = DEFAULT_BATCH_SIZE, shouldContinue = () => true} = options; + let currentIndex = 0; + + return new Promise((resolve, reject) => { + function processNextBatch() { + if (!shouldContinue()) { + resolve(false); // Interrupted + return; + } + + const end = Math.min(currentIndex + batchSize, items.length); + try { + for (; currentIndex < end; currentIndex++) { + processor(items[currentIndex], currentIndex); + } + } catch (err) { + reject(err); + return; + } + + if (currentIndex < items.length) { + requestAnimationFrame(processNextBatch); + } else { + resolve(true); // Completed + } + } + processNextBatch(); + }); +} + +async function renderModuleTable(container, response, shouldContinue = () => true) { container.innerHTML = response.snippets.header; const E = createElement; @@ -226,7 +272,7 @@ } if (response.modules === undefined || response.modules === null) { - return; + return Promise.resolve(true); } const thead = E('thead', [ @@ -236,17 +282,18 @@ container.appendChild(E('table', [thead, tbody], {id: 'results', class: 'table table-striped'})); - for (const idx in response.modules) { - const module = response.modules[idx]; - - if (module.category) { - tbody.appendChild( - E('tr', [E('td', [E('i', [], {class: 'fa fa-folder-open'}), '\u00a0' + module.category], {colspan: 3})]) - ); - } - - tbody.appendChild(renderModuleRow(module, response.snippets)); - } + return batchProcess( + response.modules, + module => { + if (module.category) { + tbody.appendChild( + E('tr', [E('td', [E('i', [], {class: 'fa fa-folder-open'}), '\u00a0' + module.category], {colspan: 3})]) + ); + } + tbody.appendChild(renderModuleRow(module, response.snippets)); + }, + {shouldContinue} + ); } function renderJobLink(jobId) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/assets/javascripts/running.js new/openQA-5.1773427330.0b172206/assets/javascripts/running.js --- old/openQA-5.1773333964.ffc5eff5/assets/javascripts/running.js 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/assets/javascripts/running.js 2026-03-13 19:42:10.000000000 +0100 @@ -142,7 +142,8 @@ const previewContainer = document.getElementById('preview_container_out'); const resultCells = resultsTable.getElementsByClassName('result'); testStatus.textDataMissing = false; - modules.forEach(function (module, moduleIndex) { + + batchProcess(modules, (module, moduleIndex) => { const resultCell = resultCells[moduleIndex]; if (!resultCell) { return; @@ -164,16 +165,21 @@ } // actually update the row resultRow.replaceWith(renderModuleRow(module, snippets)); - }); - - testStatus.running = newStatus.running; - developerMode.detailsForCurrentModuleUploaded = false; - updateDeveloperMode(); + }) + .then(completed => { + if (!completed) return; + testStatus.running = newStatus.running; + developerMode.detailsForCurrentModuleUploaded = false; + updateDeveloperMode(); - // reload broken thumbnails one last time - if (newState === 'done') { - reloadBrokenThumbnails(true); - } + // reload broken thumbnails one last time + if (newState === 'done') { + reloadBrokenThumbnails(true); + } + }) + .catch(error => { + console.error('Error batch processing test modules:', error); + }); }) .catch(error => { console.log('ERROR: modlist fail'); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/assets/javascripts/test_result.js new/openQA-5.1773427330.0b172206/assets/javascripts/test_result.js --- old/openQA-5.1773333964.ffc5eff5/assets/javascripts/test_result.js 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/assets/javascripts/test_result.js 2026-03-13 19:42:10.000000000 +0100 @@ -487,6 +487,15 @@ } } +function renderTabContent(tabConfig, response) { + const customRenderer = tabConfig.renderContents; + if (customRenderer) { + return customRenderer.call(tabConfig, response); + } + tabConfig.panelElement.innerHTML = response; + return Promise.resolve(); +} + function loadTabPanelElement(tabName, tabConfig) { const tabPanelElement = document.getElementById(tabName); if (!tabPanelElement) { @@ -497,21 +506,27 @@ return false; } tabConfig.panelElement = tabPanelElement; // for easier access in custom renderers - fetch(ajaxUrl, {method: 'GET'}) + if (tabConfig._abortController) { + tabConfig._abortController.abort(); + } + tabConfig._abortController = new AbortController(); + fetch(ajaxUrl, {method: 'GET', signal: tabConfig._abortController.signal}) .then(response => { if (!response.ok) throw `Server returned ${response.status}: ${response.statusText}`; if (response.headers.get('Content-Type').includes('application/json')) return response.json(); return response.text(); }) .then(response => { - const customRenderer = tabConfig.renderContents; - if (customRenderer) { - return customRenderer.call(tabConfig, response); + if (!tabConfig.isActive) { + tabConfig._deferredResponse = response; + return; } - tabPanelElement.innerHTML = response; + tabConfig._deferredResponse = undefined; + return renderTabContent(tabConfig, response); }) .catch(error => { - console.error(error); + if (error.name === 'AbortError') return; + console.error(`Error loading tab '${tabName}':`, error); const customRenderer = tabConfig.renderError; if (customRenderer) { return customRenderer.call(tabConfig, error); @@ -542,6 +557,17 @@ if (!tabConfig.initialized) { return (tabConfig.initialized = loadTabPanelElement(tabName, tabConfig)); } + if (tabConfig._deferredResponse) { + const response = tabConfig._deferredResponse; + renderTabContent(tabConfig, response) + .then(() => { + tabConfig._deferredResponse = undefined; + }) + .catch(error => { + console.error(`Error rendering deferred content for tab '${tabName}':`, error); + tabConfig._deferredResponse = undefined; + }); + } const showHandler = tabConfig.onShow; if (showHandler) { return showHandler.call(tabConfig); @@ -557,6 +583,10 @@ return false; } tabConfig.isActive = false; + if (!tabConfig.initialized && tabConfig._abortController) { + tabConfig._abortController.abort(); + tabConfig._abortController = undefined; + } const hideHandler = tabConfig.onHide; if (hideHandler) { return hideHandler.call(tabConfig); @@ -767,76 +797,27 @@ return commits; } -function renderTestModules(response) { - this.hasContents = true; - renderModuleTable(this.panelElement, response); - - // load the embedded logfiles (autoinst-log.txt); assume that in this case no test modules are available and skip further processing - if (this.panelElement.getElementsByClassName('embedded-logfile').length > 0) { - loadEmbeddedLogFiles(); - return; - } - - setupLazyLoadingFailedSteps(); +function setupTestDetailsFilter(tabConfig) { + if (tabConfig._hasFilterHandlers) return; - // enable the external tab if there are text results - // note: It would be more efficient to query "regular details" and external results in one go because both - // are just a different representation of the same data. - if (document.getElementsByClassName('external-result-container').length) { - showTabNavElement('external'); - } - - // display the preview for the current step according to the hash - const hash = window.location.hash; - if (hash.search('#step/') === 0) { - setCurrentPreviewFromStepLinkIfPossible($("[href='" + hash + "'], [data-href='" + hash + "']")); - } - - // setup event handlers for the window - if (!this.hasWindowEventHandlers) { - // setup keyboard navigation through test details - $(window).keydown(handleKeyDownOnTestDetails); - - // ensure the size of the preview container is adjusted when the window size changes - $(window).resize(function () { - const currentPreview = $('.current_preview'); - if (currentPreview.length) { - setCurrentPreview($('.current_preview'), true); - } - }); - this.hasWindowEventHandlers = true; - } - - // setup result filter, define function to apply filter changes const detailsFilter = $('#details-filter'); const detailsNameFilter = $('#details-name-filter'); const detailsFailedOnlyFilter = $('#details-only-failed-filter'); const resultsTable = $('#results'); - let anyFilterEnabled = false; - let nameFilter = ''; - let nameFilterEnabled = false; - let failedOnlyFilterEnabled = false; - const applyFilterChanges = function (event) { - // determine enabled filter - anyFilterEnabled = !detailsFilter.hasClass('hidden'); - if (anyFilterEnabled) { - nameFilter = detailsNameFilter.val(); - nameFilterEnabled = nameFilter.length !== 0; - failedOnlyFilterEnabled = detailsFailedOnlyFilter.prop('checked'); - anyFilterEnabled = nameFilterEnabled || failedOnlyFilterEnabled; - } - // show everything if no filter present - if (!anyFilterEnabled) { + const applyFilterChanges = () => { + const anyFilterEnabled = !detailsFilter.hasClass('hidden'); + const nameFilter = detailsNameFilter.val(); + const nameFilterEnabled = anyFilterEnabled && nameFilter.length !== 0; + const failedOnlyFilterEnabled = anyFilterEnabled && detailsFailedOnlyFilter.prop('checked'); + + if (!nameFilterEnabled && !failedOnlyFilterEnabled) { resultsTable.find('tbody tr').show(); return; } - // hide all categories resultsTable.find('tbody tr td[colspan="3"]').parent('tr').hide(); - - // show/hide table rows considering filter - $.each(resultsTable.find('tbody .result'), function (index, td) { + $.each(resultsTable.find('tbody .result'), (index, td) => { const tdElement = $(td); const trElement = tdElement.parent('tr'); const stepMaches = @@ -846,15 +827,60 @@ }); }; - detailsNameFilter.keyup(applyFilterChanges); - detailsFailedOnlyFilter.change(applyFilterChanges); - - // setup filter toggle - $('.details-filter-toggle').on('click', function (event) { + detailsNameFilter.on('keyup', applyFilterChanges); + detailsFailedOnlyFilter.on('change', applyFilterChanges); + $('.details-filter-toggle').on('click', event => { event.preventDefault(); detailsFilter.toggleClass('hidden'); applyFilterChanges(); }); + + tabConfig._hasFilterHandlers = true; +} + +function setupTestDetailsWindowEventHandlers(tabConfig) { + if (tabConfig._hasWindowEventHandlers) return; + $(window).keydown(handleKeyDownOnTestDetails); + $(window).resize(() => { + if ($('.current_preview').length) { + setCurrentPreview($('.current_preview'), true); + } + }); + tabConfig._hasWindowEventHandlers = true; +} + +function renderTestModules(response) { + this.hasContents = true; + const tabConfig = this; + + return renderModuleTable(this.panelElement, response, () => tabConfig.isActive) + .then(completed => { + if (!completed) return; + + if (tabConfig.panelElement.getElementsByClassName('embedded-logfile').length > 0) { + loadEmbeddedLogFiles(); + return; + } + + setupLazyLoadingFailedSteps(); + + if (document.getElementsByClassName('external-result-container').length) { + showTabNavElement('external'); + } + + const hash = window.location.hash; + if (hash.search('#step/') === 0) { + setCurrentPreviewFromStepLinkIfPossible($("[href='" + hash + "'], [data-href='" + hash + "']")); + } + + setupTestDetailsWindowEventHandlers(tabConfig); + setupTestDetailsFilter(tabConfig); + }) + .catch(error => { + console.error('Error rendering test modules:', error); + tabConfig.panelElement.innerHTML = ''; + tabConfig.panelElement.appendChild(document.createTextNode(`Unable to render test modules: ${error}`)); + }); } function renderExternalTab(response) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/codecov.yml new/openQA-5.1773427330.0b172206/codecov.yml --- old/openQA-5.1773333964.ffc5eff5/codecov.yml 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/codecov.yml 2026-03-13 19:42:10.000000000 +0100 @@ -26,6 +26,8 @@ - lib/OpenQA/WebAPI/ - lib/OpenQA/Shared/ - lib/OpenQA/Task/ + - lib/OpenQA/Script/ + - lib/OpenQA/Schema/ tests: target: 100.0 threshold: 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/container/devel:openQA:ci/base/Dockerfile new/openQA-5.1773427330.0b172206/container/devel:openQA:ci/base/Dockerfile --- old/openQA-5.1773333964.ffc5eff5/container/devel:openQA:ci/base/Dockerfile 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/container/devel:openQA:ci/base/Dockerfile 2026-03-13 19:42:10.000000000 +0100 @@ -8,16 +8,16 @@ # only dependencies for CircleCI to be able to load/save the package cache # and fix permissions for the unprivileged user we use -RUN zypper -n in tar gzip sudo +RUN zypper -n in tar gzip sudo && zypper clean -a # these are autoinst dependencies -RUN zypper install -y gcc-c++ cmake ninja pkgconfig\(opencv4\) pkg-config perl\(Module::CPANfile\) pkgconfig\(fftw3\) pkgconfig\(libpng\) pkgconfig\(sndfile\) pkgconfig\(theoraenc\) tesseract-ocr +RUN zypper install -y gcc-c++ cmake ninja pkgconfig\(opencv4\) pkg-config perl\(Module::CPANfile\) pkgconfig\(fftw3\) pkgconfig\(libpng\) pkgconfig\(sndfile\) pkgconfig\(theoraenc\) tesseract-ocr && zypper clean -a # openQA dependencies -RUN zypper install -y perl-CSS-Sass ruby-devel npm python3-base python3-requests git-core rsync curl 'postgresql-devel>=14' 'postgresql-server>=14' qemu qemu-tools tar xorg-x11-fonts sudo make +RUN zypper install -y perl-CSS-Sass ruby-devel npm python3-base python3-requests git-core rsync curl 'postgresql-devel>=14' 'postgresql-server>=14' qemu qemu-tools tar xorg-x11-fonts sudo make && zypper clean -a # openQA chromedriver for Selenium tests -RUN zypper install -y chromedriver +RUN zypper install -y chromedriver && zypper clean -a ENV LANG=en_US.UTF-8 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Schema/Result/JobLocks.pm new/openQA-5.1773427330.0b172206/lib/OpenQA/Schema/Result/JobLocks.pm --- old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Schema/Result/JobLocks.pm 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/lib/OpenQA/Schema/Result/JobLocks.pm 2026-03-13 19:42:10.000000000 +0100 @@ -1,4 +1,4 @@ -# Copyright 2015 SUSE LLC +# Copyright SUSE LLC # SPDX-License-Identifier: GPL-2.0-or-later package OpenQA::Schema::Result::JobLocks; @@ -33,16 +33,4 @@ __PACKAGE__->set_primary_key('name', 'owner'); __PACKAGE__->belongs_to(owner => 'OpenQA::Schema::Result::Jobs', 'owner'); - -# translate job ids stored in locked_by to jobs -sub locked_by_jobs { - my ($self) = @_; - my $rsource = $self->result_source; - my $schema = $rsource->schema; - - return unless $self->locked_by; - my @locked_ids = split /,/, $self->locked_by; - return $schema->resultset('Jobs')->search({id => {-in => \@locked_ids}})->all; -} - 1; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Schema/Result/Jobs.pm new/openQA-5.1773427330.0b172206/lib/OpenQA/Schema/Result/Jobs.pm --- old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Schema/Result/Jobs.pm 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/lib/OpenQA/Schema/Result/Jobs.pm 2026-03-13 19:42:10.000000000 +0100 @@ -450,7 +450,7 @@ $hashref{$field} = $obj->$field->datetime(); } else { - die "unknown field type: $ref"; + die "unknown field type: $ref"; # uncoverable statement } } @@ -1075,13 +1075,9 @@ my $path = $self->worker->get_property('WORKER_TMPDIR'); return unless -d $path; # we can't help $path .= "/$file_name"; - if (open my $fd, '>>', $path) { - print $fd $log->{data}; - close $fd; - } - else { - print STDERR "can't open $path: $!\n"; - } + return log_error "can't open $path: $!" unless open my $fd, '>>', $path; + print $fd $log->{data}; + close $fd; } sub update_result ($self, $result, $state = undef) { @@ -1417,23 +1413,17 @@ } if ($chunk->is_last) { - # XXX: Watch out also apparmor permissions - my $sum; - my $real_sum; $last++; - # Perform weak check on last bytes if files > 250MB - if ($chunk->end > 250000000) { - $sum = $chunk->end; - $real_sum = -s $temp_final_file->to_string; - } - else { - $sum = $chunk->total_cksum; - $real_sum = $chunk->file_digest($temp_final_file->to_string); - } + # Perform weak check on the number of bytes if the file size is > 250 MB + my $temp_final_str = $temp_final_file->to_string; + my ($sum, $real_sum) + = $chunk->end > 250000000 + ? ($chunk->end, -s $temp_final_str) + : ($chunk->total_cksum, $chunk->file_digest($temp_final_str)); $temp_chunk_folder->remove_tree - && die Mojo::Exception->new("Checksum mismatch expected $sum got: $real_sum ( weak check on last bytes )") + && die Mojo::Exception->new("Checksum mismatch expected $sum got: $real_sum (weak check on last bytes)") unless $sum eq $real_sum; $temp_final_file->move_to($final_file); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Script/CloneJob.pm new/openQA-5.1773427330.0b172206/lib/OpenQA/Script/CloneJob.pm --- old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Script/CloneJob.pm 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/lib/OpenQA/Script/CloneJob.pm 2026-03-13 19:42:10.000000000 +0100 @@ -26,6 +26,7 @@ create_url_handler split_jobid post_jobs + openqa_baseurl ); use constant GLOBAL_SETTINGS => (qw(WORKER_CLASS _GROUP _GROUP_ID)); @@ -73,6 +74,11 @@ } } +sub _handle_unexpected_return_code ($tx) { # uncoverable statement + warn sprintf 'unexpected return code: %s %s', $tx->res->code, $tx->res->message; # uncoverable statement + exit 1; # uncoverable statement +} + sub clone_job_get_job ($jobid, $url_handler, $options) { my $url = $url_handler->{remote_url}->clone; $url->path("jobs/$jobid"); @@ -84,10 +90,7 @@ $err->{code} //= ''; die "failed to get job '$jobid': $err->{code} $err->{message}"; } - if ($tx->res->code != HTTP_OK) { - warn sprintf 'unexpected return code: %s %s', $tx->res->code, $tx->res->message; - exit 1; - } + _handle_unexpected_return_code($tx) unless $tx->res->code == HTTP_OK; my $job = $tx->res->json->{job}; print STDERR Cpanel::JSON::XS->new->pretty->encode($job) if $options->{verbose}; return $job; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Utils.pm new/openQA-5.1773427330.0b172206/lib/OpenQA/Utils.pm --- old/openQA-5.1773333964.ffc5eff5/lib/OpenQA/Utils.pm 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/lib/OpenQA/Utils.pm 2026-03-13 19:42:10.000000000 +0100 @@ -82,7 +82,7 @@ use constant UNCONSTRAINED_BUGREF_REGEX => $BUGREF_REGEX; use constant BUGREF_REGEX => qr{(?:^|(?<=<p>)|(?<=\s|,))$BUGREF_REGEX(?![\w\"])}; -use constant LABEL_REGEX => qr/\blabel:(?<match>([\w:#]+))\b/; +use constant LABEL_REGEX => qr/\blabel:(?<match>([\w:#\/-]+))\b/; use constant FLAG_REGEX => qr/\bflag:(?<match>([\w:#]+))\b/; use constant ONE_SECOND_IN_MICROSECONDS => 1_000_000; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/06-users.t new/openQA-5.1773427330.0b172206/t/06-users.t --- old/openQA-5.1773333964.ffc5eff5/t/06-users.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/06-users.t 2026-03-13 19:42:10.000000000 +0100 @@ -14,16 +14,17 @@ OpenQA::Test::Database->new->create; my $t = Test::Mojo->new('OpenQA::WebAPI'); +my $users = $t->app->schema->resultset('Users'); subtest 'new users are not ops and admins' => sub { my $mordred_id = 'https://openid.badguys.uk/mordred'; - my $user = $t->app->schema->resultset('Users')->create({username => $mordred_id}); + my $user = $users->create({username => $mordred_id}); ok !$user->is_admin, 'new users are not admin by default'; ok !$user->is_operator, 'new users are not operator by default'; }; subtest 'system user presence' => sub { - my $system_user = $t->app->schema->resultset('Users')->system; + my $system_user = $users->system; ok $system_user, 'system user exists'; ok !$system_user->is_admin, 'system user is not an admin'; ok !$system_user->is_operator, 'system user is not an operator'; @@ -31,7 +32,6 @@ }; subtest 'new user is admin if no admin is present' => sub { - my $users = $t->app->schema->resultset('Users'); my $admins = $users->search({is_admin => 1}); $_->update({is_admin => 0}) while $admins->next; ok !$users->search({is_admin => 1})->all, 'no admin is present'; @@ -40,4 +40,10 @@ ok $user->is_operator, 'new user is operator by default if there was no admin'; }; +subtest 'gravatar URL' => sub { + my $user = $users->create({username => 'user-without-e-mail'}); + is $user->gravatar, '//www.gravatar.com/avatar?s=40', 'without e-mail'; + like $users->find(1)->gravatar, qr|//www.gravatar.com/avatar/[a-f0-9]{32}\?d=wavatar&s=40|, 'with e-mail'; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/10-jobs-results.t new/openQA-5.1773427330.0b172206/t/10-jobs-results.t --- old/openQA-5.1773333964.ffc5eff5/t/10-jobs-results.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/10-jobs-results.t 2026-03-13 19:42:10.000000000 +0100 @@ -196,6 +196,9 @@ $arbitrary_job_module->save_results(\%some_test_results); my $details_file = path($arbitrary_job_module->job->result_dir, 'details-' . $arbitrary_job_module->name . '.json'); is_deeply decode_json($details_file->slurp), \%some_test_results, 'overall structure of test results preserved'; + $some_test_results{details} = [{needles => [{json => 'foo'}]}]; + combined_like { $arbitrary_job_module->save_results(\%some_test_results) } qr/foo not found/, + 'updated for needles in "details" attempted'; }; subtest 'loading results with missing file in details' => sub { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/10-jobs.t new/openQA-5.1773427330.0b172206/t/10-jobs.t --- old/openQA-5.1773333964.ffc5eff5/t/10-jobs.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/10-jobs.t 2026-03-13 19:42:10.000000000 +0100 @@ -1179,4 +1179,35 @@ is_deeply [map { $_->text } $job->comments], $expected, 'linked label for non-bugref URL'; }; +subtest 'error handling when duplicating jobs' => sub { + my $job = $jobs->find(99937); + my $job_mock = Test::MockModule->new('OpenQA::Schema::Result::Jobs', no_auto => 1); + my $res; + + $job_mock->redefine(_create_clones => sub ($self, @args) { die "Rollback failed: foo\n" }); + combined_like { $res = $job->duplicate } qr/unable to roll back/i, 'failing rollback logged'; + is $res, 'Rollback failed after failure to clone cluster of job 99937', 'failing rollback handled'; + + $job_mock->redefine(_create_clones => sub ($self, @args) { die "internal error\n" }); + combined_like { $res = $job->duplicate } qr/rolled back after error.*internal error/, 'error logged'; + is $res, 'An internal error occurred when cloning cluster of job 99937', 'internal error handled'; +}; + +subtest 'setting and deleting properties' => sub { + my $job = $jobs->find(99937); + $job->set_property(foo => 42); + is $job->settings->find({key => 'foo'})->value, 42, 'property has been created'; + $job->set_property(foo => undef); + is $job->settings->find({key => 'foo'}), undef, 'property has been deleted'; +}; + +subtest 'adding logs to result file list, including virtio console logs' => sub { + my $job = $jobs->find(99937); + $job->set_property(VIRTIO_CONSOLE_NUM => 2); + path($job->result_dir, 'serial_terminal1.txt')->spew('foo'); + my $files = $job->test_resultfile_list; + my @expected = qw(video.ogv autoinst-log.txt serial0.txt serial_terminal1.txt); + is_deeply $files, \@expected, 'list of existing result files returned' or always_explain $files; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/16-utils-runcmd.t new/openQA-5.1773427330.0b172206/t/16-utils-runcmd.t --- old/openQA-5.1773333964.ffc5eff5/t/16-utils-runcmd.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/16-utils-runcmd.t 2026-03-13 19:42:10.000000000 +0100 @@ -14,6 +14,7 @@ use OpenQA::Test::Case; use OpenQA::Test::TimeLimit '10'; use OpenQA::Utils; +use OpenQA::Schema::Result::Jobs; use Mojo::File 'tempdir'; use Test::MockModule; use Test::Mojo; @@ -108,6 +109,11 @@ 'no error (only info) logged if check returns false (despite Git returning 1)'; }; + subtest 'log diff computation helper of job' => sub { + like OpenQA::Schema::Result::Jobs::git_log_diff(undef, $empty_tmp_dir, 'HEAD'), qr/test.*foo/s, + 'commit and file mentioned'; + }; + subtest 'cache_ref' => sub { my $test_file = $empty_tmp_dir->child('foo')->touch; my $test_file_2 = "$empty_tmp_dir/bar"; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/35-script_clone_job.t new/openQA-5.1773427330.0b172206/t/35-script_clone_job.t --- old/openQA-5.1773333964.ffc5eff5/t/35-script_clone_job.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/35-script_clone_job.t 2026-03-13 19:42:10.000000000 +0100 @@ -27,10 +27,19 @@ has status_line => 'some status'; } # uncoverable statement +package Test::FakeLWPUserAgentMirrorTxn { + use Mojo::Base -base, -signatures; + has error => undef; + has res => sub { Test::FakeLWPUserAgentMirrorResult->new(is_success => 0, code => 404) }; +} # uncoverable statement + package Test::FakeLWPUserAgent { use Mojo::Base -base, -signatures; has mirrored => sub { {} }; has missing => 0; + has max_redirects => undef; + + sub get ($self, $url) { Test::FakeLWPUserAgentMirrorTxn->new } sub mirror ($self, $from, $dest) { my @res @@ -62,6 +71,14 @@ WORKER_CLASS => 'qemu_x86_64', ); +subtest 'getting job' => sub { + my $clone_mock = Test::MockModule->new('OpenQA::Script::CloneJob'); + $clone_mock->redefine(_handle_unexpected_return_code => sub ($tx) { die 'unexpected return code' }); + my $url_handler = {remote => Test::FakeLWPUserAgent->new, remote_url => Mojo::URL->new('foo')}; + throws_ok { clone_job_get_job(42, $url_handler, {'ignore-missing-assets' => 1}) } qr/unexpected return code/, + 'unexpected return code handled'; +}; + subtest 'clone job apply settings tests' => sub { my %test_settings = %child_settings; $test_settings{HDD_1} = 'new.qcow2'; @@ -456,4 +473,10 @@ $ua->get('http://foobar/some/path'); }; +subtest 'determining base URL' => sub { + is openqa_baseurl(Mojo::URL->new('http://foo:80/bar')), 'http://foo', 'default http port removed'; + is openqa_baseurl(Mojo::URL->new('https://foo:443/bar')), 'https://foo', 'default https port removed'; + is openqa_baseurl(Mojo::URL->new('http://foo:9526/bar')), 'http://foo:9526', 'custom port preserved'; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/39-scheduled_products-table.t new/openQA-5.1773427330.0b172206/t/39-scheduled_products-table.t --- old/openQA-5.1773333964.ffc5eff5/t/39-scheduled_products-table.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/39-scheduled_products-table.t 2026-03-13 19:42:10.000000000 +0100 @@ -157,4 +157,15 @@ ok $scheduled_products->find(3), 'scheduled product with jobs still present'; }; +subtest 'handling failed job cancellation' => sub { + my $jobs_mock = Test::MockModule->new('OpenQA::Schema::ResultSet::Jobs'); + $jobs_mock->redefine(cancel_by_settings => sub { die "fake error" }); + $schema->txn_begin; + my $jobs = {settings_result => [{DISTRI => 'foo', VERSION => 'bar', BUILD => '42'}]}; + $scheduled_products_mock->redefine(_generate_jobs => $jobs); + my $res = $scheduled_product->_schedule_iso({_OBSOLETE => 1}, $signal_guard); + like "@{$res->{notes}}", qr/.*failed to cancel.*fake error.*/i, 'note added' or always_explain $res; + $schema->txn_rollback; +}; + done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/api/10-jobgroups.t new/openQA-5.1773427330.0b172206/t/api/10-jobgroups.t --- old/openQA-5.1773333964.ffc5eff5/t/api/10-jobgroups.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/api/10-jobgroups.t 2026-03-13 19:42:10.000000000 +0100 @@ -591,5 +591,14 @@ } }; +subtest 'helper for removing test suite defaults' => sub { + my $helper = \&OpenQA::Schema::Result::JobGroups::_remove_test_suite_defaults; + my $group = {scenarios => {x86_64 => {product => [{foo => {machine => '64bit'}}]}}}; + my $test_suites = {x86_64 => {foo => 1}}; + my $scenarios = []; + $helper->(qw(product 64bit x86_64), $group, $test_suites, $scenarios); + is_deeply $scenarios, ['foo'], 'scenario is added'; + is $group->{scenarios}->{x86_64}->{product}, $scenarios, 'scenarios are assigned to group'; +}; done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/t/ui/14-dashboard-parents.t new/openQA-5.1773427330.0b172206/t/ui/14-dashboard-parents.t --- old/openQA-5.1773333964.ffc5eff5/t/ui/14-dashboard-parents.t 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/t/ui/14-dashboard-parents.t 2026-03-13 19:42:10.000000000 +0100 @@ -140,8 +140,10 @@ isnt scalar @{$driver->find_elements('opensuse test', 'link_text')}, 0, "child group 'opensuse test' present'"; }; +my $test_parent = $parent_groups->find({name => 'Test parent'}); + subtest 'View grouped by group' => sub { - $driver->get('/parent_group_overview/' . $parent_groups->find({name => 'Test parent'})->id); + $driver->get('/parent_group_overview/' . $test_parent->id); $driver->find_element_by_id('grouped_by_group_tab')->click(); is $driver->find_element_by_id('grouped_by_group_tab')->get_attribute('class'), @@ -154,5 +156,11 @@ $driver->find_element_by_id('grouped_by_group')->is_displayed(); }; +subtest 'rendered description' => sub { + $test_parent->update({description => '**test description**'}); + $driver->get('/parent_group_overview/' . $test_parent->id); + is $driver->find_element_by_id('group_description')->get_text, 'test description', 'description rendered'; +}; + kill_driver(); done_testing(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/openQA-5.1773333964.ffc5eff5/tools/test_helm_chart new/openQA-5.1773427330.0b172206/tools/test_helm_chart --- old/openQA-5.1773333964.ffc5eff5/tools/test_helm_chart 2026-03-12 17:46:04.000000000 +0100 +++ new/openQA-5.1773427330.0b172206/tools/test_helm_chart 2026-03-13 19:42:10.000000000 +0100 @@ -6,7 +6,7 @@ cd container/helm -gw_disabled=(--helm-extra-set-args "--set=gateway.enabled=false") +ct_extra=(--helm-extra-set-args "--set=gateway.enabled=false --set=image.pullPolicy=IfNotPresent --set=worker.image.pullPolicy=IfNotPresent") ct_install_args=() -[[ "${1:-lint}" != "lint" ]] && ct_install_args=("${gw_disabled[@]}") +[[ "${1:-lint}" != "lint" ]] && ct_install_args=("${ct_extra[@]}") ct "${1:-lint}" --debug --all --config ct.yaml "${ct_install_args[@]}" ++++++ openQA.obsinfo ++++++ --- /var/tmp/diff_new_pack.rtYZIL/_old 2026-03-14 22:24:01.045162651 +0100 +++ /var/tmp/diff_new_pack.rtYZIL/_new 2026-03-14 22:24:01.057163147 +0100 @@ -1,5 +1,5 @@ name: openQA -version: 5.1773333964.ffc5eff5 -mtime: 1773333964 -commit: ffc5eff5a7f50e0a7142d097da0596d744301860 +version: 5.1773427330.0b172206 +mtime: 1773427330 +commit: 0b1722061c0be67691e018c85963d7de770032f8
