require_relative '../../spec_helper'
require_relative 'fixtures/classes'

# This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19473223/job/f69derxnlo09xhuj
# TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard.
platform_is_not :mingw do
  describe "Thread#kill" do
    before :each do
      ScratchPad.clear
    end

    it "kills sleeping thread" do
      sleeping_thread = Thread.new do
        sleep
        ScratchPad.record :after_sleep
      end
      Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep"
      sleeping_thread.kill
      sleeping_thread.join
      ScratchPad.recorded.should == nil
    end

    it "kills current thread" do
      thread = Thread.new do
        Thread.current.kill
        ScratchPad.record :after_sleep
      end
      thread.join
      ScratchPad.recorded.should == nil
    end

    it "runs ensure clause" do
      thread = ThreadSpecs.dying_thread_ensures(:kill) { ScratchPad.record :in_ensure_clause }
      thread.join
      ScratchPad.recorded.should == :in_ensure_clause
    end

    it "runs nested ensure clauses" do
      ScratchPad.record []
      @outer = Thread.new do
        begin
          @inner = Thread.new do
            begin
              sleep
            ensure
              ScratchPad << :inner_ensure_clause
            end
          end
          sleep
        ensure
          ScratchPad << :outer_ensure_clause
          @inner.kill
          @inner.join
        end
      end
      Thread.pass while @outer.status and @outer.status != "sleep"
      Thread.pass until @inner
      Thread.pass while @inner.status and @inner.status != "sleep"
      @outer.kill
      @outer.join
      ScratchPad.recorded.should.include?(:inner_ensure_clause)
      ScratchPad.recorded.should.include?(:outer_ensure_clause)
    end

    it "does not set $!" do
      thread = ThreadSpecs.dying_thread_ensures(:kill) { ScratchPad.record $! }
      thread.join
      ScratchPad.recorded.should == nil
    end

    it "does not reset $!" do
      ScratchPad.record []

      exc = RuntimeError.new("foo")
      thread = Thread.new do
        begin
          raise exc
        ensure
          ScratchPad << $!
          begin
            Thread.current.kill
          ensure
            ScratchPad << $!
          end
        end
      end
      thread.join
      ScratchPad.recorded.should == [exc, exc]
    end

    it "cannot be rescued" do
      thread = Thread.new do
        begin
          Thread.current.kill
        rescue Exception
          ScratchPad.record :in_rescue
        end
        ScratchPad.record :end_of_thread_block
      end

      thread.join
      ScratchPad.recorded.should == nil
    end

    it "kills the entire thread when a fiber is active" do
      t = Thread.new do
        Fiber.new do
          sleep
        end.resume
        ScratchPad.record :fiber_resumed
      end
      Thread.pass while t.status and t.status != "sleep"
      t.kill
      t.join
      ScratchPad.recorded.should == nil
    end

    it "kills other fibers of that thread without running their ensure clauses" do
      t = Thread.new do
        f = Fiber.new do
          ScratchPad.record :fiber_resumed
          begin
            Fiber.yield
          ensure
            ScratchPad.record :fiber_ensure
          end
        end
        f.resume
        sleep
      end
      Thread.pass until t.stop?
      t.kill
      t.join
      ScratchPad.recorded.should == :fiber_resumed
    end

    # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed
    quarantine! do
    it "killing dying running does nothing" do
      in_ensure_clause = false
      exit_loop = true
      t = ThreadSpecs.dying_thread_ensures do
        in_ensure_clause = true
        loop { if exit_loop then break end }
        ScratchPad.record :after_stop
      end

      Thread.pass until in_ensure_clause == true
      10.times { t.kill; Thread.pass }
      exit_loop = true
      t.join
      ScratchPad.recorded.should == :after_stop
    end
    end

    quarantine! do

      it "propagates inner exception to Thread.join if there is an outer ensure clause" do
        thread = ThreadSpecs.dying_thread_with_outer_ensure(:kill) { }
        -> { thread.join }.should.raise(RuntimeError, "In dying thread")
      end

      it "runs all outer ensure clauses even if inner ensure clause raises exception" do
        ThreadSpecs.join_dying_thread_with_outer_ensure(:kill) { ScratchPad.record :in_outer_ensure_clause }
        ScratchPad.recorded.should == :in_outer_ensure_clause
      end

      it "sets $! in outer ensure clause if inner ensure clause raises exception" do
        ThreadSpecs.join_dying_thread_with_outer_ensure(:kill) { ScratchPad.record $! }
        ScratchPad.recorded.to_s.should == "In dying thread"
      end
    end

    it "can be rescued by outer rescue clause when inner ensure clause raises exception" do
      thread = Thread.new do
        begin
          begin
            Thread.current.kill
          ensure
            raise "In dying thread"
          end
        rescue Exception
          ScratchPad.record $!
        end
        :end_of_thread_block
      end

      thread.value.should == :end_of_thread_block
      ScratchPad.recorded.to_s.should == "In dying thread"
    end

    it "is deferred if ensure clause does Thread.stop" do
      ThreadSpecs.wakeup_dying_sleeping_thread(:kill) { Thread.stop; ScratchPad.record :after_sleep }
      ScratchPad.recorded.should == :after_sleep
    end

    # Hangs on 1.8.6.114 OS X, possibly also on Linux
    quarantine! do
      it "is deferred if ensure clause sleeps" do
        ThreadSpecs.wakeup_dying_sleeping_thread(:kill) { sleep; ScratchPad.record :after_sleep }
        ScratchPad.recorded.should == :after_sleep
      end
    end

    # This case occurred in JRuby where native threads are used to provide
    # the same behavior as MRI green threads. Key to this issue was the fact
    # that the thread which called #exit in its block was also being explicitly
    # sent #join from outside the thread. The 100.times provides a certain
    # probability that the deadlock will occur. It was sufficient to reliably
    # reproduce the deadlock in JRuby.
    it "does not deadlock when called from within the thread while being joined from without" do
      100.times do
        t = Thread.new { Thread.stop; Thread.current.kill }
        Thread.pass while t.status and t.status != "sleep"
        t.wakeup.should == t
        t.join.should == t
      end
    end
  end

  describe "Thread.kill" do
    it "causes the given thread to exit" do
      thread = Thread.new { sleep }
      Thread.pass while thread.status and thread.status != "sleep"
      Thread.kill(thread).should == thread
      thread.join
      thread.status.should == false
    end
  end
end
