test-loop 11.0.0 → 12.2.0
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 +17 -0
- data/README.md +80 -45
- data/bin/test-loop +1 -1
- data/lib/test/loop/notify.rb +2 -1
- data/lib/test/loop/rails.rb +20 -0
- data/lib/test/loop.rb +124 -93
- metadata +6 -8
- data/lib/test/loop/support/minitest.rb +0 -5
- data/lib/test/loop/support/rails.rb +0 -21
- data/lib/test/loop/support/testunit.rb +0 -7
data/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
(the ISC license)
|
|
2
|
+
|
|
3
|
+
Copyright 2010 Suraj N. Kurapati <sunaku@gmail.com>
|
|
4
|
+
Copyright 2011 Brian D. Burns <burns180@gmail.com>
|
|
5
|
+
Copyright 2011 Daniel Pittman <daniel@rimspace.net>
|
|
6
|
+
|
|
7
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
9
|
+
copyright notice and this permission notice appear in all copies.
|
|
10
|
+
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
12
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
13
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
14
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
15
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
16
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
17
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
CHANGED
|
@@ -4,33 +4,50 @@ test-loop - Continuous testing for Ruby with fork/eval
|
|
|
4
4
|
test-loop is a fast continuous testing tool for Ruby that automatically
|
|
5
5
|
detects and tests changes in your application in an efficient manner:
|
|
6
6
|
|
|
7
|
-
1. Absorbs the test execution overhead into the main Ruby process.
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
1. Absorbs the test execution overhead into the main Ruby process.
|
|
8
|
+
|
|
9
|
+
2. Forks to run your test files without overhead and in parallel.
|
|
10
|
+
|
|
11
|
+
3. Avoids running unchanged test blocks inside changed test files.
|
|
10
12
|
|
|
11
13
|
------------------------------------------------------------------------------
|
|
12
14
|
Features
|
|
13
15
|
------------------------------------------------------------------------------
|
|
14
16
|
|
|
15
|
-
* Tests *changes* in your Ruby application: avoids running (1) unchanged
|
|
16
|
-
|
|
17
|
+
* Tests *changes* in your Ruby application: avoids running (1) unchanged
|
|
18
|
+
test files and (2) unchanged test blocks inside changed test files.
|
|
19
|
+
|
|
20
|
+
* Supports Test::Unit, RSpec, and any other testing framework that (1)
|
|
21
|
+
reflects failures in the process' exit status and (2) is loaded by your
|
|
22
|
+
application's `test/test_helper.rb` or `spec/spec_helper.rb` file.
|
|
23
|
+
|
|
24
|
+
* Reabsorbs test execution overhead if the test or spec helper file changes.
|
|
25
|
+
|
|
26
|
+
* Executes test files in parallel, making full use of multiple processors.
|
|
17
27
|
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
application's `test/test_helper.rb` or `spec/spec_helper.rb` file.
|
|
28
|
+
* Logs the output from your tests into separate files: one log per test.
|
|
29
|
+
The path to a log file is simply the path of its test file plus ".log".
|
|
21
30
|
|
|
22
|
-
*
|
|
31
|
+
* Generally I/O bound, so you can keep it running without CPU slowdown.
|
|
32
|
+
|
|
33
|
+
* Configurable through a `.test-loop` file in your working directory.
|
|
34
|
+
|
|
35
|
+
* Implemented in less than 240 lines (SLOC) of pure Ruby code! :-)
|
|
36
|
+
|
|
37
|
+
------------------------------------------------------------------------------
|
|
38
|
+
Prerequisites
|
|
39
|
+
------------------------------------------------------------------------------
|
|
23
40
|
|
|
24
|
-
*
|
|
41
|
+
* Ruby 1.8.7 or 1.9.2 or newer.
|
|
25
42
|
|
|
26
|
-
*
|
|
27
|
-
The path to a log file is simply the path of its test file plus ".log".
|
|
43
|
+
* Operating system that supports POSIX signals and the `fork()` system call.
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
To check if your system qualifies, launch `irb` and enter the following:
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
Process.respond_to? :fork # must be true
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
signals = %w[INT TSTP QUIT TERM CHLD].sort
|
|
50
|
+
Signal.list.keys.sort & signals == signals # must be true
|
|
34
51
|
|
|
35
52
|
------------------------------------------------------------------------------
|
|
36
53
|
Installation
|
|
@@ -59,19 +76,23 @@ If installed as a Git clone:
|
|
|
59
76
|
|
|
60
77
|
You can monitor your test processes in another terminal:
|
|
61
78
|
|
|
62
|
-
watch 'ps
|
|
79
|
+
watch 'ps xfw | sed -n "1p; /test-loop/p" | fgrep -v sed'
|
|
80
|
+
|
|
81
|
+
If it stops responding, you can annihilate test-loop from another terminal:
|
|
82
|
+
|
|
83
|
+
pkill -9 -f test-loop
|
|
63
84
|
|
|
64
85
|
------------------------------------------------------------------------------
|
|
65
86
|
Operation
|
|
66
87
|
------------------------------------------------------------------------------
|
|
67
88
|
|
|
68
|
-
* Press Control-Z or send the SIGTSTP signal to forcibly run all
|
|
69
|
-
|
|
89
|
+
* Press Control-Z or send the SIGTSTP signal to forcibly run all
|
|
90
|
+
tests, even if there are no changes in your Ruby application.
|
|
70
91
|
|
|
71
|
-
* Press Control
|
|
72
|
-
|
|
92
|
+
* Press Control-\\ or send the SIGQUIT signal to forcibly reabsorb
|
|
93
|
+
the test execution overhead, even if its sources have not changed.
|
|
73
94
|
|
|
74
|
-
* Press Control-C or send the SIGINT signal to quit the test loop.
|
|
95
|
+
* Press Control-C or send the SIGINT signal to quit the test loop.
|
|
75
96
|
|
|
76
97
|
------------------------------------------------------------------------------
|
|
77
98
|
Configuration
|
|
@@ -81,19 +102,23 @@ test-loop looks for a configuration file named `.test-loop` in the current
|
|
|
81
102
|
working directory. This configuration file is a normal Ruby script in which
|
|
82
103
|
you can query and modify the `Test::Loop` OpenStruct configuration as follows:
|
|
83
104
|
|
|
105
|
+
### Test::Loop.delay_per_iteration
|
|
106
|
+
|
|
107
|
+
Number of seconds to wait after each loop iteration. The default value is 1.
|
|
108
|
+
|
|
84
109
|
### Test::Loop.overhead_file_globs
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
111
|
+
Array of file globbing patterns that describe a set of Ruby scripts that are
|
|
112
|
+
loaded into the main Ruby process as overhead.
|
|
88
113
|
|
|
89
114
|
### Test::Loop.reabsorb_file_globs
|
|
90
115
|
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
Array of file globbing patterns that describe a set of files which cause the
|
|
117
|
+
overhead to be reabsorbed whenever they change.
|
|
93
118
|
|
|
94
119
|
### Test::Loop.test_file_matchers
|
|
95
120
|
|
|
96
|
-
|
|
121
|
+
Hash that maps a file globbing pattern describing a set of source files to a
|
|
97
122
|
lambda function yielding a file globbing pattern describing a set of test
|
|
98
123
|
files that need to be run. In other words, whenever the source files (the
|
|
99
124
|
hash key; left-hand side of the mapping) change, their associated test files
|
|
@@ -102,8 +127,8 @@ hash key; left-hand side of the mapping) change, their associated test files
|
|
|
102
127
|
For example, if test files had the same names as their source files followed
|
|
103
128
|
by an underscore and the file name in reverse like this:
|
|
104
129
|
|
|
105
|
-
* `lib/hello.rb` => `test/hello_olleh.rb`
|
|
106
|
-
* `app/world.rb` => `spec/world_ldrow.rb`
|
|
130
|
+
* `lib/hello.rb` => `test/hello_olleh.rb`
|
|
131
|
+
* `app/world.rb` => `spec/world_ldrow.rb`
|
|
107
132
|
|
|
108
133
|
Then you would add the following to your configuration file:
|
|
109
134
|
|
|
@@ -128,9 +153,9 @@ will return `nil`, thereby excluding those source files from being tested.
|
|
|
128
153
|
|
|
129
154
|
### Test::Loop.test_name_parser
|
|
130
155
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
Lambda function that is passed a line of source code to determine whether that
|
|
157
|
+
line can be considered as a test definition, in which case it must return the
|
|
158
|
+
name of the test being defined.
|
|
134
159
|
|
|
135
160
|
### Test::Loop.before_each_test
|
|
136
161
|
|
|
@@ -188,7 +213,8 @@ preset does this for you):
|
|
|
188
213
|
unless run_status.success? or run_status.signaled?
|
|
189
214
|
title = 'FAIL at %s in %0.1fs' % [started_at.strftime('%r'), elapsed_time]
|
|
190
215
|
|
|
191
|
-
|
|
216
|
+
statistics = File.readlines(log_file).grep(/^\d+ \w+,/)
|
|
217
|
+
message = test_file + "\n" + statistics.join
|
|
192
218
|
|
|
193
219
|
Thread.new do # run in background
|
|
194
220
|
system 'notify-send', '-i', 'dialog-error', title, message or
|
|
@@ -210,7 +236,8 @@ configuration file:
|
|
|
210
236
|
title = '%s at %s in %0.1fs' %
|
|
211
237
|
[success ? 'PASS' : 'FAIL', started_at.strftime('%X'), elapsed_time]
|
|
212
238
|
|
|
213
|
-
|
|
239
|
+
statistics = File.readlines(log_file).grep(/^\d+ \w+,/)
|
|
240
|
+
message = test_file + "\n" + statistics.join
|
|
214
241
|
|
|
215
242
|
Thread.new do # run in background
|
|
216
243
|
system 'notify-send', '-i', "dialog-#{success ? 'information' : 'error'}", title, message or
|
|
@@ -220,7 +247,7 @@ configuration file:
|
|
|
220
247
|
}
|
|
221
248
|
|
|
222
249
|
------------------------------------------------------------------------------
|
|
223
|
-
Configuration
|
|
250
|
+
Configuration presets
|
|
224
251
|
------------------------------------------------------------------------------
|
|
225
252
|
|
|
226
253
|
The following sub-libraries provide "preset" configurations. To use them,
|
|
@@ -231,26 +258,34 @@ your application's `test/test_helper.rb` or `spec/spec_helper.rb` file.
|
|
|
231
258
|
|
|
232
259
|
Shows on-screen-display notifications for test failures.
|
|
233
260
|
|
|
261
|
+
### require 'test/loop/rails'
|
|
262
|
+
|
|
263
|
+
Provides support for the Ruby on Rails web framework.
|
|
264
|
+
|
|
265
|
+
### require 'test-loop/coco'
|
|
266
|
+
|
|
267
|
+
Integrates the [coco](https://github.com/lkdjiin/coco) code coverage library.
|
|
268
|
+
|
|
234
269
|
------------------------------------------------------------------------------
|
|
235
270
|
Known issues
|
|
236
271
|
------------------------------------------------------------------------------
|
|
237
272
|
|
|
238
273
|
### Ruby on Rails
|
|
239
274
|
|
|
240
|
-
* Ensure that your `config/environments/test.rb` file disables class caching
|
|
241
|
-
|
|
242
|
-
|
|
275
|
+
* Ensure that your `config/environments/test.rb` file disables class caching
|
|
276
|
+
as follows (**NOTE:** if you are using Rails 3, the `test/loop/rails`
|
|
277
|
+
preset will automatically do this for you):
|
|
243
278
|
|
|
244
|
-
|
|
279
|
+
config.cache_classes = false
|
|
245
280
|
|
|
246
|
-
|
|
247
|
-
|
|
281
|
+
Otherwise, test-loop will appear to ignore source-code changes in your
|
|
282
|
+
models, controllers, helpers, and other Ruby source files.
|
|
248
283
|
|
|
249
|
-
* SQLite3 [raises `SQLite3::BusyException: database is locked` errors](
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
* SQLite3 [raises `SQLite3::BusyException: database is locked` errors](
|
|
285
|
+
https://github.com/sunaku/test-loop/issues/2 ) because test-loop runs your
|
|
286
|
+
test files in parallel. You can work around this by using an [in-memory
|
|
287
|
+
adapter for SQLite3]( https://github.com/mvz/memory_test_fix ) or by using
|
|
288
|
+
different database software (such as MySQL) for your test environment.
|
|
254
289
|
|
|
255
290
|
------------------------------------------------------------------------------
|
|
256
291
|
License
|
data/bin/test-loop
CHANGED
data/lib/test/loop/notify.rb
CHANGED
|
@@ -4,7 +4,8 @@ Test::Loop.after_each_test.push lambda {
|
|
|
4
4
|
|test_file, log_file, run_status, started_at, elapsed_time|
|
|
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
|
+
statistics = File.readlines(log_file).grep(/^\d+ \w+,/)
|
|
8
|
+
message = test_file + "\n" + statistics.join
|
|
8
9
|
Thread.new do # run in background
|
|
9
10
|
system 'notify-send', '-i', 'dialog-error', title, message or
|
|
10
11
|
system 'growlnotify', '-a', 'Xcode', '-m', message, title or
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'test/loop'
|
|
2
|
+
|
|
3
|
+
Test::Loop.reabsorb_file_globs.push(
|
|
4
|
+
'config/**/*.{rb,yml}',
|
|
5
|
+
'db/schema.rb',
|
|
6
|
+
'Gemfile.lock'
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
Test::Loop.test_file_matchers['{app,lib,test,spec}/**/*.rb'] =
|
|
10
|
+
Test::Loop.test_file_matchers.delete('lib/**/*.rb')
|
|
11
|
+
|
|
12
|
+
require 'rails/railtie'
|
|
13
|
+
Class.new Rails::Railtie do
|
|
14
|
+
config.before_initialize do |app|
|
|
15
|
+
if app.config.cache_classes
|
|
16
|
+
warn "test-loop: Setting #{app.class}.config.cache_classes = false"
|
|
17
|
+
app.config.cache_classes = false
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/test/loop.rb
CHANGED
|
@@ -4,6 +4,8 @@ require 'diff/lcs'
|
|
|
4
4
|
module Test
|
|
5
5
|
Loop = OpenStruct.new
|
|
6
6
|
|
|
7
|
+
Loop.delay_per_iteration = 1
|
|
8
|
+
|
|
7
9
|
Loop.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
|
|
8
10
|
|
|
9
11
|
Loop.reabsorb_file_globs = Loop.overhead_file_globs.dup
|
|
@@ -49,19 +51,15 @@ module Test
|
|
|
49
51
|
|
|
50
52
|
class << Loop
|
|
51
53
|
def run
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
@lines_by_file = {} # path => readlines
|
|
55
|
-
@last_ran_at = @started_at = Time.now
|
|
56
|
-
|
|
57
|
-
register_signals
|
|
54
|
+
init_shared_vars
|
|
55
|
+
trap_user_signals
|
|
58
56
|
load_user_config
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
load_user_overhead
|
|
58
|
+
init_worker_queue
|
|
59
|
+
enter_testing_loop
|
|
62
60
|
|
|
63
61
|
rescue Interrupt
|
|
64
|
-
# allow user to break the loop
|
|
62
|
+
# allow user to break the loop
|
|
65
63
|
|
|
66
64
|
rescue Exception => error
|
|
67
65
|
STDERR.puts error.inspect, error.backtrace
|
|
@@ -69,19 +67,23 @@ module Test
|
|
|
69
67
|
reload_master_process
|
|
70
68
|
|
|
71
69
|
ensure
|
|
72
|
-
|
|
70
|
+
stop_worker_queue
|
|
73
71
|
notify 'Goodbye!'
|
|
74
72
|
end
|
|
75
73
|
|
|
76
74
|
private
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
MASTER_EXECV = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
|
|
77
|
+
MASTER_ENV = Hash[ENV.map {|k,v| [k.freeze, v.freeze] }].freeze
|
|
79
78
|
RESUME_ENV_KEY = 'TEST_LOOP_RESUME_FILES'.freeze
|
|
80
79
|
|
|
81
80
|
ANSI_CLEAR_LINE = "\e[2K\e[0G".freeze
|
|
82
81
|
ANSI_GREEN = "\e[32m%s\e[0m".freeze
|
|
83
82
|
ANSI_RED = "\e[31m%s\e[0m".freeze
|
|
84
83
|
|
|
84
|
+
Worker = Struct.new(:test_file, :log_file, :started_at, :finished_at,
|
|
85
|
+
:exit_status)
|
|
86
|
+
|
|
85
87
|
def notify message
|
|
86
88
|
# using print() because puts() is not an atomic operation.
|
|
87
89
|
# also, clear the line before printing because some shells emit
|
|
@@ -89,49 +91,48 @@ module Test
|
|
|
89
91
|
print "#{ANSI_CLEAR_LINE}test-loop: #{message}\n"
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
trap :TERM, :IGNORE
|
|
96
|
-
|
|
97
|
-
master_pid = $$
|
|
98
|
-
master_trap = lambda do |signal, &handler|
|
|
99
|
-
trap signal do
|
|
100
|
-
if $$ == master_pid
|
|
101
|
-
handler.call
|
|
102
|
-
else
|
|
103
|
-
# ignore future ocurrences of this signal in worker processes
|
|
104
|
-
trap signal, :IGNORE
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
94
|
+
def pause_momentarily
|
|
95
|
+
sleep delay_per_iteration
|
|
96
|
+
end
|
|
108
97
|
|
|
109
|
-
|
|
110
|
-
|
|
98
|
+
def init_shared_vars
|
|
99
|
+
@lines_by_file = {} # path => readlines
|
|
100
|
+
@last_ran_at = @started_at = Time.now
|
|
101
|
+
@worker_by_pid = {}
|
|
111
102
|
end
|
|
112
103
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
def trap_user_signals
|
|
105
|
+
trap(:INT) { raise Interrupt }
|
|
106
|
+
trap(:TSTP) { forcibly_run_all_tests }
|
|
107
|
+
trap(:QUIT) { reload_master_process }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def forcibly_run_all_tests
|
|
111
|
+
notify 'Running all tests...'
|
|
112
|
+
@last_ran_at = Time.at(0)
|
|
113
|
+
@lines_by_file.clear
|
|
117
114
|
end
|
|
118
115
|
|
|
119
116
|
# The given test files are passed down (along with currently running
|
|
120
117
|
# test files) to the next incarnation of test-loop for resumption.
|
|
121
118
|
def reload_master_process test_files = []
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
test_files.concat currently_running_test_files
|
|
120
|
+
stop_worker_queue
|
|
121
|
+
ENV.replace MASTER_ENV.merge(RESUME_ENV_KEY => test_files.inspect)
|
|
122
|
+
exec(*MASTER_EXECV)
|
|
125
123
|
end
|
|
126
124
|
|
|
127
125
|
def load_user_config
|
|
128
126
|
if File.exist? config_file = File.join(Dir.pwd, '.test-loop')
|
|
129
127
|
notify 'Loading configuration...'
|
|
130
128
|
load config_file
|
|
129
|
+
|
|
130
|
+
# ...and if the configuration file changes, reload everything.
|
|
131
|
+
reabsorb_file_globs.push config_file
|
|
131
132
|
end
|
|
132
133
|
end
|
|
133
134
|
|
|
134
|
-
def
|
|
135
|
+
def load_user_overhead
|
|
135
136
|
notify 'Absorbing overhead...'
|
|
136
137
|
$LOAD_PATH.unshift 'lib', 'test', 'spec'
|
|
137
138
|
Dir[*overhead_file_globs].each do |file|
|
|
@@ -139,23 +140,11 @@ module Test
|
|
|
139
140
|
end
|
|
140
141
|
end
|
|
141
142
|
|
|
142
|
-
def
|
|
143
|
-
Dir[File.expand_path '../loop/support/*.rb', __FILE__].each {|f| load f }
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def pause_momentarily
|
|
147
|
-
sleep 1
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def forcibly_run_all_tests
|
|
151
|
-
notify 'Running all tests...'
|
|
152
|
-
@last_ran_at = Time.at(0)
|
|
153
|
-
@lines_by_file.clear
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def run_test_loop
|
|
143
|
+
def enter_testing_loop
|
|
157
144
|
notify 'Ready for testing!'
|
|
158
145
|
loop do
|
|
146
|
+
reap_worker_queue
|
|
147
|
+
|
|
159
148
|
# find test files that have been modified since the last run
|
|
160
149
|
test_files = test_file_matchers.map do |source_glob, test_matcher|
|
|
161
150
|
Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
|
|
@@ -179,38 +168,85 @@ module Test
|
|
|
179
168
|
|
|
180
169
|
# fork workers to run the test files in parallel,
|
|
181
170
|
# excluding test files that are already running
|
|
182
|
-
test_files
|
|
183
|
-
synchronize { test_files - @running_files }
|
|
184
|
-
|
|
171
|
+
test_files -= currently_running_test_files
|
|
185
172
|
unless test_files.empty?
|
|
186
173
|
@last_ran_at = Time.now
|
|
187
|
-
test_files.each {|file|
|
|
174
|
+
test_files.each {|file| fork_worker Worker.new(file) }
|
|
188
175
|
end
|
|
189
176
|
|
|
190
177
|
pause_momentarily
|
|
191
178
|
end
|
|
192
179
|
end
|
|
193
180
|
|
|
194
|
-
def
|
|
195
|
-
@
|
|
196
|
-
|
|
181
|
+
def currently_running_test_files
|
|
182
|
+
@worker_by_pid.values.map(&:test_file)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def init_worker_queue
|
|
186
|
+
# collect children (of which some may be workers) for reaping below
|
|
187
|
+
@exited_child_infos = []
|
|
188
|
+
trap :CHLD do
|
|
189
|
+
finished_at = Time.now
|
|
190
|
+
begin
|
|
191
|
+
while wait2_array = Process.wait2(-1, Process::WNOHANG)
|
|
192
|
+
@exited_child_infos.push [wait2_array, finished_at]
|
|
193
|
+
end
|
|
194
|
+
rescue SystemCallError
|
|
195
|
+
# raised by wait() when there are no child processes at all
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# reap finished workers from previous iterations of the loop
|
|
201
|
+
def reap_worker_queue
|
|
202
|
+
while info = @exited_child_infos.shift
|
|
203
|
+
(child_pid, exit_status), finished_at = info
|
|
204
|
+
if worker = @worker_by_pid.delete(child_pid)
|
|
205
|
+
worker.exit_status = exit_status
|
|
206
|
+
worker.finished_at = finished_at
|
|
207
|
+
reap_worker worker
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def stop_worker_queue
|
|
213
|
+
notify 'Stopping tests...'
|
|
214
|
+
trap :CHLD, 'DEFAULT'
|
|
215
|
+
@worker_by_pid.each_key do |worker_pid|
|
|
216
|
+
begin
|
|
217
|
+
Process.kill :TERM, -worker_pid
|
|
218
|
+
rescue SystemCallError
|
|
219
|
+
# worker is already terminated
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
Process.waitall
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def fork_worker worker
|
|
226
|
+
notify "TEST #{worker.test_file}"
|
|
227
|
+
|
|
228
|
+
worker.log_file = worker.test_file + '.log'
|
|
197
229
|
|
|
198
230
|
# cache the contents of the test file for diffing below
|
|
199
|
-
new_lines = File.readlines(test_file)
|
|
200
|
-
old_lines = @lines_by_file[test_file] || new_lines
|
|
201
|
-
@lines_by_file[test_file] = new_lines
|
|
231
|
+
new_lines = File.readlines(worker.test_file)
|
|
232
|
+
old_lines = @lines_by_file[worker.test_file] || new_lines
|
|
233
|
+
@lines_by_file[worker.test_file] = new_lines
|
|
234
|
+
|
|
235
|
+
worker.started_at = Time.now
|
|
236
|
+
pid = fork do
|
|
237
|
+
# detach worker from master's terminal device so that
|
|
238
|
+
# it does not receieve the user's control-key presses
|
|
239
|
+
Process.setsid
|
|
202
240
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# workers can be killed by sending it to the entire process group
|
|
206
|
-
trap :TERM, :DEFAULT
|
|
241
|
+
# unregister signal handlers inherited from master process
|
|
242
|
+
[:INT, :TSTP, :QUIT].each {|sig| trap sig, 'DEFAULT' }
|
|
207
243
|
|
|
208
|
-
#
|
|
209
|
-
|
|
244
|
+
# detach worker from master's standard input stream
|
|
245
|
+
STDIN.reopen IO.pipe.first
|
|
210
246
|
|
|
211
247
|
# capture test output in log file because tests are run in parallel
|
|
212
248
|
# which makes it difficult to understand interleaved output thereof
|
|
213
|
-
|
|
249
|
+
STDERR.reopen(STDOUT.reopen(worker.log_file, 'w')).sync = true
|
|
214
250
|
|
|
215
251
|
# determine which test blocks have changed inside the test file
|
|
216
252
|
test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
|
|
@@ -226,39 +262,34 @@ module Test
|
|
|
226
262
|
end.compact.uniq
|
|
227
263
|
|
|
228
264
|
# tell the testing framework to run only the changed test blocks
|
|
229
|
-
before_each_test.each
|
|
265
|
+
before_each_test.each do |hook|
|
|
266
|
+
hook.call worker.test_file, worker.log_file, test_names
|
|
267
|
+
end
|
|
230
268
|
|
|
231
269
|
# make the process title Test::Unit friendly and ps(1) searchable
|
|
232
|
-
$0 = "test-loop #{test_file}"
|
|
270
|
+
$0 = "test-loop #{worker.test_file}"
|
|
233
271
|
|
|
234
272
|
# after loading the user's test file, the at_exit() hook of the
|
|
235
273
|
# user's testing framework will take care of running the tests and
|
|
236
274
|
# reflecting any failures in the worker process' exit status
|
|
237
|
-
load test_file
|
|
275
|
+
load worker.test_file
|
|
238
276
|
end
|
|
239
277
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
notify "TEST #{test_file}"
|
|
243
|
-
|
|
244
|
-
# wait for worker to finish
|
|
245
|
-
Process.waitpid worker_pid
|
|
246
|
-
run_status = $?
|
|
247
|
-
elapsed_time = Time.now - @last_ran_at
|
|
248
|
-
|
|
249
|
-
# report test results along with any failure logs
|
|
250
|
-
if run_status.success?
|
|
251
|
-
notify ANSI_GREEN % "PASS #{test_file}"
|
|
252
|
-
elsif run_status.exited?
|
|
253
|
-
notify ANSI_RED % "FAIL #{test_file}"
|
|
254
|
-
STDERR.print File.read(log_file)
|
|
255
|
-
end
|
|
278
|
+
@worker_by_pid[pid] = worker
|
|
279
|
+
end
|
|
256
280
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
281
|
+
def reap_worker worker
|
|
282
|
+
# report test results along with any failure logs
|
|
283
|
+
if worker.exit_status.success?
|
|
284
|
+
notify ANSI_GREEN % "PASS #{worker.test_file}"
|
|
285
|
+
elsif worker.exit_status.exited?
|
|
286
|
+
notify ANSI_RED % "FAIL #{worker.test_file}"
|
|
287
|
+
STDERR.print File.read(worker.log_file)
|
|
288
|
+
end
|
|
260
289
|
|
|
261
|
-
|
|
290
|
+
after_each_test.each do |hook|
|
|
291
|
+
hook.call worker.test_file, worker.log_file, worker.exit_status,
|
|
292
|
+
worker.started_at, worker.finished_at - worker.started_at
|
|
262
293
|
end
|
|
263
294
|
end
|
|
264
295
|
end
|
metadata
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
name: test-loop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease:
|
|
5
|
-
version:
|
|
5
|
+
version: 12.2.0
|
|
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-
|
|
15
|
-
default_executable:
|
|
15
|
+
date: 2011-06-01 00:00:00 Z
|
|
16
16
|
dependencies:
|
|
17
17
|
- !ruby/object:Gem::Dependency
|
|
18
18
|
name: diff-lcs
|
|
@@ -34,14 +34,12 @@ extensions: []
|
|
|
34
34
|
extra_rdoc_files: []
|
|
35
35
|
|
|
36
36
|
files:
|
|
37
|
+
- LICENSE
|
|
37
38
|
- README.md
|
|
38
39
|
- bin/test-loop
|
|
39
40
|
- lib/test/loop.rb
|
|
40
41
|
- lib/test/loop/notify.rb
|
|
41
|
-
- lib/test/loop/
|
|
42
|
-
- lib/test/loop/support/minitest.rb
|
|
43
|
-
- lib/test/loop/support/testunit.rb
|
|
44
|
-
has_rdoc: true
|
|
42
|
+
- lib/test/loop/rails.rb
|
|
45
43
|
homepage: http://github.com/sunaku/test-loop
|
|
46
44
|
licenses: []
|
|
47
45
|
|
|
@@ -65,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
65
63
|
requirements: []
|
|
66
64
|
|
|
67
65
|
rubyforge_project:
|
|
68
|
-
rubygems_version: 1.
|
|
66
|
+
rubygems_version: 1.8.5
|
|
69
67
|
signing_key:
|
|
70
68
|
specification_version: 3
|
|
71
69
|
summary: Continuous testing for Ruby with fork/eval
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
if defined? Rails::VERSION
|
|
2
|
-
Test::Loop.reabsorb_file_globs.push(
|
|
3
|
-
'config/**/*.{rb,yml}',
|
|
4
|
-
'test/factories/*.rb',
|
|
5
|
-
'Gemfile.lock'
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
Test::Loop.test_file_matchers['app/**/*.rb'] =
|
|
9
|
-
Test::Loop.test_file_matchers['lib/**/*.rb']
|
|
10
|
-
|
|
11
|
-
class << Test::Loop
|
|
12
|
-
class Railtie < Rails::Railtie
|
|
13
|
-
config.before_initialize do |app|
|
|
14
|
-
if app.config.cache_classes
|
|
15
|
-
warn "test-loop: Setting #{app.class}.config.cache_classes = false"
|
|
16
|
-
app.config.cache_classes = false
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|