impersonator 0.1.1 → 0.1.2
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/.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
|