right_develop 2.1.5 → 2.2.0
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.
- 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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.2.0
|
data/bin/right_develop
CHANGED
data/lib/right_develop.rb
CHANGED
@@ -0,0 +1,194 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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
|
+
require 'right_develop'
|
24
|
+
require 'right_develop/testing/servers/might_api/lib/config'
|
25
|
+
require 'tmpdir'
|
26
|
+
|
27
|
+
module RightDevelop::Commands
|
28
|
+
class Server
|
29
|
+
include RightSupport::Log::Mixin
|
30
|
+
|
31
|
+
CONFIG_CLASS = ::RightDevelop::Testing::Server::MightApi::Config
|
32
|
+
|
33
|
+
TASKS = CONFIG_CLASS::VALID_MODES
|
34
|
+
|
35
|
+
class PlainFormatter < ::Logger::Formatter
|
36
|
+
def call(severity, time, progname, msg)
|
37
|
+
sprintf("%s\n", msg2str(msg))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parse command-line options and create a Command object
|
42
|
+
def self.create
|
43
|
+
task_list = TASKS.sort.inject([]) do |a, (k, v)|
|
44
|
+
a << ' * %s%s' % [k.to_s.ljust(10), v]
|
45
|
+
end.join("\n")
|
46
|
+
|
47
|
+
options = Trollop.options do
|
48
|
+
banner <<-EOS
|
49
|
+
The 'server' command starts a server in the foreground to assist in testing.
|
50
|
+
The behavior of the server depends on the type specified.
|
51
|
+
|
52
|
+
Usage:
|
53
|
+
right_develop server <task> [options]
|
54
|
+
|
55
|
+
Where <task> is one of:
|
56
|
+
#{task_list}
|
57
|
+
|
58
|
+
And [options] are selected from:
|
59
|
+
EOS
|
60
|
+
opt :root_dir, 'Root directory for config and fixtures',
|
61
|
+
:default => ::Dir.pwd
|
62
|
+
opt :port, 'Port on which server will listen',
|
63
|
+
:default => 9292
|
64
|
+
opt :force, 'Force overwrite of any existing recording',
|
65
|
+
:default => false
|
66
|
+
opt :throttle, 'Playback delay as a percentage of recorded response time',
|
67
|
+
:default => 0
|
68
|
+
opt :debug, 'Enable verbose debug output',
|
69
|
+
:default => false
|
70
|
+
end
|
71
|
+
|
72
|
+
task = ARGV.shift.to_s
|
73
|
+
if TASKS.keys.include?(task)
|
74
|
+
self.new(task.to_sym, options)
|
75
|
+
else
|
76
|
+
::Trollop.die "unknown task #{task}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param [Symbol] task one of :prune or :tickets
|
81
|
+
# @option options [String] :root_dir for config and fixtures.
|
82
|
+
# @option options [String] :ruby_version to select with rbenv when running server
|
83
|
+
# @option options [String] :debug is true for debug-level logging
|
84
|
+
def initialize(task, options)
|
85
|
+
logger = ::Logger.new(STDOUT)
|
86
|
+
logger.level = options[:debug] ? ::Logger::DEBUG : ::Logger::WARN
|
87
|
+
logger.formatter = PlainFormatter.new
|
88
|
+
::RightSupport::Log::Mixin.default_logger = logger
|
89
|
+
|
90
|
+
@task = task
|
91
|
+
@options = options
|
92
|
+
end
|
93
|
+
|
94
|
+
# Run the task that was specified when this object was instantiated. This
|
95
|
+
# method does no work; it just delegates to a task method.
|
96
|
+
def run
|
97
|
+
run_might_api(@task, @options)
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def shell
|
103
|
+
::RightDevelop::Utility::Shell
|
104
|
+
end
|
105
|
+
|
106
|
+
def run_might_api(mode, options)
|
107
|
+
if shell.is_windows?
|
108
|
+
::Trollop.die 'Not supported under Windows'
|
109
|
+
end
|
110
|
+
if RUBY_VERSION < '1.9.3'
|
111
|
+
::Trollop.die 'Requires a minimum of ruby 1.9.3'
|
112
|
+
end
|
113
|
+
server_root_dir = ::File.expand_path('../../testing/servers/might_api', __FILE__)
|
114
|
+
root_dir = ::File.expand_path(options[:root_dir])
|
115
|
+
[server_root_dir, root_dir].each do |dir|
|
116
|
+
unless ::File.directory?(dir)
|
117
|
+
::Trollop.die "Missing expected directory: #{dir.inspect}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# sanity checks.
|
122
|
+
config = nil
|
123
|
+
::Dir.chdir(root_dir) do
|
124
|
+
config = CONFIG_CLASS.from_file(
|
125
|
+
CONFIG_CLASS::DEFAULT_CONFIG_PATH,
|
126
|
+
mode: mode,
|
127
|
+
log_level: options[:debug] ? :debug : :info,
|
128
|
+
throttle: options[:throttle])
|
129
|
+
end
|
130
|
+
fixtures_dir = config.fixtures_dir
|
131
|
+
tmp_root_dir = ::Dir.mktmpdir
|
132
|
+
tmp_config_path = ::File.join(tmp_root_dir, CONFIG_CLASS::DEFAULT_CONFIG_PATH)
|
133
|
+
case mode
|
134
|
+
when :record
|
135
|
+
if ::File.directory?(fixtures_dir)
|
136
|
+
if options[:force]
|
137
|
+
logger.warn("Overwriting existing #{fixtures_dir.inspect} due to force=true")
|
138
|
+
::FileUtils.rm_rf(fixtures_dir)
|
139
|
+
else
|
140
|
+
::Trollop.die "Unable to record over existing directory: #{fixtures_dir.inspect}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
when :playback
|
144
|
+
unless ::File.directory?(fixtures_dir)
|
145
|
+
::Trollop.die "Missing expected directory: #{fixtures_dir.inspect}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# copy fixtures tmp location so that multiple server instances can
|
149
|
+
# playback the same fixture directory but keep their own state (even if
|
150
|
+
# original gets re-recorded, etc.).
|
151
|
+
tmp_fixtures_dir = ::File.expand_path(CONFIG_CLASS::FIXTURES_DIR_NAME, tmp_root_dir)
|
152
|
+
::FileUtils.mkdir_p(tmp_fixtures_dir)
|
153
|
+
::FileUtils.cp_r(::File.join(fixtures_dir, '.'), tmp_fixtures_dir)
|
154
|
+
config.fixtures_dir(tmp_fixtures_dir)
|
155
|
+
config.normalize_fixtures_dir(logger)
|
156
|
+
end
|
157
|
+
|
158
|
+
# write updated config to tmp location.
|
159
|
+
::FileUtils.mkdir_p(::File.dirname(tmp_config_path))
|
160
|
+
::File.open(tmp_config_path, 'w') { |f| f.puts ::YAML.dump(config.to_hash) }
|
161
|
+
|
162
|
+
::Dir.chdir(server_root_dir) do
|
163
|
+
logger.warn("in #{server_root_dir.inspect}")
|
164
|
+
logger.warn('Preparing to run server...')
|
165
|
+
shell.execute('bundle check || bundle install')
|
166
|
+
logger.level = options[:debug] ? ::Logger::DEBUG : ::Logger::INFO
|
167
|
+
begin
|
168
|
+
cmd = "cat #{tmp_config_path.inspect} | bundle exec rackup --port #{options[:port]}"
|
169
|
+
shell.execute(cmd)
|
170
|
+
rescue ::Interrupt
|
171
|
+
# server runs in foreground so interrupt is normal
|
172
|
+
ensure
|
173
|
+
case mode
|
174
|
+
when :record
|
175
|
+
# remove temporary record state file from root of fixtures directory.
|
176
|
+
::Dir[::File.join(config.fixtures_dir, '*.yml')].each do |path|
|
177
|
+
begin
|
178
|
+
::File.unlink(path)
|
179
|
+
rescue ::Exception => e
|
180
|
+
logger.error("Unable to remove #{path.inspect}:\n #{e.class}: #{e.message}")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
begin
|
185
|
+
::FileUtils.rm_rf(tmp_root_dir)
|
186
|
+
rescue ::Exception => e
|
187
|
+
logger.error("Unable to remove #{tmp_root_dir.inspect}:\n #{e.class}: #{e.message}")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end # Server
|
194
|
+
end # RightDevelop::Commands
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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'
|
25
|
+
|
26
|
+
module RightDevelop
|
27
|
+
module Testing
|
28
|
+
autoload :Client, 'right_develop/testing/clients'
|
29
|
+
autoload :Recording, 'right_develop/testing/recording'
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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'
|
25
|
+
|
26
|
+
module RightDevelop
|
27
|
+
module Testing
|
28
|
+
module Client
|
29
|
+
autoload :Formats, 'right_develop/testing/clients/formats'
|
30
|
+
|
31
|
+
autoload :RecordMetadata, 'right_develop/testing/clients/record_metadata'
|
32
|
+
|
33
|
+
autoload :Rest, 'right_develop/testing/clients/rest'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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'
|
25
|
+
|
26
|
+
module RightDevelop
|
27
|
+
module Testing
|
28
|
+
module Client
|
29
|
+
module Rest
|
30
|
+
autoload :Request, 'right_develop/testing/clients/rest/requests'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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
|
+
module RightDevelop
|
27
|
+
module Testing
|
28
|
+
module Client
|
29
|
+
module Rest
|
30
|
+
module Request
|
31
|
+
autoload :Base, 'right_develop/testing/clients/rest/requests/base'
|
32
|
+
autoload :Playback, 'right_develop/testing/clients/rest/requests/playback'
|
33
|
+
autoload :Record, 'right_develop/testing/clients/rest/requests/record'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 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
|
+
require 'right_develop/testing/recording/metadata'
|
26
|
+
|
27
|
+
require 'digest/md5'
|
28
|
+
require 'rack/utils'
|
29
|
+
require 'rest_client'
|
30
|
+
require 'right_support'
|
31
|
+
require 'thread'
|
32
|
+
require 'yaml'
|
33
|
+
|
34
|
+
module RightDevelop::Testing::Client::Rest::Request
|
35
|
+
|
36
|
+
# Base class for record/playback request implementations.
|
37
|
+
class Base < ::RestClient::Request
|
38
|
+
|
39
|
+
# metadata.
|
40
|
+
METADATA_CLASS = ::RightDevelop::Testing::Recording::Metadata
|
41
|
+
|
42
|
+
# semaphore
|
43
|
+
MUTEX = ::Mutex.new
|
44
|
+
|
45
|
+
attr_reader :fixtures_dir, :logger, :route_path, :route_data
|
46
|
+
attr_reader :state_file_path, :request_timestamp, :response_timestamp
|
47
|
+
attr_reader :request_metadata, :response_metadata
|
48
|
+
|
49
|
+
def initialize(args)
|
50
|
+
args = args.dup
|
51
|
+
unless @fixtures_dir = args.delete(:fixtures_dir)
|
52
|
+
raise ::ArgumentError, 'fixtures_dir is required'
|
53
|
+
end
|
54
|
+
unless @logger = args.delete(:logger)
|
55
|
+
raise ::ArgumentError, 'logger is required'
|
56
|
+
end
|
57
|
+
unless @route_path = args.delete(:route_path)
|
58
|
+
raise ::ArgumentError, 'route_path is required'
|
59
|
+
end
|
60
|
+
unless @route_data = args.delete(:route_data)
|
61
|
+
raise ::ArgumentError, 'route_data is required'
|
62
|
+
end
|
63
|
+
unless @route_data[:subdir]
|
64
|
+
raise ::ArgumentError, "route_data is invalid: #{route_data.inspect}"
|
65
|
+
end
|
66
|
+
unless @state_file_path = args.delete(:state_file_path)
|
67
|
+
raise ::ArgumentError, 'state_file_path is required'
|
68
|
+
end
|
69
|
+
|
70
|
+
# resolve request metadata before initializing base class in order to set
|
71
|
+
# any timeout values.
|
72
|
+
request_verb = args[:method] or raise ::ArgumentError, "must pass :method"
|
73
|
+
request_verb = request_verb.to_s.upcase
|
74
|
+
request_headers = (args[:headers] || {}).dup
|
75
|
+
request_url = args[:url] or raise ::ArgumentError, "must pass :url"
|
76
|
+
request_url = process_url_params(request_url, request_headers)
|
77
|
+
if request_body = args[:payload]
|
78
|
+
# currently only supporting string payload or nil.
|
79
|
+
unless request_body.kind_of?(::String)
|
80
|
+
raise ::ArgumentError, 'args[:payload] must be a string'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
rm = nil
|
85
|
+
with_state_lock do |state|
|
86
|
+
rm = METADATA_CLASS.new(
|
87
|
+
mode: recording_mode,
|
88
|
+
kind: :request,
|
89
|
+
logger: @logger,
|
90
|
+
route_data: @route_data,
|
91
|
+
uri: METADATA_CLASS.normalize_uri(request_url),
|
92
|
+
verb: request_verb,
|
93
|
+
headers: request_headers,
|
94
|
+
body: request_body,
|
95
|
+
variables: state[:variables])
|
96
|
+
end
|
97
|
+
@request_metadata = rm
|
98
|
+
unless rm.timeouts.empty?
|
99
|
+
args = args.dup
|
100
|
+
if rm.timeouts[:open_timeout]
|
101
|
+
args[:open_timeout] = Integer(rm.timeouts[:open_timeout])
|
102
|
+
end
|
103
|
+
if rm.timeouts[:read_timeout]
|
104
|
+
args[:timeout] = Integer(rm.timeouts[:read_timeout])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@response_metadata = nil
|
108
|
+
|
109
|
+
super(args)
|
110
|
+
|
111
|
+
if @block_response
|
112
|
+
raise ::NotImplementedError,
|
113
|
+
'block_response not supported for record/playback'
|
114
|
+
end
|
115
|
+
if @raw_response
|
116
|
+
raise ::ArgumentError, 'raw_response not supported for record/playback'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Overrides log_request to capture start-time for network request.
|
121
|
+
#
|
122
|
+
# @return [Object] undefined
|
123
|
+
def log_request
|
124
|
+
result = super
|
125
|
+
push_outstanding_request
|
126
|
+
result
|
127
|
+
end
|
128
|
+
|
129
|
+
# Overrides log_response to capture end-time for network request.
|
130
|
+
#
|
131
|
+
# @param [RestClient::Response] to capture
|
132
|
+
#
|
133
|
+
# @return [Object] undefined
|
134
|
+
def log_response(response)
|
135
|
+
pop_outstanding_request
|
136
|
+
super
|
137
|
+
end
|
138
|
+
|
139
|
+
# Handles a timeout raised by a Net::HTTP call.
|
140
|
+
#
|
141
|
+
# @return [Net::HTTPResponse] response or nil if subclass responsibility
|
142
|
+
def handle_timeout
|
143
|
+
pop_outstanding_request
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
|
147
|
+
# Removes the current request from the FIFO queue of outstanding requests in
|
148
|
+
# case of error, redirect, etc.
|
149
|
+
def forget_outstanding_request
|
150
|
+
ruid = request_uid
|
151
|
+
ork = outstanding_request_key
|
152
|
+
with_state_lock do |state|
|
153
|
+
outstanding = state[:outstanding]
|
154
|
+
if outstanding_requests = outstanding[ork]
|
155
|
+
if outstanding_requests.delete(ruid)
|
156
|
+
logger.debug("Forgot outstanding request uid=#{ruid.inspect} at #{ork.inspect}")
|
157
|
+
end
|
158
|
+
outstanding.delete(ork) if outstanding_requests.empty?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
protected
|
164
|
+
|
165
|
+
# @return [Symbol] recording mode as one of [:record, :playback]
|
166
|
+
def recording_mode
|
167
|
+
raise NotImplementedError, 'Must be overridden'
|
168
|
+
end
|
169
|
+
|
170
|
+
# @return [String] unique identifier for this request (for this process)
|
171
|
+
def request_uid
|
172
|
+
@request_uid ||= ::Digest::MD5.hexdigest("#{::Process.pid}, #{self.object_id}")
|
173
|
+
end
|
174
|
+
|
175
|
+
# @return [String] path to current outstanding request, if any
|
176
|
+
def outstanding_request_key
|
177
|
+
::File.join(request_metadata.uri.path, request_metadata.checksum)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Holds the state file lock for block.
|
181
|
+
#
|
182
|
+
# @yield [state] gives exclusive state access to block
|
183
|
+
# @yieldparam [Hash] state
|
184
|
+
# @yieldreturn [Object] anything
|
185
|
+
#
|
186
|
+
# @return [Object] block result
|
187
|
+
def with_state_lock
|
188
|
+
result = nil
|
189
|
+
MUTEX.synchronize do # mutex for thread sync
|
190
|
+
state_dir = ::File.dirname(state_file_path)
|
191
|
+
::FileUtils.mkdir_p(state_dir) unless ::File.directory?(state_dir)
|
192
|
+
::File.open(state_file_path, ::File::RDWR | File::CREAT, 0644) do |f|
|
193
|
+
f.flock(::File::LOCK_EX) # file lock for process sync
|
194
|
+
state_yaml = f.read
|
195
|
+
if state_yaml.empty?
|
196
|
+
state = { epoch: 0, variables: {}, outstanding: {} }
|
197
|
+
else
|
198
|
+
state = ::YAML.load(state_yaml)
|
199
|
+
end
|
200
|
+
result = yield(state)
|
201
|
+
f.seek(0)
|
202
|
+
f.truncate(0)
|
203
|
+
f.puts(::YAML.dump(state))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
result
|
207
|
+
end
|
208
|
+
|
209
|
+
# Keeps a FIFO queue of outstanding requests using request object id in
|
210
|
+
# state to ensure responses are synchronous. This is important for record/
|
211
|
+
# playback of long polling with many threads/processes doing the polling. We
|
212
|
+
# do not want a younger long polling request to steal responses from an
|
213
|
+
# older request because we cannot maintain asynchronous state for playback.
|
214
|
+
#
|
215
|
+
# To make this blocking behavior more reasonable for testing, configure
|
216
|
+
# shorter timeouts for API calls that you know are long polling; the default
|
217
|
+
# read timeout value is 60 seconds. The timeout only effects playback time
|
218
|
+
# if you use throttle > 0.
|
219
|
+
def push_outstanding_request
|
220
|
+
ruid = request_uid
|
221
|
+
ork = outstanding_request_key
|
222
|
+
with_state_lock do |state|
|
223
|
+
outstanding = state[:outstanding]
|
224
|
+
outstanding_requests = outstanding[ork] ||= []
|
225
|
+
outstanding_requests << ruid
|
226
|
+
logger.debug("Pushed outstanding request uid=#{ruid.inspect} at #{ork.inspect}.")
|
227
|
+
end
|
228
|
+
@request_timestamp = ::Time.now.to_i
|
229
|
+
true
|
230
|
+
end
|
231
|
+
|
232
|
+
# Blocks until all similar previous requests have been popped from the
|
233
|
+
# queue. This is simple if there is a single producer/consumer of
|
234
|
+
# request/response but more complex as threads are introduced.
|
235
|
+
def pop_outstanding_request
|
236
|
+
@response_timestamp = ::Time.now.to_i
|
237
|
+
ruid = request_uid
|
238
|
+
ork = outstanding_request_key
|
239
|
+
while ruid do
|
240
|
+
with_state_lock do |state|
|
241
|
+
outstanding = state[:outstanding]
|
242
|
+
outstanding_requests = outstanding[ork]
|
243
|
+
if outstanding_requests.first == ruid
|
244
|
+
outstanding_requests.shift
|
245
|
+
outstanding.delete(ork) if outstanding_requests.empty?
|
246
|
+
logger.debug("Popped outstanding request uid=#{ruid.inspect} at #{ork.inspect}.")
|
247
|
+
ruid = nil
|
248
|
+
end
|
249
|
+
end
|
250
|
+
sleep 0.1 if ruid
|
251
|
+
end
|
252
|
+
true
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return [RightDevelop::Testing::Client::RecordMetdata] metadata for response
|
256
|
+
def create_response_metadata(state, response_code, response_headers, response_body)
|
257
|
+
METADATA_CLASS.new(
|
258
|
+
mode: recording_mode,
|
259
|
+
kind: :response,
|
260
|
+
logger: logger,
|
261
|
+
route_data: @route_data,
|
262
|
+
effective_route_config: request_metadata.effective_route_config,
|
263
|
+
uri: request_metadata.uri,
|
264
|
+
verb: request_metadata.verb,
|
265
|
+
http_status: response_code,
|
266
|
+
headers: response_headers,
|
267
|
+
body: response_body,
|
268
|
+
variables: state[:variables])
|
269
|
+
end
|
270
|
+
|
271
|
+
# Directory common to all fixtures of the given kind.
|
272
|
+
def fixtures_route_dir(kind, epoch)
|
273
|
+
::File.join(
|
274
|
+
@fixtures_dir,
|
275
|
+
epoch.to_s,
|
276
|
+
@route_data[:subdir],
|
277
|
+
kind.to_s)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Expands path to fixture file given kind, state, etc.
|
281
|
+
def fixture_file_path(kind, epoch)
|
282
|
+
# remove API root from path because we are already under an API-specific
|
283
|
+
# subdirectory and the route base path may be redundant.
|
284
|
+
unless request_metadata.uri.path.start_with?(@route_path)
|
285
|
+
raise ::ArgumentError,
|
286
|
+
"Request URI = #{request_metadata.uri.path.inspect} did not start with #{@route_path.inspect}."
|
287
|
+
end
|
288
|
+
route_relative_path = request_metadata.uri.path[@route_path.length..-1]
|
289
|
+
::File.join(
|
290
|
+
fixtures_route_dir(kind, epoch),
|
291
|
+
route_relative_path,
|
292
|
+
request_metadata.checksum + '.yml')
|
293
|
+
end
|
294
|
+
|
295
|
+
def request_file_path(epoch)
|
296
|
+
fixture_file_path(:request, epoch)
|
297
|
+
end
|
298
|
+
|
299
|
+
def response_file_path(epoch)
|
300
|
+
fixture_file_path(:response, epoch)
|
301
|
+
end
|
302
|
+
|
303
|
+
end # Base
|
304
|
+
|
305
|
+
end # RightDevelop::Testing::Client::Rest
|