exceptional_synchrony 1.0.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,3 @@
1
- require 'hobo_support'
2
1
  require 'em-synchrony'
3
2
  require 'em-synchrony/em-http'
4
3
  require 'exception_handling'
@@ -21,13 +21,13 @@ module ExceptionalSynchrony
21
21
  result
22
22
  end
23
23
  when :failed
24
- if result.respond_to?(:error) && result.error =~ /timeout/i
25
- raise Timeout::Error
24
+ if result.respond_to?(:error)
25
+ handle_result_error(result)
26
26
  else
27
- raise Failure, result.inspect
27
+ raise_failure_for_result(result)
28
28
  end
29
29
  else
30
- raise ArgumentError, "No deferred status set yet: #{deferred_status.inspect} #{result.inspect}"
30
+ raise ArgumentError, "No deferred status set yet: #{deferred_status.inspect} #{truncated_inspect(result)}"
31
31
  end
32
32
  end
33
33
 
@@ -38,6 +38,38 @@ module ExceptionalSynchrony
38
38
  ex
39
39
  end
40
40
  end
41
+
42
+ private
43
+
44
+ def handle_result_error(result)
45
+ error = result.error
46
+ if error_is_a_timeout?(error)
47
+ raise Timeout::Error
48
+ else
49
+ raise_failure_for_result(result, error: error)
50
+ end
51
+ end
52
+
53
+ def raise_failure_for_result(result, error: nil)
54
+ result_string = truncated_inspect(result)
55
+ error_string = if error
56
+ "ERROR = #{truncated_inspect(error)}; "
57
+ end
58
+ raise Failure, "#{error_string}RESULT = #{result_string}"
59
+ end
60
+
61
+ def truncated_inspect(obj)
62
+ inspection = obj.inspect[0, 101]
63
+ if inspection.length > 100
64
+ inspection[0, 92] + '...TRUNC'
65
+ else
66
+ inspection
67
+ end
68
+ end
69
+
70
+ def error_is_a_timeout?(error)
71
+ error =~ /timeout/i || error == Errno::ETIMEDOUT
72
+ end
41
73
  end
42
74
  end
43
75
  end
@@ -5,6 +5,10 @@ require 'em-http'
5
5
  require 'em-synchrony/em-http'
6
6
 
7
7
  module ExceptionalSynchrony
8
+ # It is important for this exception to be inherited from Exception so that
9
+ # when thrown it does not get caught by the EventMachine.error_handler.
10
+ class FatalRunError < Exception; end
11
+
8
12
  class EventMachineProxy
9
13
 
10
14
  attr_reader :connection
@@ -41,6 +45,12 @@ module ExceptionalSynchrony
41
45
  @synchrony.sleep(seconds)
42
46
  end
43
47
 
48
+ def yield_to_reactor
49
+ if reactor_running?
50
+ @synchrony.sleep(0)
51
+ end
52
+ end
53
+
44
54
  def next_tick(&block)
45
55
  @synchrony.next_tick do
46
56
  ensure_completely_safe("next_tick") do
@@ -54,17 +64,39 @@ module ExceptionalSynchrony
54
64
  @proxy_class.next_tick { } #Fake out EventMachine's epoll mechanism so we don't block until timers fire
55
65
  end
56
66
 
67
+ def defers_finished?
68
+ @proxy_class.defers_finished?
69
+ end
70
+
57
71
  def connect(server, port = nil, handler = nil, *args, &block)
58
72
  @proxy_class.connect(server, port, handler, *args, &block)
59
73
  end
60
74
 
61
- def run(&block)
62
- ensure_completely_safe("run") do
63
- if @proxy_class.respond_to?(:synchrony)
64
- @proxy_class.synchrony(&block)
65
- else
66
- @proxy_class.run(&block)
67
- end
75
+ # The on_error option has these possible values:
76
+ # :log - log any rescued StandardError exceptions and continue
77
+ # :raise - raise FatalRunError for any rescued StandardError exceptions
78
+ def run(on_error: :log, &block)
79
+ case on_error
80
+ when :log then run_with_error_logging(&block)
81
+ when :raise then run_with_error_raising(&block)
82
+ else raise ArgumentError, "Invalid on_error: #{on_error.inspect}, must be :log or :raise"
83
+ end
84
+ end
85
+
86
+ # This method will execute the block on the background thread pool
87
+ # By default, it will block the caller until the background thread has finished, so that the result can be returned
88
+ # :wait_for_result - setting this to false will prevent the caller from being blocked by this deferred work
89
+ def defer(context, wait_for_result: true, &block)
90
+ if wait_for_result
91
+ deferrable = EventMachine::DefaultDeferrable.new
92
+ callback = -> (result) { deferrable.succeed(result) }
93
+
94
+ EventMachine.defer(nil, callback) { CallbackExceptions.return_exception(&block) }
95
+ EventMachine::Synchrony.sync(deferrable)
96
+ CallbackExceptions.map_deferred_result(deferrable)
97
+ else
98
+ EventMachine.defer { ExceptionHandling.ensure_completely_safe("defer", &block) }
99
+ nil
68
100
  end
69
101
  end
70
102
 
@@ -90,6 +122,38 @@ module ExceptionalSynchrony
90
122
  yield
91
123
  end
92
124
  end
125
+
126
+ def rescue_exceptions_and_ensure_exit(context)
127
+ yield
128
+ rescue StandardError => ex
129
+ # Raise a non-StandardError so that not caught by EM.error_handler.
130
+ # Expecting rescued exception to be stored in this new exception's cause.
131
+ raise FatalRunError, "Fatal EventMachine #{context} error\n#{ex.class.name}: #{ex.message}"
132
+ end
133
+
134
+ private
135
+
136
+ def run_with_error_logging(&block)
137
+ ensure_completely_safe("run_with_error_logging") do
138
+ if @proxy_class.respond_to?(:synchrony)
139
+ @proxy_class.synchrony(&block)
140
+ else
141
+ @proxy_class.run(&block)
142
+ end
143
+ end
144
+ end
145
+
146
+ def run_with_error_raising(&block)
147
+ run_block = -> { rescue_exceptions_and_ensure_exit("run_with_error_raising", &block) }
148
+
149
+ rescue_exceptions_and_ensure_exit("run_with_error_raising") do
150
+ if @proxy_class.respond_to?(:synchrony)
151
+ @proxy_class.synchrony(&run_block)
152
+ else
153
+ @proxy_class.run(&run_block)
154
+ end
155
+ end
156
+ end
93
157
  end
94
158
 
95
159
  EMP = EventMachineProxy.new(EventMachine, EventMachine::HttpRequest)
@@ -1,15 +1,17 @@
1
1
  module ExceptionalSynchrony
2
2
  class LimitedWorkQueue
3
+
3
4
  def initialize(em, limit)
4
5
  @em = em
5
6
  limit > 0 or raise ArgumentError, "limit must be positive"
6
7
  @limit = limit
7
8
  @worker_count = 0
8
9
  @job_procs = []
10
+ @paused = false
9
11
  end
10
12
 
11
13
  # Adds a job_proc to work.
12
- def add(proc = nil, &block)
14
+ def add!(proc = nil, &block)
13
15
  job = proc || block
14
16
  job.respond_to?(:call) or raise "Must respond_to?(:call)! #{job.inspect}"
15
17
  if @job_procs.any? && job.respond_to?(:merge) && (merged_queue = job.merge(@job_procs))
@@ -17,8 +19,9 @@ module ExceptionalSynchrony
17
19
  else
18
20
  @job_procs << job
19
21
  end
20
- work!
22
+ work! unless paused?
21
23
  end
24
+ alias_method :add, :add!
22
25
 
23
26
  def workers_empty?
24
27
  @worker_count.zero?
@@ -32,18 +35,39 @@ module ExceptionalSynchrony
32
35
  @job_procs.empty?
33
36
  end
34
37
 
35
- private
38
+ def paused?
39
+ @paused
40
+ end
41
+
42
+ def pause!
43
+ @paused = true
44
+ end
45
+
46
+ def unpause!
47
+ @paused = false
48
+ end
49
+
50
+ def items
51
+ @job_procs
52
+ end
53
+
36
54
  def work!
37
55
  until queue_empty? || workers_full?
38
56
  job_proc = @job_procs.shift
39
57
  @worker_count += 1
40
58
  Fiber.new do
41
- job_proc.call
42
- worker_done
59
+ begin
60
+ job_proc.call
61
+ rescue => ex
62
+ ExceptionHandling.log_error(ex, "LimitedWorkQueue encountered an exception")
63
+ ensure
64
+ worker_done
65
+ end
43
66
  end.resume
44
67
  end
45
68
  end
46
69
 
70
+ private
47
71
  def worker_done
48
72
  @worker_count -= 1
49
73
  work!
@@ -1,5 +1,6 @@
1
1
  require 'fiber'
2
2
  require 'set'
3
+ require 'invoca/utils/hash'
3
4
 
4
5
  module ExceptionalSynchrony
5
6
  class ParallelSync
@@ -1,3 +1,3 @@
1
1
  module ExceptionalSynchrony
2
- VERSION = '1.0.1'
2
+ VERSION = '1.3.0'
3
3
  end
data/test/test_helper.rb CHANGED
@@ -1,11 +1,23 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require 'bundler'
4
+ Bundler.setup(:default, :development)
5
+
1
6
  require_relative '../lib/exceptional_synchrony.rb'
2
7
 
3
8
  require 'minitest/autorun' or raise "Already loaded minitest?"
4
9
  require 'minitest/pride'
10
+ require 'minitest/reporters'
11
+ Minitest::Reporters.use! [
12
+ Minitest::Reporters::DefaultReporter.new,
13
+ Minitest::Reporters::JUnitReporter.new
14
+ ]
5
15
  require 'webmock'
6
16
  require 'webmock/minitest'
7
17
  require 'rr'
8
18
 
19
+ ActiveSupport::TestCase.test_order = :sorted
20
+
9
21
  module TestHelper
10
22
  @@constant_overrides = []
11
23
 
@@ -1,4 +1,5 @@
1
1
  require_relative '../test_helper'
2
+ require 'pry'
2
3
 
3
4
  describe ExceptionalSynchrony::CallbackExceptions do
4
5
  describe "ensure_callback" do
@@ -60,14 +61,14 @@ describe ExceptionalSynchrony::CallbackExceptions do
60
61
  deferrable = EM::DefaultDeferrable.new
61
62
  deferrable.succeed(12)
62
63
  result = ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
63
- result.must_equal 12
64
+ expect(result).must_equal(12)
64
65
  end
65
66
 
66
67
  it "should map success values to an array" do
67
68
  deferrable = EM::DefaultDeferrable.new
68
69
  deferrable.succeed(12, 13, 14)
69
70
  result = ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
70
- result.must_equal [12, 13, 14]
71
+ expect(result).must_equal([12, 13, 14])
71
72
  end
72
73
 
73
74
  it "should map success exception values to raise" do
@@ -77,7 +78,7 @@ describe ExceptionalSynchrony::CallbackExceptions do
77
78
  result = assert_raises(ArgumentError) do
78
79
  ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
79
80
  end
80
- result.must_equal exception
81
+ expect(result).must_equal(exception)
81
82
  end
82
83
  end
83
84
 
@@ -87,8 +88,18 @@ describe ExceptionalSynchrony::CallbackExceptions do
87
88
  deferrable.fail(first: "a", last: "b")
88
89
  result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
89
90
  ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
90
- end
91
- result.message.must_equal "{:first=>\"a\", :last=>\"b\"}"
91
+ end.message
92
+ expect(result).must_equal("RESULT = {:first=>\"a\", :last=>\"b\"}")
93
+ end
94
+
95
+ it "should truncate long failures" do
96
+ deferrable = EM::DefaultDeferrable.new
97
+ deferrable.fail('a'*75 + 'b'*75)
98
+ result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
99
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
100
+ end.message
101
+ expected_message = "RESULT = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbb...TRUNC"
102
+ expect(result).must_equal(expected_message)
92
103
  end
93
104
 
94
105
  it "should map failure exceptions to raise" do
@@ -97,9 +108,9 @@ describe ExceptionalSynchrony::CallbackExceptions do
97
108
  deferrable.fail(exception)
98
109
  result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
99
110
  ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
100
- end
101
- result.message.must_match /ArgumentError/
102
- result.message.must_match /Wrong argument!/
111
+ end.message
112
+ expect(result).must_match(/ArgumentError/)
113
+ expect(result).must_match(/Wrong argument!/)
103
114
  end
104
115
 
105
116
  it "should map timeout failure to raise TimeoutError" do
@@ -114,6 +125,47 @@ describe ExceptionalSynchrony::CallbackExceptions do
114
125
  ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
115
126
  end
116
127
  end
128
+
129
+ it "should map Errno::ETIMEDOUT to TimeoutError" do
130
+ deferrable = EM::DefaultDeferrable.new
131
+
132
+ def deferrable.error
133
+ Errno::ETIMEDOUT
134
+ end
135
+
136
+ deferrable.fail(deferrable)
137
+ assert_raises(Timeout::Error) do
138
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
139
+ end
140
+ end
141
+
142
+ it "should map other SystemCallError exceptions to Failures with the error in the message" do
143
+ deferrable = EM::DefaultDeferrable.new
144
+
145
+ def deferrable.error
146
+ Errno::ECONNREFUSED
147
+ end
148
+
149
+ deferrable.fail(deferrable)
150
+ result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
151
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
152
+ end
153
+ expect(result.message).must_match(/\AERROR = Errno::ECONNREFUSED; RESULT = #<EventMachine::DefaultDeferrable/)
154
+ end
155
+
156
+ it "should map any other errors to Failure with the error in the message" do
157
+ deferrable = EM::DefaultDeferrable.new
158
+
159
+ def deferrable.error
160
+ ArgumentError.new("Some errror")
161
+ end
162
+
163
+ deferrable.fail(deferrable)
164
+ result = assert_raises(ExceptionalSynchrony::CallbackExceptions::Failure) do
165
+ ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
166
+ end
167
+ expect(result.message).must_match(/\AERROR = #<ArgumentError: Some errror>; RESULT = #<EventMachine::DefaultDeferrable/)
168
+ end
117
169
  end
118
170
 
119
171
  describe "no status" do
@@ -122,29 +174,29 @@ describe ExceptionalSynchrony::CallbackExceptions do
122
174
  result = assert_raises(ArgumentError) do
123
175
  ExceptionalSynchrony::CallbackExceptions.map_deferred_result(deferrable)
124
176
  end
125
- result.message.must_match /no deferred status set yet/i
177
+ expect(result.message).must_match(/no deferred status set yet/i)
126
178
  end
127
179
  end
128
180
  end
129
181
 
130
182
  describe "return_exception" do
131
183
  it "should return the value if no exception" do
132
- ExceptionalSynchrony::CallbackExceptions.return_exception do
184
+ expect(ExceptionalSynchrony::CallbackExceptions.return_exception do
133
185
  14
134
- end.must_equal 14
186
+ end).must_equal(14)
135
187
  end
136
188
 
137
189
  it "should yield its args" do
138
- ExceptionalSynchrony::CallbackExceptions.return_exception(0, 1) do |a, b|
190
+ expect(ExceptionalSynchrony::CallbackExceptions.return_exception(0, 1) do |a, b|
139
191
  assert_equal [0, 1], [a, b]
140
192
  14
141
- end.must_equal 14
193
+ end).must_equal(14)
142
194
  end
143
195
 
144
196
  it "should rescue any exception that was raised and return it" do
145
- ExceptionalSynchrony::CallbackExceptions.return_exception do
197
+ expect(ExceptionalSynchrony::CallbackExceptions.return_exception do
146
198
  raise ArgumentError, "An argument error occurred"
147
- end.inspect.must_equal ArgumentError.new("An argument error occurred").inspect
199
+ end.inspect).must_equal(ArgumentError.new("An argument error occurred").inspect)
148
200
  end
149
201
  end
150
202
  end
@@ -3,10 +3,39 @@ require_relative '../test_helper'
3
3
  describe ExceptionalSynchrony::EventMachineProxy do
4
4
  include TestHelper
5
5
 
6
+ class RunProxyMock
7
+ class << self
8
+ def run(&block)
9
+ block.call
10
+ :run
11
+ end
12
+
13
+ def error_handler
14
+ end
15
+ end
16
+ end
17
+
18
+ class SynchronyProxyMock < RunProxyMock
19
+ class << self
20
+ def synchrony(&block)
21
+ block.call
22
+ :synchrony
23
+ end
24
+ end
25
+ end
26
+
27
+ def stop_em_after_defers_finish!(em)
28
+ check_finished_counter = 0
29
+ em.add_periodic_timer(0.1) do
30
+ (check_finished_counter += 1) > 20 and raise "defer never finished!"
31
+ em.defers_finished? and em.stop
32
+ end
33
+ end
34
+
6
35
  before do
7
36
  @em = ExceptionalSynchrony::EventMachineProxy.new(EventMachine, nil)
8
37
  @yielded_value = nil
9
- @block = lambda { |value| @yielded_value = value }
38
+ @block = -> (value) { @yielded_value = value }
10
39
  end
11
40
 
12
41
  it "should proxy add_timer" do
@@ -39,7 +68,68 @@ describe ExceptionalSynchrony::EventMachineProxy do
39
68
  ServerClass = Class.new
40
69
  mock(EventMachine).connect(ServerClass, 8080, :handler, :extra_arg).yields(:called)
41
70
  @em.connect(ServerClass, 8080, :handler, :extra_arg, &@block)
42
- @yielded_value.must_equal :called
71
+ expect(@yielded_value).must_equal(:called)
72
+ end
73
+
74
+ describe "#yield_to_reactor" do
75
+ it "should give control to other threads when the reactor is running" do
76
+ mock(@em).reactor_running? { true }
77
+ mock(EventMachine::Synchrony).sleep(0)
78
+ @em.yield_to_reactor
79
+ end
80
+
81
+ it "should be a no-op if the reactor is not running" do
82
+ mock(@em).reactor_running? { false }
83
+ stub(EventMachine::Synchrony).sleep(0) { raise "Should not sleep!" }
84
+ @em.yield_to_reactor
85
+ end
86
+ end
87
+
88
+ describe "#defer" do
89
+ before do
90
+ logger = Logger.new(STDERR)
91
+ logger.extend ContextualLogger::LoggerMixin
92
+ ExceptionHandling.logger = logger
93
+ end
94
+
95
+ it "should output its block's output when it doesn't raise an error, by default" do
96
+ @em.run do
97
+ assert_equal 12, @em.defer("#defer success") { 12 }
98
+ @em.stop
99
+ end
100
+ end
101
+
102
+ it "should not wait for its block to run if option is passed" do
103
+ @block_ran = false
104
+
105
+ @em.run do
106
+ assert_nil @em.defer("#defer success", wait_for_result: false) { @block_ran = true; 12 }
107
+ refute @block_ran
108
+ stop_em_after_defers_finish!(@em)
109
+ end
110
+
111
+ assert @block_ran
112
+ end
113
+
114
+ it "should handle exceptions when not waiting for its block to run" do
115
+ mock(ExceptionHandling).log_error(is_a(RuntimeError), "defer", {})
116
+
117
+ @em.run do
118
+ assert_nil @em.defer("#defer success", wait_for_result: false) { raise RuntimeError, "error in defer" }
119
+ stop_em_after_defers_finish!(@em)
120
+ end
121
+ end
122
+
123
+ it "should raise an error when its block raises an error" do
124
+ @em.run do
125
+ ex = assert_raises(ArgumentError) do
126
+ @em.defer("#defer raising an error") { raise ArgumentError, "!!!" }
127
+ end
128
+
129
+ assert_equal "!!!", ex.message
130
+ @em.stop
131
+ end
132
+ end
43
133
  end
44
134
 
45
135
  EXCEPTION = ArgumentError.new('in block')
@@ -50,45 +140,68 @@ describe ExceptionalSynchrony::EventMachineProxy do
50
140
  end
51
141
 
52
142
  it "add_timer" do
53
- mock(ExceptionHandling).log_error(EXCEPTION, "add_timer")
143
+ mock(ExceptionHandling).log_error(EXCEPTION, "add_timer", {})
54
144
  mock(EventMachine::Synchrony).add_timer(10) { |duration, *args| args.first.call }
55
145
  @em.add_timer(10) { raise EXCEPTION }
56
146
  end
57
147
 
58
148
  it "add_periodic_timer" do
59
- mock(ExceptionHandling).log_error(EXCEPTION, "add_periodic_timer")
149
+ mock(ExceptionHandling).log_error(EXCEPTION, "add_periodic_timer", {})
60
150
  mock(EventMachine::Synchrony).add_periodic_timer(10) { |duration, *args| args.first.call }
61
151
  @em.add_periodic_timer(10) { raise EXCEPTION }
62
152
  end
63
153
 
64
154
  it "next_tick" do
65
- mock(ExceptionHandling).log_error(EXCEPTION, "next_tick")
155
+ mock(ExceptionHandling).log_error(EXCEPTION, "next_tick", {})
66
156
  mock(EventMachine::Synchrony).next_tick { |*args| args.first.call }
67
157
  @em.next_tick { raise EXCEPTION }
68
158
  end
69
159
  end
70
160
 
71
- [false, true].each do |synchrony|
72
- describe "synchrony = #{synchrony}" do
73
- it "should dispatch to the proxy's synchrony method instead of run iff synchrony" do
74
- proxy_mock = Struct.new(:proxy, :class_connection) do
75
- if synchrony
76
- def self.synchrony(&block)
77
- block.(:synchrony)
161
+ { synchrony: SynchronyProxyMock, run: RunProxyMock }.each do |method, proxy_mock|
162
+ describe "run" do
163
+ before do
164
+ @proxy = ExceptionalSynchrony::EventMachineProxy.new(proxy_mock, nil)
165
+ end
166
+
167
+ it "should raise ArgumentError if on_error has invalid value" do
168
+ assert_raises(ArgumentError, "Invalid on_error: :ignore, must be :log or :raise") do
169
+ @proxy.run(on_error: :ignore)
170
+ end
171
+ end
172
+
173
+ describe "without error" do
174
+ [:log, :raise].each do |on_error|
175
+ describe "when using #{method} and on_error = #{on_error}" do
176
+ it "should dispatch to the proxy's synchrony method instead of run iff synchrony" do
177
+ dispatched = false
178
+ assert_equal method, (@proxy.run(on_error: on_error) { dispatched = true })
179
+ assert_equal true, dispatched
78
180
  end
79
181
  end
182
+ end
183
+ end
80
184
 
81
- def self.run(&block)
82
- block.(:run)
83
- end
185
+ describe "with error" do
186
+ before do
187
+ set_test_const('ExceptionalSynchrony::EventMachineProxy::WRAP_WITH_ENSURE_COMPLETELY_SAFE', true)
84
188
  end
85
189
 
86
- mock(proxy_mock).error_handler
190
+ describe "when using #{method} and on_error = :log" do
191
+ it "should rescue any exceptions and log them" do
192
+ mock(ExceptionHandling).log_error(EXCEPTION, "run_with_error_logging", {})
87
193
 
88
- proxy = ExceptionalSynchrony::EventMachineProxy.new(proxy_mock, nil)
194
+ @proxy.run(on_error: :log) { raise EXCEPTION }
195
+ end
196
+ end
89
197
 
90
- proxy.run(&@block)
91
- @yielded_value.must_equal synchrony ? :synchrony : :run
198
+ describe "when using #{method} and on_error = :raise" do
199
+ it "should rescue any exceptions and raise FatalRunError" do
200
+ assert_raises(ExceptionalSynchrony::FatalRunError, "Fatal EventMachine run error") do
201
+ @proxy.run(on_error: :raise) { raise EXCEPTION }
202
+ end
203
+ end
204
+ end
92
205
  end
93
206
  end
94
207
  end