bmabey-spork 0.4.4 → 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/README.rdoc +51 -23
  2. data/assets/bootstrap.rb +1 -1
  3. data/bin/spork +0 -0
  4. data/features/cucumber_rails_integration.feature +113 -0
  5. data/features/diagnostic_mode.feature +41 -0
  6. data/features/rails_delayed_loading_workarounds.feature +80 -0
  7. data/features/rspec_rails_integration.feature +90 -0
  8. data/features/steps/rails_steps.rb +53 -0
  9. data/features/steps/sandbox_steps.rb +98 -0
  10. data/features/support/env.rb +98 -0
  11. data/features/unknown_app_framework.feature +42 -0
  12. data/lib/spork.rb +85 -26
  13. data/lib/spork/app_framework.rb +74 -0
  14. data/lib/spork/app_framework/rails.rb +158 -0
  15. data/lib/spork/app_framework/rails_stub_files/application.rb +3 -0
  16. data/lib/spork/app_framework/rails_stub_files/application_controller.rb +3 -0
  17. data/lib/spork/app_framework/rails_stub_files/application_helper.rb +3 -0
  18. data/lib/spork/app_framework/unknown.rb +6 -0
  19. data/lib/spork/custom_io_streams.rb +25 -0
  20. data/lib/spork/diagnoser.rb +103 -0
  21. data/lib/spork/forker.rb +70 -0
  22. data/lib/spork/runner.rb +28 -10
  23. data/lib/spork/server.rb +88 -46
  24. data/lib/spork/server/cucumber.rb +21 -9
  25. data/lib/spork/server/rspec.rb +1 -1
  26. data/spec/spec_helper.rb +93 -35
  27. data/spec/spork/app_framework/rails_spec.rb +22 -0
  28. data/spec/spork/app_framework/unknown_spec.rb +12 -0
  29. data/spec/spork/app_framework_spec.rb +16 -0
  30. data/spec/spork/diagnoser_spec.rb +105 -0
  31. data/spec/spork/forker_spec.rb +44 -0
  32. data/spec/spork/runner_spec.rb +2 -2
  33. data/spec/spork/server/cucumber_spec.rb +25 -0
  34. data/spec/spork/server/rspec_spec.rb +17 -3
  35. data/spec/spork/server_spec.rb +32 -17
  36. data/spec/spork_spec.rb +112 -13
  37. metadata +33 -3
@@ -0,0 +1,3 @@
1
+ # This is a stub used to help Spork delay the loading of the real ApplicationController
2
+ class ::ApplicationController < ActionController::Base
3
+ end
@@ -0,0 +1,3 @@
1
+ # This is a stub used to help Spork delay the loading of the real ApplicationController
2
+ class ::ApplicationController < ActionController::Base
3
+ end
@@ -0,0 +1,3 @@
1
+ # This is a stub used to help Spork delay the loading of the real ApplicationHelper
2
+ module ::ApplicationHelper
3
+ end
@@ -0,0 +1,6 @@
1
+ # This is used if no supported appliction framework is detected
2
+ class Spork::AppFramework::Unknown < Spork::AppFramework
3
+ def entry_point
4
+ nil
5
+ end
6
+ end
@@ -0,0 +1,25 @@
1
+ # This class is mainly used for testing.
2
+ # When included (and used), it gives us an opportunity to stub out the output streams used for a given class
3
+ module Spork::CustomIOStreams
4
+ def self.included(klass)
5
+ klass.send(:extend, ::Spork::CustomIOStreams::ClassMethods)
6
+ end
7
+
8
+ def stderr
9
+ self.class.stderr
10
+ end
11
+
12
+ def stdout
13
+ self.class.stdout
14
+ end
15
+
16
+ module ClassMethods
17
+ def stderr
18
+ $stderr
19
+ end
20
+
21
+ def stdout
22
+ $stdout
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,103 @@
1
+ # The Diagnoser hooks into load and require and keeps track of when files are required / loaded, and who loaded them.
2
+ # It's used when you run spork --diagnose
3
+ #
4
+ # = Example
5
+ #
6
+ # Spork::Diagnoser.install_hook!('/path/env.rb', '/path')
7
+ # require '/path/to/env.rb'
8
+ # Spork::Diagnoser.output_results(STDOUT)
9
+ class Spork::Diagnoser
10
+ class << self
11
+ def loaded_files
12
+ @loaded_files ||= {}
13
+ end
14
+
15
+ # Installs the diagnoser hook into Kernel#require and Kernel#load
16
+ #
17
+ # == Parameters
18
+ #
19
+ # * +entry_file+ - The file that is used to load the project. Used to filter the backtrace so anything that happens after it is hidden.
20
+ # * +dir+ - The project directory. Any file loaded outside of this directory will not be logged.
21
+ def install_hook!(entry_file = nil, dir = Dir.pwd)
22
+ @dir = File.expand_path(Dir.pwd, dir)
23
+ @entry_file = entry_file
24
+
25
+ Kernel.class_eval do
26
+ alias :require_without_diagnoser :require
27
+ alias :load_without_diagnoser :load
28
+
29
+ def require(string)
30
+ ::Spork::Diagnoser.add_included_file(string, caller)
31
+ require_without_diagnoser(string)
32
+ end
33
+
34
+ def load(string)
35
+ ::Spork::Diagnoser.add_included_file(string, caller)
36
+ load_without_diagnoser(string)
37
+ end
38
+ end
39
+ end
40
+
41
+ def add_included_file(filename, callstack)
42
+ filename = expand_filename(filename)
43
+ return unless File.exist?(filename)
44
+ loaded_files[filename] = filter_callstack(caller) if subdirectory?(filename)
45
+ end
46
+
47
+ # Uninstall the hook. Generally useful only for testing the Diagnoser.
48
+ def remove_hook!
49
+ return unless Kernel.private_instance_methods.include?('require_without_diagnoser')
50
+ Kernel.class_eval do
51
+ alias :require :require_without_diagnoser
52
+ alias :load :load_without_diagnoser
53
+
54
+ undef_method(:require_without_diagnoser)
55
+ undef_method(:load_without_diagnoser)
56
+ end
57
+ true
58
+ end
59
+
60
+ # output the results of a diagnostic run.
61
+ #
62
+ # == Parameters
63
+ #
64
+ # * +stdout+ - An IO stream to output the results to.
65
+ def output_results(stdout)
66
+ project_prefix = Dir.pwd + "/"
67
+ minimify = lambda { |f| f.gsub(project_prefix, '')}
68
+ stdout.puts "- Spork Diagnosis -\n"
69
+ stdout.puts "-- Summary --"
70
+ stdout.puts loaded_files.keys.sort.map(&minimify)
71
+ stdout.puts "\n\n\n"
72
+ stdout.puts "-- Detail --"
73
+ loaded_files.keys.sort.each do |file|
74
+ stdout.puts "\n\n\n--- #{minimify.call(file)} ---\n"
75
+ stdout.puts loaded_files[file].map(&minimify)
76
+ end
77
+ end
78
+
79
+ private
80
+ def filter_callstack(callstack, entry_file = @entry_file)
81
+ callstack.pop until callstack.empty? || callstack.last.include?(@entry_file) if @entry_file
82
+ callstack.map do |line|
83
+ next if line.include?('lib/spork/diagnoser.rb')
84
+ line.gsub!('require_without_diagnoser', 'require')
85
+ line
86
+ end.compact
87
+ end
88
+
89
+ def expand_filename(filename)
90
+ ([Dir.pwd] + $:).each do |attempted_path|
91
+ attempted_filename = File.expand_path(filename, attempted_path)
92
+ return attempted_filename if File.file?(attempted_filename)
93
+ attempted_filename = attempted_filename + ".rb"
94
+ return attempted_filename if File.file?(attempted_filename)
95
+ end
96
+ filename
97
+ end
98
+
99
+ def subdirectory?(directory)
100
+ File.expand_path(directory, Dir.pwd).include?(@dir)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,70 @@
1
+ # A helper class that allows you to run a block inside of a fork, and then get the result from that block.
2
+ #
3
+ # == Example:
4
+ #
5
+ # forker = Spork::Forker.new do
6
+ # sleep 3
7
+ # "success"
8
+ # end
9
+ #
10
+ # forker.result # => "success"
11
+ class Spork::Forker
12
+
13
+ # Raised if the fork died (was killed) before it sent it's response back.
14
+ class ForkDiedException < Exception; end
15
+ def initialize(&block)
16
+ return unless block_given?
17
+ @child_io, @server_io = UNIXSocket.socketpair
18
+ @child_pid = Kernel.fork do
19
+ @server_io.close
20
+ Marshal.dump(yield, @child_io)
21
+ # wait for the parent to acknowledge receipt of the result.
22
+ master_response =
23
+ begin
24
+ Marshal.load(@child_io)
25
+ rescue EOFError
26
+ nil
27
+ end
28
+
29
+ # terminate, skipping any at_exit blocks.
30
+ exit!(0)
31
+ end
32
+ @child_io.close
33
+ end
34
+
35
+ # Wait for the fork to finish running, and then return its return value.
36
+ #
37
+ # If the fork was aborted, then result returns nil.
38
+ def result
39
+ return unless running?
40
+ result_thread = Thread.new do
41
+ begin
42
+ @result = Marshal.load(@server_io)
43
+ Marshal.dump('ACK', @server_io)
44
+ rescue ForkDiedException
45
+ @result = nil
46
+ end
47
+ end
48
+ Process.wait(@child_pid)
49
+ result_thread.raise(ForkDiedException) if @result.nil?
50
+ @child_pid = nil
51
+ @result
52
+ end
53
+
54
+ # abort the current running fork
55
+ def abort
56
+ if running?
57
+ Process.kill(Signal.list['TERM'], @child_pid)
58
+ @child_pid = nil
59
+ true
60
+ end
61
+ end
62
+
63
+ def running?
64
+ return false unless @child_pid
65
+ Process.getpgid(@child_pid)
66
+ true
67
+ rescue Errno::ESRCH
68
+ false
69
+ end
70
+ end
@@ -2,6 +2,7 @@ require 'optparse'
2
2
  require 'spork/server'
3
3
 
4
4
  module Spork
5
+ # This is used by bin/spork. It's wrapped in a class because it's easier to test that way.
5
6
  class Runner
6
7
  attr_reader :server
7
8
 
@@ -19,7 +20,9 @@ module Spork
19
20
 
20
21
  opt.separator "Options:"
21
22
  opt.on("-b", "--bootstrap") {|ignore| @options[:bootstrap] = true }
23
+ opt.on("-d", "--diagnose") {|ignore| @options[:diagnose] = true }
22
24
  opt.on("-h", "--help") {|ignore| @options[:help] = true }
25
+ opt.on("-p", "--port [PORT]") {|port| @options[:port] = port }
23
26
  non_option_args = args.select { |arg| ! args[0].match(/^-/) }
24
27
  @options[:server_matcher] = non_option_args[0]
25
28
  opt.parse!(args)
@@ -41,11 +44,12 @@ module Spork
41
44
  text.string
42
45
  end
43
46
 
47
+ # Returns a server for the specified (or the detected default) testing framework. Returns nil if none detected, or if the specified is not supported or available.
44
48
  def find_server
45
49
  if options[:server_matcher]
46
50
  @server = Spork::Server.supported_servers(options[:server_matcher]).first
47
51
  unless @server
48
- @output.puts <<-ERROR
52
+ @error.puts <<-ERROR
49
53
  #{options[:server_matcher].inspect} didn't match a supported test framework.
50
54
 
51
55
  #{supported_servers_text}
@@ -54,7 +58,7 @@ module Spork
54
58
  end
55
59
 
56
60
  unless @server.available?
57
- @output.puts <<-USEFUL_ERROR
61
+ @error.puts <<-USEFUL_ERROR
58
62
  I can't find the helper file #{@server.helper_file} for the #{@server.server_name} testing framework.
59
63
  Are you running me from the project directory?
60
64
  USEFUL_ERROR
@@ -63,7 +67,7 @@ Are you running me from the project directory?
63
67
  else
64
68
  @server = Spork::Server.available_servers.first
65
69
  if @server.nil?
66
- @output.puts <<-USEFUL_ERROR
70
+ @error.puts <<-USEFUL_ERROR
67
71
  I can't find any testing frameworks to use.
68
72
  Are you running me from a project directory?
69
73
  USEFUL_ERROR
@@ -76,14 +80,28 @@ Are you running me from a project directory?
76
80
  def run
77
81
  return false unless find_server
78
82
  ENV["DRB"] = 'true'
79
- ENV["RAILS_ENV"] ||= 'test' if server.using_rails?
80
- @output.puts "Using #{server.server_name}"
81
- return server.bootstrap if options[:bootstrap]
82
- return(false) unless server.preload
83
- server.run
84
- return true
85
- end
83
+ @error.puts "Using #{server.server_name}"
84
+ @error.flush
85
+
86
+ server.port = options[:port]
86
87
 
88
+ case
89
+ when options[:bootstrap]
90
+ server.bootstrap
91
+ when options[:diagnose]
92
+ require 'spork/diagnoser'
93
+
94
+ Spork::Diagnoser.install_hook!(server.entry_point)
95
+ server.preload
96
+ Spork::Diagnoser.output_results(@output)
97
+ return true
98
+ else
99
+ return(false) unless server.preload
100
+ server.run
101
+ return true
102
+ end
103
+ end
104
+
87
105
  private
88
106
  attr_reader :options
89
107
 
@@ -1,33 +1,41 @@
1
1
  require 'drb/drb'
2
2
  require 'rbconfig'
3
+ require 'spork/forker.rb'
4
+ require 'spork/custom_io_streams.rb'
5
+ require 'spork/app_framework.rb'
3
6
 
4
- # This is based off of spec_server.rb from rspec-rails (David Chelimsky), which was based on Florian Weber's TDDMate
7
+ # An abstract class that is implemented to create a server
8
+ #
9
+ # (This was originally based off of spec_server.rb from rspec-rails (David Chelimsky), which was based on Florian Weber's TDDMate)
5
10
  class Spork::Server
6
11
  @@supported_servers = []
7
12
 
8
13
  LOAD_PREFERENCE = ['RSpec', 'Cucumber']
9
14
  BOOTSTRAP_FILE = File.dirname(__FILE__) + "/../../assets/bootstrap.rb"
10
15
 
16
+ include Spork::CustomIOStreams
17
+
18
+ # Abstract method: returns the servers port. Override this to return the port that should be used by the test framework.
11
19
  def self.port
12
20
  raise NotImplemented
13
21
  end
14
22
 
23
+ # Abstract method: returns the entry file that loads the testing environment, such as spec/spec_helper.rb.
15
24
  def self.helper_file
16
25
  raise NotImplemented
17
26
  end
18
27
 
28
+ # Convenience method that turns the class name without the namespace
19
29
  def self.server_name
20
30
  self.name.gsub('Spork::Server::', '')
21
31
  end
22
32
 
23
- def self.inherited(subclass)
24
- @@supported_servers << subclass
25
- end
26
-
33
+ # Returns a list of all testing servers that have detected their testing framework being used in the project.
27
34
  def self.available_servers
28
35
  supported_servers.select { |s| s.available? }
29
36
  end
30
37
 
38
+ # Returns a list of all servers that have been implemented (it keeps track of them automatically via Class.inherited)
31
39
  def self.supported_servers(starting_with = nil)
32
40
  @@supported_servers.sort! { |a,b| a.load_preference_index <=> b.load_preference_index }
33
41
  return @@supported_servers if starting_with.nil?
@@ -36,28 +44,28 @@ class Spork::Server
36
44
  end
37
45
  end
38
46
 
47
+ # Returns true if the testing frameworks helper file exists. Override if this is not sufficient to detect your testing framework.
39
48
  def self.available?
40
49
  File.exist?(helper_file)
41
50
  end
42
51
 
52
+ # Used to specify
43
53
  def self.load_preference_index
44
54
  LOAD_PREFERENCE.index(server_name) || LOAD_PREFERENCE.length
45
55
  end
46
56
 
47
- def self.using_rails?
48
- File.exist?("config/environment.rb")
49
- end
50
-
57
+ # Detects if the test helper has been bootstrapped.
51
58
  def self.bootstrapped?
52
59
  File.read(helper_file).include?("Spork.prefork")
53
60
  end
54
61
 
62
+ # Bootstraps the current test helper file by prepending a Spork.prefork and Spork.each_run block at the beginning.
55
63
  def self.bootstrap
56
64
  if bootstrapped?
57
- puts "Already bootstrapped!"
65
+ stderr.puts "Already bootstrapped!"
58
66
  return
59
67
  end
60
- puts "Bootstrapping #{helper_file}."
68
+ stderr.puts "Bootstrapping #{helper_file}."
61
69
  contents = File.read(helper_file)
62
70
  bootstrap_code = File.read(BOOTSTRAP_FILE)
63
71
  File.open(helper_file, "wb") do |f|
@@ -65,7 +73,7 @@ class Spork::Server
65
73
  f.puts contents
66
74
  end
67
75
 
68
- puts "Done. Edit #{helper_file} now with your favorite text editor and follow the instructions."
76
+ stderr.puts "Done. Edit #{helper_file} now with your favorite text editor and follow the instructions."
69
77
  true
70
78
  end
71
79
 
@@ -74,65 +82,101 @@ class Spork::Server
74
82
  new.listen
75
83
  end
76
84
 
85
+ # Sets up signals and starts the DRb service. If it's successful, it doesn't return. Not ever. You don't need to override this.
77
86
  def listen
78
87
  trap("SIGINT") { sig_int_received }
79
88
  trap("SIGTERM") { abort; exit!(0) }
80
89
  trap("USR2") { abort; restart } if Signal.list.has_key?("USR2")
81
90
  DRb.start_service("druby://127.0.0.1:#{port}", self)
82
- puts "Spork is ready and listening on #{port}!"
91
+ stderr.puts "Spork is ready and listening on #{port}!"
92
+ stderr.flush
83
93
  DRb.thread.join
84
94
  end
85
95
 
86
96
  def port
87
- self.class.port
97
+ self.class.instance_variable_get("@port") || self.class.port
98
+ end
99
+
100
+ def self.port= p
101
+ @port = p
88
102
  end
89
103
 
90
104
  def helper_file
91
105
  self.class.helper_file
92
106
  end
93
107
 
108
+ # This is the public facing method that is served up by DRb. To use it from the client side (in a testing framework):
109
+ #
110
+ # DRb.start_service("druby://localhost:0") # this allows Ruby to do some magical stuff so you can pass an output stream over DRb.
111
+ # # see http://redmine.ruby-lang.org/issues/show/496 to see why localhost:0 is used.
112
+ # spec_server = DRbObject.new_with_uri("druby://127.0.0.1:8989")
113
+ # spec_server.run(options.argv, $stderr, $stdout)
114
+ #
115
+ # When implementing a test server, don't override this method: override run_tests instead.
94
116
  def run(argv, stderr, stdout)
95
- return false if running?
96
- $stdout = stdout
97
- $stderr = stderr
98
- @child_pid = Kernel.fork do
99
- Spork.exec_each_run(helper_file)
117
+ abort if running?
118
+
119
+ @child = ::Spork::Forker.new do
120
+ $stdout, $stderr = stdout, stderr
121
+ Spork.exec_each_run { load helper_file }
100
122
  run_tests(argv, stderr, stdout)
101
123
  end
102
- Process.wait(@child_pid)
103
- @child_pid = nil
104
- true
124
+ @child.result
105
125
  end
106
126
 
127
+ # returns whether or not the child (a test run) is running right now.
107
128
  def running?
108
- !! @child_pid
129
+ @child && @child.running?
109
130
  end
110
131
 
132
+ protected
133
+ # Abstract method: here is where the server runs the tests.
134
+ def run_tests(argv, input, output)
135
+ raise NotImplemented
136
+ end
137
+
111
138
  private
139
+ def self.inherited(subclass)
140
+ @@supported_servers << subclass
141
+ end
142
+
143
+ def self.framework
144
+ @framework ||= Spork::AppFramework.detect_framework
145
+ end
146
+
147
+ def self.entry_point
148
+ bootstrapped? ? helper_file : framework.entry_point
149
+ end
150
+
112
151
  def self.preload
113
- if bootstrapped?
114
- puts "Loading Spork.prefork block..."
115
- Spork.exec_prefork(helper_file)
116
- else
117
- puts "#{helper_file} has not been sporked. Run spork --bootstrap to do so."
118
- # are we in a rails app?
119
- if using_rails?
120
- puts "Preloading Rails environment"
121
- require "config/environment.rb"
122
- else
123
- puts "There's nothing I can really do for you. Bailing."
124
- return false
152
+ Spork.exec_prefork do
153
+ unless bootstrapped?
154
+ stderr.puts "#{helper_file} has not been bootstrapped. Run spork --bootstrap to do so."
155
+ stderr.flush
156
+
157
+ if framework.bootstrap_required?
158
+ stderr.puts "I can't do anything for you by default for the framework your using: #{framework.short_name}.\nYou must bootstrap #{helper_file} to continue."
159
+ stderr.flush
160
+ return false
161
+ else
162
+ load(framework.entry_point)
163
+ end
125
164
  end
165
+
166
+ framework.preload do
167
+ if bootstrapped?
168
+ stderr.puts "Loading Spork.prefork block..."
169
+ stderr.flush
170
+ load(helper_file)
171
+ end
172
+ end
126
173
  end
127
174
  true
128
175
  end
129
176
 
130
- def run_tests(argv, input, output)
131
- raise NotImplemented
132
- end
133
-
134
177
  def restart
135
- puts "restarting"
178
+ stderr.puts "restarting"
179
+ stderr.flush
136
180
  config = ::Config::CONFIG
137
181
  ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT']
138
182
  command_line = [ruby, $0, ARGV].flatten.join(' ')
@@ -140,20 +184,18 @@ class Spork::Server
140
184
  end
141
185
 
142
186
  def abort
143
- if running?
144
- Process.kill(Signal.list['TERM'], @child_pid)
145
- true
146
- end
187
+ @child && @child.abort
147
188
  end
148
189
 
149
190
  def sig_int_received
150
191
  if running?
151
192
  abort
152
- puts "Running specs stopped. Press CTRL-C again to stop the server."
193
+ stderr.puts "Running tests stopped. Press CTRL-C again to stop the server."
194
+ stderr.flush
153
195
  else
154
196
  exit!(0)
155
197
  end
156
198
  end
157
199
  end
158
200
 
159
- Dir[File.dirname(__FILE__) + "/server/*.rb"].each { |file| require file }
201
+ Dir[File.dirname(__FILE__) + "/server/*.rb"].each { |file| require file }