specjour 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.markdown CHANGED
@@ -1,6 +1,41 @@
1
1
  History
2
2
  =======
3
3
 
4
+ 0.5.0 / 2012-02-20
5
+ ----------------------
6
+
7
+ * [changed] Printer uses UNIX select instead of GServer (threads)
8
+ * [changed] Database is always dropped and reloaded using schema.rb or
9
+ structure.sql
10
+ * [removed] RSpec < 2.8 compatibility
11
+ * [added] Memory utilizing forks. No longer forking and execing means workers
12
+ start running tests faster.
13
+ * [added] Configuration.after_load hook; runs after loading the environment
14
+ * [added] Configurable rsync port
15
+ * [added] Specs distributed by example, not file! Means better
16
+ distribution/fast spec suites.
17
+ * [added] Rails compiled asset directory (tmp/cache) to the rsync inclusion
18
+ list. Workers won't have to compile assets during integration tests.
19
+ * [fixed] SQL structure files can be used to build the database.
20
+ * [fixed] Long timeout while waiting for bonjour requests. The bonjour code has
21
+ been rewritten.
22
+ * [fixed] Load specjour in its own environment when running bundle exec specjour
23
+ * [fixed] Forks running their parent's exit handlers.
24
+ * [fixed] Database creation when the app depends on a database upon environment
25
+ load (something as simple as a scope would cause this dependency). As long as
26
+ the regular test environment can be loaded, a worker without a database
27
+ shouldn't raise an exception, instead the db should be created.
28
+
29
+ [Full Changelog](https://github.com/sandro/specjour/compare/v0.4.1...0.5.0)
30
+
31
+ 0.4.1 / 2011-06-17
32
+ ------------------
33
+
34
+ l4rk and leshill
35
+
36
+ * [fixed] Cucumber failure reports not displayed
37
+
38
+
4
39
  0.4.0 / 2011-03-09
5
40
  ------------------
6
41
 
data/README.markdown CHANGED
@@ -102,6 +102,15 @@ By default, the dispatcher looks for managers matching the project's directory n
102
102
  ~/bizconf $ specjour listen -p bizconf_09
103
103
  ~/bizconf $ specjour -a bizconf_09
104
104
 
105
+ ## Working with git
106
+ Commit the .specjour directory but ignore the performance file. The performance
107
+ file constantly changes, there's no need to commit it. Specjour uses it in an
108
+ attempt to optimize the run order; ensuring each machine gets at least one
109
+ long-running test.
110
+
111
+ $ cat .gitignore
112
+ /.specjour/performance
113
+
105
114
  ## Compatibility
106
115
 
107
116
  * RSpec 2
data/Rakefile CHANGED
@@ -1,5 +1,4 @@
1
- require 'rubygems'
2
- require 'rake'
1
+ require 'bundler/gem_tasks'
3
2
 
4
3
  require 'rspec/core/rake_task'
5
4
  RSpec::Core::RakeTask.new(:spec)
@@ -19,12 +18,12 @@ end
19
18
 
20
19
  desc "tag, push gem, push to github"
21
20
  task :prerelease do
22
- version = `cat VERSION`.strip
21
+ require 'specjour'
23
22
  command = %(
24
- git tag v#{version} &&
23
+ git tag v#{Specjour::VERSION} &&
25
24
  rake build &&
26
25
  git push &&
27
- gem push pkg/specjour-#{version}.gem &&
26
+ gem push pkg/specjour-#{Specjour::VERSION}.gem &&
28
27
  git push --tags
29
28
  )
30
29
  puts command
data/lib/specjour/cli.rb CHANGED
@@ -10,25 +10,29 @@ module Specjour
10
10
  method_option :alias, :aliases => "-a", :desc => "Project name advertised to listeners"
11
11
  end
12
12
 
13
+ def self.rsync_port_option
14
+ method_option :rsync_port, :type => :numeric, :default => 23456, :desc => "Port to use for rsync daemon"
15
+ end
16
+
17
+ # allow specjour to be called with path arguments
13
18
  def self.start(original_args=ARGV, config={})
14
- real_tasks = all_tasks.keys | HELP_MAPPINGS
19
+ real_tasks = all_tasks.keys | @map.keys
15
20
  unless real_tasks.include? original_args.first
16
21
  original_args.unshift default_task
17
22
  end
18
23
  super(original_args)
19
24
  end
20
25
 
21
-
22
26
  default_task :dispatch
23
27
 
24
28
  class_option :log, :aliases => "-l", :type => :boolean, :desc => "Print debug messages to $stderr"
25
29
 
26
-
27
- desc "listen", "Wait for incoming tests"
28
- long_desc <<-D
30
+ desc "listen", "Listen for incoming tests to run"
31
+ long_desc <<-DESC
29
32
  Advertise availability to run tests for the current directory.
30
- D
33
+ DESC
31
34
  worker_option
35
+ rsync_port_option
32
36
  method_option :projects, :aliases => "-p", :type => :array, :desc => "Projects supported by this listener"
33
37
  def listen
34
38
  handle_logging
@@ -38,52 +42,66 @@ module Specjour
38
42
  Specjour::Manager.new(args).start
39
43
  end
40
44
 
41
- desc "dispatch [PROJECT_PATH]", "Run tests in the current directory"
45
+ desc "load", "load the app, then fork workers", :hide => true
46
+ worker_option
47
+ method_option :printer_uri, :required => true
48
+ method_option :project_path, :required => true
49
+ method_option :task, :required => true
50
+ method_option :test_paths, :type => :array, :default => []
51
+ method_option :quiet, :type => :boolean, :default => false
52
+ def load
53
+ handle_logging
54
+ handle_workers
55
+ append_to_program_name "load"
56
+ Specjour::Loader.new(args).start
57
+ end
58
+
59
+ desc "dispatch [test_paths]", "Send tests to a listener"
42
60
  worker_option
43
61
  dispatcher_option
44
- def dispatch(path = Dir.pwd)
62
+ rsync_port_option
63
+ long_desc <<-DESC
64
+ This is run when you simply type `specjour`.
65
+ By default, it will run the specs and features found in the current directory.
66
+ If you like, you can run a subset of tests by specifying the folder containing the tests.\n
67
+ Examples\n
68
+ `specjour dispatch spec`\n
69
+ `specjour dispatch features`\n
70
+ `specjour dispatch spec/models features/sign_up.feature`\n
71
+ DESC
72
+ def dispatch(*paths)
45
73
  handle_logging
46
74
  handle_workers
47
- handle_dispatcher(path)
75
+ handle_dispatcher(paths)
48
76
  append_to_program_name "dispatch"
49
77
  Specjour::Dispatcher.new(args).start
50
78
  end
51
79
 
52
- desc "prepare [PROJECT_PATH]", "Prepare all listening workers"
53
- long_desc <<-D
80
+ desc "prepare [PROJECT_PATH]", "Run the prepare task on all listening workers"
81
+ long_desc <<-DESC
54
82
  Run the Specjour::Configuration.prepare block on all listening workers.
55
- Defaults to dropping and schema loading the database.
56
- D
83
+ Defaults to dropping the database, then loading the schema.
84
+ DESC
57
85
  worker_option
58
86
  dispatcher_option
87
+ rsync_port_option
59
88
  def prepare(path = Dir.pwd)
60
89
  handle_logging
61
90
  handle_workers
62
- handle_dispatcher(path)
91
+ args[:project_path] = File.expand_path(path)
92
+ args[:project_alias] = args.delete(:alias)
93
+ args[:test_paths] = []
63
94
  args[:worker_task] = 'prepare'
64
95
  append_to_program_name "prepare"
65
96
  Specjour::Dispatcher.new(args).start
66
97
  end
67
98
 
99
+ map %w(-v --version) => :version
68
100
  desc "version", "Show the current version"
69
101
  def version
70
102
  puts Specjour::VERSION
71
103
  end
72
104
 
73
- desc "work", "INTERNAL USE ONLY", :hide => true
74
- method_option :project_path, :required => true
75
- method_option :printer_uri, :required => true
76
- method_option :number, :type => :numeric, :required => true
77
- method_option :preload_spec
78
- method_option :preload_feature
79
- method_option :task, :required => true
80
- method_option :quiet, :type => :boolean
81
- def work
82
- handle_logging
83
- append_to_program_name "work"
84
- Specjour::Worker.new(args).start
85
- end
86
-
87
105
  protected
88
106
 
89
107
  def append_to_program_name(command)
@@ -102,9 +120,15 @@ module Specjour
102
120
  args[:worker_size] = options["workers"] || CPU.cores
103
121
  end
104
122
 
105
- def handle_dispatcher(path)
106
- args[:project_path] = path
123
+ def handle_dispatcher(paths)
124
+ if paths.empty?
125
+ args[:project_path] = Dir.pwd
126
+ else
127
+ args[:project_path] = File.expand_path(paths.first.sub(/(spec|features).*$/, ''))
128
+ end
129
+ args[:test_paths] = paths
107
130
  args[:project_alias] = args.delete(:alias)
131
+ raise ArgumentError, "Cannot dispatch line numbers" if paths.any? {|p| p =~ /:\d+/}
108
132
  end
109
133
  end
110
134
  end
@@ -2,18 +2,24 @@ module Specjour
2
2
  module Configuration
3
3
  extend self
4
4
 
5
- attr_writer :before_fork, :after_fork, :prepare
5
+ attr_writer :before_fork, :after_fork, :after_load, :prepare
6
6
 
7
- # This block is run by each worker the manager forks.
8
- # The Rails plugin uses this block to clear the databases defined in
9
- # ActiveRecord.
10
- # Set your own block if the default doesn't work for you.
7
+ # This block is run by each worker before they begin running tests.
8
+ # The default action is to migrate the database, and clear it of any old
9
+ # data.
11
10
  def after_fork
12
11
  @after_fork ||= default_after_fork
13
12
  end
14
13
 
15
- # This block is run after before forking. When ActiveRecord is
16
- # defined, the default before_block disconnects from the database.
14
+ # This block is run after the manager loads the app into memory, but before
15
+ # forking new worker processes. The default action is to disconnect from
16
+ # the ActiveRecord database.
17
+ def after_load
18
+ @after_load ||= default_after_load
19
+ end
20
+
21
+ # This block is run by the manager before forking workers. The default
22
+ # action is to run bundle install.
17
23
  def before_fork
18
24
  @before_fork ||= default_before_fork
19
25
  end
@@ -29,6 +35,7 @@ module Specjour
29
35
  def reset
30
36
  @before_fork = nil
31
37
  @after_fork = nil
38
+ @after_load = nil
32
39
  @prepare = nil
33
40
  end
34
41
 
@@ -40,7 +47,6 @@ module Specjour
40
47
 
41
48
  def default_before_fork
42
49
  lambda do
43
- ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base)
44
50
  bundle_install
45
51
  end
46
52
  end
@@ -51,6 +57,12 @@ module Specjour
51
57
  end
52
58
  end
53
59
 
60
+ def default_after_load
61
+ lambda do
62
+ ActiveRecord::Base.remove_connection if rails_with_ar?
63
+ end
64
+ end
65
+
54
66
  def default_prepare
55
67
  lambda do
56
68
  if rails_with_ar?
@@ -6,7 +6,7 @@ module Specjour
6
6
  attr_reader :uri
7
7
  attr_writer :socket
8
8
 
9
- def_delegators :socket, :flush, :closed?, :gets, :each
9
+ def_delegators :socket, :flush, :close, :closed?, :gets, :each
10
10
 
11
11
  def self.wrap(established_connection)
12
12
  host, port = established_connection.peeraddr.values_at(3,1)
@@ -26,7 +26,7 @@ module Specjour
26
26
  end
27
27
 
28
28
  def disconnect
29
- socket.close
29
+ socket.close unless socket && socket.closed?
30
30
  end
31
31
 
32
32
  def socket
@@ -34,9 +34,8 @@ module Specjour
34
34
  end
35
35
 
36
36
  def timeout(&block)
37
- Timeout.timeout(2, &block)
37
+ Timeout.timeout(0.5, &block)
38
38
  rescue Timeout::Error
39
- raise Error, "Connection to dispatcher timed out", []
40
39
  end
41
40
 
42
41
  def next_test
@@ -78,7 +77,7 @@ module Specjour
78
77
 
79
78
  def will_reconnect(&block)
80
79
  block.call
81
- rescue SystemCallError => error
80
+ rescue SystemCallError, IOError => error
82
81
  unless Specjour.interrupted?
83
82
  reconnect
84
83
  retry
data/lib/specjour/cpu.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Specjour
2
2
  module CPU
3
3
  def self.cores
4
- case RUBY_PLATFORM
4
+ case platform
5
5
  when /darwin/
6
6
  command('hostinfo') =~ /^(\d+).+physically/
7
7
  $1.to_i
@@ -15,5 +15,9 @@ module Specjour
15
15
  def self.command(cmd)
16
16
  %x(#{cmd})
17
17
  end
18
+
19
+ def self.platform
20
+ RUBY_PLATFORM
21
+ end
18
22
  end
19
23
  end
@@ -1,7 +1,8 @@
1
1
  module Specjour
2
2
  module Cucumber
3
3
  module Preloader
4
- def self.load(feature_file)
4
+ def self.load
5
+ require 'cucumber' unless defined?(::Cucumber::Cli)
5
6
  configuration = ::Cucumber::Cli::Configuration.new
6
7
  configuration.parse! []
7
8
  runtime = ::Cucumber::Runtime.new(configuration)
@@ -1,25 +1,16 @@
1
1
  module Specjour
2
2
  module Cucumber
3
3
  begin
4
- require 'cucumber'
5
4
  require 'cucumber/formatter/progress'
6
5
 
7
6
  require 'specjour/cucumber/distributed_formatter'
8
7
  require 'specjour/cucumber/final_report'
9
8
  require 'specjour/cucumber/preloader'
10
- require 'specjour/cucumber/main_ext'
11
9
  require 'specjour/cucumber/runner'
12
-
13
- ::Cucumber::Cli::Options.class_eval { def print_profile_information; end }
14
10
  rescue LoadError
15
11
  end
16
12
 
17
13
  class << self; attr_accessor :runtime; end
18
14
 
19
- def self.wants_to_quit
20
- if defined?(::Cucumber) && ::Cucumber.respond_to?(:wants_to_quit=)
21
- ::Cucumber.wants_to_quit = true
22
- end
23
- end
24
15
  end
25
16
  end
@@ -1,17 +1,14 @@
1
+ # encoding: utf-8
1
2
  module Specjour
2
3
  module DbScrub
3
4
 
4
5
  begin
5
6
  require 'rake'
6
- if defined?(Rails) && Rails.version =~ /^3/
7
- task(:environment) {}
7
+ extend Rake::DSL if defined?(Rake::DSL)
8
+ if defined?(Rails)
9
+ Rake::Task.define_task(:environment) { }
8
10
  load 'rails/tasks/misc.rake'
9
11
  load 'active_record/railties/databases.rake'
10
- else
11
- load 'tasks/misc.rake'
12
- load 'tasks/databases.rake'
13
- Rake::Task["db:structure:dump"].clear
14
- Rake::Task["environment"].clear
15
12
  end
16
13
  rescue LoadError
17
14
  Specjour.logger.debug "Failed to load Rails rake tasks"
@@ -25,18 +22,15 @@ module Specjour
25
22
 
26
23
  def scrub
27
24
  connect_to_database
28
- if pending_migrations?
29
- puts "Migrating schema for database #{ENV['TEST_ENV_NUMBER']}..."
30
- schema_load_task.invoke
31
- else
32
- purge_tables
33
- end
25
+ puts "Resetting database #{ENV['TEST_ENV_NUMBER']}"
26
+ schema_load_task.invoke
34
27
  end
35
28
 
36
29
  protected
37
30
 
38
31
  def connect_to_database
39
32
  ActiveRecord::Base.remove_connection
33
+ ActiveRecord::Base.configurations = Rails.application.config.database_configuration
40
34
  ActiveRecord::Base.establish_connection
41
35
  connection
42
36
  rescue # assume the database doesn't exist
@@ -47,20 +41,12 @@ module Specjour
47
41
  ActiveRecord::Base.connection
48
42
  end
49
43
 
50
- def purge_tables
51
- connection.disable_referential_integrity do
52
- tables_to_purge.each do |table|
53
- connection.delete "delete from #{table}"
54
- end
55
- end
56
- end
57
-
58
44
  def pending_migrations?
59
45
  ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations.any?
60
46
  end
61
47
 
62
48
  def schema_load_task
63
- Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]]
49
+ Rake::Task[{ :sql => "db:test:load_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]]
64
50
  end
65
51
 
66
52
  def tables_to_purge
@@ -4,18 +4,19 @@ module Specjour
4
4
  Thread.abort_on_exception = true
5
5
  include SocketHelper
6
6
 
7
- attr_reader :project_alias, :managers, :manager_threads, :hosts, :options, :all_tests, :drb_connection_errors
7
+ attr_reader :project_alias, :managers, :manager_threads, :hosts, :options, :drb_connection_errors, :test_paths, :rsync_port
8
8
  attr_accessor :worker_size, :project_path
9
9
 
10
10
  def initialize(options = {})
11
11
  Specjour.load_custom_hooks
12
12
  @options = options
13
- @project_path = File.expand_path options[:project_path]
13
+ @project_path = options[:project_path]
14
+ @test_paths = options[:test_paths]
14
15
  @worker_size = 0
15
16
  @managers = []
16
17
  @drb_connection_errors = Hash.new(0)
17
- find_tests
18
- clear_manager_threads
18
+ @rsync_port = options[:rsync_port]
19
+ reset_manager_threads
19
20
  end
20
21
 
21
22
  def start
@@ -23,41 +24,23 @@ module Specjour
23
24
  gather_managers
24
25
  rsync_daemon.start
25
26
  dispatch_work
26
- printer.join if dispatching_tests?
27
+ printer.start if dispatching_tests?
27
28
  wait_on_managers
28
29
  exit printer.exit_status
29
30
  end
30
31
 
31
32
  protected
32
33
 
33
- def find_tests
34
- if project_path.match(/(.+)\/((spec|features)(?:\/\w+)*)$/)
35
- self.project_path = $1
36
- @all_tests = $3 == 'spec' ? all_specs($2) : all_features($2)
37
- else
38
- @all_tests = all_specs | all_features
39
- end
40
- end
41
-
42
- def all_specs(tests_path = 'spec')
43
- Dir[File.join(".", tests_path, "**/*_spec.rb")].sort
44
- end
45
-
46
- def all_features(tests_path = 'features')
47
- Dir[File.join(".", tests_path, "**/*.feature")].sort
48
- end
49
-
50
34
  def add_manager(manager)
51
35
  set_up_manager(manager)
52
36
  managers << manager
53
37
  self.worker_size += manager.worker_size
54
38
  end
55
39
 
56
- def command_managers(async = false, &block)
40
+ def command_managers(&block)
57
41
  managers.each do |manager|
58
42
  manager_threads << Thread.new(manager, &block)
59
43
  end
60
- wait_on_managers unless async
61
44
  end
62
45
 
63
46
  def dispatcher_uri
@@ -69,8 +52,7 @@ module Specjour
69
52
  managers.each do |manager|
70
53
  puts "#{manager.hostname} (#{manager.worker_size})"
71
54
  end
72
- printer.worker_size = worker_size
73
- command_managers(true) { |m| m.dispatch rescue DRb::DRbConnError }
55
+ command_managers { |m| m.dispatch rescue DRb::DRbConnError }
74
56
  end
75
57
 
76
58
  def dispatching_tests?
@@ -85,12 +67,12 @@ module Specjour
85
67
  rescue DRb::DRbConnError => e
86
68
  drb_connection_errors[uri] += 1
87
69
  Specjour.logger.debug "#{e.message}: couldn't connect to manager at #{uri}"
88
- retry if drb_connection_errors[uri] < 5
70
+ sleep(0.1) && retry if drb_connection_errors[uri] < 5
89
71
  end
90
72
 
91
73
  def fork_local_manager
92
74
  puts "No listeners found on this machine, starting one..."
93
- manager_options = {:worker_size => options[:worker_size], :registered_projects => [project_alias]}
75
+ manager_options = {:worker_size => options[:worker_size], :registered_projects => [project_alias], :rsync_port => rsync_port}
94
76
  manager = Manager.start_quietly manager_options
95
77
  fetch_manager(manager.drb_uri)
96
78
  at_exit do
@@ -101,23 +83,23 @@ module Specjour
101
83
  end
102
84
 
103
85
  def gather_managers
104
- puts "Looking for managers..."
86
+ puts "Looking for listeners..."
105
87
  gather_remote_managers
106
88
  fork_local_manager if local_manager_needed?
107
- abort "No managers found" if managers.size.zero?
89
+ abort "No listeners found" if managers.size.zero?
108
90
  end
109
91
 
110
92
  def gather_remote_managers
111
- browser = DNSSD::Service.new
112
- Timeout.timeout(3) do
113
- browser.browse '_druby._tcp' do |reply|
114
- if reply.flags.add?
115
- resolve_reply(reply)
116
- end
117
- browser.stop unless reply.flags.more_coming?
93
+ replies = []
94
+ Timeout.timeout(1) do
95
+ DNSSD.browse!('_druby._tcp') do |reply|
96
+ replies << reply if reply.flags.add?
97
+ break unless reply.flags.more_coming?
118
98
  end
99
+ raise Timeout::Error
119
100
  end
120
101
  rescue Timeout::Error
102
+ replies.each {|r| resolve_reply(r)}
121
103
  end
122
104
 
123
105
  def local_manager_needed?
@@ -129,7 +111,7 @@ module Specjour
129
111
  end
130
112
 
131
113
  def printer
132
- @printer ||= Printer.start(all_tests)
114
+ @printer ||= Printer.new
133
115
  end
134
116
 
135
117
  def project_alias
@@ -140,34 +122,36 @@ module Specjour
140
122
  @project_name ||= File.basename(project_path)
141
123
  end
142
124
 
143
- def clear_manager_threads
125
+ def reset_manager_threads
144
126
  @manager_threads = []
145
127
  end
146
128
 
147
129
  def resolve_reply(reply)
148
- DNSSD.resolve!(reply) do |resolved|
130
+ DNSSD.resolve!(reply.name, reply.type, reply.domain, flags=0, reply.interface) do |resolved|
149
131
  Specjour.logger.debug "Bonjour discovered #{resolved.target}"
150
- resolved_ip = ip_from_hostname(resolved.target)
151
- uri = URI::Generic.build :scheme => reply.service_name, :host => resolved_ip, :port => resolved.port
152
- fetch_manager(uri)
153
- resolved.service.stop if resolved.service.started?
132
+ if resolved.text_record && resolved.text_record['version'] == Specjour::VERSION
133
+ resolved_ip = ip_from_hostname(resolved.target)
134
+ uri = URI::Generic.build :scheme => reply.service_name, :host => resolved_ip, :port => resolved.port
135
+ fetch_manager(uri)
136
+ else
137
+ puts "Found #{resolved.target} but its version doesn't match v#{Specjour::VERSION}. Skipping..."
138
+ end
139
+ break unless resolved.flags.more_coming?
154
140
  end
155
141
  end
156
142
 
157
143
  def rsync_daemon
158
- @rsync_daemon ||= RsyncDaemon.new(project_path, project_name)
144
+ @rsync_daemon ||= RsyncDaemon.new(project_path, project_name, rsync_port)
159
145
  end
160
146
 
161
147
  def set_up_manager(manager)
162
148
  manager.project_name = project_name
163
149
  manager.dispatcher_uri = dispatcher_uri
164
- manager.preload_spec = all_tests.detect {|f| f =~ /_spec\.rb$/}
165
- manager.preload_feature = all_tests.detect {|f| f =~ /\.feature$/}
150
+ manager.test_paths = test_paths
166
151
  manager.worker_task = worker_task
167
152
  at_exit do
168
153
  begin
169
154
  manager.interrupted = Specjour.interrupted?
170
- manager.kill_worker_processes
171
155
  rescue DRb::DRbConnError
172
156
  end
173
157
  end
@@ -175,7 +159,7 @@ module Specjour
175
159
 
176
160
  def wait_on_managers
177
161
  manager_threads.each {|t| t.join; t.exit}
178
- clear_manager_threads
162
+ reset_manager_threads
179
163
  end
180
164
 
181
165
  def worker_task
@@ -0,0 +1,26 @@
1
+ module Specjour::Fork
2
+
3
+ module_function
4
+
5
+ # fork, but don't run the parent's exit handlers
6
+ # The one exit handler we lose however, is the printing out
7
+ # of exceptions, so reincorporate that.
8
+ def fork
9
+ Kernel.fork do
10
+ at_exit { exit! }
11
+ begin
12
+ yield
13
+ rescue StandardError => e
14
+ $stderr.puts "#{e.class} #{e.message}", e.backtrace
15
+ end
16
+ end
17
+ end
18
+
19
+ def fork_quietly
20
+ fork do
21
+ $stdout = StringIO.new
22
+ yield
23
+ end
24
+ end
25
+
26
+ end