smoke_signals 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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!
@@ -0,0 +1,4 @@
1
+ desc "Run all tests"
2
+ task :test do
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test', 'smoke_signals_test'))
4
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'smoke_signals'
4
+ require 'test/unit'
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