Hello community,
here is the log from the commit of package yast2-installation for
openSUSE:Factory checked in at 2015-07-05 17:52:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/yast2-installation (Old)
and /work/SRC/openSUSE:Factory/.yast2-installation.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "yast2-installation"
Changes:
--------
--- /work/SRC/openSUSE:Factory/yast2-installation/yast2-installation.changes
2015-06-03 08:27:46.000000000 +0200
+++
/work/SRC/openSUSE:Factory/.yast2-installation.new/yast2-installation.changes
2015-07-05 17:52:01.000000000 +0200
@@ -1,0 +2,29 @@
+Wed Jul 1 10:46:50 CEST 2015 - [email protected]
+
+- Fixed handling user request to change an installation proposal
+ (bsc#936448)
+- 3.1.148
+
+-------------------------------------------------------------------
+Mon Jun 29 13:11:57 UTC 2015 - [email protected]
+
+- fixed menu button label in the proposal (bsc#936427)
+- 3.1.147
+
+-------------------------------------------------------------------
+Mon Jun 29 08:41:17 UTC 2015 - [email protected]
+
+- add ability to hide export button (fate#315161)
+- 3.1.146
+
+-------------------------------------------------------------------
+Wed Jun 17 09:29:09 CEST 2015 - [email protected]
+
+- Implemented triggers for installation proposal (FATE#317488).
+ Any *_proposal client can define 'trigger' in 'MakeProposal'
+ that defines in which circumstances it should be called again
+ after all proposals have been called, e.g., if partitioning or
+ software selection changes.
+- 3.1.145
+
+-------------------------------------------------------------------
Old:
----
yast2-installation-3.1.144.tar.bz2
New:
----
yast2-installation-3.1.148.tar.bz2
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ yast2-installation.spec ++++++
--- /var/tmp/diff_new_pack.MtreG3/_old 2015-07-05 17:52:02.000000000 +0200
+++ /var/tmp/diff_new_pack.MtreG3/_new 2015-07-05 17:52:02.000000000 +0200
@@ -17,7 +17,7 @@
Name: yast2-installation
-Version: 3.1.144
+Version: 3.1.148
Release: 0
BuildRoot: %{_tmppath}/%{name}-%{version}-build
++++++ yast2-installation-3.1.144.tar.bz2 -> yast2-installation-3.1.148.tar.bz2
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/yast2-installation-3.1.144/doc/proposal_api.md
new/yast2-installation-3.1.148/doc/proposal_api.md
--- old/yast2-installation-3.1.144/doc/proposal_api.md 2015-06-02
10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/doc/proposal_api.md 2015-07-01
10:59:37.000000000 +0200
@@ -88,6 +88,25 @@
* _string_ `help` (optional) Helptext for this module which appears in the
standard dialog
help (particular helps for modules sorted by presentation order).
+* _map_ `trigger` defines circumstances when the proposal should be called
again at the end.
+ For intance, when partitioning or software selection changes.
+ Mandatory keys of the trigger are:
+
+ * _map_ `expect` containing _string_ `class` and _string_ `method` that will
be called and its result compared with `value`
+ * _any_ `value` expected value, if the evaluated code does not match the
`value`, proposal will be called again
+
+ Example:
+
+ {
+ "trigger" => {
+ "expect" => {
+ "class" => "Yast::Packages",
+ "method" => "CountSizeToBeDownloaded"
+ }
+ "value" => 88883333
+ }
+ }
+
### AskUser
Run an interactive workflow - let user decide upon values he might want to
change.
May contain one single dialog, a sequence of dialogs or one master dialog with
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/yast2-installation-3.1.144/package/yast2-installation.changes
new/yast2-installation-3.1.148/package/yast2-installation.changes
--- old/yast2-installation-3.1.144/package/yast2-installation.changes
2015-06-02 10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/package/yast2-installation.changes
2015-07-01 10:59:37.000000000 +0200
@@ -1,4 +1,33 @@
-------------------------------------------------------------------
+Wed Jul 1 10:46:50 CEST 2015 - [email protected]
+
+- Fixed handling user request to change an installation proposal
+ (bsc#936448)
+- 3.1.148
+
+-------------------------------------------------------------------
+Mon Jun 29 13:11:57 UTC 2015 - [email protected]
+
+- fixed menu button label in the proposal (bsc#936427)
+- 3.1.147
+
+-------------------------------------------------------------------
+Mon Jun 29 08:41:17 UTC 2015 - [email protected]
+
+- add ability to hide export button (fate#315161)
+- 3.1.146
+
+-------------------------------------------------------------------
+Wed Jun 17 09:29:09 CEST 2015 - [email protected]
+
+- Implemented triggers for installation proposal (FATE#317488).
+ Any *_proposal client can define 'trigger' in 'MakeProposal'
+ that defines in which circumstances it should be called again
+ after all proposals have been called, e.g., if partitioning or
+ software selection changes.
+- 3.1.145
+
+-------------------------------------------------------------------
Tue Jun 2 08:41:03 UTC 2015 - [email protected]
- fix crash in Upgrade when creating post upgrade snapshot
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/yast2-installation-3.1.144/package/yast2-installation.spec
new/yast2-installation-3.1.148/package/yast2-installation.spec
--- old/yast2-installation-3.1.144/package/yast2-installation.spec
2015-06-02 10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/package/yast2-installation.spec
2015-07-01 10:59:37.000000000 +0200
@@ -17,7 +17,7 @@
Name: yast2-installation
-Version: 3.1.144
+Version: 3.1.148
Release: 0
BuildRoot: %{_tmppath}/%{name}-%{version}-build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/yast2-installation-3.1.144/src/lib/installation/proposal_runner.rb
new/yast2-installation-3.1.148/src/lib/installation/proposal_runner.rb
--- old/yast2-installation-3.1.144/src/lib/installation/proposal_runner.rb
2015-06-02 10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/src/lib/installation/proposal_runner.rb
2015-07-01 10:59:37.000000000 +0200
@@ -74,6 +74,9 @@
return :auto
end
+ args = (Yast::WFM.Args || []).first || {}
+ @hide_export = args["hide_export"]
+
log.info "Installation step #2"
@proposal_mode = Yast::GetInstArgs.proposal
@@ -653,8 +656,10 @@
change_point = ReplacePoint(
Id(:rep_menu),
# menu button
- MenuButton(Id(:menu_dummy), _("&Yast::Change..."), [Item(Id(:dummy),
"")])
+ MenuButton(Id(:menu_dummy), _("&Change..."), [Item(Id(:dummy), "")])
)
+ elsif @hide_export
+ change_point = Empty()
else
change_point = PushButton(
Id(:export_config),
@@ -706,7 +711,7 @@
Label(
if Yast::UI.TextMode()
_(
- "Click a headline to make changes or use the
\"Yast::Change...\" menu below."
+ "Click a headline to make changes or use the \"Change...\"
menu below."
)
else
_(
@@ -745,7 +750,7 @@
# now build the menu button
menu_list = @submodules_presentation.each_with_object([]) do |submod,
menu|
- descr = @store.descriptions[submod] || {}
+ descr = @store.description_for(submod) || {}
next if descr.empty?
id = descr["id"]
@@ -769,8 +774,8 @@
end
# menu button item
- menu_list << Item(Id(:reset_to_defaults), _("&Reset to defaults")) <<
- Item(Id(:export_config), _("&Export Configuration"))
+ menu_list << Item(Id(:reset_to_defaults), _("&Reset to defaults"))
+ menu_list << Item(Id(:export_config), _("&Export Configuration")) unless
@hide_export
# menu button
Yast::UI.ReplaceWidget(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/yast2-installation-3.1.144/src/lib/installation/proposal_store.rb
new/yast2-installation-3.1.148/src/lib/installation/proposal_store.rb
--- old/yast2-installation-3.1.144/src/lib/installation/proposal_store.rb
2015-06-02 10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/src/lib/installation/proposal_store.rb
2015-07-01 10:59:37.000000000 +0200
@@ -29,7 +29,12 @@
include Yast::Logger
include Yast::I18n
- # @ param[String] proposal_mode one of initial, service, network, hardware,
+ # How many times to maximally (re)run the proposal while some proposal
clients
+ # try to re-trigger their run again, number includes their initial run
+ # and resets before each proposal loop starts
+ MAX_LOOPS_IN_PROPOSAL = 8
+
+ # @param [String] proposal_mode one of initial, service, network, hardware,
# uml, ... or anything else
def initialize(proposal_mode)
Yast.import "Mode"
@@ -131,8 +136,13 @@
@proposal_names.map!(&:first) # first element is name of client
- # FIXME: add filter to only installed clients
- @proposal_names
+ missing_proposals = @proposal_names.reject { |proposal|
Yast::WFM::ClientExists(proposal) }
+ unless missing_proposals.empty?
+ log.warn "These proposals are missing on system: #{missing_proposals}"
+ end
+
+ # Filter missing proposals out
+ @proposal_names -= missing_proposals
end
# returns single list of modules presentation order or list of tabs with
list of modules
@@ -145,73 +155,85 @@
# Makes proposal for all proposal clients.
# @param callback Called after each client/part, to report progress. Gets
# part name and part result as arguments
- def make_proposals(force_reset: false, language_changed: false, callback:
Proc.new)
- @link2submod = {}
+ def make_proposals(force_reset: false, language_changed: false, callback:
proc {})
+ clear_proposals
- proposal_names.each do |submod|
- proposal_map = make_proposal(submod, force_reset: force_reset,
- language_changed:
language_changed)
-
- callback.call(submod, proposal_map)
-
- # update link map
- (proposal_map["links"] || []).each do |link|
- @link2submod[link] = submod
- end
+ # At first run, all clients will be called
+ call_proposals = proposal_names
+ log.info "Proposals to call: #{call_proposals}"
+
+ loop do
+ call_proposals.each do |client|
+ description_map = make_proposal(client, force_reset: force_reset,
+ language_changed: language_changed, callback: callback)
- if proposal_map["language_changed"]
- @descriptions = nil # invalid descriptions cache
- return make_proposals(force_reset: force_reset, language_changed:
true)
+ break unless parse_description_map(client, description_map,
force_reset, callback)
end
- break if proposal_map["warning_level"] == :fatal
- end
- end
-
- # Calls all clients/parts to retrieve the description
- # @return [Hash{String => Hash}] map client/part names to hashes with keys
- # "id", "menu_title" "rich_text_title"
http://www.rubydoc.info/github/yast/yast-yast2/Installation/ProposalClient:description
- def descriptions
- return @descriptions if @descriptions
+ # Second and next runs: only triggered clients will be called
+ call_proposals = proposal_names.select { |client|
should_be_called_again?(client) }
- missing_no = 1
- @id_mapping = {}
- @descriptions = proposal_names.each_with_object({}) do |client, res|
- description = Yast::WFM.CallFunction(client, ["Description", {}])
- if !description["id"]
- log.warn "proposal client #{client} missing key 'id' in
#{description}"
+ break if call_proposals.empty?
+ log.info "These proposals want to be called again: #{call_proposals}"
- description["id"] = "module_#{missing_no}"
- missing_no += 1
+ unless should_run_proposals_again?(call_proposals)
+ log.warn "Too many loops in proposal, exiting"
+ break
end
+ end
- @id_mapping[description["id"]] = client
+ log.info "Making proposals have finished"
+ end
- res[client] = description
+ # Calls a given client/part to retrieve their description
+ # @return [Hash] with keys "id", "menu_title" "rich_text_title"
+ # @see
http://www.rubydoc.info/github/yast/yast-yast2/Installation/ProposalClient:description
+ def description_for(client)
+ @descriptions ||= {}
+ return @descriptions[client] if @descriptions.key?(client)
+
+ description = Yast::WFM.CallFunction(client, ["Description", {}])
+
+ unless description.key?("id")
+ log.warn "proposal client #{client} is missing key 'id' in
#{description}"
+ @missing_no ||= 1
+ description["id"] = "module_#{@missing_no}"
+ @missing_no += 1
end
+
+ @descriptions[client] = description
+ end
+
+ # Returns all currently cached client descriptions
+ #
+ # @return [Hash] with descriptions
+ def descriptions
+ @descriptions ||= {}
end
+ # Returns ID for given client
+ #
# @return [String] an id provided by the description API
def id_for(client)
- descriptions[client]["id"]
+ description_for(client).fetch("id", client)
end
+ # Returns UI title for given client
+ #
+ # @param [String] client
+ # @return [String] a title provided by the description API
def title_for(client)
- descriptions[client]["rich_text_title"] ||
- descriptions[client]["rich_text_raw_title"] ||
+ description = description_for(client)
+
+ description["rich_text_title"] ||
+ description["rich_text_raw_title"] ||
client
end
- # Calls `ask_user`, to change a setting interactively (if link is the
+ # Calls client('AskUser'), to change a setting interactively (if link is
the
# heading for the part) or noninteractively (if it is a "shortcut")
def handle_link(link)
- client = @id_mapping[link]
- client ||= @link2submod[link]
-
- if !client
- log.error "unknown link #{link}. Broken proposal client?"
- return nil
- end
+ client = client_for_link(link)
data = {
"has_next" => false,
@@ -221,8 +243,143 @@
Yast::WFM.CallFunction(client, ["AskUser", data])
end
+ # Returns client name that handles the given link returned by UI,
+ # raises exception if link is unknown.
+ # Link can be either the client ID or a shortcut link from proposal text.
+ #
+ # @param [String] link ID
+ # @return [String] client name
+ def client_for_link(link)
+ raise "There are no client proposals known, call 'client(MakeProposal)'
first" if @proposals.nil?
+
+ matching_client = @proposals.find do |_client, proposal|
+ link == proposal["id"] || proposal.fetch("links", []).include?(link)
+ end
+
+ raise "Unknown user request #{link}. Broken proposal client?" if
matching_client.nil?
+
+ matching_client.first
+ end
+
private
+ # Evaluates the given description map, and handles all the events
+ # by returning whether to continue in the current proposal loop
+ # Also stores proposals for later use
+ #
+ # @return [Boolean] whether to continue with iteration over proposals
+ def parse_description_map(client, description_map, force_reset, callback)
+ raise "Invalid proposal from client #{client}" if description_map.nil?
+
+ if description_map["warning_level"] == :fatal
+ log.error "There is an error in the proposal"
+ return false
+ end
+
+ if description_map["language_changed"]
+ log.info "Language changed, reseting proposal"
+ # Invalidate all descriptions at once, they will be lazy-loaded again
with new translations
+ invalidate_description
+ make_proposals(force_reset: force_reset, language_changed: true,
callback: callback)
+ return false
+ end
+
+ description_map["id"] = id_for(client)
+
+ @proposals ||= {}
+ @proposals[client] = description_map
+
+ true
+ end
+
+ def clear_proposals
+ @proposals_run_counter = {}
+ @proposals = {}
+ end
+
+ # Updates internal counter that holds information how many times
+ # has been each proposal called during the current make_proposals run
+ def update_proposals_counter(proposals)
+ @proposals_run_counter ||= {}
+
+ proposals.each do |proposal|
+ @proposals_run_counter[proposal] ||= 0
+ @proposals_run_counter[proposal] += 1
+ end
+ end
+
+ # Finds out whether we can call given proposals again during
+ # the current make_proposals run
+ def should_run_proposals_again?(proposals)
+ update_proposals_counter(proposals)
+
+ log.info "Proposal counters: #{@proposals_run_counter}"
+ @proposals_run_counter.values.max < MAX_LOOPS_IN_PROPOSAL
+ end
+
+ # Returns whether given trigger definition is correct
+ # e.g., all mandatory parts are there
+ #
+ # @param [Hash] trigger definition
+ # @rturn [Boolean] whether it is correct
+ def valid_trigger?(trigger_def)
+ trigger_def.key?("expect") &&
+ trigger_def["expect"].is_a?(Hash) &&
+ trigger_def["expect"].key?("class") &&
+ trigger_def["expect"]["class"].is_a?(String) &&
+ trigger_def["expect"].key?("method") &&
+ trigger_def["expect"]["method"].is_a?(String) &&
+ trigger_def.key?("value")
+ end
+
+ # Returns whether given client should be called again during 'this'
+ # proposal run according to triggers in proposals
+ #
+ # @param [String] client name
+ # @return [Boolean] whether it should be called
+ def should_be_called_again?(client)
+ @proposals ||= {}
+ return false unless @proposals.fetch(client, {}).key?("trigger")
+
+ trigger = @proposals[client]["trigger"]
+
+ raise "Incorrect definition of 'trigger': #{trigger.inspect} \n" \
+ "both [Hash] 'expect', including keys [Symbol] 'class' and [Symbol]
'method', \n" \
+ "and [Any] 'value' must be set" unless valid_trigger?(trigger)
+
+ expectation_class = trigger["expect"]["class"]
+ expectation_method = trigger["expect"]["method"]
+ expectation_value = trigger["value"]
+
+ log.info "Calling
#{expectation_class}.send(#{expectation_method.inspect})"
+
+ begin
+ value = Object.const_get(expectation_class).send(expectation_method)
+ rescue StandardError, ScriptError => error
+ raise "Checking the trigger expectations for #{client} have
failed:\n#{error}"
+ end
+
+ if value == expectation_value
+ log.info "Proposal client #{client}: returned value matches
expectation #{value.inspect}"
+ return false
+ else
+ log.info "Proposal client #{client}: returned value #{value.inspect} "
\
+ "does not match expected value #{expectation_value.inspect}"
+ return true
+ end
+ end
+
+ # Invalidates proposal description coming from a given client
+ #
+ # @param [String] client or nil for all descriptions
+ def invalidate_description(client = nil)
+ if client.nil?
+ @descriptions = {}
+ else
+ @descriptions.delete(client)
+ end
+ end
+
def properties
@proposal_properties ||= Yast::ProductControl.getProposalProperties(
Yast::Stage.stage,
@@ -231,7 +388,7 @@
)
end
- def make_proposal(client, force_reset: false, language_changed: false)
+ def make_proposal(client, force_reset: false, language_changed: false,
callback: proc {})
proposal = Yast::WFM.CallFunction(
client,
[
@@ -245,6 +402,9 @@
log.debug "#{client} MakeProposal() returns #{proposal}"
+ raise "Callback is not a block: #{callback.class}" unless callback.is_a?
Proc
+ callback.call(client, proposal)
+
proposal
end
@@ -352,9 +512,8 @@
modules_order = modules_order[current_tab]
modules_order.each_with_object("") do |client, text|
- if descriptions[client] && !descriptions[client]["help"].to_s.empty?
- text << descriptions[client]["help"]
- end
+ description = description_for(client)
+ text << description["help"] if description["help"]
end
else
""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/yast2-installation-3.1.144/test/proposal_store_test.rb
new/yast2-installation-3.1.148/test/proposal_store_test.rb
--- old/yast2-installation-3.1.144/test/proposal_store_test.rb 2015-06-02
10:54:08.000000000 +0200
+++ new/yast2-installation-3.1.148/test/proposal_store_test.rb 2015-07-01
10:59:37.000000000 +0200
@@ -5,6 +5,7 @@
require "installation/proposal_store"
Yast.import "ProductControl"
+Yast.import "Installation"
describe ::Installation::ProposalStore do
subject { ::Installation::ProposalStore.new "initial" }
@@ -88,7 +89,13 @@
end
describe "#proposal_names" do
+ before do
+ allow(Yast::WFM).to receive(:ClientExists).and_return(true)
+ end
+
it "returns array with string names of clients" do
+ allow(Yast::WFM).to
receive(:ClientExists).with(/test3/).and_return(false)
+
allow(Yast::ProductControl).to receive(:getProposals)
.and_return([
["test1"],
@@ -98,7 +105,7 @@
expect(subject.proposal_names).to include("test1")
expect(subject.proposal_names).to include("test2")
- expect(subject.proposal_names).to include("test3")
+ expect(subject.proposal_names).not_to include("test3")
end
it "use same order as in control file to preserve evaluation order" do
@@ -183,4 +190,306 @@
end
end
end
+
+ let(:proposal_names) { ["proposal_a", "proposal_b", "proposal_c"] }
+
+ let(:proposal_a) do
+ {
+ "rich_text_title" => "Proposal A",
+ "menu_title" => "&Proposal A",
+ "id" => "proposal_a"
+ }
+ end
+
+ let(:proposal_a_desc) do
+ {
+ "preformatted_proposal" => "Values proposed for A",
+ "links" => ["proposal_a-link_1", "proposal_a-link_2"]
+ }
+ end
+
+ let(:proposal_a_expected_val) { "/" }
+
+ let(:proposal_a_desc_with_trigger) do
+ {
+ "preformatted_proposal" => "Values proposed for A",
+ "links" => ["proposal_a-link_1", "proposal_a-link_2"],
+ "trigger" => {
+ "expect" => {
+ "class" => "Yast::Installation",
+ "method" => "destdir"
+ },
+ "value" => proposal_a_expected_val
+ }
+ }
+ end
+
+ let(:proposal_b) do
+ {
+ "rich_text_title" => "Proposal B",
+ "menu_title" => "&Proposal B",
+ "id" => "proposal_b"
+ }
+ end
+
+ let(:proposal_b_desc) do
+ {
+ "preformatted_proposal" => "Values proposed for B"
+ }
+ end
+
+ let(:proposal_b_desc_with_language_change) do
+ {
+ "preformatted_proposal" => "Values proposed for B",
+ "language_changed" => true
+ }
+ end
+
+ let(:proposal_b_desc_with_fatal_error) do
+ {
+ "preformatted_proposal" => "Values proposed for A",
+ "warning_level" => :fatal,
+ "warning" => "some fatal error"
+ }
+ end
+
+ let(:proposal_c) do
+ {
+ "rich_text_title" => "Proposal C",
+ "menu_title" => "&Proposal C"
+ }
+ end
+
+ let(:proposal_c_desc) do
+ {
+ "preformatted_proposal" => "Values proposed for C"
+ }
+ end
+
+ let(:proposal_c_desc_with_incorrect_trigger) do
+ {
+ "preformatted_proposal" => "Values proposed for C",
+ "trigger" => {
+ # 'expect' must be a string that is evaluated later
+ "expect" => 333,
+ "value" => "anything"
+ }
+ }
+ end
+
+ let(:proposal_c_desc_with_exception) do
+ {
+ "preformatted_proposal" => "Values proposed for C",
+ "trigger" => {
+ # 'expect' must be a string that is evaluated later
+ "expect" => {
+ "class" => "Erroneous",
+ "method" => "big_mistake"
+ },
+ "value" => 22
+ }
+ }
+ end
+
+ describe "#make_proposals" do
+ before do
+ allow(subject).to receive(:proposal_names).and_return(proposal_names)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_a",
["Description", anything]).and_return(proposal_a)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["Description", anything]).and_return(proposal_b)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_c",
["Description", anything]).and_return(proposal_c)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_a",
["MakeProposal", anything]).and_return(proposal_a_desc)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["MakeProposal", anything]).and_return(proposal_b_desc)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_c",
["MakeProposal", anything]).and_return(proposal_c_desc)
+ end
+
+ context "when all proposals return correct data" do
+ it "for each proposal client, calls given callback and creates new
proposal" do
+ @callback = 0
+ callback = proc { @callback += 1 }
+
+ expect { subject.make_proposals(callback: callback) }.not_to
raise_exception
+ expect(@callback).to eq(proposal_names.size)
+ end
+ end
+
+ context "when some proposal returns invalid data (e.g. crashes)" do
+ it "raises an exception" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
anything).and_return(nil)
+
+ expect { subject.make_proposals }.to raise_exception(/Invalid proposal
from client/)
+ end
+ end
+
+ context "when given callback is not a block" do
+ it "raises an exception" do
+ expect { subject.make_proposals(callback: 4) }.to
raise_exception(/Callback is not a block/)
+ end
+ end
+
+ context "when returned proposal contains a 'trigger' section" do
+ it "for each proposal client, creates new proposal and calls the client
while trigger evaluates to true" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_a",
anything).and_return(proposal_a_desc_with_trigger)
+
+ # Mock evaluation of the trigger
+ allow(Yast::Installation).to receive(:destdir).and_return("/x", "/y",
proposal_a_expected_val)
+
+ # 1. initial call 2. (...) via trigger
+ expect(subject).to receive(:make_proposal).with("proposal_a",
anything).exactly(3).times.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_b",
anything).exactly(1).times.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_c",
anything).exactly(1).times.and_call_original
+
+ subject.make_proposals
+ end
+ end
+
+ context "when returned proposal triggers changing a language" do
+ it "calls all proposals again with language_changed: true" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["MakeProposal", anything]).and_return(proposal_b_desc_with_language_change,
proposal_b_desc)
+
+ # Call proposals till the one that changes the language
+ expect(subject).to receive(:make_proposal).with("proposal_a",
hash_including(language_changed: false)).once.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_b",
hash_including(language_changed: false)).once.and_call_original
+
+ # Call all again with language_changed: true
+ expect(subject).to receive(:make_proposal).with("proposal_a",
hash_including(language_changed: true)).once.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_b",
hash_including(language_changed: true)).once.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_c",
hash_including(language_changed: true)).once.and_call_original
+
+ subject.make_proposals
+ end
+ end
+
+ context "when returned proposal contains a fatal error" do
+ it "calls all proposals till fatal error is received, then it stops
proceeding immediately" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["MakeProposal", anything]).and_return(proposal_b_desc_with_fatal_error)
+
+ expect(subject).to receive(:make_proposal).with("proposal_a",
anything).once.and_call_original
+ expect(subject).to receive(:make_proposal).with("proposal_b",
anything).once.and_call_original
+ # Proposal C is never called, as it goes after proposal B
+ expect(subject).not_to receive(:make_proposal).with("proposal_c",
anything)
+
+ subject.make_proposals
+ end
+ end
+
+ context "when trigger from proposal is incorrectly set" do
+ it "raises an exception" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["MakeProposal", anything]).and_return(proposal_c_desc_with_incorrect_trigger)
+
+ expect { subject.make_proposals }.to raise_error(/Incorrect
definition/)
+ end
+ end
+
+ context "when trigger from proposal raises an exception" do
+ it "raises an exception" do
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_c",
["MakeProposal", anything]).and_return(proposal_c_desc_with_exception)
+
+ expect { subject.make_proposals }.to raise_error(/Checking the trigger
expectations for proposal_c have failed/)
+ end
+ end
+
+ context "When any proposal client wants to retrigger its run more than
MAX_LOOPS_IN_PROPOSAL times" do
+ it "stops iterating over proposals immediately" do
+ allow(subject).to
receive(:should_be_called_again?).with(/proposal_(a|b)/).and_return(false)
+ # Proposal C wants to be called again and again
+ allow(subject).to
receive(:should_be_called_again?).with("proposal_c").and_return(true)
+
+ expect(subject).to receive(:make_proposal).with(/proposal_(a|b)/,
anything).twice.and_call_original
+ # Number of calls including the initial one
+ expect(subject).to receive(:make_proposal).with("proposal_c",
anything).exactly(8).times.and_call_original
+
+ subject.make_proposals
+ end
+ end
+ end
+
+ let(:client_description) do
+ {
+ "rich_text_title" => "Software",
+ "menu_title" => "&Software",
+ "id" => "software"
+ }
+ end
+
+ let(:client_name) { "software_proposal" }
+
+ describe "#description_for" do
+ it "returns description for a given client" do
+ expect(Yast::WFM).to receive(:CallFunction).with(client_name,
["Description", {}]).and_return(client_description).once
+
+ desc1 = subject.description_for(client_name)
+ # description should be cached
+ desc2 = subject.description_for(client_name)
+
+ expect(desc1["id"]).to eq("software")
+ expect(desc2["id"]).to eq("software")
+ end
+ end
+
+ describe "#id_for" do
+ it "returns id for a given client" do
+ allow(subject).to
receive(:description_for).with(client_name).and_return(client_description)
+
+ expect(subject.id_for(client_name)).to eq(client_description["id"])
+ end
+ end
+
+ describe "#title_for" do
+ it "returns title for a given client" do
+ allow(subject).to
receive(:description_for).with(client_name).and_return(client_description)
+
+ expect(subject.title_for(client_name)).to
eq(client_description["rich_text_title"])
+ end
+ end
+
+ describe "#handle_link" do
+ before do
+ allow(subject).to receive(:proposal_names).and_return(proposal_names)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_a",
["Description", anything]).and_return(proposal_a)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["Description", anything]).and_return(proposal_b)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_c",
["Description", anything]).and_return(proposal_c)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_a",
["MakeProposal", anything]).and_return(proposal_a_desc)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_b",
["MakeProposal", anything]).and_return(proposal_b_desc)
+ allow(Yast::WFM).to receive(:CallFunction).with("proposal_c",
["MakeProposal", anything]).and_return(proposal_c_desc)
+ end
+
+ context "when client('MakeProposal') has not been called before" do
+ it "raises an exception" do
+ expect { subject.handle_link("proposal_a-link_2") }.to raise_error(/no
client proposals known/)
+ end
+ end
+
+ context "when no client matches the given link" do
+ it "raises an exception" do
+ # Cache some proposals first
+ subject.make_proposals
+
+ expect { subject.handle_link("unknown_link") }.to raise_error(/Unknown
user request/)
+ end
+ end
+
+ context "when client('MakeProposal') has been called before" do
+ context "when handling link from returned proposal" do
+ it "calls a respective client(AskUser) and returns its result" do
+ # Proposals need to be cached first
+ subject.make_proposals
+
+ expect(Yast::WFM).to receive(:CallFunction).with("proposal_a",
+ ["AskUser", { "has_next" => false, "chosen_id" =>
"proposal_a-link_2" }]).and_return(:next)
+ expect(subject.handle_link("proposal_a-link_2")).to eq(:next)
+ end
+ end
+
+ context "when handling link == client id from Description" do
+ it "calls a respective client(AskUser) and returns its result" do
+ # Proposals need to be cached first
+ subject.make_proposals
+
+ expect(Yast::WFM).to receive(:CallFunction).with("proposal_a",
+ ["AskUser", { "has_next" => false, "chosen_id" => "proposal_a"
}]).and_return(:next)
+ expect(subject.handle_link("proposal_a")).to eq(:next)
+ end
+ end
+ end
+ end
end