Skip to content

Commit

Permalink
Add drop_unused_requests option to exclude unused interactions (#946)
Browse files Browse the repository at this point in the history
Co-authored-by: Melissa Glickman <melissa@insrc.com>
Co-authored-by: Olle Jonsson <olle.jonsson@gmail.com>
  • Loading branch information
3 people committed Oct 18, 2022
1 parent 47a8094 commit 93ae14e
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 13 deletions.
87 changes: 87 additions & 0 deletions features/cassettes/drop_unused_requests.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
Feature: Drop Unused Requests

If set to true, this cassette option will cause VCR to drop any unused requests
from the cassette when a cassette is ejected. This is useful for reducing the
size of cassettes that contain a large number of requests that are not used.

The option defaults to false (mostly for backwards compatibility).

Background:
Given a file named "vcr_config.rb" with:
"""ruby
require 'vcr'
VCR.configure do |c|
c.hook_into :webmock
c.cassette_library_dir = 'cassettes'
end
"""
And a previously recorded cassette file "cassettes/example.yml" with:
"""
---
http_interactions:
- request:
method: get
uri: http://example.com/foo
body:
encoding: UTF-8
string: ""
headers: {}
response:
status:
code: 200
message: OK
headers:
Content-Length:
- "5"
body:
encoding: UTF-8
string: Existing Response
http_version: "1.1"
recorded_at: Tue, 01 Nov 2011 04:58:44 GMT
recorded_with: VCR 2.0.0
"""

Scenario: Unused requests are not dropped from the cassette by default
Given a file named "not_dropped_by_default.rb" with:
"""ruby
$server = start_sinatra_app do
get('/') { 'New Response' }
end
require 'vcr'
VCR.configure do |c|
c.hook_into :webmock
c.cassette_library_dir = 'cassettes'
end
VCR.use_cassette('example', :record => :all) do
puts Net::HTTP.get_response('localhost', '/', $server.port).body
end
"""
When I run `ruby not_dropped_by_default.rb`
Then the file "cassettes/example.yml" should contain "New Response"
And the file "cassettes/example.yml" should contain "Existing Response"

Scenario: Unused requests are dropped from the cassette when the option is set
Given a file named "drop_unused_requests_set.rb" with:
"""ruby
$server = start_sinatra_app do
get('/') { 'New Response' }
end
require 'vcr'
VCR.configure do |c|
c.hook_into :webmock
c.cassette_library_dir = 'cassettes'
end
VCR.use_cassette('example', :record => :all, :drop_unused_requests => true) do
puts Net::HTTP.get_response('localhost', '/', $server.port).body
end
"""
When I run `ruby drop_unused_requests_set.rb`
Then the file "cassettes/example.yml" should contain "New Response"
But the file "cassettes/example.yml" should not contain "Existing Response"
4 changes: 2 additions & 2 deletions features/configuration/debug_logging.feature
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Feature: Debug Logging
Given that port numbers in "record.log" are normalized to "7777"
Then the file "record.log" should contain exactly:
"""
[Cassette: 'example'] Initialized with options: {:record=>:once, :record_on_error=>true, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system, :persister_options=>{}}
[Cassette: 'example'] Initialized with options: {:record=>:once, :record_on_error=>true, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :drop_unused_requests=>false, :serialize_with=>:yaml, :persist_with=>:file_system, :persister_options=>{}}
[webmock] Handling request: [get http://localhost:7777/] (disabled: false)
[Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :host, :path] and 0 interaction(s): { }
[webmock] Identified request type (recordable) for [get http://localhost:7777/]
Expand All @@ -44,7 +44,7 @@ Feature: Debug Logging
Given that port numbers in "playback.log" are normalized to "7777"
Then the file "playback.log" should contain exactly:
"""
[Cassette: 'example'] Initialized with options: {:record=>:once, :record_on_error=>true, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system, :persister_options=>{}}
[Cassette: 'example'] Initialized with options: {:record=>:once, :record_on_error=>true, :match_requests_on=>[:method, :host, :path], :allow_unused_http_interactions=>true, :drop_unused_requests=>false, :serialize_with=>:yaml, :persist_with=>:file_system, :persister_options=>{}}
[webmock] Handling request: [get http://localhost:7777/] (disabled: false)
[Cassette: 'example'] Initialized HTTPInteractionList with request matchers [:method, :host, :path] and 1 interaction(s): { [get http://localhost:7777/] => [200 "Hello World"] }
[Cassette: 'example'] Checking if [get http://localhost:7777/] matches [get http://localhost:7777/] using [:method, :host, :path]
Expand Down
18 changes: 18 additions & 0 deletions features/configuration/default_cassette_options.feature
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ Feature: default_cassette_options
When I run `ruby default_record_on_error.rb`
Then the output should contain "Record on error: true"

Scenario: `:drop_unused_requests` defaults to `false` when it has not been set
Given a file named "default_drop_unused_requests.rb" with:
"""ruby
require 'vcr'
VCR.configure do |c|
# not important for this example, but must be set to something
c.hook_into :webmock
c.cassette_library_dir = 'cassettes'
end
VCR.use_cassette('example') do
puts "Drop unused requests: #{VCR.current_cassette.drop_unused_requests}"
end
"""
When I run `ruby default_drop_unused_requests.rb`
Then the output should contain "Drop unused requests: false"

Scenario: cassettes can set their own options
Given a file named "default_cassette_options.rb" with:
"""ruby
Expand Down
18 changes: 15 additions & 3 deletions lib/vcr/cassette.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class Cassette
# @return [Boolean, nil] Should outdated interactions be recorded back to file
attr_reader :clean_outdated_http_interactions

# @return [Boolean] Should unused requests be dropped from the cassette?
attr_reader :drop_unused_requests

# @return [Array<Symbol>] If set, {VCR::Configuration#before_record} and
# {VCR::Configuration#before_playback} hooks with a corresponding tag will apply.
attr_reader :tags
Expand Down Expand Up @@ -176,7 +179,8 @@ def assert_valid_options!
:record, :record_on_error, :erb, :match_requests_on, :re_record_interval, :tag, :tags,
:update_content_length_header, :allow_playback_repeats, :allow_unused_http_interactions,
:exclusive, :serialize_with, :preserve_exact_body_bytes, :decode_compressed_response,
:recompress_response, :persist_with, :persister_options, :clean_outdated_http_interactions
:recompress_response, :persist_with, :persister_options, :clean_outdated_http_interactions,
:drop_unused_requests
]

if invalid_options.size > 0
Expand All @@ -186,7 +190,7 @@ def assert_valid_options!

def extract_options
[:record_on_error, :erb, :match_requests_on, :re_record_interval, :clean_outdated_http_interactions,
:allow_playback_repeats, :allow_unused_http_interactions, :exclusive].each do |name|
:allow_playback_repeats, :allow_unused_http_interactions, :exclusive, :drop_unused_requests].each do |name|
instance_variable_set("@#{name}", @options[name])
end

Expand Down Expand Up @@ -259,6 +263,10 @@ def should_remove_matching_existing_interactions?
record_mode == :all
end

def should_remove_unused_interactions?
@drop_unused_requests
end

def should_assert_no_unused_interactions?
!(@allow_unused_http_interactions || $!)
end
Expand All @@ -277,7 +285,11 @@ def merged_interactions
end
end

up_to_date_interactions(old_interactions) + new_recorded_interactions
if should_remove_unused_interactions?
new_recorded_interactions
else
up_to_date_interactions(old_interactions) + new_recorded_interactions
end
end

def up_to_date_interactions(interactions)
Expand Down
1 change: 1 addition & 0 deletions lib/vcr/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ def initialize
:record_on_error => true,
:match_requests_on => RequestMatcherRegistry::DEFAULT_MATCHERS,
:allow_unused_http_interactions => true,
:drop_unused_requests => false,
:serialize_with => :yaml,
:persist_with => :file_system,
:persister_options => {}
Expand Down
33 changes: 25 additions & 8 deletions spec/lib/vcr/cassette_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,9 @@ def stub_old_interactions(interactions)

[:all, :none, :new_episodes].each do |record_mode|
context "for a :record => :#{record_mode} cassette with previously recorded interactions" do
subject { VCR::Cassette.new('example', :record => record_mode, :match_requests_on => [:uri]) }
let(:drop_unused_requests) { false }

subject { VCR::Cassette.new('example', :record => record_mode, :match_requests_on => [:uri], drop_unused_requests: drop_unused_requests) }

before(:each) do
base_dir = "#{VCR::SPEC_ROOT}/fixtures/cassette_spec"
Expand All @@ -636,31 +638,46 @@ def interaction(response_body, request_attributes)

let(:interaction_foo_1) { interaction("foo 1", :uri => 'http://foo.com/') }
let(:interaction_foo_2) { interaction("foo 2", :uri => 'http://foo.com/') }
let(:unused_request_foo) { interaction("unused foo", :uri => 'http://foo_unused.com/') }
let(:interaction_bar) { interaction("bar", :uri => 'http://bar.com/') }

let(:saved_recorded_interactions) { YAML.load_file(subject.file)['http_interactions'].map { |h| VCR::HTTPInteraction.from_hash(h) } }
let(:now) { Time.utc(2011, 6, 11, 12, 30) }

before(:each) do
allow(Time).to receive(:now).and_return(now)
allow(subject).to receive(:previously_recorded_interactions).and_return([interaction_foo_1])
allow(subject).to receive(:previously_recorded_interactions).and_return([interaction_foo_1,unused_request_foo])
subject.record_http_interaction(interaction_foo_2)
subject.record_http_interaction(interaction_bar)
subject.eject
end

if record_mode == :all
it 'replaces previously recorded interactions with new ones when the requests match' do
expect(saved_recorded_interactions.first).to eq(interaction_foo_2)
expect(saved_recorded_interactions).not_to include(interaction_foo_1)
context "preserves old requests" do
it 'only replaces previously recorded interactions with new ones when the requests match' do
expect(saved_recorded_interactions).to include(interaction_bar)
expect(saved_recorded_interactions).to include(unused_request_foo)
expect(saved_recorded_interactions).to include(interaction_foo_2)
expect(saved_recorded_interactions).not_to include(interaction_foo_1)
end

it 'appends new recorded interactions that do not match existing ones' do
expect(saved_recorded_interactions.last).to eq(interaction_bar)
end
end

it 'appends new recorded interactions that do not match existing ones' do
expect(saved_recorded_interactions.last).to eq(interaction_bar)
context "with drop_unused_requests option added to all" do
let(:drop_unused_requests) { true }

it "drops unused requests" do
expect(saved_recorded_interactions).to match_array([interaction_bar, interaction_foo_2])
end
end


else
it 'appends new recorded interactions after existing ones' do
expect(saved_recorded_interactions).to eq([interaction_foo_1, interaction_foo_2, interaction_bar])
expect(saved_recorded_interactions).to eq([interaction_foo_1, unused_request_foo, interaction_foo_2, interaction_bar])
end
end
end
Expand Down
1 change: 1 addition & 0 deletions spec/lib/vcr/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
expect(subject.default_cassette_options).to eq({
:match_requests_on => VCR::RequestMatcherRegistry::DEFAULT_MATCHERS,
:allow_unused_http_interactions => true,
:drop_unused_requests => false,
:record => :once,
:record_on_error => true,
:serialize_with => :yaml,
Expand Down

0 comments on commit 93ae14e

Please sign in to comment.