Skip to content

Commit

Permalink
Corrected precedence of onCall, withArgs & returns/throws
Browse files Browse the repository at this point in the history
Fixes #23
Fixes #29
Fixes #31
  • Loading branch information
jonathan-benn-copilot authored and note35 committed Dec 2, 2017
1 parent bc701e7 commit 45f8240
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 50 deletions.
20 changes: 17 additions & 3 deletions sinon/lib/stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,24 @@ def __init__(self, obj=None, prop=None, func=None):
super(SinonStub, self).wrap2stub(self._stubfunc)
self._copy = self._cond_args = self._cond_kwargs = self._oncall = None
# Todo: target is a dirty hack
self._conditions = {"args":[], "kwargs":[], "action": [], "oncall": [], "target": self.obj}
self._conditions = {"args":[], "kwargs":[], "action":[], "oncall":[], "target": self.obj}
self._conditions["default"] = lambda *args, **kwargs: None

def _append_condition(self, sinon_stub_condition, func):
'''
Permanently saves the current (volatile) conditions, which would be otherwise lost
In the _conditions dictionary, the keys "args", "kwargs", "oncall" and "action"
each refer to a list. All 4 lists have a value appended each time the user calls
returns or throws to add a condition to the stub. Hence, all 4 lists are in sync,
so a single index refers to the same condition in all 4 lists.
e.g.
stub.withArgs(5).returns(7)
# conditions: args [(5,)] kwargs [()] action [7] oncall [None]
stub.withArgs(10).onFirstCall().returns(14)
# conditions: args [(5,),(10,)] kwargs [(),()] action [7,14] oncall [None,1]
Args:
sinon_stub_condition: the _SinonStubCondition object that holds the current conditions
func: returns a value or raises an exception, as specified by the user
Expand Down Expand Up @@ -113,7 +125,8 @@ def returns(self, obj):
Args: obj (anything)
Return: a SinonStub object (able to be chained)
"""
super(SinonStub, self).wrap2stub(lambda *args, **kwargs: obj)
self._conditions["default"] = lambda *args, **kwargs: obj
super(SinonStub, self).wrap2stub(self._conditions["default"], self._conditions)
return self

def throws(self, exception=Exception):
Expand All @@ -126,7 +139,8 @@ def throws(self, exception=Exception):
"""
def exception_function(*args, **kwargs):
raise exception
super(SinonStub, self).wrap2stub(exception_function)
self._conditions["default"] = exception_function
super(SinonStub, self).wrap2stub(self._conditions["default"], self._conditions)
return self

class _SinonStubCondition(SinonStub):
Expand Down
155 changes: 120 additions & 35 deletions sinon/lib/util/Wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,20 @@ def classproperty(func): #pylint: disable=missing-docstring
func = classmethod(func)
return ClassPropertyDescriptor(func)


class EmptyClass(object): #pylint: disable=too-few-public-methods,missing-docstring
pass

def empty_function(*args, **kwargs): #pylint: disable=unused-argument,missing-docstring
pass

def wrapspy(func, customfunc=None, condition=None): #pylint: disable=missing-docstring
def wrapspy(func, customfunc=None, conditions=None): #pylint: disable=missing-docstring
if customfunc:
@__add_spy
def wrapped_fn(*args, **kwargs):
"""
A wrapped stub function
"""
return __gen_stub_func(customfunc, condition, wrapped_fn, *args, **kwargs)
return __gen_stub_func(customfunc, conditions, wrapped_fn, *args, **kwargs)
else:
@__add_spy
def wrapped_fn(*args, **kwargs):
Expand All @@ -61,13 +60,13 @@ def wrapped(*args, **kwargs):
wrapped.callCount += 1
wrapped.args_list.append(args)
wrapped.kwargs_list.append(kwargs)

call = SpyCall()
call.args = args
call.kwargs = kwargs
call.stack = traceback.format_stack()
wrapped.call_list.append(call)

try:
ret = func(*args, **kwargs)
wrapped.ret_list.append(ret)
Expand All @@ -89,53 +88,139 @@ def wrapped(*args, **kwargs):
wrapped.LOCK = True
return wrapped

def __gen_index_list(condition, *args, **kwargs):
# Generating index list based on arguments and condition
# Note: ignore args[0] because it is callback in this condition
# combination
def __gen_index_list(conditions, *args, **kwargs):
"""
Args:
conditions: dictionary, the SinonStub's conditions (the user-defined behaviour for the stub)
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
Returns:
list, the list of indices in conditions for which the user args/kwargs match
"""
return __get_call_indices(conditions["target"], args, kwargs, conditions["args"], conditions["kwargs"])

def __get_call_indices(target, args, kwargs, args_list, kwargs_list):
"""
Args:
target: object, the callback (self) argument for the owner of the stubbed function
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
args_list: list, a list of argument tuples
kwargs_list: list, a list of keyword argument dictionaries
Returns:
list, the list of indices in args_list/kwargs_list for which the user args/kwargs match
"""
# Todo: dirty hack
if len(args) > 0:
if condition["target"] == args[0].__class__:
if target == args[0].__class__:
# Note: ignore args[0] because it is a callback (self) in this condition combination
args = args[1:]
args_list = [i[1:] if i and i[0].__class__ == target else i for i in args_list]

if args and kwargs:
if args in condition["args"] and kwargs in condition["kwargs"]:
args_indices = [i for i, x in enumerate(condition["args"]) if x == args]
kwargs_indices = [i for i, x in enumerate(condition["kwargs"]) if x == kwargs]
if args in args_list and kwargs in kwargs_list:
args_indices = [i for i, x in enumerate(args_list) if x == args]
kwargs_indices = [i for i, x in enumerate(kwargs_list) if x == kwargs]
return list(set(args_indices).intersection(kwargs_indices))
# args only
elif args:
if args in condition["args"]:
return [i for i, x in enumerate(condition["args"]) if x == args and not condition["kwargs"][i]]
if args in args_list:
return [i for i, x in enumerate(args_list) if x == args and not kwargs_list[i]]
#kwargs only
elif kwargs:
if kwargs in condition["kwargs"]:
return [i for i, x in enumerate(condition["kwargs"]) if x == kwargs and not condition["args"][i]]
if kwargs in kwargs_list:
return [i for i, x in enumerate(kwargs_list) if x == kwargs and not args_list[i]]
else:
return []

def __get_call_count(target, args, kwargs, args_list, kwargs_list):
"""
Args:
target: object, the callback (self) argument for the owner of the stubbed function
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
args_list: list, the tuples of args from all the times this stub was called
kwargs_list: list, the dictionaries of kwargs from all the times this stub was called
Returns:
integer, the number of times this combination of args/kwargs has been called
"""
return len(__get_call_indices(target, args, kwargs, args_list, kwargs_list))

def __gen_retfunc_with_args(index_list, condition, func, *args, **kwargs):
def __gen_retfunc_with_args(index_list, conditions, func, *args, **kwargs):
"""
@return customfunc by condition(args/kwargs || args+oncall/kwargs+oncall)
Pre-conditions:
(1) The user has created a stub and specified the stub behaviour ("conditions")
(2) The user has called the stub function ("func") with the specified "args" and "kwargs"
(3) One or more 'withArgs' conditions were applicable in this case
Args:
index_list: list, the list of indices in conditions for which the user args/kwargs match
conditions: dictionary, the SinonStub's conditions (the user-defined behaviour for the stub)
func: function, the SinonStub function wrapper (as defined by calls to returns/throws)
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
Returns:
any type, the appropriate return value, based on the stub's behaviour setup and the user input
"""
for i in reversed(index_list):
if not condition["oncall"][i] or condition["oncall"][i] == func.callCount:
return condition["action"][i](*args, **kwargs)
# indices with an arg and oncall have higher priority and should be checked first
indices_with_oncall = [i for i in reversed(index_list) if conditions["oncall"][i]]
# if there are any combined withArgs+onCall conditions
if indices_with_oncall:
call_count = __get_call_count(conditions["target"], args, kwargs, func.args_list, func.kwargs_list)
for i in indices_with_oncall:
if conditions["oncall"][i] == call_count:
return conditions["action"][i](*args, **kwargs)
# else if there are simple withArgs conditions
indices_without_oncall = [i for i in reversed(index_list) if not conditions["oncall"][i]]
if indices_without_oncall:
max_index = max(indices_without_oncall)
return conditions["action"][max_index](*args, **kwargs)
# else all conditions did not match
return conditions["default"](*args, **kwargs)

def __gen_retfunc_without_args(index_list, condition, func, *args, **kwargs):
def __gen_retfunc_without_args(conditions, func, *args, **kwargs):
"""
@return customfunc by condition(oncall)
Pre-conditions:
(1) The user has created a stub and specified the stub behaviour ("conditions")
(2) The user has called the stub function ("func") with the specified "args" and "kwargs"
(3) No 'withArgs' conditions were applicable in this case
Args:
conditions: dictionary, the SinonStub's conditions (the user-defined behaviour for the stub)
func: function, the SinonStub function wrapper (as defined by calls to returns/throws)
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
Returns:
any type, the appropriate return value, based on the stub's behaviour setup and the user input
"""
if func.callCount in condition["oncall"]:
index_list = [i for i, x in enumerate(condition["oncall"]) if x and not condition["args"][i] and not condition["kwargs"][i]]
# if there might be applicable onCall conditions
if func.callCount in conditions["oncall"]:
index_list = [i for i, x in enumerate(conditions["oncall"]) if x and not conditions["args"][i] and not conditions["kwargs"][i]]
for i in reversed(index_list):
if func.callCount == condition["oncall"][i]:
return condition["action"][i](*args, **kwargs)
# if the onCall condition applies
if func.callCount == conditions["oncall"][i]:
return conditions["action"][i](*args, **kwargs)
# else all conditions did not match
return conditions["default"](*args, **kwargs)

def __gen_stub_func(customfunc, condition, func, *args, **kwargs):
if condition:
index_list = __gen_index_list(condition, *args, **kwargs)
def __gen_stub_func(customfunc, conditions, func, *args, **kwargs):
"""
Args:
customfunc: function, the user's custom function with which they want to replace the original
conditions: dictionary, the SinonStub's conditions (the user-defined behaviour for the stub)
func: function, the SinonStub function wrapper (as defined by calls to returns/throws)
args: tuple, the arguments inputed by the user
kwargs: dictionary, the keyword arguments inputed by the user
Returns:
any type, the appropriate return value, based on the stub's behaviour setup and the user input
"""
# if the user defined stub behavioural conditions
if conditions:
index_list = __gen_index_list(conditions, *args, **kwargs)
# if there are 'withArgs' conditions that might be applicable
if index_list:
return __gen_retfunc_with_args(index_list, condition, func, *args, **kwargs)
return __gen_retfunc_without_args(index_list, condition, func, *args, **kwargs)
#return stub function
return customfunc(*args, **kwargs)
return __gen_retfunc_with_args(index_list, conditions, func, *args, **kwargs)
# else no 'withArgs' conditions are applicable
else:
return __gen_retfunc_without_args(conditions, func, *args, **kwargs)
# else there are no behavioural conditions
else:
return customfunc(*args, **kwargs)

0 comments on commit 45f8240

Please sign in to comment.