specjour 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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