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.
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