1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-08 16:53:23 +00:00

Record Multiple Matching Requests

This change allows us to record multiple matching requests to
the same URL, and then play them back sequentially.

Closes #40, #41
This commit is contained in:
Kevin McCarthy
2013-11-09 12:49:33 -10:00
parent d50ded68ca
commit 89403c255c
11 changed files with 92 additions and 120 deletions

View File

@@ -100,7 +100,7 @@ VCR supports 4 record modes (with the same behavior as Ruby's VCR):
* 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,
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).
@@ -162,8 +162,6 @@ part of the API. The fields are as follows:
* `responses`: A list of the responses made.
* `play_count`: The number of times this cassette has had a response
played back
* `play_counts`: A collections.Counter showing the number of times each
response has been played back, indexed by the request
* `response_of(request)`: Access the response for a given request.
The Request object has the following properties
@@ -259,6 +257,7 @@ 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.4.0: Change default request recording behavior for multiple requests. If you make the same request multiple times to the same URL, the response might be different each time (maybe the response has a timestamp in it or something), so this will make the same request multiple times and save them all. Then, when you are replaying the cassette, the responses will be played back in the same order in which they were received. If you were making multiple requests to the same URL in a cassette before version 0.4.0, you might need to regenerate your cassette files. Also, removes support for the cassette.play_count counter API, since individual requests aren't unique anymore. A cassette might contain the same request several times.
* 0.3.5: Fix compatibility with requests 2.x
* 0.3.4: Bugfix: close file before renaming it. This fixes an issue on Windows. Thanks @smallcode for the fix.
* 0.3.3: Bugfix for error message when an unreigstered custom matcher

View File

@@ -40,8 +40,10 @@ def test_override_set_cassette_library_dir(tmpdir):
def test_override_match_on(tmpdir):
my_vcr = vcr.VCR(match_on=['method'])
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
with my_vcr.use_cassette(str(tmpdir.join('test.json'))):
urllib2.urlopen('http://httpbin.org/')
with my_vcr.use_cassette(str(tmpdir.join('test.json'))) as cass:
urllib2.urlopen('http://httpbin.org/get')
assert len(cass) == 1

View File

@@ -0,0 +1,20 @@
import pytest
from urllib2 import urlopen
import vcr
def test_making_extra_request_raises_exception(tmpdir):
# make two requests in the first request that are considered
# identical (since the match is based on method)
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
urlopen('http://httpbin.org/status/200')
urlopen('http://httpbin.org/status/201')
# Now, try to make three requests. The first two should return the
# correct status codes in order, and the third should raise an
# exception.
with vcr.use_cassette(str(tmpdir.join('test.json')), match_on=['method']):
assert urlopen('http://httpbin.org/status/200').getcode() == 200
assert urlopen('http://httpbin.org/status/201').getcode() == 201
with pytest.raises(Exception):
urlopen('http://httpbin.org/status/200')

View File

@@ -20,10 +20,11 @@ def test_once_record_mode(tmpdir):
with pytest.raises(Exception):
response = urllib2.urlopen('http://httpbin.org/get').read()
def test_once_record_mode_two_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get two same files
# get two of the same file
response1 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.urlopen('http://httpbin.org/').read()
@@ -32,14 +33,16 @@ def test_once_record_mode_two_times(tmpdir):
response = urllib2.urlopen('http://httpbin.org/').read()
response = urllib2.urlopen('http://httpbin.org/').read()
def test_once_mode_three_times(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
with vcr.use_cassette(testfile, record_mode="once"):
# get three same files
# get three of the same file
response1 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.urlopen('http://httpbin.org/').read()
response2 = urllib2.urlopen('http://httpbin.org/').read()
def test_new_episodes_record_mode(tmpdir):
testfile = str(tmpdir.join('recordmode.yml'))
@@ -80,7 +83,6 @@ def test_all_record_mode(tmpdir):
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.

View File

@@ -18,7 +18,11 @@ def test_registered_serializer_true_matcher(tmpdir):
# These 2 different urls are stored as the same request
urllib2.urlopen('http://httpbin.org/')
urllib2.urlopen('https://httpbin.org/get')
assert len(cass) == 1
with my_vcr.use_cassette(testfile, match_on=['true']) as cass:
# I can get the response twice even though I only asked for it once
urllib2.urlopen('http://httpbin.org/get')
urllib2.urlopen('https://httpbin.org/get')
def test_registered_serializer_false_matcher(tmpdir):

View File

@@ -24,7 +24,6 @@ def test_registered_serializer(tmpdir):
my_vcr.register_serializer('mock', ms)
tmpdir.join('test.mock').write('test_data')
with my_vcr.use_cassette(str(tmpdir.join('test.mock')), serializer='mock'):
urllib2.urlopen('http://httpbin.org/')
# Serializer deserialized once
assert ms.serialize_count == 1
# and serialized the test data string

View File

@@ -25,33 +25,30 @@ def test_status_code(scheme, tmpdir):
'''Ensure that we can read the status code'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).status_code == requests.get(url).status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
status_code = requests.get(url).status_code
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert status_code == requests.get(url).status_code
def test_headers(scheme, tmpdir):
'''Ensure that we can read the headers back'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).headers == requests.get(url).headers
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
headers = requests.get(url).headers
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
assert headers == requests.get(url).headers
def test_body(tmpdir, scheme):
'''Ensure the responses are all identical enough'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert content == requests.get(url).content
def test_auth(tmpdir, scheme):
@@ -59,14 +56,12 @@ def test_auth(tmpdir, scheme):
auth = ('user', 'passwd')
url = scheme + '://httpbin.org/basic-auth/user/passwd'
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
one = requests.get(url, auth=auth)
with vcr.use_cassette(str(tmpdir.join('auth.yaml'))) as cass:
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_auth_failed(tmpdir, scheme):
@@ -80,8 +75,6 @@ def test_auth_failed(tmpdir, scheme):
two = requests.get(url, auth=auth)
assert one.content == two.content
assert one.status_code == two.status_code == 401
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_post(tmpdir, scheme):
@@ -89,22 +82,22 @@ def test_post(tmpdir, scheme):
data = {'key1': 'value1', 'key2': 'value2'}
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
req1 = requests.post(url, data).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
req2 = requests.post(url, data).content
assert req1 == req2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert req1 == req2
def test_redirects(tmpdir, scheme):
'''Ensure that we can handle redirects'''
url = scheme + '://httpbin.org/redirect-to?url=bytes/1024'
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert requests.get(url).content == requests.get(url).content
content = requests.get(url).content
with vcr.use_cassette(str(tmpdir.join('requests.yaml'))) as cass:
assert content == requests.get(url).content
# Ensure that we've now cached *two* responses. One for the redirect
# and one for the final fetch
assert len(cass) == 2

View File

@@ -25,52 +25,44 @@ def test_response_code(scheme, tmpdir):
'''Ensure we can read a response code from a fetch'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).getcode() == urllib2.urlopen(url).getcode()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
code = urllib2.urlopen(url).getcode()
with vcr.use_cassette(str(tmpdir.join('atts.yaml'))) as cass:
assert code == urllib2.urlopen(url).getcode()
def test_random_body(scheme, tmpdir):
'''Ensure we can read the content, and that it's served from cache'''
url = scheme + '://httpbin.org/bytes/1024'
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
body = urllib2.urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('body.yaml'))) as cass:
assert body == urllib2.urlopen(url).read()
def test_response_headers(scheme, tmpdir):
'''Ensure we can get information from the response'''
url = scheme + '://httpbin.org/'
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
open1 = urllib2.urlopen(url).info().items()
with vcr.use_cassette(str(tmpdir.join('headers.yaml'))) as cass:
open2 = urllib2.urlopen(url).info().items()
assert open1 == open2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
def test_multiple_requests(scheme, tmpdir):
'''Ensure that we can cache multiple requests'''
urls = [
scheme + '://httpbin.org/',
scheme + '://httpbin.org/',
scheme + '://httpbin.org/get',
scheme + '://httpbin.org/bytes/1024'
]
with vcr.use_cassette(str(tmpdir.join('multiple.yaml'))) as cass:
for index in range(len(urls)):
url = urls[index]
assert len(cass) == index
assert cass.play_count == index
assert urllib2.urlopen(url).read() == urllib2.urlopen(url).read()
assert len(cass) == index + 1
assert cass.play_count == index + 1
map(urllib2.urlopen, urls)
assert len(cass) == len(urls)
def test_get_data(scheme, tmpdir):
@@ -78,14 +70,12 @@ def test_get_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/get?' + data
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url).read()
with vcr.use_cassette(str(tmpdir.join('get_data.yaml'))) as cass:
res2 = urllib2.urlopen(url).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert len(cass) == 1
assert cass.play_count == 1
assert res1 == res2
def test_post_data(scheme, tmpdir):
@@ -93,13 +83,13 @@ def test_post_data(scheme, tmpdir):
data = urlencode({'some': 1, 'data': 'here'})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_post_unicode_data(scheme, tmpdir):
@@ -107,13 +97,11 @@ def test_post_unicode_data(scheme, tmpdir):
data = urlencode({'snowman': u''.encode('utf-8')})
url = scheme + '://httpbin.org/post'
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
# Ensure that this is empty to begin with
assert_cassette_empty(cass)
res1 = urllib2.urlopen(url, data).read()
with vcr.use_cassette(str(tmpdir.join('post_data.yaml'))) as cass:
res2 = urllib2.urlopen(url, data).read()
assert res1 == res2
# Ensure that we've now cached a single response
assert_cassette_has_one_response(cass)
assert res1 == res2
assert_cassette_has_one_response(cass)
def test_cross_scheme(tmpdir):

View File

@@ -18,21 +18,6 @@ def test_cassette_not_played():
assert not a.play_count
def test_cassette_played():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('foo')
assert a.play_count == 2
def test_cassette_play_counter():
a = Cassette('test')
a.mark_played('foo')
a.mark_played('bar')
assert a.play_counts['foo'] == 1
assert a.play_counts['bar'] == 1
def test_cassette_append():
a = Cassette('test')
a.append('foo', 'bar')

View File

@@ -36,6 +36,7 @@ class Cassette(object):
self.data = []
self.play_counts = Counter()
self.dirty = False
self.rewound = False
self.record_mode = record_mode
@property
@@ -50,26 +51,11 @@ class Cassette(object):
def responses(self):
return [response for (request, response) in self.data]
@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
'''
self.play_counts[request] += 1
def append(self, request, response):
'''Add a request, response pair to this cassette'''
self.data.append((request, response))
@@ -80,17 +66,14 @@ class Cassette(object):
Find the response corresponding to a request
'''
responses = []
for stored_request, response in self.data:
for index, (stored_request, response) in enumerate(self.data):
if requests_match(request, stored_request, self._match_on):
responses.append(response)
index = self.play_counts[request]
try:
return responses[index]
except IndexError:
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
if self.play_counts[index] == 0:
self.play_counts[index] += 1
return response
# I decided that a KeyError is the best exception to raise
# if the cassette doesn't contain the request asked for.
raise KeyError
def _as_dict(self):
return {"requests": self.requests, "responses": self.responses}
@@ -113,6 +96,7 @@ class Cassette(object):
for request, response in zip(requests, responses):
self.append(request, response)
self.dirty = False
self.rewound = True
except IOError:
pass

View File

@@ -161,12 +161,8 @@ 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 and \
self.cassette.record_mode != "all":
if self._vcr_request in self.cassette and self.cassette.record_mode != "all" and self.cassette.rewound:
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: