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,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
|