test-loop 5.0.3 → 6.0.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/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
|