test-loop 5.0.3 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +67 -53
- data/bin/test-loop +33 -32
- metadata +4 -4
data/README.md
CHANGED
@@ -14,6 +14,17 @@ test files in your test suite correspond to those changes, and uses diffing to
|
|
14
14
|
find and run only those test blocks that have changed inside your test files.
|
15
15
|
|
16
16
|
|
17
|
+
> IMPORTANT note for Ruby on Rails users!
|
18
|
+
> ---------------------------------------
|
19
|
+
>
|
20
|
+
> Ensure that your `config/environments/test.rb` file disables class caching:
|
21
|
+
>
|
22
|
+
> config.cache_classes = false
|
23
|
+
>
|
24
|
+
> Otherwise, test-loop will appear to ignore class-level changes in your
|
25
|
+
> models, controllers, etc. and will thereby cause you great frustration!
|
26
|
+
|
27
|
+
|
17
28
|
Features
|
18
29
|
--------
|
19
30
|
|
@@ -76,76 +87,79 @@ Configuration
|
|
76
87
|
-------------
|
77
88
|
|
78
89
|
test-loop looks for a configuration file named `.test-loop` in the current
|
79
|
-
working directory. This configuration file is a normal Ruby
|
80
|
-
|
90
|
+
working directory. This configuration file is a normal Ruby script, in which
|
91
|
+
you can query and modify the `$test_loop_config` OpenStruct object as follows:
|
92
|
+
|
93
|
+
* `$test_loop_config.overhead_file_globs` is an array of file globbing
|
94
|
+
patterns that describe a set of Ruby scripts that are loaded into the main
|
95
|
+
Ruby process as overhead.
|
96
|
+
|
97
|
+
* `$test_loop_config.reabsorb_file_globs` is an array of file globbing
|
98
|
+
patterns that describe a set of files which cause the overhead to be
|
99
|
+
reabsorbed whenever they change.
|
100
|
+
|
101
|
+
* `$test_loop_config.test_file_matchers` is a hash that maps a file globbing
|
102
|
+
pattern describing a set of source files to a lambda function yielding a
|
103
|
+
file globbing pattern describing a set of test files that need to be run.
|
104
|
+
In other words, whenever the source files (the hash key; left-hand side of
|
105
|
+
the mapping) change, their associated test files (the hash value; right-hand
|
106
|
+
side of the mapping) are run.
|
81
107
|
|
82
|
-
|
83
|
-
|
108
|
+
For example, if test files had the same names as their source files but the
|
109
|
+
letters were in reverse order like this:
|
84
110
|
|
85
|
-
*
|
86
|
-
|
111
|
+
* lib/hello.rb => test/olleh.rb
|
112
|
+
* app/world.rb => spec/ldrow.rb
|
87
113
|
|
88
|
-
|
89
|
-
describing a set of source files to a lambda function yielding a file
|
90
|
-
globbing pattern describing a set of test files that need to be run. In
|
91
|
-
other words, whenever the source files (the hash key; left-hand side of the
|
92
|
-
mapping) change, their associated test files (the hash value; right-hand
|
93
|
-
side of the mapping) are run.
|
114
|
+
Then you would add the following to your configuration file:
|
94
115
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
'{lib,app}/**/*.rb' => lambda do |path|
|
101
|
-
extn = File.extname(path)
|
102
|
-
name = File.basename(path, extn)
|
103
|
-
"{test,spec}/**/#{name.reverse}#{extn}" # <== notice the reverse()
|
104
|
-
end
|
105
|
-
}
|
116
|
+
$test_loop_config.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
|
117
|
+
extn = File.extname(path)
|
118
|
+
name = File.basename(path, extn)
|
119
|
+
"{test,spec}/**/#{name.reverse}#{extn}" # <== notice the reverse()
|
120
|
+
end
|
106
121
|
|
107
|
-
*
|
108
|
-
code to determine whether that line can be considered as a
|
109
|
-
in which case it must return the name of the test being
|
122
|
+
* `$test_loop_config.test_name_parser` is a lambda function that is passed a
|
123
|
+
line of source code to determine whether that line can be considered as a
|
124
|
+
test definition, in which case it must return the name of the test being
|
125
|
+
defined.
|
110
126
|
|
111
|
-
*
|
112
|
-
process before loading the test file. It is passed
|
113
|
-
file and the names of tests (
|
114
|
-
|
127
|
+
* `$test_loop_config.before_each_test` is a lambda function that is executed
|
128
|
+
inside the worker process before loading the test file. It is passed (1)
|
129
|
+
the path to the test file and (2) the names of tests (which were identified
|
130
|
+
by `$test_loop_config.test_name_parser`) inside the test file that have
|
131
|
+
changed since the last time the test file was run.
|
115
132
|
|
116
133
|
These test names should be passed down to your chosen testing library,
|
117
134
|
instructing it to skip all other tests except those passed down to it. This
|
118
135
|
accelerates your test-driven development cycle and improves productivity!
|
119
136
|
|
120
|
-
*
|
121
|
-
process after all tests have finished running. It is
|
122
|
-
|
123
|
-
|
137
|
+
* `$test_loop_config.after_all_tests` is a lambda function that is executed
|
138
|
+
inside the master process after all tests have finished running. It is
|
139
|
+
passed (1) a list of passing test files along with the exit statuses of the
|
140
|
+
worker processes that ran them, (2) a list of failing test files along with
|
141
|
+
the exit statuses of the worker processes that ran them, (3) the time when
|
142
|
+
test execution began, and (4) how many seconds it took for the overall test
|
143
|
+
execution to complete.
|
124
144
|
|
125
145
|
For example, to display a summary of the test execution results as an OSD
|
126
|
-
notification
|
127
|
-
configuration file:
|
128
|
-
|
129
|
-
:after_all_tests => lambda do |success, ran_at, files, statuses|
|
130
|
-
icon = success ? 'apple-green' : 'apple-red'
|
131
|
-
title = "#{success ? 'PASS' : 'FAIL'} at #{ran_at}"
|
132
|
-
details = files.zip(statuses).map do |file, status|
|
133
|
-
"#{status.success? ? '✔' : '✘'} #{file}"
|
134
|
-
end
|
135
|
-
system 'notify-send', '-i', icon, title, details.join("\n")
|
136
|
-
end
|
146
|
+
notification, add the following to your configuration file:
|
137
147
|
|
138
|
-
|
148
|
+
$test_loop_config.after_all_tests = lambda do |passes, fails, started_at, elapsed_time|
|
149
|
+
success = fails.empty?
|
139
150
|
|
140
|
-
|
151
|
+
title = started_at.strftime("#{success ? 'PASS' : 'FAIL'} at %X on %x")
|
141
152
|
|
142
|
-
|
153
|
+
message = '%d ran, %d passed, %d failed in %0.1f seconds' %
|
154
|
+
[passes.length + fails.length, passes.length, fails.length, elapsed_time]
|
143
155
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
156
|
+
Thread.new do # launch in background
|
157
|
+
system 'notify-send', '-i', "dialog-#{success ? 'information' : 'error'}", title, message or
|
158
|
+
system 'growlnotify', '-a', 'Xcode', '-m', message, title or
|
159
|
+
system 'xmessage', '-timeout', '5', '-title', title, message or
|
160
|
+
puts title, message
|
161
|
+
end
|
162
|
+
end
|
149
163
|
|
150
164
|
|
151
165
|
License
|
data/bin/test-loop
CHANGED
@@ -24,24 +24,21 @@
|
|
24
24
|
|
25
25
|
process_invocation_vector = [$0, *ARGV].map! {|s| s.dup }
|
26
26
|
|
27
|
+
require 'ostruct'
|
27
28
|
require 'diff/lcs'
|
28
29
|
|
29
30
|
begin
|
30
31
|
notify = lambda {|message| puts "test-loop: #{message}" }
|
31
32
|
|
32
|
-
#
|
33
|
-
|
34
|
-
config_file = File.join(Dir.pwd, '.test-loop')
|
35
|
-
config_data = File.read(config_file) if File.exist? config_file
|
36
|
-
config = eval(config_data.to_s, TOPLEVEL_BINDING, config_file) || {}
|
33
|
+
# supply default configuration
|
34
|
+
config = $test_loop_config = OpenStruct.new
|
37
35
|
|
38
|
-
|
39
|
-
push('{test,spec}/*{test,spec}_helper.rb').uniq!
|
36
|
+
config.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
|
40
37
|
|
41
|
-
|
42
|
-
|
38
|
+
config.reabsorb_file_globs = config.overhead_file_globs +
|
39
|
+
['config/*.{rb,yml}', 'Gemfile.lock', '.test-loop']
|
43
40
|
|
44
|
-
|
41
|
+
config.test_file_matchers = {
|
45
42
|
# source files that correspond to test files
|
46
43
|
'{lib,app}/**/*.rb' => lambda do |path|
|
47
44
|
extn = File.extname(path)
|
@@ -51,16 +48,16 @@ begin
|
|
51
48
|
|
52
49
|
# the actual test files themselves
|
53
50
|
'{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
|
54
|
-
|
51
|
+
}
|
55
52
|
|
56
|
-
config
|
53
|
+
config.test_name_parser = lambda do |line|
|
57
54
|
case line
|
58
55
|
when /^\s*def\s+test_(\w+)/ then $1
|
59
56
|
when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
|
60
57
|
end
|
61
58
|
end
|
62
59
|
|
63
|
-
config
|
60
|
+
config.before_each_test = lambda do |test_file, test_names|
|
64
61
|
unless test_names.empty?
|
65
62
|
test_name_pattern = test_names.map do |name|
|
66
63
|
# sanitize string interpolation and non-method-name characters
|
@@ -68,23 +65,27 @@ begin
|
|
68
65
|
end.join('|')
|
69
66
|
|
70
67
|
case File.basename(test_file)
|
71
|
-
|
72
68
|
when /(\b|_)test(\b|_)/ # Test::Unit
|
73
69
|
ARGV.push '--name', "/#{test_name_pattern}/"
|
74
|
-
|
75
70
|
when /(\b|_)spec(\b|_)/ # RSpec
|
76
71
|
ARGV.push '--example', test_name_pattern
|
77
72
|
end
|
78
73
|
end
|
79
74
|
end
|
80
75
|
|
81
|
-
config
|
76
|
+
config.after_all_tests = lambda {|passes, fails, started_at, elapsed_time|}
|
77
|
+
|
78
|
+
# load user's configuration overrides
|
79
|
+
if File.exist? config_file = File.join(Dir.pwd, '.test-loop')
|
80
|
+
notify.call 'Loading configuration...'
|
81
|
+
load config_file
|
82
|
+
end
|
82
83
|
|
83
84
|
# absorb test execution overhead into master process
|
84
85
|
$LOAD_PATH.unshift 'lib', 'test', 'spec'
|
85
86
|
|
86
87
|
notify.call 'Absorbing overhead...'
|
87
|
-
Dir[*config
|
88
|
+
Dir[*config.overhead_file_globs].each do |file|
|
88
89
|
require File.basename(file, File.extname(file))
|
89
90
|
end
|
90
91
|
|
@@ -97,7 +98,7 @@ begin
|
|
97
98
|
notify.call 'Ready for testing!'
|
98
99
|
loop do
|
99
100
|
# figure out what test files need to be run
|
100
|
-
test_files = config
|
101
|
+
test_files = config.test_file_matchers.map do |source_glob, test_matcher|
|
101
102
|
Dir[source_glob].select {|file| File.mtime(file) > last_ran_at }.
|
102
103
|
map {|path| Dir[test_matcher.call path] }
|
103
104
|
end.flatten.uniq
|
@@ -120,14 +121,14 @@ begin
|
|
120
121
|
# search backwards from the line that changed up to
|
121
122
|
# the first line in the file for test definitions
|
122
123
|
change.position.downto(0) do |i|
|
123
|
-
if test_name = config
|
124
|
+
if test_name = config.test_name_parser.call(new_lines[i])
|
124
125
|
throw :found, test_name
|
125
126
|
end
|
126
127
|
end; nil # prevent unsuccessful search from returning an integer
|
127
128
|
end
|
128
129
|
end.compact.uniq
|
129
130
|
|
130
|
-
config
|
131
|
+
config.before_each_test.call test_file, test_names
|
131
132
|
|
132
133
|
load test_file
|
133
134
|
|
@@ -139,20 +140,21 @@ begin
|
|
139
140
|
end
|
140
141
|
|
141
142
|
# wait for worker processes to finish and report results
|
142
|
-
|
143
|
+
test_runs = Process.waitall.map {|pid, status| status }
|
144
|
+
elapsed_time = Time.now - last_ran_at
|
143
145
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
146
|
+
passes, fails = test_files.zip(test_runs).each do |file, run|
|
147
|
+
notify.call "#{run.success? ? 'PASS' : 'FAIL'} #{file}"
|
148
|
+
end.partition {|file, run| run.success? }
|
149
|
+
|
150
|
+
notify.call '%d ran, %d passed, %d failed in %0.1f seconds' %
|
151
|
+
[test_runs.length, passes.length, fails.length, elapsed_time]
|
150
152
|
|
151
|
-
config
|
153
|
+
config.after_all_tests.call passes, fails, last_ran_at, elapsed_time
|
152
154
|
end
|
153
155
|
|
154
156
|
# reabsorb test execution overhead as necessary
|
155
|
-
if Dir[*config
|
157
|
+
if Dir[*config.reabsorb_file_globs].any? {|file| File.mtime(file) > started_at }
|
156
158
|
notify.call 'Restarting loop...'
|
157
159
|
exec(*process_invocation_vector)
|
158
160
|
end
|
@@ -161,9 +163,8 @@ begin
|
|
161
163
|
end
|
162
164
|
|
163
165
|
rescue Interrupt
|
164
|
-
# user wants to quit the loop
|
165
|
-
|
166
|
-
Process.kill :SIGKILL, -$$
|
166
|
+
# user wants to quit the loop so terminate all worker processes
|
167
|
+
Process.kill :SIGKILL, -$$ # negative PID propagates signal to children
|
167
168
|
|
168
169
|
rescue Exception => error
|
169
170
|
STDERR.puts error.inspect, error.backtrace
|
metadata
CHANGED
@@ -3,10 +3,10 @@ name: test-loop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
|
-
-
|
6
|
+
- 6
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version:
|
8
|
+
- 0
|
9
|
+
version: 6.0.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Suraj N. Kurapati
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-02-09 00:00:00 -08:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|