impersonator 0.1.2 → 0.1.3
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/Gemfile.lock +1 -1
- data/README.md +6 -5
- data/lib/impersonator/proxy.rb +1 -8
- data/lib/impersonator/record_mode.rb +56 -0
- data/lib/impersonator/recording.rb +44 -79
- data/lib/impersonator/replay_mode.rb +68 -0
- data/lib/impersonator/version.rb +1 -1
- 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: 88bdf9e7b7619dc442feaf3630312ef3ae0a30cedbd4c1593e4b896d93fac5a2
|
4
|
+
data.tar.gz: 073f80859044bd9852a62ab292cf029f52056bcf49446b03a94981cf8c9cde1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c832394967e28325e1d021957dd14721595b77439c809fdfd15d6491c1ad447486f061f573b7371d3db8bc9e7812b57780181f3f0812cfda8d8de9806a9cd7bc
|
7
|
+
data.tar.gz: e2e0408e9c6f7ce5e35d51b9ccc214af75dcfbd30cf0003f6c8ce17725367ed1cec00eaf852f0d0d4249e711ca16c88929dcd58b9b4864b5457feb9ed0d1b0e9
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -32,7 +32,7 @@ And then execute:
|
|
32
32
|
Use `Impersonator.impersonate` passing in a list of methods to impersonate and a block that will instantiate the object at record time:
|
33
33
|
|
34
34
|
```ruby
|
35
|
-
Impersonator.impersonate(:add, :divide) { Calculator.new }
|
35
|
+
calculator = Impersonator.impersonate(:add, :divide) { Calculator.new }
|
36
36
|
```
|
37
37
|
|
38
38
|
* At record time, `Calculator` will be instantiated and their methods normally invoked, recording the returned values (and yielded values if any).
|
@@ -47,15 +47,15 @@ end
|
|
47
47
|
|
48
48
|
# The first time it records...
|
49
49
|
Impersonator.recording('calculator add') do
|
50
|
-
|
51
|
-
puts
|
50
|
+
calculator = Impersonator.impersonate(:add) { Calculator.new }
|
51
|
+
puts calculator.add(2, 3) # 5
|
52
52
|
end
|
53
53
|
|
54
54
|
# The next time it replays
|
55
55
|
Object.send :remove_const, :Calculator # Calculator does not even have to exist now
|
56
56
|
Impersonator.recording('calculator add') do
|
57
|
-
|
58
|
-
puts
|
57
|
+
calculator = Impersonator.impersonate(:add) { Calculator.new }
|
58
|
+
puts calculator.add(2, 3) # 5
|
59
59
|
end
|
60
60
|
```
|
61
61
|
|
@@ -189,6 +189,7 @@ end
|
|
189
189
|
|
190
190
|
## Links
|
191
191
|
|
192
|
+
- [API documentation at rubydoc.info](https://www.rubydoc.info/github/jorgemanrubia/impersonator)
|
192
193
|
- [Blog post](https://www.jorgemanrubia.com/2019/06/16/impersonator-a-ruby-library-to-record-and-replay-object-interactions/)
|
193
194
|
|
194
195
|
## Contributing
|
data/lib/impersonator/proxy.rb
CHANGED
@@ -67,14 +67,7 @@ module Impersonator
|
|
67
67
|
matching_configuration = method_matching_configurations_by_method[method_name.to_sym]
|
68
68
|
method = Method.new(name: method_name, arguments: args, block: block,
|
69
69
|
matching_configuration: matching_configuration)
|
70
|
-
|
71
|
-
recording.replay(method)
|
72
|
-
else
|
73
|
-
spiable_block = method&.block_spy&.block
|
74
|
-
@impersonated_object.send(method_name, *args, &spiable_block).tap do |return_value|
|
75
|
-
recording.record(method, return_value)
|
76
|
-
end
|
77
|
-
end
|
70
|
+
recording.invoke(@impersonated_object, method, args)
|
78
71
|
end
|
79
72
|
end
|
80
73
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Impersonator
|
2
|
+
# The state of a {Recording recording} in record mode
|
3
|
+
class RecordMode
|
4
|
+
include HasLogger
|
5
|
+
|
6
|
+
# recording file path
|
7
|
+
attr_reader :recording_path
|
8
|
+
|
9
|
+
# @param [String] recording_path the file path to the recording file
|
10
|
+
def initialize(recording_path)
|
11
|
+
@recording_path = recording_path
|
12
|
+
end
|
13
|
+
|
14
|
+
# Start a recording session
|
15
|
+
def start
|
16
|
+
logger.debug 'Recording mode'
|
17
|
+
make_sure_recordings_dir_exists
|
18
|
+
@method_invocations = []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Records the method invocation
|
22
|
+
#
|
23
|
+
# @param [Object, Double] impersonated_object
|
24
|
+
# @param [MethodInvocation] method
|
25
|
+
# @param [Array<Object>] args
|
26
|
+
def invoke(impersonated_object, method, args)
|
27
|
+
spiable_block = method&.block_spy&.block
|
28
|
+
impersonated_object.send(method.name, *args, &spiable_block).tap do |return_value|
|
29
|
+
record(method, return_value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Finishes the record session
|
34
|
+
def finish
|
35
|
+
File.open(recording_path, 'w') do |file|
|
36
|
+
YAML.dump(@method_invocations, file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Record a {MethodInvocation method invocation} with a given return value
|
43
|
+
# @param [Method] method
|
44
|
+
# @param [Object] return_value
|
45
|
+
def record(method, return_value)
|
46
|
+
method_invocation = MethodInvocation.new(method_instance: method, return_value: return_value)
|
47
|
+
|
48
|
+
@method_invocations << method_invocation
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_sure_recordings_dir_exists
|
52
|
+
dirname = File.dirname(recording_path)
|
53
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -3,6 +3,21 @@
|
|
3
3
|
module Impersonator
|
4
4
|
# A recording is responsible for saving interactions at record time, and replaying them at
|
5
5
|
# replay time.
|
6
|
+
#
|
7
|
+
# A recording is always in one of two states.
|
8
|
+
#
|
9
|
+
# * {RecordMode Record mode}
|
10
|
+
# * {ReplayMode Replay mode}
|
11
|
+
#
|
12
|
+
# The state objects are responsible of dealing with the recording logic, which happens in 3
|
13
|
+
# moments:
|
14
|
+
#
|
15
|
+
# * {#start}
|
16
|
+
# * {#invoke}
|
17
|
+
# * {#finish}
|
18
|
+
#
|
19
|
+
# @see RecordMode
|
20
|
+
# @see ReplayMode
|
6
21
|
class Recording
|
7
22
|
include HasLogger
|
8
23
|
|
@@ -15,57 +30,39 @@ module Impersonator
|
|
15
30
|
@label = label
|
16
31
|
@recordings_path = recordings_path
|
17
32
|
@disabled = disabled
|
33
|
+
|
34
|
+
initialize_current_mode
|
18
35
|
end
|
19
36
|
|
20
37
|
# Start a recording/replay session
|
21
38
|
def start
|
22
39
|
logger.debug "Starting recording #{label}..."
|
23
|
-
|
24
|
-
start_in_replay_mode
|
25
|
-
else
|
26
|
-
start_in_record_mode
|
27
|
-
end
|
40
|
+
current_mode.start
|
28
41
|
end
|
29
42
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# @param [Method] method
|
41
|
-
def replay(method)
|
42
|
-
method_invocation = @method_invocations.shift
|
43
|
-
unless method_invocation
|
44
|
-
raise Impersonator::Errors::MethodInvocationError, 'Unexpected method invocation received:'\
|
45
|
-
"#{method}"
|
46
|
-
end
|
47
|
-
|
48
|
-
validate_method_signature!(method, method_invocation.method_instance)
|
49
|
-
replay_block(method_invocation, method)
|
50
|
-
|
51
|
-
method_invocation.return_value
|
43
|
+
# Handles the invocation of a given method on the impersonated object
|
44
|
+
#
|
45
|
+
# It will either record the interaction or replay it dependening on if there
|
46
|
+
# is a recording available or not
|
47
|
+
#
|
48
|
+
# @param [Object, Double] impersonated_object
|
49
|
+
# @param [MethodInvocation] method
|
50
|
+
# @param [Array<Object>] args
|
51
|
+
def invoke(impersonated_object, method, args)
|
52
|
+
current_mode.invoke(impersonated_object, method, args)
|
52
53
|
end
|
53
54
|
|
54
55
|
# Finish a record/replay session.
|
55
56
|
def finish
|
56
57
|
logger.debug "Recording #{label} finished"
|
57
|
-
|
58
|
-
finish_in_record_mode
|
59
|
-
else
|
60
|
-
finish_in_replay_mode
|
61
|
-
end
|
58
|
+
current_mode.finish
|
62
59
|
end
|
63
60
|
|
64
61
|
# Return whether it is currently at replay mode
|
65
62
|
#
|
66
63
|
# @return [Boolean]
|
67
64
|
def replay_mode?
|
68
|
-
@replay_mode
|
65
|
+
@current_mode == replay_mode
|
69
66
|
end
|
70
67
|
|
71
68
|
# Return whether it is currently at record mode
|
@@ -77,66 +74,34 @@ module Impersonator
|
|
77
74
|
|
78
75
|
private
|
79
76
|
|
80
|
-
|
81
|
-
!@disabled && File.exist?(file_path)
|
82
|
-
end
|
83
|
-
|
84
|
-
def replay_block(recorded_method_invocation, method_to_replay)
|
85
|
-
block_spy = recorded_method_invocation.method_instance.block_spy
|
86
|
-
block_spy&.block_invocations&.each do |block_invocation|
|
87
|
-
method_to_replay.block.call(*block_invocation.arguments)
|
88
|
-
end
|
89
|
-
end
|
77
|
+
attr_reader :current_mode
|
90
78
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
79
|
+
def initialize_current_mode
|
80
|
+
@current_mode = if can_replay?
|
81
|
+
replay_mode
|
82
|
+
else
|
83
|
+
record_mode
|
84
|
+
end
|
95
85
|
end
|
96
86
|
|
97
|
-
def
|
98
|
-
|
99
|
-
@replay_mode = false
|
100
|
-
make_sure_recordings_dir_exists
|
101
|
-
@method_invocations = []
|
87
|
+
def can_replay?
|
88
|
+
!@disabled && File.exist?(recording_path)
|
102
89
|
end
|
103
90
|
|
104
|
-
def
|
105
|
-
|
106
|
-
YAML.dump(@method_invocations, file)
|
107
|
-
end
|
91
|
+
def record_mode
|
92
|
+
@record_mode ||= RecordMode.new(recording_path)
|
108
93
|
end
|
109
94
|
|
110
|
-
def
|
111
|
-
|
112
|
-
raise Impersonator::Errors::MethodInvocationError,
|
113
|
-
"Expecting #{@method_invocations.length} method invocations"\
|
114
|
-
" that didn't happen: #{@method_invocations.inspect}"
|
115
|
-
end
|
95
|
+
def replay_mode
|
96
|
+
@replay_mode ||= ReplayMode.new(recording_path)
|
116
97
|
end
|
117
98
|
|
118
|
-
def
|
99
|
+
def recording_path
|
119
100
|
File.join(@recordings_path, "#{label_as_file_name}.yml")
|
120
101
|
end
|
121
102
|
|
122
103
|
def label_as_file_name
|
123
104
|
label.downcase.gsub(/[\(\)\s \#:]/, '-').gsub(/[\-]+/, '-').gsub(/(^-)|(-$)/, '')
|
124
105
|
end
|
125
|
-
|
126
|
-
def make_sure_recordings_dir_exists
|
127
|
-
dirname = File.dirname(file_path)
|
128
|
-
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
129
|
-
end
|
130
|
-
|
131
|
-
def validate_method_signature!(expected_method, actual_method)
|
132
|
-
unless actual_method == expected_method
|
133
|
-
raise Impersonator::Errors::MethodInvocationError, <<~ERROR
|
134
|
-
Expecting:
|
135
|
-
#{expected_method}
|
136
|
-
But received:
|
137
|
-
#{actual_method}
|
138
|
-
ERROR
|
139
|
-
end
|
140
|
-
end
|
141
106
|
end
|
142
107
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Impersonator
|
2
|
+
# The state of a {Recording recording} in replay mode
|
3
|
+
class ReplayMode
|
4
|
+
include HasLogger
|
5
|
+
|
6
|
+
# recording file path
|
7
|
+
attr_reader :recording_path
|
8
|
+
|
9
|
+
# @param [String] recording_path the file path to the recording file
|
10
|
+
def initialize(recording_path)
|
11
|
+
@recording_path = recording_path
|
12
|
+
end
|
13
|
+
|
14
|
+
# Start a replay session
|
15
|
+
def start
|
16
|
+
logger.debug 'Replay mode'
|
17
|
+
@replay_mode = true
|
18
|
+
@method_invocations = YAML.load_file(recording_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Replays the method invocation
|
22
|
+
#
|
23
|
+
# @param [Object, Double] impersonated_object not used in replay mode
|
24
|
+
# @param [MethodInvocation] method
|
25
|
+
# @param [Array<Object>] args not used in replay mode
|
26
|
+
def invoke(_impersonated_object, method, _args)
|
27
|
+
method_invocation = @method_invocations.shift
|
28
|
+
unless method_invocation
|
29
|
+
raise Impersonator::Errors::MethodInvocationError, 'Unexpected method invocation received:'\
|
30
|
+
"#{method}"
|
31
|
+
end
|
32
|
+
|
33
|
+
validate_method_signature!(method, method_invocation.method_instance)
|
34
|
+
replay_block(method_invocation, method)
|
35
|
+
|
36
|
+
method_invocation.return_value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Finishes the record session
|
40
|
+
def finish
|
41
|
+
unless @method_invocations.empty?
|
42
|
+
raise Impersonator::Errors::MethodInvocationError,
|
43
|
+
"Expecting #{@method_invocations.length} method invocations"\
|
44
|
+
" that didn't happen: #{@method_invocations.inspect}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def replay_block(recorded_method_invocation, method_to_replay)
|
51
|
+
block_spy = recorded_method_invocation.method_instance.block_spy
|
52
|
+
block_spy&.block_invocations&.each do |block_invocation|
|
53
|
+
method_to_replay.block.call(*block_invocation.arguments)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_method_signature!(expected_method, actual_method)
|
58
|
+
unless actual_method == expected_method
|
59
|
+
raise Impersonator::Errors::MethodInvocationError, <<~ERROR
|
60
|
+
Expecting:
|
61
|
+
#{expected_method}
|
62
|
+
But received:
|
63
|
+
#{actual_method}
|
64
|
+
ERROR
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/impersonator/version.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.3
|
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-
|
11
|
+
date: 2019-07-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zeitwerk
|
@@ -117,7 +117,9 @@ files:
|
|
117
117
|
- lib/impersonator/method_invocation.rb
|
118
118
|
- lib/impersonator/method_matching_configuration.rb
|
119
119
|
- lib/impersonator/proxy.rb
|
120
|
+
- lib/impersonator/record_mode.rb
|
120
121
|
- lib/impersonator/recording.rb
|
122
|
+
- lib/impersonator/replay_mode.rb
|
121
123
|
- lib/impersonator/version.rb
|
122
124
|
homepage: https://github.com/jorgemanrubia/impersonator
|
123
125
|
licenses:
|