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