right_develop 3.1.8 → 3.1.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 04ae23ffcb2adcaf347b19ff9f25fc3d4b179623
4
- data.tar.gz: f272d7a94045c77748e6a4c5887082d56ab4c042
3
+ metadata.gz: f9bea4294ac48a9bb1c124143e96594b3993867c
4
+ data.tar.gz: 1817c459f9d80fea412f1e1e7a66c011c6629f0d
5
5
  SHA512:
6
- metadata.gz: a80de468e508630973d7d859561e5a08dfa6dc1ec35dd33e9558bc1f2a2323e069ce91b2bede4486efdb8e7abbabc2ca96f4224180895a78f953edb4eb14fb28
7
- data.tar.gz: 60e69d6668880d62ada2a1862319f109f8200dfa12969377c8bbc50c0c99a17391e75d361b2d156ee5095a9d96d73f2134f74cca56690eb33683a9f548b885d9
6
+ metadata.gz: 814de826f973fd7678c8723ea686bebc140aa18e39497e7d77d683c208dc6dddaa8895ab0bf121400c0005862ba01b49806c2ee9462863d4be55d27b71d20b5c
7
+ data.tar.gz: 65416ad1cbe5429b1e74b842449b739992f9d1e2d32e53569c209932f6ab8204e43c1596e576ff290d7995d15d393e9bfc62d3057b50bb4d28d7691bb8443e7e
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.1.8
1
+ 3.1.9
@@ -40,9 +40,10 @@ module RightDevelop::Testing::Client::Rest::Request
40
40
 
41
41
  # fake Net::HTTPResponse
42
42
  class FakeNetHttpResponse
43
- attr_reader :code, :body, :elapsed_seconds, :call_count
43
+ attr_reader :code, :body, :delay_seconds, :elapsed_seconds, :call_count
44
44
 
45
45
  def initialize(response_hash, response_metadata)
46
+ @delay_seconds = response_metadata.delay_seconds
46
47
  @elapsed_seconds = Integer(response_hash[:elapsed_seconds] || 0)
47
48
  @code = response_metadata.http_status.to_s
48
49
  @headers = response_metadata.headers.inject({}) do |h, (k, v)|
@@ -140,6 +141,15 @@ module RightDevelop::Testing::Client::Rest::Request
140
141
  logger.debug("throttle delay = #{delay}")
141
142
  sleep delay
142
143
  end
144
+
145
+ # there may be a configured response delay (in addition to throttling)
146
+ # which allows for other responses to complete before the current
147
+ # response thread is unblocked. the response delay is absolute and not
148
+ # subject to the throttle factor.
149
+ if (delay = response.delay_seconds) > 0
150
+ logger.debug("configured response delay = #{delay}")
151
+ sleep delay
152
+ end
143
153
  log_response(response)
144
154
  process_result(response, &block)
145
155
  else
@@ -170,11 +180,15 @@ module RightDevelop::Testing::Client::Rest::Request
170
180
  file_path = nil
171
181
  past_epochs = state[:past_epochs] ||= []
172
182
  try_epochs = [state[:epoch]] + past_epochs
173
- tried_paths = []
183
+ first_tried_path = nil
184
+ first_tried_epoch = nil
185
+ last_tried_epoch = nil
174
186
  try_epochs.each do |epoch|
175
187
  file_path = response_file_path(epoch)
176
188
  break if ::File.file?(file_path)
177
- tried_paths << file_path
189
+ first_tried_path = file_path unless first_tried_path
190
+ first_tried_epoch = epoch unless first_tried_epoch
191
+ last_tried_epoch = epoch
178
192
  file_path = nil
179
193
  end
180
194
  if file_path
@@ -189,8 +203,10 @@ module RightDevelop::Testing::Client::Rest::Request
189
203
  response_hash[:body])
190
204
  result = FakeNetHttpResponse.new(response_hash, response_metadata)
191
205
  else
192
- raise PLAYBACK_ERROR,
193
- "Unable to locate response file(s): \"#{tried_paths.join("\", \"")}\""
206
+ msg = 'Unable to locate response file(s) in epoch range ' +
207
+ "[#{first_tried_epoch} - #{last_tried_epoch}]:\n " +
208
+ first_tried_path.inspect
209
+ raise PLAYBACK_ERROR, msg
194
210
  end
195
211
  logger.debug("Played back response from #{file_path.inspect}.")
196
212
 
@@ -52,7 +52,7 @@ module RightDevelop::Testing::Recording
52
52
 
53
53
  # keys allowed under the deep route configuration.
54
54
  ALLOWED_KINDS = %w(request response)
55
- ALLOWED_CONFIG_ACTIONS = %w(significant timeouts transform variables)
55
+ ALLOWED_CONFIG_ACTIONS = %w(delay_seconds significant timeouts transform variables)
56
56
  ALLOWED_TIMEOUTS = %w(open_timeout read_timeout)
57
57
  ALLOWED_VARIABLE_TYPES = %w(body header query)
58
58
 
@@ -178,11 +178,15 @@ module RightDevelop::Testing::Recording
178
178
  # normalize routes for efficient usage but keep them separate from
179
179
  # user's config so that .to_hash returns something understandable and
180
180
  # JSONizable/YAMLable.
181
- @normalized_routes = value.inject(RightSupport::Data::Mash.new) do |r, (k, v)|
181
+ mutable_routes = value.inject(RightSupport::Data::Mash.new) do |r, (k, v)|
182
182
  r[normalize_route_prefix(k)] = normalize_route_data(k, v)
183
183
  r
184
184
  end
185
- @config_hash['routes'] = ::RightSupport::Data::HashTools.deep_clone(value)
185
+
186
+ # deep freeze routes to detect any case where code is unintentionally
187
+ # modifying the route hash.
188
+ @normalized_routes = ::RightSupport::Data::HashTools.deep_freeze!(mutable_routes)
189
+ @config_hash['routes'] = ::RightSupport::Data::HashTools.deep_clone2(value)
186
190
  else
187
191
  raise ConfigError, 'routes must be a hash'
188
192
  end
@@ -554,6 +558,15 @@ module RightDevelop::Testing::Recording
554
558
 
555
559
  rst[rst_k] = rst_v.inject(RightSupport::Data::Mash.new) do |kc, (kc_k, kc_v)|
556
560
  case kc_k
561
+ when METADATA_CLASS::DELAY_SECONDS_KEY
562
+ begin
563
+ kc_v = Float(kc_v)
564
+ rescue ::ArgumentError
565
+ location = position_string(position, subpath + [rst_k, kc_k])
566
+ message = 'Invalid route configuration delay_seconds value at ' +
567
+ "#{location}: #{kc_v.inspect}"
568
+ raise ConfigError, message
569
+ end
557
570
  when METADATA_CLASS::TIMEOUTS_KEY
558
571
  # sanity check.
559
572
  kc_v = kc_v.inject(RightSupport::Data::Mash.new) do |h, (k, v)|
@@ -43,11 +43,12 @@ module RightDevelop::Testing::Recording
43
43
  KINDS = %w(request response)
44
44
 
45
45
  # route-relative config keys.
46
- MATCHERS_KEY = 'matchers'.freeze
47
- SIGNIFICANT_KEY = 'significant'.freeze
48
- TIMEOUTS_KEY = 'timeouts'.freeze
49
- TRANSFORM_KEY = 'transform'.freeze
50
- VARIABLES_KEY = 'variables'.freeze
46
+ DELAY_SECONDS_KEY = 'delay_seconds'.freeze
47
+ MATCHERS_KEY = 'matchers'.freeze
48
+ SIGNIFICANT_KEY = 'significant'.freeze
49
+ TIMEOUTS_KEY = 'timeouts'.freeze
50
+ TRANSFORM_KEY = 'transform'.freeze
51
+ VARIABLES_KEY = 'variables'.freeze
51
52
 
52
53
  # finds the value index for a recorded variable, if any.
53
54
  VARIABLE_INDEX_REGEX = /\[(\d+)\]$/
@@ -83,6 +84,7 @@ module RightDevelop::Testing::Recording
83
84
  end
84
85
 
85
86
  # exceptions.
87
+ class ConfigurationError < StandardError; end
86
88
  class RecordingError < StandardError; end
87
89
  class PlaybackError < StandardError; end
88
90
 
@@ -142,11 +144,12 @@ module RightDevelop::Testing::Recording
142
144
 
143
145
  # apply the configuration by substituting for variables in the request and
144
146
  # by obfuscating wherever a variable name is nil.
147
+ erck = @effective_route_config[@kind]
145
148
  case @mode
146
149
  when 'validate'
147
- # do nothing; used to validate the fixtures before playback, etc.
150
+ # used to validate the fixtures before playback; no variable
151
+ # substitution should be performed.
148
152
  else
149
- erck = @effective_route_config[@kind]
150
153
  if effective_variables = erck && erck[VARIABLES_KEY]
151
154
  recursive_replace_variables(
152
155
  [@kind, VARIABLES_KEY],
@@ -154,10 +157,10 @@ module RightDevelop::Testing::Recording
154
157
  effective_variables,
155
158
  erck[TRANSFORM_KEY])
156
159
  end
157
- if logger.debug?
158
- logger.debug("#{@kind} effective_route_config = #{@effective_route_config[@kind].inspect}")
159
- logger.debug("#{@kind} typenames_to_values = #{@typenames_to_values.inspect}")
160
- end
160
+ end
161
+ if logger.debug?
162
+ logger.debug("#{@kind} effective_route_config = #{@effective_route_config[@kind].inspect}")
163
+ logger.debug("#{@kind} typenames_to_values = #{@typenames_to_values.inspect}")
161
164
  end
162
165
 
163
166
  # recreate headers and body from data using variable substitutions and
@@ -181,6 +184,12 @@ module RightDevelop::Testing::Recording
181
184
  @checksum ||= compute_checksum
182
185
  end
183
186
 
187
+ # @return [Float] delay in seconds (of response) from effective
188
+ # configuration or empty
189
+ def delay_seconds
190
+ Float((@effective_route_config[@kind] || {})[DELAY_SECONDS_KEY] || 0)
191
+ end
192
+
184
193
  # @return [Hash] timeouts from effective configuration or empty
185
194
  def timeouts
186
195
  (@effective_route_config[@kind] || {})[TIMEOUTS_KEY] || {}
@@ -347,7 +356,7 @@ module RightDevelop::Testing::Recording
347
356
  # note that we never want to fail to proxy back a response because
348
357
  # the server doesn't know what JSON is; log a warning and continue.
349
358
  # this actually happens with Right API 1.5 health-check saying
350
- # response is 'application/json' but returning "ok"
359
+ # response is 'application/json' but returning "\"ok\""
351
360
  logger.warn("Failed to parse JSON data from #{body.inspect}: #{e.class} #{e.message}")
352
361
  end
353
362
  else
@@ -377,10 +386,10 @@ module RightDevelop::Testing::Recording
377
386
  when ::Hash, ::Array
378
387
  body_hash = body
379
388
  when ::String
380
- body_hash = parse_body(normalized_headers, body)
381
- return body unless body_hash
382
- else
383
- return body
389
+ # use unparsed original body to avoid losing information when we are
390
+ # unable to parse or parse a literal JSON string as happens in the case
391
+ # of RightAPI's health-check.
392
+ return @body
384
393
  end
385
394
  case ct = compute_content_type(normalized_headers)
386
395
  when 'application/x-www-form-urlencoded'
@@ -433,11 +442,16 @@ module RightDevelop::Testing::Recording
433
442
  match_deep(@typenames_to_values[qualifier_type], qualifier_name_to_value)
434
443
  end
435
444
  if all_matched
445
+ # note that we must be careful to deep clone the existing result
446
+ # before merging as any previously-merged configuration (shared
447
+ # from the original configuration) would be modified by deep
448
+ # merging. use .deep_merge instead of .deep_merge!
449
+ #
436
450
  # the final data is the union of all configurations matching
437
451
  # this request path and qualifiers. the uri regex and other
438
452
  # data used to match the request parameters is eliminated from
439
453
  # the final configuration.
440
- ::RightSupport::Data::HashTools.deep_merge!(result, configuration)
454
+ result = ::RightSupport::Data::HashTools.deep_merge(result, configuration)
441
455
  end
442
456
  end
443
457
  end
@@ -771,37 +785,15 @@ module RightDevelop::Testing::Recording
771
785
  significant_data = RightSupport::Data::Mash.new(verb: @verb)
772
786
  significant_data[:http_status] = @http_status if @http_status
773
787
 
774
- # headers
775
- copy_if_significant(:header, significant, significant_data)
776
-
777
- # query
778
- unless copy_if_significant(:query, significant, significant_data)
779
- # entire query string is significant by default.
780
- significant_data[:query] = @typenames_to_values[:query]
781
- end
782
-
783
- # body
784
- unless copy_if_significant(:body, significant, significant_data)
785
- case body_value = @typenames_to_values[:body]
786
- when nil
787
- # body is either nil, empty or was not parsable; insert the checksum
788
- # of the original body.
789
- case @body
790
- when nil, '', ' '
791
- significant_data[:body_checksum] = 'empty'
792
- else
793
- significant_data[:body_checksum] = ::Digest::MD5.hexdigest(@body)
794
- end
795
- else
796
- # body was parsed but no single element was considered significant.
797
- # use the parsed body so that it can be 'normalized' in sorted order.
798
- significant_data[:body] = body_value
799
- end
788
+ # significance by type.
789
+ [:header, :query, :body].each do |type|
790
+ copy_if_significant(type, significant, significant_data)
800
791
  end
801
792
 
802
793
  # use deep-sorted JSON to prevent random ordering changing the checksum.
803
794
  checksum_data = self.class.deep_sorted_json(significant_data)
804
- if logger.debug? && @mode != 'validate'
795
+ if logger.debug?
796
+ logger.debug("#{@kind} significant = #{significant.inspect}")
805
797
  logger.debug("#{@kind} checksum_data = #{checksum_data.inspect}")
806
798
  end
807
799
  ::Digest::MD5.hexdigest(checksum_data)
@@ -815,41 +807,138 @@ module RightDevelop::Testing::Recording
815
807
  # @param [Hash] significant selectors
816
808
  # @param [Hash] significant_data to populate
817
809
  #
818
- # @return [TrueClass|FalseClass] true if any were significant
810
+ # @return [TrueClass] always true
819
811
  def copy_if_significant(type, significant, significant_data)
820
- if significant_type = significant[type]
821
- significant_data[type] = recursive_selective_hash_copy(
822
- RightSupport::Data::Mash.new, @typenames_to_values[type], significant_type)
823
- true
812
+ case significant_type = significant[type]
813
+ when nil
814
+ # no explicit significance declared; use the default behavior for type.
815
+ default_copy_if_significant(type, significant_data)
816
+ when false
817
+ # no fields are significant; copy nothing.
818
+ when true
819
+ # all fields are significant; copy everything.
820
+ significant_data[type] = @typenames_to_values[type]
824
821
  else
825
- false
822
+ # recursively copy significant values from hash.
823
+ significant_data[type] = recursive_selective_hash_copy(
824
+ @typenames_to_values[type],
825
+ significant_type,
826
+ [type])
826
827
  end
828
+ true
827
829
  end
828
830
 
829
- # Recursively selects and copies values from source to target.
830
- def recursive_selective_hash_copy(target, source, selections, path = [])
831
- selections.each do |k, v|
832
- case v
831
+ # Copies fields by type only if considered significant by default.
832
+ #
833
+ # @param [String|Symbol] type of significance
834
+ # @param [Hash] significant_data to populate
835
+ #
836
+ # @return [TrueClass] always true
837
+ def default_copy_if_significant(type, significant_data)
838
+ case type
839
+ when :header
840
+ # headers are insignificant by default
841
+ when :query
842
+ significant_data[:query] = @typenames_to_values[:query]
843
+ when :body
844
+ case body_value = @typenames_to_values[:body]
833
845
  when nil
834
- # hash to nil; user configured by using flat hashes instead of arrays.
835
- # it's a style thing that makes the YAML look prettier.
836
- copy_hash_value(target, source, path + [k])
837
- when ::Array
838
- # also supporting arrays of names at top level or under a hash.
839
- v.each { |item| copy_hash_value(target, source, path + [item]) }
840
- when ::Hash
841
- # recursion.
842
- recursive_selective_hash_copy(target, source, v, path + [k])
846
+ # body is either nil, empty or was not parsable; insert the checksum
847
+ # of the original body.
848
+ case @body
849
+ when nil, '', ' '
850
+ significant_data[:body_checksum] = 'empty'
851
+ else
852
+ significant_data[:body_checksum] = ::Digest::MD5.hexdigest(@body)
853
+ end
854
+ else
855
+ # body was parsed but no single element was considered significant.
856
+ # use the parsed body so that it can be 'normalized' in sorted order.
857
+ significant_data[:body] = body_value
843
858
  end
859
+ else
860
+ raise ::NotImplementedError,
861
+ "Unexpected significant type: #{type.inspect}"
844
862
  end
845
- target
863
+ true
846
864
  end
847
865
 
848
- # copies a single value between hashes by path.
849
- def copy_hash_value(target, source, path)
850
- value = ::RightSupport::Data::HashTools.deep_get(source, path)
851
- ::RightSupport::Data::HashTools.deep_set!(target, path, value)
852
- true
866
+ # Recursively selects values from source.
867
+ #
868
+ # @param [Object] source for selection
869
+ # @param [Object] selections to perform
870
+ # @param [Array] log_path as array of keys or indexes for logging only
871
+ #
872
+ # @return [Object] target of same type as source
873
+ #
874
+ def recursive_selective_hash_copy(source, selections, log_path)
875
+ return nil if source.nil?
876
+ result = nil
877
+ case selections
878
+ when nil, true
879
+ # hash to nil or true; nil means that user configured by using flat
880
+ # hashes instead of arrays, which is a style thing that makes the YAML
881
+ # look prettier. true means explictly consider all elements at current
882
+ # key to be significant.
883
+ result = ::RightSupport::Data::HashTools.deep_clone2(source)
884
+ when false
885
+ # explicitly declared nothing at current level to be significant.
886
+ when ::Array
887
+ # supporting special case of an array of all key names as a list of
888
+ # significant hash key/values.
889
+ if selections.all? { |item| item.is_a?(::String) || item.is_a?(::Symbol) }
890
+ # select hash values by listed keys.
891
+ unless source.kind_of?(::Hash)
892
+ msg = 'Mismatched significant qualifier with source type at ' +
893
+ "#{log_path.join('/')}: #{source.class}"
894
+ raise ::ConfigurationError, msg
895
+ end
896
+ selections.each do |k|
897
+ source_value = source[k]
898
+ unless source_value.nil?
899
+ result ||= ::RightSupport::Data::Mash.new
900
+ result[k] = ::RightSupport::Data::HashTools.deep_clone2(source_value)
901
+ end
902
+ end
903
+ else
904
+ # select specific hash elements of array with specific significance.
905
+ # if a particular element has no siginificance then its selector can
906
+ # be a literal false or else it can fall off the end of the listed
907
+ # selectors.
908
+ unless source.kind_of?(::Array)
909
+ msg = 'Mismatched significant qualifier with source type at ' +
910
+ "#{log_path.join('/')}: #{source.class}"
911
+ raise ::ConfigurationError, msg
912
+ end
913
+ selections.each_with_index do |item, idx|
914
+ if idx < source.size
915
+ result ||= []
916
+ result << recursive_selective_hash_copy(
917
+ source[idx], item, log_path + ["[#{idx}]"])
918
+ else
919
+ break
920
+ end
921
+ end
922
+ end
923
+ when ::Hash
924
+ unless source.kind_of?(::Hash)
925
+ msg = 'Mismatched significant qualifier with source type at ' +
926
+ "#{log_path.join('/')}: #{source.class}"
927
+ raise ::ConfigurationError, msg
928
+ end
929
+ selections.each do |k, v|
930
+ target = recursive_selective_hash_copy(source[k], v, log_path + [k])
931
+ unless target.nil?
932
+ result ||= ::RightSupport::Data::Mash.new
933
+ result[k] = target
934
+ end
935
+ end
936
+ result
937
+ else
938
+ raise ::ConfigurationError,
939
+ "Unexpected significant value type at #{log_path.join('/')}: #{v.class}"
940
+ end
941
+ result
853
942
  end
854
943
 
855
944
  end # Metadata
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'rack'
4
4
  gem 'rest-client'
5
- gem 'right_support', '>= 2.8.31'
5
+ gem 'right_support', '>= 2.8.38'
@@ -5,7 +5,7 @@ GEM
5
5
  rack (1.5.2)
6
6
  rest-client (1.6.7)
7
7
  mime-types (>= 1.16)
8
- right_support (2.8.31)
8
+ right_support (2.8.38)
9
9
 
10
10
  PLATFORMS
11
11
  ruby
@@ -13,4 +13,4 @@ PLATFORMS
13
13
  DEPENDENCIES
14
14
  rack
15
15
  rest-client
16
- right_support (>= 2.8.31)
16
+ right_support (>= 2.8.38)
@@ -140,16 +140,18 @@ module RightDevelop::Testing::Server::MightApi
140
140
  # load request/response pair to validate.
141
141
  request_file_path = ::File.join(requests_dir, path)
142
142
  response_file_path = ::File.join(responses_dir, path)
143
+ logger.debug("Normalizing request #{request_file_path.inspect} ...")
143
144
  request_data = RightSupport::Data::Mash.new(::YAML.load_file(request_file_path))
144
145
  response_data = RightSupport::Data::Mash.new(::YAML.load_file(response_file_path))
145
146
 
146
147
  # if confing contains unreachable (i.e. no available route) files
147
148
  # then that is ignorable.
148
149
  query_string = request_data[:query]
150
+ path_parent = ::File.dirname(path)
149
151
  uri = METADATA_CLASS.normalize_uri(
150
152
  URI::HTTP.build(
151
153
  host: 'none',
152
- path: (route_path + ::File.dirname(path)),
154
+ path: path_parent == '.' ? route_path : (route_path + path_parent),
153
155
  query: request_data[:query]).to_s)
154
156
 
155
157
  # compute checksum from recorded request metadata.
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: right_develop 3.1.8 ruby lib
5
+ # stub: right_develop 3.1.9 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "right_develop"
9
- s.version = "3.1.8"
9
+ s.version = "3.1.9"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Tony Spataro"]
14
- s.date = "2014-11-11"
14
+ s.date = "2014-12-01"
15
15
  s.description = "A toolkit of development tools created by RightScale."
16
16
  s.email = "support@rightscale.com"
17
17
  s.executables = ["right_develop"]
@@ -81,7 +81,7 @@ Gem::Specification.new do |s|
81
81
  ]
82
82
  s.homepage = "https://github.com/rightscale/right_develop"
83
83
  s.licenses = ["MIT"]
84
- s.rubygems_version = "2.2.0"
84
+ s.rubygems_version = "2.2.2"
85
85
  s.summary = "Reusable dev & test code."
86
86
 
87
87
  if s.respond_to? :specification_version then
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: right_develop
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.8
4
+ version: 3.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Spataro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-11 00:00:00.000000000 Z
11
+ date: 2014-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: right_support
@@ -249,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
249
  version: '0'
250
250
  requirements: []
251
251
  rubyforge_project:
252
- rubygems_version: 2.2.0
252
+ rubygems_version: 2.2.2
253
253
  signing_key:
254
254
  specification_version: 4
255
255
  summary: Reusable dev & test code.