# -*- test-case-name: mv3d.test.mockwx -*- # Copyright (C) 2009-2012 Mortal Coil Games # See LICENSE for details. """ Utility for creating unit tests that test wxpython code. This can create a mock version of a whole module including all classes and methods in those classes. The mock versions will return a BlackHole object when called and will raise appropriate exceptions when they are called with the wrong number of arguments. Released under the MIT license. @author: mike@mv3d.com (c) 2009-2011 Mike Handverger """ import inspect try: import wx from wx.lib import ogl from wx.lib import agw from wx import aui from wx.lib.agw import floatspin from wx.lib import scrolledpanel from wx.lib import rcsizer from wx.lib.agw import genericmessagedialog from wx.lib import buttons from wx.py import crust except ImportError: wx = None from twisted.trial.unittest import TestCase class BlackHole(object): """ This object black hole's anything you do with it. Call any function on it, get/set any attribute, etc. It just ignores it. """ def __init__(self, *args, **kwargs): pass def __getattr__(self, name): # print name if name == "__trunc__": raise AttributeError if name == "__str__": raise AttributeError return self def __setitem__(self, name, value): pass def __getitem__(self, name): if name > 1000: raise IndexError("BlackHole doesn't support items > 1000.") return self def __call__(self, *_args, **_keys): return BlackHole() def __float__(self): return 0.0 def __int__(self): return 0 def __str__(self): return "BlackHole%d" % id(self) def __repr__(self): return "" % id(self) def __rmul__(self, other): return other def __add__(self, other): return other def __len__(self): return 0 def __and__(self, other): return 0 def __rand__(self, other): return 0 def copyMethod(method): """ Tries to make a mock copy of a method. Makes a good attempt at producing a method that will a) return a BlackHole and b) raise TypeError if you call it with the wrong number of args. Obviously methods that take *args or **kwargs won't validate as well. """ fargs, varargs, varkw, defaults = inspect.getargspec(method) if defaults is None: defaults = [] minArgCount = len(fargs) - len(defaults) maxArgCount = len(fargs) if varkw or varargs: maxArgCount = None maxNonKwArgs = len(fargs) if varargs is not None: maxNonKwArgs = None def _copy(*args, **kwargs): argCount = len(args) + len(kwargs) if argCount < minArgCount: raise TypeError("%s takes %d to %d args (%d given)" % ( method.__name__, minArgCount, maxArgCount, argCount)) if maxArgCount is not None and argCount > maxArgCount: raise TypeError("%s takes %d to %d args (%d given)" % ( method.__name__, minArgCount, maxArgCount, argCount)) if maxNonKwArgs is not None and len(args) > maxNonKwArgs: raise TypeError("%s takes %d to %d args (%d given)" % ( method.__name__, minArgCount, maxArgCount, argCount)) if method.__name__ not in ["__init__", "ConvertToBitmap", "GetFirstChild"]: return BlackHole() elif method.__name__ == "GetFirstChild": hole = BlackHole() hole.IsOk = lambda: False return hole, BlackHole() if "repr" in method.__name__: def _copy(self, *args): return "<%s at %d>" % (self.__class__.__name__, id(self)) _copy.__name__ = method.__name__ return _copy def copyClass(cls, skips=()): """ Makes a mock copy of a class. Each method in the original class is mocked up using copyMethod. Anything named like something in skips (list of strings) will be skipped. """ class _Copy(object): __doc__ = cls.__doc__ for name, method in inspect.getmembers(cls, inspect.ismethod): if not name in skips: setattr(_Copy, name, copyMethod(method)) for name, obj in inspect.getmembers(cls, lambda x: not inspect.isroutine(x) and not inspect.isdatadescriptor(x)): if not name.startswith("_") and not name in skips: setattr(_Copy, name, obj) for name, obj in inspect.getmembers(cls, inspect.isdatadescriptor): val = BlackHole() if "count" in name.lower(): val = 0 setattr(_Copy, name, val) _Copy.__name__ = cls.__name__ _Copy.__module__ = cls.__module__ return _Copy def copyModule(module, skips=()): """ Creates a mock copy of a whole module. At the moment, it just copies all classes in the module using copyClass. Skips can be specified as things that will be ignored (either classes or methods) """ newDict = dict() for name, cls in inspect.getmembers(module, inspect.isclass): if not name in skips: newDict[name] = copyClass(cls, skips) return newDict class MockWx(object): """ Mocks up several of the wx modules using copyModule and replacing the imported module's dict. The initial state of the modules are stored so that we can revert these changes at will. """ if wx is not None: mockWxEntries = copyModule(wx, ["PyEventBinder", "PyCommandEvent"]) realWx = wx.__dict__.copy() mockOglEntries = copyModule(ogl) realOgl = ogl.__dict__.copy() mockAgwEntries = copyModule(agw) realAgw = agw.__dict__.copy() mockAuiEntries = copyModule(aui) realAui = aui.__dict__.copy() mockFloatspinEntries = copyModule(floatspin) realFloatspin = floatspin.__dict__.copy() mockScrolledPanelEntries = copyModule(scrolledpanel) realScrolledPanel = scrolledpanel.__dict__.copy() mockRCSizerEntries = copyModule(rcsizer) realRCSizer = rcsizer.__dict__.copy() mockGenericMessageDialogEntries = copyModule(genericmessagedialog) realGenericMessageDialog = genericmessagedialog.__dict__.copy() mockButtonsEntries = copyModule(buttons) realButtons = buttons.__dict__.copy() mockCrustEntries = copyModule(crust) realCrust = crust.__dict__.copy() def install(self): """ Installs the mock versions of the modules. """ if wx is None: return wx.__dict__.update(self.mockWxEntries) wx.keys = {} wx.GetKeyState = lambda key: wx.keys.get(key, False) ogl.__dict__.update(self.mockOglEntries) agw.__dict__.update(self.mockAgwEntries) aui.__dict__.update(self.mockAuiEntries) aui.AuiToolBarItem.GetState = lambda: 0 floatspin.__dict__.update(self.mockFloatspinEntries) scrolledpanel.__dict__.update(self.mockScrolledPanelEntries) rcsizer.__dict__.update(self.mockRCSizerEntries) genericmessagedialog.__dict__.update( self.mockGenericMessageDialogEntries) buttons.__dict__.update(self.mockButtonsEntries) crust.__dict__.update(self.mockCrustEntries) def uninstall(self): """ Re-installs the real versions of the modules. """ if wx is None: return scrolledpanel.__dict__.update(self.realScrolledPanel) floatspin.__dict__.update(self.realFloatspin) aui.__dict__.update(self.realAui) agw.__dict__.update(self.realAgw) ogl.__dict__.update(self.realOgl) wx.__dict__.update(self.realWx) rcsizer.__dict__.update(self.realRCSizer) genericmessagedialog.__dict__.update( self.realGenericMessageDialog) buttons.__dict__.update(self.realButtons) crust.__dict__.update(self.realCrust) theMockWx = MockWx() installMockWx = theMockWx.install uninstallMockWx = theMockWx.uninstall class MockWxTest(TestCase): """ Base class for a test that uses a mock wx setup. You will need to installMockWx before importing anything that you are testing which uses wx. Then uninstall it. """ if wx is None: skip = "You need wxPython to run this test." def setUp(self): installMockWx() self.wx = wx def tearDown(self): uninstallMockWx()