spy_rb 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/spy/api.rb +30 -7
- data/lib/spy/blueprint.rb +22 -0
- data/lib/spy/core.rb +34 -16
- data/lib/spy/determine_visibility.rb +16 -0
- data/lib/spy/errors.rb +3 -3
- data/lib/spy/fake_method.rb +14 -0
- data/lib/spy/instance.rb +36 -26
- data/lib/spy/method_call.rb +4 -3
- data/lib/spy/multi.rb +21 -0
- data/lib/spy/registry.rb +20 -32
- data/lib/spy/strategy/base.rb +89 -0
- data/lib/spy/strategy/intercept.rb +44 -0
- data/lib/spy/strategy/wrap.rb +39 -0
- data/lib/spy/version.rb +1 -1
- metadata +15 -28
- data/lib/spy/instance/api/internal.rb +0 -104
- data/lib/spy/instance/strategy/intercept.rb +0 -23
- data/lib/spy/instance/strategy/wrap.rb +0 -24
- data/lib/spy/instance/strategy.rb +0 -33
- data/lib/spy/registry_entry.rb +0 -13
- data/lib/spy/registry_store.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 17d070307a491deecda090c82e836fdef23ee5cd
|
4
|
+
data.tar.gz: c50f4e881b3a1387ca83918a6f4213ccab5ca7bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28e4f0bc37039af35f5b6a0c75d14cc85148306bb34ddb4dd270c452cd7452864226601a70392620c300f2618ab7d5119520cc8aa0f1f79cbed99fe1aaa7ba11
|
7
|
+
data.tar.gz: 160210e00e62cc3effe46a3130d59bd746d0ca0d280ed9996cd858d2a39a4c9c4855921a15f91ae3e7f41afb28b0f2daf3e0163372f80a5aebed30c318a8fa53
|
data/lib/spy/api.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spy/core'
|
2
|
+
require 'spy/blueprint'
|
2
3
|
|
3
4
|
module Spy
|
4
5
|
# The core module that users will interface. `Spy::API` is implemented
|
@@ -20,7 +21,13 @@ module Spy
|
|
20
21
|
# @param [Symbol] msg - the name of the method to spy on
|
21
22
|
# @returns [Spy::Instance]
|
22
23
|
def on(target, msg)
|
23
|
-
|
24
|
+
if target.methods.include?(msg)
|
25
|
+
core.add_spy(Blueprint.new(target, msg, :method))
|
26
|
+
elsif target.respond_to?(msg)
|
27
|
+
core.add_spy(Blueprint.new(target, msg, :dynamic_delegation))
|
28
|
+
else
|
29
|
+
raise ArgumentError
|
30
|
+
end
|
24
31
|
end
|
25
32
|
|
26
33
|
# Spies on calls to a method made on any instance of some class or module
|
@@ -30,7 +37,23 @@ module Spy
|
|
30
37
|
# @returns [Spy::Instance]
|
31
38
|
def on_any_instance(target, msg)
|
32
39
|
raise ArgumentError unless target.respond_to?(:instance_method)
|
33
|
-
core.add_spy(target,
|
40
|
+
core.add_spy(Blueprint.new(target, msg, :instance_method))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Spies on all of the calls made to the given object
|
44
|
+
#
|
45
|
+
# @param object - the thing to spy on
|
46
|
+
# @returns [Spy::Multi]
|
47
|
+
def on_object(object)
|
48
|
+
core.add_multi_spy(Blueprint.new(object, :all, :methods))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Spies on all of the calls made to the given class or module
|
52
|
+
#
|
53
|
+
# @param klass - the thing to spy on
|
54
|
+
# @returns [Spy::Multi]
|
55
|
+
def on_class(klass)
|
56
|
+
core.add_multi_spy(Blueprint.new(klass, :all, :instance_methods))
|
34
57
|
end
|
35
58
|
|
36
59
|
# Stops spying on the method and restores its original functionality
|
@@ -43,9 +66,9 @@ module Spy
|
|
43
66
|
#
|
44
67
|
# Spy.restore(receiver, msg)
|
45
68
|
#
|
46
|
-
# @example stop spying on the given object, message, and
|
69
|
+
# @example stop spying on the given object, message, and type (e.g. :method, :instance_method, :dynamic_delegation)
|
47
70
|
#
|
48
|
-
# Spy.restore(object, msg,
|
71
|
+
# Spy.restore(object, msg, type)
|
49
72
|
#
|
50
73
|
# @param args - supports multiple signatures
|
51
74
|
def restore(*args)
|
@@ -54,10 +77,10 @@ module Spy
|
|
54
77
|
core.remove_all_spies if args.first == :all
|
55
78
|
when 2
|
56
79
|
target, msg = *args
|
57
|
-
core.remove_spy(target,
|
80
|
+
core.remove_spy(Blueprint.new(target, msg, :method))
|
58
81
|
when 3
|
59
|
-
target, msg,
|
60
|
-
core.remove_spy(target,
|
82
|
+
target, msg, type = *args
|
83
|
+
core.remove_spy(Blueprint.new(target, msg, type))
|
61
84
|
else
|
62
85
|
raise ArgumentError
|
63
86
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Spy
|
2
|
+
class Blueprint
|
3
|
+
attr_reader :target, :msg, :type
|
4
|
+
|
5
|
+
def initialize(target, msg, type)
|
6
|
+
@target = target
|
7
|
+
@msg = msg
|
8
|
+
@type = type
|
9
|
+
@caller = _caller
|
10
|
+
end
|
11
|
+
|
12
|
+
alias :_caller :caller
|
13
|
+
|
14
|
+
def caller
|
15
|
+
@caller
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
[@target.object_id, @msg, @type].join("|")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/spy/core.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'spy/instance'
|
2
2
|
require 'spy/registry'
|
3
|
+
require 'spy/multi'
|
3
4
|
require 'spy/errors'
|
4
5
|
|
5
6
|
module Spy
|
@@ -9,43 +10,60 @@ module Spy
|
|
9
10
|
# Syntactic sugar (like `Spy.restore(object, msg)` vs `Spy.restore(:all)`)
|
10
11
|
# should be handled in `Spy::API` and utilize `Spy::Core`
|
11
12
|
class Core
|
13
|
+
UNSAFE_METHODS = [:object_id, :__send__, :__id__, :method, :singleton_class]
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@registry = Registry.new
|
17
|
+
end
|
18
|
+
|
12
19
|
# Start spying on the given object and method
|
13
20
|
#
|
14
|
-
# @param [
|
15
|
-
# @param [Method, UnboundMethod] method - the method to spy on
|
21
|
+
# @param [Spy::Blueprint] blueprint - data for building the spy
|
16
22
|
# @returns [Spy::Instance]
|
17
23
|
# @raises [Spy::Errors::AlreadySpiedError] if the method is already
|
18
24
|
# being spied on
|
19
|
-
def add_spy(
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
def add_spy(blueprint)
|
26
|
+
if prev = @registry.get(blueprint)
|
27
|
+
raise Errors::AlreadySpiedError.new("Already spied on here:\n\t#{prev[0].caller.join("\n\t")}")
|
28
|
+
end
|
29
|
+
spy = Instance.new(blueprint)
|
30
|
+
@registry.insert(blueprint, spy)
|
23
31
|
spy.start
|
24
32
|
end
|
25
33
|
|
34
|
+
# Start spying on all of the given objects and methods
|
35
|
+
#
|
36
|
+
# @param [Spy::Blueprint] blueprint - data for building the spy
|
37
|
+
# @returns [Spy::Multi]
|
38
|
+
def add_multi_spy(multi_blueprint)
|
39
|
+
target = multi_blueprint.target
|
40
|
+
type = multi_blueprint.type
|
41
|
+
methods = target.public_send(type).reject(&method(:unsafe_method?))
|
42
|
+
spies = methods.map do |method_name|
|
43
|
+
singular_type = type.to_s.sub(/s$/, '').to_sym
|
44
|
+
add_spy(Blueprint.new(multi_blueprint.target, method_name, singular_type))
|
45
|
+
end
|
46
|
+
Multi.new(spies)
|
47
|
+
end
|
48
|
+
|
26
49
|
# Stop spying on the given object and method
|
27
50
|
#
|
28
|
-
# @param [Object] object - the object being spied on
|
29
|
-
# @param [Method, UnboundMethod] method - the method to stop spying on
|
30
51
|
# @raises [Spy::Errors::MethodNotSpiedError] if the method is not already
|
31
52
|
# being spied on
|
32
|
-
def remove_spy(
|
33
|
-
spy = registry.remove(
|
53
|
+
def remove_spy(blueprint)
|
54
|
+
spy = @registry.remove(blueprint)
|
34
55
|
spy.stop
|
35
56
|
end
|
36
57
|
|
37
58
|
# Stops spying on all objects and methods
|
38
|
-
#
|
39
|
-
# @raises [Spy::Errors::UnableToEmptySpyRegistryError] if for some reason
|
40
|
-
# a spy was not removed
|
41
59
|
def remove_all_spies
|
42
|
-
registry.remove_all(&:stop)
|
60
|
+
@registry.remove_all.each(&:stop)
|
43
61
|
end
|
44
62
|
|
45
63
|
private
|
46
64
|
|
47
|
-
def
|
48
|
-
|
65
|
+
def unsafe_method?(name)
|
66
|
+
UNSAFE_METHODS.include?(name)
|
49
67
|
end
|
50
68
|
end
|
51
69
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Spy
|
2
|
+
module DetermineVisibility
|
3
|
+
# @param [Method, UnboundMethod] method
|
4
|
+
# @returns [Symbol] whether the method is public, private, or protected
|
5
|
+
def self.call(method)
|
6
|
+
owner = method.owner
|
7
|
+
%w(public private protected).each do |vis|
|
8
|
+
query = "#{vis}_method_defined?"
|
9
|
+
if owner.respond_to?(query) && owner.send(query, method.name)
|
10
|
+
return vis
|
11
|
+
end
|
12
|
+
end
|
13
|
+
raise NoMethodError, "couldn't find method #{method.name} belonging to #{owner}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/spy/errors.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Spy
|
2
2
|
module Errors
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
Error = Class.new(StandardError)
|
4
|
+
MethodNotSpiedError = Class.new(Spy::Errors::Error)
|
5
|
+
AlreadySpiedError = Class.new(Spy::Errors::Error)
|
6
6
|
end
|
7
7
|
end
|
data/lib/spy/instance.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
require 'spy/
|
2
|
-
require 'spy/
|
1
|
+
require 'spy/fake_method'
|
2
|
+
require 'spy/strategy/wrap'
|
3
|
+
require 'spy/strategy/intercept'
|
3
4
|
|
4
5
|
# An instance of a spied method
|
5
6
|
# - Holds a reference to the original method
|
@@ -7,21 +8,30 @@ require 'spy/instance/api/internal'
|
|
7
8
|
# - Provides hooks for callbacks
|
8
9
|
module Spy
|
9
10
|
class Instance
|
10
|
-
|
11
|
+
attr_reader :original, :spied, :strategy, :call_history
|
11
12
|
|
12
|
-
|
13
|
+
def initialize(blueprint)
|
14
|
+
original =
|
15
|
+
case blueprint.type
|
16
|
+
when :dynamic_delegation
|
17
|
+
FakeMethod.new(blueprint.msg) { |*args, &block| blueprint.target.method_missing(blueprint.msg, *args, &block) }
|
18
|
+
when :instance_method
|
19
|
+
blueprint.target.instance_method(blueprint.msg)
|
20
|
+
else
|
21
|
+
blueprint.target.method(blueprint.msg)
|
22
|
+
end
|
13
23
|
|
14
|
-
def initialize(spied, original)
|
15
|
-
@spied = spied
|
16
24
|
@original = original
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@before_callbacks = []
|
20
|
-
@after_callbacks = []
|
21
|
-
@around_procs = []
|
25
|
+
@spied = blueprint.target
|
26
|
+
@strategy = choose_strategy(blueprint)
|
22
27
|
@call_history = []
|
23
|
-
|
24
|
-
@
|
28
|
+
|
29
|
+
@internal = {}
|
30
|
+
@internal[:conditional_filters] = []
|
31
|
+
@internal[:before_callbacks] = []
|
32
|
+
@internal[:after_callbacks]= []
|
33
|
+
@internal[:around_procs] = []
|
34
|
+
@internal[:instead]= nil
|
25
35
|
end
|
26
36
|
|
27
37
|
def name
|
@@ -47,42 +57,42 @@ module Spy
|
|
47
57
|
end
|
48
58
|
|
49
59
|
def when(&block)
|
50
|
-
@conditional_filters << block
|
60
|
+
@internal[:conditional_filters] << block
|
51
61
|
self
|
52
62
|
end
|
53
63
|
|
54
64
|
# Expect block to yield. Call the rest of the chain
|
55
65
|
# when it does
|
56
66
|
def wrap(&block)
|
57
|
-
@around_procs << block
|
67
|
+
@internal[:around_procs] << block
|
58
68
|
self
|
59
69
|
end
|
60
70
|
|
61
71
|
def before(&block)
|
62
|
-
@before_callbacks << block
|
72
|
+
@internal[:before_callbacks] << block
|
63
73
|
self
|
64
74
|
end
|
65
75
|
|
66
76
|
def after(&block)
|
67
|
-
@after_callbacks << block
|
77
|
+
@internal[:after_callbacks] << block
|
68
78
|
self
|
69
79
|
end
|
70
80
|
|
71
81
|
def instead(&block)
|
72
|
-
@instead = block
|
82
|
+
@internal[:instead] = block
|
83
|
+
self
|
73
84
|
end
|
74
85
|
|
75
86
|
private
|
76
87
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
88
|
+
def choose_strategy(blueprint)
|
89
|
+
if blueprint.type == :dynamic_delegation
|
90
|
+
Strategy::Intercept.new(self)
|
91
|
+
elsif @original.owner == @spied || @original.owner == @spied.singleton_class
|
92
|
+
Strategy::Wrap.new(self)
|
93
|
+
else
|
94
|
+
Strategy::Intercept.new(self)
|
84
95
|
end
|
85
|
-
raise NoMethodError, "couldn't find method #{@original.name} belonging to #{owner}"
|
86
96
|
end
|
87
97
|
end
|
88
98
|
end
|
data/lib/spy/method_call.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
module Spy
|
2
2
|
class MethodCall
|
3
|
-
attr_reader :name, :receiver, :args, :block
|
3
|
+
attr_reader :name, :receiver, :caller, :args, :block
|
4
4
|
attr_accessor :result
|
5
5
|
|
6
|
-
def initialize(replayer, name, receiver, *args)
|
6
|
+
def initialize(replayer, name, receiver, method_caller, *args)
|
7
7
|
@replayer = replayer
|
8
8
|
@name = name
|
9
9
|
@receiver = receiver
|
10
10
|
@args = args
|
11
|
-
@
|
11
|
+
@caller = method_caller
|
12
|
+
@block = proc { receiver.instance_eval(&Proc.new) } if block_given?
|
12
13
|
end
|
13
14
|
|
14
15
|
def replay
|
data/lib/spy/multi.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Spy
|
2
|
+
class Multi
|
3
|
+
attr_reader :spies
|
4
|
+
|
5
|
+
def initialize(spies)
|
6
|
+
@spies = spies
|
7
|
+
end
|
8
|
+
|
9
|
+
def call_count
|
10
|
+
@spies.map(&:call_count).reduce(&:+)
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](name)
|
14
|
+
@spies.find { |spy| spy.name == name }
|
15
|
+
end
|
16
|
+
|
17
|
+
def uncalled
|
18
|
+
@spies.select { |spy| spy.call_count == 0 }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/spy/registry.rb
CHANGED
@@ -1,56 +1,44 @@
|
|
1
1
|
require 'spy/errors'
|
2
|
-
require 'spy/registry_store'
|
3
|
-
require 'spy/registry_entry'
|
4
2
|
|
5
3
|
module Spy
|
6
4
|
# Responsible for managing the top-level state of which spies exist.
|
7
5
|
class Registry
|
6
|
+
def initialize
|
7
|
+
@store = {}
|
8
|
+
end
|
9
|
+
|
8
10
|
# Keeps track of the spy for later management. Ensures spy uniqueness
|
9
11
|
#
|
10
|
-
# @param [
|
11
|
-
# @param [Method, UnboundMethod] method - the method being spied on
|
12
|
+
# @param [Spy::Blueprint]
|
12
13
|
# @param [Spy::Instance] spy - the instantiated spy
|
13
14
|
# @raises [Spy::Errors::AlreadySpiedError] if the spy is already being
|
14
15
|
# tracked
|
15
|
-
def insert(
|
16
|
-
|
17
|
-
raise Errors::AlreadySpiedError if store
|
18
|
-
store
|
16
|
+
def insert(blueprint, spy)
|
17
|
+
key = blueprint.to_s
|
18
|
+
raise Errors::AlreadySpiedError if @store[key]
|
19
|
+
@store[key] = [blueprint, spy]
|
19
20
|
end
|
20
21
|
|
21
22
|
# Stops tracking the spy
|
22
23
|
#
|
23
|
-
# @param [
|
24
|
-
# @param [Method, UnboundMethod] method - the method being spied on
|
24
|
+
# @param [Spy::Blueprint]
|
25
25
|
# @raises [Spy::Errors::MethodNotSpiedError] if the spy isn't being tracked
|
26
|
-
def remove(
|
27
|
-
|
28
|
-
raise Errors::MethodNotSpiedError unless store
|
29
|
-
store.
|
26
|
+
def remove(blueprint)
|
27
|
+
key = blueprint.to_s
|
28
|
+
raise Errors::MethodNotSpiedError unless @store[key]
|
29
|
+
@store.delete(key)[1]
|
30
30
|
end
|
31
31
|
|
32
32
|
# Stops tracking all spies
|
33
|
-
#
|
34
|
-
# @raises [Spy::Errors::UnableToEmptySpyRegistryError] if any spies were
|
35
|
-
# still being tracked after removing all of the spies
|
36
33
|
def remove_all
|
37
|
-
store
|
38
|
-
|
34
|
+
store = @store
|
35
|
+
@store = {}
|
36
|
+
store.values.map(&:last)
|
39
37
|
end
|
40
38
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
# spied on
|
45
|
-
def include?(spied, method)
|
46
|
-
entry = RegistryEntry.new(spied, method)
|
47
|
-
store.include? entry
|
48
|
-
end
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def store
|
53
|
-
@store ||= RegistryStore.new
|
39
|
+
def get(blueprint)
|
40
|
+
key = blueprint.to_s
|
41
|
+
@store[key]
|
54
42
|
end
|
55
43
|
end
|
56
44
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spy/method_call'
|
2
|
+
|
3
|
+
module Spy
|
4
|
+
module Strategy
|
5
|
+
module Base
|
6
|
+
class << self
|
7
|
+
def call(spy, receiver, *args, &block)
|
8
|
+
spy.instance_eval do
|
9
|
+
# TODO - abstract the method call into an object and cache this in
|
10
|
+
# method using an instance variable instead of a local variable.
|
11
|
+
# This will let us be a bit more elegant about how we do before/after
|
12
|
+
# callbacks. We can also merge MethodCall with this responsibility so
|
13
|
+
# it isn't just a data struct
|
14
|
+
is_active = if @internal[:conditional_filters].any?
|
15
|
+
mc = Spy::Strategy::Base._build_method_call(spy, receiver, *args, &block)
|
16
|
+
@internal[:conditional_filters].all? { |f| f.call(mc) }
|
17
|
+
else
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
return Spy::Strategy::Base._call_original(spy, receiver, *args, &block) unless is_active
|
22
|
+
|
23
|
+
if @internal[:before_callbacks].any?
|
24
|
+
mc = Spy::Strategy::Base._build_method_call(spy, receiver, *args, &block)
|
25
|
+
@internal[:before_callbacks].each { |f| f.call(mc) }
|
26
|
+
end
|
27
|
+
|
28
|
+
if @internal[:around_procs].any?
|
29
|
+
mc = Spy::Strategy::Base._build_method_call(spy, receiver, *args, &block)
|
30
|
+
|
31
|
+
# Procify the original call
|
32
|
+
# Still return the result from it
|
33
|
+
result = nil
|
34
|
+
original_proc = proc do
|
35
|
+
result = Spy::Strategy::Base._call_and_record(spy, receiver, args, { :record => mc }, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Keep wrapping the original proc with each around_proc
|
39
|
+
@internal[:around_procs].reduce(original_proc) do |p, wrapper|
|
40
|
+
proc { wrapper.call(mc, &p) }
|
41
|
+
end.call
|
42
|
+
else
|
43
|
+
result = Spy::Strategy::Base._call_and_record(spy, receiver, args, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
if @internal[:after_callbacks].any?
|
47
|
+
mc = @call_history.last
|
48
|
+
@internal[:after_callbacks].each { |f| f.call(mc) }
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def _build_method_call(spy, receiver, *args, &block)
|
56
|
+
Spy::MethodCall.new(
|
57
|
+
proc { Spy::Strategy::Base._call_original(spy, receiver, *args, &block) },
|
58
|
+
spy.original.name,
|
59
|
+
receiver,
|
60
|
+
caller[7..-1],
|
61
|
+
*args,
|
62
|
+
&block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def _call_and_record(spy, receiver, args, opts = {}, &block)
|
66
|
+
spy.instance_eval do
|
67
|
+
if @internal[:instead]
|
68
|
+
@internal[:instead].call(Spy::Strategy::Base._build_method_call(spy, receiver, *args, &block))
|
69
|
+
else
|
70
|
+
record = opts[:record] || Spy::Strategy::Base._build_method_call(spy, receiver, *args, &block)
|
71
|
+
@call_history << record
|
72
|
+
|
73
|
+
result = Spy::Strategy::Base._call_original(spy, receiver, *args, &block)
|
74
|
+
record.result = result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def _call_original(spy, receiver, *args, &block)
|
80
|
+
if spy.original.is_a?(UnboundMethod)
|
81
|
+
spy.original.bind(receiver).call(*args, &block)
|
82
|
+
else
|
83
|
+
spy.original.call(*args, &block)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spy/determine_visibility'
|
2
|
+
require 'spy/strategy/base'
|
3
|
+
|
4
|
+
module Spy
|
5
|
+
module Strategy
|
6
|
+
class Intercept
|
7
|
+
def initialize(spy)
|
8
|
+
@spy = spy
|
9
|
+
@target =
|
10
|
+
case spy.original
|
11
|
+
when Method
|
12
|
+
spy.spied.singleton_class
|
13
|
+
when UnboundMethod
|
14
|
+
spy.spied
|
15
|
+
when FakeMethod
|
16
|
+
spy.spied.singleton_class
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def apply
|
21
|
+
spy = @spy
|
22
|
+
@target.class_eval do
|
23
|
+
# Add the spy to the intercept target
|
24
|
+
define_method spy.original.name do |*args, &block|
|
25
|
+
Spy::Strategy::Base.call(spy, self, *args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Make the visibility of the spy match the spied original
|
29
|
+
unless spy.original.is_a?(FakeMethod)
|
30
|
+
visibility = DetermineVisibility.call(spy.original)
|
31
|
+
send(visibility, spy.original.name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def undo
|
37
|
+
spy = @spy
|
38
|
+
@target.class_eval do
|
39
|
+
remove_method spy.original.name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spy/determine_visibility'
|
2
|
+
require 'spy/strategy/base'
|
3
|
+
|
4
|
+
module Spy
|
5
|
+
module Strategy
|
6
|
+
class Wrap
|
7
|
+
def initialize(spy)
|
8
|
+
@spy = spy
|
9
|
+
@visibility = DetermineVisibility.call(spy.original)
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply
|
13
|
+
spy = @spy
|
14
|
+
visibility = @visibility
|
15
|
+
@spy.original.owner.class_eval do
|
16
|
+
undef_method spy.original.name
|
17
|
+
|
18
|
+
# Replace the method with the spy
|
19
|
+
define_method spy.original.name do |*args, &block|
|
20
|
+
Spy::Strategy::Base.call(spy, self, *args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Make the visibility of the spy match the spied original
|
24
|
+
send(visibility, spy.original.name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def undo
|
29
|
+
spy = @spy
|
30
|
+
visibility = @visibility
|
31
|
+
spy.original.owner.class_eval do
|
32
|
+
remove_method spy.original.name
|
33
|
+
define_method spy.original.name, spy.original
|
34
|
+
send(visibility, spy.original.name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/spy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spy_rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Bodah
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -38,24 +38,10 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
description: Spy brings everything that's great about Sinon.JS to Ruby. Mocking frameworks
|
56
|
-
work by stubbing out functionality. Spy works by listening in on functionality and
|
57
|
-
allowing it to run in the background. Spy is designed to be lightweight and work
|
58
|
-
alongside Mocking frameworks instead of trying to replace them entirely.
|
41
|
+
description: Mocking frameworks work by stubbing out functionality. Spy works by listening
|
42
|
+
in on functionality and allowing it to run in the background. Spy is designed to
|
43
|
+
be lightweight and work alongside Mocking frameworks instead of trying to replace
|
44
|
+
them entirely.
|
59
45
|
email: jb3689@yahoo.com
|
60
46
|
executables: []
|
61
47
|
extensions: []
|
@@ -63,17 +49,18 @@ extra_rdoc_files: []
|
|
63
49
|
files:
|
64
50
|
- lib/spy.rb
|
65
51
|
- lib/spy/api.rb
|
52
|
+
- lib/spy/blueprint.rb
|
66
53
|
- lib/spy/core.rb
|
54
|
+
- lib/spy/determine_visibility.rb
|
67
55
|
- lib/spy/errors.rb
|
56
|
+
- lib/spy/fake_method.rb
|
68
57
|
- lib/spy/instance.rb
|
69
|
-
- lib/spy/instance/api/internal.rb
|
70
|
-
- lib/spy/instance/strategy.rb
|
71
|
-
- lib/spy/instance/strategy/intercept.rb
|
72
|
-
- lib/spy/instance/strategy/wrap.rb
|
73
58
|
- lib/spy/method_call.rb
|
59
|
+
- lib/spy/multi.rb
|
74
60
|
- lib/spy/registry.rb
|
75
|
-
- lib/spy/
|
76
|
-
- lib/spy/
|
61
|
+
- lib/spy/strategy/base.rb
|
62
|
+
- lib/spy/strategy/intercept.rb
|
63
|
+
- lib/spy/strategy/wrap.rb
|
77
64
|
- lib/spy/version.rb
|
78
65
|
homepage: https://github.com/jbodah/spy_rb
|
79
66
|
licenses:
|
@@ -95,8 +82,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
82
|
version: '0'
|
96
83
|
requirements: []
|
97
84
|
rubyforge_project:
|
98
|
-
rubygems_version: 2.
|
85
|
+
rubygems_version: 2.5.2
|
99
86
|
signing_key:
|
100
87
|
specification_version: 4
|
101
|
-
summary:
|
88
|
+
summary: Test Spies for Ruby
|
102
89
|
test_files: []
|
@@ -1,104 +0,0 @@
|
|
1
|
-
require 'spy/method_call'
|
2
|
-
|
3
|
-
module Spy
|
4
|
-
class Instance
|
5
|
-
module API
|
6
|
-
# The API we expose internally to our collaborators
|
7
|
-
module Internal
|
8
|
-
# TODO: Not sure if this is the best place for this
|
9
|
-
#
|
10
|
-
# Defines the spy on the target object
|
11
|
-
def attach_to(target)
|
12
|
-
spy = self
|
13
|
-
target.class_eval do
|
14
|
-
define_method spy.original.name do |*args, &block|
|
15
|
-
spy.call(self, *args, &block)
|
16
|
-
end
|
17
|
-
send(spy.visibility, spy.original.name)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# Call the spied method using the given receiver and arguments.
|
22
|
-
#
|
23
|
-
# receiver is required to allow calling of UnboundMethods such as
|
24
|
-
# instance methods defined on a Class
|
25
|
-
def call(receiver, *args, &block)
|
26
|
-
# TODO - abstract the method call into an object and cache this in
|
27
|
-
# method using an instance variable instead of a local variable.
|
28
|
-
# This will let us be a bit more elegant about how we do before/after
|
29
|
-
# callbacks. We can also merge MethodCall with this responsibility so
|
30
|
-
# it isn't just a data struct
|
31
|
-
is_active = if @conditional_filters.any?
|
32
|
-
mc = build_method_call(receiver, *args, &block)
|
33
|
-
@conditional_filters.all? { |f| f.call(mc) }
|
34
|
-
else
|
35
|
-
true
|
36
|
-
end
|
37
|
-
|
38
|
-
return call_original(receiver, *args, &block) unless is_active
|
39
|
-
|
40
|
-
if @before_callbacks.any?
|
41
|
-
mc = build_method_call(receiver, *args, &block)
|
42
|
-
@before_callbacks.each { |f| f.call(mc) }
|
43
|
-
end
|
44
|
-
|
45
|
-
if @around_procs.any?
|
46
|
-
mc = build_method_call(receiver, *args, &block)
|
47
|
-
|
48
|
-
# Procify the original call
|
49
|
-
# Still return the result from it
|
50
|
-
result = nil
|
51
|
-
original_proc = proc do
|
52
|
-
result = call_and_record(receiver, args, { :record => mc }, &block)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Keep wrapping the original proc with each around_proc
|
56
|
-
@around_procs.reduce(original_proc) do |p, wrapper|
|
57
|
-
proc { wrapper.call(mc, &p) }
|
58
|
-
end.call
|
59
|
-
else
|
60
|
-
result = call_and_record(receiver, args, &block)
|
61
|
-
end
|
62
|
-
|
63
|
-
if @after_callbacks.any?
|
64
|
-
mc = @call_history.last
|
65
|
-
@after_callbacks.each { |f| f.call(mc) }
|
66
|
-
end
|
67
|
-
|
68
|
-
result
|
69
|
-
end
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
def build_method_call(receiver, *args, &block)
|
74
|
-
Spy::MethodCall.new(
|
75
|
-
proc { call_original(receiver, *args, &block) },
|
76
|
-
original.name,
|
77
|
-
receiver,
|
78
|
-
*args,
|
79
|
-
&block)
|
80
|
-
end
|
81
|
-
|
82
|
-
def call_and_record(receiver, args, opts = {}, &block)
|
83
|
-
if @instead
|
84
|
-
@instead.call build_method_call(receiver, *args, &block)
|
85
|
-
else
|
86
|
-
record = opts[:record] || build_method_call(receiver, *args, &block)
|
87
|
-
@call_history << record
|
88
|
-
|
89
|
-
result = call_original(receiver, *args, &block)
|
90
|
-
record.result = result
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def call_original(receiver, *args, &block)
|
95
|
-
if original.is_a?(UnboundMethod)
|
96
|
-
original.bind(receiver).call(*args, &block)
|
97
|
-
else
|
98
|
-
original.call(*args, &block)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module Spy
|
2
|
-
class Instance
|
3
|
-
module Strategy
|
4
|
-
class Intercept
|
5
|
-
def initialize(spy, intercept_target)
|
6
|
-
@spy = spy
|
7
|
-
@intercept_target = intercept_target
|
8
|
-
end
|
9
|
-
|
10
|
-
def apply
|
11
|
-
@spy.attach_to(@intercept_target)
|
12
|
-
end
|
13
|
-
|
14
|
-
def undo
|
15
|
-
spy = @spy
|
16
|
-
@intercept_target.class_eval do
|
17
|
-
remove_method spy.original.name
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
module Spy
|
2
|
-
class Instance
|
3
|
-
module Strategy
|
4
|
-
class Wrap
|
5
|
-
def initialize(spy)
|
6
|
-
@spy = spy
|
7
|
-
end
|
8
|
-
|
9
|
-
def apply
|
10
|
-
@spy.attach_to(@spy.original.owner)
|
11
|
-
end
|
12
|
-
|
13
|
-
def undo
|
14
|
-
spy = @spy
|
15
|
-
spy.original.owner.class_eval do
|
16
|
-
remove_method spy.original.name
|
17
|
-
define_method spy.original.name, spy.original
|
18
|
-
send(spy.visibility, spy.original.name)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
require 'spy/instance/strategy/wrap'
|
2
|
-
require 'spy/instance/strategy/intercept'
|
3
|
-
|
4
|
-
module Spy
|
5
|
-
class Instance
|
6
|
-
module Strategy
|
7
|
-
class << self
|
8
|
-
def factory_build(spy)
|
9
|
-
if spy.original.is_a?(Method)
|
10
|
-
pick_strategy(spy, spy.spied.singleton_class)
|
11
|
-
else
|
12
|
-
pick_strategy(spy, spy.spied)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def pick_strategy(spy, spied_on)
|
19
|
-
if spy.original.owner == spied_on
|
20
|
-
# If the object we're spying on is the owner of
|
21
|
-
# the method under spy then we need to wrap that
|
22
|
-
# method
|
23
|
-
Strategy::Wrap.new(spy)
|
24
|
-
else
|
25
|
-
# Otherwise we can intercept it by abusing the
|
26
|
-
# inheritance hierarchy
|
27
|
-
Strategy::Intercept.new(spy, spied_on)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
data/lib/spy/registry_entry.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
module Spy
|
2
|
-
# Isolates the format we serialize spies in when we track them
|
3
|
-
class RegistryEntry < Struct.new(:spied, :method, :spy)
|
4
|
-
def key
|
5
|
-
receiver = method.is_a?(Method) ? method.receiver : nil
|
6
|
-
"#{receiver.object_id}|#{method.name}|#{method.class}"
|
7
|
-
end
|
8
|
-
|
9
|
-
def ==(other)
|
10
|
-
key == other.key
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
data/lib/spy/registry_store.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
module Spy
|
2
|
-
# Works with RegistryEntry abstractions to allow the
|
3
|
-
# store data structure to be easily swapped
|
4
|
-
class RegistryStore
|
5
|
-
include Enumerable
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@internal = {}
|
9
|
-
end
|
10
|
-
|
11
|
-
def insert(entry)
|
12
|
-
@internal[entry.key] = entry
|
13
|
-
end
|
14
|
-
|
15
|
-
def remove(entry)
|
16
|
-
@internal.delete(entry.key)
|
17
|
-
end
|
18
|
-
|
19
|
-
def each
|
20
|
-
return to_enum unless block_given?
|
21
|
-
@internal.values.each { |v| yield v }
|
22
|
-
end
|
23
|
-
|
24
|
-
def empty?
|
25
|
-
none?
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|