__author__ = "raphtee@google.com (Travis Miller)" import re, collections, StringIO, sys, unittest class StubNotFoundError(Exception): 'Raised when god is asked to unstub an attribute that was not stubbed' pass class CheckPlaybackError(Exception): 'Raised when mock playback does not match recorded calls.' pass class SaveDataAfterCloseStringIO(StringIO.StringIO): """Saves the contents in a final_data property when close() is called. Useful as a mock output file object to test both that the file was closed and what was written. Properties: final_data: Set to the StringIO's getvalue() data when close() is called. None if close() has not been called. """ final_data = None def close(self): self.final_data = self.getvalue() StringIO.StringIO.close(self) class argument_comparator(object): def is_satisfied_by(self, parameter): raise NotImplementedError class equality_comparator(argument_comparator): def __init__(self, value): self.value = value @staticmethod def _types_match(arg1, arg2): if isinstance(arg1, basestring) and isinstance(arg2, basestring): return True return type(arg1) == type(arg2) @classmethod def _compare(cls, actual_arg, expected_arg): if isinstance(expected_arg, argument_comparator): return expected_arg.is_satisfied_by(actual_arg) if not cls._types_match(expected_arg, actual_arg): return False if isinstance(expected_arg, list) or isinstance(expected_arg, tuple): # recurse on lists/tuples if len(actual_arg) != len(expected_arg): return False for actual_item, expected_item in zip(actual_arg, expected_arg): if not cls._compare(actual_item, expected_item): return False elif isinstance(expected_arg, dict): # recurse on dicts if not cls._compare(sorted(actual_arg.keys()), sorted(expected_arg.keys())): return False for key, value in actual_arg.iteritems(): if not cls._compare(value, expected_arg[key]): return False elif actual_arg != expected_arg: return False return True def is_satisfied_by(self, parameter): return self._compare(parameter, self.value) def __str__(self): if isinstance(self.value, argument_comparator): return str(self.value) return repr(self.value) class regex_comparator(argument_comparator): def __init__(self, pattern, flags=0): self.regex = re.compile(pattern, flags) def is_satisfied_by(self, parameter): return self.regex.search(parameter) is not None def __str__(self): return self.regex.pattern class is_string_comparator(argument_comparator): def is_satisfied_by(self, parameter): return isinstance(parameter, basestring) def __str__(self): return "a string" class is_instance_comparator(argument_comparator): def __init__(self, cls): self.cls = cls def is_satisfied_by(self, parameter): return isinstance(parameter, self.cls) def __str__(self): return "is a %s" % self.cls class anything_comparator(argument_comparator): def is_satisfied_by(self, parameter): return True def __str__(self): return 'anything' class base_mapping(object): def __init__(self, symbol, return_obj, *args, **dargs): self.return_obj = return_obj self.symbol = symbol self.args = [equality_comparator(arg) for arg in args] self.dargs = dict((key, equality_comparator(value)) for key, value in dargs.iteritems()) self.error = None def match(self, *args, **dargs): if len(args) != len(self.args) or len(dargs) != len(self.dargs): return False for i, expected_arg in enumerate(self.args): if not expected_arg.is_satisfied_by(args[i]): return False # check for incorrect dargs for key, value in dargs.iteritems(): if key not in self.dargs: return False if not self.dargs[key].is_satisfied_by(value): return False # check for missing dargs for key in self.dargs.iterkeys(): if key not in dargs: return False return True def __str__(self): return _dump_function_call(self.symbol, self.args, self.dargs) class function_mapping(base_mapping): def __init__(self, symbol, return_val, *args, **dargs): super(function_mapping, self).__init__(symbol, return_val, *args, **dargs) def and_return(self, return_obj): self.return_obj = return_obj def and_raises(self, error): self.error = error class function_any_args_mapping(function_mapping): """A mock function mapping that doesn't verify its arguments.""" def match(self, *args, **dargs): return True class mock_function(object): def __init__(self, symbol, default_return_val=None, record=None, playback=None): self.default_return_val = default_return_val self.num_calls = 0 self.args = [] self.dargs = [] self.symbol = symbol self.record = record self.playback = playback self.__name__ = symbol def __call__(self, *args, **dargs): self.num_calls += 1 self.args.append(args) self.dargs.append(dargs) if self.playback: return self.playback(self.symbol, *args, **dargs) else: return self.default_return_val def expect_call(self, *args, **dargs): mapping = function_mapping(self.symbol, None, *args, **dargs) if self.record: self.record(mapping) return mapping def expect_any_call(self): """Like expect_call but don't give a hoot what arguments are passed.""" mapping = function_any_args_mapping(self.symbol, None) if self.record: self.record(mapping) return mapping class mask_function(mock_function): def __init__(self, symbol, original_function, default_return_val=None, record=None, playback=None): super(mask_function, self).__init__(symbol, default_return_val, record, playback) self.original_function = original_function def run_original_function(self, *args, **dargs): return self.original_function(*args, **dargs) class mock_class(object): def __init__(self, cls, name, default_ret_val=None, record=None, playback=None): self.__name = name self.__record = record self.__playback = playback for symbol in dir(cls): if symbol.startswith("_"): continue orig_symbol = getattr(cls, symbol) if callable(orig_symbol): f_name = "%s.%s" % (self.__name, symbol) func = mock_function(f_name, default_ret_val, self.__record, self.__playback) setattr(self, symbol, func) else: setattr(self, symbol, orig_symbol) def __repr__(self): return '<mock_class: %s>' % self.__name class mock_god(object): NONEXISTENT_ATTRIBUTE = object() def __init__(self, debug=False, fail_fast=True, ut=None): """ With debug=True, all recorded method calls will be printed as they happen. With fail_fast=True, unexpected calls will immediately cause an exception to be raised. With False, they will be silently recorded and only reported when check_playback() is called. """ self.recording = collections.deque() self.errors = [] self._stubs = [] self._debug = debug self._fail_fast = fail_fast self._ut = ut def set_fail_fast(self, fail_fast): self._fail_fast = fail_fast def create_mock_class_obj(self, cls, name, default_ret_val=None): record = self.__record_call playback = self.__method_playback errors = self.errors class cls_sub(cls): cls_count = 0 # overwrite the initializer def __init__(self, *args, **dargs): pass @classmethod def expect_new(typ, *args, **dargs): obj = typ.make_new(*args, **dargs) mapping = base_mapping(name, obj, *args, **dargs) record(mapping) return obj def __new__(typ, *args, **dargs): return playback(name, *args, **dargs) @classmethod def make_new(typ, *args, **dargs): obj = super(cls_sub, typ).__new__(typ, *args, **dargs) typ.cls_count += 1 obj_name = "%s_%s" % (name, typ.cls_count) for symbol in dir(obj): if (symbol.startswith("__") and symbol.endswith("__")): continue if isinstance(getattr(typ, symbol, None), property): continue orig_symbol = getattr(obj, symbol) if callable(orig_symbol): f_name = ("%s.%s" % (obj_name, symbol)) func = mock_function(f_name, default_ret_val, record, playback) setattr(obj, symbol, func) else: setattr(obj, symbol, orig_symbol) return obj return cls_sub def create_mock_class(self, cls, name, default_ret_val=None): """ Given something that defines a namespace cls (class, object, module), and a (hopefully unique) name, will create a mock_class object with that name and that possessess all the public attributes of cls. default_ret_val sets the default_ret_val on all methods of the cls mock. """ return mock_class(cls, name, default_ret_val, self.__record_call, self.__method_playback) def create_mock_function(self, symbol, default_return_val=None): """ create a mock_function with name symbol and default return value of default_ret_val. """ return mock_function(symbol, default_return_val, self.__record_call, self.__method_playback) def mock_up(self, obj, name, default_ret_val=None): """ Given an object (class instance or module) and a registration name, then replace all its methods with mock function objects (passing the orignal functions to the mock functions). """ for symbol in dir(obj): if symbol.startswith("__"): continue orig_symbol = getattr(obj, symbol) if callable(orig_symbol): f_name = "%s.%s" % (name, symbol) func = mask_function(f_name, orig_symbol, default_ret_val, self.__record_call, self.__method_playback) setattr(obj, symbol, func) def stub_with(self, namespace, symbol, new_attribute): original_attribute = getattr(namespace, symbol, self.NONEXISTENT_ATTRIBUTE) # You only want to save the original attribute in cases where it is # directly associated with the object in question. In cases where # the attribute is actually inherited via some sort of hierarchy # you want to delete the stub (restoring the original structure) attribute_is_inherited = (hasattr(namespace, '__dict__') and symbol not in namespace.__dict__) if attribute_is_inherited: original_attribute = self.NONEXISTENT_ATTRIBUTE newstub = (namespace, symbol, original_attribute, new_attribute) self._stubs.append(newstub) setattr(namespace, symbol, new_attribute) def stub_function(self, namespace, symbol): mock_attribute = self.create_mock_function(symbol) self.stub_with(namespace, symbol, mock_attribute) def stub_class_method(self, cls, symbol): mock_attribute = self.create_mock_function(symbol) self.stub_with(cls, symbol, staticmethod(mock_attribute)) def stub_class(self, namespace, symbol): attr = getattr(namespace, symbol) mock_class = self.create_mock_class_obj(attr, symbol) self.stub_with(namespace, symbol, mock_class) def stub_function_to_return(self, namespace, symbol, object_to_return): """Stub out a function with one that always returns a fixed value. @param namespace The namespace containing the function to stub out. @param symbol The attribute within the namespace to stub out. @param object_to_return The value that the stub should return whenever it is called. """ self.stub_with(namespace, symbol, lambda *args, **dargs: object_to_return) def _perform_unstub(self, stub): namespace, symbol, orig_attr, new_attr = stub if orig_attr == self.NONEXISTENT_ATTRIBUTE: delattr(namespace, symbol) else: setattr(namespace, symbol, orig_attr) def unstub(self, namespace, symbol): for stub in reversed(self._stubs): if (namespace, symbol) == (stub[0], stub[1]): self._perform_unstub(stub) self._stubs.remove(stub) return raise StubNotFoundError() def unstub_all(self): self._stubs.reverse() for stub in self._stubs: self._perform_unstub(stub) self._stubs = [] def __method_playback(self, symbol, *args, **dargs): if self._debug: print >> sys.__stdout__, (' * Mock call: ' + _dump_function_call(symbol, args, dargs)) if len(self.recording) != 0: func_call = self.recording[0] if func_call.symbol != symbol: msg = ("Unexpected call: %s\nExpected: %s" % (_dump_function_call(symbol, args, dargs), func_call)) self._append_error(msg) return None if not func_call.match(*args, **dargs): msg = ("Incorrect call: %s\nExpected: %s" % (_dump_function_call(symbol, args, dargs), func_call)) self._append_error(msg) return None # this is the expected call so pop it and return self.recording.popleft() if func_call.error: raise func_call.error else: return func_call.return_obj else: msg = ("unexpected call: %s" % (_dump_function_call(symbol, args, dargs))) self._append_error(msg) return None def __record_call(self, mapping): self.recording.append(mapping) def _append_error(self, error): if self._debug: print >> sys.__stdout__, ' *** ' + error if self._fail_fast: raise CheckPlaybackError(error) self.errors.append(error) def check_playback(self): """ Report any errors that were encounterd during calls to __method_playback(). """ if len(self.errors) > 0: if self._debug: print '\nPlayback errors:' for error in self.errors: print >> sys.__stdout__, error if self._ut: self._ut.fail('\n'.join(self.errors)) raise CheckPlaybackError elif len(self.recording) != 0: errors = [] for func_call in self.recording: error = "%s not called" % (func_call,) errors.append(error) print >> sys.__stdout__, error if self._ut: self._ut.fail('\n'.join(errors)) raise CheckPlaybackError self.recording.clear() def mock_io(self): """Mocks and saves the stdout & stderr output""" self.orig_stdout = sys.stdout self.orig_stderr = sys.stderr self.mock_streams_stdout = StringIO.StringIO('') self.mock_streams_stderr = StringIO.StringIO('') sys.stdout = self.mock_streams_stdout sys.stderr = self.mock_streams_stderr def unmock_io(self): """Restores the stdout & stderr, and returns both output strings""" sys.stdout = self.orig_stdout sys.stderr = self.orig_stderr values = (self.mock_streams_stdout.getvalue(), self.mock_streams_stderr.getvalue()) self.mock_streams_stdout.close() self.mock_streams_stderr.close() return values def _arg_to_str(arg): if isinstance(arg, argument_comparator): return str(arg) return repr(arg) def _dump_function_call(symbol, args, dargs): arg_vec = [] for arg in args: arg_vec.append(_arg_to_str(arg)) for key, val in dargs.iteritems(): arg_vec.append("%s=%s" % (key, _arg_to_str(val))) return "%s(%s)" % (symbol, ', '.join(arg_vec))