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


Reply via email to