test-loop 7.0.1 → 8.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.
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