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