diff --git a/README.md b/README.md index a53cfb3..480a988 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/integration/test_config.py b/tests/integration/test_config.py index 2d70968..898b0f7 100644 --- a/tests/integration/test_config.py +++ b/tests/integration/test_config.py @@ -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 diff --git a/tests/integration/test_multiple.py b/tests/integration/test_multiple.py new file mode 100644 index 0000000..7871585 --- /dev/null +++ b/tests/integration/test_multiple.py @@ -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') diff --git a/tests/integration/test_record_mode.py b/tests/integration/test_record_mode.py index f38c9bb..a3d69f1 100644 --- a/tests/integration/test_record_mode.py +++ b/tests/integration/test_record_mode.py @@ -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. diff --git a/tests/integration/test_register_matcher.py b/tests/integration/test_register_matcher.py index 8bd11b4..573dbf3 100644 --- a/tests/integration/test_register_matcher.py +++ b/tests/integration/test_register_matcher.py @@ -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): diff --git a/tests/integration/test_register_serializer.py b/tests/integration/test_register_serializer.py index f6d0320..b3123ae 100644 --- a/tests/integration/test_register_serializer.py +++ b/tests/integration/test_register_serializer.py @@ -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 diff --git a/tests/integration/test_requests.py b/tests/integration/test_requests.py index b9758b3..e399098 100644 --- a/tests/integration/test_requests.py +++ b/tests/integration/test_requests.py @@ -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 diff --git a/tests/integration/test_urllib2.py b/tests/integration/test_urllib2.py index 566c5bb..d0e66c0 100644 --- a/tests/integration/test_urllib2.py +++ b/tests/integration/test_urllib2.py @@ -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): diff --git a/tests/unit/test_cassettes.py b/tests/unit/test_cassettes.py index df2e755..bc95219 100644 --- a/tests/unit/test_cassettes.py +++ b/tests/unit/test_cassettes.py @@ -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') diff --git a/vcr/cassette.py b/vcr/cassette.py index b6d1bd1..c4e711b 100644 --- a/vcr/cassette.py +++ b/vcr/cassette.py @@ -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 diff --git a/vcr/stubs/__init__.py b/vcr/stubs/__init__.py index 5132358..eec9bc2 100644 --- a/vcr/stubs/__init__.py +++ b/vcr/stubs/__init__.py @@ -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: