test-loop 9.1.0 → 9.1.1

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 (4) hide show
  1. data/README.md +18 -12
  2. data/bin/test-loop +2 -232
  3. data/lib/test/loop.rb +230 -0
  4. metadata +5 -20
data/README.md CHANGED
@@ -9,17 +9,6 @@ detects and tests changes in your application in an efficient manner:
9
9
  3. Avoids running unchanged test blocks inside changed test files.
10
10
 
11
11
 
12
- > IMPORTANT note for Ruby on Rails users!
13
- > ---------------------------------------
14
- >
15
- > Ensure that your `config/environments/test.rb` file disables class caching:
16
- >
17
- > config.cache_classes = false
18
- >
19
- > Otherwise, test-loop will appear to ignore class-level changes in your
20
- > models, controllers, helpers, etc. thereby causing you great frustration!
21
-
22
-
23
12
  Features
24
13
  --------
25
14
 
@@ -67,7 +56,7 @@ If installed as a Ruby gem:
67
56
 
68
57
  If installed as a Git clone:
69
58
 
70
- ruby bin/test-loop
59
+ env RUBYLIB=lib ruby bin/test-loop
71
60
 
72
61
 
73
62
  Operation
@@ -193,6 +182,23 @@ you can query and modify the `Test::Loop` OpenStruct configuration as follows:
193
182
  end
194
183
 
195
184
 
185
+ Known issues
186
+ ------------
187
+
188
+ * Ensure that your `config/environments/test.rb` file disables class caching:
189
+
190
+ config.cache_classes = false
191
+
192
+ Otherwise, test-loop will appear to ignore class-level changes in your
193
+ models, controllers, helpers, etc. thereby causing you great frustration!
194
+
195
+ * SQLite3 [raises `SQLite3::BusyException: database is locked` errors](
196
+ https://github.com/sunaku/test-loop/issues/2 ) because test-loop runs your
197
+ test files in parallel. You can work around this by using an [in-memory
198
+ adapter for SQLite3]( https://github.com/mvz/memory_test_fix ) or by using
199
+ different database software (such as MySQL) for your test environment.
200
+
201
+
196
202
  License
197
203
  -------
198
204
 
data/bin/test-loop CHANGED
@@ -1,233 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
- #
3
- # test-loop - Continuous testing for Ruby with fork/eval
4
- # https://github.com/sunaku/test-loop#readme
5
- #
6
- ####
7
- #
8
- # (the ISC license)
9
- #
10
- # Copyright 2010 Suraj N. Kurapati <sunaku@gmail.com>
11
- #
12
- # Permission to use, copy, modify, and/or distribute this software for any
13
- # purpose with or without fee is hereby granted, provided that the above
14
- # copyright notice and this permission notice appear in all copies.
15
- #
16
- # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
17
- # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
18
- # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
19
- # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
20
- # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
21
- # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
22
- # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23
- #
24
-
25
- require 'ostruct'
26
- require 'diff/lcs'
27
- require 'ansi'
28
-
29
- module Test
30
- Loop = OpenStruct.new
31
-
32
- Loop.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
33
-
34
- Loop.reabsorb_file_globs = Loop.overhead_file_globs +
35
- ['config/**/*.{rb,yml}', 'Gemfile.lock']
36
-
37
- Loop.test_file_matchers = {
38
- # source files that correspond to test files
39
- '{lib,app}/**/*.rb' => lambda do |path|
40
- extn = File.extname(path)
41
- base = File.basename(path, extn)
42
- "{test,spec}/**/#{base}_{test,spec}#{extn}"
43
- end,
44
-
45
- # the actual test files themselves
46
- '{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
47
- }
48
-
49
- Loop.test_name_parser = lambda do |line|
50
- case line
51
- when /^\s*def\s+test_(\w+)/ then $1
52
- when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
53
- end
54
- end
55
-
56
- Loop.before_each_test = lambda do |test_file, log_file, test_names|
57
- unless test_names.empty?
58
- test_name_pattern = test_names.map do |name|
59
- # sanitize string interpolations and invalid method name characters
60
- name.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
61
- end.join('|')
62
-
63
- case File.basename(test_file)
64
- when /(\b|_)test(\b|_)/ # Test::Unit
65
- ARGV.push '--name', "/#{test_name_pattern}/"
66
- when /(\b|_)spec(\b|_)/ # RSpec
67
- ARGV.push '--example', test_name_pattern
68
- end
69
- end
70
- end
71
-
72
- Loop.after_each_test =
73
- lambda {|test_file, log_file, run_status, started_at, elapsed_time|}
74
-
75
- class << Loop
76
- def run
77
- init_test_loop
78
- register_signals
79
- load_user_config
80
- absorb_overhead
81
- run_test_loop
82
- rescue Exception => error
83
- STDERR.puts error.inspect, error.backtrace
84
- pause_momentarily
85
- reload_master_process
86
- end
87
-
88
- private
89
-
90
- EXEC_VECTOR = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
91
-
92
- def notify message
93
- # using print() because puts() is not an atomic operation
94
- print "test-loop: #{message}\n"
95
- end
96
-
97
- def register_signals
98
- # puts newline to shield normal output from control-key interference:
99
- # some shells like BASH emit text when control-key combos are pressed
100
- trap(:INT) { puts; kill_master_and_workers }
101
- trap(:QUIT) { puts; reload_master_process }
102
- trap(:TSTP) { puts; forcibly_run_all_tests }
103
- end
104
-
105
- def kill_master_and_workers
106
- Process.kill :KILL, -$$
107
- end
108
-
109
- def reload_master_process
110
- notify 'Restarting loop...'
111
- exec(*EXEC_VECTOR)
112
- end
113
-
114
- def pause_momentarily
115
- sleep 1
116
- end
117
-
118
- def load_user_config
119
- if File.exist? config_file = File.join(Dir.pwd, '.test-loop')
120
- notify 'Loading configuration...'
121
- load config_file
122
- end
123
- end
124
-
125
- def absorb_overhead
126
- notify 'Absorbing overhead...'
127
- $LOAD_PATH.unshift 'lib', 'test', 'spec'
128
- Dir[*overhead_file_globs].each do |file|
129
- require File.basename(file, File.extname(file))
130
- end
131
- end
132
-
133
- def init_test_loop
134
- @running_files = []
135
- @running_files_lock = Mutex.new
136
- @lines_by_file = {} # path => readlines
137
- @last_ran_at = @started_at = Time.now
138
- end
139
-
140
- def forcibly_run_all_tests
141
- @last_ran_at = Time.at(0)
142
- @lines_by_file.clear
143
- end
144
-
145
- def run_test_loop
146
- notify 'Ready for testing!'
147
- loop do
148
- # figure out what test files need to be run
149
- test_files = test_file_matchers.map do |source_glob, test_matcher|
150
- Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
151
- map {|path| Dir[test_matcher.call path] }
152
- end.flatten.uniq
153
-
154
- test_files = @running_files_lock.
155
- synchronize { test_files - @running_files }
156
-
157
- # fork worker processes to run the test files in parallel
158
- @last_ran_at = Time.now
159
- test_files.each {|file| run_test_file file }
160
-
161
- # reabsorb test execution overhead as necessary
162
- reload_master_process if Dir[*reabsorb_file_globs].
163
- any? {|file| File.mtime(file) > @started_at }
164
-
165
- pause_momentarily
166
- end
167
- end
168
-
169
- def run_test_file test_file
170
- @running_files_lock.synchronize { @running_files.push test_file }
171
- log_file = test_file + '.log'
172
-
173
- # cache the contents of the test file for diffing below
174
- new_lines = File.readlines(test_file)
175
- old_lines = @lines_by_file[test_file] || new_lines
176
- @lines_by_file[test_file] = new_lines
177
-
178
- worker_pid = fork do
179
- # capture test output in log file because tests are run in parallel
180
- # which makes it difficult to understand interleaved output thereof
181
- $stdout.reopen log_file, 'w'
182
- $stdout.sync = true
183
- $stderr.reopen $stdout
184
-
185
- # determine which test blocks have changed inside the test file
186
- test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
187
- catch :found do
188
- # search backwards from the line that changed up to
189
- # the first line in the file for test definitions
190
- change.position.downto(0) do |i|
191
- if test_name = test_name_parser.call(new_lines[i])
192
- throw :found, test_name
193
- end
194
- end; nil # prevent unsuccessful search from returning an integer
195
- end
196
- end.compact.uniq
197
-
198
- # tell the testing framework to run only the changed test blocks
199
- before_each_test.call test_file, log_file, test_names
200
-
201
- # after loading the user's test file, the at_exit() hook of the
202
- # user's testing framework will take care of running the tests and
203
- # reflecting any failures in the worker process' exit status
204
- load $0 = test_file # set $0 because Test::Unit outputs it
205
- end
206
-
207
- # monitor and report on the worker's progress
208
- Thread.new do
209
- notify "TEST #{test_file}"
210
-
211
- # wait for worker to finish
212
- Process.waitpid worker_pid
213
- run_status = $?
214
- elapsed_time = Time.now - @last_ran_at
215
-
216
- # report test results along with any failure logs
217
- if run_status.success?
218
- notify ANSI::Code.green("PASS #{test_file}")
219
- else
220
- notify ANSI::Code.red("FAIL #{test_file}")
221
- STDERR.print File.read(log_file)
222
- end
223
-
224
- after_each_test.call \
225
- test_file, log_file, run_status, @last_ran_at, elapsed_time
226
-
227
- @running_files_lock.synchronize { @running_files.delete test_file }
228
- end
229
- end
230
- end
231
- end
232
-
233
- Test::Loop.run if $0 == __FILE__
2
+ require 'test/loop'
3
+ Test::Loop.run
data/lib/test/loop.rb ADDED
@@ -0,0 +1,230 @@
1
+ #
2
+ # test-loop - Continuous testing for Ruby with fork/eval
3
+ # https://github.com/sunaku/test-loop#readme
4
+ #
5
+ ####
6
+ #
7
+ # (the ISC license)
8
+ #
9
+ # Copyright 2010 Suraj N. Kurapati <sunaku@gmail.com>
10
+ #
11
+ # Permission to use, copy, modify, and/or distribute this software for any
12
+ # purpose with or without fee is hereby granted, provided that the above
13
+ # copyright notice and this permission notice appear in all copies.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
16
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
18
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
21
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22
+ #
23
+
24
+ require 'ostruct'
25
+ require 'diff/lcs'
26
+ require 'ansi'
27
+
28
+ module Test
29
+ Loop = OpenStruct.new
30
+
31
+ Loop.overhead_file_globs = ['{test,spec}/{test,spec}_helper.rb']
32
+
33
+ Loop.reabsorb_file_globs = Loop.overhead_file_globs +
34
+ ['config/**/*.{rb,yml}', 'Gemfile.lock']
35
+
36
+ Loop.test_file_matchers = {
37
+ # source files that correspond to test files
38
+ '{lib,app}/**/*.rb' => lambda do |path|
39
+ extn = File.extname(path)
40
+ base = File.basename(path, extn)
41
+ "{test,spec}/**/#{base}_{test,spec}#{extn}"
42
+ end,
43
+
44
+ # the actual test files themselves
45
+ '{test,spec}/**/*_{test,spec}.rb' => lambda {|path| path }
46
+ }
47
+
48
+ Loop.test_name_parser = lambda do |line|
49
+ case line
50
+ when /^\s*def\s+test_(\w+)/ then $1
51
+ when /^\s*(test|context|should|describe|it)\b.+?(['"])(.*?)\2/ then $3
52
+ end
53
+ end
54
+
55
+ Loop.before_each_test = lambda do |test_file, log_file, test_names|
56
+ unless test_names.empty?
57
+ test_name_pattern = test_names.map do |name|
58
+ # sanitize string interpolations and invalid method name characters
59
+ name.gsub(/\#\{.*?\}/, ' ').strip.gsub(/\W+/, '.*')
60
+ end.join('|')
61
+
62
+ case File.basename(test_file)
63
+ when /(\b|_)test(\b|_)/ # Test::Unit
64
+ ARGV.push '--name', "/#{test_name_pattern}/"
65
+ when /(\b|_)spec(\b|_)/ # RSpec
66
+ ARGV.push '--example', test_name_pattern
67
+ end
68
+ end
69
+ end
70
+
71
+ Loop.after_each_test =
72
+ lambda {|test_file, log_file, run_status, started_at, elapsed_time|}
73
+
74
+ class << Loop
75
+ def run
76
+ init_test_loop
77
+ register_signals
78
+ load_user_config
79
+ absorb_overhead
80
+ run_test_loop
81
+ rescue Exception => error
82
+ STDERR.puts error.inspect, error.backtrace
83
+ pause_momentarily
84
+ reload_master_process
85
+ end
86
+
87
+ private
88
+
89
+ EXEC_VECTOR = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
90
+
91
+ def notify message
92
+ # using print() because puts() is not an atomic operation
93
+ print "test-loop: #{message}\n"
94
+ end
95
+
96
+ def register_signals
97
+ # puts newline to shield normal output from control-key interference:
98
+ # some shells like BASH emit text when control-key combos are pressed
99
+ trap(:INT) { puts; kill_master_and_workers }
100
+ trap(:QUIT) { puts; reload_master_process }
101
+ trap(:TSTP) { puts; forcibly_run_all_tests }
102
+ end
103
+
104
+ def kill_master_and_workers
105
+ Process.kill :KILL, -$$
106
+ end
107
+
108
+ def reload_master_process
109
+ notify 'Restarting loop...'
110
+ exec(*EXEC_VECTOR)
111
+ end
112
+
113
+ def pause_momentarily
114
+ sleep 1
115
+ end
116
+
117
+ def load_user_config
118
+ if File.exist? config_file = File.join(Dir.pwd, '.test-loop')
119
+ notify 'Loading configuration...'
120
+ load config_file
121
+ end
122
+ end
123
+
124
+ def absorb_overhead
125
+ notify 'Absorbing overhead...'
126
+ $LOAD_PATH.unshift 'lib', 'test', 'spec'
127
+ Dir[*overhead_file_globs].each do |file|
128
+ require File.basename(file, File.extname(file))
129
+ end
130
+ end
131
+
132
+ def init_test_loop
133
+ @running_files = []
134
+ @running_files_lock = Mutex.new
135
+ @lines_by_file = {} # path => readlines
136
+ @last_ran_at = @started_at = Time.now
137
+ end
138
+
139
+ def forcibly_run_all_tests
140
+ @last_ran_at = Time.at(0)
141
+ @lines_by_file.clear
142
+ end
143
+
144
+ def run_test_loop
145
+ notify 'Ready for testing!'
146
+ loop do
147
+ # figure out what test files need to be run
148
+ test_files = test_file_matchers.map do |source_glob, test_matcher|
149
+ Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
150
+ map {|path| Dir[test_matcher.call path] }
151
+ end.flatten.uniq
152
+
153
+ test_files = @running_files_lock.
154
+ synchronize { test_files - @running_files }
155
+
156
+ # fork worker processes to run the test files in parallel
157
+ @last_ran_at = Time.now
158
+ test_files.each {|file| run_test_file file }
159
+
160
+ # reabsorb test execution overhead as necessary
161
+ reload_master_process if Dir[*reabsorb_file_globs].
162
+ any? {|file| File.mtime(file) > @started_at }
163
+
164
+ pause_momentarily
165
+ end
166
+ end
167
+
168
+ def run_test_file test_file
169
+ @running_files_lock.synchronize { @running_files.push test_file }
170
+ log_file = test_file + '.log'
171
+
172
+ # cache the contents of the test file for diffing below
173
+ new_lines = File.readlines(test_file)
174
+ old_lines = @lines_by_file[test_file] || new_lines
175
+ @lines_by_file[test_file] = new_lines
176
+
177
+ worker_pid = fork do
178
+ # capture test output in log file because tests are run in parallel
179
+ # which makes it difficult to understand interleaved output thereof
180
+ $stdout.reopen log_file, 'w'
181
+ $stdout.sync = true
182
+ $stderr.reopen $stdout
183
+
184
+ # determine which test blocks have changed inside the test file
185
+ test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
186
+ catch :found do
187
+ # search backwards from the line that changed up to
188
+ # the first line in the file for test definitions
189
+ change.position.downto(0) do |i|
190
+ if test_name = test_name_parser.call(new_lines[i])
191
+ throw :found, test_name
192
+ end
193
+ end; nil # prevent unsuccessful search from returning an integer
194
+ end
195
+ end.compact.uniq
196
+
197
+ # tell the testing framework to run only the changed test blocks
198
+ before_each_test.call test_file, log_file, test_names
199
+
200
+ # after loading the user's test file, the at_exit() hook of the
201
+ # user's testing framework will take care of running the tests and
202
+ # reflecting any failures in the worker process' exit status
203
+ load $0 = test_file # set $0 because Test::Unit outputs it
204
+ end
205
+
206
+ # monitor and report on the worker's progress
207
+ Thread.new do
208
+ notify "TEST #{test_file}"
209
+
210
+ # wait for worker to finish
211
+ Process.waitpid worker_pid
212
+ run_status = $?
213
+ elapsed_time = Time.now - @last_ran_at
214
+
215
+ # report test results along with any failure logs
216
+ if run_status.success?
217
+ notify ANSI::Code.green("PASS #{test_file}")
218
+ else
219
+ notify ANSI::Code.red("FAIL #{test_file}")
220
+ STDERR.print File.read(log_file)
221
+ end
222
+
223
+ after_each_test.call \
224
+ test_file, log_file, run_status, @last_ran_at, elapsed_time
225
+
226
+ @running_files_lock.synchronize { @running_files.delete test_file }
227
+ end
228
+ end
229
+ end
230
+ end
metadata CHANGED
@@ -1,12 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 9
7
- - 1
8
- - 0
9
- version: 9.1.0
4
+ prerelease:
5
+ version: 9.1.1
10
6
  platform: ruby
11
7
  authors:
12
8
  - Suraj N. Kurapati
@@ -14,7 +10,7 @@ autorequire:
14
10
  bindir: bin
15
11
  cert_chain: []
16
12
 
17
- date: 2011-02-23 00:00:00 -08:00
13
+ date: 2011-03-15 00:00:00 -07:00
18
14
  default_executable:
19
15
  dependencies:
20
16
  - !ruby/object:Gem::Dependency
@@ -25,10 +21,6 @@ dependencies:
25
21
  requirements:
26
22
  - - ">="
27
23
  - !ruby/object:Gem::Version
28
- segments:
29
- - 1
30
- - 1
31
- - 2
32
24
  version: 1.1.2
33
25
  type: :runtime
34
26
  version_requirements: *id001
@@ -40,10 +32,6 @@ dependencies:
40
32
  requirements:
41
33
  - - ">="
42
34
  - !ruby/object:Gem::Version
43
- segments:
44
- - 1
45
- - 2
46
- - 2
47
35
  version: 1.2.2
48
36
  type: :runtime
49
37
  version_requirements: *id002
@@ -58,6 +46,7 @@ extra_rdoc_files: []
58
46
  files:
59
47
  - README.md
60
48
  - bin/test-loop
49
+ - lib/test/loop.rb
61
50
  has_rdoc: true
62
51
  homepage: http://github.com/sunaku/test-loop
63
52
  licenses: []
@@ -72,21 +61,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
72
61
  requirements:
73
62
  - - ">="
74
63
  - !ruby/object:Gem::Version
75
- segments:
76
- - 0
77
64
  version: "0"
78
65
  required_rubygems_version: !ruby/object:Gem::Requirement
79
66
  none: false
80
67
  requirements:
81
68
  - - ">="
82
69
  - !ruby/object:Gem::Version
83
- segments:
84
- - 0
85
70
  version: "0"
86
71
  requirements: []
87
72
 
88
73
  rubyforge_project:
89
- rubygems_version: 1.3.7
74
+ rubygems_version: 1.6.2
90
75
  signing_key:
91
76
  specification_version: 3
92
77
  summary: Continuous testing for Ruby with fork/eval