spin 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,136 @@
1
+ Spin
2
+ ====
3
+
4
+ Spin speeds up your Rails testing workflow.
5
+
6
+ By preloading your Rails environment in one process and then using fork(2) for each test run you don't load the same code over and over and over...
7
+ Spin works with an autotest(ish) workflow.
8
+
9
+ Installation
10
+ ===========
11
+
12
+ Spin is available as a rubygem.
13
+
14
+ ``` ruby
15
+ gem i spin
16
+ ```
17
+
18
+ Spin is a tool for Rails 3 apps. It is compatible with the following testing libraries:
19
+
20
+ * any version of test/unit or MiniTest
21
+ * RSpec 2.x
22
+
23
+ Usage
24
+ =====
25
+
26
+ There are two components to Spin, a server and client. The server has to be running for anything interesting to happen. You can start the Spin server from your `Rails.root` with the following command:
27
+
28
+ ``` bash
29
+ spin serve
30
+ ```
31
+
32
+ As soon as the server is running it will be ready to accept from clients. You can use the following command to specify a file for the server to load:
33
+
34
+ ``` bash
35
+ spin push test/unit/product_test.rb
36
+ ```
37
+
38
+ Or push multiple files to be loaded at once:
39
+
40
+ ``` bash
41
+ spin push test/unit/product_test.rb test/unit/shop_test.rb test/unit/cart_test.rb
42
+ ```
43
+
44
+ Or, when using RSpec, run the whole suite:
45
+
46
+ ``` bash
47
+ spin push spec
48
+ ```
49
+
50
+ Running a single RSpec example by adding a line number is also possible, e.g:
51
+
52
+ ``` bash
53
+ spin push spec/models/user_spec.rb:14
54
+ ```
55
+
56
+ If you experience issues with `test_helper.rb` not being available you may need to add your test directory to the load path using the `-I` option:
57
+
58
+ ``` bash
59
+ spin serve -Itest
60
+ ```
61
+
62
+ Send a SIGQUIT to spin serve (`Ctrl+\`) if you want to re-run the last files that were ran via `spin push [files]`.
63
+
64
+ ### With Kicker
65
+
66
+ As mentioned, this tool works best with an autotest(ish) workflow. I haven't actually used with with `autotest` itself, but it works great with [kicker](http://github.com/alloy/kicker). Here's the suggested workflow for a Rails app:
67
+
68
+ 1. Start up the spin server
69
+
70
+ ``` bash
71
+ spin serve
72
+ ```
73
+
74
+ 2. Start up `kicker` using the custom binary option (and any other options you want)
75
+
76
+ ``` bash
77
+ kicker -r rails -b 'spin push'
78
+ ```
79
+
80
+ 3. Faster testing workflow!
81
+
82
+ Motivation
83
+ ==========
84
+
85
+ A few months back I did an experiment. I opened up the source code to my local copy of the ActiveRecord gem. I added a line at the top of `active_record/base` that incremented a counter in Redis each time it was evaluated. After about a week that counter was well above 2000!
86
+
87
+ How did I load the ActiveRecord gem over 2000 times in one week? Autotest. I was using it all day while developing. The Rails version that the app was tracking doesn't change very often, yet I had to load the same code over and over again.
88
+
89
+ Given that there's no way to compile Ruby code into a faster representation I immediately thought of fork(2). I just need a process to load up Rails and wait around until I need it. When I want to run the tests I just fork(2) that idle process and run the test. Then I only have to load Rails once at the start of my workflow, fork(2) takes care of sharing the code with each child process.
90
+
91
+ I threw together the first version of this project in about 20 minutes and noticed an immediate difference in the speed of my testing workflow. Did I mention that I work on a big app? It takes about 10 seconds(!) to load Rails and all of the gem dependencies. With a bit more hacking I was able to get the idle process to load both Rails and my application dependencies, so each test run just initializes the application and loads the files needed for the test run.
92
+
93
+ (10 seconds saved per test run) x (2000 test runs per week) = (lots of time saved!)
94
+
95
+ ### How is it different from Spork?
96
+
97
+ There's another project ([spork](https://github.com/sporkrb/spork)) that aims to solve the same problem, but takes a different approach.
98
+
99
+ 1. It's unobtrusive.
100
+
101
+ Your application needs to know about Spork, Spin works entirely outside of your application.
102
+
103
+ You'll need to add spork to your Gemfile and introduce your `test_helper.rb` to spork. Spork needs to know details about your app's loading process.
104
+
105
+ Spin is designed so that your app never has to know about it. You can use Spin to run your tests while the rest of your team doesn't even know that Spin exists.
106
+
107
+ 2. It's simple.
108
+
109
+ Spin should work out of the box with any Rails app. No custom configuration required.
110
+
111
+ 3. It doesn't do any [crazy monkey patching](https://github.com/sporkrb/spork-rails/blob/master/lib/spork/app_framework/rails.rb#L43-80).
112
+
113
+ Docs
114
+ ============
115
+
116
+ [Rocco](http://rtomayko.github.com/rocco/)-annotated source:
117
+
118
+ * [spin](http://jstorimer.github.com/spin/)
119
+ * [spin serve](http://jstorimer.github.com/spin/#section-spin_serve)
120
+ * [spin push](http://jstorimer.github.com/spin/#section-spin_push)
121
+
122
+ Hacking
123
+ =======
124
+
125
+ I take pull requests, and it's commit bit, and there are no tests.
126
+
127
+ Related Projects
128
+ ===============
129
+
130
+ If Spin isn't scratching your itch then one of these projects might:
131
+
132
+ * [guard-spin](https://github.com/vizjerai/guard-spin)
133
+ * [Spork](https://github.com/sporkrb/spork)
134
+ * [TestR](https://github.com/sunaku/testr)
135
+ * [Zeus](https://github.com/burke/zeus)
136
+
data/bin/spin CHANGED
@@ -1,357 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
- # Spin will speed up your autotest(ish) workflow for Rails.
3
-
4
- # Spin preloads your Rails environment for testing, so you don't load the same code over and over and over... Spin works best with an autotest(ish) workflow.
5
-
6
- require 'socket'
7
- # This brings in `Dir.tmpdir`
8
- require 'tempfile'
9
- # This lets us hash the parameters we want to include in the filename
10
- # without having to worry about subdirectories, special chars, etc.
11
- require 'digest/md5'
12
- # So we can tell users how much time they're saving by preloading their
13
- # environment.
14
- require 'benchmark'
15
- require 'optparse'
16
- require 'pathname'
17
-
18
- SEPARATOR = '|'
19
-
20
- def usage
21
- <<-USAGE
22
- Usage: spin serve
23
- spin push <file> <file>...
24
- Spin preloads your Rails environment to speed up your autotest(ish) workflow.
25
- USAGE
26
- end
27
-
28
- def socket_file
29
- key = Digest::MD5.hexdigest [Dir.pwd, 'spin-gem'].join
30
- [Dir.tmpdir, key].join('/')
31
- end
32
-
33
- def determine_test_framework(force_rspec, force_testunit)
34
- if force_rspec
35
- :rspec
36
- elsif force_testunit
37
- :testunit
38
- elsif defined?(RSpec)
39
- :rspec
40
- else
41
- :testunit
42
- end
43
- end
44
-
45
- def disconnect(connection)
46
- connection.print "\0"
47
- connection.close
48
- end
49
-
50
- def rails_root(preload)
51
- path = Pathname.pwd
52
- until path.join(preload).file?
53
- return if path.root?
54
- path = path.parent
55
- end
56
- path
57
- end
58
-
59
- # ## spin serve
60
- def serve(force_rspec, force_testunit, time, push_results, preload)
61
- root_path = rails_root(preload) and Dir.chdir(root_path)
62
- file = socket_file
63
- Spin.parse_hook_file(root_path) if root_path
64
-
65
- # We delete the tmp file for the Unix socket if it already exists. The file
66
- # is scoped to the `pwd`, so if it already exists then it must be from an
67
- # old run of `spin serve` and can be cleaned up.
68
- File.delete(file) if File.exist?(file)
69
-
70
- # This socket is how we communicate with `spin push`.
71
- socket = UNIXServer.open(file)
72
-
73
- # Trap SIGINT (Ctrl-C) so that we exit cleanly.
74
- trap('SIGINT') {
75
- socket.close
76
- exit
77
- }
78
-
79
- ENV['RAILS_ENV'] = 'test' unless ENV['RAILS_ENV']
80
-
81
- test_framework = nil
82
-
83
- if root_path
84
- sec = Benchmark.realtime {
85
- # We require config/application because that file (typically) loads Rails
86
- # and any Bundler deps, as well as loading the initialization code for
87
- # the app, but it doesn't actually perform the initialization. That happens
88
- # in config/environment.
89
- #
90
- # In my experience that's the best we can do in terms of preloading. Rails
91
- # and the gem dependencies rarely change and so don't need to be reloaded.
92
- # But you can't initialize the application because any non-trivial app will
93
- # involve it's models/controllers, etc. in its initialization, which you
94
- # definitely don't want to preload.
95
- Spin.execute_hook(:before_preload)
96
- require File.expand_path preload.sub('.rb','')
97
- Spin.execute_hook(:after_preload)
98
-
99
- # Determine the test framework to use using the passed-in 'force' options
100
- # or else default to checking for defined constants.
101
- test_framework = determine_test_framework(force_rspec, force_testunit)
102
-
103
- # Preload RSpec to save some time on each test run
104
- begin
105
- require 'rspec/autorun'
106
-
107
- # Tell RSpec it's running with a tty to allow colored output
108
- if RSpec.respond_to?(:configure)
109
- RSpec.configure do |c|
110
- c.tty = true if c.respond_to?(:tty=)
111
- end
112
- end
113
- rescue LoadError
114
- end if test_framework == :rspec
115
- }
116
- # This is the amount of time that you'll save on each subsequent test run.
117
- puts "Preloaded Rails env in #{sec}s..."
118
- else
119
- warn "Could not find #{preload}. Are you running this from the root of a Rails project?"
120
- end
121
-
122
- puts "Pushing test results back to push processes" if push_results
123
-
124
- loop do
125
-
126
- # If we're not going to push the results,
127
- # Trap SIGQUIT (Ctrl+\) and re-run the last files that were
128
- # pushed.
129
- if !push_results
130
- trap('QUIT') do
131
- fork_and_run(@last_files_ran, push_results, test_framework, nil)
132
- # See WAIT below
133
- Process.wait
134
- end
135
- end
136
-
137
- # Since `spin push` reconnects each time it has new files for us we just
138
- # need to accept(2) connections from it.
139
- conn = socket.accept
140
- # This should be a list of relative paths to files.
141
- files = conn.gets.chomp
142
- files = files.split(SEPARATOR)
143
-
144
- # If spin is started with the time flag we will track total execution so
145
- # you can easily compare it with time rspec spec for example
146
- start = Time.now if time
147
-
148
- # If we're not sending results back to the push process, we can disconnect
149
- # it immediately.
150
- disconnect(conn) unless push_results
151
-
152
- fork_and_run(files, push_results, test_framework, conn)
153
-
154
- # WAIT: We don't want the parent process handling multiple test runs at the same
155
- # time because then we'd need to deal with multiple test databases, and
156
- # that destroys the idea of being simple to use. So we wait(2) until the
157
- # child process has finished running the test.
158
- Process.wait
159
-
160
- # If we are tracking time we will output it here after everything has
161
- # finished running
162
- puts "Total execution time was #{Time.now - start} seconds" if start
163
-
164
- # Tests have now run. If we were pushing results to a push process, we can
165
- # now disconnect it.
166
- begin
167
- disconnect(conn) if push_results
168
- rescue Errno::EPIPE
169
- # Don't abort if the client already disconnected
170
- end
171
- end
172
- ensure
173
- File.delete(file) if file && File.exist?(file)
174
- end
175
-
176
- def fork_and_run(files, push_results, test_framework, conn)
177
- Spin.execute_hook(:before_fork)
178
- # We fork(2) before loading the file so that our pristine preloaded
179
- # environment is untouched. The child process will load whatever code it
180
- # needs to, then it exits and we're back to the baseline preloaded app.
181
- fork do
182
- # To push the test results to the push process instead of having them
183
- # displayed by the server, we reopen $stdout/$stderr to the open
184
- # connection.
185
- tty = files.delete "tty?"
186
- if push_results
187
- $stdout.reopen(conn)
188
- if tty
189
- def $stdout.tty?
190
- true
191
- end
192
- end
193
- $stderr.reopen(conn)
194
- end
195
-
196
- Spin.execute_hook(:after_fork)
197
-
198
- puts
199
- puts "Loading #{files.inspect}"
200
-
201
- # Unfortunately rspec's interface isn't as simple as just requiring the
202
- # test file that you want to run (suddenly test/unit seems like the less
203
- # crazy one!).
204
- if test_framework == :rspec
205
- # We pretend the filepath came in as an argument and duplicate the
206
- # behaviour of the `rspec` binary.
207
- ARGV.push files
208
- else
209
- # We require the full path of the file here in the child process.
210
- files.each { |f| require File.expand_path f }
211
- end
212
-
213
- end
214
- @last_files_ran = files
215
- end
216
-
217
- # ## spin push
218
- def push(preload)
219
- # The filenames that we will spin up to `spin serve` are passed in as
220
- # arguments.
221
- files_to_load = ARGV
222
-
223
- # We reject anything in ARGV that isn't a file that exists. This takes
224
- # care of scripts that specify files like `spin push -r file.rb`. The `-r`
225
- # bit will just be ignored.
226
- #
227
- # We build a string like `file1.rb|file2.rb` and pass it up to the server.
228
- files_to_load = files_to_load.map do |file|
229
- args = file.split(':')
230
-
231
- file_name = args.first.to_s
232
- line_number = args.last.to_i
233
-
234
- # If the file exists then we can push it up just like it is
235
- file_name = if File.exist?(file_name)
236
- file_name
237
- # kicker-2.5.0 now gives us file names without extensions, so we have to try adding it
238
- elsif File.extname(file_name).empty?
239
- full_file_name = [file_name, 'rb'].join('.')
240
- full_file_name if File.exist?(full_file_name)
241
- end
242
-
243
- if line_number > 0
244
- abort "You specified a line number. Only one file can be pushed in this case." if files_to_load.length > 1
245
-
246
- "#{file_name}:#{line_number}"
247
- else
248
- file_name
249
- end
250
- end.compact.uniq
251
-
252
- if root_path = rails_root(preload)
253
- files_to_load.map! do |file|
254
- Pathname.new(file).expand_path.relative_path_from(root_path).to_s
255
- end
256
- Dir.chdir root_path
257
- end
258
-
259
- files_to_load << "tty?" if $stdout.tty?
260
- f = files_to_load.join(SEPARATOR)
261
-
262
- abort if f.empty?
263
- puts "Spinning up #{f}"
264
-
265
- # This is the other end of the socket that `spin serve` opens. At this point
266
- # `spin serve` will accept(2) our connection.
267
- socket = UNIXSocket.open(socket_file)
268
- # We put the filenames on the socket for the server to read and then load.
269
- socket.puts f
270
-
271
- while line = socket.readpartial(100)
272
- break if line[-1,1] == "\0"
273
- print line
274
- end
275
- rescue Errno::ECONNREFUSED, Errno::ENOENT
276
- abort "Connection was refused. Have you started up `spin serve` yet?"
277
- end
278
-
279
- module Spin
280
- HOOKS = [:before_fork, :after_fork, :before_preload, :after_preload]
281
-
282
- def self.hook(name, &block)
283
- raise unless HOOKS.include?(name)
284
- _hooks(name) << block
285
- end
286
-
287
- def self.execute_hook(name)
288
- raise unless HOOKS.include?(name)
289
- _hooks(name).each(&:call)
290
- end
291
-
292
- def self.parse_hook_file(root)
293
- file = root.join(".spin.rb")
294
- load(file) if File.exist?(file)
295
- end
296
-
297
- private
298
-
299
- def self._hooks(name)
300
- @hooks ||= {}
301
- @hooks[name] ||= []
302
- @hooks[name]
303
- end
304
- end
305
-
306
- force_rspec = false
307
- force_testunit = false
308
- time = false
309
- push_results = false
310
- preload = "config/application.rb"
311
- options = OptionParser.new do |opts|
312
- opts.banner = usage
313
- opts.separator ""
314
- opts.separator "Server Options:"
315
-
316
- opts.on("-I", "--load-path=DIR#{File::PATH_SEPARATOR}DIR", "Appends directory to $LOAD_PATH") do |dirs|
317
- $LOAD_PATH.concat(dirs.split(File::PATH_SEPARATOR))
318
- end
319
-
320
- opts.on('--rspec', 'Force the selected test framework to RSpec') do |v|
321
- force_rspec = v
322
- end
323
-
324
- opts.on('--test-unit', 'Force the selected test framework to Test::Unit') do |v|
325
- force_testunit = v
326
- end
327
-
328
- opts.on('-t', '--time', 'See total execution time for each test run') do |v|
329
- time = true
330
- end
331
-
332
- opts.on('--push-results', 'Push test results to the push process') do |v|
333
- push_results = v
334
- end
335
-
336
- opts.on('--preload FILE', "Preload this file instead of #{preload}") do |v|
337
- preload = v
338
- end
339
-
340
- opts.separator "General Options:"
341
- opts.on('-e', 'Stub to keep kicker happy')
342
- opts.on('-h', '--help') do
343
- $stderr.puts opts
344
- exit 1
345
- end
346
- end
347
- options.parse!
348
-
349
- subcommand = ARGV.shift
350
- case subcommand
351
- when 'serve' then serve(force_rspec, force_testunit, time, push_results, preload)
352
- when 'push' then push(preload)
353
- else
354
- $stderr.puts options
355
- exit 1
356
- end
357
-
2
+ require 'spin/cli'
3
+ Spin::CLI.run(ARGV)
@@ -0,0 +1,289 @@
1
+ require 'spin/version'
2
+ require 'spin/hooks'
3
+ require 'socket'
4
+ require 'tempfile' # Dir.tmpdir
5
+ # This lets us hash the parameters we want to include in the filename
6
+ # without having to worry about subdirectories, special chars, etc.
7
+ require 'digest/md5'
8
+ # So we can tell users how much time they're saving by preloading their
9
+ # environment.
10
+ require 'benchmark'
11
+ require 'pathname'
12
+
13
+ module Spin
14
+ extend Spin::Hooks
15
+
16
+ PUSH_FILE_SEPARATOR = '|'
17
+
18
+ class << self
19
+ def serve(options)
20
+ ENV['RAILS_ENV'] = 'test' unless ENV['RAILS_ENV']
21
+
22
+ if root_path = rails_root(options[:preload])
23
+ Dir.chdir(root_path)
24
+ Spin.parse_hook_file(root_path)
25
+ else
26
+ warn "Could not find #{options[:preload]}. Are you running this from the root of a Rails project?"
27
+ end
28
+
29
+ open_socket do |socket|
30
+ preload(options) if root_path
31
+
32
+ puts "Pushing test results back to push processes" if options[:push_results]
33
+
34
+ loop do
35
+ run_pushed_tests(socket, options)
36
+ end
37
+ end
38
+ end
39
+
40
+ def push(argv, options)
41
+ files_to_load = convert_push_arguments_to_files(argv)
42
+
43
+ if root_path = rails_root(options[:preload])
44
+ make_files_relative(files_to_load, root_path)
45
+ Dir.chdir root_path
46
+ end
47
+
48
+ files_to_load << "tty?" if $stdout.tty?
49
+
50
+ abort if files_to_load.empty?
51
+
52
+ puts "Spinning up #{files_to_load.join(" ")}"
53
+ send_files_to_serve(files_to_load)
54
+ end
55
+
56
+ private
57
+
58
+ def send_files_to_serve(files_to_load)
59
+ # This is the other end of the socket that `spin serve` opens. At this point
60
+ # `spin serve` will accept(2) our connection.
61
+ socket = UNIXSocket.open(socket_file)
62
+
63
+ # We put the filenames on the socket for the server to read and then load.
64
+ socket.puts files_to_load.join(PUSH_FILE_SEPARATOR)
65
+
66
+ while line = socket.readpartial(100)
67
+ break if line[-1,1] == "\0"
68
+ print line
69
+ end
70
+ rescue Errno::ECONNREFUSED, Errno::ENOENT
71
+ abort "Connection was refused. Have you started up `spin serve` yet?"
72
+ end
73
+
74
+ # The filenames that we will spin up to `spin serve` are passed in as
75
+ # arguments.
76
+ def convert_push_arguments_to_files(argv)
77
+ files_to_load = argv
78
+
79
+ # We reject anything in ARGV that isn't a file that exists. This takes
80
+ # care of scripts that specify files like `spin push -r file.rb`. The `-r`
81
+ # bit will just be ignored.
82
+ #
83
+ # We build a string like `file1.rb|file2.rb` and pass it up to the server.
84
+ files_to_load = files_to_load.map do |file|
85
+ args = file.split(':')
86
+
87
+ file_name = args.first.to_s
88
+ line_number = args.last.to_i
89
+
90
+ # If the file exists then we can push it up just like it is
91
+ file_name = if File.exist?(file_name)
92
+ file_name
93
+ # kicker-2.5.0 now gives us file names without extensions, so we have to try adding it
94
+ elsif File.extname(file_name).empty?
95
+ full_file_name = [file_name, 'rb'].join('.')
96
+ full_file_name if File.exist?(full_file_name)
97
+ end
98
+
99
+ if line_number > 0
100
+ abort "You specified a line number. Only one file can be pushed in this case." if files_to_load.length > 1
101
+
102
+ "#{file_name}:#{line_number}"
103
+ else
104
+ file_name
105
+ end
106
+ end.reject(&:empty?).uniq
107
+ end
108
+
109
+ def make_files_relative(files_to_load, root_path)
110
+ files_to_load.map! do |file|
111
+ Pathname.new(file).expand_path.relative_path_from(root_path).to_s
112
+ end
113
+ end
114
+
115
+ def run_pushed_tests(socket, options)
116
+ rerun_last_tests_on_quit(options) unless options[:push_results]
117
+
118
+ # Since `spin push` reconnects each time it has new files for us we just
119
+ # need to accept(2) connections from it.
120
+ conn = socket.accept
121
+ # This should be a list of relative paths to files.
122
+ files = conn.gets.chomp
123
+ files = files.split(PUSH_FILE_SEPARATOR)
124
+
125
+ # If spin is started with the time flag we will track total execution so
126
+ # you can easily compare it with time rspec spec for example
127
+ start = Time.now if options[:time]
128
+
129
+ # If we're not sending results back to the push process, we can disconnect
130
+ # it immediately.
131
+ disconnect(conn) unless options[:push_results]
132
+
133
+ fork_and_run(files, conn, options)
134
+
135
+ # WAIT: We don't want the parent process handling multiple test runs at the same
136
+ # time because then we'd need to deal with multiple test databases, and
137
+ # that destroys the idea of being simple to use. So we wait(2) until the
138
+ # child process has finished running the test.
139
+ Process.wait
140
+
141
+ # If we are tracking time we will output it here after everything has
142
+ # finished running
143
+ puts "Total execution time was #{Time.now - start} seconds" if start
144
+
145
+ # Tests have now run. If we were pushing results to a push process, we can
146
+ # now disconnect it.
147
+ begin
148
+ disconnect(conn) if options[:push_results]
149
+ rescue Errno::EPIPE
150
+ # Don't abort if the client already disconnected
151
+ end
152
+ end
153
+
154
+ # Trap SIGQUIT (Ctrl+\) and re-run the last files that were pushed
155
+ # TODO test this
156
+ def rerun_last_tests_on_quit(options)
157
+ trap('QUIT') do
158
+ fork_and_run(@last_files_ran, nil, options)
159
+ Process.wait
160
+ end
161
+ end
162
+
163
+ def preload(options)
164
+ duration = Benchmark.realtime do
165
+ # We require config/application because that file (typically) loads Rails
166
+ # and any Bundler deps, as well as loading the initialization code for
167
+ # the app, but it doesn't actually perform the initialization. That happens
168
+ # in config/environment.
169
+ #
170
+ # In my experience that's the best we can do in terms of preloading. Rails
171
+ # and the gem dependencies rarely change and so don't need to be reloaded.
172
+ # But you can't initialize the application because any non-trivial app will
173
+ # involve it's models/controllers, etc. in its initialization, which you
174
+ # definitely don't want to preload.
175
+ execute_hook(:before_preload)
176
+ require File.expand_path options[:preload].sub('.rb', '')
177
+ execute_hook(:after_preload)
178
+
179
+ # Determine the test framework to use using the passed-in 'force' options
180
+ # or else default to checking for defined constants.
181
+ options[:test_framework] ||= determine_test_framework
182
+
183
+ # Preload RSpec to save some time on each test run
184
+ if options[:test_framework]
185
+ begin
186
+ require 'rspec/autorun'
187
+
188
+ # Tell RSpec it's running with a tty to allow colored output
189
+ if RSpec.respond_to?(:configure)
190
+ RSpec.configure do |c|
191
+ c.tty = true if c.respond_to?(:tty=)
192
+ end
193
+ end
194
+ rescue LoadError
195
+ end
196
+ end
197
+ end
198
+ # This is the amount of time that you'll save on each subsequent test run.
199
+ puts "Preloaded Rails env in #{duration}s..."
200
+ end
201
+
202
+ # This socket is how we communicate with `spin push`.
203
+ # We delete the tmp file for the Unix socket if it already exists. The file
204
+ # is scoped to the `pwd`, so if it already exists then it must be from an
205
+ # old run of `spin serve` and can be cleaned up.
206
+ def open_socket
207
+ file = socket_file
208
+ File.delete(file) if File.exist?(file)
209
+ socket = UNIXServer.open(file)
210
+
211
+ # Trap SIGINT (Ctrl-C) so that we exit cleanly.
212
+ trap('SIGINT') do
213
+ socket.close
214
+ exit
215
+ end
216
+
217
+ yield socket
218
+ ensure
219
+ File.delete(file) if file && File.exist?(file)
220
+ end
221
+
222
+ def determine_test_framework
223
+ if defined?(RSpec)
224
+ :rspec
225
+ else
226
+ :testunit
227
+ end
228
+ end
229
+
230
+ def disconnect(connection)
231
+ connection.print "\0"
232
+ connection.close
233
+ end
234
+
235
+ def rails_root(preload)
236
+ path = Pathname.pwd
237
+ until path.join(preload).file?
238
+ return if path.root?
239
+ path = path.parent
240
+ end
241
+ path
242
+ end
243
+
244
+ def fork_and_run(files, conn, options)
245
+ execute_hook(:before_fork)
246
+ # We fork(2) before loading the file so that our pristine preloaded
247
+ # environment is untouched. The child process will load whatever code it
248
+ # needs to, then it exits and we're back to the baseline preloaded app.
249
+ fork do
250
+ # To push the test results to the push process instead of having them
251
+ # displayed by the server, we reopen $stdout/$stderr to the open
252
+ # connection.
253
+ tty = files.delete "tty?"
254
+ if options[:push_results]
255
+ $stdout.reopen(conn)
256
+ if tty
257
+ def $stdout.tty?
258
+ true
259
+ end
260
+ end
261
+ $stderr.reopen(conn)
262
+ end
263
+
264
+ execute_hook(:after_fork)
265
+
266
+ puts
267
+ puts "Loading #{files.inspect}"
268
+
269
+ # Unfortunately rspec's interface isn't as simple as just requiring the
270
+ # test file that you want to run (suddenly test/unit seems like the less
271
+ # crazy one!).
272
+ if options[:test_framework] == :rspec
273
+ # We pretend the filepath came in as an argument and duplicate the
274
+ # behaviour of the `rspec` binary.
275
+ ARGV.concat files
276
+ else
277
+ # We require the full path of the file here in the child process.
278
+ files.each { |f| require File.expand_path f }
279
+ end
280
+ end
281
+ @last_files_ran = files
282
+ end
283
+
284
+ def socket_file
285
+ key = Digest::MD5.hexdigest [Dir.pwd, 'spin-gem'].join
286
+ [Dir.tmpdir, key].join('/')
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,54 @@
1
+ require 'spin'
2
+ require 'optparse'
3
+
4
+ module Spin
5
+ module CLI
6
+ class << self
7
+ def run(argv)
8
+ options = {
9
+ :preload => "config/application.rb"
10
+ }
11
+
12
+ parser = OptionParser.new do |opts|
13
+ opts.banner = usage
14
+ opts.separator ""
15
+ opts.separator "Server Options:"
16
+
17
+ opts.on("-I", "--load-path=DIR#{File::PATH_SEPARATOR}DIR", "Appends directory to $LOAD_PATH") do |dirs|
18
+ $LOAD_PATH.concat(dirs.split(File::PATH_SEPARATOR))
19
+ end
20
+
21
+ opts.on("--rspec", "Force the selected test framework to RSpec") { options[:test_framework] = :testunit }
22
+ opts.on("--test-unit", "Force the selected test framework to Test::Unit") { options[:test_framework] = :rspec }
23
+ opts.on("-t", "--time", "See total execution time for each test run") { options[:time] = true }
24
+ opts.on("--push-results", "Push test results to the push process") { options[:push_results] = true }
25
+ opts.on("--preload FILE", "Preload this file instead of #{options[:preload]}") { |v| options[:preload] = v }
26
+ opts.separator "General Options:"
27
+ opts.on("-e", "Stub to keep kicker happy")
28
+ opts.on("-v", "--version", "Show Version") { puts Spin::VERSION; exit }
29
+ opts.on("-h", "--help") { $stderr.puts opts; exit }
30
+ end
31
+ parser.parse!
32
+
33
+ subcommand = argv.shift
34
+ case subcommand
35
+ when "serve" then Spin.serve(options)
36
+ when "push" then Spin.push(argv, options)
37
+ else
38
+ $stderr.puts parser
39
+ exit 1
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def usage
46
+ <<-USAGE.gsub(/^\s{8}/,"")
47
+ Usage: spin serve
48
+ spin push <file> <file>...
49
+ Spin preloads your Rails environment to speed up your autotest(ish) workflow.
50
+ USAGE
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ module Spin
2
+ module Hooks
3
+ HOOKS = [:before_fork, :after_fork, :before_preload, :after_preload]
4
+
5
+ def hook(name, &block)
6
+ raise unless HOOKS.include?(name)
7
+ _hooks(name) << block
8
+ end
9
+
10
+ def execute_hook(name)
11
+ raise unless HOOKS.include?(name)
12
+ _hooks(name).each(&:call)
13
+ end
14
+
15
+ def parse_hook_file(root)
16
+ file = root.join(".spin.rb")
17
+ load(file) if File.exist?(file)
18
+ end
19
+
20
+ private
21
+
22
+ def _hooks(name)
23
+ @hooks ||= {}
24
+ @hooks[name] ||= []
25
+ @hooks[name]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Spin
2
+ VERSION = "0.6.0"
3
+ end
@@ -0,0 +1,234 @@
1
+
2
+ describe "Spin" do
3
+ before do
4
+ # kill all Threads that might be hanging around
5
+ Thread.list.each { |thread| thread.exit unless thread == Thread.current }
6
+ end
7
+
8
+ around do |example|
9
+ folder = File.expand_path("../tmp", __FILE__)
10
+ `rm -rf #{folder}`
11
+ ensure_folder folder
12
+ Dir.chdir folder do
13
+ example.call
14
+ end
15
+ `rm -rf #{folder}`
16
+ end
17
+
18
+ def root
19
+ File.expand_path '../..', __FILE__
20
+ end
21
+
22
+ def spin(command, options={})
23
+ command = spin_command(command)
24
+ result = `#{command}`
25
+ raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail]
26
+ result
27
+ end
28
+
29
+ def spin_command(command)
30
+ "ruby -I #{root}/lib #{root}/bin/spin #{command} 2>&1"
31
+ end
32
+
33
+ def record_serve(output, command)
34
+ IO.popen(spin_command("serve #{command}")) do |pipe|
35
+ while str = pipe.readpartial(100)
36
+ output << str
37
+ end rescue EOFError
38
+ end
39
+ end
40
+
41
+ def write(file, content)
42
+ ensure_folder File.dirname(file)
43
+ File.open(file, 'w'){|f| f.write content }
44
+ end
45
+
46
+ def read(file)
47
+ File.read file
48
+ end
49
+
50
+ def delete(file)
51
+ `rm #{file}`
52
+ end
53
+
54
+ def ensure_folder(folder)
55
+ `mkdir -p #{folder}` unless File.exist?(folder)
56
+ end
57
+
58
+ def serve_and_push(serve_command, push_commands)
59
+ serve_output = ""
60
+ t1 = Thread.new { record_serve(serve_output, serve_command) }
61
+ sleep 0.1
62
+ push_output = [*push_commands].map{ |cmd| spin("push #{cmd}") }
63
+ sleep 0.2
64
+ t1.kill
65
+ [serve_output, push_output]
66
+ end
67
+
68
+ context "with simple setup" do
69
+ before do
70
+ write "config/application.rb", "$xxx = 1234"
71
+ write "test/foo_test.rb", "puts $xxx * 2"
72
+ @default_pushed = "Spinning up test/foo_test.rb\n"
73
+ end
74
+
75
+ it "shows help when no arguments are given" do
76
+ spin("", :fail => true).should include("General Options:")
77
+ end
78
+
79
+ it "can serve and push" do
80
+ served, pushed = serve_and_push("", "test/foo_test.rb")
81
+ served.should include "Preloaded Rails env in "
82
+ served.should include "2468"
83
+ pushed.first.should == @default_pushed
84
+ end
85
+
86
+ it "can run files without .rb extension" do
87
+ served, pushed = serve_and_push("", "test/foo_test")
88
+ served.should include "Preloaded Rails env in "
89
+ served.should include "2468"
90
+ pushed.first.should == @default_pushed
91
+ end
92
+
93
+ it "can run multiple times" do
94
+ write "test/foo_test.rb", "puts $xxx *= 2"
95
+ served, pushed = serve_and_push("", ["test/foo_test.rb", "test/foo_test.rb", "test/foo_test.rb"])
96
+ served.should include "Preloaded Rails env in "
97
+ served.scan("2468").size.should == 3
98
+ pushed.size.should == 3
99
+ pushed.each{|x| x.should == @default_pushed }
100
+ end
101
+
102
+ it "can run multiple files at once" do
103
+ write "test/bar_test.rb", "puts $xxx / 2"
104
+ served, pushed = serve_and_push("", "test/foo_test.rb test/bar_test.rb")
105
+ served.should include "Preloaded Rails env in "
106
+ served.should include "2468"
107
+ served.should include "617"
108
+ pushed.first.should == "Spinning up test/foo_test.rb test/bar_test.rb\n"
109
+ end
110
+
111
+ it "complains when the preloaded file cannot be found" do
112
+ delete "config/application.rb"
113
+ write "test/foo_test.rb", "puts 2468"
114
+ served, pushed = serve_and_push("", "test/foo_test.rb")
115
+ served.should_not include "Preloaded Rails env in "
116
+ served.should include "Could not find config/application.rb. Are you running"
117
+ served.should include "2468"
118
+ pushed.first.should == @default_pushed
119
+ end
120
+
121
+ context "RSpec" do
122
+ before do
123
+ write "config/application.rb", "module RSpec;end"
124
+ end
125
+
126
+ it "can run files" do
127
+ write "spec/foo_spec.rb", "RSpec.configure{}; puts 'YES'"
128
+ served, pushed = serve_and_push("", "spec/foo_spec.rb")
129
+ served.should include "YES"
130
+ end
131
+
132
+ it "can run by line" do
133
+ write "spec/foo_spec.rb", <<-RUBY
134
+ describe "x" do
135
+ it("a"){ puts "AAA" }
136
+ it("b"){ puts "BBB" }
137
+ it("c"){ puts "CCC" }
138
+ end
139
+ RUBY
140
+ served, pushed = serve_and_push("", "spec/foo_spec.rb:3")
141
+ served.should_not include "AAA"
142
+ served.should include "BBB"
143
+ served.should_not include "CCC"
144
+ end
145
+ end
146
+
147
+ context "options" do
148
+ it "can show current version" do
149
+ spin("--version").should =~ /^\d+\.\d+\.\d+/
150
+ end
151
+
152
+ it "can show help" do
153
+ spin("--help").should include("General Options:")
154
+ end
155
+
156
+ it "can --push-results" do
157
+ served, pushed = serve_and_push("--push-results", "test/foo_test.rb")
158
+ served.should include "Preloaded Rails env in "
159
+ served.should_not include "2468"
160
+ pushed.first.should include "2468"
161
+ end
162
+
163
+ it "can --preload a different file" do
164
+ write "config/application.rb", "raise"
165
+ write "config/environment.rb", "$xxx = 1234"
166
+ served, pushed = serve_and_push("--preload config/environment.rb", "test/foo_test.rb")
167
+ served.should include "Preloaded Rails env in "
168
+ served.should include "2468"
169
+ pushed.first.should == @default_pushed
170
+ end
171
+
172
+ it "can add load paths via -I" do
173
+ write "lib/bar.rb", "puts 'bar'"
174
+ write "test/foo_test.rb", "require 'bar'"
175
+ served, pushed = serve_and_push("-Itest:lib", "test/foo_test.rb")
176
+ served.should include "bar"
177
+ pushed.first.should == @default_pushed
178
+ end
179
+
180
+ it "ignores -e" do
181
+ served, pushed = serve_and_push("-e", "test/foo_test.rb -e")
182
+ served.should include "Preloaded Rails env in "
183
+ served.should include "2468"
184
+ pushed.first.should == @default_pushed
185
+ end
186
+
187
+ # TODO process never reaches after the fork block with only 1 push command
188
+ it "can show total execution time" do
189
+ served, pushed = serve_and_push("--time", ["test/foo_test.rb", "test/foo_test.rb"])
190
+ served.should include "Total execution time was 0."
191
+ pushed.first.should == @default_pushed
192
+ end
193
+ end
194
+
195
+ context "hooks" do
196
+ before do
197
+ write "config/application.rb", "$calls << :real_preload"
198
+ write "test/calls_test.rb", "puts '>>' + $calls.inspect + '<<'"
199
+ end
200
+
201
+ it "calls preload hooks in correct order" do
202
+ write ".spin.rb", <<-RUBY
203
+ $calls = []
204
+ [:before_preload, :after_preload].each do |hook|
205
+ Spin.hook(hook) { $calls << hook }
206
+ end
207
+ RUBY
208
+ served, pushed = serve_and_push("--time", "test/calls_test.rb")
209
+ served[/>>.*<</].should == ">>[:before_preload, :real_preload, :after_preload]<<"
210
+ end
211
+
212
+ it "can have multiple hooks" do
213
+ write ".spin.rb", <<-RUBY
214
+ $calls = []
215
+ Spin.hook(:before_preload) { $calls << :before_preload_1 }
216
+ Spin.hook(:before_preload) { $calls << :before_preload_2 }
217
+ RUBY
218
+ served, pushed = serve_and_push("--time", "test/calls_test.rb")
219
+ served[/>>.*<</].should == ">>[:before_preload_1, :before_preload_2, :real_preload]<<"
220
+ end
221
+
222
+ it "can hook before/after fork" do
223
+ write ".spin.rb", <<-RUBY
224
+ $calls = []
225
+ [:before_fork, :after_fork].each do |hook|
226
+ Spin.hook(hook) { $calls << hook }
227
+ end
228
+ RUBY
229
+ served, pushed = serve_and_push("--time", "test/calls_test.rb")
230
+ served[/>>.*<</].should == ">>[:real_preload, :before_fork, :after_fork]<<"
231
+ end
232
+ end
233
+ end
234
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-27 00:00:00.000000000 Z
12
+ date: 2013-02-13 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ! 'Spin preloads your Rails environment to speed up your autotest(ish)
15
15
  workflow.
@@ -24,8 +24,13 @@ executables:
24
24
  extensions: []
25
25
  extra_rdoc_files: []
26
26
  files:
27
- - !binary |-
28
- YmluL3NwaW4=
27
+ - README.md
28
+ - lib/spin/cli.rb
29
+ - lib/spin/hooks.rb
30
+ - lib/spin/version.rb
31
+ - lib/spin.rb
32
+ - spec/integration_spec.rb
33
+ - bin/spin
29
34
  homepage: http://jstorimer.github.com/spin
30
35
  licenses: []
31
36
  post_install_message:
@@ -46,8 +51,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
51
  version: '0'
47
52
  requirements: []
48
53
  rubyforge_project:
49
- rubygems_version: 1.8.11
54
+ rubygems_version: 1.8.25
50
55
  signing_key:
51
56
  specification_version: 3
52
57
  summary: Spin preloads your Rails environment to speed up your autotest(ish) workflow.
53
- test_files: []
58
+ test_files:
59
+ - spec/integration_spec.rb