spy_rb 2.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|