micron 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +19 -0
  3. data/Gemfile.lock +88 -0
  4. data/Rakefile +40 -0
  5. data/VERSION +1 -0
  6. data/bin/micron +4 -0
  7. data/lib/micron.rb +29 -0
  8. data/lib/micron/app.rb +127 -0
  9. data/lib/micron/app/options.rb +73 -0
  10. data/lib/micron/assertion.rb +10 -0
  11. data/lib/micron/fork_runner.rb +55 -0
  12. data/lib/micron/minitest.rb +45 -0
  13. data/lib/micron/proc_runner.rb +114 -0
  14. data/lib/micron/rake.rb +29 -0
  15. data/lib/micron/reporter.rb +30 -0
  16. data/lib/micron/reporter/console.rb +146 -0
  17. data/lib/micron/reporter/coverage.rb +37 -0
  18. data/lib/micron/runner.rb +95 -0
  19. data/lib/micron/runner/backtrace_filter.rb +39 -0
  20. data/lib/micron/runner/clazz.rb +45 -0
  21. data/lib/micron/runner/clazz19.rb +24 -0
  22. data/lib/micron/runner/debug.rb +22 -0
  23. data/lib/micron/runner/exception_info.rb +16 -0
  24. data/lib/micron/runner/fork_worker.rb +185 -0
  25. data/lib/micron/runner/forking_clazz.rb +40 -0
  26. data/lib/micron/runner/liveness_checker.rb +40 -0
  27. data/lib/micron/runner/liveness_checker/ping.rb +65 -0
  28. data/lib/micron/runner/liveness_checker/pong.rb +36 -0
  29. data/lib/micron/runner/method.rb +124 -0
  30. data/lib/micron/runner/parallel_clazz.rb +135 -0
  31. data/lib/micron/runner/proc_clazz.rb +48 -0
  32. data/lib/micron/runner/process_reaper.rb +98 -0
  33. data/lib/micron/runner/shim.rb +68 -0
  34. data/lib/micron/runner/test_file.rb +79 -0
  35. data/lib/micron/test_case.rb +36 -0
  36. data/lib/micron/test_case/assertions.rb +701 -0
  37. data/lib/micron/test_case/lifecycle_hooks.rb +74 -0
  38. data/lib/micron/test_case/redir_logging.rb +85 -0
  39. data/lib/micron/test_case/teardown_coverage.rb +13 -0
  40. data/lib/micron/util/ex.rb +23 -0
  41. data/lib/micron/util/io.rb +54 -0
  42. data/lib/micron/util/thread_dump.rb +29 -0
  43. data/micron.gemspec +97 -0
  44. 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