test-loop 11.0.1 → 12.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.
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