vcr 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.travis.yml +2 -1
  2. data/CHANGELOG.md +58 -1
  3. data/Gemfile +0 -6
  4. data/README.md +12 -3
  5. data/Rakefile +2 -2
  6. data/features/.nav +2 -0
  7. data/features/cassettes/allow_unused_http_interactions.feature +86 -0
  8. data/features/cassettes/naming.feature +3 -2
  9. data/features/cassettes/persistence.feature +63 -0
  10. data/features/configuration/debug_logging.feature +3 -3
  11. data/features/configuration/ignore_request.feature +1 -1
  12. data/features/hooks/after_http_request.feature +1 -1
  13. data/features/step_definitions/cli_steps.rb +33 -0
  14. data/features/support/env.rb +0 -1
  15. data/lib/vcr.rb +13 -0
  16. data/lib/vcr/cassette.rb +57 -44
  17. data/lib/vcr/cassette/{reader.rb → erb_renderer.rb} +9 -11
  18. data/lib/vcr/cassette/http_interaction_list.rb +18 -0
  19. data/lib/vcr/cassette/persisters.rb +42 -0
  20. data/lib/vcr/cassette/persisters/file_system.rb +60 -0
  21. data/lib/vcr/cassette/serializers.rb +1 -1
  22. data/lib/vcr/cassette/serializers/json.rb +1 -1
  23. data/lib/vcr/cassette/serializers/psych.rb +1 -1
  24. data/lib/vcr/cassette/serializers/syck.rb +1 -1
  25. data/lib/vcr/cassette/serializers/yaml.rb +1 -1
  26. data/lib/vcr/configuration.rb +49 -25
  27. data/lib/vcr/errors.rb +6 -0
  28. data/lib/vcr/library_hooks/excon.rb +1 -1
  29. data/lib/vcr/library_hooks/fakeweb.rb +16 -3
  30. data/lib/vcr/library_hooks/faraday.rb +13 -0
  31. data/lib/vcr/library_hooks/typhoeus.rb +5 -1
  32. data/lib/vcr/library_hooks/webmock.rb +90 -35
  33. data/lib/vcr/middleware/faraday.rb +12 -4
  34. data/lib/vcr/request_handler.rb +10 -2
  35. data/lib/vcr/structs.rb +31 -7
  36. data/lib/vcr/version.rb +1 -1
  37. data/script/ci.sh +14 -0
  38. data/spec/spec_helper.rb +8 -1
  39. data/spec/support/shared_example_groups/hook_into_http_library.rb +31 -1
  40. data/spec/vcr/cassette/{reader_spec.rb → erb_renderer_spec.rb} +15 -21
  41. data/spec/vcr/cassette/http_interaction_list_spec.rb +15 -0
  42. data/spec/vcr/cassette/persisters/file_system_spec.rb +64 -0
  43. data/spec/vcr/cassette/persisters_spec.rb +39 -0
  44. data/spec/vcr/cassette/serializers_spec.rb +4 -3
  45. data/spec/vcr/cassette_spec.rb +65 -41
  46. data/spec/vcr/configuration_spec.rb +33 -2
  47. data/spec/vcr/library_hooks/fakeweb_spec.rb +11 -0
  48. data/spec/vcr/library_hooks/faraday_spec.rb +24 -0
  49. data/spec/vcr/library_hooks/webmock_spec.rb +82 -2
  50. data/spec/vcr/middleware/faraday_spec.rb +32 -0
  51. data/spec/vcr/structs_spec.rb +60 -20
  52. data/spec/vcr/util/hooks_spec.rb +7 -0
  53. data/spec/vcr_spec.rb +7 -0
  54. data/vcr.gemspec +2 -2
  55. metadata +31 -26
  56. data/Guardfile +0 -9
  57. data/script/FullBuildRakeFile +0 -56
  58. data/script/full_build +0 -1
  59. data/script/spec +0 -1
data/lib/vcr.rb CHANGED
@@ -3,6 +3,7 @@ require 'vcr/util/variable_args_block_caller'
3
3
 
4
4
  require 'vcr/cassette'
5
5
  require 'vcr/cassette/serializers'
6
+ require 'vcr/cassette/persisters'
6
7
  require 'vcr/configuration'
7
8
  require 'vcr/deprecations'
8
9
  require 'vcr/errors'
@@ -80,12 +81,19 @@ module VCR
80
81
  # @option options :allow_playback_repeats [Boolean] Whether or not to
81
82
  # allow a single HTTP interaction to be played back multiple times.
82
83
  # Defaults to false.
84
+ # @options options :allow_unused_http_interactions [Boolean] If set to
85
+ # false, an error will be raised if a cassette is ejected before all
86
+ # previously recorded HTTP interactions have been used.
87
+ # Defaults to true.
83
88
  # @option options :exclusive [Boolean] Whether or not to use only this
84
89
  # cassette and to completely ignore any cassettes in the cassettes stack.
85
90
  # Defaults to false.
86
91
  # @option options :serialize_with [Symbol] Which serializer to use.
87
92
  # Valid values are :yaml, :syck, :psych, :json or any registered
88
93
  # custom serializer. Defaults to :yaml.
94
+ # @option options :persist_with [Symbol] Which cassette persister to
95
+ # use. Defaults to :file_system. You can also register and use a
96
+ # custom persister.
89
97
  # @option options :preserve_exact_body_bytes [Boolean] Whether or not
90
98
  # to base64 encode the bytes of the requests and responses for this cassette
91
99
  # when serializing it. See also `VCR::Configuration#preserve_exact_body_bytes`.
@@ -292,6 +300,11 @@ module VCR
292
300
  @cassette_serializers ||= Cassette::Serializers.new
293
301
  end
294
302
 
303
+ # @private
304
+ def cassette_persisters
305
+ @cassette_persisters ||= Cassette::Persisters.new
306
+ end
307
+
295
308
  # @private
296
309
  def record_http_interaction(interaction)
297
310
  return unless cassette = current_cassette
data/lib/vcr/cassette.rb CHANGED
@@ -1,8 +1,5 @@
1
- require 'fileutils'
2
- require 'erb'
3
-
4
1
  require 'vcr/cassette/http_interaction_list'
5
- require 'vcr/cassette/reader'
2
+ require 'vcr/cassette/erb_renderer'
6
3
  require 'vcr/cassette/serializers'
7
4
 
8
5
  module VCR
@@ -46,35 +43,14 @@ module VCR
46
43
  # @param (see VCR#insert_cassette)
47
44
  # @see VCR#insert_cassette
48
45
  def initialize(name, options = {})
49
- options = VCR.configuration.default_cassette_options.merge(options)
50
- invalid_options = options.keys - [
51
- :record, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
52
- :update_content_length_header, :allow_playback_repeats, :exclusive,
53
- :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response
54
- ]
55
-
56
- if invalid_options.size > 0
57
- raise ArgumentError.new("You passed the following invalid options to VCR::Cassette.new: #{invalid_options.inspect}.")
58
- end
59
-
60
- @name = name
61
- @record_mode = options[:record]
62
- @erb = options[:erb]
63
- @match_requests_on = options[:match_requests_on]
64
- @re_record_interval = options[:re_record_interval]
65
- @tags = Array(options.fetch(:tags) { options[:tag] })
66
- @tags << :update_content_length_header if options[:update_content_length_header]
67
- @tags << :preserve_exact_body_bytes if options[:preserve_exact_body_bytes]
68
- @tags << :decode_compressed_response if options[:decode_compressed_response]
69
- @allow_playback_repeats = options[:allow_playback_repeats]
70
- @exclusive = options[:exclusive]
71
- @serializer = VCR.cassette_serializers[options[:serialize_with]]
72
- @record_mode = :all if should_re_record?
73
- @parent_list = @exclusive ? HTTPInteractionList::NullList : VCR.http_interactions
46
+ @name = name
47
+ @options = VCR.configuration.default_cassette_options.merge(options)
74
48
 
49
+ assert_valid_options!
50
+ extract_options
75
51
  raise_error_unless_valid_record_mode
76
52
 
77
- log "Initialized with options: #{options.inspect}"
53
+ log "Initialized with options: #{@options.inspect}"
78
54
  end
79
55
 
80
56
  # Ejects the current cassette. The cassette will no longer be used.
@@ -82,6 +58,7 @@ module VCR
82
58
  # disk.
83
59
  def eject
84
60
  write_recorded_interactions_to_disk
61
+ http_interactions.assert_no_unused_interactions! unless @allow_unused_http_interactions
85
62
  end
86
63
 
87
64
  # @private
@@ -106,17 +83,21 @@ module VCR
106
83
  end
107
84
 
108
85
  # @return [String] The file for this cassette.
86
+ # @raise [NotImplementedError] if the configured cassette persister
87
+ # does not support resolving file paths.
109
88
  # @note VCR will take care of sanitizing the cassette name to make it a valid file name.
110
89
  def file
111
- return nil unless VCR.configuration.cassette_library_dir
112
- File.join(VCR.configuration.cassette_library_dir, "#{sanitized_name}.#{@serializer.file_extension}")
90
+ unless @persister.respond_to?(:absolute_path_to_file)
91
+ raise NotImplementedError, "The configured cassette persister does not support resolving file paths"
92
+ end
93
+ @persister.absolute_path_to_file(storage_key)
113
94
  end
114
95
 
115
96
  # @return [Boolean] Whether or not the cassette is recording.
116
97
  def recording?
117
98
  case record_mode
118
99
  when :none; false
119
- when :once; file.nil? || !File.size?(file)
100
+ when :once; raw_cassette_bytes.to_s.empty?
120
101
  else true
121
102
  end
122
103
  end
@@ -131,8 +112,44 @@ module VCR
131
112
 
132
113
  private
133
114
 
115
+ def assert_valid_options!
116
+ invalid_options = @options.keys - [
117
+ :record, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
118
+ :update_content_length_header, :allow_playback_repeats, :allow_unused_http_interactions,
119
+ :exclusive, :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response,
120
+ :persist_with
121
+ ]
122
+
123
+ if invalid_options.size > 0
124
+ raise ArgumentError.new("You passed the following invalid options to VCR::Cassette.new: #{invalid_options.inspect}.")
125
+ end
126
+ end
127
+
128
+ def extract_options
129
+ [:erb, :match_requests_on, :re_record_interval,
130
+ :allow_playback_repeats, :allow_unused_http_interactions, :exclusive].each do |name|
131
+ instance_variable_set("@#{name}", @options[name])
132
+ end
133
+
134
+ assign_tags
135
+
136
+ @record_mode = @options[:record]
137
+ @serializer = VCR.cassette_serializers[@options[:serialize_with]]
138
+ @persister = VCR.cassette_persisters[@options[:persist_with]]
139
+ @record_mode = :all if should_re_record?
140
+ @parent_list = @exclusive ? HTTPInteractionList::NullList : VCR.http_interactions
141
+ end
142
+
143
+ def assign_tags
144
+ @tags = Array(@options.fetch(:tags) { @options[:tag] })
145
+
146
+ [:update_content_length_header, :preserve_exact_body_bytes, :decode_compressed_response].each do |tag|
147
+ @tags << tag if @options[tag]
148
+ end
149
+ end
150
+
134
151
  def previously_recorded_interactions
135
- @previously_recorded_interactions ||= if file && File.size?(file)
152
+ @previously_recorded_interactions ||= if !raw_cassette_bytes.to_s.empty?
136
153
  deserialized_hash['http_interactions'].map { |h| HTTPInteraction.from_hash(h) }.tap do |interactions|
137
154
  invoke_hook(:before_playback, interactions)
138
155
 
@@ -145,8 +162,8 @@ module VCR
145
162
  end
146
163
  end
147
164
 
148
- def sanitized_name
149
- name.to_s.gsub(/[^\w\-\/]+/, '_')
165
+ def storage_key
166
+ @storage_key ||= [name, @serializer.file_extension].join('.')
150
167
  end
151
168
 
152
169
  def raise_error_unless_valid_record_mode
@@ -159,7 +176,6 @@ module VCR
159
176
  return false unless @re_record_interval
160
177
  previously_recorded_at = earliest_interaction_recorded_at
161
178
  return false unless previously_recorded_at
162
- return false unless File.exist?(file)
163
179
 
164
180
  now = Time.now
165
181
 
@@ -189,8 +205,8 @@ module VCR
189
205
  record_mode == :all
190
206
  end
191
207
 
192
- def raw_yaml_content
193
- VCR::Cassette::Reader.new(file, erb).read
208
+ def raw_cassette_bytes
209
+ @raw_cassette_bytes ||= VCR::Cassette::ERBRenderer.new(@persister[storage_key], erb, name).render
194
210
  end
195
211
 
196
212
  def merged_interactions
@@ -213,14 +229,11 @@ module VCR
213
229
  end
214
230
 
215
231
  def write_recorded_interactions_to_disk
216
- return unless VCR.configuration.cassette_library_dir
217
232
  return if new_recorded_interactions.none?
218
233
  hash = serializable_hash
219
234
  return if hash["http_interactions"].none?
220
235
 
221
- directory = File.dirname(file)
222
- FileUtils.mkdir_p directory unless File.exist?(directory)
223
- File.open(file, 'w') { |f| f.write @serializer.serialize(hash) }
236
+ @persister[storage_key] = @serializer.serialize(hash)
224
237
  end
225
238
 
226
239
  def invoke_hook(type, interactions)
@@ -232,7 +245,7 @@ module VCR
232
245
  end
233
246
 
234
247
  def deserialized_hash
235
- @deserialized_hash ||= @serializer.deserialize(raw_yaml_content).tap do |hash|
248
+ @deserialized_hash ||= @serializer.deserialize(raw_cassette_bytes).tap do |hash|
236
249
  unless hash.is_a?(Hash) && hash['http_interactions'].is_a?(Array)
237
250
  raise Errors::InvalidCassetteFormatError.new \
238
251
  "#{file} does not appear to be a valid VCR 2.0 cassette. " +
@@ -1,13 +1,15 @@
1
+ require 'erb'
2
+
1
3
  module VCR
2
4
  class Cassette
3
5
  # @private
4
- class Reader
5
- def initialize(file_name, erb)
6
- @file_name, @erb = file_name, erb
6
+ class ERBRenderer
7
+ def initialize(raw_template, erb, cassette_name=nil)
8
+ @raw_template, @erb, @cassette_name = raw_template, erb, cassette_name
7
9
  end
8
10
 
9
- def read
10
- return file_content unless use_erb?
11
+ def render
12
+ return @raw_template if @raw_template.nil? || !use_erb?
11
13
  binding = binding_for_variables if erb_variables
12
14
  template.result(binding)
13
15
  rescue NameError => e
@@ -20,7 +22,7 @@ module VCR
20
22
  example_hash = (erb_variables || {}).merge(e.name => 'some value')
21
23
 
22
24
  raise Errors::MissingERBVariableError.new(
23
- "The ERB in the #{@file_name} cassette file references undefined variable #{e.name}. " +
25
+ "The ERB in the #{@cassette_name} cassette file references undefined variable #{e.name}. " +
24
26
  "Pass it to the cassette using :erb => #{ example_hash.inspect }."
25
27
  )
26
28
  end
@@ -33,12 +35,8 @@ module VCR
33
35
  @erb if @erb.is_a?(Hash)
34
36
  end
35
37
 
36
- def file_content
37
- @file_content ||= File.read(@file_name)
38
- end
39
-
40
38
  def template
41
- @template ||= ERB.new(file_content)
39
+ @template ||= ERB.new(@raw_template)
42
40
  end
43
41
 
44
42
  @@struct_cache = Hash.new do |hash, attributes|
@@ -54,8 +54,26 @@ module VCR
54
54
  @interactions.size
55
55
  end
56
56
 
57
+ # Checks if there are no unused interactions left.
58
+ #
59
+ # @raise [VCR::Errors::UnusedHTTPInteractionError] if not all interactions were played back.
60
+ def assert_no_unused_interactions!
61
+ return unless has_unused_interactions?
62
+
63
+ descriptions = @interactions.map do |i|
64
+ " - #{request_summary(i.request)} => #{response_summary(i.response)}"
65
+ end.join("\n")
66
+
67
+ raise Errors::UnusedHTTPInteractionError, "There are unused HTTP interactions left in the cassette:\n#{descriptions}"
68
+ end
69
+
57
70
  private
58
71
 
72
+ # @return [Boolean] Whether or not there are unused interactions left in the list.
73
+ def has_unused_interactions?
74
+ @interactions.size > 0
75
+ end
76
+
59
77
  def request_summary(request)
60
78
  super(request, @request_matchers)
61
79
  end
@@ -0,0 +1,42 @@
1
+ module VCR
2
+ class Cassette
3
+ # Keeps track of the cassette persisters in a hash-like object.
4
+ class Persisters
5
+ autoload :FileSystem, 'vcr/cassette/persisters/file_system'
6
+
7
+ # @private
8
+ def initialize
9
+ @persisters = {}
10
+ end
11
+
12
+ # Gets the named persister.
13
+ #
14
+ # @param name [Symbol] the name of the persister
15
+ # @return the named persister
16
+ # @raise [ArgumentError] if there is not a persister for the given name
17
+ def [](name)
18
+ @persisters.fetch(name) do |_|
19
+ @persisters[name] = case name
20
+ when :file_system then FileSystem
21
+ else raise ArgumentError, "The requested VCR cassette persister " +
22
+ "(#{name.inspect}) is not registered."
23
+ end
24
+ end
25
+ end
26
+
27
+ # Registers a persister.
28
+ #
29
+ # @param name [Symbol] the name of the persister
30
+ # @param value [#[], #[]=] the persister object. It must implement `[]` and `[]=`.
31
+ def []=(name, value)
32
+ if @persisters.has_key?(name)
33
+ warn "WARNING: There is already a VCR cassette persister " +
34
+ "registered for #{name.inspect}. Overriding it."
35
+ end
36
+
37
+ @persisters[name] = value
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,60 @@
1
+ require 'fileutils'
2
+
3
+ module VCR
4
+ class Cassette
5
+ class Persisters
6
+ # The only built-in cassette persister. Persists cassettes to the file system.
7
+ module FileSystem
8
+ extend self
9
+
10
+ # @private
11
+ attr_reader :storage_location
12
+
13
+ # @private
14
+ def storage_location=(dir)
15
+ FileUtils.mkdir_p(dir) if dir
16
+ @storage_location = dir ? absolute_path_for(dir) : nil
17
+ end
18
+
19
+ # Gets the cassette for the given storage key (file name).
20
+ #
21
+ # @param [String] file_name the file name
22
+ # @return [String] the cassette content
23
+ def [](file_name)
24
+ path = absolute_path_to_file(file_name)
25
+ return nil unless File.exist?(path)
26
+ File.read(path)
27
+ end
28
+
29
+ # Sets the cassette for the given storage key (file name).
30
+ #
31
+ # @param [String] file_name the file name
32
+ # @param [String] content the content to store
33
+ def []=(file_name, content)
34
+ path = absolute_path_to_file(file_name)
35
+ directory = File.dirname(path)
36
+ FileUtils.mkdir_p(directory) unless File.exist?(directory)
37
+ File.open(path, 'w') { |f| f.write(content) }
38
+ end
39
+
40
+ # @private
41
+ def absolute_path_to_file(file_name)
42
+ return nil unless storage_location
43
+ File.join(storage_location, sanitized_file_name_from(file_name))
44
+ end
45
+
46
+ private
47
+
48
+ def absolute_path_for(path)
49
+ Dir.chdir(path) { Dir.pwd }
50
+ end
51
+
52
+ def sanitized_file_name_from(file_name)
53
+ parts = file_name.to_s.split('.')
54
+ file_extension = '.' + parts.pop if parts.size > 1
55
+ parts.join('.').gsub(/[^\w\-\/]+/, '_') + file_extension.to_s
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -33,7 +33,7 @@ module VCR
33
33
  #
34
34
  # @param name [Symbol] the name of the serializer
35
35
  # @param value [#file_extension, #serialize, #deserialize] the serializer object. It must implement
36
- # +file_extension()+, +serialize(Hash)+ and +deserialize(String)+.
36
+ # `file_extension()`, `serialize(Hash)` and `deserialize(String)`.
37
37
  def []=(name, value)
38
38
  if @serializers.has_key?(name)
39
39
  warn "WARNING: There is already a VCR cassette serializer registered for #{name.inspect}. Overriding it."
@@ -36,7 +36,7 @@ module VCR
36
36
  # Deserializes the given string using `MultiJson`.
37
37
  #
38
38
  # @param [String] string the JSON string
39
- # @param [Hash] hash the deserialized object
39
+ # @return [Hash] the deserialized object
40
40
  def deserialize(string)
41
41
  handle_encoding_errors do
42
42
  MultiJson.decode(string)
@@ -35,7 +35,7 @@ module VCR
35
35
  # Deserializes the given string using Psych.
36
36
  #
37
37
  # @param [String] string the YAML string
38
- # @param [Hash] hash the deserialized object
38
+ # @return [Hash] the deserialized object
39
39
  def deserialize(string)
40
40
  handle_encoding_errors do
41
41
  ::Psych.load(string)
@@ -35,7 +35,7 @@ module VCR
35
35
  # Deserializes the given string using Syck.
36
36
  #
37
37
  # @param [String] string the YAML string
38
- # @param [Hash] hash the deserialized object
38
+ # @return [Hash] the deserialized object
39
39
  def deserialize(string)
40
40
  handle_encoding_errors do
41
41
  using_syck { ::YAML.load(string) }
@@ -37,7 +37,7 @@ module VCR
37
37
  # Deserializes the given string using YAML.
38
38
  #
39
39
  # @param [String] string the YAML string
40
- # @param [Hash] hash the deserialized object
40
+ # @return [Hash] the deserialized object
41
41
  def deserialize(string)
42
42
  handle_encoding_errors do
43
43
  ::YAML.load(string)