diff --git a/tests/unit/test_cassettes.py b/tests/unit/test_cassettes.py index 413b154..3d358de 100644 --- a/tests/unit/test_cassettes.py +++ b/tests/unit/test_cassettes.py @@ -10,6 +10,7 @@ from vcr.compat import mock, contextlib from vcr.cassette import Cassette from vcr.errors import UnhandledHTTPRequestError from vcr.patch import force_reset +from vcr.matchers import path, method, query, host from vcr.stubs import VCRHTTPSConnection @@ -294,3 +295,18 @@ def test_use_as_decorator_on_generator(): assert httplib.HTTPConnection is not original_http_connetion yield 2 assert list(test_function()) == [1, 2] + + +def test_similar_requests(tmpdir): + # WIP needs to be finished + @Cassette.use(inject=True, match_on=(path, query, host, method)) + def test_function(cassette): + conn = httplib.HTTPConnection("www.python.org") + conn.request("GET", "/index.html?test=1") + + conn = httplib.HTTPConnection("www.python.org") + conn.request("GET", "/index.html?test=0") + + conn = httplib.HTTPConnection("www.cool.org") + conn.request("GET", "/index.html?test=0") + cassette.similar_requests() diff --git a/vcr/cassette.py b/vcr/cassette.py index db94224..79b62b6 100644 --- a/vcr/cassette.py +++ b/vcr/cassette.py @@ -1,6 +1,7 @@ -import sys import inspect import logging +import operator +import sys import wrapt @@ -145,9 +146,33 @@ class CassetteContextDecorator(object): return new_args_getter +class SimilarityScorer(object): + + def __init__(self, matchers, request, ascending=False): + self._matchers = matchers + self._request = request + self._ascending = False + + def score(self, candidate, play_count): + value = 1 + total = 0 + if play_count < 1: + total += value + if self._ascending: + value *= 2 + for matcher in self._matchers[::-1]: + if matcher(self._request, candidate): + total += value + if self._ascending: + value *= 2 + return total + + class Cassette(object): """A container for recorded requests and responses""" + max_playcount = 1 + @classmethod def load(cls, **kwargs): """Instantiate and load the cassette stored at the specified path.""" @@ -166,13 +191,14 @@ class Cassette(object): def __init__(self, path, serializer=yamlserializer, record_mode='once', match_on=(uri, method), before_record_request=None, before_record_response=None, custom_patches=(), - inject=False): + inject=False, similarity_scorer_factory=None): self._path = path self._serializer = serializer self._match_on = match_on self._before_record_request = before_record_request or (lambda x: x) self._before_record_response = before_record_response or (lambda x: x) + self._similarity_scorer_factory = similarity_scorer_factory or SimilarityScorer self.inject = inject self.record_mode = record_mode self.custom_patches = custom_patches @@ -227,6 +253,20 @@ class Cassette(object): if requests_match(request, stored_request, self._match_on): yield index, response + def failing_matchers(self, a, b): + return [matcher for matcher in self._match_on if not matcher(a, b)] + + def similar_requests(self, request): + scorer = self._similarity_scorer_factory(self._match_on, request).score + scored_requests = [ + ( + stored_request, + scorer(stored_request, self.play_counts[index]) + ) + for index, (stored_request, response) in enumerate(self.data) + ] + return sorted(scored_requests, key=operator.itemgetter(1), reverse=True) + def can_play_response_for(self, request): request = self._before_record_request(request) return request and request in self and \ @@ -239,7 +279,7 @@ class Cassette(object): hasn't been played back before, and mark it as played """ for index, response in self._responses(request): - if self.play_counts[index] == 0: + if self.play_counts[index] < self.max_playcount: self.play_counts[index] += 1 return response # The cassette doesn't contain the request asked for. diff --git a/vcr/stubs/__init__.py b/vcr/stubs/__init__.py index e140945..ed575d2 100644 --- a/vcr/stubs/__init__.py +++ b/vcr/stubs/__init__.py @@ -227,12 +227,26 @@ class VCRConnection(object): if self.cassette.write_protected and self.cassette.filter_request( self._vcr_request ): + most_similar_request = None + failing_matchers = None + most_similar_request_info = None + try: + most_similar_request_info = self.cassette.similar_requests(self._vcr_request) + most_similar_request = most_similar_request_info[0][0] + failing_matchers = self.cassette.failing_matchers( + self._vcr_request, most_similar_request + ) + except Exception as err: + print "XXXX {0}".format(err) + import ipdb; ipdb.set_trace() raise CannotOverwriteExistingCassetteException( "No match for the request (%r) was found. " "Can't overwrite existing cassette (%r) in " - "your current record mode (%r)." + "your current record mode (%r). Most similar request was (%r). " + "It differed from the request according to (%r). \n\n\n(%r)" % (self._vcr_request, self.cassette._path, - self.cassette.record_mode) + self.cassette.record_mode, most_similar_request, + failing_matchers, most_similar_request_info) ) # Otherwise, we should send the request, then get the response