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,36 @@
|
|
1
|
+
module Micron
|
2
|
+
class Runner
|
3
|
+
class LivenessChecker
|
4
|
+
|
5
|
+
class Pong
|
6
|
+
|
7
|
+
include Debug
|
8
|
+
|
9
|
+
attr_reader :thread
|
10
|
+
|
11
|
+
def initialize(reader, writer)
|
12
|
+
@thread = Thread.new(reader, writer) { |reader, writer|
|
13
|
+
|
14
|
+
Thread.current[:name] = "ponger"
|
15
|
+
debug "thread started"
|
16
|
+
|
17
|
+
begin
|
18
|
+
writer.sync = true
|
19
|
+
while line = reader.readline
|
20
|
+
debug "got ping request, replying"
|
21
|
+
writer.puts "pong"
|
22
|
+
end
|
23
|
+
|
24
|
+
rescue Exception => ex
|
25
|
+
debug "caught error: #{Micron.dump_ex(ex)}"
|
26
|
+
end
|
27
|
+
|
28
|
+
debug "thread exiting from #{$$}"
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
end # Pong
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
|
2
|
+
require 'hitimes'
|
3
|
+
|
4
|
+
module Micron
|
5
|
+
class Runner
|
6
|
+
class Method
|
7
|
+
|
8
|
+
attr_reader :clazz, :name, :durations
|
9
|
+
attr_accessor :passed, :ex, :assertions
|
10
|
+
attr_reader :stdout, :stderr
|
11
|
+
|
12
|
+
def initialize(clazz, name)
|
13
|
+
@passed = false
|
14
|
+
@durations = {}
|
15
|
+
@assertions = 0
|
16
|
+
|
17
|
+
@clazz = clazz
|
18
|
+
@name = name
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
out, err = Micron.capture_io {
|
23
|
+
run_test()
|
24
|
+
}
|
25
|
+
@stdout = out
|
26
|
+
@stderr = err
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Execute the actual test
|
31
|
+
def run_test
|
32
|
+
t = nil
|
33
|
+
|
34
|
+
begin
|
35
|
+
t = clazz.create
|
36
|
+
if t.respond_to? :micron_method= then
|
37
|
+
t.micron_method = self # minitest compat shim
|
38
|
+
end
|
39
|
+
|
40
|
+
time(:setup) { setup(t) }
|
41
|
+
|
42
|
+
time(:runtime) { t.send(name) }
|
43
|
+
self.passed = true
|
44
|
+
|
45
|
+
rescue *PASSTHROUGH_EXCEPTIONS
|
46
|
+
raise
|
47
|
+
|
48
|
+
rescue Exception => e
|
49
|
+
self.passed = false
|
50
|
+
self.ex = ExceptionInfo.new(e)
|
51
|
+
|
52
|
+
ensure
|
53
|
+
self.assertions += t._assertions if not t.nil?
|
54
|
+
time(:teardown) {
|
55
|
+
teardown(t) if not t.nil?
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def passed?
|
61
|
+
passed
|
62
|
+
end
|
63
|
+
|
64
|
+
def skipped?
|
65
|
+
ex.kind_of?(Skip)
|
66
|
+
end
|
67
|
+
|
68
|
+
def failed?
|
69
|
+
!passed
|
70
|
+
end
|
71
|
+
|
72
|
+
def status
|
73
|
+
if skipped? then
|
74
|
+
"skip"
|
75
|
+
elsif passed? then
|
76
|
+
"pass"
|
77
|
+
else
|
78
|
+
"fail"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get the total duration of this method's run (setup + runtime + teardown)
|
83
|
+
def total_duration
|
84
|
+
n = 0.0
|
85
|
+
@durations.values.each{ |d| n += d }
|
86
|
+
n
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Time the given block of code and enter it into the log
|
93
|
+
def time(name, &block)
|
94
|
+
@durations[name] = Hitimes::Interval.measure(&block)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Call setup methods
|
98
|
+
def setup(t)
|
99
|
+
t.before_setup
|
100
|
+
t.setup
|
101
|
+
t.after_setup
|
102
|
+
end
|
103
|
+
|
104
|
+
# Call teardown methods
|
105
|
+
def teardown(t)
|
106
|
+
%w{before_teardown teardown after_teardown}.each do |hook|
|
107
|
+
begin
|
108
|
+
t.send(hook)
|
109
|
+
rescue *PASSTHROUGH_EXCEPTIONS
|
110
|
+
raise
|
111
|
+
rescue Exception => e
|
112
|
+
self.passed = false
|
113
|
+
if self.ex.nil? then
|
114
|
+
self.ex = e
|
115
|
+
else
|
116
|
+
self.ex = [ self.ex, ExceptionInfo.new(e) ].flatten!
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
|
2
|
+
require "thwait"
|
3
|
+
|
4
|
+
require "micron/runner/process_reaper"
|
5
|
+
|
6
|
+
module Micron
|
7
|
+
class Runner
|
8
|
+
|
9
|
+
# Base class for parallel Clazz implementations
|
10
|
+
class ParallelClazz < Clazz
|
11
|
+
|
12
|
+
def run
|
13
|
+
# spawn tests in separate processes
|
14
|
+
tests = []
|
15
|
+
debug "spawning #{methods.size} methods"
|
16
|
+
methods.each do |method|
|
17
|
+
tests << spawn_test(method)
|
18
|
+
end
|
19
|
+
|
20
|
+
# wait for all test methods to return
|
21
|
+
@methods = wait_for_tests(tests)
|
22
|
+
|
23
|
+
# collect results
|
24
|
+
# @methods = collect_results(finished)
|
25
|
+
debug "collected #{@methods.size} methods"
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Wait for all test processes to complete, rerunning failures if needed
|
32
|
+
#
|
33
|
+
# @param [ForkWorker] tests
|
34
|
+
#
|
35
|
+
# @return [Array<Method>]
|
36
|
+
def wait_for_tests(tests)
|
37
|
+
|
38
|
+
# OUT.puts "waiting for tests"
|
39
|
+
|
40
|
+
results = []
|
41
|
+
watchers = []
|
42
|
+
hang_watchers = []
|
43
|
+
|
44
|
+
test_queue = Queue.new
|
45
|
+
tests.each { |t| test_queue.push(t) }
|
46
|
+
|
47
|
+
meta_watcher = Thread.new {
|
48
|
+
# thread which will make sure we're watching all tests, including
|
49
|
+
# any that get respawned
|
50
|
+
Thread.current[:name] = "meta_watcher"
|
51
|
+
|
52
|
+
while true
|
53
|
+
test = test_queue.pop # blocking
|
54
|
+
debug "creating watcher for #{test.pid}"
|
55
|
+
|
56
|
+
watchers << Thread.new(test) { |test|
|
57
|
+
Thread.current[:name] = "watcher-#{test.pid}"
|
58
|
+
debug "start"
|
59
|
+
|
60
|
+
while true
|
61
|
+
begin
|
62
|
+
status = test.wait2.to_i
|
63
|
+
# puts "process #{test.pid} exited with status #{status}"
|
64
|
+
|
65
|
+
if status == 0 then
|
66
|
+
method = collect_result(test)
|
67
|
+
Micron.runner.report(:end_method, method)
|
68
|
+
results << method
|
69
|
+
|
70
|
+
elsif status == 6 || status == 4 || status == 9 then
|
71
|
+
# segfault/coredump due to coverage
|
72
|
+
# puts "process #{test.pid} returned error"
|
73
|
+
method = test.context
|
74
|
+
test_queue << spawn_test(method) # new watcher thread will be spawned
|
75
|
+
debug "respawned failed test: #{method.clazz.name}##{method.name}"
|
76
|
+
|
77
|
+
else
|
78
|
+
debug
|
79
|
+
debug "== UNKNOWN ERROR! =="
|
80
|
+
debug "STATUS: #{status}"
|
81
|
+
if !test.stdout.empty? then
|
82
|
+
debug "STDOUT:"
|
83
|
+
debug test.stdout
|
84
|
+
else
|
85
|
+
debug "NO STDOUT"
|
86
|
+
end
|
87
|
+
if !test.stderr.empty? then
|
88
|
+
debug "STDERR:"
|
89
|
+
debug test.stderr
|
90
|
+
else
|
91
|
+
debug "NO STDERR"
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
rescue Errno::ECHILD
|
97
|
+
debug "retrying wait2"
|
98
|
+
next # retry - should get cached status
|
99
|
+
|
100
|
+
rescue Exception => ex
|
101
|
+
debug "caught: #{Micron.dump_ex(ex)}"
|
102
|
+
end
|
103
|
+
|
104
|
+
break # break loop by default
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
debug "exit thread"
|
109
|
+
watchers.delete(Thread.current)
|
110
|
+
}
|
111
|
+
|
112
|
+
# create another thread to make sure the process didn't hang after
|
113
|
+
# throwing an error on stderr
|
114
|
+
hang_watchers << ProcessReaper.create(test)
|
115
|
+
end
|
116
|
+
|
117
|
+
debug "exiting"
|
118
|
+
}
|
119
|
+
|
120
|
+
while !watchers.empty? || !test_queue.empty?
|
121
|
+
watchers.reject!{ |w| !w.alive? } # prune dead threads
|
122
|
+
ThreadsWait.all_waits(*watchers)
|
123
|
+
end
|
124
|
+
debug "all watcher threads finished for #{self.name}"
|
125
|
+
meta_watcher.kill
|
126
|
+
hang_watchers.each{ |t| t.kill }
|
127
|
+
|
128
|
+
return results
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
|
5
|
+
# A Clazz implementation which will fork/exec before running each test method
|
6
|
+
class ProcClazz < ParallelClazz
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Spawn a process for the given method
|
11
|
+
#
|
12
|
+
# @param [Method] method
|
13
|
+
# @param [Boolean] dispose_output If true, throw away stdout/stderr (default: true)
|
14
|
+
#
|
15
|
+
# @return [Hash]
|
16
|
+
def spawn_test(method, dispose_output=true)
|
17
|
+
# fork/exec once per method, synchronously
|
18
|
+
ENV["MICRON_TEST_CLASS"] = method.clazz.name
|
19
|
+
ENV["MICRON_TEST_METHOD"] = method.name.to_s
|
20
|
+
|
21
|
+
ForkWorker.new(method) {
|
22
|
+
exec("bundle exec micron --runmethod")
|
23
|
+
}.run
|
24
|
+
end
|
25
|
+
|
26
|
+
# Because we fork exec, we can't just read the back from a pipe. Instead,
|
27
|
+
# the child process dumps it to a file and we load it from there.
|
28
|
+
#
|
29
|
+
# @param [ForkWorker] test
|
30
|
+
#
|
31
|
+
# @return [Method]
|
32
|
+
def collect_result(test)
|
33
|
+
results = []
|
34
|
+
data_file = File.join(ENV["MICRON_PATH"], "#{test.pid}.data")
|
35
|
+
|
36
|
+
File.open(data_file) do |f|
|
37
|
+
while !f.eof
|
38
|
+
results << Marshal.load(f) # read Method from child via file
|
39
|
+
end
|
40
|
+
end
|
41
|
+
File.delete(data_file)
|
42
|
+
|
43
|
+
return results.first # should always be just one
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
|
2
|
+
module Micron
|
3
|
+
class Runner
|
4
|
+
class ProcessReaper
|
5
|
+
|
6
|
+
extend Debug
|
7
|
+
|
8
|
+
def self.create(test)
|
9
|
+
Thread.new(test) { |test|
|
10
|
+
|
11
|
+
Thread.current[:name] = "reaper-#{test.pid}"
|
12
|
+
debug "started"
|
13
|
+
|
14
|
+
err = 0
|
15
|
+
sel = 0
|
16
|
+
open = false
|
17
|
+
while true
|
18
|
+
|
19
|
+
if test.wait_nonblock then
|
20
|
+
# process exited!
|
21
|
+
break
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
|
26
|
+
if err > 10 then # should wait about 3 sec for the proc to exit
|
27
|
+
debug "Unleash the reaper!!"
|
28
|
+
Process.kill(9, test.pid)
|
29
|
+
break
|
30
|
+
end
|
31
|
+
|
32
|
+
if !open then
|
33
|
+
if IO.select([test.err], nil, nil, 1).nil? then
|
34
|
+
sel += 1
|
35
|
+
debug "select = #{sel}"
|
36
|
+
# if sel > 3 then
|
37
|
+
# # thread dead??
|
38
|
+
# debug "not ready yet?! Unleash the reaper!! #{test.pid}"
|
39
|
+
# Process.kill(9, test.pid)
|
40
|
+
# break
|
41
|
+
# end
|
42
|
+
err += 1 if err > 0
|
43
|
+
debug "err = #{err}"
|
44
|
+
Thread.pass
|
45
|
+
next
|
46
|
+
end
|
47
|
+
debug "opened err io"
|
48
|
+
open = true
|
49
|
+
end
|
50
|
+
|
51
|
+
str = test.err.read_nonblock(1024*16)
|
52
|
+
debug str if !str.nil?
|
53
|
+
if !str.nil? &&
|
54
|
+
(str.include?("malloc: *** error for object") ||
|
55
|
+
str.include?("Segmentation fault")) then
|
56
|
+
|
57
|
+
debug "looks like we got an error"
|
58
|
+
err = 1
|
59
|
+
end
|
60
|
+
|
61
|
+
rescue EOFError
|
62
|
+
debug "caught EOFError"
|
63
|
+
err = 1
|
64
|
+
# see if it exited
|
65
|
+
if test.wait_nonblock then
|
66
|
+
# exited, we're all good..
|
67
|
+
debug "hang watcher exiting since it looks like process exited also"
|
68
|
+
break
|
69
|
+
end
|
70
|
+
|
71
|
+
rescue Errno::EWOULDBLOCK
|
72
|
+
# debug "would block?!"
|
73
|
+
open = false
|
74
|
+
next
|
75
|
+
|
76
|
+
rescue Exception => ex
|
77
|
+
debug "caught another ex?!"
|
78
|
+
debug Micron.dump_ex(ex, true)
|
79
|
+
err = 1
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
if err > 0 then
|
84
|
+
err += 1
|
85
|
+
sleep 0.1
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
reapers.delete(Thread.current)
|
91
|
+
debug "thread exiting"
|
92
|
+
}
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
module Micron
|
5
|
+
class Runner
|
6
|
+
class Shim
|
7
|
+
|
8
|
+
# Create a temp shim path
|
9
|
+
def self.setup
|
10
|
+
|
11
|
+
ruby_path = `which ruby`.strip
|
12
|
+
shim = <<-EOF
|
13
|
+
#!#{ruby_path}
|
14
|
+
|
15
|
+
if ENV["BUNDLE_GEMFILE"] then
|
16
|
+
require "bundler"
|
17
|
+
Bundler.setup(:default, :development)
|
18
|
+
end
|
19
|
+
|
20
|
+
require "easycov"
|
21
|
+
|
22
|
+
EasyCov.path = ENV["EASYCOV_PATH"]
|
23
|
+
EasyCov.filters << EasyCov::IGNORE_GEMS << EasyCov::IGNORE_STDLIB
|
24
|
+
EasyCov.start
|
25
|
+
EasyCov.install_exit_hook()
|
26
|
+
|
27
|
+
script = ARGV.shift
|
28
|
+
$0 = script
|
29
|
+
require script
|
30
|
+
EOF
|
31
|
+
|
32
|
+
@shim_dir = Dir.mktmpdir("micron-shim-")
|
33
|
+
file = File.join(@shim_dir, "ruby")
|
34
|
+
File.open(file, 'w') do |f|
|
35
|
+
f.write(shim)
|
36
|
+
end
|
37
|
+
File.chmod(0777, file)
|
38
|
+
EasyCov::Filters.stdlib_paths # make sure this gets cached in env
|
39
|
+
|
40
|
+
end # setup
|
41
|
+
|
42
|
+
# Clean up any existing shim dirs. This should be called only when the
|
43
|
+
# master process exists (i.e. Micron::App)
|
44
|
+
def self.cleanup!
|
45
|
+
# return
|
46
|
+
Dir.glob(File.join(Dir.tmpdir, "micron-shim-*")).each do |d|
|
47
|
+
FileUtils.rm_rf(d)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Wrap the given call with our shim PATH. Any calls to ruby will be
|
52
|
+
# redirected to our script to enable coverage collection.
|
53
|
+
def self.wrap(&block)
|
54
|
+
# enable shim
|
55
|
+
ENV["EASYCOV_PATH"] = EasyCov.path
|
56
|
+
old_path = ENV["PATH"]
|
57
|
+
ENV["PATH"] = "#{@shim_dir}:#{old_path}"
|
58
|
+
|
59
|
+
# call orig method
|
60
|
+
block.call()
|
61
|
+
|
62
|
+
# disable shim
|
63
|
+
ENV["PATH"] = old_path
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|