remotely_exceptional 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5db0afaa461aa92d936cae11184f651757c23e50
4
+ data.tar.gz: 6b8dfd4d4a3ff0c8602e435126de50e8ccc5dc70
5
+ SHA512:
6
+ metadata.gz: 5697edf8df114e0a6c5c1953f76ae740e23dd69e6fc552dda3198e03e14a3a198f57d2e43f70be73fc5410afe2fc969333a20162d4190e333eb8160ac47fd084
7
+ data.tar.gz: 2a3ed81301611e4cff979f3a20b7ad7335d749b7751e7066a0cb95c70493334350f9d32b30b6f6245eb088a82ec7185229240d008071f72fe78a81b60622602b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :doc do
6
+ gem "yard"
7
+ end
8
+
9
+ group :test do
10
+ gem "coveralls", :require => false
11
+ gem "guard"
12
+ gem "guard-minitest"
13
+ gem "minitest", ">= 3.0"
14
+ gem "mocha"
15
+ gem "simplecov", :require => false
16
+ end
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ gem_name = File.basename(Dir[File.expand_path("../*.gemspec", __FILE__)].first)[0..-9]
2
+
3
+ guard(:minitest, :all_after_pass => false, :all_on_start => false) do
4
+ watch(%r{^lib/#{gem_name}/(.+)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
5
+ watch(%r{^test/.+_test\.rb$})
6
+ watch(%r{^(?:test/test_helper(.*)|lib/#{gem_name})\.rb$}) { "test" }
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Danny Guinther
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # RemotelyExceptional
2
+ [![Gem Version](https://badge.fury.io/rb/remotely_exceptional.svg)](http://badge.fury.io/rb/remotely_exceptional)
3
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/remotely_exceptional)
4
+ [![Build Status](https://travis-ci.org/tdg5/remotely_exceptional.svg)](https://travis-ci.org/tdg5/remotely_exceptional)
5
+ [![Coverage Status](https://coveralls.io/repos/tdg5/remotely_exceptional/badge.svg)](https://coveralls.io/r/tdg5/remotely_exceptional)
6
+ [![Code Climate](https://codeclimate.com/github/tdg5/remotely_exceptional/badges/gpa.svg)](https://codeclimate.com/github/tdg5/remotely_exceptional)
7
+ [![Dependency Status](https://gemnasium.com/tdg5/remotely_exceptional.svg)](https://gemnasium.com/tdg5/remotely_exceptional)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem "remotely_exceptional"
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ $ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```bash
26
+ $ gem install remotely_exceptional
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it ( https://github.com/tdg5/remotely_exceptional/fork )
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
36
+ 4. Push to the branch (`git push origin my-new-feature`)
37
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.pattern = "test/**/*_test.rb"
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,11 @@
1
+ require "remotely_exceptional/version"
2
+ require "remotely_exceptional/exceptions"
3
+ require "remotely_exceptional/handler"
4
+ require "remotely_exceptional/remote_handling"
5
+
6
+ # The namespace for the RemotelyExceptional gem.
7
+ module RemotelyExceptional
8
+ # The namespace that specialized handlers live in.
9
+ module Handlers
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module RemotelyExceptional
2
+ class Error < ::RuntimeError
3
+ end
4
+
5
+ class InvalidHandlerResponse < RemotelyExceptional::Error
6
+ attr_accessor :original_exception
7
+ def initialize(*)
8
+ super
9
+ @original_exception = $!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,91 @@
1
+ # Mixin providing basic functionality required for matching and handling
2
+ # exceptions.
3
+ module RemotelyExceptional::Handler
4
+ # Actions that will be taken on any object that includes this module.
5
+ #
6
+ # @param includer [Class,Module] The class or module that has included this
7
+ # module.
8
+ def self.included(includer)
9
+ includer.extend(ClassMethods)
10
+ end
11
+
12
+ # Factory function for creating classes with Handler behaviors. Creates a new
13
+ # class with Handler behaviors from the given super class and block. By
14
+ # default the super class of the new class will be Object. The given block
15
+ # will be used as the matcher of the generated class.
16
+ #
17
+ # @param super_class [Class] An optional super class to use when creating a
18
+ # new class with Handler behaviors.
19
+ # @yieldparam [Exception] exception_instance The exception instance that
20
+ # should be evaluated for a match.
21
+ # @yieldreturn [Boolean] A boolean value indicating whether or not the
22
+ # exception instance was matched.
23
+ # @return [Class] Returns a new class extended with Handler behaviors.
24
+ def self.new(super_class = Object, &block)
25
+ raise ArgumentError, "Block required" unless block_given?
26
+ handler_class = Class.new(super_class)
27
+ handler_class.send(:include, self)
28
+ handler_class.instance_variable_set(:@matcher, block)
29
+ handler_class
30
+ end
31
+
32
+ # Class-level handler behaviors that will be added to any object that includes
33
+ # this module.
34
+ module ClassMethods
35
+ # Used by Ruby's rescue keyword to evaluate if an exception instance can be
36
+ # caught by this Class or Module. Delegates to {#matcher}.
37
+ #
38
+ # @param exception [Exception] The exception instance that should be evaluated
39
+ # for a match.
40
+ # @return [Boolean] Returns a Boolean value indicating whether or not the
41
+ # exception instance matches this handler.
42
+ def ===(exception)
43
+ matcher.call(exception)
44
+ end
45
+
46
+ # Factory method that takes in an exception and an optional Hash of
47
+ # additional contextual information and creates a new Handler instance from
48
+ # that data. The generated Handler instance is then used to handle the the
49
+ # exception.
50
+ #
51
+ # @param exception [Exception] The exception to handle. Defaults to $!.
52
+ # @param context [Hash{Symbol=>Object}] An optional Hash of additional
53
+ # contextual information about the exception.
54
+ # @return [Symbol] Returns a symbol indicating what action should be taken
55
+ # to continue execution. Depending on the situation, valid values include:
56
+ # [:continue, :raise, :retry]
57
+ def handle(exception = $!, context = {})
58
+ instance = new
59
+ context, exception = exception, $! if exception.is_a?(Hash)
60
+ instance.instance_variable_set(:@exception, exception)
61
+ instance.instance_variable_set(:@context, context)
62
+ instance.handle
63
+ end
64
+
65
+ private
66
+
67
+ # The block used by the class to evaluate matching exceptions.
68
+ def matcher
69
+ @matcher
70
+ end
71
+ end
72
+
73
+ attr_reader :context, :exception
74
+
75
+ # Placeholder method, must be implemented by including class. Should
76
+ # encapsulate the logic required to handle an exception matced by the class.
77
+ # Should take no arguments.
78
+ #
79
+ # @raise [NotImplementedError] Raised when the including class does not
80
+ # provide it's own #handle instance method.
81
+ # @return [Symbol] Returns a symbol indicating what action should be taken
82
+ # to continue execution. Depending on the situation, valid values include:
83
+ # [:continue, :raise, :retry]
84
+ # @return [Array<(Symbol, Object)>] Returns a symbol indicating what action
85
+ # should be taken to continue execution and an object that should be used as
86
+ # the result of the rescue operation. Depending on the situation, valid
87
+ # action values include: [:continue, :raise, :retry]
88
+ def handle
89
+ raise NotImplementedError, "#{__method__} must be implemented by including class!"
90
+ end
91
+ end
@@ -0,0 +1,178 @@
1
+ module RemotelyExceptional::Handlers::PrioritizedHandler
2
+ # The default priority that should be used when registering handlers.
3
+ DEFAULT_PRIORITY = 1000.freeze
4
+
5
+ # Hash#default_proc for hashes that should return a new Set if a given key is
6
+ # not defined.
7
+ HASH_BUILDER = lambda { |hash, key| hash[key] = Set.new }.freeze
8
+
9
+ def self.included(includer)
10
+ includer.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ # Determines if any of the available handlers match the provided exception.
15
+ #
16
+ # @param exception [Exception] An exception.
17
+ # @return [Boolean] Returns true if a handler is available that matches the
18
+ # given exception. Returns false if none of the available handlers match the
19
+ # given exception.
20
+ def ===(exception)
21
+ !!handler_for_exception(exception)
22
+ end
23
+
24
+ # Returns the Hash of block handlers by priority.
25
+ #
26
+ # @return [Hash{Integer,Set}] The Hash of priorities and which block handlers
27
+ # belong to those priorities.
28
+ def block_handlers
29
+ Thread.current["#{name}.block_handlers"] ||= Hash.new(&HASH_BUILDER)
30
+ end
31
+
32
+ # The default priority level that should be used for handlers when no priority
33
+ # is provided.
34
+ #
35
+ # @return [Integer] The default priority level.
36
+ def default_priority
37
+ const_get(:DEFAULT_PRIORITY)
38
+ end
39
+
40
+ # Finds the handler with the highest priority that matches the exception and
41
+ # uses this handler to handle the exception. If no handlers are available that
42
+ # match the given exception, the exception is re-raised.
43
+ #
44
+ # @param exception [Exception] The exception to handle.
45
+ # @param context [Hash{Symbol=>Object}] An optional Hash of additional
46
+ # contextual information about the exception.
47
+ # @raise [exception] The given exception is reraised if no handler is found
48
+ # that matches the given exception.
49
+ # @return [Symbol] Returns a symbol indicating what action should be taken
50
+ # to continue execution. Depending on the situation, valid values include:
51
+ # [:continue, :raise, :retry]
52
+ def handle(exception = $!, context = {})
53
+ context, exception = exception, $! if exception.is_a?(Hash)
54
+ priority_handler = handler_for_exception(exception)
55
+ raise exception if !priority_handler
56
+ priority_handler.handle(exception, context)
57
+ end
58
+
59
+ # Finds the handler with the highest priority that matches the given
60
+ # exception. Returns nil if no matching handler can be found.
61
+ #
62
+ # @param exception [Exception] The exception to find a matching handler for.
63
+ # @return [RemotelyExceptional::Handler] Returns the handler with the
64
+ # highest priority that matches the given exception. If no handler is found,
65
+ # returns nil.
66
+ # @return [nil] Returns nil if no matching handler could be found.
67
+ def handler_for_exception(exception)
68
+ prioritized_handlers.detect { |handler| handler === exception }
69
+ end
70
+
71
+ # Returns an enumerator that yields block handlers and registered handlers in
72
+ # priority ASC, name ASC order. The collection is lazily generated, so changes
73
+ # to the sets of handlers may appear during traversal. If consistent state is
74
+ # necessary, force the returned enumerator to eagerly generate the full
75
+ # collection using #to_a or similar.
76
+ #
77
+ # @return [Enumerator<RemotelyExceptional::Handler>] An enumerator of all
78
+ # known block handlers and registered handlers in priority ASC, name ASC
79
+ # order.
80
+ def prioritized_handlers
81
+ Enumerator.new do |yielder|
82
+ priorities = (registered_handlers.keys | block_handlers.keys).sort!
83
+ priorities.uniq!
84
+ priorities.each do |priority|
85
+ if registered_handlers.key?(priority)
86
+ collected_handlers = registered_handlers[priority].to_a
87
+ end
88
+ if block_handlers.key?(priority)
89
+ temp_handlers = block_handlers[priority].to_a
90
+ collected_handlers &&= collected_handlers.concat(temp_handlers)
91
+ collected_handlers ||= temp_handlers
92
+ end
93
+ collected_handlers.sort_by!(&:name)
94
+ collected_handlers.uniq!
95
+ collected_handlers.each { |handler| yielder << handler }
96
+ end
97
+ end
98
+ end
99
+
100
+ # Adds the given handler to the set of registered handlers. Optionally, a
101
+ # priority may be supplied. If no priority is supplied the {::default_priority
102
+ # default priority} is used.
103
+ #
104
+ # @param handler [RemotelyExceptional::Handler] The handler that should be
105
+ # registered.
106
+ # @param options [Hash{Symbol=>Object}] A Hash of optional arguments.
107
+ # @option options [Integer] :priority ({::default_priority}) The priority of
108
+ # the handler.
109
+ # @return [Boolean] Returns true if the handler was successfully registered
110
+ # for the given priority. Returns false if the handler was already registered
111
+ # for the given priority.
112
+ def register_handler(handler, options = {})
113
+ priority = options[:priority] || default_priority
114
+ !!registered_handlers[priority].add?(handler)
115
+ end
116
+
117
+ # Returns the Hash of registered handlers by priority.
118
+ #
119
+ # @return [Hash{Integer,Set<RemotelyExceptional::Handler>}] The Hash of
120
+ # priorities and which handlers belong to those priorities.
121
+ def registered_handlers
122
+ Thread.current["#{name}.registered_handlers"] ||= Hash.new(&HASH_BUILDER)
123
+ end
124
+
125
+ # Removes the given handler. By default removes the handler from the
126
+ # {::default_priority default_priority}, but a :priority option may be
127
+ # supplied to remove the handler from a specified priority.
128
+ #
129
+ # @param handler [RemotelyExceptional::Handler] The handler that should be
130
+ # removed.
131
+ # @param options [Hash{Symbol=>Object}] A Hash of optional arguments.
132
+ # @option options [Integer] :priority ({::default_priority}) The priority that
133
+ # should be searched for the given handler.
134
+ # @return [Boolean] Returns true if the handler was successfully removed for
135
+ # the given priority. Returns false if the handler was not registered for
136
+ # the given priority.
137
+ def remove_handler(handler, options = {})
138
+ priority = options[:priority] || default_priority
139
+ registered_handlers.key?(priority) &&
140
+ !!registered_handlers[priority].delete(handler)
141
+ end
142
+
143
+ # Clears all {::block_handlers block handlers} and {::registered_handlers
144
+ # registered handlers}.
145
+ #
146
+ # @return [true]
147
+ def reset_handlers!
148
+ Thread.current["#{name}.registered_handlers"] = nil
149
+ Thread.current["#{name}.block_handlers"] = nil
150
+ true
151
+ end
152
+
153
+ # Registers a handler for the duration of the given block. By default
154
+ # registers the block at the {::default_priority default priority}, but a
155
+ # specific priority may be supplied as an option.
156
+ #
157
+ # @param handler [RemotelyExceptional::Handler] The handler that should be
158
+ # registered.
159
+ # @param options [Hash{Symbol=>Object}] A Hash of optional arguments.
160
+ # @option options [Integer] :priority ({::default_priority}) The priority that
161
+ # should be used to register the handler.
162
+ # @raise [ArgumentError] if a block is not provided.
163
+ # @return [Boolean] Returns true if the block handler was successfully
164
+ # registered for the given priority. Returns false if a matching block
165
+ # handler was already registered for the given priority.
166
+ def with_handler(handler, options = {})
167
+ raise ArgumentError, "Block required!" unless block_given?
168
+
169
+ priority = options[:priority] || default_priority
170
+ if block_handlers[priority].add?(handler)
171
+ added_handler = true
172
+ end
173
+ yield
174
+ ensure
175
+ block_handlers[priority].delete(handler) if added_handler
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,42 @@
1
+ module RemotelyExceptional::RemoteHandling
2
+ # Executes the given block of code in a context that allows for remote
3
+ # handling of exceptions using the specified handler class. Optionally,
4
+ # additional contextual information may be provided in case of an exception.
5
+ #
6
+ # @param handler [RemotelyExceptional::Handler] The handler that should be
7
+ # used to match and handle any exceptions that occur.
8
+ # @param context [Hash{Symbol=>Object}] Optional contextual information that
9
+ # will be made available to the handler if an exception occurs.
10
+ # @raise [ArgumentError] Raised if the provided handler is not a valid
11
+ # RemotelyExceptional::Handler.
12
+ # @raise [RemotelyExceptional::InvalidHandlerResponse] Raised if an exception
13
+ # is raised but the handler does not return a valid action symbol.
14
+ # @raise [Exception] Depending on the Handler used, could raise any error
15
+ # returned by the handler's handle method.
16
+ # @return [Object] Returns the result of the given block if no exception
17
+ # occurs.
18
+ # @return [Object, nil] If an exception occurs may return a result value
19
+ # provided by the exception handler's handle method. If the handler's handle
20
+ # method does not specify a result, nil will be returned instead.
21
+ def remotely_exceptional(handler, context = {})
22
+ raise ArgumentError, "Invalid Handler! Got #{handler.inspect}" unless handler &&
23
+ handler.respond_to?(:ancestors) &&
24
+ handler.ancestors.include?(RemotelyExceptional::Handler)
25
+
26
+ # Must explicitly use begin otherwise TypeError will occur if handler is not
27
+ # a Class or Module. We can raise a more specific error if begin is used.
28
+ begin
29
+ yield
30
+ rescue handler
31
+ response_code, result = handler.handle(context)
32
+ case response_code
33
+ when :raise then result ? raise(result) : raise
34
+ when :retry then retry
35
+ when :continue then result
36
+ else
37
+ msg = "Handler did not return an expected response code!"
38
+ raise RemotelyExceptional::InvalidHandlerResponse, msg
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,4 @@
1
+ module RemotelyExceptional
2
+ # The version of the RemotelyExceptional gem
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "remotely_exceptional/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "remotely_exceptional"
8
+ spec.version = RemotelyExceptional::VERSION
9
+ spec.authors = ["Danny Guinther"]
10
+ spec.email = ["dannyguinther@gmail.com"]
11
+ spec.summary = %q{Remote control of exceptions raised in distant contexts.}
12
+ spec.description = %q{Remote control of exceptions raised in distant contexts.}
13
+ spec.homepage = "https://github.com/tdg5/remotely_exceptional"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake", "~> 0"
23
+ end
@@ -0,0 +1,23 @@
1
+ if ENV["CI"]
2
+ require "simplecov"
3
+ require "coveralls"
4
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
5
+ SimpleCov.root(File.expand_path("../..", __FILE__))
6
+ SimpleCov.start do
7
+ add_filter "test"
8
+ end
9
+ end
10
+
11
+ require "minitest/autorun"
12
+ require "mocha/setup"
13
+ require "remotely_exceptional"
14
+
15
+ # Use alternate shoulda-style DSL for tests
16
+ class RemotelyExceptional::TestCase < Minitest::Spec
17
+ class << self
18
+ alias :setup :before
19
+ alias :teardown :after
20
+ alias :context :describe
21
+ alias :should :it
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ require "test_helper"
2
+
3
+ class RemotelyExceptional::ExceptionsTest < RemotelyExceptional::TestCase
4
+ exception = RemotelyExceptional::InvalidHandlerResponse
5
+ context exception.name do
6
+ subject { exception }
7
+ context "#original_exception" do
8
+ should "capture the original exception if one exists" do
9
+ assert_nil subject.new.original_exception
10
+ exception = ArgumentError
11
+ rescued = false
12
+ begin
13
+ raise exception
14
+ rescue exception
15
+ rescued = true
16
+ assert_kind_of exception, subject.new.original_exception
17
+ end
18
+ assert_equal true, rescued
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,135 @@
1
+ require "test_helper"
2
+
3
+ class RemotelyExceptional::HandlerTest < RemotelyExceptional::TestCase
4
+ Subject = RemotelyExceptional::Handler
5
+ class TestMixer
6
+ include Subject
7
+ end
8
+
9
+ context Subject.name do
10
+ subject { Subject }
11
+
12
+ context "::new" do
13
+ should "raise ArgumentError if no block was given" do
14
+ assert_raises(ArgumentError) do
15
+ subject.new
16
+ end
17
+ end
18
+
19
+ should "return a new class that includes #{Subject.name}" do
20
+ test_class = subject.new {|other| true }
21
+ assert_equal true, test_class.ancestors.include?(subject)
22
+ assert_kind_of subject, test_class.new
23
+ end
24
+
25
+ should "set the matcher to the given block" do
26
+ block = proc {|other| true }
27
+ test_class = subject.new(&block)
28
+ assert_equal block, test_class.send(:matcher)
29
+ end
30
+
31
+ should "take an optional super_class argument" do
32
+ test_super_class = Class.new
33
+ test_class = subject.new(test_super_class) {|other| true }
34
+ assert_kind_of test_super_class, test_class.new
35
+ end
36
+ end
37
+ end
38
+
39
+ context TestMixer.name do
40
+ subject { TestMixer }
41
+
42
+ context "included behaviors" do
43
+ context "::new" do
44
+ should "not be overriden by the module" do
45
+ assert_kind_of subject, subject.new
46
+ end
47
+ end
48
+
49
+ context "::===" do
50
+ should "call the matcher with the given argument" do
51
+ expected_argument = ArgumentError
52
+ subject.expects(:matcher).returns(lambda { |other| other == expected_argument })
53
+ assert_equal true, subject === expected_argument
54
+ end
55
+ end
56
+
57
+ context "::handle" do
58
+ setup do
59
+ @exception = ArgumentError.new
60
+ @context = { :context => true }
61
+ @instance = subject.new
62
+ subject.expects(:new).at_least(1).returns(@instance)
63
+ end
64
+
65
+ should "create a new instance and invoke #handle" do
66
+ @instance.expects(:handle)
67
+ subject.handle(@exception, @context)
68
+ end
69
+
70
+ should "set the exception and context of the instance correctly" do
71
+ @instance.stubs(:handle)
72
+ subject.handle(@exception, @context)
73
+ assert_equal @exception, @instance.exception
74
+ assert_equal @context, @instance.context
75
+ end
76
+
77
+ should "should automatically detect exception if not provided in an exception context" do
78
+ @instance.stubs(:handle)
79
+ begin
80
+ raise @exception
81
+ rescue
82
+ subject.handle(@context)
83
+ end
84
+ # Should be the exception in an exception context
85
+ assert_equal @context, @instance.context
86
+ assert_equal @exception, @instance.exception
87
+ end
88
+
89
+ should "have a nil exception when not provided an exception outside of an exception context" do
90
+ @instance.stubs(:handle)
91
+ subject.handle(@context)
92
+ assert_equal @context, @instance.context
93
+ # Should be nil outside of an exception context
94
+ assert_nil @instance.exception
95
+ end
96
+ end
97
+
98
+ context "rescue" do
99
+ should "call the matcher with an exception class when used to rescue" do
100
+ expected_argument = ArgumentError
101
+ subject.expects(:matcher).returns(lambda { |other| !other.is_a?(expected_argument) })
102
+
103
+ rescued = false
104
+ assert_raises(ArgumentError) do
105
+ begin
106
+ raise ArgumentError
107
+ rescue subject
108
+ rescued = true
109
+ end
110
+ end
111
+ assert_equal false, rescued
112
+ end
113
+
114
+ should "invoke rescue code when the matcher matches the exception" do
115
+ expected_argument = ArgumentError
116
+ subject.expects(:matcher).returns(lambda { |other| other.is_a?(expected_argument) })
117
+
118
+ rescued = false
119
+ begin
120
+ raise ArgumentError
121
+ rescue subject
122
+ rescued = true
123
+ end
124
+ assert_equal true, rescued
125
+ end
126
+ end
127
+
128
+ context "#handle" do
129
+ should "raise NotImplementedError if not overriden" do
130
+ assert_raises(NotImplementedError) { subject.new.handle }
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,297 @@
1
+ require "test_helper"
2
+ require "remotely_exceptional/handlers/prioritized_handler"
3
+
4
+ class RemotelyExceptional::Handlers::PrioritizedHandlerTest < RemotelyExceptional::TestCase
5
+ Subject = RemotelyExceptional::Handlers::PrioritizedHandler
6
+
7
+ AlphaHandler = RemotelyExceptional::Handler.new { |ex| ArgumentError === ex }
8
+ BetaHandler = RemotelyExceptional::Handler.new { |ex| RuntimeError === ex }
9
+ OmegaHandler = RemotelyExceptional::Handler.new { |ex| Exception === ex }
10
+
11
+ class TestSubject
12
+ include Subject
13
+ end
14
+
15
+ context "module that includes #{Subject.name}" do
16
+ subject { TestSubject }
17
+
18
+ setup { subject.reset_handlers! }
19
+
20
+ context "::===" do
21
+ should "return true if a handler is registered that matches the exception" do
22
+ subject.register_handler(AlphaHandler)
23
+ assert_equal true, subject === ArgumentError.new
24
+ end
25
+
26
+ should "return false if none of the handlers match the exception" do
27
+ subject.register_handler(AlphaHandler)
28
+ assert_equal false, subject === RuntimeError.new
29
+ end
30
+ end
31
+
32
+ context "::default_priority" do
33
+ should "return 1000" do
34
+ assert_equal 1000, subject.default_priority
35
+ end
36
+ end
37
+
38
+ context "::handle" do
39
+ should "delegate handling to the matching handler with the highest priority" do
40
+ subject.register_handler(AlphaHandler)
41
+ subject.register_handler(OmegaHandler, :priority => 10)
42
+ ex = ArgumentError.new
43
+ result = :continue
44
+ context = { :context => :foo }
45
+ OmegaHandler.expects(:handle).with(ex, context).returns(result)
46
+ assert_equal result, subject.handle(ex, context)
47
+ end
48
+
49
+ should "re-raise the exception if no matching handler is found" do
50
+ ex = ArgumentError.new
51
+ assert_raises(ex.class) { subject.handle(ex) }
52
+ end
53
+ end
54
+
55
+ context "::handler_for_exception" do
56
+ should "return the matching handler" do
57
+ subject.register_handler(AlphaHandler)
58
+ assert_equal AlphaHandler, subject.handler_for_exception(ArgumentError.new)
59
+ end
60
+
61
+ should "return the matching handler with the highest priority" do
62
+ subject.register_handler(AlphaHandler)
63
+ subject.register_handler(OmegaHandler, :priority => 10)
64
+ assert_equal OmegaHandler, subject.handler_for_exception(ArgumentError.new)
65
+ end
66
+
67
+ should "return nil if no matching handler is found" do
68
+ subject.register_handler(AlphaHandler)
69
+ subject.register_handler(BetaHandler)
70
+ assert_nil subject.handler_for_exception(SystemStackError.new)
71
+ end
72
+ end
73
+
74
+ context "::prioritized_handlers" do
75
+ should "yield handlers in priority ASC, name ASC order" do
76
+ setup_registered_handlers(subject)
77
+ found_handlers = nil
78
+ # Should be yielded first
79
+ subject.with_handler(BetaHandler, :priority => 5) do
80
+ # Should be yielded last
81
+ subject.with_handler(OmegaHandler, :priority => 2500) do
82
+ # Should only be yielded once for this priority
83
+ subject.with_handler(AlphaHandler) do
84
+ found_handlers = subject.prioritized_handlers.to_a
85
+ end
86
+ end
87
+ end
88
+ @expected_handlers.unshift(BetaHandler)
89
+ @expected_handlers.push(OmegaHandler)
90
+ assert_equal @expected_handlers, found_handlers
91
+ end
92
+
93
+ should "work when there are only registered handlers" do
94
+ setup_registered_handlers(subject)
95
+ subject.instance_variable_set(:@block_handlers, nil)
96
+ assert_equal @expected_handlers, subject.prioritized_handlers.to_a
97
+ end
98
+
99
+ should "work when there are only block handlers" do
100
+ found_handlers = nil
101
+ subject.with_handler(BetaHandler, :priority => 5) do
102
+ subject.with_handler(OmegaHandler, :priority => 2500) do
103
+ subject.with_handler(AlphaHandler) do
104
+ found_handlers = subject.prioritized_handlers.to_a
105
+ end
106
+ end
107
+ end
108
+ expected_handlers = [
109
+ BetaHandler,
110
+ AlphaHandler,
111
+ OmegaHandler,
112
+ ]
113
+ assert_equal expected_handlers, found_handlers
114
+ end
115
+
116
+ should "yield a handler only once per priority level" do
117
+ subject.register_handler(AlphaHandler)
118
+ found_handlers = nil
119
+ subject.with_handler(AlphaHandler) do
120
+ found_handlers = subject.prioritized_handlers.to_a
121
+ end
122
+ assert_equal 1, found_handlers.length
123
+ assert_equal AlphaHandler, found_handlers.first
124
+ end
125
+
126
+ context "set scan" do
127
+ should "not create empty sets when no keys exist" do
128
+ subject.prioritized_handlers.to_a
129
+ assert_empty subject.send(:registered_handlers)
130
+ assert_empty subject.send(:block_handlers)
131
+ end
132
+
133
+ should "not create empty sets when a key exists in one set" do
134
+ priority = 1000
135
+ subject.with_handler(AlphaHandler, :priority => priority) do
136
+ subject.prioritized_handlers.to_a
137
+ end
138
+ assert_equal false, subject.send(:registered_handlers).key?(priority)
139
+
140
+ priority = 10
141
+ subject.register_handler(AlphaHandler, :priority => priority)
142
+ assert_equal false, subject.send(:block_handlers).key?(priority)
143
+ end
144
+ end
145
+ end
146
+
147
+ context "::register_handler" do
148
+ should "return false if the handler was not registered" do
149
+ priority = 10
150
+ assert_equal true, subject.register_handler(AlphaHandler, :priority => priority)
151
+ assert_equal false, subject.register_handler(AlphaHandler, :priority => priority)
152
+ end
153
+
154
+ should "return true if the handler was registered" do
155
+ priority = 10
156
+ assert_equal true, subject.register_handler(AlphaHandler, :priority => priority)
157
+ end
158
+
159
+ should "register the handler with the provided priority" do
160
+ priority = 10
161
+ registered_handlers = subject.send(:registered_handlers)
162
+ registered_handlers.expects(:[]).with(priority).returns(Set.new)
163
+ assert_equal true, subject.register_handler(AlphaHandler, :priority => priority)
164
+ end
165
+
166
+ should "register the handler with the default priority if no priority given" do
167
+ priority = subject.default_priority
168
+ registered_handlers = subject.send(:registered_handlers)
169
+ registered_handlers.expects(:[]).with(priority).returns(Set.new)
170
+ assert_equal true, subject.register_handler(AlphaHandler)
171
+ end
172
+ end
173
+
174
+ context "::remove_handler" do
175
+ should "return false if the handler was not removed" do
176
+ assert_equal false, subject.remove_handler(AlphaHandler)
177
+ end
178
+
179
+ should "return true if the handler was removed" do
180
+ assert_equal true, subject.register_handler(AlphaHandler)
181
+ assert_equal true, subject.remove_handler(AlphaHandler)
182
+ end
183
+
184
+ should "remove a handler with a matching priority" do
185
+ priority = 10
186
+ assert_equal true, subject.register_handler(AlphaHandler, :priority => priority)
187
+ assert_equal true, subject.remove_handler(AlphaHandler, :priority => priority)
188
+ end
189
+
190
+ should "not remove a handler with a non-matching priority" do
191
+ priority = 10
192
+ assert_equal true, subject.register_handler(AlphaHandler, :priority => priority)
193
+ assert_equal false, subject.remove_handler(AlphaHandler)
194
+ end
195
+ end
196
+
197
+ context "::reset_handlers!" do
198
+ should "clear all handlers" do
199
+ subject.register_handler(AlphaHandler, :priority => 10)
200
+ initial_found_handlers = final_found_handlers = nil
201
+ subject.with_handler(AlphaHandler) do
202
+ initial_found_handlers = subject.prioritized_handlers.to_a
203
+ subject.reset_handlers!
204
+ final_found_handlers = subject.prioritized_handlers.to_a
205
+ end
206
+ assert_equal 2, initial_found_handlers.length
207
+ assert_equal 0, final_found_handlers.length
208
+ end
209
+ end
210
+
211
+ context "::with_handler" do
212
+ should "raise ArgumentError if no block is given" do
213
+ assert_raises(ArgumentError) do
214
+ subject.with_handler(AlphaHandler)
215
+ end
216
+ end
217
+
218
+ should "register the handler if it is not already registered" do
219
+ assert_equal true, subject.prioritized_handlers.none?
220
+ subject.with_handler(AlphaHandler) do
221
+ found_handlers = subject.prioritized_handlers.to_a
222
+ assert_equal 1, found_handlers.length
223
+ assert_equal AlphaHandler, subject.prioritized_handlers.first
224
+
225
+ # Should not add the handler again.
226
+ subject.with_handler(AlphaHandler) do
227
+ found_handlers = subject.prioritized_handlers.to_a
228
+ assert_equal 1, found_handlers.length
229
+ assert_equal AlphaHandler, subject.prioritized_handlers.first
230
+ end
231
+ end
232
+ assert_equal true, subject.prioritized_handlers.none?
233
+ end
234
+
235
+ should "register the handler with the given priority" do
236
+ priority = 10
237
+ assert_equal true, subject.prioritized_handlers.none?
238
+ subject.with_handler(AlphaHandler, :priority => priority) do
239
+ found_handler = subject.send(:block_handlers)[priority].first
240
+ assert_equal AlphaHandler, found_handler
241
+ end
242
+ assert_equal true, subject.prioritized_handlers.none?
243
+ end
244
+
245
+ should "register the handler with the default priority if no priority given" do
246
+ priority = subject.default_priority
247
+ assert_equal true, subject.prioritized_handlers.none?
248
+ subject.with_handler(AlphaHandler, :priority => priority) do
249
+ found_handler = subject.send(:block_handlers)[priority].first
250
+ assert_equal AlphaHandler, found_handler
251
+ end
252
+ assert_equal true, subject.prioritized_handlers.none?
253
+ end
254
+
255
+ should "remove the handler after yielding" do
256
+ assert_equal true, subject.prioritized_handlers.none?
257
+ subject.with_handler(AlphaHandler) do
258
+ assert_equal AlphaHandler, subject.prioritized_handlers.first
259
+ end
260
+ assert_equal true, subject.prioritized_handlers.none?
261
+ end
262
+
263
+ should "remove the handler even if an error occurs" do
264
+ assert_equal true, subject.prioritized_handlers.none?
265
+ assert_raises(Exception) do
266
+ subject.with_handler(AlphaHandler) do
267
+ assert_equal AlphaHandler, subject.prioritized_handlers.first
268
+ raise Exception
269
+ end
270
+ end
271
+ assert_equal true, subject.prioritized_handlers.none?
272
+ end
273
+ end
274
+
275
+ end
276
+
277
+ def setup_registered_handlers(handler)
278
+ # 5
279
+ assert_equal true, handler.register_handler(AlphaHandler)
280
+ # 2
281
+ assert_equal true, handler.register_handler(AlphaHandler, :priority => 500)
282
+ # 3
283
+ assert_equal true, handler.register_handler(BetaHandler, :priority => 500)
284
+ # 1
285
+ assert_equal true, handler.register_handler(OmegaHandler, :priority => 50)
286
+ # 4
287
+ assert_equal true, handler.register_handler(OmegaHandler, :priority => 500)
288
+
289
+ @expected_handlers = [
290
+ OmegaHandler,
291
+ AlphaHandler,
292
+ BetaHandler,
293
+ OmegaHandler,
294
+ AlphaHandler,
295
+ ]
296
+ end
297
+ end
@@ -0,0 +1,110 @@
1
+ require "test_helper"
2
+
3
+ class RemotelyExceptional::RemoteHandlingTest < RemotelyExceptional::TestCase
4
+ Subject = RemotelyExceptional::RemoteHandling
5
+
6
+ class TestMixer
7
+ include Subject
8
+ end
9
+
10
+ class TestHandler
11
+ include RemotelyExceptional::Handler
12
+ def self.matcher
13
+ lambda { |ex| ex.is_a?(exception_class) }
14
+ end
15
+
16
+ def self.exception_class
17
+ RuntimeError
18
+ end
19
+ end
20
+
21
+ context "class that includes #{Subject.name}" do
22
+
23
+ context "#remotely_exceptional" do
24
+ subject { TestMixer.new }
25
+
26
+ setup do
27
+ @handler = TestHandler
28
+ @instance = TestHandler.new
29
+ @handler.stubs(:new).returns(@instance)
30
+ end
31
+
32
+ should "raise ArgumentError unless a Handler is given" do
33
+ [nil, Class.new, Module.new, :not_a_handler].each do |handler|
34
+ assert_raises(ArgumentError) { subject.remotely_exceptional(handler) }
35
+ end
36
+ end
37
+
38
+ should "yield to the provided block" do
39
+ block_called = false
40
+ subject.remotely_exceptional(@handler) do
41
+ block_called = true
42
+ end
43
+ assert_equal true, block_called
44
+ end
45
+
46
+ context "response codes" do
47
+ should "raise InvalidHandlerResponse if unrecognized response code" do
48
+ @instance.expects(:handle).returns(:not_a_thing)
49
+ exception = assert_raises(RemotelyExceptional::InvalidHandlerResponse) do
50
+ subject.remotely_exceptional(@handler) do
51
+ raise @handler.exception_class
52
+ end
53
+ end
54
+ assert_kind_of @handler.exception_class, exception.original_exception
55
+ end
56
+
57
+ should "retry if retry code is given" do
58
+ @instance.expects(:handle).returns(:retry)
59
+ already_called = false
60
+ retried = false
61
+ subject.remotely_exceptional(@handler) do
62
+ if already_called
63
+ retried = true
64
+ else
65
+ already_called = true
66
+ raise @handler.exception_class
67
+ end
68
+ end
69
+ assert_equal true, retried
70
+ end
71
+
72
+ should "raise if raise code is given" do
73
+ @instance.expects(:handle).returns(:raise)
74
+ assert_raises(@handler.exception_class) do
75
+ subject.remotely_exceptional(@handler) do
76
+ raise @handler.exception_class
77
+ end
78
+ end
79
+ end
80
+
81
+ should "raise given exception if raise code is given with exception" do
82
+ exception_class = ArgumentError
83
+ @instance.expects(:handle).returns([:raise, exception_class])
84
+ assert_raises(exception_class) do
85
+ subject.remotely_exceptional(@handler) do
86
+ raise @handler.exception_class
87
+ end
88
+ end
89
+ end
90
+
91
+ should "continue if continue code is given" do
92
+ @instance.expects(:handle).returns(:continue)
93
+ result = subject.remotely_exceptional(@handler) do
94
+ raise @handler.exception_class
95
+ end
96
+ assert_nil result
97
+ end
98
+
99
+ should "continue and return given value if continue code is given with value" do
100
+ expected_result = 42
101
+ @instance.expects(:handle).returns([:continue, expected_result])
102
+ result = subject.remotely_exceptional(@handler) do
103
+ raise @handler.exception_class
104
+ end
105
+ assert_equal expected_result, result
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,13 @@
1
+ require "test_helper"
2
+
3
+ class RemotelyExceptionalTest < RemotelyExceptional::TestCase
4
+ Subject = RemotelyExceptional
5
+
6
+ subject { Subject }
7
+
8
+ context Subject.name do
9
+ should "be defined" do
10
+ assert defined?(subject), "Expected #{subject.name} to be defined!"
11
+ end
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remotely_exceptional
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Danny Guinther
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Remote control of exceptions raised in distant contexts.
42
+ email:
43
+ - dannyguinther@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".gitignore"
49
+ - ".travis.yml"
50
+ - Gemfile
51
+ - Guardfile
52
+ - LICENSE
53
+ - README.md
54
+ - Rakefile
55
+ - lib/remotely_exceptional.rb
56
+ - lib/remotely_exceptional/exceptions.rb
57
+ - lib/remotely_exceptional/handler.rb
58
+ - lib/remotely_exceptional/handlers/prioritized_handler.rb
59
+ - lib/remotely_exceptional/remote_handling.rb
60
+ - lib/remotely_exceptional/version.rb
61
+ - remotely_exceptional.gemspec
62
+ - test/test_helper.rb
63
+ - test/unit/exceptions_test.rb
64
+ - test/unit/handler_test.rb
65
+ - test/unit/handlers/prioritized_handler_test.rb
66
+ - test/unit/remote_handling_test.rb
67
+ - test/unit/remotely_exceptional_test.rb
68
+ homepage: https://github.com/tdg5/remotely_exceptional
69
+ licenses:
70
+ - MIT
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 2.2.2
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Remote control of exceptions raised in distant contexts.
92
+ test_files:
93
+ - test/test_helper.rb
94
+ - test/unit/exceptions_test.rb
95
+ - test/unit/handler_test.rb
96
+ - test/unit/handlers/prioritized_handler_test.rb
97
+ - test/unit/remote_handling_test.rb
98
+ - test/unit/remotely_exceptional_test.rb
99
+ has_rdoc: