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 +1 -0
- data/README.md +3 -3
- data/bin/test-loop +0 -1
- data/lib/test/loop.rb +102 -79
- data/lib/test/loop/notify.rb +1 -1
- metadata +5 -5
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
|
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
|
-
|
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
|
-
|
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
data/lib/test/loop.rb
CHANGED
@@ -49,11 +49,12 @@ module Test
|
|
49
49
|
|
50
50
|
class << Loop
|
51
51
|
def run
|
52
|
-
|
53
|
-
|
52
|
+
init_shared_vars
|
53
|
+
trap_user_signals
|
54
54
|
load_user_config
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
91
|
-
|
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
|
98
|
-
|
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
|
138
|
-
notify '
|
139
|
-
|
140
|
-
|
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
|
147
|
-
|
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
|
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
|
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 -=
|
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|
|
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
|
214
|
-
|
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
|
-
|
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
|
-
|
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
|
-
[:
|
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
|
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[
|
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
|
data/lib/test/loop/notify.rb
CHANGED
@@ -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
|
-
|
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.
|
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-
|
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.
|
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:
|