Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add a rake task to migrate the cassettes from the 1.x format to the 2…

….x format.

Also, normalize nil body to a blank string.

It's nice to always be able to treat the body as a string and not need to worry about a nil special case.
  • Loading branch information...
commit 26c1a8a1450a9612cd6a5cf8da5a88296f039109 1 parent d78941d
@myronmarston authored
View
32 Rakefile
@@ -94,3 +94,35 @@ task :release => [:require_ruby_18, :prep_relish_release, :relish]
# For gem-test: http://gem-testers.org/
task :test => :spec
+load './lib/vcr/tasks/vcr.rake'
+
+desc "Migrate cucumber cassettes"
+task :migrate_cucumber_cassettes do
+ require 'vcr/cassette/migrator'
+ Dir["features/**/*.feature"].each do |feature_file|
+ puts " - Migrating #{feature_file}"
+ contents = File.read(feature_file)
+
+ # http://rubular.com/r/gjzkoaYX2O
+ contents.scan(/:\n^\s+"""\n([\s\S]+?)"""/).each do |captures|
+ capture = captures.first
+ indentation = capture[/^ +/]
+ cassette_yml = capture.gsub(/^#{indentation}/, '')
+ new_yml = nil
+
+ Dir.mktmpdir do |dir|
+ file_name = "#{dir}/cassette.yml"
+ File.open(file_name, 'w') { |f| f.write(cassette_yml) }
+ VCR::Cassette::Migrator.new(dir, StringIO.new).migrate!
+ new_yml = File.read(file_name)
+ end
+
+ new_yml.gsub!(/^/, indentation)
+ new_yml << indentation
+ contents.gsub!(capture, new_yml)
+ end
+
+ File.open(feature_file, 'w') { |f| f.write(contents) }
+ end
+end
+
View
99 lib/vcr/cassette/migrator.rb
@@ -0,0 +1,99 @@
+require 'yaml'
+require 'vcr/structs'
+require 'uri'
+
+module VCR
+ class Cassette
+ class Migrator
+ def initialize(dir, out = $stdout)
+ @dir, @out = dir, out
+ end
+
+ def migrate!
+ @out.puts "Migrating VCR cassettes in #{@dir}..."
+ Dir["#{@dir}/**/*.yml"].each do |cassette|
+ migrate_cassette(cassette)
+ end
+ end
+
+ private
+
+ def migrate_cassette(cassette)
+ unless http_interactions = load_yaml(cassette)
+ @out.puts " - Ignored #{relative_casssette_name(cassette)} since it could not be parsed as YAML (does it have some ERB?)"
+ return
+ end
+
+ unless valid_vcr_1_cassette?(http_interactions)
+ @out.puts " - Ignored #{relative_casssette_name(cassette)} since it does not appear to be a valid VCR 1.x cassette"
+ return
+ end
+
+ http_interactions.map! do |interaction|
+ remove_unnecessary_standard_port(interaction)
+ denormalize_http_header_keys(interaction.request)
+ denormalize_http_header_keys(interaction.response)
+ normalize_body(interaction.request)
+ normalize_body(interaction.response)
+ interaction.to_hash
+ end
+
+ File.open(cassette, 'w') { |f| f.write ::YAML.dump(http_interactions) }
+ @out.puts " - Migrated #{relative_casssette_name(cassette)}"
+ end
+
+ def load_yaml(cassette)
+ ::YAML.load_file(cassette)
+ rescue *yaml_load_errors
+ return nil
+ end
+
+ def yaml_load_errors
+ [ArgumentError].tap do |errors|
+ errors << Psych::SyntaxError if defined?(Psych::SyntaxError)
+ end
+ end
+
+ def relative_casssette_name(cassette)
+ cassette.gsub(%r|\A#{Regexp.escape(@dir)}/?|, '')
+ end
+
+ def valid_vcr_1_cassette?(content)
+ content.is_a?(Array) &&
+ content.map(&:class).uniq == [HTTPInteraction]
+ end
+
+ def remove_unnecessary_standard_port(interaction)
+ uri = URI(interaction.request.uri)
+ if uri.scheme == 'http' && uri.port == 80 ||
+ uri.scheme == 'https' && uri.port == 443
+ uri.port = nil
+ interaction.request.uri = uri.to_s
+ end
+ rescue URI::InvalidURIError
+ # ignore this URI.
+ # This can occur when the user uses the filter_sensitive_data option
+ # to put a substitution string in their URI
+ end
+
+ def denormalize_http_header_keys(object)
+ object.headers = {}.tap do |denormalized|
+ object.headers.each do |k, v|
+ denormalized[denormalize_header_key(k)] = v
+ end if object.headers
+ end
+ end
+
+ def denormalize_header_key(key)
+ key.split('-'). # 'user-agent' => %w(user agent)
+ each { |w| w.capitalize! }. # => %w(User Agent)
+ join('-')
+ end
+
+ def normalize_body(object)
+ object.body = '' if object.body.nil?
+ end
+ end
+ end
+end
+
View
32 lib/vcr/structs.rb
@@ -10,7 +10,7 @@ def initialize(*args)
# or attributes, so that it is serialized to YAML as a raw string.
# This is needed for rest-client. See this ticket for more info:
# http://github.com/myronmarston/vcr/issues/4
- self.body = String.new(body) if body.is_a?(String)
+ self.body = String.new(body.to_s)
end
end
@@ -55,6 +55,24 @@ def convert_to_raw_strings(array)
end
end
+ module OrderedHashSerializer
+ def each
+ @ordered_keys.each do |key|
+ yield key, self[key]
+ end
+ end
+
+ if RUBY_VERSION =~ /1.9/
+ # 1.9 hashes are already ordered.
+ def self.apply_to(*args); end
+ else
+ def self.apply_to(hash, keys)
+ hash.instance_variable_set(:@ordered_keys, keys)
+ hash.extend self
+ end
+ end
+ end
+
class Request < Struct.new(:method, :uri, :body, :headers)
include Normalizers::Header
include Normalizers::Body
@@ -65,7 +83,7 @@ def to_hash
'uri' => uri,
'body' => body,
'headers' => headers
- }
+ }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
def self.from_hash(hash)
@@ -89,7 +107,9 @@ class HTTPInteraction < Struct.new(:request, :response)
def_delegators :request, :uri, :method
def to_hash
- { 'request' => request.to_hash, 'response' => response.to_hash }
+ { 'request' => request.to_hash, 'response' => response.to_hash }.tap do |hash|
+ OrderedHashSerializer.apply_to(hash, members)
+ end
end
def self.from_hash(hash)
@@ -152,7 +172,7 @@ def to_hash
'headers' => headers,
'body' => body,
'http_version' => http_version
- }
+ }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
def self.from_hash(hash)
@@ -172,7 +192,9 @@ def update_content_length_header
class ResponseStatus < Struct.new(:code, :message)
def to_hash
- { 'code' => code, 'message' => message }
+ {
+ 'code' => code, 'message' => message
+ }.tap { |h| OrderedHashSerializer.apply_to(h, members) }
end
def self.from_hash(hash)
View
9 lib/vcr/tasks/vcr.rake
@@ -0,0 +1,9 @@
+namespace :vcr do
+ desc "Migrate cassettes from the VCR 1.x format to the VCR 2.x format."
+ task :migrate_cassettes do
+ dir = ENV.fetch('DIR') { raise "You must pass the cassette library directory as DIR=<directory>" }
+ require 'vcr/cassette/migrator'
+ VCR::Cassette::Migrator.new(dir).migrate!
+ end
+end
+
View
166 spec/vcr/cassette/migrator_spec.rb
@@ -0,0 +1,166 @@
+require 'tmpdir'
+require 'vcr/cassette/migrator'
+
+describe VCR::Cassette::Migrator do
+ let(:original_contents) { <<-EOF
+---
+- !ruby/struct:VCR::HTTPInteraction
+ request: !ruby/struct:VCR::Request
+ method: :get
+ uri: http://example.com:80/foo
+ body:
+ headers:
+ response: !ruby/struct:VCR::Response
+ status: !ruby/struct:VCR::ResponseStatus
+ code: 200
+ message: OK
+ headers:
+ content-type:
+ - text/html;charset=utf-8
+ content-length:
+ - "9"
+ body: Hello foo
+ http_version: "1.1"
+- !ruby/struct:VCR::HTTPInteraction
+ request: !ruby/struct:VCR::Request
+ method: :get
+ uri: http://localhost:7777/bar
+ body:
+ headers:
+ response: !ruby/struct:VCR::Response
+ status: !ruby/struct:VCR::ResponseStatus
+ code: 200
+ message: OK
+ headers:
+ content-type:
+ - text/html;charset=utf-8
+ content-length:
+ - "9"
+ body: Hello bar
+ http_version: "1.1"
+EOF
+ }
+
+ let(:updated_contents) { <<-EOF
+---
+- request:
+ method: get
+ uri: http://example.com/foo
+ body: ""
+ headers: {}
+
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Content-Type:
+ - text/html;charset=utf-8
+ Content-Length:
+ - "9"
+ body: Hello foo
+ http_version: "1.1"
+- request:
+ method: get
+ uri: http://localhost:7777/bar
+ body: ""
+ headers: {}
+
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Content-Type:
+ - text/html;charset=utf-8
+ Content-Length:
+ - "9"
+ body: Hello bar
+ http_version: "1.1"
+EOF
+ }
+
+ attr_accessor :dir
+
+ around(:each) do |example|
+ Dir.mktmpdir do |dir|
+ self.dir = dir
+ example.run
+ end
+ end
+
+ # Use syck on all rubies for consistent results...
+ before(:each) do
+ YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
+ end
+
+ after(:each) do
+ YAML::ENGINE.yamler = 'psych' if defined?(YAML::ENGINE)
+ end
+
+ let(:out_io) { StringIO.new }
+ let(:file_name) { File.join(dir, "example.yml") }
+ let(:output) { out_io.rewind; out_io.read }
+
+ subject { described_class.new(dir, out_io) }
+
+ it 'migrates a cassette from the 1.x to 2.x format' do
+ File.open(file_name, 'w') { |f| f.write(original_contents) }
+ subject.migrate!
+ File.read(file_name).should eq(updated_contents)
+ output.should match(/Migrated example.yml/)
+ end
+
+ it 'ignores files that do not contain arrays' do
+ File.open(file_name, 'w') { |f| f.write(true.to_yaml) }
+ subject.migrate!
+ File.read(file_name).should eq(true.to_yaml)
+ output.should match(/Ignored example.yml since it does not appear to be a valid VCR 1.x cassette/)
+ end
+
+ it 'ignores files that contain YAML arrays of other things' do
+ File.open(file_name, 'w') { |f| f.write([{}, {}].to_yaml) }
+ subject.migrate!
+ File.read(file_name).should eq([{}, {}].to_yaml)
+ output.should match(/Ignored example.yml since it does not appear to be a valid VCR 1.x cassette/)
+ end
+
+ it 'ignores URIs that have sensitive data substitutions' do
+ modified_contents = original_contents.gsub('example.com', '<HOST>')
+ File.open(file_name, 'w') { |f| f.write(modified_contents) }
+ subject.migrate!
+ File.read(file_name).should eq(updated_contents.gsub('example.com', '<HOST>:80'))
+ end
+
+ it 'ignores files that are empty' do
+ File.open(file_name, 'w') { |f| f.write('') }
+ subject.migrate!
+ File.read(file_name).should eq('')
+ output.should match(/Ignored example.yml since it could not be parsed as YAML/)
+ end
+
+ shared_examples_for "ignoring invalid YAML" do
+ it 'ignores files that cannot be parsed as valid YAML (such as ERB cassettes)' do
+ modified_contents = original_contents.gsub(/\A---/, "---\n<% 3.times do %>")
+ modified_contents = modified_contents.gsub(/\z/, "<% end %>")
+ File.open(file_name, 'w') { |f| f.write(modified_contents) }
+ subject.migrate!
+ File.read(file_name).should eq(modified_contents)
+ output.should match(/Ignored example.yml since it could not be parsed as YAML/)
+ end
+ end
+
+ context 'with syck' do
+ it_behaves_like "ignoring invalid YAML"
+ end
+
+ context 'with psych' do
+ before(:each) do
+ pending "psych not available" unless defined?(YAML::ENGINE)
+ YAML::ENGINE.yamler = 'psych'
+ end
+
+ it_behaves_like "ignoring invalid YAML"
+ end
+end
+
View
17 spec/vcr/structs_spec.rb
@@ -39,6 +39,10 @@
body.instance_variable_set(:@foo, 7)
YAML.dump(instance(body).body).should eq(YAML.dump("My String"))
end
+
+ it 'converts nil to a blank string' do
+ instance(nil).body.should eq("")
+ end
end
module VCR
@@ -134,6 +138,19 @@ module VCR
'http_version' => '1.1'
})
end
+
+ def assert_yielded_keys(hash, *keys)
+ yielded_keys = []
+ hash.each { |k, v| yielded_keys << k }
+ yielded_keys.should eq(keys)
+ end
+
+ it 'yields the entries in the expected order so the hash can be serialized in that order' do
+ assert_yielded_keys hash, 'request', 'response'
+ assert_yielded_keys hash['request'], 'method', 'uri', 'body', 'headers'
+ assert_yielded_keys hash['response'], 'status', 'headers', 'body', 'http_version'
+ assert_yielded_keys hash['response']['status'], 'code', 'message'
+ end
end
describe '#filter!' do
Please sign in to comment.
Something went wrong with that request. Please try again.