vcr 5.0.0 → 6.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fb0121a881f4d4b1ac0f68b5078f5674f137e07a4a0580b70a438f920c5953db
4
- data.tar.gz: 551623fd6fd0317c2510e1a02c36f6a8c3cac4bba72752ffd31907de0e1bfef9
3
+ metadata.gz: 0230f475b5c666e17c97d0b81695860d18a54c40d07dccfb82ab63098fc60084
4
+ data.tar.gz: b79ad4ff6425ef66da8810b75f9cdc787d656c97cb5d60acf31c9ba09a4b6a5d
5
5
  SHA512:
6
- metadata.gz: d6ad287e1f993836c3adac326228312596935313d65313fb60b07b16a962b0b5b9cff3f74db5200ce21e2c5ba5b6e32da0fd445d73fe7024c35308fecfcdc688
7
- data.tar.gz: bcf77106091efc49ae8ee789f076b162eb28491349a2e04ef0a1220ae5b1cfb3501de2e3106d8aa14459c7577207775650232ebf11235fe110c28dfa16614149
6
+ metadata.gz: 22d462bea8b4048575d82db89ecc36327be825267d76f69fa366ca98894b691210b6e859f4ab7d7acaf5fef537749b010ea166dcb09af642677bf402d6aaf855
7
+ data.tar.gz: 9f2a224345cabd78d03c0ac5f687b6261c7e1e34fe64c24dca7c4c46057de6122ec74d58c11ff3e3aefb8cc848efda899441c65c3d7dd0bdad86fbcccda9163d
@@ -32,7 +32,7 @@ module VCR
32
32
  end
33
33
 
34
34
  def erb_variables
35
- @erb if @erb.is_a?(Hash)
35
+ @erb if @erb.is_a?(Hash) && !@erb.empty?
36
36
  end
37
37
 
38
38
  def template
@@ -40,7 +40,9 @@ module VCR
40
40
  end
41
41
 
42
42
  @@struct_cache = Hash.new do |hash, attributes|
43
- hash[attributes] = Struct.new(*attributes)
43
+ attributes = attributes.map(&:to_sym)
44
+ hash[attributes] = Struct.new(*attributes) unless hash.key?(attributes)
45
+ hash[attributes]
44
46
  end
45
47
 
46
48
  def variables_object
@@ -50,7 +50,11 @@ module VCR
50
50
  end
51
51
 
52
52
  def load_yaml(cassette)
53
- ::YAML.load_file(cassette)
53
+ if ::YAML.respond_to?(:unsafe_load_file)
54
+ ::YAML.unsafe_load_file(cassette)
55
+ else
56
+ ::YAML.load_file(cassette)
57
+ end
54
58
  rescue *@yaml_load_errors
55
59
  return nil
56
60
  end
@@ -55,7 +55,15 @@ module VCR
55
55
  file_extension = '.' + parts.pop
56
56
  end
57
57
 
58
- parts.join('.').gsub(/[^[:word:]\-\/]+/, '_') + file_extension.to_s
58
+ file_name = parts.join('.').gsub(/[^[:word:]\-\/]+/, '_') + file_extension.to_s
59
+ file_name.downcase! if downcase_cassette_names?
60
+ file_name
61
+ end
62
+
63
+ def downcase_cassette_names?
64
+ !!VCR.configuration
65
+ .default_cassette_options
66
+ .dig(:persister_options, :downcase_cassette_names)
59
67
  end
60
68
  end
61
69
  end
@@ -1,9 +1,9 @@
1
- require 'multi_json'
1
+ require 'json'
2
2
 
3
3
  module VCR
4
4
  class Cassette
5
5
  class Serializers
6
- # The JSON serializer. Uses `MultiJson` under the covers.
6
+ # The JSON serializer.
7
7
  #
8
8
  # @see Psych
9
9
  # @see Syck
@@ -11,10 +11,14 @@ module VCR
11
11
  module JSON
12
12
  extend self
13
13
  extend EncodingErrorHandling
14
+ extend SyntaxErrorHandling
14
15
 
15
16
  # @private
16
- ENCODING_ERRORS = [MultiJson::DecodeError, ArgumentError]
17
- ENCODING_ERRORS << EncodingError if defined?(EncodingError)
17
+ ENCODING_ERRORS = [ArgumentError]
18
+ ENCODING_ERRORS << ::JSON::GeneratorError
19
+
20
+ # @private
21
+ SYNTAX_ERRORS = [::JSON::ParserError]
18
22
 
19
23
  # The file extension to use for this serializer.
20
24
  #
@@ -23,23 +27,25 @@ module VCR
23
27
  "json"
24
28
  end
25
29
 
26
- # Serializes the given hash using `MultiJson`.
30
+ # Serializes the given hash using `JSON`.
27
31
  #
28
32
  # @param [Hash] hash the object to serialize
29
33
  # @return [String] the JSON string
30
34
  def serialize(hash)
31
35
  handle_encoding_errors do
32
- MultiJson.encode(hash)
36
+ ::JSON.pretty_generate(hash)
33
37
  end
34
38
  end
35
39
 
36
- # Deserializes the given string using `MultiJson`.
40
+ # Deserializes the given string using `JSON`.
37
41
  #
38
42
  # @param [String] string the JSON string
39
43
  # @return [Hash] the deserialized object
40
44
  def deserialize(string)
41
45
  handle_encoding_errors do
42
- MultiJson.decode(string)
46
+ handle_syntax_errors do
47
+ ::JSON.parse(string)
48
+ end
43
49
  end
44
50
  end
45
51
  end
@@ -11,10 +11,14 @@ module VCR
11
11
  module Psych
12
12
  extend self
13
13
  extend EncodingErrorHandling
14
+ extend SyntaxErrorHandling
14
15
 
15
16
  # @private
16
17
  ENCODING_ERRORS = [ArgumentError]
17
18
 
19
+ # @private
20
+ SYNTAX_ERRORS = [::Psych::SyntaxError]
21
+
18
22
  # The file extension to use for this serializer.
19
23
  #
20
24
  # @return [String] "yml"
@@ -28,7 +32,9 @@ module VCR
28
32
  # @return [String] the YAML string
29
33
  def serialize(hash)
30
34
  handle_encoding_errors do
31
- ::Psych.dump(hash)
35
+ result = ::Psych.dump(hash)
36
+ result.gsub!(": \n", ": null\n") # set canonical null value in order to avoid trailing whitespaces
37
+ result
32
38
  end
33
39
  end
34
40
 
@@ -38,7 +44,9 @@ module VCR
38
44
  # @return [Hash] the deserialized object
39
45
  def deserialize(string)
40
46
  handle_encoding_errors do
41
- ::Psych.load(string)
47
+ handle_syntax_errors do
48
+ ::Psych.load(string)
49
+ end
42
50
  end
43
51
  end
44
52
  end
@@ -11,10 +11,14 @@ module VCR
11
11
  module Syck
12
12
  extend self
13
13
  extend EncodingErrorHandling
14
+ extend SyntaxErrorHandling
14
15
 
15
16
  # @private
16
17
  ENCODING_ERRORS = [ArgumentError]
17
18
 
19
+ # @private
20
+ SYNTAX_ERRORS = [::Psych::SyntaxError]
21
+
18
22
  # The file extension to use for this serializer.
19
23
  #
20
24
  # @return [String] "yml"
@@ -38,7 +42,9 @@ module VCR
38
42
  # @return [Hash] the deserialized object
39
43
  def deserialize(string)
40
44
  handle_encoding_errors do
41
- using_syck { ::YAML.load(string) }
45
+ handle_syntax_errors do
46
+ using_syck { ::YAML.load(string) }
47
+ end
42
48
  end
43
49
  end
44
50
 
@@ -13,10 +13,14 @@ module VCR
13
13
  module YAML
14
14
  extend self
15
15
  extend EncodingErrorHandling
16
+ extend SyntaxErrorHandling
16
17
 
17
18
  # @private
18
19
  ENCODING_ERRORS = [ArgumentError]
19
20
 
21
+ # @private
22
+ SYNTAX_ERRORS = [::Psych::SyntaxError]
23
+
20
24
  # The file extension to use for this serializer.
21
25
  #
22
26
  # @return [String] "yml"
@@ -30,7 +34,9 @@ module VCR
30
34
  # @return [String] the YAML string
31
35
  def serialize(hash)
32
36
  handle_encoding_errors do
33
- ::YAML.dump(hash)
37
+ result = ::YAML.dump(hash)
38
+ result.gsub!(": \n", ": null\n") # set canonical null value in order to avoid trailing whitespaces
39
+ result
34
40
  end
35
41
  end
36
42
 
@@ -40,7 +46,13 @@ module VCR
40
46
  # @return [Hash] the deserialized object
41
47
  def deserialize(string)
42
48
  handle_encoding_errors do
43
- ::YAML.load(string)
49
+ handle_syntax_errors do
50
+ if ::YAML.respond_to?(:unsafe_load)
51
+ ::YAML.unsafe_load(string)
52
+ else
53
+ ::YAML.load(string)
54
+ end
55
+ end
44
56
  end
45
57
  end
46
58
  end
@@ -54,6 +54,16 @@ module VCR
54
54
  raise
55
55
  end
56
56
  end
57
+
58
+ # @private
59
+ module SyntaxErrorHandling
60
+ def handle_syntax_errors
61
+ yield
62
+ rescue *self::SYNTAX_ERRORS => e
63
+ e.message << "\nNote: This is a VCR cassette. If it is using ERB, you may have forgotten to pass the `:erb` option to `use_cassette`."
64
+ raise
65
+ end
66
+ end
57
67
  end
58
68
  end
59
69
 
data/lib/vcr/cassette.rb CHANGED
@@ -24,6 +24,13 @@ module VCR
24
24
  # plays them back, or does both.
25
25
  attr_reader :record_mode
26
26
 
27
+ # @return [Boolean] The cassette's record_on_error mode. When the code that uses the cassette
28
+ # raises an error (for example a test failure) and record_on_error is set to false, no
29
+ # cassette will be recorded. This is useful when you are TDD'ing an API integration: when
30
+ # an error is raised that often means your request is invalid, so you don't want the cassette
31
+ # to be recorded.
32
+ attr_reader :record_on_error
33
+
27
34
  # @return [Array<Symbol, #call>] List of request matchers. Used to find a response from an
28
35
  # existing HTTP interaction to play back.
29
36
  attr_reader :match_requests_on
@@ -66,13 +73,28 @@ module VCR
66
73
  # @param (see VCR#eject_casssette)
67
74
  # @see VCR#eject_cassette
68
75
  def eject(options = {})
69
- write_recorded_interactions_to_disk
76
+ write_recorded_interactions_to_disk if should_write_recorded_interactions_to_disk?
70
77
 
71
78
  if should_assert_no_unused_interactions? && !options[:skip_no_unused_interactions_assertion]
72
79
  http_interactions.assert_no_unused_interactions!
73
80
  end
74
81
  end
75
82
 
83
+ # @private
84
+ def run_failed!
85
+ @run_failed = true
86
+ end
87
+
88
+ # @private
89
+ def run_failed?
90
+ @run_failed = false unless defined?(@run_failed)
91
+ @run_failed
92
+ end
93
+
94
+ def should_write_recorded_interactions_to_disk?
95
+ !run_failed? || record_on_error
96
+ end
97
+
76
98
  # @private
77
99
  def http_interactions
78
100
  # Without this mutex, under threaded access, an HTTPInteractionList will overwrite
@@ -151,10 +173,10 @@ module VCR
151
173
 
152
174
  def assert_valid_options!
153
175
  invalid_options = @options.keys - [
154
- :record, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
176
+ :record, :record_on_error, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
155
177
  :update_content_length_header, :allow_playback_repeats, :allow_unused_http_interactions,
156
178
  :exclusive, :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response,
157
- :recompress_response, :persist_with, :clean_outdated_http_interactions
179
+ :recompress_response, :persist_with, :persister_options, :clean_outdated_http_interactions
158
180
  ]
159
181
 
160
182
  if invalid_options.size > 0
@@ -163,17 +185,16 @@ module VCR
163
185
  end
164
186
 
165
187
  def extract_options
166
- [:erb, :match_requests_on, :re_record_interval, :clean_outdated_http_interactions,
188
+ [:record_on_error, :erb, :match_requests_on, :re_record_interval, :clean_outdated_http_interactions,
167
189
  :allow_playback_repeats, :allow_unused_http_interactions, :exclusive].each do |name|
168
190
  instance_variable_set("@#{name}", @options[name])
169
191
  end
170
192
 
171
193
  assign_tags
172
194
 
173
- @record_mode = @options[:record]
174
195
  @serializer = VCR.cassette_serializers[@options[:serialize_with]]
175
196
  @persister = VCR.cassette_persisters[@options[:persist_with]]
176
- @record_mode = :all if should_re_record?
197
+ @record_mode = should_re_record?(@options[:record]) ? :all : @options[:record]
177
198
  @parent_list = @exclusive ? HTTPInteractionList::NullList : VCR.http_interactions
178
199
  end
179
200
 
@@ -209,9 +230,10 @@ module VCR
209
230
  end
210
231
  end
211
232
 
212
- def should_re_record?
233
+ def should_re_record?(record_mode)
213
234
  return false unless @re_record_interval
214
235
  return false unless originally_recorded_at
236
+ return false if record_mode == :none
215
237
 
216
238
  now = Time.now
217
239
 
@@ -492,10 +492,12 @@ module VCR
492
492
  @rspec_metadata_configured = false
493
493
  @default_cassette_options = {
494
494
  :record => :once,
495
+ :record_on_error => true,
495
496
  :match_requests_on => RequestMatcherRegistry::DEFAULT_MATCHERS,
496
497
  :allow_unused_http_interactions => true,
497
498
  :serialize_with => :yaml,
498
- :persist_with => :file_system
499
+ :persist_with => :file_system,
500
+ :persister_options => {}
499
501
  }
500
502
 
501
503
  self.uri_parser = URI
@@ -2,137 +2,121 @@ require 'vcr/util/version_checker'
2
2
  require 'vcr/request_handler'
3
3
  require 'typhoeus'
4
4
 
5
- if Float(Typhoeus::VERSION[/^\d+\.\d+/]) < 0.5
6
- require 'vcr/library_hooks/typhoeus_0.4'
7
- else
8
- VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.5.0').check_version!
9
-
10
- module VCR
11
- class LibraryHooks
5
+ module VCR
6
+ class LibraryHooks
7
+ # @private
8
+ module Typhoeus
12
9
  # @private
13
- module Typhoeus
14
- # @private
15
- class RequestHandler < ::VCR::RequestHandler
16
- attr_reader :request
17
- def initialize(request)
18
- @request = request
19
- request.block_connection = false if VCR.turned_on?
20
- end
21
-
22
- def vcr_request
23
- @vcr_request ||= VCR::Request.new \
24
- request.options.fetch(:method, :get),
25
- request.url,
26
- request_body,
27
- request.options.fetch(:headers, {})
28
- end
29
-
30
- private
10
+ class RequestHandler < ::VCR::RequestHandler
11
+ attr_reader :request
12
+ def initialize(request)
13
+ @request = request
14
+ request.block_connection = false if VCR.turned_on?
15
+ end
31
16
 
32
- def externally_stubbed?
33
- ::Typhoeus::Expectation.find_by(request)
34
- end
17
+ def vcr_request
18
+ @vcr_request ||= VCR::Request.new \
19
+ request.options.fetch(:method, :get),
20
+ request.url,
21
+ request.encoded_body,
22
+ request.options.fetch(:headers, {})
23
+ end
35
24
 
36
- def set_typed_request_for_after_hook(*args)
37
- super
38
- request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request)
39
- end
25
+ private
40
26
 
41
- def on_unhandled_request
42
- invoke_after_request_hook(nil)
43
- super
44
- end
27
+ def externally_stubbed?
28
+ ::Typhoeus::Expectation.find_by(request)
29
+ end
45
30
 
46
- def on_stubbed_by_vcr_request
47
- response = ::Typhoeus::Response.new \
48
- :http_version => stubbed_response.http_version,
49
- :code => stubbed_response.status.code,
50
- :status_message => stubbed_response.status.message,
51
- :headers => stubbed_response_headers,
52
- :body => stubbed_response.body,
53
- :effective_url => stubbed_response.adapter_metadata.fetch('effective_url', request.url),
54
- :mock => true
55
-
56
- first_header_line = "HTTP/#{stubbed_response.http_version} #{response.code} #{response.status_message}\r\n"
57
- response.instance_variable_set(:@first_header_line, first_header_line)
58
- response.instance_variable_get(:@options)[:response_headers] =
59
- first_header_line + response.headers.map { |k,v| "#{k}: #{v}"}.join("\r\n")
60
-
61
- response
62
- end
31
+ def set_typed_request_for_after_hook(*args)
32
+ super
33
+ request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request)
34
+ end
63
35
 
64
- def stubbed_response_headers
65
- @stubbed_response_headers ||= {}.tap do |hash|
66
- stubbed_response.headers.each do |key, values|
67
- hash[key] = values.size == 1 ? values.first : values
68
- end if stubbed_response.headers
69
- end
70
- end
36
+ def on_unhandled_request
37
+ invoke_after_request_hook(nil)
38
+ super
39
+ end
71
40
 
72
- if ::Typhoeus::Request.method_defined?(:encoded_body)
73
- def request_body
74
- request.encoded_body
75
- end
76
- else
77
- def request_body
78
- request.options.fetch(:body, "")
79
- end
80
- end
41
+ def on_stubbed_by_vcr_request
42
+ response = ::Typhoeus::Response.new \
43
+ :http_version => stubbed_response.http_version,
44
+ :code => stubbed_response.status.code,
45
+ :status_message => stubbed_response.status.message,
46
+ :headers => stubbed_response_headers,
47
+ :body => stubbed_response.body,
48
+ :effective_url => stubbed_response.adapter_metadata.fetch('effective_url', request.url),
49
+ :mock => true
50
+
51
+ first_header_line = "HTTP/#{stubbed_response.http_version} #{response.code} #{response.status_message}\r\n"
52
+ response.instance_variable_set(:@first_header_line, first_header_line)
53
+ response.instance_variable_get(:@options)[:response_headers] =
54
+ first_header_line + response.headers.map { |k,v| "#{k}: #{v}"}.join("\r\n")
55
+
56
+ response
81
57
  end
82
58
 
83
- # @private
84
- class << self
85
- def vcr_response_from(response)
86
- VCR::Response.new \
87
- VCR::ResponseStatus.new(response.code, response.status_message),
88
- response.headers,
89
- response.body,
90
- response.http_version,
91
- { "effective_url" => response.effective_url }
59
+ def stubbed_response_headers
60
+ @stubbed_response_headers ||= {}.tap do |hash|
61
+ stubbed_response.headers.each do |key, values|
62
+ hash[key] = values.size == 1 ? values.first : values
63
+ end if stubbed_response.headers
92
64
  end
65
+ end
66
+ end
93
67
 
94
- def collect_chunks(request)
95
- chunks = ''
96
- request.on_body.unshift(
97
- Proc.new do |body, response|
98
- chunks += body
99
- request.instance_variable_set(:@chunked_body, chunks)
100
- end
101
- )
102
- end
68
+ # @private
69
+ class << self
70
+ def vcr_response_from(response)
71
+ VCR::Response.new \
72
+ VCR::ResponseStatus.new(response.code, response.status_message),
73
+ response.headers,
74
+ response.body,
75
+ response.http_version,
76
+ { "effective_url" => response.effective_url }
77
+ end
103
78
 
104
- def restore_body_from_chunks(response, request)
105
- response.options[:response_body] = request.instance_variable_get(:@chunked_body)
106
- end
79
+ def collect_chunks(request)
80
+ chunks = ''
81
+ request.on_body.unshift(
82
+ Proc.new do |body, response|
83
+ chunks += body
84
+ request.instance_variable_set(:@chunked_body, chunks)
85
+ end
86
+ )
107
87
  end
108
88
 
109
- ::Typhoeus.on_complete do |response|
110
- request = response.request
89
+ def restore_body_from_chunks(response, request)
90
+ response.options[:response_body] = request.instance_variable_get(:@chunked_body)
91
+ end
92
+ end
111
93
 
112
- restore_body_from_chunks(response, request) if request.streaming?
94
+ ::Typhoeus.on_complete do |response|
95
+ request = response.request
113
96
 
114
- unless VCR.library_hooks.disabled?(:typhoeus)
115
- vcr_response = vcr_response_from(response)
116
- typed_vcr_request = request.send(:remove_instance_variable, :@__typed_vcr_request)
97
+ restore_body_from_chunks(response, request) if request.streaming?
117
98
 
118
- unless request.response.mock
119
- http_interaction = VCR::HTTPInteraction.new(typed_vcr_request, vcr_response)
120
- VCR.record_http_interaction(http_interaction)
121
- end
99
+ unless VCR.library_hooks.disabled?(:typhoeus)
100
+ vcr_response = vcr_response_from(response)
101
+ typed_vcr_request = request.send(:remove_instance_variable, :@__typed_vcr_request)
122
102
 
123
- VCR.configuration.invoke_hook(:after_http_request, typed_vcr_request, vcr_response)
103
+ unless request.response.mock
104
+ http_interaction = VCR::HTTPInteraction.new(typed_vcr_request, vcr_response)
105
+ VCR.record_http_interaction(http_interaction)
124
106
  end
107
+
108
+ VCR.configuration.invoke_hook(:after_http_request, typed_vcr_request, vcr_response)
125
109
  end
110
+ end
126
111
 
127
- ::Typhoeus.before do |request|
128
- collect_chunks(request) if request.streaming?
129
- if response = VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle
130
- request.on_headers.each { |cb| cb.call(response) }
131
- request.on_body.each { |cb| cb.call(response.body, response) }
132
- request.finish(response)
133
- else
134
- true
135
- end
112
+ ::Typhoeus.before do |request|
113
+ collect_chunks(request) if request.streaming?
114
+ if response = VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle
115
+ request.on_headers.each { |cb| cb.call(response) }
116
+ request.on_body.each { |cb| cb.call(response.body, response) }
117
+ request.finish(response)
118
+ else
119
+ true
136
120
  end
137
121
  end
138
122
  end
@@ -146,4 +130,3 @@ VCR.configuration.after_library_hooks_loaded do
146
130
  WebMock::HttpLibAdapters::TyphoeusAdapter.disable!
147
131
  end
148
132
  end
149
-
@@ -12,8 +12,6 @@ module VCR
12
12
  module WebMock
13
13
  extend self
14
14
 
15
- @global_hook_disabled_requests = {}
16
-
17
15
  def with_global_hook_disabled(request)
18
16
  global_hook_disabled_requests << request
19
17
 
@@ -25,19 +23,12 @@ module VCR
25
23
  end
26
24
 
27
25
  def global_hook_disabled?(request)
28
- requests = @global_hook_disabled_requests[Thread.current.object_id]
26
+ requests = Thread.current[:_vcr_webmock_disabled_requests]
29
27
  requests && requests.include?(request)
30
28
  end
31
29
 
32
30
  def global_hook_disabled_requests
33
- requests = @global_hook_disabled_requests[Thread.current.object_id]
34
- return requests if requests
35
-
36
- ObjectSpace.define_finalizer(Thread.current, lambda {
37
- @global_hook_disabled_requests.delete(Thread.current.object_id)
38
- })
39
-
40
- @global_hook_disabled_requests[Thread.current.object_id] = []
31
+ Thread.current[:_vcr_webmock_disabled_requests] ||= []
41
32
  end
42
33
 
43
34
  # @private
@@ -9,8 +9,8 @@ module VCR
9
9
  include Enumerable
10
10
 
11
11
  # Creates a new list of context-owned cassettes and linked cassettes
12
- # @param [Array] context-owned cassettes
13
- # @param [Array] context-unowned (linked) cassettes
12
+ # @param cassettes [Array] context-owned cassettes
13
+ # @param linked_cassettes [Array] context-unowned (linked) cassettes
14
14
  def initialize(cassettes, linked_cassettes)
15
15
  @cassettes = cassettes
16
16
  @linked_cassettes = linked_cassettes
@@ -52,8 +52,8 @@ module VCR
52
52
  end
53
53
 
54
54
  # Create a new CassetteList
55
- # @param [Array] context-owned cassettes
56
- # @param [Array] context-unowned (linked) cassettes
55
+ # @param cassettes [Array] context-owned cassettes
56
+ # @param linked_cassettes [Array] context-unowned (linked) cassettes
57
57
  def self.list(cassettes, linked_cassettes)
58
58
  CassetteList.new(cassettes, linked_cassettes)
59
59
  end
@@ -74,7 +74,7 @@ module VCR
74
74
  def after_request(response)
75
75
  vcr_response = vcr_response_for(response)
76
76
 
77
- if should_record?
77
+ if vcr_response && should_record?
78
78
  VCR.record_http_interaction(VCR::HTTPInteraction.new(vcr_request, vcr_response))
79
79
  end
80
80
 
@@ -31,6 +31,11 @@ module VCR
31
31
  RequestHandler.new(@app, env).handle
32
32
  end
33
33
 
34
+ # Close any persistent connections.
35
+ def close
36
+ @app.close if @app.respond_to?(:close)
37
+ end
38
+
34
39
  # @private
35
40
  class RequestHandler < ::VCR::RequestHandler
36
41
  attr_reader :app, :env
@@ -25,6 +25,10 @@ module VCR
25
25
  end
26
26
  end
27
27
 
28
+ def localhost_ignored?
29
+ (LOCALHOST_ALIASES & ignore_hosts.to_a).any?
30
+ end
31
+
28
32
  def ignore_hosts(*hosts)
29
33
  ignored_hosts.merge(hosts)
30
34
  end
@@ -110,12 +110,12 @@ module VCR
110
110
 
111
111
  def register_built_ins
112
112
  register(:method) { |r1, r2| r1.method == r2.method }
113
- register(:uri) { |r1, r2| r1.uri == r2.uri }
113
+ register(:uri) { |r1, r2| r1.parsed_uri == r2.parsed_uri }
114
114
  register(:body) { |r1, r2| r1.body == r2.body }
115
115
  register(:headers) { |r1, r2| r1.headers == r2.headers }
116
116
 
117
117
  register(:host) do |r1, r2|
118
- r1.parsed_uri.host == r2.parsed_uri.host
118
+ r1.parsed_uri.host.chomp('.') == r2.parsed_uri.host.chomp('.')
119
119
  end
120
120
  register(:path) do |r1, r2|
121
121
  r1.parsed_uri.path == r2.parsed_uri.path
data/lib/vcr/structs.rb CHANGED
@@ -62,7 +62,7 @@ module VCR
62
62
  super
63
63
 
64
64
  if body && !body.is_a?(String)
65
- raise ArgumentError, "#{self.class} initialized with an invalid body: #{body.inspect}."
65
+ raise ArgumentError, "#{self.class} initialized with an invalid (non-String) body of class #{body.class}: #{body.inspect}."
66
66
  end
67
67
 
68
68
  # Ensure that the body is a raw string, in case the string instance
@@ -346,9 +346,9 @@ module VCR
346
346
  {
347
347
  'status' => status.to_hash,
348
348
  'headers' => headers,
349
- 'body' => serializable_body,
350
- 'http_version' => http_version
349
+ 'body' => serializable_body
351
350
  }.tap do |hash|
351
+ hash['http_version'] = http_version if http_version
352
352
  hash['adapter_metadata'] = adapter_metadata unless adapter_metadata.empty?
353
353
  end
354
354
  end
@@ -454,9 +454,10 @@ module VCR
454
454
 
455
455
  case type
456
456
  when 'gzip'
457
- args = [StringIO.new(body)]
458
- args << { :encoding => 'ASCII-8BIT' } if ''.respond_to?(:encoding)
459
- yield Zlib::GzipReader.new(*args).read
457
+ gzip_reader_options = {}
458
+ gzip_reader_options[:encoding] = 'ASCII-8BIT' if ''.respond_to?(:encoding)
459
+ yield Zlib::GzipReader.new(StringIO.new(body),
460
+ **gzip_reader_options).read
460
461
  when 'deflate'
461
462
  yield Zlib::Inflate.inflate(body)
462
463
  when 'identity', NilClass
@@ -23,10 +23,10 @@ module VCR
23
23
  # Adds `Before` and `After` cucumber hooks for the named tags that
24
24
  # will cause a VCR cassette to be used for scenarios with matching tags.
25
25
  #
26
- # @param [Array<String>] tag_names the cucumber scenario tags
27
- # @param [(optional) Hash] options the cassette options. Specify
28
- # `:use_scenario_name => true` to automatically name the
29
- # cassette according to the scenario name.
26
+ # @param tag_names [Array<String,Hash>] the cucumber scenario tags. If
27
+ # the last argument is a hash it is treated as cassette options.
28
+ # - `:use_scenario_name => true` to automatically name the
29
+ # cassette according to the scenario name.
30
30
  def tags(*tag_names)
31
31
  original_options = tag_names.last.is_a?(::Hash) ? tag_names.pop : {}
32
32
  tag_names.each do |tag_name|
@@ -47,10 +47,21 @@ module VCR
47
47
  scenario.scenario_outline.name,
48
48
  scenario.name.split("\n").first
49
49
  ].join("/")
50
- else
50
+ elsif scenario.respond_to?(:feature)
51
51
  [ scenario.feature.name.split("\n").first,
52
52
  scenario.name.split("\n").first
53
53
  ].join("/")
54
+ elsif scenario.location.lines.min == scenario.location.lines.max
55
+ # test case from a regular scenario in cucumber version 4
56
+ [ scenario.location.file.split("/").last.split(".").first,
57
+ scenario.name.split("\n").first
58
+ ].join("/")
59
+ else
60
+ # test case from a scenario with examples ("scenario outline") in cucumber version 4
61
+ [ scenario.location.file.split("/").last.split(".").first,
62
+ scenario.name.split("\n").first,
63
+ "Example at line #{scenario.location.lines.max}"
64
+ ].join("/")
54
65
  end
55
66
  else
56
67
  "cucumber_tags/#{tag_name.gsub(/\A@/, '')}"
@@ -5,38 +5,50 @@ module VCR
5
5
  module Metadata
6
6
  extend self
7
7
 
8
+ def vcr_cassette_name_for(metadata)
9
+ description =
10
+ if metadata[:description].empty?
11
+ # we have an "it { is_expected.to be something }" block
12
+ metadata[:scoped_id]
13
+ else
14
+ metadata[:description]
15
+ end
16
+ example_group =
17
+ if metadata.key?(:example_group)
18
+ metadata[:example_group]
19
+ else
20
+ metadata[:parent_example_group]
21
+ end
22
+
23
+ if example_group
24
+ [vcr_cassette_name_for(example_group), description].join('/')
25
+ else
26
+ description
27
+ end
28
+ end
29
+
8
30
  def configure!
9
31
  ::RSpec.configure do |config|
10
- vcr_cassette_name_for = lambda do |metadata|
11
- description = if metadata[:description].empty?
12
- # we have an "it { is_expected.to be something }" block
13
- metadata[:scoped_id]
14
- else
15
- metadata[:description]
16
- end
17
- example_group = if metadata.key?(:example_group)
18
- metadata[:example_group]
19
- else
20
- metadata[:parent_example_group]
21
- end
22
-
23
- if example_group
24
- [vcr_cassette_name_for[example_group], description].join('/')
25
- else
26
- description
27
- end
28
- end
29
32
 
30
33
  when_tagged_with_vcr = { :vcr => lambda { |v| !!v } }
31
34
 
32
35
  config.before(:each, when_tagged_with_vcr) do |ex|
33
36
  example = ex.respond_to?(:metadata) ? ex : ex.example
34
37
 
38
+ cassette_name = nil
35
39
  options = example.metadata[:vcr]
36
- options = options.is_a?(Hash) ? options.dup : {} # in case it's just :vcr => true
40
+ options = case options
41
+ when Hash #=> vcr: { cassette_name: 'foo' }
42
+ options.dup
43
+ when String #=> vcr: 'bar'
44
+ cassette_name = options.dup
45
+ {}
46
+ else #=> :vcr or vcr: true
47
+ {}
48
+ end
37
49
 
38
- cassette_name = options.delete(:cassette_name) ||
39
- vcr_cassette_name_for[example.metadata]
50
+ cassette_name ||= options.delete(:cassette_name) ||
51
+ VCR::RSpec::Metadata.vcr_cassette_name_for(example.metadata)
40
52
  VCR.insert_cassette(cassette_name, options)
41
53
  end
42
54
 
data/lib/vcr/version.rb CHANGED
@@ -10,7 +10,7 @@ module VCR
10
10
  # * `parts` [Array<Integer>] List of the version parts.
11
11
  def version
12
12
  @version ||= begin
13
- string = '5.0.0'
13
+ string = +'6.1.0'
14
14
 
15
15
  def string.parts
16
16
  split('.').map { |p| p.to_i }
@@ -28,7 +28,7 @@ module VCR
28
28
  parts[2]
29
29
  end
30
30
 
31
- string
31
+ string.freeze
32
32
  end
33
33
  end
34
34
  end
data/lib/vcr.rb CHANGED
@@ -52,7 +52,7 @@ module VCR
52
52
  # Inserts the named cassette using the given cassette options.
53
53
  # New HTTP interactions, if allowed by the cassette's `:record` option, will
54
54
  # be recorded to the cassette. The cassette's existing HTTP interactions
55
- # will be used to stub requests, unless prevented by the cassete's
55
+ # will be used to stub requests, unless prevented by the cassette's
56
56
  # `:record` option.
57
57
  #
58
58
  # @example
@@ -107,6 +107,12 @@ module VCR
107
107
  # @option options :persist_with [Symbol] Which cassette persister to
108
108
  # use. Defaults to :file_system. You can also register and use a
109
109
  # custom persister.
110
+ # @option options :persister_options [Hash] Pass options to the
111
+ # persister specified in `persist_with`. Currently available options for the file_system persister:
112
+ # - `:downcase_cassette_names`: when `true`, names of cassettes will be
113
+ # normalized in lowercase before reading and writing, which can avoid
114
+ # confusion when using both case-sensitive and case-insensitive file
115
+ # systems.
110
116
  # @option options :preserve_exact_body_bytes [Boolean] Whether or not
111
117
  # to base64 encode the bytes of the requests and responses for this cassette
112
118
  # when serializing it. See also `VCR::Configuration#preserve_exact_body_bytes`.
@@ -186,6 +192,9 @@ module VCR
186
192
 
187
193
  begin
188
194
  call_block(block, cassette)
195
+ rescue StandardError
196
+ cassette.run_failed!
197
+ raise
189
198
  ensure
190
199
  eject_cassette
191
200
  end
@@ -194,13 +203,13 @@ module VCR
194
203
  # Inserts multiple cassettes the given names
195
204
  #
196
205
  # @example
197
- # cassettes = [
198
- # { name: 'github' },
199
- # { name: 'apple', options: { erb: true } }
200
- # ]
201
- # VCR.use_cassettes() do
202
- # # make multiple HTTP requests
203
- # end
206
+ # cassettes = [
207
+ # { name: 'github' },
208
+ # { name: 'apple', options: { erb: true } }
209
+ # ]
210
+ # VCR.use_cassettes(cassettes) do
211
+ # # make multiple HTTP requests
212
+ # end
204
213
  def use_cassettes(cassettes, &block)
205
214
  cassette = cassettes.pop
206
215
  use_cassette(cassette[:name], cassette[:options] || {}) do
@@ -257,6 +266,7 @@ module VCR
257
266
  # @see #turn_off!
258
267
  # @see #turn_on!
259
268
  # @see #turned_on?
269
+ # @see #turned_on
260
270
  def turned_off(options = {})
261
271
  turn_off!(options)
262
272
 
@@ -292,11 +302,28 @@ module VCR
292
302
  set_context_value(:turned_off, true)
293
303
  end
294
304
 
305
+ # Turns on VCR, for the duration of a block.
306
+ # @param (see #turn_off!)
307
+ # @return [void]
308
+ # @see #turn_off!
309
+ # @see #turned_off
310
+ # @see #turned_on?
311
+ def turned_on(options = {})
312
+ turn_on!
313
+
314
+ begin
315
+ yield
316
+ ensure
317
+ turn_off!(options)
318
+ end
319
+ end
320
+
295
321
  # Turns on VCR, if it has previously been turned off.
296
322
  # @return [void]
297
323
  # @see #turn_off!
298
324
  # @see #turned_off
299
325
  # @see #turned_on?
326
+ # @see #turned_on
300
327
  def turn_on!
301
328
  set_context_value(:turned_off, false)
302
329
  end
@@ -308,6 +335,9 @@ module VCR
308
335
  # @see #turn_off!
309
336
  # @see #turned_off
310
337
  def turned_on?
338
+ linked_context = current_context[:linked_context]
339
+ return !linked_context[:turned_off] if linked_context
340
+
311
341
  !context_value(:turned_off)
312
342
  end
313
343
 
metadata CHANGED
@@ -1,14 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vcr
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Myron Marston
8
- autorequire:
8
+ - Kurtis Rainbolt-Greene
9
+ - Olle Jonsson
10
+ autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
- date: 2019-05-27 00:00:00.000000000 Z
13
+ date: 2022-03-13 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: bundler
@@ -44,28 +46,28 @@ dependencies:
44
46
  requirements:
45
47
  - - "~>"
46
48
  - !ruby/object:Gem::Version
47
- version: 3.1.4
49
+ version: 3.4.4
48
50
  type: :development
49
51
  prerelease: false
50
52
  version_requirements: !ruby/object:Gem::Requirement
51
53
  requirements:
52
54
  - - "~>"
53
55
  - !ruby/object:Gem::Version
54
- version: 3.1.4
56
+ version: 3.4.4
55
57
  - !ruby/object:Gem::Dependency
56
58
  name: rake
57
59
  requirement: !ruby/object:Gem::Requirement
58
60
  requirements:
59
- - - "~>"
61
+ - - ">="
60
62
  - !ruby/object:Gem::Version
61
- version: '10.1'
63
+ version: 12.3.3
62
64
  type: :development
63
65
  prerelease: false
64
66
  version_requirements: !ruby/object:Gem::Requirement
65
67
  requirements:
66
- - - "~>"
68
+ - - ">="
67
69
  - !ruby/object:Gem::Version
68
- version: '10.1'
70
+ version: 12.3.3
69
71
  - !ruby/object:Gem::Dependency
70
72
  name: pry
71
73
  requirement: !ruby/object:Gem::Requirement
@@ -150,48 +152,74 @@ dependencies:
150
152
  - - ">="
151
153
  - !ruby/object:Gem::Version
152
154
  version: '0'
155
+ - !ruby/object:Gem::Dependency
156
+ name: hashdiff
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: 1.0.0.beta1
162
+ - - "<"
163
+ - !ruby/object:Gem::Version
164
+ version: 2.0.0
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: 1.0.0.beta1
172
+ - - "<"
173
+ - !ruby/object:Gem::Version
174
+ version: 2.0.0
153
175
  - !ruby/object:Gem::Dependency
154
176
  name: cucumber
155
177
  requirement: !ruby/object:Gem::Requirement
156
178
  requirements:
157
179
  - - "~>"
158
180
  - !ruby/object:Gem::Version
159
- version: 2.0.2
181
+ version: '7.0'
160
182
  type: :development
161
183
  prerelease: false
162
184
  version_requirements: !ruby/object:Gem::Requirement
163
185
  requirements:
164
186
  - - "~>"
165
187
  - !ruby/object:Gem::Version
166
- version: 2.0.2
188
+ version: '7.0'
167
189
  - !ruby/object:Gem::Dependency
168
190
  name: aruba
169
191
  requirement: !ruby/object:Gem::Requirement
170
192
  requirements:
171
193
  - - "~>"
172
194
  - !ruby/object:Gem::Version
173
- version: 0.5.3
195
+ version: 0.14.14
174
196
  type: :development
175
197
  prerelease: false
176
198
  version_requirements: !ruby/object:Gem::Requirement
177
199
  requirements:
178
200
  - - "~>"
179
201
  - !ruby/object:Gem::Version
180
- version: 0.5.3
202
+ version: 0.14.14
181
203
  - !ruby/object:Gem::Dependency
182
204
  name: faraday
183
205
  requirement: !ruby/object:Gem::Requirement
184
206
  requirements:
185
- - - "~>"
207
+ - - ">="
186
208
  - !ruby/object:Gem::Version
187
209
  version: 0.11.0
210
+ - - "<"
211
+ - !ruby/object:Gem::Version
212
+ version: 2.0.0
188
213
  type: :development
189
214
  prerelease: false
190
215
  version_requirements: !ruby/object:Gem::Requirement
191
216
  requirements:
192
- - - "~>"
217
+ - - ">="
193
218
  - !ruby/object:Gem::Version
194
219
  version: 0.11.0
220
+ - - "<"
221
+ - !ruby/object:Gem::Version
222
+ version: 2.0.0
195
223
  - !ruby/object:Gem::Dependency
196
224
  name: httpclient
197
225
  requirement: !ruby/object:Gem::Requirement
@@ -210,14 +238,14 @@ dependencies:
210
238
  name: excon
211
239
  requirement: !ruby/object:Gem::Requirement
212
240
  requirements:
213
- - - '='
241
+ - - ">="
214
242
  - !ruby/object:Gem::Version
215
243
  version: 0.62.0
216
244
  type: :development
217
245
  prerelease: false
218
246
  version_requirements: !ruby/object:Gem::Requirement
219
247
  requirements:
220
- - - '='
248
+ - - ">="
221
249
  - !ruby/object:Gem::Version
222
250
  version: 0.62.0
223
251
  - !ruby/object:Gem::Dependency
@@ -234,20 +262,6 @@ dependencies:
234
262
  - - ">="
235
263
  - !ruby/object:Gem::Version
236
264
  version: '0'
237
- - !ruby/object:Gem::Dependency
238
- name: multi_json
239
- requirement: !ruby/object:Gem::Requirement
240
- requirements:
241
- - - ">="
242
- - !ruby/object:Gem::Version
243
- version: '0'
244
- type: :development
245
- prerelease: false
246
- version_requirements: !ruby/object:Gem::Requirement
247
- requirements:
248
- - - ">="
249
- - !ruby/object:Gem::Version
250
- version: '0'
251
265
  - !ruby/object:Gem::Dependency
252
266
  name: json
253
267
  requirement: !ruby/object:Gem::Requirement
@@ -307,7 +321,7 @@ dependencies:
307
321
  description: Record your test suite's HTTP interactions and replay them during future
308
322
  test runs for fast, deterministic, accurate tests.
309
323
  email:
310
- - myron.marston@gmail.com
324
+ - kurtis@rainbolt-greene.online
311
325
  executables: []
312
326
  extensions: []
313
327
  extra_rdoc_files: []
@@ -332,7 +346,6 @@ files:
332
346
  - lib/vcr/library_hooks/excon.rb
333
347
  - lib/vcr/library_hooks/faraday.rb
334
348
  - lib/vcr/library_hooks/typhoeus.rb
335
- - lib/vcr/library_hooks/typhoeus_0.4.rb
336
349
  - lib/vcr/library_hooks/webmock.rb
337
350
  - lib/vcr/linked_cassette.rb
338
351
  - lib/vcr/middleware/excon.rb
@@ -354,9 +367,10 @@ files:
354
367
  - lib/vcr/version.rb
355
368
  homepage: https://relishapp.com/vcr/vcr/docs
356
369
  licenses:
370
+ - Hippocratic-2.1
357
371
  - MIT
358
372
  metadata: {}
359
- post_install_message:
373
+ post_install_message:
360
374
  rdoc_options: []
361
375
  require_paths:
362
376
  - lib
@@ -364,15 +378,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
364
378
  requirements:
365
379
  - - ">="
366
380
  - !ruby/object:Gem::Version
367
- version: 1.9.3
381
+ version: '2.6'
368
382
  required_rubygems_version: !ruby/object:Gem::Requirement
369
383
  requirements:
370
384
  - - ">="
371
385
  - !ruby/object:Gem::Version
372
386
  version: '0'
373
387
  requirements: []
374
- rubygems_version: 3.0.3
375
- signing_key:
388
+ rubygems_version: 3.2.7
389
+ signing_key:
376
390
  specification_version: 4
377
391
  summary: Record your test suite's HTTP interactions and replay them during future
378
392
  test runs for fast, deterministic, accurate tests.
@@ -1,103 +0,0 @@
1
- VCR::VersionChecker.new('Typhoeus', Typhoeus::VERSION, '0.3.2').check_version!
2
-
3
- module VCR
4
- class LibraryHooks
5
- # @private
6
- module Typhoeus
7
- # @private
8
- class RequestHandler < ::VCR::RequestHandler
9
- attr_reader :request
10
- def initialize(request)
11
- @request = request
12
- end
13
-
14
- def vcr_request
15
- @vcr_request ||= VCR::Request.new \
16
- request.method,
17
- request.url,
18
- request.body,
19
- request.headers
20
- end
21
-
22
- private
23
-
24
- def externally_stubbed?
25
- ::Typhoeus::Hydra.stubs.detect { |stub| stub.matches?(request) }
26
- end
27
-
28
- def set_typed_request_for_after_hook(*args)
29
- super
30
- request.instance_variable_set(:@__typed_vcr_request, @after_hook_typed_request)
31
- end
32
-
33
- def on_unhandled_request
34
- invoke_after_request_hook(nil)
35
- super
36
- end
37
-
38
- def on_stubbed_by_vcr_request
39
- ::Typhoeus::Response.new \
40
- :http_version => stubbed_response.http_version,
41
- :code => stubbed_response.status.code,
42
- :status_message => stubbed_response.status.message,
43
- :headers_hash => stubbed_response_headers,
44
- :body => stubbed_response.body
45
- end
46
-
47
- def stubbed_response_headers
48
- @stubbed_response_headers ||= {}.tap do |hash|
49
- stubbed_response.headers.each do |key, values|
50
- hash[key] = values.size == 1 ? values.first : values
51
- end if stubbed_response.headers
52
- end
53
- end
54
- end
55
-
56
- # @private
57
- def self.vcr_response_from(response)
58
- VCR::Response.new \
59
- VCR::ResponseStatus.new(response.code, response.status_message),
60
- response.headers_hash,
61
- response.body,
62
- response.http_version
63
- end
64
-
65
- ::Typhoeus::Hydra.after_request_before_on_complete do |request|
66
- unless VCR.library_hooks.disabled?(:typhoeus)
67
- vcr_response = vcr_response_from(request.response)
68
- typed_vcr_request = request.send(:remove_instance_variable, :@__typed_vcr_request)
69
-
70
- unless request.response.mock?
71
- http_interaction = VCR::HTTPInteraction.new(typed_vcr_request, vcr_response)
72
- VCR.record_http_interaction(http_interaction)
73
- end
74
-
75
- VCR.configuration.invoke_hook(:after_http_request, typed_vcr_request, vcr_response)
76
- end
77
- end
78
-
79
- ::Typhoeus::Hydra.register_stub_finder do |request|
80
- VCR::LibraryHooks::Typhoeus::RequestHandler.new(request).handle
81
- end
82
- end
83
- end
84
- end
85
-
86
- # @private
87
- module Typhoeus
88
- class << Hydra
89
- # ensure HTTP requests are always allowed; VCR takes care of disallowing
90
- # them at the appropriate times in its hook
91
- def allow_net_connect_with_vcr?(*args)
92
- VCR.turned_on? ? true : allow_net_connect_without_vcr?
93
- end
94
-
95
- alias allow_net_connect_without_vcr? allow_net_connect?
96
- alias allow_net_connect? allow_net_connect_with_vcr?
97
- end unless Hydra.respond_to?(:allow_net_connect_with_vcr?)
98
- end
99
-
100
- VCR.configuration.after_library_hooks_loaded do
101
- ::Kernel.warn "WARNING: VCR's Typhoeus 0.4 integration is deprecated and will be removed in VCR 3.0."
102
- end
103
-