kevin1024 / vcrpy

Automatically mock your HTTP interactions to simplify and speed up testing
MIT License
2.64k stars 376 forks source link

Confusing error message if play count mismatches #516

Open spookylukey opened 4 years ago

spookylukey commented 4 years ago

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.

ilyakamens commented 4 years ago

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.

mmyyrroonn commented 4 years ago

I met this issue too. Is there any way to fix it?

Juliehzl commented 4 years ago

the same issue to me

iluxonchik commented 3 years ago

same issue. fixed by adding the second request manually

MauriceBenink commented 3 years ago

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.

shacker commented 3 years ago

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?

shacker commented 3 years ago

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.

roharvey commented 3 years ago

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/

shacker commented 3 years ago

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 :(

philvarner commented 1 week ago

I just encountered this when updating a dependency that now makes slightly different requests. I also found that I had to run pytest --vcr-record=new_episodes (using with pytest-vcr) multiple times to get it to actually record all of the several new instances of the same URL requested.