test-loop 9.1.0 → 9.1.1

Sign up to get free protection for your applications and to get access to all the features.
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