micron 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +88 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/bin/micron +4 -0
- data/lib/micron.rb +29 -0
- data/lib/micron/app.rb +127 -0
- data/lib/micron/app/options.rb +73 -0
- data/lib/micron/assertion.rb +10 -0
- data/lib/micron/fork_runner.rb +55 -0
- data/lib/micron/minitest.rb +45 -0
- data/lib/micron/proc_runner.rb +114 -0
- data/lib/micron/rake.rb +29 -0
- data/lib/micron/reporter.rb +30 -0
- data/lib/micron/reporter/console.rb +146 -0
- data/lib/micron/reporter/coverage.rb +37 -0
- data/lib/micron/runner.rb +95 -0
- data/lib/micron/runner/backtrace_filter.rb +39 -0
- data/lib/micron/runner/clazz.rb +45 -0
- data/lib/micron/runner/clazz19.rb +24 -0
- data/lib/micron/runner/debug.rb +22 -0
- data/lib/micron/runner/exception_info.rb +16 -0
- data/lib/micron/runner/fork_worker.rb +185 -0
- data/lib/micron/runner/forking_clazz.rb +40 -0
- data/lib/micron/runner/liveness_checker.rb +40 -0
- data/lib/micron/runner/liveness_checker/ping.rb +65 -0
- data/lib/micron/runner/liveness_checker/pong.rb +36 -0
- data/lib/micron/runner/method.rb +124 -0
- data/lib/micron/runner/parallel_clazz.rb +135 -0
- data/lib/micron/runner/proc_clazz.rb +48 -0
- data/lib/micron/runner/process_reaper.rb +98 -0
- data/lib/micron/runner/shim.rb +68 -0
- data/lib/micron/runner/test_file.rb +79 -0
- data/lib/micron/test_case.rb +36 -0
- data/lib/micron/test_case/assertions.rb +701 -0
- data/lib/micron/test_case/lifecycle_hooks.rb +74 -0
- data/lib/micron/test_case/redir_logging.rb +85 -0
- data/lib/micron/test_case/teardown_coverage.rb +13 -0
- data/lib/micron/util/ex.rb +23 -0
- data/lib/micron/util/io.rb +54 -0
- data/lib/micron/util/thread_dump.rb +29 -0
- data/micron.gemspec +97 -0
- metadata +184 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
|
4
|
+
# Default backtrace filter
|
5
|
+
#
|
6
|
+
# This can be swapped out by setting Micron::Runner.backtrace_filter
|
7
|
+
# to a block, proc, lambda, or a class with a #call method.
|
8
|
+
class BacktraceFilter
|
9
|
+
|
10
|
+
def call(bt)
|
11
|
+
return ["No backtrace"] unless bt and !bt.empty?
|
12
|
+
|
13
|
+
new_bt = []
|
14
|
+
|
15
|
+
unless $DEBUG then
|
16
|
+
|
17
|
+
if bt.first =~ %r{^/.*?/lib/micron/test_case/assertions.rb:\d+:in} then
|
18
|
+
# first line is an assertion, pop it off
|
19
|
+
bt.shift
|
20
|
+
end
|
21
|
+
|
22
|
+
bt.each do |line|
|
23
|
+
break if line =~ %r{(bin|lib)/micron}
|
24
|
+
new_bt << line
|
25
|
+
end
|
26
|
+
|
27
|
+
# make sure we didn't remove everything - if we did, the error was in our code
|
28
|
+
new_bt = bt.reject { |line| line =~ %r{(bin|lib)/micron} } if new_bt.empty?
|
29
|
+
new_bt = bt.dup if new_bt.empty?
|
30
|
+
else
|
31
|
+
new_bt = bt.dup
|
32
|
+
end
|
33
|
+
|
34
|
+
new_bt
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
class Clazz
|
5
|
+
|
6
|
+
include Debug
|
7
|
+
|
8
|
+
attr_reader :name, :methods
|
9
|
+
|
10
|
+
def initialize(clazz, file)
|
11
|
+
@name = clazz.to_s
|
12
|
+
@file = file
|
13
|
+
@methods = test_methods.map { |m| Method.new(self, m) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Create a new instance of the Class represented by this object
|
17
|
+
def create
|
18
|
+
name_to_const.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
methods.each do |method|
|
23
|
+
method.run
|
24
|
+
Micron.runner.report(:end_method, method)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def test_methods
|
32
|
+
create.public_methods.find_all { |m|
|
33
|
+
m.to_s =~ /^test_/
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def name_to_const
|
38
|
+
Module.const_get(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
require "micron/runner/clazz19" if RUBY_VERSION =~ /1.9/
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
class Clazz
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
# Compat fix for Ruby 1.9.x
|
9
|
+
def name_to_const
|
10
|
+
|
11
|
+
names = name.split('::')
|
12
|
+
names.shift if names.empty? || names.first.empty?
|
13
|
+
|
14
|
+
constant = Module
|
15
|
+
names.each do |name|
|
16
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
return constant
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
module Debug
|
5
|
+
|
6
|
+
def debug(str="")
|
7
|
+
|
8
|
+
return
|
9
|
+
|
10
|
+
name = Thread.current[:name] || ""
|
11
|
+
if !name.empty? then
|
12
|
+
name = "[#{name} #{$$}] "
|
13
|
+
else
|
14
|
+
name = "[#{$$}]"
|
15
|
+
end
|
16
|
+
|
17
|
+
puts "#{name} #{str}"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
|
2
|
+
require "micron/runner/debug"
|
3
|
+
require "micron/runner/liveness_checker"
|
4
|
+
|
5
|
+
module Micron
|
6
|
+
class Runner
|
7
|
+
|
8
|
+
# Fork Worker
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# fw = Micron::Runner::ForkWorker.new do
|
12
|
+
# system("ls")
|
13
|
+
# "sup"
|
14
|
+
# end
|
15
|
+
# fw.run.wait
|
16
|
+
# puts fw.stdout
|
17
|
+
# puts fw.result # => sup
|
18
|
+
#
|
19
|
+
class ForkWorker
|
20
|
+
|
21
|
+
include Debug
|
22
|
+
|
23
|
+
CHUNK_SIZE = 1024 * 16
|
24
|
+
|
25
|
+
attr_reader :pid, :context, :status
|
26
|
+
|
27
|
+
def initialize(context=nil, capture_stdout=true, capture_stderr=true, &block)
|
28
|
+
@context = context
|
29
|
+
@capture_stdout = capture_stdout
|
30
|
+
@capture_stderr = capture_stderr
|
31
|
+
@block = block
|
32
|
+
@done = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def run(check_liveness=false)
|
36
|
+
@out, @err = IO.pipe, IO.pipe
|
37
|
+
@parent_read, @child_write = IO.pipe
|
38
|
+
@child_write.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
39
|
+
|
40
|
+
@liveness_checker = LivenessChecker.new if check_liveness
|
41
|
+
|
42
|
+
@pid = fork do
|
43
|
+
|
44
|
+
Thread.current[:name] = "worker"
|
45
|
+
Micron.trap_thread_dump()
|
46
|
+
|
47
|
+
# close unused readers in child
|
48
|
+
@out.first.close
|
49
|
+
@err.first.close
|
50
|
+
@parent_read.close
|
51
|
+
|
52
|
+
# redirect io
|
53
|
+
STDOUT.reopen(@out.last) if @capture_stdout
|
54
|
+
STDERR.reopen(@err.last) if @capture_stderr
|
55
|
+
STDOUT.sync = STDERR.sync = true
|
56
|
+
|
57
|
+
clean_parent_file_descriptors()
|
58
|
+
|
59
|
+
@liveness_checker.pong if check_liveness
|
60
|
+
|
61
|
+
# run
|
62
|
+
debug("calling block")
|
63
|
+
ret = @block.call()
|
64
|
+
debug("block returned")
|
65
|
+
|
66
|
+
# Pass result to parent via pipe
|
67
|
+
Marshal.dump(ret, @child_write)
|
68
|
+
@child_write.flush
|
69
|
+
debug("wrote result to pipe")
|
70
|
+
|
71
|
+
# cleanup
|
72
|
+
@out.last.close
|
73
|
+
@err.last.close
|
74
|
+
@child_write.close
|
75
|
+
end
|
76
|
+
|
77
|
+
# close unused writers in parent
|
78
|
+
@out.last.close
|
79
|
+
@err.last.close
|
80
|
+
@child_write.close
|
81
|
+
|
82
|
+
@liveness_checker.ping(self) if check_liveness
|
83
|
+
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
# Blocking wait for process to finish
|
88
|
+
#
|
89
|
+
# @return [self]
|
90
|
+
def wait
|
91
|
+
return self if @done
|
92
|
+
|
93
|
+
Process.wait(@pid)
|
94
|
+
@done = true
|
95
|
+
@status = $?
|
96
|
+
@liveness_checker.stop if @liveness_checker
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Blocking wait for process to finish
|
101
|
+
#
|
102
|
+
# @return [Process::Status]
|
103
|
+
def wait2
|
104
|
+
return @status if @done
|
105
|
+
|
106
|
+
pid, @status = Process.wait2(@pid)
|
107
|
+
@done = true
|
108
|
+
@liveness_checker.stop if @liveness_checker
|
109
|
+
return @status
|
110
|
+
end
|
111
|
+
|
112
|
+
# Non-blocking wait for process to finish
|
113
|
+
#
|
114
|
+
# @return [Process::Status] nil if not yet complete
|
115
|
+
def wait_nonblock
|
116
|
+
return @status if @done
|
117
|
+
|
118
|
+
pid, @status = Process.waitpid2(@pid, Process::WNOHANG)
|
119
|
+
if !@status.nil? then
|
120
|
+
@done = true
|
121
|
+
@liveness_checker.stop if @liveness_checker
|
122
|
+
end
|
123
|
+
return @status
|
124
|
+
end
|
125
|
+
|
126
|
+
def result
|
127
|
+
if !@done then
|
128
|
+
wait()
|
129
|
+
end
|
130
|
+
@result = Marshal.load(@parent_read)
|
131
|
+
@parent_read.close
|
132
|
+
@result
|
133
|
+
end
|
134
|
+
|
135
|
+
def stdout
|
136
|
+
return @stdout if !@stdout.nil?
|
137
|
+
@stdout = @out.first.read
|
138
|
+
@out.first.close
|
139
|
+
@stdout ||= ""
|
140
|
+
@stdout
|
141
|
+
end
|
142
|
+
|
143
|
+
def stderr
|
144
|
+
return @stderr if !@stderr.nil?
|
145
|
+
@stderr = @err.first.read
|
146
|
+
@err.first.close
|
147
|
+
@stderr ||= ""
|
148
|
+
@stderr
|
149
|
+
end
|
150
|
+
|
151
|
+
def out
|
152
|
+
@out.first
|
153
|
+
end
|
154
|
+
|
155
|
+
def err
|
156
|
+
@err.first
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
|
163
|
+
# Cleanup all FDs inherited from the parent. We don't need them and we
|
164
|
+
# may throw errors if they are left open. 8192 should be high enough.
|
165
|
+
def clean_parent_file_descriptors
|
166
|
+
# Don't clean $stdin, $stdout, $stderr (0-2) or our own pipes
|
167
|
+
keep = [ @child_write.to_i, @out.last.to_i, @err.last.to_i ]
|
168
|
+
keep += @liveness_checker.fds if @liveness_checker
|
169
|
+
|
170
|
+
3.upto(8192) do |n|
|
171
|
+
if !keep.include? n then
|
172
|
+
begin
|
173
|
+
fd = IO.for_fd(n)
|
174
|
+
fd.close if fd
|
175
|
+
rescue
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
# A Clazz implementation which forks before running each test method
|
6
|
+
class ForkingClazz < ParallelClazz
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def spawn_test(method)
|
11
|
+
ForkWorker.new(method) {
|
12
|
+
$0 = "micron: method"
|
13
|
+
# ERR.puts "#{$0} (#{$$})"
|
14
|
+
|
15
|
+
EasyCov.start
|
16
|
+
|
17
|
+
Shim.wrap {
|
18
|
+
method.run
|
19
|
+
}
|
20
|
+
|
21
|
+
method
|
22
|
+
}.run(true)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Collect the result data
|
26
|
+
#
|
27
|
+
# @param [Array] finished Completed pids & their associated methods
|
28
|
+
#
|
29
|
+
# @return [Array<Method>]
|
30
|
+
def collect_results(finished)
|
31
|
+
finished.map{ |f| collect_result(f) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def collect_result(worker)
|
35
|
+
worker.result
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
require "micron/runner/liveness_checker/ping"
|
3
|
+
require "micron/runner/liveness_checker/pong"
|
4
|
+
|
5
|
+
module Micron
|
6
|
+
class Runner
|
7
|
+
|
8
|
+
class LivenessChecker
|
9
|
+
|
10
|
+
include Debug
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@ping = IO.pipe
|
14
|
+
@pong = IO.pipe
|
15
|
+
end
|
16
|
+
|
17
|
+
def ping(pid)
|
18
|
+
@pinger = Ping.new(@pong.first, @ping.last, pid)
|
19
|
+
@ping.first.close
|
20
|
+
@pong.last.close
|
21
|
+
end
|
22
|
+
|
23
|
+
def pong
|
24
|
+
@ponger = Pong.new(@ping.first, @pong.last)
|
25
|
+
@ping.last.close
|
26
|
+
@pong.first.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop
|
30
|
+
@pinger.thread.kill
|
31
|
+
end
|
32
|
+
|
33
|
+
def fds
|
34
|
+
(@ping + @pong).map { |f| f.to_i }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
require "timeout"
|
3
|
+
|
4
|
+
module Micron
|
5
|
+
class Runner
|
6
|
+
class LivenessChecker
|
7
|
+
|
8
|
+
class Ping
|
9
|
+
|
10
|
+
include Debug
|
11
|
+
|
12
|
+
attr_reader :thread
|
13
|
+
|
14
|
+
def initialize(reader, writer, worker)
|
15
|
+
@thread = Thread.new(reader, writer, worker) { |reader, writer, worker|
|
16
|
+
|
17
|
+
Thread.current[:name] = "pinger-#{worker.pid}"
|
18
|
+
last_pong = Hitimes::Interval.now
|
19
|
+
|
20
|
+
begin
|
21
|
+
writer.sync = true
|
22
|
+
|
23
|
+
while true
|
24
|
+
begin
|
25
|
+
writer.puts "ping"
|
26
|
+
debug "sent ping"
|
27
|
+
|
28
|
+
reply = Timeout.timeout(0.1) { reader.readline }
|
29
|
+
|
30
|
+
if "pong\n" == reply then
|
31
|
+
last_pong = Hitimes::Interval.now
|
32
|
+
debug "got pong response"
|
33
|
+
end
|
34
|
+
|
35
|
+
rescue Exception => ex
|
36
|
+
if worker.wait_nonblock then
|
37
|
+
# process exited, no need to do anything more
|
38
|
+
# debug "no pong in #{last_pong.to_f} sec, but process exited"
|
39
|
+
break
|
40
|
+
end
|
41
|
+
|
42
|
+
debug "no pong received: #{Micron.dump_ex(ex)}"
|
43
|
+
if last_pong.to_f > 5.0 then
|
44
|
+
debug "no pong in #{last_pong.to_f} sec! Unleash the reaper!!"
|
45
|
+
Process.kill(9, worker.pid)
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sleep 0.1
|
51
|
+
end # while
|
52
|
+
|
53
|
+
rescue => ex
|
54
|
+
debug "caught: #{Micron.dump_ex(ex)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
debug "ping thread exiting"
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
end # Ping
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|