
#
# testing ruote
#
# Tue Jun  2 18:48:02 JST 2009
#

require File.expand_path('../base', __FILE__)

require 'ruote/participant'


class FtOnErrorTest < Test::Unit::TestCase
  include FunctionalBase

  class TroubleMaker
    include Ruote::LocalParticipant

    def consume(workitem)
      hits = (workitem.fields['hits'] || 0) + 1
      workitem.fields['hits'] = hits
      workitem.trace << "#{hits.to_s}\n"
      raise 'Houston, we have a problem !' if hits == 1
      workitem.trace << 'done.'
      reply(workitem)
    end

    def cancel(fei, flavour)
      # nothing to do
    end
  end

  def test_on_error

    pdef = Ruote.process_definition do
      sequence :on_error => 'catcher' do
        nada
      end
    end

    @dashboard.register_participant :catcher do
      tracer << "caught\n"
    end

    assert_trace('caught', pdef)

    assert_equal 1, logger.log.select { |e| e['action'] == 'fail' }.size
  end

  def test_on_error_unknown_participant_name

    pdef = Ruote.process_definition :name => 'test' do
      participant :mark_started
      sequence :on_error => :mark_failed do
        participant :bogus
      end
      participant :mark_finished
    end

    @dashboard.context.stash[:marks] = []

    @dashboard.register_participant 'mark\_.+' do |workitem|
      stash[:marks] << workitem.participant_name
    end

    wfid = @dashboard.launch(pdef)

    wait_for(wfid)

    assert_equal(
      %w[ mark_started mark_failed mark_finished ],
      @dashboard.context.stash[:marks])
  end

  def test_on_error_unknown_participant_name_2

    pdef = Ruote.process_definition :name => 'test' do
      participant :mark_started
      participant :bogus, :on_error => :mark_failed
      participant :mark_finished
    end

    @dashboard.context.stash[:marks] = []

    @dashboard.register_participant 'mark\_.+' do |workitem|
      stash[:marks] << workitem.participant_name
    end

    wfid = @dashboard.launch(pdef)

    wait_for(wfid)

    assert_equal(
      %w[ mark_started mark_failed mark_finished ],
      @dashboard.context.stash[:marks])
  end

  def test_on_error_neutralization

    pdef = Ruote.process_definition do
      sequence :on_error => 'catcher' do
        sequence :on_error => '' do
          nada
        end
      end
    end

    @dashboard.register_participant :catcher do
      tracer << "caught\n"
    end

    wfid = @dashboard.launch(pdef)
    wait_for(wfid)
    ps = @dashboard.process(wfid)

    assert_equal(1, ps.errors.size)
  end

  # redo and retry are aliases, the faulty segment is retried
  #
  def test_on_error_redo

    pdef = Ruote.process_definition do
      sequence :on_error => :redo do
        troublemaker
      end
    end

    @dashboard.register_participant :troublemaker, TroubleMaker

    assert_trace(%w[ 1 2 done. ], pdef)
  end

  # redo and retry are aliases, the faulty segment is retried
  #
  def test_on_error_retry

    pdef = Ruote.process_definition do
      sequence :on_error => :retry do
        troublemaker
      end
    end

    @dashboard.register_participant :troublemaker, TroubleMaker

    assert_trace(%w[ 1 2 done. ], pdef)
  end

  def test_on_error_raise

    pdef = Ruote.define do
      sequence :on_error => :raise do
        error 'nada'
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'error_intercepted', r['action']
    assert_equal 'nada', r['error']['message']
  end

  def test_on_error_undo

    pdef = Ruote.process_definition do
      sequence do
        echo 'a'
        sequence :on_error => :undo do
          echo 'b'
          nemo
          echo 'c'
        end
        echo 'd'
      end
    end

    wfid = assert_trace(%w[ a b d ], pdef)

    assert_nil @dashboard.process(wfid)
  end

  def test_on_error_undo_single_expression

    @dashboard.register_participant :nemo do |wi|
      wi.fields['fail_count'] = 1
      raise 'nemo'
    end

    pdef = Ruote.process_definition do
      sequence do
        echo 'in'
        nemo :on_error => 'undo'
        echo '${f:error}|${f:fail_count}'
      end
    end

    wfid = assert_trace(%w[ in |1 ], pdef)

    assert_nil @dashboard.process(wfid)
  end

  def test_on_error_pass

    pdef = Ruote.process_definition do
      sequence do
        echo 'a'
        sequence :on_error => :pass do
          echo 'b'
          nemo
          echo 'c'
        end
        echo 'd'
      end
    end

    wfid = assert_trace(%w[ a b d ], pdef)

    assert_nil @dashboard.process(wfid)
  end

  def test_missing_handler_triggers_regular_error

    pdef = Ruote.process_definition :on_error => 'failpath' do
      nemo
    end

    wfid = @dashboard.launch(pdef)
    wait_for(wfid)
    ps = @dashboard.process(wfid)

    assert_equal 1, ps.errors.size

    assert_equal 1, logger.log.select { |e| e['action'] == 'error_intercepted' }.size
  end

  def test_on_error_at_process_level

    pdef = Ruote.process_definition :on_error => 'failpath' do
      nemo
      define :failpath do
        echo 'failed.'
      end
    end

    assert_trace('failed.', pdef)
  end

  def test_with_concurrence

    pdef = Ruote.process_definition do
      sequence do
        concurrence :on_error => 'emil' do
          alpha
          error 'nada0'
          error 'nada1'
        end
        echo 'done.'
      end
    end

    @dashboard.context.stash[:a_count] = 0
    @dashboard.context.stash[:e_count] = 0

    @dashboard.register_participant(:alpha) { |wi| stash[:a_count] += 1 }
    @dashboard.register_participant(:emil) { |wi| stash[:e_count] += 1 }

    assert_trace 'done.', pdef
    assert_equal 1, @dashboard.context.stash[:a_count]
    assert_equal 1, @dashboard.context.stash[:e_count]
  end

  def test_participant_on_error

    pdef = Ruote.process_definition do
      troublemaker :on_error => 'handle_error'
      define 'handle_error' do
        troublespotter
      end
    end

    @dashboard.register_participant :troublemaker do |wi|
      wi.fields['seen'] = true
      raise 'Beijing, we have a problem !'
    end
    @dashboard.register_participant :troublespotter do |wi|
      stash[:workitem] = wi
      tracer << 'err...'
    end

    wfid = @dashboard.launch(pdef)
    wait_for(wfid)

    #er = @dashboard.process(wfid).errors.first
    #puts er.message
    #puts er.trace

    wi = @dashboard.context.stash[:workitem]

    assert_equal 'err...', @tracer.to_s
    assert_equal 'RuntimeError', wi.error['class']
    assert_equal 'Beijing, we have a problem !', wi.error['message']
    assert_equal Array, wi.error['trace'].class
    assert_equal true, wi.fields['seen']

    assert_equal(
      %w[ at class details deviations fei message trace tree ],
      wi.error.keys.sort)
  end

  class Murphy
    include Ruote::LocalParticipant

    def cancel(fei, flavour)
      # nothing to do
    end
    def consume(workitem)
      raise "something got wrong"
    end
  end

  def test_subprocess_on_error

    pdef = Ruote.process_definition do
      sequence :on_error => 'error_path' do
        murphy
      end
      define 'error_path' do
        catcher
      end
    end

    @dashboard.register do
      murphy FtOnErrorTest::Murphy
      catchall
    end

    @dashboard.launch(pdef)

    @dashboard.wait_for(:catcher)
  end

  class RescuerOne
    include Ruote::LocalParticipant
    def consume(workitem)
      @context.tracer << 'one'
      reply_to_engine(workitem)
    end
    def accept?(workitem)
      false
    end
  end
  class RescuerTwo
    include Ruote::LocalParticipant
    def consume(workitem)
      @context.tracer << 'two'
      reply_to_engine(workitem)
    end
    #def accept?(workitem)
    #  true
    #end
  end

  def test_participants_and_accept

    pdef = Ruote.process_definition do
      sequence :on_error => 'rescuer' do
        nada
      end
    end

    @dashboard.register do
      rescuer RescuerOne
      rescuer RescuerTwo
    end

    assert_trace('two', pdef)

    assert_equal 1, logger.log.select { |e| e['action'] == 'fail' }.size
  end

  # Only to show what's behind :on_error
  #
  def test_on_error_multi

    pdef = Ruote.define do
      sequence :on_error => [
        [ /unknown participant/, 'alpha' ],
        [ nil, 'bravo' ]
      ] do
        nada
      end
    end

    @dashboard.register_participant /alpha|bravo/ do |workitem|
      tracer << workitem.participant_name
    end

    wfid = @dashboard.launch(pdef)
    @dashboard.wait_for(wfid)

    assert_equal 'alpha', @tracer.to_s
  end

  # Let's be open
  #
  def test_on_error_multi_nice

    pdef = Ruote.process_definition do
      sequence :on_error => [
        { /unknown participant/ => 'alpha' },
        { // => 'bravo' }
      ] do
        nada
      end
    end

    @dashboard.register_participant /alpha|bravo/ do |workitem|
      tracer << workitem.participant_name
    end

    wfid = @dashboard.launch(pdef)
    @dashboard.wait_for(wfid)

    assert_equal 'alpha', @tracer.to_s
  end

  def test_on_error_multi_pass

    @dashboard.register_participant /alpha|bravo/ do |workitem|
      tracer << workitem.participant_name
    end

    pdef = Ruote.define do
      sequence :on_error => [
        { /unknown participant/ => :pass },
        { // => 'bravo' }
      ] do
        nada
      end
      echo 'done.'
    end

    wfid = @dashboard.launch(pdef)
    @dashboard.wait_for(wfid)

    assert_equal 'done.', @tracer.to_s
  end

  def test_on_error_rewind

    pdef = Ruote.define do
      cursor :on_error => 'rewind' do
        echo 'in'
        inc 'v:counter'
        error 'fail', :if => '${v:counter} == 1'
        echo 'over.'
      end
    end

    wfid = @dashboard.launch(pdef)
    @dashboard.wait_for(wfid)

    assert_equal %w[ in in over. ], @tracer.to_a
  end

  def test_on_error_jump_to

    pdef = Ruote.define do
      cursor :on_error => 'jump to shark' do
        alpha
        error 'fail'
        bravo
        shark
        delta
      end
    end

    @dashboard.register '.+' do |workitem|
      tracer << workitem.participant_name + "\n"
    end

    wfid = @dashboard.launch(pdef)
    @dashboard.wait_for(wfid)

    assert_equal %w[ alpha shark delta ], @tracer.to_a
  end

  def test_on_error_var

    pdef = Ruote.define do
      define 'sub0' do
        set 'v:/a' => '$f:__error__'
      end
      sequence :on_error => 'sub0' do
        error 'nada'
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal(
      'terminated',
      r['action'])
    assert_equal(
      %w[ at class details deviations fei message trace tree ],
      r['variables']['a'].keys.sort)
    assert_equal(
      [ 'error', { 'nada' => nil }, [] ],
      r['variables']['a']['tree'])
  end

  def test_on_error_kill_process

    pdef = Ruote.define do
      sequence do
        sequence :on_error => 'cancel_process' do
          nemo0
        end
        nemo1
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
  end

  #
  # the "second take" feature

  def test_second_take

    @dashboard.register_participant :troublemaker, TroubleMaker

    pdef = Ruote.define do
      define 'sub0' do
        set '__on_error__' => 'redo'
      end
      sequence :on_error => 'sub0' do
        troublemaker
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
    assert_equal 3, r['workitem']['fields']['_trace'].size
  end

  def test_blank_second_take

    pdef = Ruote.define do
      define 'sub0' do
        set '__on_error__' => ''
      end
      sequence :on_error => 'sub0' do
        nada
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
  end

  def test_second_take_raise

    pdef = Ruote.define do
      define 'sub0' do
        set '__on_error__' => 'raise'
      end
      sequence :on_error => 'sub0' do
        error 'nada'
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'error_intercepted', r['action']
    assert_equal 'nada', r['error']['message']
  end

  # "stashing that for now"
  #
#  def test_second_take_skip
#
#    pdef = Ruote.define do
#      define 'sub0' do
#        set '__on_error__' => 'cancel'
#      end
#      define 'sub1' do
#        set 'sub1' => true
#      end
#      sequence :on_error => 'sub0', :on_cancel => 'sub1' do
#        error 'nada'
#      end
#    end
#
#    wfid = @dashboard.launch(pdef)
#    r = @dashboard.wait_for(wfid)
#
#    #assert_equal 'error_intercepted', r['action']
#    #assert_equal 'nada', r['error']['message']
#  end

  # behaves like 'undo' when there is no on_cancel present
  #
  def test_on_error_cancel

    pdef = Ruote.define do
      sequence :on_error => 'cancel' do
        echo 'n'
        error 'nada'
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
    assert_equal %w[ n ], @tracer.to_a
  end

  def test_on_error_and_on_cancel

    pdef = Ruote.define do
      define 'rollback' do
        echo 'rollback'
      end
      sequence :on_cancel => 'rollback', :on_error => 'cancel_process' do
        echo 'in'
        error 'nada'
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']

    assert_equal %w[ in rollback ], @tracer.to_a

    assert_equal(
      1,
      @dashboard.logger.log.select { |m| m['action'] == 'cancel_process' }.size)
  end

  def test_on_error_immediate

    # the rollback isn't performed,
    # the !kill_process short-circuits cancelling the errored segment...

    pdef = Ruote.define do
      define 'rollback' do
        echo 'rollback'
      end
      sequence :on_error => '!kill_process' do
        sequence :on_cancel => 'rollback' do
          echo 'in'
          error 'nada'
        end
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
    assert_equal %w[ in ], @tracer.to_a

    assert_nil @dashboard.ps(wfid)
  end

  def test_on_error_immediate_with_tree

    pdef = Ruote.define do
      define 'rollback' do
        echo 'rollback'
      end
      sequence :on_error => [ '!kill_process', {}, [] ] do
        sequence :on_cancel => 'rollback' do
          echo 'in'
          error 'nada'
        end
      end
    end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
    assert_equal %w[ in ], @tracer.to_a

    assert_nil @dashboard.ps(wfid)
  end

  def test_on_error_store_to_field

    pdef =
      Ruote.define do
        sequence :on_error => 'store: x' do
          error 'nada'
        end
        echo 'out'
      end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal %w[ out ], @tracer.to_a
    assert_equal 'Ruote::ForcedError', r['workitem']['fields']['x']['class']
  end

  def test_on_error_store_to_variable

    pdef =
      Ruote.define do
        #sequence :on_error => 'store:v:y' do
        sequence :on_error => 'bury:v:y' do
          error 'nada'
        end
        echo 'out'
      end

    wfid = @dashboard.launch(pdef)
    r = @dashboard.wait_for(wfid)

    assert_equal 'terminated', r['action']
    assert_equal %w[ out ], @tracer.to_a
    assert_equal 'Ruote::ForcedError', r['variables']['y']['class']
  end
end

