spork 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ Feature: Spork Debugger integration
2
+ As a developer
3
+ I want to invoke the debugger my specs within Spork
4
+ In order to drill in and figure out what's wrong
5
+
6
+ Background: Rails App with RSpec and Spork
7
+
8
+ Given a file named "spec/spec_helper.rb" with:
9
+ """
10
+ require 'rubygems'
11
+ require 'spork'
12
+ require 'spork/ext/ruby-debug'
13
+
14
+ Spork.prefork do
15
+ require 'spec'
16
+ end
17
+
18
+ Spork.each_run do
19
+ end
20
+ """
21
+
22
+ Scenario: Invoking the debugger via 'debugger'
23
+ Given a file named "spec/debugger_spec.rb" with:
24
+ """
25
+ require File.dirname(__FILE__) + '/spec_helper.rb'
26
+
27
+ describe "Debugger" do
28
+ it "should debug" do
29
+ @variable = "variable contents"
30
+ debugger
31
+ puts "it worked!"
32
+ end
33
+ end
34
+ """
35
+
36
+ When I fire up a spork instance with "spork rspec"
37
+ And I run this in the background: spec --drb spec/debugger_spec.rb
38
+
39
+ Then the spork window should output a line containing "Debug Session Started"
40
+
41
+ When I type this in the spork window: "e @variable"
42
+ Then the spork window should output a line containing "variable contents"
43
+
44
+ When I type this in the spork window: "continue"
45
+
46
+ Then the spork window should output a line containing "Debug Session Terminated"
47
+ And the output should contain "it worked!"
@@ -33,25 +33,22 @@ Given /^the following code appears in "([^\"]*)" after \/([^\\\/]*)\/:$/ do |fil
33
33
  end
34
34
  end
35
35
 
36
- When /^I run (spork|spec|cucumber)($| .*$)/ do |command, spork_opts|
37
- case command
38
- when 'spork'
39
- command = SporkWorld::BINARY
40
- when 'cucumber'
41
- command = Cucumber::BINARY
42
- else
43
- command = %x{which #{command}}.chomp
44
- end
45
- run "#{SporkWorld::RUBY_BINARY} -I #{Cucumber::LIBDIR} #{command} #{spork_opts}"
36
+ When /^I run (spork|spec|cucumber)(| .*)$/ do |command, args|
37
+ run(localized_command(command, args))
38
+ end
39
+
40
+ When /^I run this in the background: (spork|spec|cucumber)(| .*)$/ do |command, args|
41
+ @background_script = run_in_background(localized_command(command, args))
46
42
  end
47
43
 
48
44
  When /^I fire up a spork instance with "spork(.*)"$/ do |spork_opts|
49
- run_in_background "#{SporkWorld::RUBY_BINARY} -I #{Cucumber::LIBDIR} #{SporkWorld::BINARY} #{spork_opts}"
45
+ @spork_server = run_in_background("#{SporkWorld::RUBY_BINARY} -I #{Cucumber::LIBDIR} #{SporkWorld::BINARY} #{spork_opts}")
46
+
50
47
  output = ""
51
48
  begin
52
49
  status = Timeout::timeout(15) do
53
50
  # Something that should be interrupted if it takes too much time...
54
- while line = @bg_stderr.gets
51
+ while line = @spork_server.stderr.gets
55
52
  output << line
56
53
  puts line
57
54
  break if line.include?("Spork is ready and listening")
@@ -63,7 +60,29 @@ When /^I fire up a spork instance with "spork(.*)"$/ do |spork_opts|
63
60
  end
64
61
  end
65
62
 
66
- Then /the file "([^\"]*)" should include "([^\"]*)"/ do |filename, content|
63
+ Then /^the spork window should output a line containing "(.+)"/ do |expected|
64
+ output = ""
65
+ begin
66
+ status = Timeout::timeout(5) do
67
+ # Something that should be interrupted if it takes too much time...
68
+ while line = @spork_server.stdout.gets
69
+ output << line
70
+ puts line
71
+ break if output.include?(expected)
72
+ end
73
+ end
74
+ rescue Timeout::Error
75
+ output.should include(expected)
76
+ end
77
+ end
78
+
79
+ When /^I type this in the spork window: "(.+)"/ do |line|
80
+ @spork_server.stdin.puts(line)
81
+ @spork_server.stdin.flush
82
+ end
83
+
84
+
85
+ Then /^the file "([^\"]*)" should include "([^\"]*)"$/ do |filename, content|
67
86
  in_current_dir do
68
87
  File.read(filename).should include(content)
69
88
  end
@@ -0,0 +1,63 @@
1
+ class BackgroundJob
2
+ attr_reader :stdin, :stdout, :stderr, :pid
3
+ def initialize(pid, stdin, stdout, stderr)
4
+ @pid, @stdin, @stdout, @stderr = pid, stdin, stdout, stderr
5
+ ObjectSpace.define_finalizer(self) { kill }
6
+ end
7
+
8
+ def self.run(command)
9
+ command = sanitize_params(command) if command.is_a?(Array)
10
+ child_stdin, parent_stdin = IO::pipe
11
+ parent_stdout, child_stdout = IO::pipe
12
+ parent_stderr, child_stderr = IO::pipe
13
+
14
+ pid = Kernel.fork do
15
+ [parent_stdin, parent_stdout, parent_stderr].each { |io| io.close }
16
+
17
+ STDIN.reopen(child_stdin)
18
+ STDOUT.reopen(child_stdout)
19
+ STDERR.reopen(child_stderr)
20
+
21
+ [child_stdin, child_stdout, child_stderr].each { |io| io.close }
22
+
23
+ exec command
24
+ end
25
+
26
+ [child_stdin, child_stdout, child_stderr].each { |io| io.close }
27
+ parent_stdin.sync = true
28
+
29
+ new(pid, parent_stdin, parent_stdout, parent_stderr)
30
+ end
31
+
32
+ def self.sanitize_params(params)
33
+ params.map { |p| p.gsub(' ', '\ ') }.join(" ")
34
+ end
35
+
36
+ def kill(signal = 'TERM')
37
+ if running?
38
+ Process.kill(Signal.list[signal], @pid)
39
+ true
40
+ end
41
+ end
42
+
43
+ def interrupt
44
+ kill('INT')
45
+ end
46
+
47
+ def running?
48
+ return false unless @pid
49
+ Process.getpgid(@pid)
50
+ true
51
+ rescue Errno::ESRCH
52
+ false
53
+ end
54
+
55
+ def wait(timeout = 1000)
56
+ Timeout.timeout(timeout) do
57
+ Process.wait(@pid)
58
+ end
59
+ true
60
+ rescue Timeout::Error
61
+ false
62
+ end
63
+ end
@@ -6,6 +6,8 @@ require 'spec/expectations'
6
6
  require 'timeout'
7
7
  require 'spork'
8
8
 
9
+ require(File.dirname(__FILE__) + '/background_job.rb')
10
+
9
11
  class SporkWorld
10
12
  BINARY = File.expand_path(File.dirname(__FILE__) + '/../../bin/spork')
11
13
  RUBY_BINARY = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
@@ -25,6 +27,20 @@ class SporkWorld
25
27
 
26
28
  private
27
29
  attr_reader :last_exit_status, :last_stderr, :last_stdout, :background_jobs
30
+ def last_stderr
31
+ return @last_stderr if @last_stderr
32
+ if @background_job
33
+ @last_stderr = @background_job.stderr.read
34
+ end
35
+ end
36
+
37
+
38
+ def last_stdout
39
+ return @last_stdout if @last_stdout
40
+ if @background_job
41
+ @last_stdout = @background_job.stdout.read
42
+ end
43
+ end
28
44
 
29
45
  def create_file(file_name, file_content)
30
46
  file_content.gsub!("SPORK_LIB", "'#{spork_lib_dir}'") # Some files, such as Rakefiles need to use the lib dir
@@ -38,6 +54,18 @@ class SporkWorld
38
54
  Dir.chdir(@current_dir, &block)
39
55
  end
40
56
 
57
+ def localized_command(command, args)
58
+ case command
59
+ when 'spork'
60
+ command = SporkWorld::BINARY
61
+ when 'cucumber'
62
+ command = Cucumber::BINARY
63
+ else
64
+ command = %x{which #{command}}.chomp
65
+ end
66
+ "#{SporkWorld::RUBY_BINARY} -I #{Cucumber::LIBDIR} #{command} #{args}"
67
+ end
68
+
41
69
  def run(command)
42
70
  stderr_file = Tempfile.new('spork')
43
71
  stderr_file.close
@@ -49,36 +77,21 @@ class SporkWorld
49
77
  end
50
78
 
51
79
  def run_in_background(command)
52
- child_stdin, parent_stdin = IO::pipe
53
- parent_stdout, child_stdout = IO::pipe
54
- parent_stderr, child_stderr = IO::pipe
55
-
56
- background_jobs << Kernel.fork do
57
- [parent_stdin, parent_stdout, parent_stderr].each { |io| io.close }
58
-
59
- STDIN.reopen(child_stdin)
60
- STDOUT.reopen(child_stdout)
61
- STDERR.reopen(child_stderr)
62
-
63
- [child_stdin, child_stdout, child_stderr].each { |io| io.close }
64
-
65
- in_current_dir do
66
- exec command
67
- end
80
+ in_current_dir do
81
+ @background_job = BackgroundJob.run(command)
68
82
  end
69
-
70
- [child_stdin, child_stdout, child_stderr].each { |io| io.close }
71
- parent_stdin.sync = true
72
-
73
- @bg_stdin, @bg_stdout, @bg_stderr = [parent_stdin, parent_stdout, parent_stderr]
83
+ @background_jobs << @background_job
84
+ @background_job
74
85
  end
75
86
 
76
87
  def terminate_background_jobs
77
88
  if @background_jobs
78
- @background_jobs.each do |pid|
79
- Process.kill(Signal.list['TERM'], pid)
89
+ @background_jobs.each do |background_job|
90
+ background_job.kill
80
91
  end
81
92
  end
93
+ @background_jobs.clear
94
+ @background_job = nil
82
95
  end
83
96
 
84
97
  end
@@ -0,0 +1,142 @@
1
+ require 'ruby-debug'
2
+ require 'socket'
3
+ require 'forwardable'
4
+
5
+ # Experimental! No automated tests are checking this, use at your own risk!
6
+
7
+ class SporkDebugger
8
+ DEFAULT_PORT = 10_123
9
+ HOST = '127.0.0.1'
10
+
11
+ extend Forwardable
12
+ def_delegators :state, *[:prepare_debugger, :initialize]
13
+ attr_reader :state
14
+
15
+ class << self
16
+ attr_reader :instance
17
+ def run
18
+ @instance = new
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ @state = SporkDebugger::PreloadState.new
24
+ Spork.send(:each_run_procs).unshift(lambda do
25
+ @state = @state.transition_to_each_run_state
26
+ end)
27
+ end
28
+
29
+ module NetworkHelpers
30
+ def find_port(starting_with)
31
+ port = starting_with
32
+ begin
33
+ server = TCPServer.new(HOST, port)
34
+ server.close
35
+ rescue Errno::EADDRINUSE
36
+ port += 1
37
+ retry
38
+ end
39
+
40
+ port
41
+ end
42
+ end
43
+
44
+ class PreloadState
45
+ include NetworkHelpers
46
+ def initialize
47
+ install_hook
48
+ listen_for_connection_signals
49
+ end
50
+
51
+ def finish
52
+ @tcp_service.close; @tcp_service = nil;
53
+ end
54
+
55
+ def transition_to_each_run_state
56
+ finish
57
+ SporkDebugger::EachRunState.new(@port)
58
+ end
59
+
60
+ protected
61
+ def install_hook
62
+ Kernel.class_eval do
63
+ alias :debugger_without_spork_hook :debugger
64
+ def debugger(steps = 1)
65
+ SporkDebugger.instance.prepare_debugger
66
+ debugger_without_spork_hook
67
+ end
68
+ end
69
+ end
70
+
71
+ def listen_for_connection_signals
72
+ @port = SporkDebugger::DEFAULT_PORT
73
+ begin
74
+ @tcp_service = TCPServer.new(SporkDebugger::HOST, @port)
75
+ rescue Errno::EADDRINUSE
76
+ @port += 1
77
+ retry
78
+ end
79
+ Thread.new { main_loop }
80
+ end
81
+
82
+ def main_loop
83
+ while @tcp_service do
84
+ socket = @tcp_service.accept
85
+ port = Marshal.load(socket)
86
+ Marshal.dump(true, socket)
87
+ connect_rdebug_client(port)
88
+ socket.close
89
+ end
90
+ rescue => e
91
+ puts "#{$$} - error: #{e}"
92
+ end
93
+
94
+ def connect_rdebug_client(port = Debugger::PORT)
95
+ puts "\n\n - Debug Session Started - \n\n\n"
96
+ Debugger.start_client(SporkDebugger::HOST, port)
97
+ puts "\n\n - Debug Session Terminated - \n\n\n"
98
+ end
99
+ end
100
+
101
+ class EachRunState
102
+ include NetworkHelpers
103
+ def initialize(connection_request_port)
104
+ @connection_request_port = connection_request_port
105
+ end
106
+
107
+ def prepare_debugger
108
+ port, cport = start_rdebug_server
109
+ signal_spork_server_to_connect_to_rdebug_server(port)
110
+ wait_for_connection
111
+ puts "\n\n - breakpoint - see your spork server for the debug terminal - \n\n"
112
+ end
113
+
114
+ def signal_spork_server_to_connect_to_rdebug_server(rdebug_server_port)
115
+ socket = TCPSocket.new(SporkDebugger::HOST, @connection_request_port)
116
+ Marshal.dump(rdebug_server_port, socket)
117
+ response = Marshal.load(socket)
118
+ socket.close
119
+ end
120
+
121
+ def start_rdebug_server
122
+ Debugger.stop if Debugger.started?
123
+ port = find_port(Debugger::PORT)
124
+ cport = find_port(port + 1)
125
+ Debugger.start_remote(SporkDebugger::HOST, [port, cport]) do
126
+ Debugger.run_init_script(StringIO.new)
127
+ end
128
+ Debugger.start
129
+ [port, cport]
130
+ end
131
+
132
+ protected
133
+ def wait_for_connection
134
+ while Debugger.handler.interface.nil?; sleep 0.10; end
135
+ end
136
+ end
137
+ end
138
+
139
+ Spork.prefork do
140
+ SporkDebugger.run
141
+ end
142
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Harper
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-09-17 00:00:00 -06:00
12
+ date: 2009-09-23 00:00:00 -06:00
13
13
  default_executable: spork
14
14
  dependencies: []
15
15
 
@@ -31,9 +31,11 @@ files:
31
31
  - features/diagnostic_mode.feature
32
32
  - features/rails_delayed_loading_workarounds.feature
33
33
  - features/rspec_rails_integration.feature
34
+ - features/spork_debugger.feature
34
35
  - features/steps/general_steps.rb
35
36
  - features/steps/rails_steps.rb
36
37
  - features/steps/sandbox_steps.rb
38
+ - features/support/background_job.rb
37
39
  - features/support/env.rb
38
40
  - features/unknown_app_framework.feature
39
41
  - lib/spork.rb
@@ -45,6 +47,7 @@ files:
45
47
  - lib/spork/app_framework/unknown.rb
46
48
  - lib/spork/custom_io_streams.rb
47
49
  - lib/spork/diagnoser.rb
50
+ - lib/spork/ext/ruby-debug.rb
48
51
  - lib/spork/forker.rb
49
52
  - lib/spork/run_strategy.rb
50
53
  - lib/spork/run_strategy/forking.rb
@@ -93,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
96
  requirements: []
94
97
 
95
98
  rubyforge_project: spork
96
- rubygems_version: 1.3.4
99
+ rubygems_version: 1.3.5
97
100
  signing_key:
98
101
  specification_version: 3
99
102
  summary: spork