Skip to content
Merged
56 changes: 56 additions & 0 deletions service/lib/agama/software/callbacks/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

# Copyright (c) [2025] SUSE LLC
#
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of version 2 of the GNU General Public License as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, contact SUSE LLC.
#
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "yast"

module Agama
module Software
module Callbacks
# Base class for libzypp callbacks for sharing some common texts.
class Base
include Yast::I18n

# Constructor
def initialize
textdomain "agama"
end

# label for the "retry" action
def retry_label
# TRANSLATORS: button label, try downloading the failed package again
_("Try again")
end

# label for the "continue" action
def continue_label
# TRANSLATORS: button label, ignore the failed download, skip package installation
_("Continue anyway")
end

# label for the "abort" action
def abort_label
# TRANSLATORS: button label, abort the installation completely after an error
_("Abort installation")
end
end
end
end
end
17 changes: 11 additions & 6 deletions service/lib/agama/software/callbacks/media.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,28 @@
# To contact SUSE LLC about this file by physical or electronic mail, you may
# find current contact information at www.suse.com.

require "logger"
require "yast"
require "agama/question"
require "agama/software/callbacks/base"

Yast.import "Pkg"

module Agama
module Software
module Callbacks
# Callbacks related to media handling
class Media
class Media < Base
# Constructor
#
# @param questions_client [Agama::DBus::Clients::Questions]
# @param logger [Logger]
def initialize(questions_client, logger)
super()

textdomain "agama"
@questions_client = questions_client
@logger = logger
@logger = logger || ::Logger.new($stdout)
end

# Register the callbacks
Expand Down Expand Up @@ -74,14 +79,14 @@ def media_change(error_code, error, url, product, current, current_label, wanted
)

question = Agama::Question.new(
qclass: "software.medium_error",
qclass: "software.package_error.medium_error",
text: error,
options: [:Retry, :Skip],
default_option: :Skip,
options: [retry_label.to_sym, continue_label.to_sym],
default_option: retry_label.to_sym,
data: { "url" => url }
)
questions_client.ask(question) do |question_client|
(question_client.answer == :Retry) ? "" : "S"
(question_client.answer == retry_label.to_sym) ? "" : "S"
end
end
# rubocop:enable Metrics/ParameterLists
Expand Down
26 changes: 12 additions & 14 deletions service/lib/agama/software/callbacks/progress.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,24 @@
require "yast"
require "agama/question"
require "agama/dbus/clients/questions"
require "agama/software/callbacks/base"

Yast.import "Pkg"

module Agama
module Software
module Callbacks
# This class represents the installer status
class Progress
include Yast::I18n

class Progress < Base
class << self
def setup(pkg_count, progress, logger)
new(pkg_count, progress, logger).setup
end
end

def initialize(pkg_count, progress, logger)
super()

textdomain "agama"

@total = pkg_count
Expand Down Expand Up @@ -88,24 +89,21 @@ def done_package(error_code, description)

logger.error("Package #{current_package} failed: #{description}")

# TRANSLATORS: %s is a package name
text = _("Package %s could not be installed.") % current_package

question = Agama::Question.new(
qclass: "software.install_error",
text: text,
options: [:Retry, :Cancel, :Ignore],
default_option: :Retry,
data: { "description" => description }
qclass: "software.package_error.install_error",
text: description,
options: [retry_label.to_sym, continue_label.to_sym, abort_label.to_sym],
default_option: retry_label.to_sym,
data: { "package" => current_package }
)

questions_client.ask(question) do |question_client|
case question_client.answer
when :Retry
when retry_label.to_sym
"R"
when :Cancel
when abort_label.to_sym
"C"
when :Ignore
when continue_label.to_sym
"I"
else
logger.error("Unexpected response #{question_client.answer.inspect}, " \
Expand Down
32 changes: 17 additions & 15 deletions service/lib/agama/software/callbacks/provide.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,15 @@
require "logger"
require "yast"
require "agama/question"
require "agama/software/callbacks/base"

Yast.import "Pkg"

module Agama
module Software
module Callbacks
# Provide callbacks
class Provide
include Yast::I18n

class Provide < Base
# From https://github.com/openSUSE/libzypp/blob/d90a93fc2a248e6592bd98114f82a0b88abadb72/zypp/ZYppCallbacks.h#L111
NO_ERROR = 0
NOT_FOUND = 1
Expand All @@ -43,6 +42,8 @@ class Provide
# @param questions_client [Agama::DBus::Clients::Questions]
# @param logger [Logger]
def initialize(questions_client, logger)
super()

textdomain "agama"
@questions_client = questions_client
@logger = logger || ::Logger.new($stdout)
Expand All @@ -57,36 +58,37 @@ def setup

# DoneProvide callback
#
# @return [String] "I" for ignore, "R" for retry and "C" for abort (not implemented)
# @return [String, nil] "I" for ignore, "R" for retry and "C" for abort (not implemented)
# @see https://github.com/yast/yast-yast2/blob/19180445ab935a25edd4ae0243aa7a3bcd09c9de/library/packages/src/modules/PackageCallbacks.rb#L620
def done_provide(error, reason, name)
args = [error, reason, name]
logger.debug "DoneProvide callback: #{args.inspect}"

message = case error
error_code = case error
when NO_ERROR, NOT_FOUND
# "Not found" (error 1) is handled by the MediaChange callback.
nil
when IO_ERROR
Yast::Builtins.sformat(_("Package %1 could not be downloaded (input/output error)."),
name)
"IO_ERROR"
when INVALID
Yast::Builtins.sformat(_("Package %1 is broken, integrity check has failed."), name)
"INVALID"
else
logger.warn "DoneProvide: unknown error: '#{error}'"
nil
end

return if message.nil?
return nil if error_code.nil?

question = Agama::Question.new(
qclass: "software.provide_error",
text: message,
options: [:Retry, :Ignore],
default_option: :Retry,
data: { "reason" => reason }
qclass: "software.package_error.provide_error",
text: reason,
options: [retry_label.to_sym, continue_label.to_sym],
default_option: retry_label.to_sym,
data: { "package" => name, "error_code" => error_code }
)

questions_client.ask(question) do |question_client|
(question_client.answer == :Retry) ? "R" : "I"
(question_client.answer == retry_label.to_sym) ? "R" : "I"
end
end

Expand Down
5 changes: 5 additions & 0 deletions service/package/rubygem-agama-yast.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
-------------------------------------------------------------------
Wed Feb 19 13:35:07 UTC 2025 - Ladislav Slezák <lslezak@suse.com>

- UX: Improve the libzypp callbacks (gh#agama-project/agama#1985)

-------------------------------------------------------------------
Tue Feb 18 17:19:08 UTC 2025 - Knut Anderssen <kanderssen@suse.com>

Expand Down
4 changes: 2 additions & 2 deletions service/test/agama/software/callbacks/media_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
let(:question_client) { instance_double(Agama::DBus::Clients::Question) }

context "when the user answers :Retry" do
let(:answer) { :Retry }
let(:answer) { subject.retry_label.to_sym }

it "returns ''" do
ret = subject.media_change(
Expand All @@ -51,7 +51,7 @@
end

context "when the user answers :Skip" do
let(:answer) { :Skip }
let(:answer) { subject.continue_label.to_sym }

it "returns 'S'" do
ret = subject.media_change(
Expand Down
16 changes: 9 additions & 7 deletions service/test/agama/software/callbacks/provide_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

let(:logger) { Logger.new($stdout, level: :warn) }

let(:answer) { :Retry }
let(:answer) { subject.retry_label.to_sym }

describe "#done_provide" do
before do
Expand All @@ -50,24 +50,26 @@

context "when the there is an I/O error" do
it "registers a question informing of the error" do
reason = "could not be downloaded"
expect(questions_client).to receive(:ask) do |q|
expect(q.text).to include("could not be downloaded")
expect(q.text).to include(reason)
end
subject.done_provide(2, "Some dummy reason", "dummy-package")
subject.done_provide(2, reason, "dummy-package")
end
end

context "when the there is an I/O error" do
it "registers a question informing of the error" do
reason = "integrity check has failed"
expect(questions_client).to receive(:ask) do |q|
expect(q.text).to include("integrity check has failed")
expect(q.text).to include(reason)
end
subject.done_provide(3, "Some dummy reason", "dummy-package")
subject.done_provide(3, "integrity check has failed", "dummy-package")
end
end

context "when the user answers :Retry" do
let(:answer) { :Retry }
let(:answer) { subject.retry_label.to_sym }

it "returns 'R'" do
ret = subject.done_provide(
Expand All @@ -78,7 +80,7 @@
end

context "when the user answers :Skip" do
let(:answer) { :Ignore }
let(:answer) { subject.continue_label.to_sym }

it "returns 'I'" do
ret = subject.done_provide(
Expand Down
6 changes: 6 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Fri Feb 14 09:46:02 UTC 2025 - Ladislav Slezák <lslezak@suse.com>

- Implement specific question dialogs for the libzypp callbacks
(gh#agama-project/agama#1985)

-------------------------------------------------------------------
Fri Feb 7 14:43:08 UTC 2025 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
60 changes: 60 additions & 0 deletions web/src/components/questions/PackageErrorQuestion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) [2025] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { Question } from "~/types/questions";
import PackageErrorQuestion from "~/components/questions/PackageErrorQuestion";

const answerFn = jest.fn();
const question: Question = {
id: 1,
class: "software.package_error.provide_error",
text: "Package download failed",
options: ["Retry", "Skip"],
defaultOption: "Retry",
data: { package: "foo", error_code: "INVALID" },
};

const renderQuestion = () =>
plainRender(<PackageErrorQuestion question={question} answerCallback={answerFn} />);

describe("PackageErrorQuestion", () => {
it("renders the question text", () => {
renderQuestion();

screen.queryByText(question.text);
});

describe("when the user clicks Retry", () => {
it("calls the callback with Retry value", async () => {
const { user } = renderQuestion();

const retryButton = await screen.findByRole("button", { name: "Retry" });
await user.click(retryButton);

expect(question).toEqual(expect.objectContaining({ answer: "Retry" }));
expect(answerFn).toHaveBeenCalledWith(question);
});
});
});
Loading