#!/usr/bin/env rspec

require 'spec_helper'
require 'mcollective/runner'

module MCollective
  describe Runner do
    let(:config) do
      c = mock
      c.stubs(:loadconfig)
      c.stubs(:configured).returns(true)
      c.stubs(:mode=)
      c.stubs(:direct_addressing).returns(false)
      c.stubs(:registerinterval).returns(1)
      c.stubs(:soft_shutdown).returns(false)
      c
    end

    let(:stats) { mock }

    let(:security) do
      s = mock
      s.stubs(:initiated_by=)
      s
    end

    let(:connector) do
      c = mock
      c.stubs(:connect)
      c.stubs(:disconnect)
      c
    end

    let(:agents) { mock }

    let(:receiver_thread) do
      rt = mock
      rt
    end

    before :each do
      Config.stubs(:instance).returns(config)
      PluginManager.stubs(:[]).with('global_stats').returns(stats)
      PluginManager.stubs(:[]).with('security_plugin').returns(security)
      PluginManager.stubs(:[]).with('connector_plugin').returns(connector)
      Agents.stubs(:new).returns(agents)
    end

    describe 'initialize' do
      it 'should set up the signal handlers when not on windows' do
        Util.stubs(:windows).returns(false)
        Signal.expects(:trap).with('USR1')
        Signal.expects(:trap).with('USR2')
        Signal.expects(:trap).with('WINCH')
        Runner.new(nil)
      end

      it 'should not set up the signal handlers when on windows' do
        Util.stubs(:windows?).returns(true)
        Signal.expects(:trap).with('USR1').never
        Signal.expects(:trap).with('USR2').never
        Signal.expects(:trap).with('WINCH').never

        Util.expects(:setup_windows_sleeper)
        Runner.new(nil)
      end

      it 'should log a message when it cannot initialize the runner' do
        Config.stubs(:instance).raises('failure')
        Log.expects(:error).times(3)
        expect {
          Runner.new(nil)
        }.to raise_error 'failure'
      end
    end

    describe '#main_loop' do
      let(:runner) do
        Runner.new(nil)
      end

      let(:receiver_thread) do
        rt = mock
        rt.stubs(:alive?).returns(false)
        rt
      end

      let(:agent_thread) do
        at = mock
        at.stubs(:alive?).returns(true)
        at
      end

      before :each do
        Log.stubs(:debug)
        Log.stubs(:info)
        Log.stubs(:warn)
        Log.stubs(:error)
        runner.stubs(:start_receiver_thread).returns(receiver_thread)
      end

      context 'stopping' do
        it 'should stop normally' do
          receiver_thread.stubs(:alive?).returns(true)
          runner.instance_variable_set(:@state, :stopping)
          runner.expects(:stop_threads)
          runner.main_loop
        end

        it 'should do a soft_shutdown' do
          config.stubs(:soft_shutdown).returns(true)
          Util.stubs(:windows?).returns(false)
          runner.instance_variable_set(:@state, :stopping)
          runner.instance_variable_set(:@agent_threads, [agent_thread])
          runner.expects(:soft_shutdown)
          runner.expects(:stop_threads)
          runner.main_loop
        end
      end

      # Because paused is not a terminal state, we raise after testing pause
      # which forces a run. This means checks like #stop_threads will happen twice
      context 'pausing' do
        before :each do
          receiver_thread.stubs(:alive?).returns(false)
        end

        it 'should pause' do
          runner.instance_variable_set(:@state, :pausing)
          runner.expects(:stop_threads).twice
          runner.expects(:sleep).with(0.1).raises("error")
          runner.main_loop
        end

        it 'should resume' do
          runner.instance_variable_set(:@state, :unpausing)
          runner.expects(:stop_threads)
          runner.expects(:start_receiver_thread).returns(receiver_thread)
          runner.expects(:sleep).with(0.1).raises("error")
          runner.main_loop
        end
      end
    end

    context 'action methods' do
      let(:runner) do
        Runner.new(nil)
      end

      describe '#stop' do
        it 'should change the state to stopping' do
          runner.instance_variable_set(:@state, :running)
          runner.stop
          runner.state.should == :stopping
        end
      end

      describe '#pause' do
        it 'should change the state to pausing' do
          runner.instance_variable_set(:@state, :running)
          runner.pause
          runner.state.should == :pausing
        end

        it 'should fail if state is not running' do
          Log.expects(:error).with('Cannot pause MCollective while not in a running state')
          runner.pause
        end
      end

      describe '#resume' do
        it 'should change the state to unpausing' do
          runner.instance_variable_set(:@state, :paused)
          runner.resume
          runner.state.should == :unpausing
        end

        it 'should fail if state is not paused' do
          runner.instance_variable_set(:@state, :running)
          Log.expects(:error).with('Cannot unpause MCollective when it is not paused')
          runner.resume
        end
      end

      describe '#start_receiver_thread' do
        let(:current) do
          mock('current thread')
        end

        before :each do
          Thread.stubs(:new).yields
          Thread.stubs(:current).returns(current)
        end

        it 'should reraise exceptions' do
          runner.instance_variable_set(:@state, :running)
          runner.stubs(:receiver_thread).raises('test error')
          current.expects(:raise).once
          runner.send(:start_receiver_thread)
        end
      end

      describe '#receiver_thread' do
        let(:runner) do
          Runner.new(nil)
        end

        let(:registration_agent) do
          ra = mock
          ra.stubs(:run)
          ra
        end

        let(:request) do
          r = mock
          r.stubs(:agent).returns("rspec")
          r
        end

        before :each do
          PluginManager.stubs(:[]).with("registration_plugin").returns(registration_agent)
          Data.stubs(:load_data_sources)
          Util.stubs(:subscribe_to_direct_addressing_queue)
        end

        it 'should receive a message and spawn an agent thread' do
          runner.expects(:receive).returns(request)
          runner.expects(:agentmsg).with(request)
          runner.instance_variable_set(:@exit_receiver_thread, true)
          runner.send(:receiver_thread)
        end

        it 'should subscribe to the direct addressing queue if direct_addressing is configured' do
          runner.expects(:receive).returns(request)
          runner.expects(:agentmsg).with(request)
          config.stubs(:direct_addressing).returns(true)
          Util.expects(:subscribe_to_direct_addressing_queue)
          runner.instance_variable_set(:@exit_receiver_thread, true)
          runner.send(:receiver_thread)
        end

        it 'should load agents before data plugins' do
          load_order = sequence('load_order')
          Agents.expects(:new).in_sequence(load_order)
          Data.expects(:load_data_sources).in_sequence(load_order)
          runner.expects(:receive).returns(request)
          runner.expects(:agentmsg).with(request)
          runner.instance_variable_set(:@exit_receiver_thread, true)
          runner.send(:receiver_thread)
        end

        it 'should warn when a received message has expired' do
          runner.expects(:receive).raises(MsgTTLExpired)
          runner.instance_variable_set(:@exit_receiver_thread, true)
          Log.expects(:warn)
          runner.send(:receiver_thread)
        end

        it 'should log if a message was received by not directed at the server' do
          runner.expects(:receive).raises(NotTargettedAtUs)
          runner.instance_variable_set(:@exit_receiver_thread, true)
          Log.expects(:info)
          runner.send(:receiver_thread)
        end

        it 'should back off on MessageNotReceived and UnexpectedMessageType' do
          runner.expects(:receive).raises(MessageNotReceived.new(1))
          runner.instance_variable_set(:@exit_receiver_thread, true)
          Log.expects(:warn)
          Log.expects(:info).with("sleeping for suggested 1 seconds")
          runner.expects(:sleep).with(1)
          runner.send(:receiver_thread)
        end
      end
    end

    context "soft_shutdown" do
      let(:runner) do
        Runner.new(nil)
      end

      before(:each) do
        config.stubs(:soft_shutdown).returns(true)
      end

      describe "#soft_shutdown" do
        it "should not shutdown if the timeout is set and <= 0" do
          config.stubs(:soft_shutdown_timeout).returns(0)
          Log.expects(:warn).twice
          runner.expects(:windows_soft_shutdown).never
          runner.expects(:posix_soft_shutdown).never
          runner.send(:soft_shutdown)
        end

        it "should call the windows soft_shutdown on Windows" do
          config.stubs(:soft_shutdown_timeout).returns(1)
          Util.stubs(:windows?).returns(true)
          runner.expects(:windows_soft_shutdown)
          runner.expects(:posix_soft_shutdown).never
          runner.send(:soft_shutdown)
        end

        it "should call the posix soft_shutdown when not on windows" do
          config.stubs(:soft_shutdown_timeout).returns(1)
          Util.stubs(:windows?).returns(false)
          runner.expects(:windows_soft_shutdown).never
          runner.expects(:posix_soft_shutdown)
          runner.send(:soft_shutdown)
        end
      end

      describe "#windows_soft_shutdown" do
        it "should not shutdown if no timeout is set" do
          runner.expects(:shutdown_with_timeout).never
          Log.expects(:warn).times(3)
          runner.send(:windows_soft_shutdown, nil)
        end

        it "should shutdown in a timeout" do
          runner.expects(:shutdown_with_timeout)
          runner.send(:windows_soft_shutdown, 1)
        end
      end

      describe "#posix_soft_shutdown" do
        it "should shutdown without a timeout" do
          runner.expects(:stop_agent_threads)
          runner.send(:posix_soft_shutdown, nil)
        end

        it "should shutdown with a timeout" do
          runner.expects(:shutdown_with_timeout)
          runner.send(:posix_soft_shutdown, 1)
        end
      end

      describe "#shutdown_with_timeout" do
        it "should timeout if it can't stop agent threads in time" do
          Timeout.expects(:timeout).with(1)
          runner.send(:shutdown_with_timeout, 1)
        end
      end

      describe "#stop_agent_threads" do
        let(:agent_thread) do
          at = mock
          at.stubs(:alive?).returns(true)
          at
        end

        it "should stop all agent threads" do
          runner.instance_variable_set(:@agent_threads, [agent_thread])
          Log.stubs(:debug)
          agent_thread.expects(:join)
          runner.send(:stop_agent_threads)
        end
      end
    end
  end
end
