test-loop 12.0.2 → 12.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  Copyright 2010 Suraj N. Kurapati <sunaku@gmail.com>
4
4
  Copyright 2011 Brian D. Burns <burns180@gmail.com>
5
+ Copyright 2011 Daniel Pittman <daniel@rimspace.net>
5
6
 
6
7
  Permission to use, copy, modify, and/or distribute this software for any
7
8
  purpose with or without fee is hereby granted, provided that the above
data/README.md CHANGED
@@ -89,7 +89,7 @@ Operation
89
89
  * Press Control-Z or send the SIGTSTP signal to forcibly run all
90
90
  tests, even if there are no changes in your Ruby application.
91
91
 
92
- * Press Control-\ or send the SIGQUIT signal to forcibly reabsorb
92
+ * Press Control-\\ or send the SIGQUIT signal to forcibly reabsorb
93
93
  the test execution overhead, even if its sources have not changed.
94
94
 
95
95
  * Press Control-C or send the SIGINT signal to quit the test loop.
@@ -211,7 +211,7 @@ preset does this for you):
211
211
 
212
212
  message = test_file
213
213
 
214
- Thread.new do # run in background
214
+ fork do # run in background
215
215
  system 'notify-send', '-i', 'dialog-error', title, message or
216
216
  system 'growlnotify', '-a', 'Xcode', '-m', message, title or
217
217
  system 'xmessage', '-timeout', '5', '-title', title, message
@@ -233,7 +233,7 @@ configuration file:
233
233
 
234
234
  message = test_file
235
235
 
236
- Thread.new do # run in background
236
+ fork do # run in background
237
237
  system 'notify-send', '-i', "dialog-#{success ? 'information' : 'error'}", title, message or
238
238
  system 'growlnotify', '-a', 'Xcode', '-m', message, title or
239
239
  system 'xmessage', '-timeout', '5', '-title', title, message
data/bin/test-loop CHANGED
@@ -1,4 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'test/loop'
3
3
  Test::Loop.run
4
- exit! true # skip at_exit() handlers registered in the master process
data/lib/test/loop.rb CHANGED
@@ -49,11 +49,12 @@ module Test
49
49
 
50
50
  class << Loop
51
51
  def run
52
- initialize_vars
53
- register_signals
52
+ init_shared_vars
53
+ trap_user_signals
54
54
  load_user_config
55
- absorb_overhead
56
- run_test_loop
55
+ load_user_overhead
56
+ init_worker_queue
57
+ enter_testing_loop
57
58
 
58
59
  rescue Interrupt
59
60
  # allow user to break the loop
@@ -64,7 +65,7 @@ module Test
64
65
  reload_master_process
65
66
 
66
67
  ensure
67
- kill_workers
68
+ stop_worker_queue
68
69
  notify 'Goodbye!'
69
70
  end
70
71
 
@@ -78,7 +79,8 @@ module Test
78
79
  ANSI_GREEN = "\e[32m%s\e[0m".freeze
79
80
  ANSI_RED = "\e[31m%s\e[0m".freeze
80
81
 
81
- Worker = Struct.new(:test_file, :log_file, :started_at)
82
+ Worker = Struct.new(:test_file, :log_file, :started_at, :finished_at,
83
+ :exit_status)
82
84
 
83
85
  def notify message
84
86
  # using print() because puts() is not an atomic operation.
@@ -87,64 +89,33 @@ module Test
87
89
  print "#{ANSI_CLEAR_LINE}test-loop: #{message}\n"
88
90
  end
89
91
 
90
- def initialize_vars
91
- @running_files = []
92
+ def pause_momentarily
93
+ sleep 1
94
+ end
95
+
96
+ def init_shared_vars
92
97
  @lines_by_file = {} # path => readlines
93
98
  @last_ran_at = @started_at = Time.now
94
99
  @worker_by_pid = {}
95
100
  end
96
101
 
97
- def register_signals
98
- # this signal is ignored in master and honored in workers, so all
99
- # workers can be killed by sending it to the entire process group
100
- trap :TERM, 'IGNORE'
101
-
102
- trap :CHLD do
103
- finished_at = Time.now
104
-
105
- begin
106
- worker_pid = Process.wait
107
- run_status = $?
108
-
109
- if worker = @worker_by_pid.delete(worker_pid)
110
- @running_files.delete worker.test_file
111
-
112
- # report test results along with any failure logs
113
- if run_status.success?
114
- notify ANSI_GREEN % "PASS #{worker.test_file}"
115
- elsif run_status.exited?
116
- notify ANSI_RED % "FAIL #{worker.test_file}"
117
- STDERR.print File.read(worker.log_file)
118
- end
119
-
120
- after_each_test.each do |hook|
121
- hook.call worker.test_file, worker.log_file, run_status,
122
- worker.started_at, finished_at - worker.started_at
123
- end
124
- end
125
- rescue Errno::ECHILD
126
- # could not get the terminated child's PID.
127
- # Ruby's backtick operator can cause this:
128
- # http://stackoverflow.com/questions/1495354
129
- end
130
- end
131
-
132
- trap(:INT) { raise Interrupt }
102
+ def trap_user_signals
103
+ trap(:INT) { raise Interrupt }
133
104
  trap(:TSTP) { forcibly_run_all_tests }
134
105
  trap(:QUIT) { reload_master_process }
135
106
  end
136
107
 
137
- def kill_workers
138
- notify 'Stopping tests...'
139
- Process.kill :TERM, -$$
140
- Process.waitall
108
+ def forcibly_run_all_tests
109
+ notify 'Running all tests...'
110
+ @last_ran_at = Time.at(0)
111
+ @lines_by_file.clear
141
112
  end
142
113
 
143
114
  # The given test files are passed down (along with currently running
144
115
  # test files) to the next incarnation of test-loop for resumption.
145
116
  def reload_master_process test_files = []
146
- test_files.concat @running_files
147
- kill_workers
117
+ test_files.concat currently_running_test_files
118
+ stop_worker_queue
148
119
  ENV.replace MASTER_ENV.merge(RESUME_ENV_KEY => test_files.inspect)
149
120
  exec(*MASTER_EXECV)
150
121
  end
@@ -156,7 +127,7 @@ module Test
156
127
  end
157
128
  end
158
129
 
159
- def absorb_overhead
130
+ def load_user_overhead
160
131
  notify 'Absorbing overhead...'
161
132
  $LOAD_PATH.unshift 'lib', 'test', 'spec'
162
133
  Dir[*overhead_file_globs].each do |file|
@@ -164,19 +135,11 @@ module Test
164
135
  end
165
136
  end
166
137
 
167
- def pause_momentarily
168
- sleep 1
169
- end
170
-
171
- def forcibly_run_all_tests
172
- notify 'Running all tests...'
173
- @last_ran_at = Time.at(0)
174
- @lines_by_file.clear
175
- end
176
-
177
- def run_test_loop
138
+ def enter_testing_loop
178
139
  notify 'Ready for testing!'
179
140
  loop do
141
+ reap_worker_queue
142
+
180
143
  # find test files that have been modified since the last run
181
144
  test_files = test_file_matchers.map do |source_glob, test_matcher|
182
145
  Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
@@ -200,42 +163,85 @@ module Test
200
163
 
201
164
  # fork workers to run the test files in parallel,
202
165
  # excluding test files that are already running
203
- test_files -= @running_files
166
+ test_files -= currently_running_test_files
204
167
  unless test_files.empty?
205
168
  @last_ran_at = Time.now
206
- test_files.each {|file| run_test_file file }
169
+ test_files.each {|file| fork_worker Worker.new(file) }
207
170
  end
208
171
 
209
172
  pause_momentarily
210
173
  end
211
174
  end
212
175
 
213
- def run_test_file test_file
214
- notify "TEST #{test_file}"
176
+ def currently_running_test_files
177
+ @worker_by_pid.values.map(&:test_file)
178
+ end
179
+
180
+ def init_worker_queue
181
+ # collect children (of which some may be workers) for reaping below
182
+ @exited_child_infos = []
183
+ trap :CHLD do
184
+ finished_at = Time.now
185
+ begin
186
+ while wait2_array = Process.wait2(-1, Process::WNOHANG)
187
+ @exited_child_infos.push [wait2_array, finished_at]
188
+ end
189
+ rescue SystemCallError
190
+ # raised by wait() when there are no child processes at all
191
+ end
192
+ end
193
+ end
194
+
195
+ # reap finished workers from previous iterations of the loop
196
+ def reap_worker_queue
197
+ while info = @exited_child_infos.shift
198
+ (child_pid, exit_status), finished_at = info
199
+ if worker = @worker_by_pid.delete(child_pid)
200
+ worker.exit_status = exit_status
201
+ worker.finished_at = finished_at
202
+ reap_worker worker
203
+ end
204
+ end
205
+ end
206
+
207
+ def stop_worker_queue
208
+ notify 'Stopping tests...'
209
+ trap :CHLD, 'DEFAULT'
210
+ @worker_by_pid.each_key do |worker_pid|
211
+ begin
212
+ Process.kill :TERM, -worker_pid
213
+ rescue SystemCallError
214
+ # worker is already terminated
215
+ end
216
+ end
217
+ Process.waitall
218
+ end
219
+
220
+ def fork_worker worker
221
+ notify "TEST #{worker.test_file}"
215
222
 
216
- @running_files.push test_file
217
- log_file = test_file + '.log'
223
+ worker.log_file = worker.test_file + '.log'
218
224
 
219
225
  # cache the contents of the test file for diffing below
220
- new_lines = File.readlines(test_file)
221
- old_lines = @lines_by_file[test_file] || new_lines
222
- @lines_by_file[test_file] = new_lines
226
+ new_lines = File.readlines(worker.test_file)
227
+ old_lines = @lines_by_file[worker.test_file] || new_lines
228
+ @lines_by_file[worker.test_file] = new_lines
223
229
 
224
- started_at = Time.now
225
- worker_pid = fork do
230
+ worker.started_at = Time.now
231
+ pid = fork do
226
232
  # detach worker from master's terminal device so that
227
233
  # it does not receieve the user's control-key presses
228
234
  Process.setsid
229
235
 
230
236
  # unregister signal handlers inherited from master process
231
- [:TERM, :CHLD, :INT, :TSTP, :QUIT].each {|sig| trap sig, 'DEFAULT' }
237
+ [:INT, :TSTP, :QUIT].each {|sig| trap sig, 'DEFAULT' }
232
238
 
233
239
  # detach worker from master's standard input stream
234
240
  STDIN.reopen IO.pipe.first
235
241
 
236
242
  # capture test output in log file because tests are run in parallel
237
243
  # which makes it difficult to understand interleaved output thereof
238
- STDERR.reopen(STDOUT.reopen(log_file, 'w')).sync = true
244
+ STDERR.reopen(STDOUT.reopen(worker.log_file, 'w')).sync = true
239
245
 
240
246
  # determine which test blocks have changed inside the test file
241
247
  test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
@@ -251,18 +257,35 @@ module Test
251
257
  end.compact.uniq
252
258
 
253
259
  # tell the testing framework to run only the changed test blocks
254
- before_each_test.each {|f| f.call test_file, log_file, test_names }
260
+ before_each_test.each do |hook|
261
+ hook.call worker.test_file, worker.log_file, test_names
262
+ end
255
263
 
256
264
  # make the process title Test::Unit friendly and ps(1) searchable
257
- $0 = "test-loop #{test_file}"
265
+ $0 = "test-loop #{worker.test_file}"
258
266
 
259
267
  # after loading the user's test file, the at_exit() hook of the
260
268
  # user's testing framework will take care of running the tests and
261
269
  # reflecting any failures in the worker process' exit status
262
- load test_file
270
+ load worker.test_file
263
271
  end
264
272
 
265
- @worker_by_pid[worker_pid] = Worker.new(test_file, log_file, started_at)
273
+ @worker_by_pid[pid] = worker
274
+ end
275
+
276
+ def reap_worker worker
277
+ # report test results along with any failure logs
278
+ if worker.exit_status.success?
279
+ notify ANSI_GREEN % "PASS #{worker.test_file}"
280
+ elsif worker.exit_status.exited?
281
+ notify ANSI_RED % "FAIL #{worker.test_file}"
282
+ STDERR.print File.read(worker.log_file)
283
+ end
284
+
285
+ after_each_test.each do |hook|
286
+ hook.call worker.test_file, worker.log_file, worker.exit_status,
287
+ worker.started_at, worker.finished_at - worker.started_at
288
+ end
266
289
  end
267
290
  end
268
291
  end
@@ -5,7 +5,7 @@ Test::Loop.after_each_test.push lambda {
5
5
  unless run_status.success? or run_status.signaled?
6
6
  title = 'FAIL at %s in %0.1fs' % [started_at.strftime('%r'), elapsed_time]
7
7
  message = test_file
8
- Thread.new do # run in background
8
+ fork do # run in background
9
9
  system 'notify-send', '-i', 'dialog-error', title, message or
10
10
  system 'growlnotify', '-a', 'Xcode', '-m', message, title or
11
11
  system 'xmessage', '-timeout', '5', '-title', title, message
metadata CHANGED
@@ -2,17 +2,17 @@
2
2
  name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 12.0.2
5
+ version: 12.0.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - Suraj N. Kurapati
9
9
  - Brian D. Burns
10
+ - Daniel Pittman
10
11
  autorequire:
11
12
  bindir: bin
12
13
  cert_chain: []
13
14
 
14
- date: 2011-04-21 00:00:00 -07:00
15
- default_executable:
15
+ date: 2011-04-26 00:00:00 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: diff-lcs
@@ -40,7 +40,6 @@ files:
40
40
  - lib/test/loop.rb
41
41
  - lib/test/loop/notify.rb
42
42
  - lib/test/loop/rails.rb
43
- has_rdoc: true
44
43
  homepage: http://github.com/sunaku/test-loop
45
44
  licenses: []
46
45
 
@@ -64,9 +63,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
63
  requirements: []
65
64
 
66
65
  rubyforge_project:
67
- rubygems_version: 1.6.2
66
+ rubygems_version: 1.7.2
68
67
  signing_key:
69
68
  specification_version: 3
70
69
  summary: Continuous testing for Ruby with fork/eval
71
70
  test_files: []
72
71
 
72
+ has_rdoc: