micron 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|