1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Add support for configurable record modes

This support will let you select one of four different behaviors
for VCR's cassettes.  Closes #23
This commit is contained in:
Kevin McCarthy
2013-09-15 16:44:50 -10:00
parent 5ce67dc023
commit 03c22d79dd
7 changed files with 169 additions and 6 deletions

View File

@@ -52,6 +52,7 @@ import vcr
my_vcr = vcr.VCR(
serializer = 'json',
cassette_library_dir = 'fixtures/cassettes',
record_mode = 'once',
)
with my_vcr.use_cassette('test.json'):
@@ -61,12 +62,53 @@ with my_vcr.use_cassette('test.json'):
Otherwise, you can override options each time you use a cassette.
```python
with vcr.use_cassette('test.yml', serializer='json'):
with vcr.use_cassette('test.yml', serializer='json', record_mode='once'):
# your http code here
```
Note: Per-cassette overrides take precedence over the global config.
## Record Modes
VCR supports 4 record modes (with the same behavior as Ruby's VCR):
### once
* Replay previously recorded interactions.
* Record new interactions if there is no cassette file.
* Cause an error to be raised for new requests if there is a cassette file.
It is similar to the :new_episodes record mode, but will prevent new,
unexpected requests from being made (i.e. because the request URI
changed).
once is the default record mode, used when you do not set one.
### new_episodes
* Record new interactions.
* Replay previously recorded interactions.
It is similar to the once record mode, but will always record new
interactions, even if you have an existing recorded one that is similar,
but not identical.
This was the default behavior in versions < 0.3.0
### none
* Replay previously recorded interactions.
* Cause an error to be raised for any new requests.
This is useful when your code makes potentially dangerous
HTTP requests. The none record mode guarantees that no
new HTTP requests will be made.
### all
* Record new interactions.
* Never replay previously recorded interactions.
This can be temporarily used to force VCR to re-record
a cassette (i.e. to ensure the responses are not out of date)
or can be used when you simply want to log all HTTP requests.
## Advanced Features
If you want, VCR.py can return information about the cassette it is
@@ -159,6 +201,16 @@ This library is a work in progress, so the API might change on you.
There are probably some [bugs](https://github.com/kevin1024/vcrpy/issues?labels=bug&page=1&state=open) floating around too.
##Changelog
* 0.3.0: *Backwards incompatible release* - Added support for record
modes, and changed the default recording behavior to the "once" record
mode. Please see the documentation on record modes for more. Also,
improved the httplib mocking to add support for the `HTTPConnection.send()`
method. This means that requests won't actually be sent until the
response is read, since I need to record the entire request in order
to match up the appropriate response. I don't think this should cause
any issues unless you are sending requests without ever loading the
response (which none of the standard httplib wrappers do, as far as I
know.
* 0.2.1: Fixed missing modules in setup.py
* 0.2.0: Added configuration API, which lets you configure some settings
on VCR (see the README). Also, VCR no longer saves cassettes if they

View File

@@ -19,7 +19,7 @@ class PyTest(TestCommand):
sys.exit(errno)
setup(name='vcrpy',
version='0.2.1',
version='0.3.0',
description="A Python port of Ruby's VCR to make mocking HTTP easier",
author='Kevin McCarthy',
author_email='me@kevinmccarthy.org',

View File

@@ -45,7 +45,7 @@ def test_disk_saver_write(tmpdir):
# the mtime doesn't change
time.sleep(1)
with vcr.use_cassette(fname) as cass:
with vcr.use_cassette(fname, record_mode='any') as cass:
urllib2.urlopen('http://www.iana.org/domains/reserved').read()
urllib2.urlopen('http://httpbin.org/').read()
assert cass.play_count == 1

View File

@@ -0,0 +1,86 @@
import os
import urllib2
import pytest
import vcr
def test_once_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="once") as cass:
# make the same request again
response = urllib2.urlopen('http://httpbin.org/').read()
# the first time, it's played from the cassette.
# but, try to access something else from the same cassette, and an
# exception is raised.
with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/get').read()
def test_new_episodes_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="new_episodes"):
# cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="new_episodes") as cass:
# make the same request again
response = urllib2.urlopen('http://httpbin.org/').read()
# in the "new_episodes" record mode, we can add more requests to
# a cassette without repurcussions.
response = urllib2.urlopen('http://httpbin.org/get').read()
# the first interaction was not re-recorded, but the second was added
assert cass.play_count == 1
def test_all_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
# cassette file doesn't exist, so create.
response = urllib2.urlopen('http://httpbin.org/').read()
with vcr.use_cassette(testfile, record_mode="all") as cass:
# make the same request again
response = urllib2.urlopen('http://httpbin.org/').read()
# in the "all" record mode, we can add more requests to
# a cassette without repurcussions.
response = urllib2.urlopen('http://httpbin.org/get').read()
# The cassette was never actually played, even though it existed.
# that's because, in "all" mode, the requests all go directly to
# the source and bypass the cassette.
assert cass.play_count == 0
def test_none_record_mode(tmpdir):
# Cassette file doesn't exist, yet we are trying to make a request.
# raise hell.
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="none"):
with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/').read()
def test_none_record_mode_with_existing_cassette(tmpdir):
# create a cassette file
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="all"):
response = urllib2.urlopen('http://httpbin.org/').read()
# play from cassette file
with vcr.use_cassette(testfile, record_mode="none") as cass:
response = urllib2.urlopen('http://httpbin.org/').read()
assert cass.play_count == 1
# but if I try to hit the net, raise an exception.
with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/get').read()

View File

@@ -14,6 +14,7 @@ from .serializers import yamlserializer
class Cassette(object):
'''A container for recorded requests and responses'''
@classmethod
def load(cls, path, **kwargs):
'''Load in the cassette stored at the provided path'''
@@ -21,12 +22,13 @@ class Cassette(object):
new_cassette._load()
return new_cassette
def __init__(self, path, serializer=yamlserializer):
def __init__(self, path, serializer=yamlserializer, record_mode='once'):
self._path = path
self._serializer = serializer
self.data = OrderedDict()
self.play_counts = Counter()
self.dirty = False
self.record_mode = record_mode
@property
def play_count(self):
@@ -40,6 +42,20 @@ class Cassette(object):
def responses(self):
return self.data.values()
@property
def rewound(self):
"""
If the cassette has already been recorded in another session, and has
been loaded again fresh from disk, it has been "rewound". This means
that it should be write-only, depending on the record mode specified
"""
return not self.dirty and self.play_count
@property
def write_protected(self):
return self.rewound and self.record_mode == 'once' or \
self.record_mode == 'none'
def mark_played(self, request):
'''
Alert the cassette of a request that's been played

View File

@@ -4,13 +4,17 @@ from .serializers import yamlserializer, jsonserializer
class VCR(object):
def __init__(self, serializer='yaml', cassette_library_dir=None):
def __init__(self,
serializer='yaml',
cassette_library_dir=None,
record_mode="once"):
self.serializer = serializer
self.cassette_library_dir = cassette_library_dir
self.serializers = {
'yaml': yamlserializer,
'json': jsonserializer,
}
self.record_mode = record_mode
def _get_serializer(self, serializer_name):
try:
@@ -34,6 +38,7 @@ class VCR(object):
merged_config = {
"serializer": self._get_serializer(serializer_name),
"record_mode": kwargs.get('record_mode', self.record_mode),
}
return Cassette.load(path, **merged_config)

View File

@@ -142,13 +142,17 @@ class VCRConnectionMixin:
'''Retrieve a the response'''
# Check to see if the cassette has a response for this request. If so,
# then return it
if self._vcr_request in self.cassette:
if self._vcr_request in self.cassette and \
self.cassette.record_mode != "all":
response = self.cassette.response_of(self._vcr_request)
# Alert the cassette to the fact that we've served another
# response for the provided requests
self.cassette.mark_played(self._vcr_request)
return VCRHTTPResponse(response)
else:
if self.cassette.write_protected:
raise Exception("cassette is write protected")
# Otherwise, we should send the request, then get the response
# and return it.