test-loop 7.0.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.md +61 -52
  2. data/bin/test-loop +165 -170
  3. metadata +4 -4
data/README.md CHANGED
@@ -8,11 +8,6 @@ detects and tests changes in your application in an efficient manner:
8
8
  2. Forks to run your test files without overhead and in parallel.
9
9
  3. Avoids running unchanged test blocks inside changed test files.
10
10
 
11
- It relies on file modification times to determine what parts of your Ruby
12
- application have changed, applies a lambda mapping function to determine which
13
- test files in your test suite correspond to those changes, and uses diffing to
14
- find and run only those test blocks that have changed inside your test files.
15
-
16
11
 
17
12
  > IMPORTANT note for Ruby on Rails users!
18
13
  > ---------------------------------------
@@ -22,7 +17,7 @@ find and run only those test blocks that have changed inside your test files.
22
17
  > config.cache_classes = false
23
18
  >
24
19
  > Otherwise, test-loop will appear to ignore class-level changes in your
25
- > models, controllers, etc. and will thereby cause you great frustration!
20
+ > models, controllers, helpers, etc. thereby causing you great frustration!
26
21
 
27
22
 
28
23
  Features
@@ -92,67 +87,81 @@ Configuration
92
87
  -------------
93
88
 
94
89
  test-loop looks for a configuration file named `.test-loop` in the current
95
- working directory. This configuration file is a Ruby script in which you
96
- can query and modify the `Test::Loop::Config` OpenStruct object as follows:
97
-
98
- * `Test::Loop::Config.overhead_file_globs` is an array of file globbing
99
- patterns that describe a set of Ruby scripts that are loaded into the main
100
- Ruby process as overhead.
101
-
102
- * `Test::Loop::Config.reabsorb_file_globs` is an array of file globbing
103
- patterns that describe a set of files which cause the overhead to be
104
- reabsorbed whenever they change.
105
-
106
- * `Test::Loop::Config.test_file_matchers` is a hash that maps a file globbing
107
- pattern describing a set of source files to a lambda function yielding a
108
- file globbing pattern describing a set of test files that need to be run.
109
- In other words, whenever the source files (the hash key; left-hand side of
110
- the mapping) change, their associated test files (the hash value; right-hand
90
+ working directory. This configuration file is a normal Ruby script in which
91
+ you can query and modify the `Test::Loop` OpenStruct configuration as follows:
92
+
93
+ * `Test::Loop.overhead_file_globs` is an array of file globbing patterns that
94
+ describe a set of Ruby scripts that are loaded into the main Ruby process as
95
+ overhead.
96
+
97
+ * `Test::Loop.reabsorb_file_globs` is an array of file globbing patterns that
98
+ describe a set of files which cause the overhead to be reabsorbed whenever
99
+ they change.
100
+
101
+ * `Test::Loop.test_file_matchers` is a hash that maps a file globbing pattern
102
+ describing a set of source files to a lambda function yielding a file
103
+ globbing pattern describing a set of test files that need to be run. In
104
+ other words, whenever the source files (the hash key; left-hand side of the
105
+ mapping) change, their associated test files (the hash value; right-hand
111
106
  side of the mapping) are run.
112
107
 
113
- For example, if test files had the same names as their source files but the
114
- letters were in reverse order like this:
108
+ For example, if test files had the same names as their source files followed
109
+ by an underscore and the file name in reverse like this:
115
110
 
116
- * lib/hello.rb => test/olleh.rb
117
- * app/world.rb => spec/ldrow.rb
111
+ * lib/hello.rb => test/hello_olleh.rb
112
+ * app/world.rb => spec/world_ldrow.rb
118
113
 
119
114
  Then you would add the following to your configuration file:
120
115
 
121
- Test::Loop::Config.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
116
+ Test::Loop.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
122
117
  extn = File.extname(path)
123
118
  name = File.basename(path, extn)
124
- "{test,spec}/**/#{name.reverse}#{extn}" # <== notice the reverse()
119
+ "{test,spec}/**/#{name}_#{name.reverse}#{extn}"
120
+ end
121
+
122
+ * `Test::Loop.test_name_parser` is a lambda function that is passed a line of
123
+ source code to determine whether that line can be considered as a test
124
+ definition, in which case it must return the name of the test being defined.
125
+
126
+ * `Test::Loop.before_each_test` is a lambda function that is executed inside
127
+ the worker process before loading the test file. It is passed (1) the path
128
+ to the test file, (2) the path to the log file containing the live output of
129
+ running the test file, and (3) an array containing the names of tests (which
130
+ were identified by `Test::Loop.test_name_parser`) inside the test file that
131
+ have changed since the last run of the test file.
132
+
133
+ The default implementation of this function instructs Test::Unit and RSpec
134
+ to only run certain test blocks inside the test file. This accelerates your
135
+ test-driven development cycle and improves productivity!
136
+
137
+ If you wish to add extend the default implementation, store and recall it:
138
+
139
+ default_implementation = Test::Loop.before_each_test
140
+
141
+ Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
142
+ default_implementation.call test_file, log_file, test_names
143
+ # do something additional ...
144
+ end
145
+
146
+ Or if you want to completely replace the default implementation:
147
+
148
+ Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
149
+ # your replacement here ...
125
150
  end
126
151
 
127
- * `Test::Loop::Config.test_name_parser` is a lambda function that is passed a
128
- line of source code to determine whether that line can be considered as a
129
- test definition, in which case it must return the name of the test being
130
- defined.
131
-
132
- * `Test::Loop::Config.before_each_test` is a lambda function that is executed
133
- inside the worker process before loading the test file. It is passed (1)
134
- the path to the test file, (2) the path to the log file containing the live
135
- output of running the test file, and (3) an array containing the names of
136
- tests (which were identified by `Test::Loop::Config.test_name_parser`)
137
- inside the test file that have changed since the last run of the test file.
138
-
139
- These test names should be passed down to your chosen testing library,
140
- instructing it to skip all other tests except those passed down to it. This
141
- accelerates your test-driven development cycle and improves productivity!
142
-
143
- * `Test::Loop::Config.after_each_test` is a lambda function that is executed
144
- inside the master process after a test has finished running. It is passed
145
- (1) the path to the test file, (2) the path to the log file containing the
146
- output of running the test file, (3) a `Process::Status` object describing
147
- the exit status of the worker process that ran the test file, (4) the time
148
- when test execution began, and (5) how many seconds it took for the overall
149
- test execution to complete.
152
+ * `Test::Loop.after_each_test` is a lambda function that is executed inside
153
+ the master process after a test has finished running. It is passed (1) the
154
+ path to the test file, (2) the path to the log file containing the output of
155
+ running the test file, (3) a `Process::Status` object describing the exit
156
+ status of the worker process that ran the test file, (4) the time when test
157
+ execution began, and (5) how many seconds it took for the overall test
158
+ execution to complete.
150
159
 
151
160
  For example, to display a summary of the test execution results as an
152
161
  on-screen-display notification while also displaying the log file if the
153
162
  test failed, add the following to your configuration file:
154
163
 
155
- Test::Loop::Config.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
164
+ Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
156
165
  success = run_status.success?
157
166
 
158
167
  title = '%s at %s in %0.1fs' %
data/bin/test-loop CHANGED
@@ -26,207 +26,202 @@ require 'ostruct'
26
26
  require 'diff/lcs'
27
27
 
28
28
  module Test
29
- module Loop
30
- CONFIG_FILE = File.join(Dir.pwd, '.test-loop')
31
-
32
- Config = OpenStruct.new(
33
- :overhead_file_globs => ['{test,spec}/{test,spec}_helper.rb'],
34
-
35
- :reabsorb_file_globs => ['{test,spec}/{test,spec}_helper.rb',
36
- 'config/*.{rb,yml}', 'Gemfile.lock',
37
- File.basename(CONFIG_FILE)],
38
-
39
- :test_file_matchers => {
40
- # source files that correspond to test files
41
- '{lib,app}/**/*.rb' => lambda do |path|
42
- extn = File.extname(path)
43
- name = File.basename(path, extn)
44
- "{test,spec}/**/#{name}_{test,spec}#{extn}"
45
- end,
46
-
47
- # the actual test files themselves
48
- '{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
49
- },
50
-
51
- :test_name_parser => lambda do |line|
52
- case line
53
- when /^\s*def\s+test_(\w+)/ then $1
54
- when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
55
- end
56
- end,
57
-
58
- :before_each_test => lambda do |test_file, log_file, test_names|
59
- unless test_names.empty?
60
- test_name_pattern = test_names.map do |name|
61
- # sanitize string interpolation and non-method-name characters
62
- name.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
63
- end.join('|')
64
-
65
- case File.basename(test_file)
66
- when /(\b|_)test(\b|_)/ # Test::Unit
67
- ARGV.push '--name', "/#{test_name_pattern}/"
68
- when /(\b|_)spec(\b|_)/ # RSpec
69
- ARGV.push '--example', test_name_pattern
70
- end
71
- end
72
- end,
29
+ Loop = OpenStruct.new
30
+
31
+ Loop.config_file_path = File.join(Dir.pwd, '.test-loop')
32
+
33
+ Loop.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
34
+
35
+ Loop.reabsorb_file_globs = Loop.overhead_file_globs +
36
+ ['config/*.{rb,yml}', 'Gemfile.lock', File.basename(Loop.config_file_path)]
73
37
 
74
- :after_each_test =>
75
- lambda {|test_file, log_file, run_status, started_at, elapsed_time|}
76
- )
38
+ Loop.test_file_matchers = {
39
+ # source files that correspond to test files
40
+ '{lib,app}/**/*.rb' => lambda do |path|
41
+ extn = File.extname(path)
42
+ name = File.basename(path, extn)
43
+ "{test,spec}/**/#{name}_{test,spec}#{extn}"
44
+ end,
77
45
 
78
- def self.run
46
+ # the actual test files themselves
47
+ '{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
48
+ }
49
+
50
+ Loop.test_name_parser = lambda do |line|
51
+ case line
52
+ when /^\s*def\s+test_(\w+)/ then $1
53
+ when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
54
+ end
55
+ end
56
+
57
+ Loop.before_each_test = lambda do |test_file, log_file, test_names|
58
+ unless test_names.empty?
59
+ test_name_pattern = test_names.map do |name|
60
+ # sanitize string interpolation and non-method-name characters
61
+ name.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
62
+ end.join('|')
63
+
64
+ case File.basename(test_file)
65
+ when /(\b|_)test(\b|_)/ # Test::Unit
66
+ ARGV.push '--name', "/#{test_name_pattern}/"
67
+ when /(\b|_)spec(\b|_)/ # RSpec
68
+ ARGV.push '--example', test_name_pattern
69
+ end
70
+ end
71
+ end
72
+
73
+ Loop.after_each_test =
74
+ lambda {|test_file, log_file, run_status, started_at, elapsed_time|}
75
+
76
+ class << Loop
77
+ def run
79
78
  register_signals
80
79
  load_user_config
81
80
  absorb_overhead
82
81
  run_test_loop
83
82
  rescue Exception => error
84
83
  STDERR.puts error.inspect, error.backtrace
85
- sleep 1
84
+ pause_momentarily
86
85
  reload_master_process
87
86
  end
88
87
 
89
- class << self; private
90
- SCRIPT_NAME = File.basename($0)
91
- EXEC_VECTOR = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
88
+ private
92
89
 
93
- def notify message
94
- # using print() because puts() is not an atomic operation
95
- print "#{SCRIPT_NAME}: #{message}\n"
96
- end
90
+ SCRIPT_NAME = File.basename($0).freeze
91
+ EXEC_VECTOR = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
97
92
 
98
- def register_signals
99
- trap(:INT) { destroy_process_group }
100
- master_pid = $$ # kill only the workers, not the master
101
- trap(:USR1) { destroy_process_group unless $$ == master_pid }
102
- trap(:QUIT) { reload_master_process }
103
- trap(:TSTP) {} # ignore until ready for testing in run_test_loop()
104
- end
93
+ def notify message
94
+ # using print() because puts() is not an atomic operation
95
+ print "#{SCRIPT_NAME}: #{message}\n"
96
+ end
105
97
 
106
- def destroy_process_group
107
- Process.kill :KILL, -$$
108
- end
98
+ def register_signals
99
+ trap(:INT) { destroy_process_group }
100
+ master_pid = $$ # kill only the workers, not the master
101
+ trap(:USR1) { destroy_process_group unless $$ == master_pid }
102
+ trap(:QUIT) { reload_master_process }
103
+ trap(:TSTP) {} # ignore until ready for testing in run_test_loop()
104
+ end
109
105
 
110
- def reload_master_process
111
- notify 'Restarting loop...'
112
- Process.kill :USR1, -$$
113
- exec(*EXEC_VECTOR)
114
- end
106
+ def destroy_process_group
107
+ Process.kill :KILL, -$$
108
+ end
115
109
 
116
- def load_user_config
117
- if File.exist? CONFIG_FILE
118
- notify 'Loading configuration...'
119
- load CONFIG_FILE
120
- end
110
+ def reload_master_process
111
+ notify 'Restarting loop...'
112
+ Process.kill :USR1, -$$
113
+ exec(*EXEC_VECTOR)
114
+ end
115
+
116
+ def pause_momentarily
117
+ sleep 1
118
+ end
119
+
120
+ def load_user_config
121
+ if File.exist? config_file_path
122
+ notify 'Loading configuration...'
123
+ load config_file_path
121
124
  end
125
+ end
122
126
 
123
- def absorb_overhead
124
- notify 'Absorbing overhead...'
125
- $LOAD_PATH.unshift 'lib', 'test', 'spec'
126
- Dir[*Config.overhead_file_globs].each do |file|
127
- require File.basename(file, File.extname(file))
128
- end
127
+ def absorb_overhead
128
+ notify 'Absorbing overhead...'
129
+ $LOAD_PATH.unshift 'lib', 'test', 'spec'
130
+ Dir[*overhead_file_globs].each do |file|
131
+ require File.basename(file, File.extname(file))
129
132
  end
133
+ end
130
134
 
131
- def run_test_loop
132
- @running_files = []
133
- @running_files_lock = Mutex.new
134
-
135
- @lines_by_file = {} # path => readlines
136
- @last_ran_at = @started_at = Time.now
137
- trap(:TSTP) { @last_ran_at = Time.at(0); @lines_by_file.clear }
138
-
139
- notify 'Ready for testing!'
140
- loop do
141
- # figure out what test files need to be run
142
- test_files = Config.test_file_matchers.map \
143
- do |source_glob, test_matcher|
144
- Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
145
- map {|path| Dir[test_matcher.call path] }
146
- end.flatten.uniq
147
-
148
- test_files = @running_files_lock.
149
- synchronize { test_files - @running_files }
150
-
151
- # fork worker processes to run the test files in parallel
152
- @last_ran_at = Time.now
153
- test_files.each {|f| run_test_file f }
154
-
155
- # reabsorb test execution overhead as necessary
156
- if Dir[*Config.reabsorb_file_globs].
157
- any? {|file| File.mtime(file) > @started_at }
158
- then
159
- reload_master_process
160
- end
135
+ def run_test_loop
136
+ @running_files = []
137
+ @running_files_lock = Mutex.new
161
138
 
162
- sleep 1
163
- end
164
- end
139
+ @lines_by_file = {} # path => readlines
140
+ @last_ran_at = @started_at = Time.now
141
+ trap(:TSTP) { @last_ran_at = Time.at(0); @lines_by_file.clear }
165
142
 
166
- def run_test_file test_file
167
- @running_files_lock.synchronize { @running_files.push test_file }
168
- log_file = test_file + '.log'
169
-
170
- # cache the contents of the test file for diffing below
171
- new_lines = File.readlines(test_file)
172
- old_lines = @lines_by_file[test_file] || new_lines
173
- @lines_by_file[test_file] = new_lines
174
-
175
- worker_pid = fork do
176
- # capture test output in log file because tests are run in parallel
177
- # which makes it difficult to understand interleaved output thereof
178
- $stdout.reopen log_file, 'w'
179
- $stdout.sync = true
180
- $stderr.reopen $stdout
181
-
182
- # determine which test blocks have changed inside the test file
183
- test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map \
184
- do |change|
185
- catch :found do
186
- # search backwards from the line that changed up to
187
- # the first line in the file for test definitions
188
- change.position.downto(0) do |i|
189
- if test_name = Config.test_name_parser.call(new_lines[i])
190
- throw :found, test_name
191
- end
192
- end; nil # prevent unsuccessful search from returning an integer
193
- end
194
- end.compact.uniq
195
-
196
- # tell the testing framework to run only the changed test blocks
197
- Config.before_each_test.call test_file, log_file, test_names
198
-
199
- # after loading the user's test file, the at_exit() hook of the
200
- # user's testing framework will take care of running the tests and
201
- # reflecting any failures in the worker process' exit status
202
- load $0 = test_file # set $0 because Test::Unit outputs it
203
- end
143
+ notify 'Ready for testing!'
144
+ loop do
145
+ # figure out what test files need to be run
146
+ test_files = test_file_matchers.map do |source_glob, test_matcher|
147
+ Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
148
+ map {|path| Dir[test_matcher.call path] }
149
+ end.flatten.uniq
204
150
 
205
- # monitor and report on the worker's progress
206
- Thread.new do
207
- report = lambda do |state|
208
- notify [state, worker_pid, test_file].join("\t")
209
- end
210
- report.call :RUN
151
+ test_files = @running_files_lock.
152
+ synchronize { test_files - @running_files }
153
+
154
+ # fork worker processes to run the test files in parallel
155
+ @last_ran_at = Time.now
156
+ test_files.each {|file| run_test_file file }
211
157
 
212
- # wait for worker to finish
213
- begin
214
- Process.waitpid worker_pid
215
- rescue Errno::ECHILD
216
- # worker finished and the OS has forgotten about it already
158
+ # reabsorb test execution overhead as necessary
159
+ reload_master_process if Dir[*reabsorb_file_globs].
160
+ any? {|file| File.mtime(file) > @started_at }
161
+
162
+ pause_momentarily
163
+ end
164
+ end
165
+
166
+ def run_test_file test_file
167
+ @running_files_lock.synchronize { @running_files.push test_file }
168
+ log_file = test_file + '.log'
169
+
170
+ # cache the contents of the test file for diffing below
171
+ new_lines = File.readlines(test_file)
172
+ old_lines = @lines_by_file[test_file] || new_lines
173
+ @lines_by_file[test_file] = new_lines
174
+
175
+ worker_pid = fork do
176
+ # capture test output in log file because tests are run in parallel
177
+ # which makes it difficult to understand interleaved output thereof
178
+ $stdout.reopen log_file, 'w'
179
+ $stdout.sync = true
180
+ $stderr.reopen $stdout
181
+
182
+ # determine which test blocks have changed inside the test file
183
+ test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
184
+ catch :found do
185
+ # search backwards from the line that changed up to
186
+ # the first line in the file for test definitions
187
+ change.position.downto(0) do |i|
188
+ if test_name = test_name_parser.call(new_lines[i])
189
+ throw :found, test_name
190
+ end
191
+ end; nil # prevent unsuccessful search from returning an integer
217
192
  end
218
- elapsed_time = Time.now - @last_ran_at
193
+ end.compact.uniq
219
194
 
220
- report.call $?.success? && :PASS || :FAIL
195
+ # tell the testing framework to run only the changed test blocks
196
+ before_each_test.call test_file, log_file, test_names
221
197
 
222
- Config.after_each_test.call \
223
- test_file, log_file, $?, @last_ran_at, elapsed_time
198
+ # after loading the user's test file, the at_exit() hook of the
199
+ # user's testing framework will take care of running the tests and
200
+ # reflecting any failures in the worker process' exit status
201
+ load $0 = test_file # set $0 because Test::Unit outputs it
202
+ end
224
203
 
225
- @running_files_lock.synchronize { @running_files.delete test_file }
204
+ # monitor and report on the worker's progress
205
+ Thread.new do
206
+ report = lambda do |state|
207
+ notify [state, worker_pid, test_file].join("\t")
226
208
  end
209
+ report.call :RUN
210
+
211
+ # wait for worker to finish
212
+ begin
213
+ Process.waitpid worker_pid
214
+ rescue Errno::ECHILD
215
+ # worker finished and the OS has forgotten about it already
216
+ end
217
+ elapsed_time = Time.now - @last_ran_at
218
+
219
+ report.call $?.success? && :PASS || :FAIL
220
+ after_each_test.call test_file, log_file, $?, @last_ran_at, elapsed_time
221
+ @running_files_lock.synchronize { @running_files.delete test_file }
227
222
  end
228
223
  end
229
224
  end
230
225
  end
231
226
 
232
- Test::Loop.run
227
+ Test::Loop.run if $0 == __FILE__
metadata CHANGED
@@ -3,10 +3,10 @@ name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
- - 7
6
+ - 8
7
7
  - 0
8
- - 1
9
- version: 7.0.1
8
+ - 0
9
+ version: 8.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-02-10 00:00:00 -08:00
17
+ date: 2011-02-12 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency