diff --git a/library/packages/src/Makefile.am b/library/packages/src/Makefile.am index ff4b9dd63..0216584c1 100644 --- a/library/packages/src/Makefile.am +++ b/library/packages/src/Makefile.am @@ -23,6 +23,7 @@ ylibdir = "${yast2dir}/lib/packages" ylib_DATA = \ lib/packages/commit_result.rb \ lib/packages/dummy_callbacks.rb \ + lib/packages/file_conflict_callbacks.rb \ lib/packages/update_message.rb \ lib/packages/update_messages_view.rb diff --git a/library/packages/src/lib/packages/file_conflict_callbacks.rb b/library/packages/src/lib/packages/file_conflict_callbacks.rb new file mode 100644 index 000000000..dbd757f5f --- /dev/null +++ b/library/packages/src/lib/packages/file_conflict_callbacks.rb @@ -0,0 +1,183 @@ + +# ------------------------------------------------------------------------------ +# Copyright (c) 2016 SUSE LLC +# +# 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. +# +# ------------------------------------------------------------------------------ +# + +require "yast" + +module Packages + # Default file conflicts callbacks for package bindings. To register the + # callbacks in Yast::Pkg just call {Package::FileConflictCallbacks.register} + class FileConflictCallbacks + PKG_INSTALL_WIDGET = :progressCurrentPackage + + class << self + include Yast::Logger + include Yast::I18n + include Yast::UIShortcuts + + # register the file conflict callbacks + def register + Yast.import "Pkg" + Yast.import "UI" + Yast.import "Progress" + Yast.import "Mode" + Yast.import "CommandLine" + Yast.import "Report" + Yast.import "Label" + Yast.import "PackageCallbacks" + + textdomain "base" + + register_file_conflict_callbacks + end + + private + + # Helper function for creating an YaST function reference + def fun_ref(*args) + Yast::FunRef.new(*args) + end + + def register_file_conflict_callbacks + Yast::Pkg.CallbackFileConflictStart(fun_ref(method(:start), "void ()")) + Yast::Pkg.CallbackFileConflictProgress(fun_ref(method(:progress), + "boolean (integer)")) + Yast::Pkg.CallbackFileConflictReport(fun_ref(method(:report), + "boolean (list, list)")) + Yast::Pkg.CallbackFileConflictFinish(fun_ref(method(:finish), "void ()")) + + nil + end + + # Is package installation progress displayed? + # @return [Boolean] true if package installation progress is displayed + def pkg_installation? + Yast::UI.WidgetExists(PKG_INSTALL_WIDGET) + end + + # handle the file conflict detection start callback + def start + log.info "Starting the file conflict check..." + # TRANSLATORS: progress bar label + label = _("Checking file conflicts...") + + if Yast::Mode.commandline + Yast::CommandLine.PrintVerbose(label) + elsif pkg_installation? + # package slideshow with progress already present + Yast::UI.ChangeWidget(Id(PKG_INSTALL_WIDGET), :Value, 0) + Yast::UI.ChangeWidget(Id(PKG_INSTALL_WIDGET), :Label, label) + else + # TRANSLATORS: help text for the file conflict detection progress + help = _("

Detecting the file conflicts is in progress.

") + Yast::Progress.Simple(label, "", 100, help) + # Set also the progress bar widget label + Yast::Progress.Title(label) + end + end + + # Handle the file conflict detection progress callback. + # @param [Fixnum] progress progress in percents + # @return [Boolean] true = continue, false = abort + def progress(progress) + log.debug "File conflict progress: #{progress}%" + + if Yast::Mode.commandline + Yast::CommandLine.PrintVerboseNoCR("#{Yast::PackageCallbacksClass::CLEAR_PROGRESS_TEXT}#{progress}%") + elsif pkg_installation? + Yast::UI.ChangeWidget(Id(PKG_INSTALL_WIDGET), :Value, progress) + else + Yast::Progress.Step(progress) + end + + ui = Yast::UI.PollInput unless Yast::Mode.commandline + log.info "User input in file conflict progress (#{progress}%): #{ui}" if ui + + ui != :abort && ui != :cancel + end + + # Handle the file conflict detection result callback. + # Ask to user whether to continue. In the AutoYaST mode an error is reported + # but the installation will continue ignoring the confliucts. + # @param excluded_packages [Array] packages ignored in the check + # (e.g. not available for check in the download-as-needed mode) + # @param conflicts [Array] list of translated descriptions of + # the detected file conflicts + # @return [Boolean] true = continue, false = abort + def report(excluded_packages, conflicts) + log.info "Excluded #{excluded_packages.size} packages in file conflict check" + log.debug "Excluded packages: #{excluded_packages.inspect}" + log.info "Found #{conflicts.size} conflicts: #{conflicts.join("\n\n")}" + + # just continue installing packages if there is no conflict + return true if conflicts.empty? + + # don't ask in autoyast or command line mode, just report/log the issues and continue + if Yast::Mode.auto || Yast::Mode.commandline + # TRANSLATORS: An error message, %s is the actual list of detected conflicts + Yast::Report.Error(_("File conflicts detected, these conflicting files will " \ + "be overwritten:\n\n%s") % conflicts.join("\n")) + return true + end + + Yast::UI.OpenDialog(dialog(conflicts)) + + begin + Yast::UI.SetFocus(Id(:continue)) + ret = Yast::UI.UserInput + log.info "User Input: #{ret}" + ret == :continue + ensure + Yast::UI.CloseDialog + end + end + + # Handle the file conflict detection finish callback. + def finish + log.info "File conflict check finished" + return if Yast::Mode.commandline + + # finish the opened progress dialog + Yast::Progress.Finish unless pkg_installation? + end + + # Construct the file conflicts dialog. + # @param [Array] conflicts file conflicts reported by libzypp + # (in human readable form) + # @return [Term] UI term + def dialog(conflicts) + button_box = ButtonBox( + PushButton(Id(:continue), Opt(:default, :okButton), Yast::Label.ContinueButton), + PushButton(Id(:abort), Opt(:cancelButton), Yast::Label.AbortButton) + ) + + # TRANSLATORS: A popup label, use max. 70 chars per line, use more lines if needed + label = _("File conflicts happen when two packages attempt to install\n" \ + "files with the same name but different contents. If you continue,\n" \ + "the conflicting files will be replaced losing the previous content.") + + # TRANSLATORS: Popup heading + heading = n_("A File Conflict Detected", "File Conflicts Detected", conflicts.size) + + VBox( + Left(Heading(heading)), + VSpacing(0.2), + Left(Label(label)), + MinSize(65, 15, RichText(Opt(:plainText), conflicts.join("\n\n"))), + button_box + ) + end + end + end +end diff --git a/library/packages/src/modules/PackageCallbacks.rb b/library/packages/src/modules/PackageCallbacks.rb index 949a73d35..f4c02b3cb 100644 --- a/library/packages/src/modules/PackageCallbacks.rb +++ b/library/packages/src/modules/PackageCallbacks.rb @@ -25,6 +25,7 @@ require "yast" require "uri" require "packages/dummy_callbacks" +require "packages/file_conflict_callbacks" module Yast # Provides the default Callbacks for Pkg:: @@ -2759,8 +2760,14 @@ def SetProgressReportCallbacks nil end + def SetFileConflictCallbacks + log.warn "Registering file conflict callbacks" + ::Packages::FileConflictCallbacks.register + end + # Register package manager callbacks def InitPackageCallbacks + log.warn "*** INIT Registering callbacks" SetProcessCallbacks() SetProvideCallbacks() @@ -2775,6 +2782,8 @@ def InitPackageCallbacks SetProgressReportCallbacks() + SetFileConflictCallbacks() + # authentication callback Pkg.CallbackAuthentication( fun_ref( diff --git a/library/packages/test/Makefile.am b/library/packages/test/Makefile.am index d02cc2d3e..2a83ec62b 100644 --- a/library/packages/test/Makefile.am +++ b/library/packages/test/Makefile.am @@ -1,6 +1,7 @@ TESTS = \ commit_result_test.rb \ dummy_callbacks_test.rb \ + file_conflict_callbacks_test.rb \ package_callbacks_test.rb \ packages_ui_test.rb \ product_test.rb \ diff --git a/library/packages/test/file_conflict_callbacks_test.rb b/library/packages/test/file_conflict_callbacks_test.rb new file mode 100755 index 000000000..e1a7e2d30 --- /dev/null +++ b/library/packages/test/file_conflict_callbacks_test.rb @@ -0,0 +1,299 @@ +#! /usr/bin/env rspec + +require_relative "test_helper" + +require "packages/file_conflict_callbacks" + +# a helper class to replace Yast::Pkg +class DummyPkg + # remember the registered file conflict callback handlers to test them later + attr_reader :fc_start, :fc_progress, :fc_report, :fc_finish + + def CallbackFileConflictStart(func) + @fc_start = func + end + + def CallbackFileConflictProgress(func) + @fc_progress = func + end + + def CallbackFileConflictReport(func) + @fc_report = func + end + + def CallbackFileConflictFinish(func) + @fc_finish = func + end +end + +describe Packages::FileConflictCallbacks do + let(:dummy_pkg) { DummyPkg.new } + + before do + # catch all callbacks registration calls via this Pkg replacement + stub_const("Yast::Pkg", dummy_pkg) + + # stub console printing + Yast.import "CommandLine" + allow(Yast::CommandLine).to receive(:Print) + allow(Yast::CommandLine).to receive(:PrintVerbose) + allow(Yast::CommandLine).to receive(:PrintVerboseNoCR) + end + + describe ".register" do + it "calls the Pkg methods for registering the file conflicts handlers" do + expect(dummy_pkg).to receive(:CallbackFileConflictStart) + expect(dummy_pkg).to receive(:CallbackFileConflictProgress) + expect(dummy_pkg).to receive(:CallbackFileConflictReport) + expect(dummy_pkg).to receive(:CallbackFileConflictFinish) + + Packages::FileConflictCallbacks.register + end + end + + describe "the registered start callback handler" do + let(:start_cb) do + Packages::FileConflictCallbacks.register + dummy_pkg.fc_start + end + + context "in the command line mode" do + before do + allow(Yast::Mode).to receive(:commandline).and_return(true) + end + + it "does not call any UI method" do + ui = double("no method call expected") + stub_const("Yast::UI", ui) + + start_cb.call + end + end + + context "in UI mode" do + it "reuses the package installation progress" do + expect(Yast::UI).to receive(:WidgetExists).and_return(true) + expect(Yast::UI).to receive(:ChangeWidget).twice + + start_cb.call + end + + it "opens a new progress if installation progress was not displayed" do + expect(Yast::UI).to receive(:WidgetExists).and_return(false) + expect(Yast::Progress).to receive(:Simple) + allow(Yast::Progress).to receive(:Title) + + start_cb.call + end + end + end + + describe "the registered progress callback handler" do + let(:progress_cb) do + Packages::FileConflictCallbacks.register + dummy_pkg.fc_progress + end + + # fake progress value (percent) + let(:progress) { 42 } + + context "in the command line mode" do + before do + allow(Yast::Mode).to receive(:commandline).and_return(true) + end + + it "does not call any UI method" do + ui = double("no method call expected") + stub_const("Yast::UI", ui) + + progress_cb.call(progress) + end + + it "prints the current progress" do + expect(Yast::CommandLine).to receive(:PrintVerboseNoCR).with(/42%/) + + progress_cb.call(progress) + end + + it "returns true to continue" do + expect(progress_cb.call(progress)).to eq(true) + end + end + + context "in UI mode" do + it "returns false to abort if user clicks Abort" do + expect(Yast::UI).to receive(:PollInput).and_return(:abort) + + expect(progress_cb.call(progress)).to eq(false) + end + + it "returns true to continue when no user input" do + expect(Yast::UI).to receive(:PollInput).and_return(nil) + + expect(progress_cb.call(progress)).to eq(true) + end + + it "returns true to continue on unknown user input" do + expect(Yast::UI).to receive(:PollInput).and_return(:next) + + expect(progress_cb.call(progress)).to eq(true) + end + + it "uses the existing widget if package installation progress was displayed" do + expect(Yast::UI).to receive(:WidgetExists).and_return(true) + expect(Yast::UI).to receive(:ChangeWidget) + + progress_cb.call(progress) + end + + it "sets the progress if package installation progress was not displayed" do + expect(Yast::UI).to receive(:WidgetExists).and_return(false) + expect(Yast::Progress).to receive(:Step).with(progress) + + progress_cb.call(progress) + end + end + end + + describe "the registered report callback handler" do + let(:report_cb) do + Packages::FileConflictCallbacks.register + dummy_pkg.fc_report + end + + context "no conflict found" do + let(:conflicts) { [] } + let(:excluded) { [] } + + before do + allow(Yast::Mode).to receive(:commandline).and_return(true) + end + + it "does not check the command line mode, it behaves same as in the UI mode" do + expect(Yast::Mode).to_not receive(:commandline) + report_cb.call(excluded, conflicts) + end + + it "does not call any UI method" do + ui = double("no method call expected") + stub_const("Yast::UI", ui) + + report_cb.call(excluded, conflicts) + end + + it "returns true to continue" do + expect(report_cb.call(excluded, conflicts)).to eq(true) + end + end + + context "conflicts found" do + let(:conflicts) { ["conflict1!", "conflict2!"] } + let(:excluded) { [] } + + context "in the command line mode" do + before do + allow(Yast::Mode).to receive(:commandline).and_return(true) + end + + it "does not call any UI method" do + ui = double("no method call expected") + stub_const("Yast::UI", ui) + + report_cb.call(excluded, conflicts) + end + + it "prints the found conflicts" do + expect(Yast::Report).to receive(:Error) + + report_cb.call(excluded, conflicts) + end + + it "returns true to continue" do + expect(report_cb.call(excluded, conflicts)).to eq(true) + end + end + + context "in AutoYaST mode" do + before do + expect(Yast::Mode).to receive(:auto).and_return(true) + allow(Yast::Report).to receive(:Error) + end + + it "reporrts the found conflicts" do + expect(Yast::Report).to receive(:Error) + + report_cb.call(excluded, conflicts) + end + + it "returns true to continue" do + expect(report_cb.call(excluded, conflicts)).to eq(true) + end + end + + context "in UI mode" do + before do + allow(Yast::UI).to receive(:OpenDialog) + allow(Yast::UI).to receive(:CloseDialog) + allow(Yast::UI).to receive(:SetFocus) + end + + it "opens a Popup dialog, waits for user input and closes the dialog" do + expect(Yast::UI).to receive(:OpenDialog).ordered + expect(Yast::UI).to receive(:UserInput).ordered + expect(Yast::UI).to receive(:CloseDialog).ordered + + report_cb.call(excluded, conflicts) + end + + it "returns false to abort if user clicks Abort" do + expect(Yast::UI).to receive(:UserInput).and_return(:abort) + + expect(report_cb.call(excluded, conflicts)).to eq(false) + end + + it "returns true to continue if user clicks Continue" do + expect(Yast::UI).to receive(:UserInput).and_return(:continue) + + expect(report_cb.call(excluded, conflicts)).to eq(true) + end + end + end + end + + describe "the registered finish callback handler" do + let(:finish_cb) do + Packages::FileConflictCallbacks.register + dummy_pkg.fc_finish + end + + context "in the command line mode" do + before do + allow(Yast::Mode).to receive(:commandline).and_return(true) + end + + it "does not call any UI method" do + ui = double("no method call expected") + stub_const("Yast::UI", ui) + + finish_cb.call + end + end + + context "in UI mode" do + it "no change if installation progress was already displayed" do + ui = double("no method call expected", WidgetExists: true) + stub_const("Yast::UI", ui) + + finish_cb.call + end + + it "closes progress if installation progress was not displayed" do + expect(Yast::UI).to receive(:WidgetExists).and_return(false) + expect(Yast::Progress).to receive(:Finish) + + finish_cb.call + end + + end + end +end diff --git a/package/yast2-rpmlintrc b/package/yast2-rpmlintrc index 0d7a1fc0f..0f86a54ef 100644 --- a/package/yast2-rpmlintrc +++ b/package/yast2-rpmlintrc @@ -1 +1 @@ -addFilter("invalid-desktopfile") +addFilter("desktopfile-without-binary")