impersonator 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.yardopts +2 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +2 -2
- data/impersonator.gemspec +2 -1
- data/lib/impersonator/api.rb +42 -10
- data/lib/impersonator/block_invocation.rb +1 -0
- data/lib/impersonator/block_spy.rb +2 -0
- data/lib/impersonator/configuration.rb +3 -0
- data/lib/impersonator/double.rb +2 -0
- data/lib/impersonator/errors/configuration_error.rb +1 -0
- data/lib/impersonator/errors/method_invocation_error.rb +1 -0
- data/lib/impersonator/has_logger.rb +3 -0
- data/lib/impersonator/method.rb +12 -1
- data/lib/impersonator/method_invocation.rb +1 -0
- data/lib/impersonator/method_matching_configuration.rb +4 -0
- data/lib/impersonator/proxy.rb +27 -3
- data/lib/impersonator/recording.rb +25 -3
- data/lib/impersonator/version.rb +1 -1
- data/lib/impersonator.rb +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 44894f5dac3e7080abf3ffebf05ea83a876394629f60f9e486319f8e4e5304e2
|
4
|
+
data.tar.gz: 994a96556cef09050e108cfd55539af2f61507afae6298a994da811f187076bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83d2e1d38817599af079e1160c519a9529a820485ee851e7d9934a897f3f81ba97f3dc6add6c4e3f59f73d88095442971f9f4231666337cc75d9dd1cc7468b5b
|
7
|
+
data.tar.gz: 7c8b47fb88e82aca2d80ffc27f09c1ae37842d0ca2a0356fdaed50bc9d5cb9bb659c6687759ea21c95949af215c16a8572b09dfc975e54c839461a17d71757a7
|
data/.rubocop.yml
CHANGED
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
impersonator (0.1.
|
4
|
+
impersonator (0.1.2)
|
5
5
|
zeitwerk (~> 2.1.6)
|
6
6
|
|
7
7
|
GEM
|
@@ -41,7 +41,7 @@ GEM
|
|
41
41
|
rubocop (>= 0.60.0)
|
42
42
|
ruby-progressbar (1.10.1)
|
43
43
|
unicode-display_width (1.6.0)
|
44
|
-
zeitwerk (2.1.
|
44
|
+
zeitwerk (2.1.8)
|
45
45
|
|
46
46
|
PLATFORMS
|
47
47
|
ruby
|
data/impersonator.gemspec
CHANGED
@@ -9,7 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.email = ['jorge.manrubia@gmail.com']
|
10
10
|
|
11
11
|
spec.summary = 'Generate test stubs that replay recorded interactions'
|
12
|
-
spec.description = 'Record and replay object interactions. Ideal for mocking not-http services
|
12
|
+
spec.description = 'Record and replay object interactions. Ideal for mocking not-http services'\
|
13
|
+
' when testing (just because, for http, VCR is probably what you want)'
|
13
14
|
spec.homepage = 'https://github.com/jorgemanrubia/impersonator'
|
14
15
|
spec.license = 'MIT'
|
15
16
|
|
data/lib/impersonator/api.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# Public API exposed by the global `Impersonator` module.
|
2
3
|
module Api
|
4
|
+
# Wraps the execution of the yielded code withing a new {Recording recording} titled with the
|
5
|
+
# passed label.
|
6
|
+
#
|
7
|
+
# @param [String] label The label for the recording
|
8
|
+
# @param [Boolean] disabled `true` will disable replay mode and always execute code in *record*
|
9
|
+
# mode. `false` by default
|
3
10
|
def recording(label, disabled: false)
|
4
|
-
@current_recording = ::Impersonator::Recording.new
|
11
|
+
@current_recording = ::Impersonator::Recording.new label,
|
12
|
+
disabled: disabled,
|
13
|
+
recordings_path: configuration.recordings_path
|
5
14
|
@current_recording.start
|
6
15
|
yield
|
7
16
|
@current_recording.finish
|
@@ -9,14 +18,28 @@ module Impersonator
|
|
9
18
|
@current_recording = nil
|
10
19
|
end
|
11
20
|
|
21
|
+
# The current recording, if any, or `nil` otherwise.
|
22
|
+
#
|
23
|
+
# @return [Recording, nil]
|
12
24
|
def current_recording
|
13
25
|
@current_recording
|
14
26
|
end
|
15
27
|
|
28
|
+
# Configures how Impersonator works by yielding a {Configuration configuration} object
|
29
|
+
# you can use to tweak settings.
|
30
|
+
#
|
31
|
+
# ```
|
32
|
+
# Impersonator.configure do |config|
|
33
|
+
# config.recordings_path = 'my/own/recording/path'
|
34
|
+
# end
|
35
|
+
# ```
|
36
|
+
#
|
37
|
+
# @yieldparam config [Configuration]
|
16
38
|
def configure
|
17
39
|
yield configuration
|
18
40
|
end
|
19
41
|
|
42
|
+
# @return [Configuration]
|
20
43
|
def configuration
|
21
44
|
@configuration ||= Configuration.new
|
22
45
|
end
|
@@ -36,14 +59,17 @@ module Impersonator
|
|
36
59
|
# impersonator = Impersonator.impersonate(:add, :subtract) { Calculator.new }
|
37
60
|
# impersonator.add(3, 4)
|
38
61
|
#
|
39
|
-
# Notice that the actual object won't be instantiated in record mode. For that reason, the
|
40
|
-
# object will only respond to the list of impersonated methods.
|
62
|
+
# Notice that the actual object won't be instantiated in record mode. For that reason, the
|
63
|
+
# impersonated object will only respond to the list of impersonated methods.
|
41
64
|
#
|
42
65
|
# If you need to invoke other (not impersonated) methods see #impersonate_method instead.
|
43
66
|
#
|
44
|
-
# @
|
67
|
+
# @param [Array<Symbols, Strings>] methods list of methods to impersonate
|
68
|
+
# @return [Proxy] the impersonated proxy object
|
45
69
|
def impersonate(*methods)
|
46
|
-
|
70
|
+
unless block_given?
|
71
|
+
raise ArgumentError, 'Provide a block to instantiate the object to impersonate in record mode'
|
72
|
+
end
|
47
73
|
|
48
74
|
object_to_impersonate = if current_recording&.record_mode?
|
49
75
|
yield
|
@@ -55,14 +81,20 @@ module Impersonator
|
|
55
81
|
|
56
82
|
# Impersonates a list of methods of a given object
|
57
83
|
#
|
58
|
-
# The returned object will impersonate the list of methods and will delegate the rest of method
|
59
|
-
# to the actual object.
|
84
|
+
# The returned object will impersonate the list of methods and will delegate the rest of method
|
85
|
+
# calls to the actual object.
|
60
86
|
#
|
61
|
-
# @
|
87
|
+
# @param [Object] actual_object The actual object to impersonate
|
88
|
+
# @param [Array<Symbols, Strings>] methods list of methods to impersonate
|
89
|
+
# @return [Proxy] the impersonated proxy object
|
62
90
|
def impersonate_methods(actual_object, *methods)
|
63
|
-
|
91
|
+
unless @current_recording
|
92
|
+
raise Impersonator::Errors::ConfigurationError, 'You must start a recording to impersonate'\
|
93
|
+
' objects. Use Impersonator.recording {}'
|
94
|
+
end
|
64
95
|
|
65
|
-
::Impersonator::Proxy.new(actual_object, recording: current_recording,
|
96
|
+
::Impersonator::Proxy.new(actual_object, recording: current_recording,
|
97
|
+
impersonated_methods: methods)
|
66
98
|
end
|
67
99
|
end
|
68
100
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# An spy object that can collect {BlockInvocation block invocations}
|
2
3
|
BlockSpy = Struct.new(:block_invocations, :actual_block, keyword_init: true) do
|
4
|
+
# @return [Proc] a proc that will collect {BlockInvocation block invocations}
|
3
5
|
def block
|
4
6
|
@block ||= proc do |*arguments|
|
5
7
|
self.block_invocations ||= []
|
@@ -1,5 +1,8 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# General configuration settings for Impersonator
|
2
3
|
Configuration = Struct.new(:recordings_path, keyword_init: true) do
|
4
|
+
# @!attribute recordings_path [String] The path where recordings are saved to
|
5
|
+
|
3
6
|
DEFAULT_RECORDINGS_FOLDER = 'recordings'.freeze
|
4
7
|
|
5
8
|
def initialize(*)
|
data/lib/impersonator/double.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# A simple double implementation. It will generate empty stubs for the passed list of methods
|
2
3
|
class Double
|
4
|
+
# @param [Array<String, Symbol>] methods The list of methods this double will respond to
|
3
5
|
def initialize(*methods)
|
4
6
|
define_methods(methods)
|
5
7
|
end
|
data/lib/impersonator/method.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# A method instance
|
2
3
|
Method = Struct.new(:name, :arguments, :block, :matching_configuration, keyword_init: true) do
|
4
|
+
# @!attribute name [String] Method name
|
5
|
+
# @!attribute arguments [Array<Object>] Arguments passed to the method invocation
|
6
|
+
# @!attribute arguments [#call] The block passed to the method
|
7
|
+
# @!attribute matching_configuration [MethodMatchingConfiguration] The configuration that will
|
8
|
+
# be used to match the method invocation at replay mode
|
9
|
+
|
3
10
|
def to_s
|
4
11
|
string = name.to_s
|
5
12
|
|
@@ -10,6 +17,9 @@ module Impersonator
|
|
10
17
|
string
|
11
18
|
end
|
12
19
|
|
20
|
+
# The spy used to spy the block yield invocations
|
21
|
+
#
|
22
|
+
# @return [BlockSpy]
|
13
23
|
def block_spy
|
14
24
|
return nil if !@block_spy && !block
|
15
25
|
|
@@ -38,7 +48,8 @@ module Impersonator
|
|
38
48
|
other_arguments.delete_at(ignored_position)
|
39
49
|
end
|
40
50
|
|
41
|
-
name == other_method.name && my_arguments == other_arguments &&
|
51
|
+
name == other_method.name && my_arguments == other_arguments &&
|
52
|
+
!block_spy == !other_method.block_spy
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# Configuration options for matching methods
|
2
3
|
class MethodMatchingConfiguration
|
3
4
|
attr_reader :ignored_positions
|
4
5
|
|
@@ -6,6 +7,9 @@ module Impersonator
|
|
6
7
|
@ignored_positions = []
|
7
8
|
end
|
8
9
|
|
10
|
+
# Configure positions to ignore
|
11
|
+
#
|
12
|
+
# @param [Array<Integer>] positions The positions of arguments to ignore (0 being the first one)
|
9
13
|
def ignore_arguments_at(*positions)
|
10
14
|
ignored_positions.push(*positions)
|
11
15
|
end
|
data/lib/impersonator/proxy.rb
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
module Impersonator
|
2
|
+
# A proxy represents the impersonated object at both record and replay times.
|
3
|
+
#
|
4
|
+
# For not impersonated methods, it will just delegate to the impersonate object. For impersonated
|
5
|
+
# methods, it will interact with the {Recording recording} for recording or replaying the object
|
6
|
+
# interactions.
|
2
7
|
class Proxy
|
3
8
|
include HasLogger
|
4
9
|
|
5
10
|
attr_reader :impersonated_object
|
6
11
|
|
12
|
+
# @param [Object] impersonated_object
|
13
|
+
# @param [Recording] recording
|
14
|
+
# @param [Array<Symbol, String>] impersonated_methods The methods to impersonate
|
7
15
|
def initialize(impersonated_object, recording:, impersonated_methods:)
|
8
16
|
validate_object_has_methods_to_impersonate!(impersonated_object, impersonated_methods)
|
9
17
|
|
@@ -25,6 +33,16 @@ module Impersonator
|
|
25
33
|
impersonated_object.respond_to_missing?(method_name, *args)
|
26
34
|
end
|
27
35
|
|
36
|
+
# Configure matching options for a given method
|
37
|
+
#
|
38
|
+
# ```ruby
|
39
|
+
# impersonator.configure_method_matching_for(:add) do |config|
|
40
|
+
# config.ignore_arguments_at 0
|
41
|
+
# end
|
42
|
+
# ```
|
43
|
+
#
|
44
|
+
# @param [String, Symbol] method The method to configure matching options for
|
45
|
+
# @yieldparam config [MethodMatchingConfiguration]
|
28
46
|
def configure_method_matching_for(method)
|
29
47
|
method_matching_configurations_by_method[method.to_sym] ||= MethodMatchingConfiguration.new
|
30
48
|
yield method_matching_configurations_by_method[method]
|
@@ -39,15 +57,21 @@ module Impersonator
|
|
39
57
|
!object.respond_to?(method.to_sym)
|
40
58
|
end
|
41
59
|
|
42
|
-
|
60
|
+
unless missing_methods.empty?
|
61
|
+
raise Impersonator::Errors::ConfigurationError, 'These methods to impersonate does not'\
|
62
|
+
"exist: #{missing_methods.inspect}"
|
63
|
+
end
|
43
64
|
end
|
44
65
|
|
45
66
|
def invoke_impersonated_method(method_name, *args, &block)
|
46
|
-
|
67
|
+
matching_configuration = method_matching_configurations_by_method[method_name.to_sym]
|
68
|
+
method = Method.new(name: method_name, arguments: args, block: block,
|
69
|
+
matching_configuration: matching_configuration)
|
47
70
|
if recording.replay_mode?
|
48
71
|
recording.replay(method)
|
49
72
|
else
|
50
|
-
|
73
|
+
spiable_block = method&.block_spy&.block
|
74
|
+
@impersonated_object.send(method_name, *args, &spiable_block).tap do |return_value|
|
51
75
|
recording.record(method, return_value)
|
52
76
|
end
|
53
77
|
end
|
@@ -1,17 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Impersonator
|
4
|
+
# A recording is responsible for saving interactions at record time, and replaying them at
|
5
|
+
# replay time.
|
4
6
|
class Recording
|
5
7
|
include HasLogger
|
6
8
|
|
7
9
|
attr_reader :label
|
8
10
|
|
11
|
+
# @param [String] label
|
12
|
+
# @param [Boolean] disabled `true` for always working in *record* mode. `false` by default
|
13
|
+
# @param [String] the path to save recordings to
|
9
14
|
def initialize(label, disabled: false, recordings_path:)
|
10
15
|
@label = label
|
11
16
|
@recordings_path = recordings_path
|
12
17
|
@disabled = disabled
|
13
18
|
end
|
14
19
|
|
20
|
+
# Start a recording/replay session
|
15
21
|
def start
|
16
22
|
logger.debug "Starting recording #{label}..."
|
17
23
|
if can_replay?
|
@@ -21,15 +27,23 @@ module Impersonator
|
|
21
27
|
end
|
22
28
|
end
|
23
29
|
|
30
|
+
# Record a {MethodInvocation method invocation} with a given return value
|
31
|
+
# @param [Method] method
|
32
|
+
# @param [Object] return_value
|
24
33
|
def record(method, return_value)
|
25
34
|
method_invocation = MethodInvocation.new(method_instance: method, return_value: return_value)
|
26
35
|
|
27
36
|
@method_invocations << method_invocation
|
28
37
|
end
|
29
38
|
|
39
|
+
# Replay a method invocation
|
40
|
+
# @param [Method] method
|
30
41
|
def replay(method)
|
31
42
|
method_invocation = @method_invocations.shift
|
32
|
-
|
43
|
+
unless method_invocation
|
44
|
+
raise Impersonator::Errors::MethodInvocationError, 'Unexpected method invocation received:'\
|
45
|
+
"#{method}"
|
46
|
+
end
|
33
47
|
|
34
48
|
validate_method_signature!(method, method_invocation.method_instance)
|
35
49
|
replay_block(method_invocation, method)
|
@@ -37,6 +51,7 @@ module Impersonator
|
|
37
51
|
method_invocation.return_value
|
38
52
|
end
|
39
53
|
|
54
|
+
# Finish a record/replay session.
|
40
55
|
def finish
|
41
56
|
logger.debug "Recording #{label} finished"
|
42
57
|
if record_mode?
|
@@ -46,10 +61,16 @@ module Impersonator
|
|
46
61
|
end
|
47
62
|
end
|
48
63
|
|
64
|
+
# Return whether it is currently at replay mode
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
49
67
|
def replay_mode?
|
50
68
|
@replay_mode
|
51
69
|
end
|
52
70
|
|
71
|
+
# Return whether it is currently at record mode
|
72
|
+
#
|
73
|
+
# @return [Boolean]
|
53
74
|
def record_mode?
|
54
75
|
!replay_mode?
|
55
76
|
end
|
@@ -88,8 +109,9 @@ module Impersonator
|
|
88
109
|
|
89
110
|
def finish_in_replay_mode
|
90
111
|
unless @method_invocations.empty?
|
91
|
-
raise Impersonator::Errors::MethodInvocationError,
|
92
|
-
|
112
|
+
raise Impersonator::Errors::MethodInvocationError,
|
113
|
+
"Expecting #{@method_invocations.length} method invocations"\
|
114
|
+
" that didn't happen: #{@method_invocations.inspect}"
|
93
115
|
end
|
94
116
|
end
|
95
117
|
|
data/lib/impersonator/version.rb
CHANGED
data/lib/impersonator.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: impersonator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jorge Manrubia
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-06-
|
11
|
+
date: 2019-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -94,6 +94,8 @@ files:
|
|
94
94
|
- ".rubocop.yml"
|
95
95
|
- ".ruby-version"
|
96
96
|
- ".travis.yml"
|
97
|
+
- ".yardopts"
|
98
|
+
- CHANGELOG.md
|
97
99
|
- Gemfile
|
98
100
|
- Gemfile.lock
|
99
101
|
- LICENSE.txt
|