test-loop 11.0.1 → 12.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ (the ISC license)
2
+
3
+ Copyright 2010 Suraj N. Kurapati <sunaku@gmail.com>
4
+ Copyright 2011 Brian D. Burns <burns180@gmail.com>
5
+
6
+ Permission to use, copy, modify, and/or distribute this software for any
7
+ purpose with or without fee is hereby granted, provided that the above
8
+ copyright notice and this permission notice appear in all copies.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md CHANGED
@@ -32,6 +32,12 @@ Features
32
32
 
33
33
  * Implemented in less than 250 lines (SLOC) of pure Ruby code! :-)
34
34
 
35
+ ------------------------------------------------------------------------------
36
+ Prerequisites
37
+ ------------------------------------------------------------------------------
38
+
39
+ * Operating system that supports POSIX signals and the `fork()` system call.
40
+
35
41
  ------------------------------------------------------------------------------
36
42
  Installation
37
43
  ------------------------------------------------------------------------------
@@ -61,6 +67,10 @@ You can monitor your test processes in another terminal:
61
67
 
62
68
  watch 'ps xf | grep test-loop | sed 1,3d'
63
69
 
70
+ If it stops responding, you can annihilate test-loop from another terminal:
71
+
72
+ pkill -9 -f test-loop
73
+
64
74
  ------------------------------------------------------------------------------
65
75
  Operation
66
76
  ------------------------------------------------------------------------------
@@ -220,7 +230,7 @@ configuration file:
220
230
  }
221
231
 
222
232
  ------------------------------------------------------------------------------
223
- Configuration Presets
233
+ Configuration presets
224
234
  ------------------------------------------------------------------------------
225
235
 
226
236
  The following sub-libraries provide "preset" configurations. To use them,
@@ -231,26 +241,30 @@ your application's `test/test_helper.rb` or `spec/spec_helper.rb` file.
231
241
 
232
242
  Shows on-screen-display notifications for test failures.
233
243
 
244
+ ### require 'test/loop/rails'
245
+
246
+ Provides support for the Ruby on Rails web framework.
247
+
234
248
  ------------------------------------------------------------------------------
235
249
  Known issues
236
250
  ------------------------------------------------------------------------------
237
251
 
238
252
  ### Ruby on Rails
239
253
 
240
- * Ensure that your `config/environments/test.rb` file disables class caching
241
- as follows (**NOTE:** this is done automatically for you if you use Rails
242
- 3):
254
+ * Ensure that your `config/environments/test.rb` file disables class caching
255
+ as follows (**NOTE:** if you are using Rails 3, the `test/loop/rails`
256
+ preset will automatically do this for you):
243
257
 
244
- config.cache_classes = false
258
+ config.cache_classes = false
245
259
 
246
- Otherwise, test-loop will appear to ignore source-code changes in your
247
- models, controllers, helpers, and other Ruby source files.
260
+ Otherwise, test-loop will appear to ignore source-code changes in your
261
+ models, controllers, helpers, and other Ruby source files.
248
262
 
249
- * SQLite3 [raises `SQLite3::BusyException: database is locked` errors](
250
- https://github.com/sunaku/test-loop/issues/2 ) because test-loop runs your
251
- test files in parallel. You can work around this by using an [in-memory
252
- adapter for SQLite3]( https://github.com/mvz/memory_test_fix ) or by using
253
- different database software (such as MySQL) for your test environment.
263
+ * SQLite3 [raises `SQLite3::BusyException: database is locked` errors](
264
+ https://github.com/sunaku/test-loop/issues/2 ) because test-loop runs your
265
+ test files in parallel. You can work around this by using an [in-memory
266
+ adapter for SQLite3]( https://github.com/mvz/memory_test_fix ) or by using
267
+ different database software (such as MySQL) for your test environment.
254
268
 
255
269
  ------------------------------------------------------------------------------
256
270
  License
data/lib/test/loop.rb CHANGED
@@ -49,19 +49,14 @@ module Test
49
49
 
50
50
  class << Loop
51
51
  def run
52
- @running_files = []
53
- @running_files_lock = Mutex.new
54
- @lines_by_file = {} # path => readlines
55
- @last_ran_at = @started_at = Time.now
56
-
52
+ initialize_vars
57
53
  register_signals
58
54
  load_user_config
59
55
  absorb_overhead
60
- load_lib_support
61
56
  run_test_loop
62
57
 
63
58
  rescue Interrupt
64
- # allow user to break the loop by pressing Ctrl-C or sending SIGINT
59
+ # allow user to break the loop
65
60
 
66
61
  rescue Exception => error
67
62
  STDERR.puts error.inspect, error.backtrace
@@ -75,13 +70,16 @@ module Test
75
70
 
76
71
  private
77
72
 
78
- EXEC_VECTOR = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
73
+ MASTER_ARGV = [$0, *ARGV].map {|s| s.dup.freeze }.freeze
79
74
  RESUME_ENV_KEY = 'TEST_LOOP_RESUME_FILES'.freeze
75
+ MASTER_ENV = ENV.to_hash.delete_if {|k,v| k == RESUME_ENV_KEY }.freeze
80
76
 
81
77
  ANSI_CLEAR_LINE = "\e[2K\e[0G".freeze
82
78
  ANSI_GREEN = "\e[32m%s\e[0m".freeze
83
79
  ANSI_RED = "\e[31m%s\e[0m".freeze
84
80
 
81
+ Worker = Struct.new(:test_file, :log_file, :started_at)
82
+
85
83
  def notify message
86
84
  # using print() because puts() is not an atomic operation.
87
85
  # also, clear the line before printing because some shells emit
@@ -89,25 +87,53 @@ module Test
89
87
  print "#{ANSI_CLEAR_LINE}test-loop: #{message}\n"
90
88
  end
91
89
 
90
+ def initialize_vars
91
+ @running_files = []
92
+ @lines_by_file = {} # path => readlines
93
+ @last_ran_at = @started_at = Time.now
94
+ @worker_by_pid = {}
95
+ end
96
+
92
97
  def register_signals
93
98
  # this signal is ignored in master and honored in workers, so all
94
99
  # workers can be killed by sending it to the entire process group
95
100
  trap :TERM, :IGNORE
96
101
 
97
- master_pid = $$
98
- master_trap = lambda do |signal, &handler|
99
- trap signal do
100
- if $$ == master_pid
101
- handler.call
102
- else
103
- # ignore future ocurrences of this signal in worker processes
104
- trap signal, :IGNORE
102
+ trap :CHLD do
103
+ finished_at = Time.now
104
+
105
+ begin
106
+ worker_pid = Process.wait
107
+ run_status = $?
108
+
109
+ worker = @worker_by_pid.delete(worker_pid)
110
+ elapsed_time = finished_at - worker.started_at
111
+
112
+ # report test results along with any failure logs
113
+ if run_status.success?
114
+ notify ANSI_GREEN % "PASS #{worker.test_file}"
115
+ elsif run_status.exited?
116
+ notify ANSI_RED % "FAIL #{worker.test_file}"
117
+ STDERR.print File.read(worker.log_file)
118
+ end
119
+
120
+ after_each_test.each do |hook|
121
+ hook.call worker.test_file, worker.log_file, run_status,
122
+ worker.started_at, elapsed_time
105
123
  end
124
+
125
+ @running_files.delete worker.test_file
126
+
127
+ rescue Errno::ECHILD
128
+ # could not get the terminated child's PID.
129
+ # Ruby's backtick operator can cause this:
130
+ # http://stackoverflow.com/questions/1495354
106
131
  end
107
132
  end
108
133
 
109
- master_trap.call(:QUIT) { reload_master_process }
110
- master_trap.call(:TSTP) { forcibly_run_all_tests }
134
+ trap(:INT) { raise Interrupt }
135
+ trap(:QUIT) { reload_master_process }
136
+ trap(:TSTP) { forcibly_run_all_tests }
111
137
  end
112
138
 
113
139
  def kill_workers
@@ -119,9 +145,10 @@ module Test
119
145
  # The given test files are passed down (along with currently running
120
146
  # test files) to the next incarnation of test-loop for resumption.
121
147
  def reload_master_process test_files = []
122
- @running_files_lock.synchronize { test_files.concat @running_files }
148
+ test_files.concat @running_files
123
149
  kill_workers
124
- exec({RESUME_ENV_KEY => test_files.inspect}, *EXEC_VECTOR)
150
+ exec MASTER_ENV.merge(RESUME_ENV_KEY => test_files.inspect),
151
+ *MASTER_ARGV, {:unsetenv_others => true}
125
152
  end
126
153
 
127
154
  def load_user_config
@@ -139,10 +166,6 @@ module Test
139
166
  end
140
167
  end
141
168
 
142
- def load_lib_support
143
- Dir[File.expand_path '../loop/support/*.rb', __FILE__].each {|f| load f }
144
- end
145
-
146
169
  def pause_momentarily
147
170
  sleep 1
148
171
  end
@@ -179,9 +202,7 @@ module Test
179
202
 
180
203
  # fork workers to run the test files in parallel,
181
204
  # excluding test files that are already running
182
- test_files = @running_files_lock.
183
- synchronize { test_files - @running_files }
184
-
205
+ test_files -= @running_files
185
206
  unless test_files.empty?
186
207
  @last_ran_at = Time.now
187
208
  test_files.each {|file| run_test_file file }
@@ -192,7 +213,9 @@ module Test
192
213
  end
193
214
 
194
215
  def run_test_file test_file
195
- @running_files_lock.synchronize { @running_files.push test_file }
216
+ notify "TEST #{test_file}"
217
+
218
+ @running_files.push test_file
196
219
  log_file = test_file + '.log'
197
220
 
198
221
  # cache the contents of the test file for diffing below
@@ -200,17 +223,24 @@ module Test
200
223
  old_lines = @lines_by_file[test_file] || new_lines
201
224
  @lines_by_file[test_file] = new_lines
202
225
 
226
+ started_at = Time.now
203
227
  worker_pid = fork do
204
- # this signal is ignored in master and honored in workers, so all
205
- # workers can be killed by sending it to the entire process group
206
- trap :TERM, :DEFAULT
228
+ # handle signals meant for worker process
229
+ [:TERM, :CHLD].each {|sig| trap sig, :DEFAULT }
207
230
 
208
- # this signal is honored by master and ignored in workers
209
- trap :INT, :IGNORE
231
+ # ignore signals meant for master process
232
+ [:INT, :QUIT, :TSTP].each {|sig| trap sig, :IGNORE }
233
+
234
+ # detach worker from master's terminal device so that
235
+ # it does not receieve the user's control-key presses
236
+ Process.setsid
237
+
238
+ # detach worker from master's standard input stream
239
+ STDIN.reopen IO.pipe.first
210
240
 
211
241
  # capture test output in log file because tests are run in parallel
212
242
  # which makes it difficult to understand interleaved output thereof
213
- $stderr.reopen($stdout.reopen(log_file, 'w')).sync = true
243
+ STDERR.reopen(STDOUT.reopen(log_file, 'w')).sync = true
214
244
 
215
245
  # determine which test blocks have changed inside the test file
216
246
  test_names = Diff::LCS.diff(old_lines, new_lines).flatten.map do |change|
@@ -237,29 +267,7 @@ module Test
237
267
  load test_file
238
268
  end
239
269
 
240
- # monitor and report on the worker's progress
241
- Thread.new do
242
- notify "TEST #{test_file}"
243
-
244
- # wait for worker to finish
245
- Process.waitpid worker_pid
246
- run_status = $?
247
- elapsed_time = Time.now - @last_ran_at
248
-
249
- # report test results along with any failure logs
250
- if run_status.success?
251
- notify ANSI_GREEN % "PASS #{test_file}"
252
- elsif run_status.exited?
253
- notify ANSI_RED % "FAIL #{test_file}"
254
- STDERR.print File.read(log_file)
255
- end
256
-
257
- after_each_test.each do |f|
258
- f.call test_file, log_file, run_status, @last_ran_at, elapsed_time
259
- end
260
-
261
- @running_files_lock.synchronize { @running_files.delete test_file }
262
- end
270
+ @worker_by_pid[worker_pid] = Worker.new(test_file, log_file, started_at)
263
271
  end
264
272
  end
265
273
  end
@@ -0,0 +1,20 @@
1
+ require 'test/loop'
2
+
3
+ Test::Loop.reabsorb_file_globs.push(
4
+ 'config/**/*.{rb,yml}',
5
+ 'test/factories/*.rb',
6
+ 'Gemfile.lock'
7
+ )
8
+
9
+ Test::Loop.test_file_matchers['app/**/*.rb'] =
10
+ Test::Loop.test_file_matchers['lib/**/*.rb']
11
+
12
+ require 'rails/railtie'
13
+ Class.new Rails::Railtie do
14
+ config.before_initialize do |app|
15
+ if app.config.cache_classes
16
+ warn "test-loop: Setting #{app.class}.config.cache_classes = false"
17
+ app.config.cache_classes = false
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 11.0.1
5
+ version: 12.0.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Suraj N. Kurapati
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2011-04-14 00:00:00 -07:00
14
+ date: 2011-04-19 00:00:00 -07:00
15
15
  default_executable:
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
@@ -34,13 +34,12 @@ extensions: []
34
34
  extra_rdoc_files: []
35
35
 
36
36
  files:
37
+ - LICENSE
37
38
  - README.md
38
39
  - bin/test-loop
39
40
  - lib/test/loop.rb
40
41
  - lib/test/loop/notify.rb
41
- - lib/test/loop/support/rails.rb
42
- - lib/test/loop/support/minitest.rb
43
- - lib/test/loop/support/testunit.rb
42
+ - lib/test/loop/rails.rb
44
43
  has_rdoc: true
45
44
  homepage: http://github.com/sunaku/test-loop
46
45
  licenses: []
@@ -1,5 +0,0 @@
1
- if defined? MiniTest::Unit
2
- Test::Loop.before_each_test.push proc {
3
- MiniTest::Unit.output = $stdout
4
- }
5
- end
@@ -1,21 +0,0 @@
1
- if defined? Rails
2
- Test::Loop.reabsorb_file_globs.push(
3
- 'config/**/*.{rb,yml}',
4
- 'test/factories/*.rb',
5
- 'Gemfile.lock'
6
- )
7
-
8
- Test::Loop.test_file_matchers['app/**/*.rb'] =
9
- Test::Loop.test_file_matchers['lib/**/*.rb']
10
-
11
- if defined? Rails::Railtie
12
- Class.new Rails::Railtie do
13
- config.before_initialize do |app|
14
- if app.config.cache_classes
15
- warn "test-loop: Setting #{app.class}.config.cache_classes = false"
16
- app.config.cache_classes = false
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,7 +0,0 @@
1
- if defined? Test::Unit::AutoRunner
2
- Test::Loop.before_each_test.push proc {
3
- Test::Unit::AutoRunner.prepare do |config|
4
- config.runner_options[:output] = $stdout
5
- end
6
- }
7
- end