Skip to content

Commit

Permalink
Add a rake task to migrate the cassettes from the 1.x format to the 2…
Browse files Browse the repository at this point in the history
….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
myronmarston committed Oct 27, 2011
1 parent d78941d commit 26c1a8a
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 5 deletions.
32 changes: 32 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

99 changes: 99 additions & 0 deletions lib/vcr/cassette/migrator.rb
Original file line number Diff line number Diff line change
@@ -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

32 changes: 27 additions & 5 deletions lib/vcr/structs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions lib/vcr/tasks/vcr.rake
Original file line number Diff line number Diff line change
@@ -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

166 changes: 166 additions & 0 deletions spec/vcr/cassette/migrator_spec.rb
Original file line number Diff line number Diff line change
@@ -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

Loading

0 comments on commit 26c1a8a

Please sign in to comment.