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.
Files changed (28) hide show
  1. data/VERSION +1 -1
  2. data/bin/right_develop +4 -0
  3. data/lib/right_develop.rb +1 -0
  4. data/lib/right_develop/commands.rb +2 -1
  5. data/lib/right_develop/commands/server.rb +194 -0
  6. data/lib/right_develop/testing.rb +31 -0
  7. data/lib/right_develop/testing/clients.rb +36 -0
  8. data/lib/right_develop/testing/clients/rest.rb +34 -0
  9. data/lib/right_develop/testing/clients/rest/requests.rb +38 -0
  10. data/lib/right_develop/testing/clients/rest/requests/base.rb +305 -0
  11. data/lib/right_develop/testing/clients/rest/requests/playback.rb +293 -0
  12. data/lib/right_develop/testing/clients/rest/requests/record.rb +175 -0
  13. data/lib/right_develop/testing/recording.rb +33 -0
  14. data/lib/right_develop/testing/recording/config.rb +571 -0
  15. data/lib/right_develop/testing/recording/metadata.rb +805 -0
  16. data/lib/right_develop/testing/servers/might_api/.gitignore +3 -0
  17. data/lib/right_develop/testing/servers/might_api/Gemfile +6 -0
  18. data/lib/right_develop/testing/servers/might_api/Gemfile.lock +18 -0
  19. data/lib/right_develop/testing/servers/might_api/app/base.rb +323 -0
  20. data/lib/right_develop/testing/servers/might_api/app/echo.rb +73 -0
  21. data/lib/right_develop/testing/servers/might_api/app/playback.rb +46 -0
  22. data/lib/right_develop/testing/servers/might_api/app/record.rb +45 -0
  23. data/lib/right_develop/testing/servers/might_api/config.ru +8 -0
  24. data/lib/right_develop/testing/servers/might_api/config/init.rb +47 -0
  25. data/lib/right_develop/testing/servers/might_api/lib/config.rb +204 -0
  26. data/lib/right_develop/testing/servers/might_api/lib/logger.rb +68 -0
  27. data/right_develop.gemspec +30 -2
  28. metadata +84 -34
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.5
1
+ 2.2.0
data/bin/right_develop CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ unless Gem.respond_to?(:latest_spec_for)
4
+ fail 'right_develop command line tools require rubygems v1.8+'
5
+ end
6
+
3
7
  require "trollop"
4
8
 
5
9
  require "right_develop"
data/lib/right_develop.rb CHANGED
@@ -29,6 +29,7 @@ module RightDevelop
29
29
  autoload :Commands, 'right_develop/commands'
30
30
  autoload :Git, 'right_develop/git'
31
31
  autoload :Parsers, 'right_develop/parsers'
32
+ autoload :Testing, 'right_develop/testing'
32
33
  autoload :Utility, 'right_develop/utility'
33
34
  end
34
35
 
@@ -3,4 +3,5 @@ module RightDevelop
3
3
  end
4
4
  end
5
5
 
6
- require "right_develop/commands/git"
6
+ require 'right_develop/commands/git'
7
+ require 'right_develop/commands/server'
@@ -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