vcrpy
vcrpy copied to clipboard
Confusing error message if play count mismatches
I'm getting error messages like this:
vcr.errors.CannotOverwriteExistingCassetteException: Can't overwrite existing cassette (....yaml') in your current record mode ('once').
No match for the request (<Request (GET) https://example.com/foo>) was found.
Found 2 similar requests with 0 different matcher(s) :
1 - (<Request (GET) https://example.com/foo>).
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', 'query']
Matchers failed :
2 - (<Request (GET) https://example.com/foo>).
Matchers succeeded : ['method', 'scheme', 'host', 'port', 'path', 'query']
Matchers failed :
Seeing as no matches failed, why did it fail to match? Digging in and debugging, the issue is that the play_counts
check in Cassette.__contains__
fails. It would be helpful if this was added to the error message somehow.
This looks like a regression of #81 but that was a long time ago and the error message is quite different now.
Same thing happened to me. When I recorded, vcrpy
only seemed to record the request/response once, even though it was happening twice in my test (in a row, with the exact same request parameters). I manually copied the request/response in the cassette and appended it to the cassette, which fixed the issue.
I met this issue too. Is there any way to fix it?
the same issue to me
same issue. fixed by adding the second request manually
I also ran into this issue today.
seems like allow_playback_repeats=true
can fix it if you dont have situations where you have identical requests with different responses
However i use pytest-vcr. this means i cannot set allow_playback_repeats
.
and we also have situations where we have to send identical requests which return different responses.
But we also have situations that do the same request multiple times but give the same response.
For this i wrote a hacky fix/patch to fix it for me.
from vcr.cassette import Cassette, CassetteContextDecorator
from vcr.errors import UnhandledHTTPRequestError
from vcr.matchers import requests_match
class VCRRepeatPlayback:
# This class is a hacky way to turn a dict into a class
def __init__(self, **kwargs):
for k,v in kwargs.items():
setattr(self, k, v)
class PatchedCassette(Cassette):
"""
Array which contains all the matchers which got configured, if these match then
allow this specific request to be replayed
"""
allow_playback_repeats_matches = [
VCRRepeatPlayback(method='GET', path='/foo/bar', query=[], any_other_matcher_you_configured='foo'),
]
def __init__(self, *args, playback_repeats_on_match=None, **kwargs):
self.playback_repeats_on_match = playback_repeats_on_match or dict()
super().__init__(*args, **kwargs)
def _load(self):
super()._load()
self._populate_playback_repeats_on_match()
def _populate_playback_repeats_on_match(self):
"""
Initial population for the cassette that gets loaded.
Will check every cassette entry request, if it matches with any self.allow_playback_repeats_matches
Then it will be treated as if self.allow_playback_repeats=True for this request only
:return:
"""
# Prevent executing this multiple times
if len(self.playback_repeats_on_match) > 0:
return
for allow_playback_match in self.allow_playback_repeats_matches:
for index, (stored_request, response) in enumerate(self.data):
# Prevent overwriting entry which was already matched before
if self.playback_repeats_on_match.get(index, False):
continue
# This check is very hacky, requests_match techinally expects a request object
# However we just feed it a class which copies its attributes from a dict
self.playback_repeats_on_match[index] = requests_match(
allow_playback_match, stored_request, self._match_on
)
def play_response(self, request):
"""
Get the response corresponding to a request, but only if it
hasn't been played back before, and mark it as played
"""
for index, response in self._responses(request):
# Added allow_playback will replay if it is marked as allowed to replay
if self.play_counts[index] == 0 or self.allow_playback_repeats or self.playback_repeats_on_match[index]:
self.play_counts[index] += 1
return response
# The cassette doesn't contain the request asked for.
raise UnhandledHTTPRequestError(
"The cassette (%r) doesn't contain the request (%r) asked for" % (self._path, request)
)
def __contains__(self, request):
"""Return whether or not a request has been stored"""
for index, response in self._responses(request):
# Added allow_playback will replay if it is marked as allowed to replay
if self.play_counts[index] == 0 or self.allow_playback_repeats or self.playback_repeats_on_match[index]:
return True
return False
# This changes which class gets used as the Cassette class.
vcr_allow_playback_repeats_class = PatchedCassette
def apply_patch():
"""
patch methods/functions where Cassette class gets applied by vcr.
together with the new methods which replace the methods which get patched
:return:
"""
@classmethod
def patch_replace_class_use(cls, **kwargs):
return CassetteContextDecorator.from_args(vcr_allow_playback_repeats_class or cls, **kwargs)
@classmethod
def patch_replace_class_use_arg_getter(cls, arg_getter):
return CassetteContextDecorator(vcr_allow_playback_repeats_class or cls, arg_getter)
Cassette.use = patch_replace_class_use
Cassette.use_arg_getter = patch_replace_class_use_arg_getter
Cant guarantee it fixes it and might be version specific (we use 4.1.1) However this did fix it for me.
Wrestling with the same issue here, in a cassette recorded with five requests. Not fixed with allow_playback_repeats=True
. Willing to manually repeat a section of the cassette to get past this, but I can't see how to figure out which section needs to be repeated (and don't want to manually modify the cassette each time). Since a lot of comments here are recent, wondering if downgrading VCR might help? But to which version?
Ah! I think I've found and fixed the reason for the non-match issue I was referring to above. We have some code that uses python futures
to do multi-threaded operations. That meant the order of operations was non-deterministic, which resulted in non-deterministic ordering in VCR cassettes.
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
...
I changed the 10
to settings.CONCURRENT_MAX_WORKERS
and then made that 10 in main settings and 1 in the test settings. This way the test runner records the requests in the cassette in a deterministic order. Seems to have fixed my problem for now.
Re: recent comments, it looks like you're using the default record mode of "once" here, rather than "none" for circleci/test runners? I think that could explain why it is failing in concurrent environments but not locally. There isn't much doc on it (like why it would be a problem), but I did find the configuration section suggests you override that for CI: https://pytest-vcr.readthedocs.io/en/latest/configuration/
I think the attempt it's making to re-record is a symptom, not a cause. So yes I could set record-mode to None in the test runner, but that wouldn't solve the underlying problem - it thinks it needs to re-record because it couldn't find a match, and it couldn't find a match because the parallelism is causing it to sometimes try to play back the cassette in an order that doesn't match the recording order. There might be a different fix for that problem that would allow me to not disable the parallelism in tests, but I'm not sure what it is :(