summaryrefslogtreecommitdiff
path: root/tool/lib/test/unit/parallel.rb
blob: ac297d4a0ec1809e23140beef5abfed4771b3200 (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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# frozen_string_literal: true

require_relative "../../../test/init"

module Test
  module Unit
    class Worker < Runner # :nodoc:
      class << self
        undef autorun
      end

      undef _run_suite
      undef _run_suites
      undef run

      def increment_io(orig) # :nodoc:
        *rest, io = 32.times.inject([orig.dup]){|ios, | ios << ios.last.dup }
        rest.each(&:close)
        io
      end

      def _run_suites(suites, type) # :nodoc:
        suites.map do |suite|
          _run_suite(suite, type)
        end
      end

      def _start_method(inst)
        _report "start", Marshal.dump([inst.class.name, inst.__name__])
      end

      def _run_suite(suite, type) # :nodoc:
        @partial_report = []
        orig_testout = Test::Unit::Runner.output
        i,o = IO.pipe

        Test::Unit::Runner.output = o
        orig_stdin, orig_stdout = $stdin, $stdout

        th = Thread.new do
          begin
            while buf = (self.verbose ? i.gets : i.readpartial(1024))
              _report "p", buf or break
            end
          rescue IOError
          end
        end

        e, f, s = @errors, @failures, @skips

        begin
          result = orig_run_suite(suite, type)
        rescue Interrupt
          @need_exit = true
          result = [nil,nil]
        end

        Test::Unit::Runner.output = orig_testout
        $stdin = orig_stdin
        $stdout = orig_stdout

        o.close
        begin
          th.join
        rescue IOError
          raise unless /stream closed|closed stream/ =~ $!.message
        end
        i.close

        result << @partial_report
        @partial_report = nil
        result << [@errors-e,@failures-f,@skips-s]
        result << ($: - @old_loadpath)
        result << suite.name

        _report "done", Marshal.dump(result)
        return result
      ensure
        Test::Unit::Runner.output = orig_stdout
        $stdin = orig_stdin if orig_stdin
        $stdout = orig_stdout if orig_stdout
        o.close if o && !o.closed?
        i.close if i && !i.closed?
      end

      def run(args = []) # :nodoc:
        process_args args
        @@stop_auto_run = true
        @opts = @options.dup
        @need_exit = false

        @old_loadpath = []
        begin
          begin
            @stdout = increment_io(STDOUT)
            @stdin = increment_io(STDIN)
          rescue
            exit 2
          end
          exit 2 unless @stdout && @stdin

          @stdout.sync = true
          _report "ready!"
          while buf = @stdin.gets
            case buf.chomp
            when /^loadpath (.+?)$/
              @old_loadpath = $:.dup
              $:.push(*Marshal.load($1.unpack1("m").force_encoding("ASCII-8BIT"))).uniq!
            when /^run (.+?) (.+?)$/
              _report "okay"

              @options = @opts.dup
              suites = Test::Unit::TestCase.test_suites

              begin
                require File.realpath($1)
              rescue LoadError
                _report "after", Marshal.dump([$1, ProxyError.new($!)])
                _report "ready"
                next
              end
              _run_suites Test::Unit::TestCase.test_suites-suites, $2.to_sym

              if @need_exit
                _report "bye"
                exit
              else
                _report "ready"
              end
            when /^quit$/
              _report "bye"
              exit
            end
          end
        rescue Exception => e
          trace = e.backtrace || ['unknown method']
          err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| "\t" + t }

          if @stdout
            _report "bye", Marshal.dump(err.join("\n"))
          else
            raise "failed to report a failure due to lack of @stdout"
          end
          exit
        ensure
          @stdin.close if @stdin
          @stdout.close if @stdout
        end
      end

      def _report(res, *args) # :nodoc:
        @stdout.write(args.empty? ? "#{res}\n" : "#{res} #{args.pack("m0")}\n")
        true
      rescue Errno::EPIPE
      rescue TypeError => e
        abort("#{e.inspect} in _report(#{res.inspect}, #{args.inspect})\n#{e.backtrace.join("\n")}")
      end

      def puke(klass, meth, e) # :nodoc:
        if e.is_a?(Test::Unit::PendedError)
          new_e = Test::Unit::PendedError.new(e.message)
          new_e.set_backtrace(e.backtrace)
          e = new_e
        end
        @partial_report << [klass.name, meth, e.is_a?(Test::Unit::AssertionFailedError) ? e : ProxyError.new(e)]
        super
      end

      def record(suite, method, assertions, time, error) # :nodoc:
        case error
        when nil
        when Test::Unit::AssertionFailedError, Test::Unit::PendedError
          case error.cause
          when nil, Test::Unit::AssertionFailedError, Test::Unit::PendedError
          else
            bt = error.backtrace
            error = error.class.new(error.message)
            error.set_backtrace(bt)
          end
        else
          error = ProxyError.new(error)
        end
        _report "record", Marshal.dump([suite.name, method, assertions, time, error, suite.instance_method(method).source_location])
        super
      end
    end
  end
end

if $0 == __FILE__
  module Test
    module Unit
      class TestCase # :nodoc: all
        undef on_parallel_worker?
        def on_parallel_worker?
          true
        end
        def self.on_parallel_worker?
          true
        end
      end
    end
  end
  require 'rubygems'
  begin
    require 'rake'
  rescue LoadError
  end
  Test::Unit::Worker.new.run(ARGV)
end