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,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,16 @@
1
+
2
+ module Micron
3
+ class Runner
4
+ class ExceptionInfo
5
+
6
+ attr_reader :name, :message, :backtrace
7
+
8
+ def initialize(ex)
9
+ @name = ex.class.to_s
10
+ @message = ex.message
11
+ @backtrace = ex.backtrace
12
+ end
13
+
14
+ end
15
+ end
16
+ 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