test-loop 9.3.0 → 9.4.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 (4) hide show
  1. data/README.md +101 -80
  2. data/bin/test-loop +1 -0
  3. data/lib/test/loop.rb +27 -20
  4. metadata +2 -2
data/README.md CHANGED
@@ -59,7 +59,7 @@ If installed as a Git clone:
59
59
 
60
60
  You can monitor your test processes in another terminal:
61
61
 
62
- watch "ps xf | egrep 'test-loop|(test|spec)/.+\.rb' | sed '1,3d'"
62
+ watch 'ps xf | grep test-loop | sed 1,3d'
63
63
 
64
64
  ------------------------------------------------------------------------------
65
65
  Operation
@@ -81,119 +81,140 @@ test-loop looks for a configuration file named `.test-loop` in the current
81
81
  working directory. This configuration file is a normal Ruby script in which
82
82
  you can query and modify the `Test::Loop` OpenStruct configuration as follows:
83
83
 
84
- * `Test::Loop.overhead_file_globs` is an array of file globbing patterns that
85
- describe a set of Ruby scripts that are loaded into the main Ruby process as
86
- overhead.
84
+ ### Test::Loop.overhead_file_globs
87
85
 
88
- * `Test::Loop.reabsorb_file_globs` is an array of file globbing patterns that
89
- describe a set of files which cause the overhead to be reabsorbed whenever
90
- they change.
86
+ An array of file globbing patterns that describe a set of Ruby scripts that
87
+ are loaded into the main Ruby process as overhead.
91
88
 
92
- * `Test::Loop.test_file_matchers` is a hash that maps a file globbing pattern
93
- describing a set of source files to a lambda function yielding a file
94
- globbing pattern describing a set of test files that need to be run. In
95
- other words, whenever the source files (the hash key; left-hand side of the
96
- mapping) change, their associated test files (the hash value; right-hand
97
- side of the mapping) are run.
89
+ ### Test::Loop.reabsorb_file_globs
98
90
 
99
- For example, if test files had the same names as their source files followed
100
- by an underscore and the file name in reverse like this:
91
+ An array of file globbing patterns that describe a set of files which cause
92
+ the overhead to be reabsorbed whenever they change.
101
93
 
102
- * `lib/hello.rb` => `test/hello_olleh.rb`
103
- * `app/world.rb` => `spec/world_ldrow.rb`
94
+ ### Test::Loop.test_file_matchers
104
95
 
105
- Then you would add the following to your configuration file:
96
+ A hash that maps a file globbing pattern describing a set of source files to a
97
+ lambda function yielding a file globbing pattern describing a set of test
98
+ files that need to be run. In other words, whenever the source files (the
99
+ hash key; left-hand side of the mapping) change, their associated test files
100
+ (the hash value; right-hand side of the mapping) are run.
106
101
 
107
- Test::Loop.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
108
- extn = File.extname(path)
109
- name = File.basename(path, extn)
110
- "{test,spec}/**/#{name}_#{name.reverse}#{extn}"
111
- end
112
-
113
- * `Test::Loop.test_name_parser` is a lambda function that is passed a line of
114
- source code to determine whether that line can be considered as a test
115
- definition, in which case it must return the name of the test being defined.
102
+ For example, if test files had the same names as their source files followed
103
+ by an underscore and the file name in reverse like this:
116
104
 
117
- * `Test::Loop.before_each_test` is a lambda function that is executed inside
118
- the worker process before loading the test file. It is passed (1) the path
119
- to the test file, (2) the path to the log file containing the live output of
120
- running the test file, and (3) an array containing the names of tests (which
121
- were identified by `Test::Loop.test_name_parser`) inside the test file that
122
- have changed since the last run of the test file.
105
+ * `lib/hello.rb` => `test/hello_olleh.rb`
106
+ * `app/world.rb` => `spec/world_ldrow.rb`
123
107
 
124
- The default implementation of this function instructs Test::Unit and RSpec
125
- to only run certain test blocks inside the test file. This accelerates your
126
- test-driven development cycle and improves productivity!
108
+ Then you would add the following to your configuration file:
127
109
 
128
- If you wish to add extend the default implementation, store and recall it:
110
+ Test::Loop.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
111
+ extn = File.extname(path)
112
+ name = File.basename(path, extn)
113
+ "{test,spec}/**/#{name}_#{name.reverse}#{extn}"
114
+ end
129
115
 
130
- default_implementation = Test::Loop.before_each_test
116
+ In addition, these lambda functions can return `nil` if they do not wish for a
117
+ particular source file to be tested. For example, to ignore tests for all
118
+ source files except those within a `models/` directory, you would write:
131
119
 
132
- Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
133
- default_implementation.call test_file, log_file, test_names
134
- # do something additional ...
120
+ Test::Loop.test_file_matchers['{lib,app}/**/*.rb'] = lambda do |path|
121
+ if path.include? '/models/'
122
+ "{test,spec}/**/#{File.basename(path)}"
135
123
  end
124
+ end
136
125
 
137
- Or if you want to completely replace the default implementation:
126
+ For source files not satisfying the above constraint, this lambda function
127
+ will return `nil`, thereby excluding those source files from being tested.
138
128
 
139
- Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
140
- # your replacement here ...
141
- end
129
+ ### Test::Loop.test_name_parser
142
130
 
143
- * `Test::Loop.after_each_test` is a lambda function that is executed inside
144
- the master process after a test has finished running. It is passed (1) the
145
- path to the test file, (2) the path to the log file containing the output of
146
- running the test file, (3) a `Process::Status` object describing the exit
147
- status of the worker process that ran the test file, (4) the time when test
148
- execution began, and (5) how many seconds it took for the overall test
149
- execution to complete.
131
+ A lambda function that is passed a line of source code to determine whether
132
+ that line can be considered as a test definition, in which case it must return
133
+ the name of the test being defined.
150
134
 
151
- For example, to delete log files for successful tests, add the following to
152
- your configuration file:
135
+ ### Test::Loop.before_each_test
153
136
 
154
- Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
155
- File.delete(log_file) if run_status.success?
156
- end
137
+ A lambda function that is executed inside the worker process before loading
138
+ the test file. It is passed (1) the path to the test file, (2) the path to
139
+ the log file containing the live output of running the test file, and (3) an
140
+ array containing the names of tests (which were identified by
141
+ `Test::Loop.test_name_parser`) inside the test file that have changed since
142
+ the last run of the test file.
157
143
 
158
- For example, to see on-screen-display notifications only about test
159
- failures, add the following to your configuration file:
144
+ The default implementation of this function instructs Test::Unit and RSpec to
145
+ only run certain test blocks inside the test file. This accelerates your
146
+ test-driven development cycle and improves productivity!
160
147
 
161
- Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
162
- unless run_status.success?
163
- title = 'FAIL at %s in %0.1fs' % [started_at.strftime('%r'), elapsed_time]
148
+ If you wish to add extend the default implementation, store and recall it:
164
149
 
165
- message = test_file
150
+ default_implementation = Test::Loop.before_each_test
166
151
 
167
- Thread.new do # run in background
168
- system 'notify-send', '-i', 'dialog-error', title, message or
169
- system 'growlnotify', '-a', 'Xcode', '-m', message, title or
170
- system 'xmessage', '-timeout', '5', '-title', title, message
171
- end
172
- end
173
- end
152
+ Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
153
+ default_implementation.call test_file, log_file, test_names
154
+ # do something additional ...
155
+ end
174
156
 
175
- Note that the above functionality is available as a configuration preset:
157
+ Or if you want to completely replace the default implementation:
176
158
 
177
- require 'test/loop/notify'
159
+ Test::Loop.before_each_test = lambda do |test_file, log_file, test_names|
160
+ # your replacement here ...
161
+ end
162
+
163
+ ### Test::Loop.after_each_test
164
+
165
+ A lambda function that is executed inside the master process after a test has
166
+ finished running. It is passed (1) the path to the test file, (2) the path to
167
+ the log file containing the output of running the test file, (3) a
168
+ `Process::Status` object describing the exit status of the worker process that
169
+ ran the test file, (4) the time when test execution began, and (5) how many
170
+ seconds it took for the overall test execution to complete.
178
171
 
179
- For example, to see on-screen-display notifications about completed test
180
- runs, regardless of whether they passed or failed, add the following to your
181
- configuration file:
172
+ For example, to delete log files for successful tests, add the following to
173
+ your configuration file:
182
174
 
183
- Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
184
- success = run_status.success?
175
+ Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
176
+ File.delete(log_file) if run_status.success?
177
+ end
185
178
 
186
- title = '%s at %s in %0.1fs' %
187
- [success ? 'PASS' : 'FAIL', started_at.strftime('%X'), elapsed_time]
179
+ For example, to see on-screen-display notifications only about test failures,
180
+ add the following to your configuration file:
181
+
182
+ Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
183
+ unless run_status.success?
184
+ title = 'FAIL at %s in %0.1fs' % [started_at.strftime('%r'), elapsed_time]
188
185
 
189
186
  message = test_file
190
187
 
191
188
  Thread.new do # run in background
192
- system 'notify-send', '-i', "dialog-#{success ? 'information' : 'error'}", title, message or
189
+ system 'notify-send', '-i', 'dialog-error', title, message or
193
190
  system 'growlnotify', '-a', 'Xcode', '-m', message, title or
194
191
  system 'xmessage', '-timeout', '5', '-title', title, message
195
192
  end
196
193
  end
194
+ end
195
+
196
+ Note that the above functionality is available as a configuration preset:
197
+
198
+ require 'test/loop/notify'
199
+
200
+ For example, to see on-screen-display notifications about completed test runs,
201
+ regardless of whether they passed or failed, add the following to your
202
+ configuration file:
203
+
204
+ Test::Loop.after_each_test = lambda do |test_file, log_file, run_status, started_at, elapsed_time|
205
+ success = run_status.success?
206
+
207
+ title = '%s at %s in %0.1fs' %
208
+ [success ? 'PASS' : 'FAIL', started_at.strftime('%X'), elapsed_time]
209
+
210
+ message = test_file
211
+
212
+ Thread.new do # run in background
213
+ system 'notify-send', '-i', "dialog-#{success ? 'information' : 'error'}", title, message or
214
+ system 'growlnotify', '-a', 'Xcode', '-m', message, title or
215
+ system 'xmessage', '-timeout', '5', '-title', title, message
216
+ end
217
+ end
197
218
 
198
219
  ------------------------------------------------------------------------------
199
220
  Configuration Presets
data/bin/test-loop CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'test/loop'
3
3
  Test::Loop.run
4
+ exit! true # skip at_exit() handlers registered in the master process
data/lib/test/loop.rb CHANGED
@@ -48,14 +48,21 @@ module Test
48
48
 
49
49
  class << Loop
50
50
  def run
51
- init_test_loop
52
- register_signals
53
- load_user_config
54
- absorb_overhead
55
- run_test_loop
56
- rescue SystemExit
57
- # allow exit() to terminate the test loop
51
+ @running_files = []
52
+ @running_files_lock = Mutex.new
53
+ @lines_by_file = {} # path => readlines
54
+ @last_ran_at = @started_at = Time.now
55
+
56
+ catch self do
57
+ register_signals
58
+ load_user_config
59
+ absorb_overhead
60
+ run_test_loop
61
+ end
62
+
63
+ kill_workers
58
64
  notify 'Goodbye!'
65
+
59
66
  rescue Exception => error
60
67
  STDERR.puts error.inspect, error.backtrace
61
68
  pause_momentarily
@@ -76,22 +83,16 @@ module Test
76
83
  print "test-loop: #{message}\n"
77
84
  end
78
85
 
79
- def init_test_loop
80
- @running_files = []
81
- @running_files_lock = Mutex.new
82
- @lines_by_file = {} # path => readlines
83
- @last_ran_at = @started_at = Time.now
84
- end
85
-
86
86
  def register_signals
87
+ # this signal is ignored in master and honored in workers, so all
88
+ # workers can be killed by sending it to the entire process group
89
+ trap :TERM, :IGNORE
90
+
87
91
  # clear line to shield normal output from control-key interference:
88
92
  # some shells like BASH emit text when control-key combos are pressed
89
- trap(:INT) { print ANSI_CLEAR_LINE; kill_workers; exit }
93
+ trap(:INT) { print ANSI_CLEAR_LINE; throw self }
90
94
  trap(:QUIT) { print ANSI_CLEAR_LINE; reload_master_process }
91
95
  trap(:TSTP) { print ANSI_CLEAR_LINE; forcibly_run_all_tests }
92
-
93
- master_pid = $$
94
- trap(:TERM) { exit unless $$ == master_pid }
95
96
  end
96
97
 
97
98
  def kill_workers
@@ -139,7 +140,7 @@ module Test
139
140
  # find test files that have been modified since the last run
140
141
  test_files = test_file_matchers.map do |source_glob, test_matcher|
141
142
  Dir[source_glob].select {|file| File.mtime(file) > @last_ran_at }.
142
- map {|path| Dir[test_matcher.call path] }
143
+ map {|path| Dir[test_matcher.call(path).to_s] }
143
144
  end.flatten.uniq
144
145
 
145
146
  # resume test files stopped by the previous incarnation of test-loop
@@ -181,6 +182,9 @@ module Test
181
182
  @lines_by_file[test_file] = new_lines
182
183
 
183
184
  worker_pid = fork do
185
+ # unregister custom signal handlers meant for master process
186
+ [:TERM, :INT, :QUIT, :TSTP].each {|sig| trap sig, :DEFAULT }
187
+
184
188
  # capture test output in log file because tests are run in parallel
185
189
  # which makes it difficult to understand interleaved output thereof
186
190
  $stdout.reopen log_file, 'w'
@@ -203,10 +207,13 @@ module Test
203
207
  # tell the testing framework to run only the changed test blocks
204
208
  before_each_test.call test_file, log_file, test_names
205
209
 
210
+ # make the process title Test::Unit friendly and ps(1) searchable
211
+ $0 = "test-loop #{test_file}"
212
+
206
213
  # after loading the user's test file, the at_exit() hook of the
207
214
  # user's testing framework will take care of running the tests and
208
215
  # reflecting any failures in the worker process' exit status
209
- load $0 = test_file # set $0 because Test::Unit outputs it
216
+ load test_file
210
217
  end
211
218
 
212
219
  # monitor and report on the worker's progress
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: test-loop
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 9.3.0
5
+ version: 9.4.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-01 00:00:00 -07:00
14
+ date: 2011-04-06 00:00:00 -07:00
15
15
  default_executable:
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency