test-loop 12.0.2 → 12.0.3

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