smoke_signals 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +19 -0
- data/README.textile +168 -0
- data/Rakefile +4 -0
- data/lib/smoke_signals.rb +224 -0
- data/smoke_signals.gemspec +20 -0
- data/test/smoke_signals_test.rb +212 -0
- data/test/test_helper.rb +4 -0
- metadata +63 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Jonathan Tran
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
h1. SmokeSignals
|
2
|
+
|
3
|
+
SmokeSignals is an implementation of Lisp-style conditions and restarts as a Ruby library. Conditions and restarts make it easy to separate policy of error recovery from implementation of error recovery. If you're unfamiliar with the concept, check out the chapter from "Practical Common Lisp":http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html.
|
4
|
+
|
5
|
+
SmokeSignals is different because:
|
6
|
+
|
7
|
+
* conditions are not errors (although they can be)
|
8
|
+
* signaling a condition does not unravel the stack (although it can)
|
9
|
+
* conditions can be handled multiple times at different levels of the call stack (or not at all)
|
10
|
+
* restarts can be established at any level in the call stack, not just where the condition is signaled
|
11
|
+
* implementation of signaling, handling, and restarting is completely hidden. (The only possible exception to this is a design decision which allows @ensure@ blocks to work, making this usable with real side-effectful programs.)
|
12
|
+
|
13
|
+
h2. Requirements
|
14
|
+
|
15
|
+
Ruby 1.8.7 or 1.9. No other gem dependencies.
|
16
|
+
|
17
|
+
h2. Installation
|
18
|
+
|
19
|
+
<pre><code>gem install smoke_signals</code></pre>
|
20
|
+
|
21
|
+
h2. Usage
|
22
|
+
|
23
|
+
<pre><code>require 'smoke_signals'</code></pre>
|
24
|
+
|
25
|
+
In a low-level function, signal a condition.
|
26
|
+
|
27
|
+
<pre><code>def parse_entry(line)
|
28
|
+
SmokeSignals::Condition.new.signal! unless satisfies_preconditions?(line)
|
29
|
+
# Do actual parsing...
|
30
|
+
end
|
31
|
+
</code></pre>
|
32
|
+
|
33
|
+
In a mid-level function, implement ways to recover from the condition. This is the mechanism of recovery that is tied to the implementation of the mid-level function.
|
34
|
+
|
35
|
+
<pre><code>def parse_log_file(filename)
|
36
|
+
File.open(filename) do |io|
|
37
|
+
io.lines.map {|line|
|
38
|
+
SmokeSignals.with_restarts(:ignore_entry => lambda { nil },
|
39
|
+
:use_value => lambda {|v| v } ) do
|
40
|
+
parse_entry(line)
|
41
|
+
end
|
42
|
+
}.compact
|
43
|
+
end
|
44
|
+
end
|
45
|
+
</code></pre>
|
46
|
+
|
47
|
+
In a high-level function, handle the condition. This sets the policy of recovery without being exposed to the underlying implementation of the mid-level function.
|
48
|
+
|
49
|
+
<pre><code>def analyze_log_file(filename)
|
50
|
+
entries = SmokeSignals.handle(lambda {|c| c.restart(:ignore_entry) }) do
|
51
|
+
parse_log_file(filename)
|
52
|
+
end
|
53
|
+
# Do something interesting with entries...
|
54
|
+
end
|
55
|
+
</code></pre>
|
56
|
+
|
57
|
+
Signaling a condition does not have to be fatal.
|
58
|
+
|
59
|
+
<pre><code># If no handlers are set, this will do nothing.
|
60
|
+
SmokeSignals::Condition.new.signal
|
61
|
+
</code></pre>
|
62
|
+
|
63
|
+
The bang flavor will raise unless it is rescued or restarted.
|
64
|
+
|
65
|
+
<pre><code># This is a fatal signal.
|
66
|
+
SmokeSignals::Condition.new.signal!
|
67
|
+
</code></pre>
|
68
|
+
|
69
|
+
Since you can handle signals multiple times by different handlers at multiple levels in the call stack, simply handling a fatal signal and returning normally is not enough. You must either rescue it or restart it.
|
70
|
+
|
71
|
+
Rescuing a condition is just like rescuing an exception with a @rescue@ block. It returns the value from the entire @handle@ block.
|
72
|
+
|
73
|
+
<pre><code>x = SmokeSignals.handle(lambda {|c| c.rescue(42) }) do
|
74
|
+
SmokeSignals::Condition.new.signal!
|
75
|
+
end
|
76
|
+
# x is 42
|
77
|
+
</code></pre>
|
78
|
+
|
79
|
+
If you were using exceptions, you might've done this...
|
80
|
+
|
81
|
+
<pre><code>x = begin
|
82
|
+
raise 'foo'
|
83
|
+
rescue
|
84
|
+
42
|
85
|
+
end
|
86
|
+
# x is 42
|
87
|
+
</code></pre>
|
88
|
+
|
89
|
+
You can limit which kinds of conditions you handle by passing a hash to @handle@.
|
90
|
+
|
91
|
+
<pre><code>class MyCondition1 < SmokeSignals::Condition; end
|
92
|
+
class MyCondition2 < SmokeSignals::Condition; end
|
93
|
+
|
94
|
+
SmokeSignals.handle(MyCondition1 => lambda {|c| c.restart(:some_restart) },
|
95
|
+
MyCondition2 => lambda {|c| c.restart(:another_restart) }) do
|
96
|
+
MyCondition1.new.signal if some_condition?
|
97
|
+
MyCondition2.new.signal if another_condition?
|
98
|
+
end
|
99
|
+
</code></pre>
|
100
|
+
|
101
|
+
By default @MyCondition1 === condition that was signaled@ is used to determine whether a handler applies or not, kind of like a @case@. You can change the default behavior by overriding @Condition#handle_by(handler)@. Either return a @Proc@ to handle it or @nil@.
|
102
|
+
|
103
|
+
You can handle a signal multiple times by returning normally from your handler. Doing this you can, for example, observe the fact that a condition has been signaled without otherwise having any effect on control flow.
|
104
|
+
|
105
|
+
<pre><code>SmokeSignals.handle(lambda {|c| puts 'this is run 2nd' }) do
|
106
|
+
SmokeSignals.handle(lambda {|c| puts 'this is run 1st' }) do
|
107
|
+
begin
|
108
|
+
SmokeSignals::Condition.new.signal
|
109
|
+
puts 'this is run 3rd because no handlers called rescue or restart'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
</code></pre>
|
114
|
+
|
115
|
+
In the case of an @ensure@ block, it is executed _after_ any handlers. It must be executed afterwards because the whole point of signal handlers is that they are run _before_ the stack is unwound. At that point, a signal handler may choose to rescue, restart, or return normally to allow other handlers to execute. In contrast, by the time an exception is caught, rescuing is not an option; it's a necessity.
|
116
|
+
|
117
|
+
<pre><code>SmokeSignals.handle(lambda {|c| puts 'this is run 2nd' }) do
|
118
|
+
SmokeSignals.handle(lambda {|c| puts 'this is run 1st' }) do
|
119
|
+
begin
|
120
|
+
SmokeSignals::Condition.new.signal
|
121
|
+
puts 'this is run 3rd because no handlers called rescue or restart'
|
122
|
+
ensure
|
123
|
+
puts 'this is run last'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
</code></pre>
|
128
|
+
|
129
|
+
@ensure@ blocks are executed after handlers, but they are executed _before_ restarts. To see why this design decision was made, consider this example.
|
130
|
+
|
131
|
+
<pre><code>def parse_file(filename)
|
132
|
+
SmokeSignals.with_restarts(:use_new_filename => lambda {|f| parse_file(f) }) do
|
133
|
+
file = nil
|
134
|
+
begin
|
135
|
+
file = File.open(filename)
|
136
|
+
if file.lines.first == '#!/keyword'
|
137
|
+
# Parse file
|
138
|
+
else
|
139
|
+
SmokeSignals::Condition.new.signal!
|
140
|
+
end
|
141
|
+
ensure
|
142
|
+
file.close if file
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
</code></pre>
|
147
|
+
|
148
|
+
If this function were called and restarted many times, and the stack were not unwound before each restart, then you would have many files open at once. This is why SmokeSignals unwinds the stack before executing restarts, meaning that @ensure@ blocks are run before restarts.
|
149
|
+
|
150
|
+
h3. Is SmokeSignals a replacement for Ruby exceptions?
|
151
|
+
|
152
|
+
Short answer: no, they're an extension.
|
153
|
+
|
154
|
+
Long answer... As shown above, you can achieve all the functionality of exceptions with SmokeSignals.
|
155
|
+
|
156
|
+
However, you're probably using some code that doesn't know about SmokeSignals and raises exceptions instead. Setting a condition handler will not handle these raised exceptions. They couldn't because in such a case, restarting would be impossible and rescuing would be a necessity. By the time an exception is handled, the stack has already been unwound.
|
157
|
+
|
158
|
+
h2. Thread Safety
|
159
|
+
|
160
|
+
This library is thread-safe because each thread has its own handlers and restarts. You cannot signal in one thread and handle it in another thread.
|
161
|
+
|
162
|
+
h2. Running Tests
|
163
|
+
|
164
|
+
<pre><code>rake test</code></pre>
|
165
|
+
|
166
|
+
h2. Special Thanks
|
167
|
+
|
168
|
+
This was inspired in part by "dynamic_vars":https://github.com/robdimarco/dynamic_vars, an implementation of thread-local dynamic bindings in Ruby!
|
data/Rakefile
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
class SmokeSignals
|
2
|
+
|
3
|
+
# This is raised by Condition#signal! if no handler rescues or
|
4
|
+
# restarts.
|
5
|
+
class UnhandledSignalError < RuntimeError
|
6
|
+
attr_accessor :condition
|
7
|
+
def initialize(condition)
|
8
|
+
super('condition was not rescued or restarted by any handlers')
|
9
|
+
self.condition = condition
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# This is raised when a signal handler attempts to execute a restart
|
14
|
+
# that has not been established.
|
15
|
+
class NoRestartError < RuntimeError
|
16
|
+
attr_accessor :restart_name
|
17
|
+
def initialize(restart_name)
|
18
|
+
super("no established restart with name: #{restart_name}")
|
19
|
+
self.restart_name = restart_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# You should never rescue this exception or any of its subclasses in
|
24
|
+
# normal usage. If you do, you should re-raise it. It is raised to
|
25
|
+
# unwind the stack and allow +ensure+ blocks to execute as you would
|
26
|
+
# expect. Normally, you should not be rescuing Exception, anyway,
|
27
|
+
# without re-raising it. A bare +rescue+ clause only rescues
|
28
|
+
# StandardError, a subclass of Exception, which is probably what you
|
29
|
+
# want.
|
30
|
+
class StackUnwindException < Exception
|
31
|
+
attr_reader :nonce
|
32
|
+
def initialize(nonce)
|
33
|
+
super("This exception is an implementation detail of SmokeSignals. If you're seeing this, either there is a bug in SmokeSignals or you are rescuing a #{self.class} when you shouldn't be. If you rescue this, you should re-raise it.")
|
34
|
+
@nonce = nonce
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# You should never rescue this exception. See StackUnwindException.
|
39
|
+
class RescueException < StackUnwindException
|
40
|
+
attr_reader :return_value
|
41
|
+
def initialize(nonce, return_value)
|
42
|
+
super(nonce)
|
43
|
+
@return_value = return_value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# You should never rescue this exception. See StackUnwindException.
|
48
|
+
class RestartException < StackUnwindException
|
49
|
+
attr_reader :restart_receiver, :restart_args
|
50
|
+
def initialize(nonce, restart_receiver, restart_args)
|
51
|
+
super(nonce)
|
52
|
+
@restart_receiver = restart_receiver
|
53
|
+
@restart_args = restart_args
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Extensible #:nodoc:
|
58
|
+
def metaclass
|
59
|
+
class << self; self; end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# This is the base class for all conditions.
|
64
|
+
class Condition
|
65
|
+
|
66
|
+
attr_accessor :nonce
|
67
|
+
|
68
|
+
# Signals this Condition.
|
69
|
+
def signal
|
70
|
+
SmokeSignals.signal(self, false)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Signals this Condition. If it is not rescued or restarted by a
|
74
|
+
# handler, UnhandledSignalError is raised.
|
75
|
+
def signal!
|
76
|
+
SmokeSignals.signal(self, true)
|
77
|
+
end
|
78
|
+
|
79
|
+
# This should only be called from within a signal handler. It
|
80
|
+
# unwinds the stack to the point where SmokeSignals::handle was
|
81
|
+
# called and returns from SmokeSignals::handle with the given
|
82
|
+
# return value.
|
83
|
+
def rescue(return_value=nil)
|
84
|
+
raise RescueException.new(self.nonce, return_value)
|
85
|
+
end
|
86
|
+
|
87
|
+
# This should only be called from within a signal handler. It
|
88
|
+
# unwinds the stack up to the point where
|
89
|
+
# SmokeSignals::with_restarts was called establishing the given
|
90
|
+
# restart, calls the restart with the given arguments, and returns
|
91
|
+
# the restart's return value from SmokeSignals::with_restarts.
|
92
|
+
def restart(name, *args)
|
93
|
+
SmokeSignals.restart(name, *args)
|
94
|
+
end
|
95
|
+
|
96
|
+
# When a Condition is signaled, this method is called by the
|
97
|
+
# internals of SmokeSignals to determine whether it should be
|
98
|
+
# handled by a given handler.
|
99
|
+
#
|
100
|
+
# If you override this method in subclasses of Condition, return a
|
101
|
+
# Proc taking the Condition as an argument that should be run to
|
102
|
+
# handle the signal. Return nil to ignore the signal.
|
103
|
+
def handle_by(handler)
|
104
|
+
if handler.is_a?(Proc)
|
105
|
+
# No pattern given, so handler applies to everything.
|
106
|
+
handler
|
107
|
+
else
|
108
|
+
applies_to, handler_fn = handler
|
109
|
+
applies = case applies_to
|
110
|
+
when Proc
|
111
|
+
applies_to.call(self)
|
112
|
+
when Array
|
113
|
+
applies_to.any? {|a| a === self }
|
114
|
+
else
|
115
|
+
applies_to === self
|
116
|
+
end
|
117
|
+
applies ? handler_fn : nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
|
125
|
+
# Establishes one or more signal handlers for the given block and
|
126
|
+
# executes it. Returns either the return value of the block or
|
127
|
+
# the value passed to Condition#rescue in a handler.
|
128
|
+
def handle(*new_handlers, &block)
|
129
|
+
orig_handlers = handlers
|
130
|
+
nonce = Object.new
|
131
|
+
if new_handlers.last.is_a?(Hash)
|
132
|
+
new_handlers.pop.reverse_each {|entry| new_handlers.push(entry) }
|
133
|
+
end
|
134
|
+
self.handlers = orig_handlers + new_handlers.map {|entry| [entry,nonce] }.reverse
|
135
|
+
begin
|
136
|
+
block.call
|
137
|
+
rescue RescueException => e
|
138
|
+
if nonce.equal?(e.nonce)
|
139
|
+
e.return_value
|
140
|
+
else
|
141
|
+
raise e
|
142
|
+
end
|
143
|
+
ensure
|
144
|
+
self.handlers = orig_handlers
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def signal(c, raise_unless_handled)
|
149
|
+
# Most recently set handlers are run first.
|
150
|
+
handlers.reverse_each do |handler, nonce|
|
151
|
+
# Check if the condition being signaled applies to this
|
152
|
+
# handler.
|
153
|
+
handler_fn = c.handle_by(handler)
|
154
|
+
next unless handler_fn
|
155
|
+
|
156
|
+
c.nonce = nonce
|
157
|
+
handler_fn.call(c)
|
158
|
+
end
|
159
|
+
raise UnhandledSignalError.new(c) if raise_unless_handled
|
160
|
+
end
|
161
|
+
|
162
|
+
# Establishes one or more restarts for the given block and
|
163
|
+
# executes it. Returns either the return value of the block or
|
164
|
+
# that of the restart if one was run.
|
165
|
+
def with_restarts(extension, &block)
|
166
|
+
orig_restarts = restarts
|
167
|
+
nonce = Object.new
|
168
|
+
if extension.is_a?(Proc)
|
169
|
+
new_restarts = Extensible.new
|
170
|
+
new_restarts.metaclass.instance_eval { include Module.new(&extension) }
|
171
|
+
else
|
172
|
+
new_restarts = extension
|
173
|
+
end
|
174
|
+
|
175
|
+
self.restarts = orig_restarts + [[new_restarts,nonce]]
|
176
|
+
begin
|
177
|
+
block.call
|
178
|
+
rescue RestartException => e
|
179
|
+
if nonce.equal?(e.nonce)
|
180
|
+
e.restart_receiver.send(*e.restart_args)
|
181
|
+
else
|
182
|
+
raise e
|
183
|
+
end
|
184
|
+
ensure
|
185
|
+
self.restarts = orig_restarts
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def restart(name, *args)
|
190
|
+
restarts.reverse_each do |restarts_obj, nonce|
|
191
|
+
obj, all_args = case restarts_obj
|
192
|
+
when Extensible
|
193
|
+
restarts_obj.respond_to?(name) ? [restarts_obj, [name] + args] : nil
|
194
|
+
else
|
195
|
+
fn = restarts_obj[name]
|
196
|
+
fn ? [fn, [:call] + args] : nil
|
197
|
+
end
|
198
|
+
next unless obj
|
199
|
+
raise RestartException.new(nonce, obj, all_args)
|
200
|
+
end
|
201
|
+
raise NoRestartError.new(name)
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def handlers
|
207
|
+
Thread.current[:SmokeSignalsHandlers] ||= []
|
208
|
+
end
|
209
|
+
|
210
|
+
def handlers=(arr)
|
211
|
+
Thread.current[:SmokeSignalsHandlers] = arr
|
212
|
+
end
|
213
|
+
|
214
|
+
def restarts
|
215
|
+
Thread.current[:SmokeSignalsRestarts] ||= []
|
216
|
+
end
|
217
|
+
|
218
|
+
def restarts=(arr)
|
219
|
+
Thread.current[:SmokeSignalsRestarts] = arr
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env gem build
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "smoke_signals"
|
6
|
+
s.version = "0.9.0"
|
7
|
+
s.authors = ["Jonathan Tran"]
|
8
|
+
s.homepage = "http://github.com/jtran/smoke_signals"
|
9
|
+
s.summary = "Lisp-style conditions and restarts for Ruby"
|
10
|
+
s.description = "SmokeSignals makes it easy to separate policy of error recovery from implementation of error recovery."
|
11
|
+
s.email = Base64.decode64("anRyYW5AYWx1bW5pLmNtdS5lZHU=\n")
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
# Ruby version
|
19
|
+
s.required_ruby_version = '>= 1.8.7'
|
20
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
class SmokeSignalsTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
S = SmokeSignals
|
6
|
+
C = SmokeSignals::Condition
|
7
|
+
|
8
|
+
def setup
|
9
|
+
# This prevents brokenness in one test from affecting others.
|
10
|
+
Thread.current[:SmokeSignalsHandlers] = nil
|
11
|
+
Thread.current[:SmokeSignalsRestarts] = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_unhandled_signal_is_ignored
|
15
|
+
C.new.signal
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_unhandled_signal_raises_for_bang_version
|
19
|
+
assert_raise SmokeSignals::UnhandledSignalError do
|
20
|
+
C.new.signal!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_handle_condition
|
25
|
+
r = S.handle(lambda {|c| c.rescue(42) }) do
|
26
|
+
C.new.signal!
|
27
|
+
end
|
28
|
+
assert_equal 42, r
|
29
|
+
assert_equal [], S.class_eval { handlers }
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_handle_condition_multiple_times
|
33
|
+
a = []
|
34
|
+
r = S.handle(lambda {|c| a << 8; c.rescue(42) }) do
|
35
|
+
S.handle(lambda {|c| a << 7 }) do
|
36
|
+
C.new.signal!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
assert_equal 42, r
|
40
|
+
assert_equal [7, 8], a
|
41
|
+
assert_equal [], S.class_eval { handlers }
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_unsignaled_handlers_do_not_run
|
45
|
+
a = []
|
46
|
+
r = S.handle(String => lambda {|c| a << 9; c.rescue(9) },
|
47
|
+
C => lambda {|c| a << 7 }) do
|
48
|
+
C.new.signal
|
49
|
+
42
|
50
|
+
end
|
51
|
+
assert_equal [7], a
|
52
|
+
assert_equal r, 42
|
53
|
+
assert_equal [], S.class_eval { handlers }
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_unestablished_restart_raises
|
57
|
+
assert_raise SmokeSignals::NoRestartError do
|
58
|
+
S.handle(lambda {|c| c.restart(:some_restart_name) }) do
|
59
|
+
C.new.signal!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_restart_with_hash
|
65
|
+
r = S.handle(lambda {|c| c.restart(:use_square_of_value, 4) }) do
|
66
|
+
r2 = S.with_restarts(:use_square_of_value => lambda {|v| v * v }) do
|
67
|
+
C.new.signal!
|
68
|
+
end
|
69
|
+
assert_equal 16, r2
|
70
|
+
r2 + 1
|
71
|
+
end
|
72
|
+
assert_equal 17, r
|
73
|
+
assert_equal [], S.class_eval { handlers }
|
74
|
+
assert_equal [], S.class_eval { restarts }
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_restart_with_proc
|
78
|
+
r = S.handle(lambda {|c| c.restart(:use_square_of_value, 4) }) do
|
79
|
+
r2 = S.with_restarts(proc {
|
80
|
+
def use_square_of_value(v)
|
81
|
+
v * v
|
82
|
+
end
|
83
|
+
|
84
|
+
def use_nil
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
}) do
|
88
|
+
C.new.signal!
|
89
|
+
end
|
90
|
+
assert_equal 16, r2
|
91
|
+
r2 + 1
|
92
|
+
end
|
93
|
+
assert_equal 17, r
|
94
|
+
assert_equal [], S.class_eval { handlers }
|
95
|
+
assert_equal [], S.class_eval { restarts }
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_restart_multiple_times
|
99
|
+
a = [:use_square_of_value, :use_value]
|
100
|
+
b = []
|
101
|
+
a.each do |name|
|
102
|
+
S.handle(lambda {|c| c.restart(name, 4) }) do
|
103
|
+
r = S.with_restarts(:use_square_of_value => lambda {|v| v * v },
|
104
|
+
:use_value => lambda {|v| v }) do
|
105
|
+
C.new.signal!
|
106
|
+
end
|
107
|
+
b << r
|
108
|
+
end
|
109
|
+
end
|
110
|
+
assert_equal [16, 4], b
|
111
|
+
assert_equal [], S.class_eval { handlers }
|
112
|
+
assert_equal [], S.class_eval { restarts }
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_rescuing_executes_ensure_block
|
116
|
+
a = []
|
117
|
+
file = nil
|
118
|
+
r = S.handle(lambda {|c| a << 7; c.rescue(42) }) do
|
119
|
+
begin
|
120
|
+
file = 'fake_file'
|
121
|
+
C.new.signal
|
122
|
+
fail 'should be handled with a rescue'
|
123
|
+
ensure
|
124
|
+
file = 'closed'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
assert_equal 42, r
|
128
|
+
assert_equal 'closed', file
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_restarting_executes_ensure_block
|
132
|
+
a = []
|
133
|
+
file = nil
|
134
|
+
r = S.handle(lambda {|c| c.restart(:use_value, 42) }) do
|
135
|
+
S.with_restarts(:use_value => lambda {|v| v }) do
|
136
|
+
begin
|
137
|
+
file = 'fake_file'
|
138
|
+
C.new.signal
|
139
|
+
fail 'should be handled with a restart'
|
140
|
+
ensure
|
141
|
+
file = 'closed'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
assert_equal 42, r
|
146
|
+
assert_equal 'closed', file
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_restarting_executes_ensure_block_before_restart
|
150
|
+
a = []
|
151
|
+
r = S.handle(lambda {|c| c.restart(:use_value, 42) }) do
|
152
|
+
S.with_restarts(:use_value => lambda {|v| a << 2; v }) do
|
153
|
+
begin
|
154
|
+
C.new.signal
|
155
|
+
fail 'should be handled with a restart'
|
156
|
+
ensure
|
157
|
+
a << 1
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
assert_equal 42, r
|
162
|
+
assert_equal [1, 2], a
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_rescuing_from_nested_handlers
|
166
|
+
a = []
|
167
|
+
r = S.handle(C => lambda {|c| a << 3; c.rescue(42) }) do
|
168
|
+
S.handle(String => lambda {|c| a << 4; c.rescue(5) }) do
|
169
|
+
C.new.signal!
|
170
|
+
end
|
171
|
+
fail 'should be handled with a rescue'
|
172
|
+
end
|
173
|
+
assert_equal 42, r
|
174
|
+
assert_equal [3], a
|
175
|
+
end
|
176
|
+
|
177
|
+
def test_restarting_from_nested_restarts
|
178
|
+
a = []
|
179
|
+
r = S.handle(lambda {|c| a << 3; c.restart(:use_value, 42) }) do
|
180
|
+
r2 = S.with_restarts(:use_value => lambda {|v| a << 4; v }) do
|
181
|
+
S.with_restarts(:use_square_of_value => lambda {|v| a << 5; v * v }) do
|
182
|
+
C.new.signal!
|
183
|
+
fail 'should be handled with a restart'
|
184
|
+
end
|
185
|
+
fail 'should be handled with a restart'
|
186
|
+
end
|
187
|
+
assert_equal [3, 4], a
|
188
|
+
assert_equal 42, r2
|
189
|
+
a << 6
|
190
|
+
7
|
191
|
+
end
|
192
|
+
assert_equal [3, 4, 6], a
|
193
|
+
assert_equal 7, r
|
194
|
+
end
|
195
|
+
|
196
|
+
def test_handle_in_multiple_threads
|
197
|
+
S.handle(lambda {|c| 7 }) do
|
198
|
+
t = Thread.new { assert_equal [], S.class_eval { handlers } }
|
199
|
+
assert_equal 1, S.class_eval { handlers.size }
|
200
|
+
t.join
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_restart_in_multiple_threads
|
205
|
+
S.with_restarts(:use_value => lambda {|v| v }) do
|
206
|
+
t = Thread.new { assert_equal [], S.class_eval { restarts } }
|
207
|
+
assert_equal 1, S.class_eval { restarts.size }
|
208
|
+
t.join
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smoke_signals
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.9.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Tran
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-03-27 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: SmokeSignals makes it easy to separate policy of error recovery from implementation of error recovery.
|
18
|
+
email: jtran@alumni.cmu.edu
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- LICENSE.txt
|
27
|
+
- README.textile
|
28
|
+
- Rakefile
|
29
|
+
- lib/smoke_signals.rb
|
30
|
+
- smoke_signals.gemspec
|
31
|
+
- test/smoke_signals_test.rb
|
32
|
+
- test/test_helper.rb
|
33
|
+
has_rdoc: true
|
34
|
+
homepage: http://github.com/jtran/smoke_signals
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.8.7
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.6.2
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Lisp-style conditions and restarts for Ruby
|
61
|
+
test_files:
|
62
|
+
- test/smoke_signals_test.rb
|
63
|
+
- test/test_helper.rb
|