summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/runner/actions/timeout.rb
blob: 1200926872b520632436bf87cc9633cd51754ec8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class TimeoutAction
  def initialize(timeout)
    @timeout = timeout
    @queue = Queue.new
    @started = now
    @fail = false
    @error_message = "took longer than the configured timeout of #{@timeout}s"
  end

  def register
    MSpec.register :start, self
    MSpec.register :before, self
    MSpec.register :after, self
    MSpec.register :finish, self
  end

  private def now
    Process.clock_gettime(Process::CLOCK_MONOTONIC)
  end

  private def fetch_item
    @queue.pop(true)
  rescue ThreadError
    nil
  end

  def start
    @thread = Thread.new do
      loop do
        if action = fetch_item
          action.call
        else
          wakeup_at = @started + @timeout
          left = wakeup_at - now
          sleep left if left > 0
          Thread.pass # Let the main thread run

          if @queue.empty?
            elapsed = now - @started
            if elapsed > @timeout
              if @current_state
                STDERR.puts "\nExample #{@error_message}:"
                STDERR.puts "#{@current_state.description}"
              else
                STDERR.puts "\nSome code outside an example #{@error_message}"
              end
              STDERR.flush

              show_backtraces
              if MSpec.subprocesses.empty?
                exit! 2
              else
                # Do not exit but signal the subprocess so we can get their output
                MSpec.subprocesses.each do |pid|
                  kill_wait_one_second :SIGTERM, pid
                  hard_kill :SIGKILL, pid
                end
                @fail = true
                @current_state = nil
                break # stop this thread, will fail in #after
              end
            end
          end
        end
      end
    end
  end

  def before(state = nil)
    time = now
    @queue << -> do
      @current_state = state
      @started = time
    end
  end

  def after(state = nil)
    @queue << -> do
      @current_state = nil
    end

    if @fail
      STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace."
      exit! 2
    end
  end

  def finish
    @thread.kill
    @thread.join
  end

  private def hard_kill(signal, pid)
    begin
      Process.kill signal, pid
    rescue Errno::ESRCH
      # Process already terminated
    end
  end

  private def kill_wait_one_second(signal, pid)
    begin
      Process.kill signal, pid
      sleep 1
    rescue Errno::ESRCH
      # Process already terminated
    end
  end

  private def show_backtraces
    java_stacktraces = -> pid {
      if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
        STDERR.puts 'Java stacktraces:'
        kill_wait_one_second :SIGQUIT, pid
      end
    }

    if MSpec.subprocesses.empty?
      java_stacktraces.call Process.pid

      STDERR.puts "\nRuby backtraces:"
      if defined?(Truffle::Debug.show_backtraces)
        Truffle::Debug.show_backtraces
      else
        Thread.list.each do |thread|
          unless thread == Thread.current
            STDERR.puts thread.inspect, thread.backtrace, ''
          end
        end
      end
    else
      MSpec.subprocesses.each do |pid|
        STDERR.puts "\nFor subprocess #{pid}"
        java_stacktraces.call pid

        if RUBY_ENGINE == 'truffleruby'
          STDERR.puts "\nRuby backtraces:"
          kill_wait_one_second :SIGALRM, pid
        else
          STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}"
        end
      end
    end
  end
end