right_develop 2.1.5 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/bin/right_develop +4 -0
- data/lib/right_develop.rb +1 -0
- data/lib/right_develop/commands.rb +2 -1
- data/lib/right_develop/commands/server.rb +194 -0
- data/lib/right_develop/testing.rb +31 -0
- data/lib/right_develop/testing/clients.rb +36 -0
- data/lib/right_develop/testing/clients/rest.rb +34 -0
- data/lib/right_develop/testing/clients/rest/requests.rb +38 -0
- data/lib/right_develop/testing/clients/rest/requests/base.rb +305 -0
- data/lib/right_develop/testing/clients/rest/requests/playback.rb +293 -0
- data/lib/right_develop/testing/clients/rest/requests/record.rb +175 -0
- data/lib/right_develop/testing/recording.rb +33 -0
- data/lib/right_develop/testing/recording/config.rb +571 -0
- data/lib/right_develop/testing/recording/metadata.rb +805 -0
- data/lib/right_develop/testing/servers/might_api/.gitignore +3 -0
- data/lib/right_develop/testing/servers/might_api/Gemfile +6 -0
- data/lib/right_develop/testing/servers/might_api/Gemfile.lock +18 -0
- data/lib/right_develop/testing/servers/might_api/app/base.rb +323 -0
- data/lib/right_develop/testing/servers/might_api/app/echo.rb +73 -0
- data/lib/right_develop/testing/servers/might_api/app/playback.rb +46 -0
- data/lib/right_develop/testing/servers/might_api/app/record.rb +45 -0
- data/lib/right_develop/testing/servers/might_api/config.ru +8 -0
- data/lib/right_develop/testing/servers/might_api/config/init.rb +47 -0
- data/lib/right_develop/testing/servers/might_api/lib/config.rb +204 -0
- data/lib/right_develop/testing/servers/might_api/lib/logger.rb +68 -0
- data/right_develop.gemspec +30 -2
- metadata +84 -34
@@ -0,0 +1,293 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_develop/testing/clients/rest'
|
25
|
+
|
26
|
+
require 'rest_client'
|
27
|
+
|
28
|
+
module RightDevelop::Testing::Client::Rest::Request
|
29
|
+
|
30
|
+
# Provides a middle-ware layer that intercepts transmition of the request and
|
31
|
+
# escapes out of the execute call with a stubbed response using throw/catch.
|
32
|
+
class Playback < ::RightDevelop::Testing::Client::Rest::Request::Base
|
33
|
+
|
34
|
+
HALT_TRANSMIT = :halt_transmit
|
35
|
+
|
36
|
+
# exceptions
|
37
|
+
class PlaybackError < StandardError; end
|
38
|
+
|
39
|
+
# fake Net::HTTPResponse
|
40
|
+
class FakeNetHttpResponse
|
41
|
+
attr_reader :code, :body, :elapsed_seconds, :call_count
|
42
|
+
|
43
|
+
def initialize(response_hash, response_metadata)
|
44
|
+
@elapsed_seconds = Integer(response_hash[:elapsed_seconds] || 0)
|
45
|
+
@code = response_metadata.http_status.to_s
|
46
|
+
@headers = response_metadata.headers.inject({}) do |h, (k, v)|
|
47
|
+
h[k] = Array(v) # expected to be an array
|
48
|
+
h
|
49
|
+
end
|
50
|
+
@body = response_metadata.body # optional
|
51
|
+
@call_count = Integer(response_hash[:call_count]) || 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def [](key)
|
55
|
+
if header = @headers[key.downcase]
|
56
|
+
header.join(', ')
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_hash; @headers; end
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :throttle
|
66
|
+
|
67
|
+
def initialize(args)
|
68
|
+
if args[:throttle]
|
69
|
+
args = args.dup
|
70
|
+
@throttle = Integer(args.delete(:throttle))
|
71
|
+
if @throttle < 0 || @throttle > 100
|
72
|
+
raise ::ArgumentError, 'throttle must be a percentage between 0 and 100'
|
73
|
+
end
|
74
|
+
else
|
75
|
+
@throttle = 0
|
76
|
+
end
|
77
|
+
super(args)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Overrides log_request to interrupt transmit before any connection is made.
|
81
|
+
#
|
82
|
+
# @raise [Symbol] always throws HALT_TRANSMIT
|
83
|
+
def log_request
|
84
|
+
super
|
85
|
+
throw(HALT_TRANSMIT, HALT_TRANSMIT)
|
86
|
+
end
|
87
|
+
|
88
|
+
RETRY_DELAY = 0.5
|
89
|
+
MAX_RETRIES = 100 # = 50 seconds; a socket usually times out in 60-120 seconds
|
90
|
+
|
91
|
+
# Overrides transmit to catch halt thrown by log_request.
|
92
|
+
#
|
93
|
+
# @param [URI[ uri of some kind
|
94
|
+
# @param [Net::HTTP] req of some kind
|
95
|
+
# @param [RestClient::Payload] of some kind
|
96
|
+
#
|
97
|
+
# @return
|
98
|
+
def transmit(uri, req, payload, &block)
|
99
|
+
caught = catch(HALT_TRANSMIT) { super }
|
100
|
+
if caught == HALT_TRANSMIT
|
101
|
+
response = nil
|
102
|
+
try_counter = 0
|
103
|
+
while response.nil?
|
104
|
+
with_state_lock do |state|
|
105
|
+
response = catch(METADATA_CLASS::HALT) do
|
106
|
+
fetch_response(state)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
case response
|
110
|
+
when METADATA_CLASS::RetryableFailure
|
111
|
+
try_counter += 1
|
112
|
+
if try_counter >= MAX_RETRIES
|
113
|
+
message =
|
114
|
+
"Released thread id=#{::Thread.current.object_id} after " <<
|
115
|
+
"#{try_counter} attempts to satisfy a retryable condition:\n" <<
|
116
|
+
response.message
|
117
|
+
raise PlaybackError, message
|
118
|
+
end
|
119
|
+
if 1 == try_counter
|
120
|
+
message = "Blocking thread id=#{::Thread.current.object_id} " <<
|
121
|
+
'until a retryable condition is satisfied...'
|
122
|
+
logger.debug(message)
|
123
|
+
end
|
124
|
+
response = nil
|
125
|
+
sleep RETRY_DELAY
|
126
|
+
else
|
127
|
+
if try_counter > 0
|
128
|
+
message = "Released thread id=#{::Thread.current.object_id} " <<
|
129
|
+
'after a retryable condition was satisfied.'
|
130
|
+
logger.debug(message)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# delay, if throttled, to simulate server response time.
|
136
|
+
if @throttle > 0 && response.elapsed_seconds > 0
|
137
|
+
delay = (Float(response.elapsed_seconds) * @throttle) / 100.0
|
138
|
+
logger.debug("throttle delay = #{delay}")
|
139
|
+
sleep delay
|
140
|
+
end
|
141
|
+
log_response(response)
|
142
|
+
process_result(response, &block)
|
143
|
+
else
|
144
|
+
raise PlaybackError,
|
145
|
+
'Unexpected RestClient::Request#transmit returned without calling RestClient::Request#log_request'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# @see RightDevelop::Testing::Client::Rest::Request::Base.handle_timeout
|
150
|
+
def handle_timeout
|
151
|
+
raise ::NotImplementedError, 'Timeout is unexpected for stubbed API call.'
|
152
|
+
end
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
# @see RightDevelop::Testing::Client::Rest::Request::Base#recording_mode
|
157
|
+
def recording_mode
|
158
|
+
:playback
|
159
|
+
end
|
160
|
+
|
161
|
+
def fetch_response(state)
|
162
|
+
# response must exist in the current epoch (i.e. can only enter next epoch
|
163
|
+
# after a valid response is found) or in a past epoch. the latter was
|
164
|
+
# allowed due to multithreaded requests causing the epoch to advance
|
165
|
+
# (in a non-throttled playback) before all requests for a past epoch have
|
166
|
+
# been made. the current epoch is always preferred over past.
|
167
|
+
logger.debug("BEGIN playback state = #{state.inspect}") if logger.debug?
|
168
|
+
file_path = nil
|
169
|
+
past_epochs = state[:past_epochs] ||= []
|
170
|
+
try_epochs = [state[:epoch]] + past_epochs
|
171
|
+
tried_paths = []
|
172
|
+
try_epochs.each do |epoch|
|
173
|
+
file_path = response_file_path(epoch)
|
174
|
+
break if ::File.file?(file_path)
|
175
|
+
tried_paths << file_path
|
176
|
+
file_path = nil
|
177
|
+
end
|
178
|
+
if file_path
|
179
|
+
response_hash = ::Mash.new(::YAML.load_file(file_path))
|
180
|
+
@response_metadata = create_response_metadata(
|
181
|
+
state,
|
182
|
+
response_hash[:http_status],
|
183
|
+
response_hash[:headers],
|
184
|
+
response_hash[:body])
|
185
|
+
result = FakeNetHttpResponse.new(response_hash, response_metadata)
|
186
|
+
else
|
187
|
+
raise PlaybackError,
|
188
|
+
"Unable to locate response file(s): \"#{tried_paths.join("\", \"")}\""
|
189
|
+
end
|
190
|
+
logger.debug("Played back response from #{file_path.inspect}.")
|
191
|
+
|
192
|
+
# determine if epoch is done, which it is if every known request has been
|
193
|
+
# responded to for the current epoch. there is a steady state at the end
|
194
|
+
# of time when all responses are given but there is no next epoch.
|
195
|
+
unless state[:end_of_time]
|
196
|
+
|
197
|
+
# list epochs once.
|
198
|
+
unless epochs = state[:epochs]
|
199
|
+
epochs = []
|
200
|
+
::Dir[::File.join(fixtures_dir, '*')].each do |path|
|
201
|
+
if ::File.directory?(path)
|
202
|
+
name = ::File.basename(path)
|
203
|
+
epochs << Integer(name) if name =~ /^\d+$/
|
204
|
+
end
|
205
|
+
end
|
206
|
+
state[:epochs] = epochs.sort!
|
207
|
+
end
|
208
|
+
|
209
|
+
# current epoch must be listed.
|
210
|
+
current_epoch = state[:epoch]
|
211
|
+
unless current_epoch == epochs.first
|
212
|
+
raise PlaybackError,
|
213
|
+
"Unable to locate current epoch directory: #{::File.join(fixtures_dir, current_epoch.to_s).inspect}"
|
214
|
+
end
|
215
|
+
|
216
|
+
# sorted epochs reveal the future.
|
217
|
+
if next_epoch = epochs[1]
|
218
|
+
# list all responses in current epoch once.
|
219
|
+
unless remaining = state[:remaining_responses]
|
220
|
+
# use all configured route subdirectories when building remaining
|
221
|
+
# responses hash.
|
222
|
+
#
|
223
|
+
# note that any unknown route fixtures would cause playback to spin
|
224
|
+
# on the same epoch forever. we could specifically select known
|
225
|
+
# route directories but it is just easier to find all here.
|
226
|
+
search_path = ::File.join(
|
227
|
+
@fixtures_dir,
|
228
|
+
current_epoch.to_s,
|
229
|
+
'*/response/**/*.yml')
|
230
|
+
remaining = state[:remaining_responses] = ::Dir[search_path].inject({}) do |h, path|
|
231
|
+
h[path] = { call_count: 0 }
|
232
|
+
h
|
233
|
+
end
|
234
|
+
if remaining.empty?
|
235
|
+
raise PlaybackError,
|
236
|
+
"Unable to determine remaining responses from #{search_path.inspect}"
|
237
|
+
end
|
238
|
+
logger.debug("Pending responses for epoch = #{current_epoch}: #{remaining.inspect}")
|
239
|
+
end
|
240
|
+
|
241
|
+
# may have been reponded before in same epoch; only care if this is
|
242
|
+
# the first time response was used unless playback is throttled.
|
243
|
+
#
|
244
|
+
# when playback is not throttled, there is no time delay (beyond the
|
245
|
+
# time needed to compute response) and the minimum number of calls per
|
246
|
+
# response is one.
|
247
|
+
#
|
248
|
+
# when playback is throttled (non-zero) we must satisfy the call count
|
249
|
+
# before advancing epoch. the point of this is to force the client to
|
250
|
+
# repeat the request the recorded number of times before the state
|
251
|
+
# appears to change.
|
252
|
+
#
|
253
|
+
# note that the user can achieve minimum delay while checking call
|
254
|
+
# count by setting @throttle = 1
|
255
|
+
if response_data = remaining[file_path]
|
256
|
+
response_data[:call_count] += 1
|
257
|
+
exhausted_response =
|
258
|
+
(0 == @throttle) ||
|
259
|
+
(response_data[:call_count] >= result.call_count)
|
260
|
+
if exhausted_response
|
261
|
+
remaining.delete(file_path)
|
262
|
+
if remaining.empty?
|
263
|
+
# time marches on.
|
264
|
+
past_epochs.unshift(epochs.shift)
|
265
|
+
state[:epoch] = next_epoch
|
266
|
+
state.delete(:remaining_responses) # reset responses for next epoch
|
267
|
+
if logger.debug?
|
268
|
+
message = <<EOF
|
269
|
+
|
270
|
+
A new epoch = #{state[:epoch]} begins due to
|
271
|
+
verb = #{request_metadata.verb}
|
272
|
+
uri = \"#{request_metadata.uri}\"
|
273
|
+
throttle = #{@throttle}
|
274
|
+
call_count = #{@throttle == 0 ? '<ignored>' : "#{response_data[:call_count]} >= #{result.call_count}"}
|
275
|
+
EOF
|
276
|
+
logger.debug(message)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
else
|
282
|
+
# the future is now; no need to add final epoch to past epochs.
|
283
|
+
state.delete(:remaining_responses)
|
284
|
+
state.delete(:epochs)
|
285
|
+
state[:end_of_time] = true
|
286
|
+
end
|
287
|
+
end
|
288
|
+
logger.debug("END playback state = #{state.inspect}") if logger.debug?
|
289
|
+
result
|
290
|
+
end
|
291
|
+
|
292
|
+
end # Base
|
293
|
+
end # RightDevelop::Testing::Client::Rest
|
@@ -0,0 +1,175 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2013 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
# ancestor
|
24
|
+
require 'right_develop/testing/clients/rest'
|
25
|
+
|
26
|
+
require 'fileutils'
|
27
|
+
require 'rest_client'
|
28
|
+
|
29
|
+
module RightDevelop::Testing::Client::Rest::Request
|
30
|
+
|
31
|
+
# Provides a middle-ware layer that intercepts response by overriding the
|
32
|
+
# logging mechanism built into rest-client Request. Request supports 'before'
|
33
|
+
# hooks (for request) but not 'after' hooks (for response) so logging is all
|
34
|
+
# we have.
|
35
|
+
class Record < ::RightDevelop::Testing::Client::Rest::Request::Base
|
36
|
+
|
37
|
+
# simulated 504 Net::HTTPResponse
|
38
|
+
class TimeoutNetHttpResponse
|
39
|
+
attr_reader :code, :body
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
message = 'Timeout'
|
43
|
+
@code = 504
|
44
|
+
@headers = {
|
45
|
+
'content-type' => 'text/plain',
|
46
|
+
'content-length' => ::Rack::Utils.bytesize(message).to_s,
|
47
|
+
'connection' => 'close',
|
48
|
+
}.inject({}) do |h, (k, v)|
|
49
|
+
h[k] = Array(v) # expected to be an array
|
50
|
+
h
|
51
|
+
end
|
52
|
+
@body = message
|
53
|
+
end
|
54
|
+
|
55
|
+
def [](key)
|
56
|
+
if header = @headers[key.downcase]
|
57
|
+
header.join(', ')
|
58
|
+
else
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_hash; @headers; end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Overrides log_request for basic logging.
|
67
|
+
#
|
68
|
+
# @param [RestClient::Response] to capture
|
69
|
+
#
|
70
|
+
# @return [Object] undefined
|
71
|
+
def log_request
|
72
|
+
logger.debug("proxied_url = #{@url.inspect}")
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
# Overrides log_response to capture both request and response.
|
77
|
+
#
|
78
|
+
# @param [RestClient::Response] to capture
|
79
|
+
#
|
80
|
+
# @return [Object] undefined
|
81
|
+
def log_response(response)
|
82
|
+
result = super
|
83
|
+
with_state_lock { |state| record_response(state, response) }
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
# @see RightDevelop::Testing::Client::Rest::Request::Base.handle_timeout
|
88
|
+
def handle_timeout
|
89
|
+
super
|
90
|
+
response = TimeoutNetHttpResponse.new
|
91
|
+
with_state_lock { |state| record_response(state, response) }
|
92
|
+
response
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
|
97
|
+
# @see RightDevelop::Testing::Client::Rest::Request::Base#recording_mode
|
98
|
+
def recording_mode
|
99
|
+
:record
|
100
|
+
end
|
101
|
+
|
102
|
+
def record_response(state, response)
|
103
|
+
# never record redirects because a redirect cannot be proxied back to the
|
104
|
+
# client (i.e. the client cannot update it's request url when proxied).
|
105
|
+
code = response.code
|
106
|
+
http_status = Integer(code)
|
107
|
+
if http_status >= 300 && http_status < 400
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
|
111
|
+
# use raw headers for response instead of the usual RestClient behavior of
|
112
|
+
# converting arrays to comma-delimited strings.
|
113
|
+
@response_metadata = create_response_metadata(
|
114
|
+
state, http_status, response.to_hash, response.body)
|
115
|
+
|
116
|
+
# record elapsed time in (integral) seconds. not intended to be a precise
|
117
|
+
# measure of time but rather used to throttle server if client is time-
|
118
|
+
# sensitive for some reason.
|
119
|
+
elapsed_seconds = @response_timestamp - @request_timestamp
|
120
|
+
response_hash = {
|
121
|
+
elapsed_seconds: elapsed_seconds,
|
122
|
+
http_status: response_metadata.http_status,
|
123
|
+
headers: response_metadata.headers.to_hash,
|
124
|
+
body: response_metadata.body,
|
125
|
+
}
|
126
|
+
|
127
|
+
# detect collision, if any, to determine if we have entered a new epoch.
|
128
|
+
ork = outstanding_request_key
|
129
|
+
call_count = 0
|
130
|
+
next_checksum = response_metadata.checksum
|
131
|
+
if response_data = (state[:response_data] ||= {})[ork]
|
132
|
+
last_checksum = response_data[:checksum]
|
133
|
+
if last_checksum != next_checksum
|
134
|
+
# note that variables never reset due to epoch change but they can be
|
135
|
+
# updated by a subsequent client request.
|
136
|
+
state[:epoch] += 100 # leave room to insert custom epochs
|
137
|
+
state[:response_data] = {} # reset checksums for next epoch
|
138
|
+
logger.debug("A new epoch = #{state[:epoch]} begins due to #{request_metadata.verb} \"#{request_metadata.uri}\"")
|
139
|
+
else
|
140
|
+
call_count = response_data[:call_count]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
call_count += 1
|
144
|
+
state[:response_data][ork] = {
|
145
|
+
checksum: next_checksum,
|
146
|
+
call_count: call_count,
|
147
|
+
}
|
148
|
+
response_hash[:call_count] = call_count
|
149
|
+
|
150
|
+
# write request unless already written.
|
151
|
+
file_path = request_file_path(state[:epoch])
|
152
|
+
unless ::File.file?(file_path)
|
153
|
+
# note that variables are not recorded because they must always be
|
154
|
+
# supplied by the client's request.
|
155
|
+
request_hash = {
|
156
|
+
verb: request_metadata.verb,
|
157
|
+
query: request_metadata.query,
|
158
|
+
headers: request_metadata.headers.to_hash,
|
159
|
+
body: request_metadata.body
|
160
|
+
}
|
161
|
+
::FileUtils.mkdir_p(::File.dirname(file_path))
|
162
|
+
::File.open(file_path, 'w') { |f| f.puts(::YAML.dump(request_hash)) }
|
163
|
+
end
|
164
|
+
logger.debug("Recorded request at #{file_path.inspect}.")
|
165
|
+
|
166
|
+
# response always written for incremented call count.
|
167
|
+
file_path = response_file_path(state[:epoch])
|
168
|
+
::FileUtils.mkdir_p(::File.dirname(file_path))
|
169
|
+
::File.open(file_path, 'w') { |f| f.puts(::YAML.dump(response_hash)) }
|
170
|
+
logger.debug("Recorded response at #{file_path.inspect}.")
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
end # Base
|
175
|
+
end # RightDevelop::Testing::Client::Rest
|