"Fossies" - the Fresh Open Source Software Archive

Member "zim-0.70/tests/__init__.py" (21 Mar 2019, 23565 Bytes) of package /linux/privat/zim-0.70.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "__init__.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 0.69.1_vs_0.70.

    1 
    2 # Copyright 2008-2017 Jaap Karssenberg <jaap.karssenberg@gmail.com>
    3 
    4 '''Zim test suite'''
    5 
    6 
    7 
    8 
    9 import os
   10 import sys
   11 import tempfile
   12 import shutil
   13 import logging
   14 import gettext
   15 import xml.etree.cElementTree as etree
   16 import types
   17 import glob
   18 
   19 try:
   20     import gi
   21     gi.require_version('Gtk', '3.0')
   22     from gi.repository import Gtk
   23 except ImportError:
   24     Gtk = None
   25 
   26 
   27 import unittest
   28 from unittest import skip, skipIf, skipUnless, expectedFailure
   29 
   30 
   31 gettext.install('zim', names=('_', 'gettext', 'ngettext'))
   32 
   33 FAST_TEST = False #: determines whether we skip slow tests or not
   34 FULL_TEST = False #: determine whether we mock filesystem tests or not
   35 
   36 # This list also determines the order in which tests will executed
   37 __all__ = [
   38     # Packaging etc.
   39     'package', 'translations',
   40     # Basic libraries
   41     'datetimetz', 'utils', 'errors', 'signals', 'actions',
   42     'fs', 'newfs',
   43     'config', 'applications',
   44     'parsing', 'tokenparser',
   45     # Notebook components
   46     'formats', 'templates',
   47     'indexers', 'indexviews', 'operations', 'notebook', 'history',
   48     'export', 'www', 'search',
   49     # Core application
   50     'widgets', 'pageview', 'save_page', 'clipboard', 'uiactions',
   51     'mainwindow', 'notebookdialog',
   52     'preferencesdialog', 'searchdialog', 'customtools', 'templateeditordialog',
   53     'main', 'plugins',
   54     # Plugins
   55     'pathbar', 'pageindex',
   56     'journal', 'printtobrowser', 'versioncontrol', 'inlinecalculator',
   57     'tasklist', 'tags', 'imagegenerators', 'tableofcontents',
   58     'quicknote', 'attachmentbrowser', 'insertsymbol',
   59     'sourceview', 'tableeditor', 'bookmarksbar', 'spell',
   60     'arithmetic', 'linesorter'
   61 ]
   62 
   63 
   64 mydir = os.path.abspath(os.path.dirname(__file__))
   65 
   66 # when a test is missing from the list that should be detected
   67 for file in glob.glob(mydir + '/*.py'):
   68     name = os.path.basename(file)[:-3]
   69     if name != '__init__' and not name in __all__:
   70         raise AssertionError('Test missing in __all__: %s' % name)
   71 
   72 # get our own data dir
   73 DATADIR = os.path.abspath(os.path.join(mydir, 'data'))
   74 
   75 # and project data dir
   76 ZIM_DATADIR = os.path.abspath(os.path.join(mydir, '../data'))
   77 
   78 # get our own tmpdir
   79 TMPDIR = os.path.abspath(os.path.join(mydir, 'tmp'))
   80     # Wanted to use tempfile.get_tempdir here to put everything in
   81     # e.g. /tmp/zim but since /tmp is often mounted as special file
   82     # system this conflicts with thrash support. For writing in source
   83     # dir we have conflict with bazaar controls, this is worked around
   84     # by a config mode switch in the bazaar backend of the version
   85     # control plugin
   86 
   87 # also get the default tmpdir and put a copy in the env
   88 REAL_TMPDIR = tempfile.gettempdir()
   89 
   90 
   91 def load_tests(loader, tests, pattern):
   92     '''Load all test cases and return a unittest.TestSuite object.
   93     The parameters 'tests' and 'pattern' are ignored.
   94     '''
   95     suite = unittest.TestSuite()
   96     for name in ['tests.' + name for name in __all__]:
   97         test = loader.loadTestsFromName(name)
   98         suite.addTest(test)
   99     return suite
  100 
  101 
  102 def _setUpEnvironment():
  103     '''Method to be run once before test suite starts'''
  104     # In fact to be loaded before loading some of the zim modules
  105     # like zim.config and any that export constants from it
  106 
  107     # NOTE: do *not* touch XDG_DATA_DIRS here because it is used by Gtk to
  108     # find resources like pixbuf loaders etc. not finding these will lead to
  109     # a crash. Especially under msys the defaults are not set but also not
  110     # map to the default folders. So not touching it is the safest path.
  111     # For zim internal usage this is overloaded in config.basedirs.set_basedirs()
  112     os.environ.update({
  113         'ZIM_TEST_RUNNING': 'True',
  114         'ZIM_TEST_ROOT': os.getcwd(),
  115         'TMP': TMPDIR,
  116         'REAL_TMP': REAL_TMPDIR,
  117         'XDG_DATA_HOME': os.path.join(TMPDIR, 'data_home'),
  118         'TEST_XDG_DATA_DIRS': os.path.join(TMPDIR, 'data_dir'),
  119         'XDG_CONFIG_HOME': os.path.join(TMPDIR, 'config_home'),
  120         'XDG_CONFIG_DIRS': os.path.join(TMPDIR, 'config_dir'),
  121         'XDG_CACHE_HOME': os.path.join(TMPDIR, 'cache_home')
  122     })
  123 
  124     if os.path.isdir(TMPDIR):
  125         shutil.rmtree(TMPDIR)
  126     os.makedirs(TMPDIR)
  127 
  128 
  129 if os.environ.get('ZIM_TEST_RUNNING') != 'True':
  130     # Do this when loaded, but not re-do in sub processes
  131     # (doing so will kill e.g. the ipc test...)
  132     _setUpEnvironment()
  133 
  134 
  135 ## Setup special logging for tests
  136 
  137 class UncaughtWarningError(AssertionError):
  138     pass
  139 
  140 
  141 class TestLoggingHandler(logging.Handler):
  142     '''Handler class that raises uncaught errors to ensure test don't fail silently'''
  143 
  144     def __init__(self, level=logging.WARNING):
  145         logging.Handler.__init__(self, level)
  146         fmt = logging.Formatter('%(levelname)s %(filename)s %(lineno)s: %(message)s')
  147         self.setFormatter(fmt)
  148 
  149     def emit(self, record):
  150         if record.levelno >= logging.WARNING \
  151         and not record.name.startswith('tests'):
  152             raise UncaughtWarningError(self.format(record))
  153         else:
  154             pass
  155 
  156 logging.getLogger().addHandler(TestLoggingHandler())
  157     # Handle all errors that make it up to the root level
  158 
  159 try:
  160     logging.getLogger('zim.test').warning('foo')
  161 except UncaughtWarningError:
  162     pass
  163 else:
  164     raise AssertionError('Raising errors on warning fails')
  165 
  166 ###
  167 
  168 
  169 from zim.newfs import LocalFolder
  170 
  171 import zim.config.manager
  172 import zim.plugins
  173 
  174 _zim_pyfiles = []
  175 
  176 def zim_pyfiles():
  177     '''Returns a list with file paths for all the zim python files'''
  178     if not _zim_pyfiles:
  179         for d, dirs, files in os.walk('zim'):
  180             _zim_pyfiles.extend([d + '/' + f for f in files if f.endswith('.py')])
  181         _zim_pyfiles.sort()
  182     for file in _zim_pyfiles:
  183         yield file # shallow copy
  184 
  185 
  186 def slowTest(obj):
  187     '''Decorator for slow tests
  188 
  189     Tests wrapped with this decorator are ignored when you run
  190     C{test.py --fast}. You can either wrap whole test classes::
  191 
  192         @tests.slowTest
  193         class MyTest(tests.TestCase):
  194             ...
  195 
  196     or individual test functions::
  197 
  198         class MyTest(tests.TestCase):
  199 
  200             @tests.slowTest
  201             def testFoo(self):
  202                 ...
  203 
  204             def testBar(self):
  205                 ...
  206     '''
  207     if FAST_TEST:
  208         wrapper = skip('Slow test')
  209         return wrapper(obj)
  210     else:
  211         return obj
  212 
  213 
  214 MOCK_ALWAYS_MOCK = 'mock' #: Always choose mock folder, alwasy fast
  215 MOCK_DEFAULT_MOCK = 'default_mock' #: By default use mock, but sometimes at random use real fs or at --full
  216 MOCK_DEFAULT_REAL = 'default_real' #: By default use real fs, mock oly for --fast
  217 MOCK_ALWAYS_REAL = 'real' #: always use real fs -- not recommended unless test fails for mock
  218 
  219 import random
  220 import time
  221 
  222 TIMINGS = []
  223 
  224 class TestCase(unittest.TestCase):
  225     '''Base class for test cases'''
  226 
  227     maxDiff = None
  228 
  229     mockConfigManager = True
  230 
  231     def run(self, *a, **kwa):
  232         start = time.time()
  233         unittest.TestCase.run(self, *a, **kwa)
  234         end = time.time()
  235         TIMINGS.append((self.__class__.__name__ + '.' + self._testMethodName, end - start))
  236 
  237     @classmethod
  238     def setUpClass(cls):
  239         if cls.mockConfigManager:
  240             zim.config.manager.makeConfigManagerVirtual()
  241             zim.plugins.resetPluginManager()
  242 
  243     @classmethod
  244     def tearDownClass(cls):
  245         if Gtk is not None:
  246             gtk_process_events() # flush any pending events / warnings
  247 
  248         zim.config.manager.resetConfigManager()
  249         zim.plugins.resetPluginManager()
  250 
  251     def setUpFolder(self, name=None, mock=MOCK_DEFAULT_MOCK):
  252         '''Convenience method to create a temporary folder for testing
  253         @param name: name postfix for the folder
  254         @param mock: mock level for this test, one of C{MOCK_ALWAYS_MOCK},
  255         C{MOCK_DEFAULT_MOCK}, C{MOCK_DEFAULT_REAL} or C{MOCK_ALWAYS_REAL}.
  256         The C{MOCK_ALWAYS_*} arguments force the use of a real folder or a
  257         mock object. The C{MOCK_DEFAULT_*} arguments give a preference but
  258         for these the behavior is overruled by "--fast" and "--full" in the
  259         test script.
  260         @returns: a L{Folder} object (either L{LocalFolder} or L{MockFolder})
  261         that is guarenteed non-existing
  262         '''
  263         path = self._get_tmp_name(name)
  264 
  265         if mock == MOCK_ALWAYS_MOCK:
  266             use_mock = True
  267         elif mock == MOCK_ALWAYS_REAL:
  268             use_mock = False
  269         else:
  270             if FULL_TEST:
  271                 use_mock = False
  272             elif FAST_TEST:
  273                 use_mock = True
  274             else:
  275                 use_mock = (mock == MOCK_DEFAULT_MOCK)
  276 
  277         if use_mock:
  278             from zim.newfs.mock import MockFolder
  279             folder = MockFolder(path)
  280         else:
  281             from zim.newfs import LocalFolder
  282             if os.path.exists(path):
  283                 logger.debug('Clear tmp folder: %s', path)
  284                 shutil.rmtree(path)
  285                 assert not os.path.exists(path) # make real sure
  286             folder = LocalFolder(path)
  287 
  288         assert not folder.exists()
  289         return folder
  290 
  291     def setUpNotebook(self, name='notebook', mock=MOCK_ALWAYS_MOCK, content={}, folder=None):
  292         '''
  293         @param name: name postfix for the folder, see L{setUpFolder}
  294         @param mock: see L{setUpFolder}, default is C{MOCK_ALWAYS_MOCK}
  295         @param content: dictionary where the keys are page names and the
  296         values the page content. If a tuple or list is given, pages are created
  297         with default text. L{Path} objects are allowed instead of page names
  298         @param folder: determine the folder to be used, only needed in special
  299         cases where the folder must be outside of the project folder, like
  300         when testing version control logic
  301         '''
  302         import datetime
  303         from zim.newfs.mock import MockFolder
  304         from zim.notebook.notebook import NotebookConfig, Notebook
  305         from zim.notebook.page import Path
  306         from zim.notebook.layout import FilesLayout
  307         from zim.notebook.index import Index
  308         from zim.formats.wiki import WIKI_FORMAT_VERSION
  309 
  310         if folder is None:
  311             folder = self.setUpFolder(name, mock)
  312         folder.touch() # Must exist for sane notebook
  313         cache_dir = folder.folder('.zim')
  314         layout = FilesLayout(folder, endofline='unix')
  315 
  316         if isinstance(folder, MockFolder):
  317             conffile = folder.file('notebook.zim')
  318             config = NotebookConfig(conffile)
  319             index = Index(':memory:', layout)
  320         else:
  321             conffile = folder.file('notebook.zim')
  322             config = NotebookConfig(conffile)
  323             cache_dir.touch()
  324             index = Index(cache_dir.file('index.db').path, layout)
  325 
  326         if isinstance(content, (list, tuple)):
  327             content = dict((p, 'test 123') for p in content)
  328 
  329         notebook = Notebook(cache_dir, config, folder, layout, index)
  330         for name, text in list(content.items()):
  331             path = Path(name) if isinstance(name, str) else name
  332             file, folder = layout.map_page(path)
  333             file.write(
  334                 (
  335                     'Content-Type: text/x-zim-wiki\n'
  336                     'Wiki-Format: %s\n'
  337                     'Creation-Date: %s\n\n'
  338                 ) % (WIKI_FORMAT_VERSION, datetime.datetime.now().isoformat())
  339                 + text
  340             )
  341 
  342         notebook.index.check_and_update()
  343         assert notebook.index.is_uptodate
  344         return notebook
  345 
  346     def create_tmp_dir(self, name=None):
  347         '''Returns a path to a tmp dir where tests can write data.
  348         The dir is removed and recreated empty every time this function
  349         is called with the same name from the same class.
  350         '''
  351         print("Deprecated: TestCase.create_tmp_dir()")
  352         folder = self.setUpFolder(name=name, mock=MOCK_ALWAYS_REAL)
  353         folder.touch()
  354         return folder.path
  355 
  356     def _get_tmp_name(self, postfix):
  357         name = self.__class__.__name__
  358         if self._testMethodName != 'runTest':
  359             name += '_' + self._testMethodName
  360 
  361         if postfix:
  362             assert '/' not in postfix and '\\' not in postfix, 'Don\'t use this method to get sub folders or files'
  363             name += '_' + postfix
  364 
  365         return os.path.join(TMPDIR, name)
  366 
  367 
  368 class LoggingFilter(logging.Filter):
  369     '''Convenience class to surpress zim errors and warnings in the
  370     test suite. Acts as a context manager and can be used with the
  371     'with' keyword.
  372 
  373     Alternatively you can call L{wrap_test()} from test C{setUp}.
  374     This will start the filter and make sure it is cleaned up again.
  375     '''
  376 
  377     # Due to how the "logging" module works, logging channels do inherit
  378     # handlers of parents but not filters. Therefore setting a filter
  379     # on the "zim" channel will not surpress messages from sub-channels.
  380     # Instead we need to set the filter both on the channel and on
  381     # top level handlers to get the desired effect.
  382 
  383     def __init__(self, logger, message=None):
  384         '''Constructor
  385         @param logger: the logging channel name
  386         @param message: can be a string, or a sequence of strings.
  387         Any messages that start with this string or any of these
  388         strings are surpressed.
  389         '''
  390         self.logger = logger
  391         self.message = message
  392 
  393     def __enter__(self):
  394         logging.getLogger(self.logger).addFilter(self)
  395         for handler in logging.getLogger().handlers:
  396             handler.addFilter(self)
  397 
  398     def __exit__(self, *a):
  399         logging.getLogger(self.logger).removeFilter(self)
  400         for handler in logging.getLogger().handlers:
  401             handler.removeFilter(self)
  402 
  403     def filter(self, record):
  404         if record.name.startswith(self.logger):
  405             msg = record.getMessage()
  406             if self.message is None:
  407                 return False
  408             elif isinstance(self.message, tuple):
  409                 return not any(msg.startswith(m) for m in self.message)
  410             else:
  411                 return not msg.startswith(self.message)
  412         else:
  413             return True
  414 
  415 
  416     def wrap_test(self, test):
  417         self.__enter__()
  418         test.addCleanup(self.__exit__)
  419 
  420 
  421 class DialogContext(object):
  422     '''Context manager to catch dialogs being opened
  423 
  424     Inteded to be used like this::
  425 
  426         def myCustomTest(dialog):
  427             self.assertTrue(isinstance(dialog, CustomDialogClass))
  428             # ...
  429             dialog.assert_response_ok()
  430 
  431         with DialogContext(
  432             myCustomTest,
  433             SomeOtherDialogClass
  434         ):
  435             gui.show_dialogs()
  436 
  437     In this example the first dialog that is run by C{gui.show_dialogs()}
  438     is checked by the function C{myCustomTest()} while the second dialog
  439     just needs to be of class C{SomeOtherDialogClass} and will then
  440     be closed with C{assert_response_ok()} by the context manager.
  441 
  442     This context only works for dialogs derived from zim's Dialog class
  443     as it uses a special hook in L{zim.gui.widgets}.
  444     '''
  445 
  446     def __init__(self, *definitions):
  447         '''Constructor
  448         @param definitions: list of either classes or methods
  449         '''
  450         self.stack = list(definitions)
  451         self.old_test_mode = None
  452 
  453     def __enter__(self):
  454         import zim.gui.widgets
  455         self.old_test_mode = zim.gui.widgets.TEST_MODE
  456         self.old_callback = zim.gui.widgets.TEST_MODE_RUN_CB
  457         zim.gui.widgets.TEST_MODE = True
  458         zim.gui.widgets.TEST_MODE_RUN_CB = self._callback
  459 
  460     def _callback(self, dialog):
  461         #~ print('>>>', dialog)
  462         if not self.stack:
  463             raise AssertionError('Unexpected dialog run: %s' % dialog)
  464 
  465         handler = self.stack.pop(0)
  466 
  467         if isinstance(handler, type): # is a class
  468             self._default_handler(handler, dialog)
  469         else: # assume a function
  470             handler(dialog)
  471 
  472     def _default_handler(self, cls, dialog):
  473         if not isinstance(dialog, cls):
  474             raise AssertionError('Expected dialog of class %s, but got %s instead' % (cls, dialog.__class__))
  475         dialog.assert_response_ok()
  476 
  477     def __exit__(self, *error):
  478         import zim.gui.widgets
  479         zim.gui.widgets.TEST_MODE = self.old_test_mode
  480         zim.gui.widgets.TEST_MODE_RUN_CB = self.old_callback
  481 
  482         has_error = any(error)
  483         if self.stack and not has_error:
  484             raise AssertionError('%i expected dialog(s) not run' % len(self.stack))
  485 
  486         return False # Raise any errors again outside context
  487 
  488 
  489 class WindowContext(DialogContext):
  490 
  491     def _default_handler(self, cls, window):
  492         if not isinstance(window, cls):
  493             raise AssertionError('Expected window of class %s, but got %s instead' % (cls, dialog.__class__))
  494 
  495 
  496 class ApplicationContext(object):
  497 
  498     def __init__(self, *callbacks):
  499         self.stack = list(callbacks)
  500 
  501     def __enter__(self):
  502         import zim.applications
  503         self.old_test_mode = zim.applications.TEST_MODE
  504         self.old_callback = zim.applications.TEST_MODE_RUN_CB
  505         zim.applications.TEST_MODE = True
  506         zim.applications.TEST_MODE_RUN_CB = self._callback
  507 
  508     def _callback(self, cmd):
  509         if not self.stack:
  510             raise AssertionError('Unexpected application run: %s' % cmd)
  511 
  512         handler = self.stack.pop(0)
  513         return handler(cmd) # need to return for pipe()
  514 
  515     def __exit__(self, *error):
  516         import zim.gui.widgets
  517         zim.applications.TEST_MODE = self.old_test_mode
  518         zim.applications.TEST_MODE_RUN_CB = self.old_callback
  519 
  520         if self.stack and not any(error):
  521             raise AssertionError('%i expected command(s) not run' % len(self.stack))
  522 
  523         return False # Raise any errors again outside context
  524 
  525 
  526 class ZimApplicationContext(object):
  527 
  528     def __init__(self, *callbacks):
  529         self.stack = list(callbacks)
  530 
  531     def __enter__(self):
  532         from zim.main import ZIM_APPLICATION
  533         self.apps_obj = ZIM_APPLICATION
  534         self.old_run = ZIM_APPLICATION._run_cmd
  535         ZIM_APPLICATION._run_cmd = self._callback
  536 
  537     def _callback(self, cmd, args):
  538         if not self.stack:
  539             raise AssertionError('Unexpected command run: %s %r' % (cmd, args))
  540 
  541         handler = self.stack.pop(0)
  542         handler(cmd, args)
  543 
  544     def __exit__(self, *error):
  545         self.apps_obj._run_cmd = self.old_run
  546 
  547         if self.stack and not any(error):
  548             raise AssertionError('%i expected command(s) not run' % len(self.stack))
  549 
  550         return False # Raise any errors again outside context
  551 
  552 
  553 
  554 class TestData(object):
  555     '''Wrapper for a set of test data in tests/data'''
  556 
  557     def __init__(self, format):
  558         assert format == 'wiki', 'TODO: add other formats'
  559         root = os.environ['ZIM_TEST_ROOT']
  560         tree = etree.ElementTree(file=root + '/tests/data/notebook-wiki.xml')
  561 
  562         test_data = []
  563         for node in tree.getiterator(tag='page'):
  564             name = node.attrib['name']
  565             text = str(node.text.lstrip('\n'))
  566             test_data.append((name, text))
  567 
  568         self._test_data = tuple(test_data)
  569 
  570     def __iter__(self):
  571         '''Yield the test data as 2 tuple (pagename, text)'''
  572         for name, text in self._test_data:
  573             yield name, text # shallow copy
  574 
  575     def items(self):
  576         return list(self)
  577 
  578     def __getitem__(self, key):
  579         return self.get(key)
  580 
  581     def get(self, pagename):
  582         '''Return text for a specific pagename'''
  583         for n, text in self._test_data:
  584             if n == pagename:
  585                 return text
  586         assert False, 'Could not find data for page: %s' % pagename
  587 
  588 
  589 WikiTestData = TestData('wiki') #: singleton to be used by various tests
  590 
  591 FULL_NOTEBOOK = WikiTestData
  592 
  593 
  594 def _expand_manifest(names):
  595     '''Build a set of all pages names and all namespaces that need to
  596     exist to host those page names.
  597     '''
  598     manifest = set()
  599     for name in names:
  600         manifest.add(name)
  601         while name.rfind(':') > 0:
  602             i = name.rfind(':')
  603             name = name[:i]
  604             manifest.add(name)
  605     return manifest
  606 
  607 def new_parsetree():
  608     '''Returns a new ParseTree object for testing
  609 
  610     Uses data from L{WikiTestData}, page C{roundtrip}
  611     '''
  612     import zim.formats.wiki
  613     parser = zim.formats.wiki.Parser()
  614     text = WikiTestData.get('roundtrip')
  615     tree = parser.parse(text)
  616     return tree
  617 
  618 def new_parsetree_from_text(text, format='wiki'):
  619     import zim.formats
  620     parser = zim.formats.get_format(format).Parser()
  621     return parser.parse(text)
  622 
  623 
  624 def new_parsetree_from_xml(xml):
  625     # For some reason this does not work with cElementTree.XMLBuilder ...
  626     from xml.etree.ElementTree import XMLParser
  627     from zim.formats import ParseTree
  628     builder = XMLParser()
  629     builder.feed(xml)
  630     root = builder.close()
  631     return ParseTree(root)
  632 
  633 
  634 def new_page():
  635     from zim.notebook import Path, Page
  636     from zim.newfs.mock import MockFile, MockFolder
  637     file = MockFile('/mock/test/page.txt')
  638     folder = MockFile('/mock/test/page/')
  639     page = Page(Path('roundtrip'), False, file, folder)
  640     page.set_parsetree(new_parsetree())
  641     return page
  642 
  643 
  644 def new_page_from_text(text, format='wiki'):
  645     from zim.notebook import Path, Page
  646     from zim.notebook import Path, Page
  647     from zim.newfs.mock import MockFile, MockFolder
  648     file = MockFile('/mock/test/page.txt')
  649     folder = MockFile('/mock/test/page/')
  650     page = Page(Path('Test'), False, file, folder)
  651     page.set_parsetree(new_parsetree_from_text(text, format))
  652     return page
  653 
  654 
  655 class Counter(object):
  656     '''Object that is callable as a function and keeps count how often
  657     it was called.
  658     '''
  659 
  660     def __init__(self, value=None):
  661         '''Constructor
  662         @param value: the value to return when called as a function
  663         '''
  664         self.value = value
  665         self.count = 0
  666 
  667     def __call__(self, *arg, **kwarg):
  668         self.count += 1
  669         return self.value
  670 
  671 
  672 class MockObjectBase(object):
  673     '''Base class for mock objects.
  674 
  675     Mock methods can be installed with L{mock_method()}. All method
  676     calls to mock methods are logged, so they can be inspected.
  677     The attribute C{mock_calls} has a list of tuples with mock methods
  678     and arguments in order they have been called.
  679     '''
  680 
  681     def __init__(self):
  682         self.mock_calls = []
  683 
  684     def mock_method(self, name, return_value):
  685         '''Installs a mock method with a given name that returns
  686         a given value.
  687         '''
  688         def my_mock_method(*arg, **kwarg):
  689             call = [name] + list(arg)
  690             if kwarg:
  691                 call.append(kwarg)
  692             self.mock_calls.append(tuple(call))
  693             return return_value
  694 
  695         setattr(self, name, my_mock_method)
  696         return my_mock_method
  697 
  698 
  699 class MockObject(MockObjectBase):
  700     '''Simple subclass of L{MockObjectBase} that automatically mocks a
  701     method which returns C{None} for any non-existing attribute.
  702     Attributes that are not methods need to be initialized explicitly.
  703     '''
  704 
  705     def __getattr__(self, name):
  706         '''Automatically mock methods'''
  707         if name == '__zim_extension_objects__':
  708             raise AttributeError
  709         else:
  710             return self.mock_method(name, None)
  711 
  712 
  713 import logging
  714 logger = logging.getLogger('tests')
  715 
  716 from functools import partial
  717 
  718 class SignalLogger(dict):
  719     '''Listening object that attaches to all signals of the target and records
  720     all signals calls in a dictionary of lists.
  721 
  722     Example usage:
  723 
  724         signals = SignalLogger(myobject)
  725         ... # some code causing signals to be emitted
  726         self.assertEqual(signals['mysignal'], [args])
  727             # assert "mysignal" is called once with "*args"
  728 
  729     If you don't want to match all arguments, the "filter_func" can be used to
  730     transform the arguments before logging.
  731 
  732         filter_func(signal_name, object, args) --> args
  733 
  734     '''
  735 
  736     def __init__(self, obj, filter_func=None):
  737         self._obj = obj
  738         self._ids = []
  739 
  740         if filter_func is None:
  741             filter_func = lambda s, o, a: a
  742 
  743         for signal in self._obj.__signals__:
  744             seen = []
  745             self[signal] = seen
  746 
  747             def handler(seen, signal, obj, *a):
  748                 seen.append(filter_func(signal, obj, a))
  749                 logger.debug('Signal: %s %r', signal, a)
  750 
  751             id = obj.connect(signal, partial(handler, seen, signal))
  752             self._ids.append(id)
  753 
  754     def __enter__(self):
  755         pass
  756 
  757     def __exit__(self, *e):
  758         self.disconnect()
  759 
  760     def clear(self):
  761         for signal, seen in list(self.items()):
  762             seen[:] = []
  763 
  764     def disconnect(self):
  765         for id in self._ids:
  766             self._obj.disconnect(id)
  767         self._ids = []
  768 
  769 
  770 
  771 class CallBackLogger(dict):
  772     '''Mock object that allows any method to be called as callback and
  773     records the calls in a dictionary.
  774     '''
  775 
  776     def __init__(self, filter_func=None):
  777         if filter_func is None:
  778             filter_func = lambda n, a, kw: (a, kw)
  779 
  780         self._filter_func = filter_func
  781 
  782     def __getattr__(self, name):
  783 
  784         def cb_method(*arg, **kwarg):
  785             logger.debug('Callback %s %r %r', name, arg, kwarg)
  786             self.setdefault(name, [])
  787 
  788             self[name].append(
  789                 self._filter_func(name, arg, kwarg)
  790             )
  791 
  792         setattr(self, name, cb_method)
  793         return cb_method
  794 
  795 
  796 class MaskedObject(object):
  797 
  798     def __init__(self, obj, *names):
  799         self.__obj = obj
  800         self.__names = names
  801 
  802     def setObjectAccess(self, *names):
  803         self.__names = names
  804 
  805     def __getattr__(self, name):
  806         if name in self.__names:
  807             return getattr(self.__obj, name)
  808         else:
  809             raise AttributeError('Acces to \'%s\' not allowed' % name)
  810 
  811 
  812 def gtk_process_events(*a):
  813     '''Method to simulate a few iterations of the gtk main loop'''
  814     assert Gtk is not None
  815     while Gtk.events_pending():
  816         Gtk.main_iteration()
  817     return True # continue
  818 
  819 
  820 def gtk_get_menu_item(menu, id):
  821     '''Get a menu item from a C{Gtk.Menu}
  822     @param menu: a C{Gtk.Menu}
  823     @param id: either the menu item label or the stock id
  824     @returns: a C{Gtk.MenuItem} or C{None}
  825     '''
  826     items = menu.get_children()
  827     ids = [i.get_property('label') for i in items]
  828         # Gtk.ImageMenuItems that have a stock id happen to use the
  829         # 'label' property to store it...
  830 
  831     assert id in ids, \
  832         'Menu item "%s" not found, we got:\n' % id \
  833         + ''.join('- %s \n' % i for i in ids)
  834 
  835     i = ids.index(id)
  836     return items[i]
  837 
  838 
  839 def gtk_activate_menu_item(menu, id):
  840     '''Trigger the 'click' action an a menu item
  841     @param menu: a C{Gtk.Menu}
  842     @param id: either the menu item label or the stock id
  843     '''
  844     item = gtk_get_menu_item(menu, id)
  845     item.activate()