| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ |
|---|
| 3 | Zine Test Suite |
|---|
| 4 | ~~~~~~~~~~~~~~~ |
|---|
| 5 | |
|---|
| 6 | This is the Zine test suite. It collects all modules in the zine |
|---|
| 7 | package, builds a TestSuite with their doctests and executes them. It also |
|---|
| 8 | collects the tests from the text files in this directory (which are too |
|---|
| 9 | extensive to put them into the code without cluttering it up). |
|---|
| 10 | |
|---|
| 11 | Please note that coverage reporting and doctest don't play well together |
|---|
| 12 | and your reports will probably miss some of the executed code. Doctest can |
|---|
| 13 | be patched to remove this incompatibility, the patch is at |
|---|
| 14 | http://tinyurl.com/doctest-patch |
|---|
| 15 | |
|---|
| 16 | :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details. |
|---|
| 17 | :license: BSD, see LICENSE for more details. |
|---|
| 18 | """ |
|---|
| 19 | |
|---|
| 20 | import sys |
|---|
| 21 | import os |
|---|
| 22 | from os.path import join, dirname |
|---|
| 23 | from unittest import TestSuite, TextTestRunner |
|---|
| 24 | from doctest import DocTestSuite, DocFileSuite |
|---|
| 25 | |
|---|
| 26 | #: the modules in this list are not tested in a full run |
|---|
| 27 | untested = ['zine.broken_plugins.hyphenation_en', |
|---|
| 28 | 'zine.broken_plugins.hyphenation_en.hyphenate', |
|---|
| 29 | 'zine.broken_plugins.notification'] |
|---|
| 30 | |
|---|
| 31 | try: |
|---|
| 32 | import coverage |
|---|
| 33 | except ImportError: |
|---|
| 34 | coverage = None |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | def suite(modnames=[], return_covermods=False): |
|---|
| 38 | """Generate the test suite. |
|---|
| 39 | |
|---|
| 40 | The first argument is a list of modules to be tested. If it is empty (which |
|---|
| 41 | it is by default), all sub-modules of the zine package are tested. |
|---|
| 42 | If the second argument is True, this function returns two objects: a |
|---|
| 43 | TestSuite instance and a list of the names of the tested modules. Otherwise |
|---|
| 44 | (which is the default) it only returns the former. This is done so that |
|---|
| 45 | this function can be used as setuptools' test_suite. |
|---|
| 46 | """ |
|---|
| 47 | |
|---|
| 48 | # the app object is used for two purposes: |
|---|
| 49 | # 1) plugins are not usable (i.e. not testable) without an initialised app |
|---|
| 50 | # 2) for functions that require an application object as argument, you can |
|---|
| 51 | # write >>> my_function(app, ...) in the tests |
|---|
| 52 | # The instance directory of this object is located in the tests directory. |
|---|
| 53 | # |
|---|
| 54 | # setup isn't imported at module level because this way coverage |
|---|
| 55 | # can track the whole zine imports |
|---|
| 56 | from zine import setup |
|---|
| 57 | instance_path = join(dirname(__file__), 'instance') |
|---|
| 58 | app = setup(instance_path) |
|---|
| 59 | |
|---|
| 60 | if return_covermods: |
|---|
| 61 | covermods = [] |
|---|
| 62 | suite = TestSuite() |
|---|
| 63 | |
|---|
| 64 | if modnames == []: |
|---|
| 65 | modnames = find_tp_modules() |
|---|
| 66 | test_files = os.listdir(dirname(__file__)) |
|---|
| 67 | for modname in modnames: |
|---|
| 68 | if modname in untested: |
|---|
| 69 | continue |
|---|
| 70 | |
|---|
| 71 | # the fromlist must contain something, otherwise the zine |
|---|
| 72 | # package is returned, not our module |
|---|
| 73 | try: |
|---|
| 74 | mod = __import__(modname, None, None, ['']) |
|---|
| 75 | except ImportError: |
|---|
| 76 | # some plugins can have external dependencies (e.g. creoleparser, |
|---|
| 77 | # pygments) that are not installed on the machine the tests are |
|---|
| 78 | # run on. Therefore, just skip those (with an error message) |
|---|
| 79 | if 'plugins.' in modname: |
|---|
| 80 | sys.stderr.write('could not import plugin %s\n' % modname) |
|---|
| 81 | continue |
|---|
| 82 | else: |
|---|
| 83 | raise |
|---|
| 84 | |
|---|
| 85 | suites = [DocTestSuite(mod, extraglobs={'app': app})] |
|---|
| 86 | filename = modname[5:] + '.txt' |
|---|
| 87 | if filename in test_files: |
|---|
| 88 | globs = {'app': app} |
|---|
| 89 | globs.update(mod.__dict__) |
|---|
| 90 | suites.append(DocFileSuite(filename, globs=globs)) |
|---|
| 91 | for i, subsuite in enumerate(suites): |
|---|
| 92 | # skip modules without any tests |
|---|
| 93 | if subsuite.countTestCases(): |
|---|
| 94 | suite.addTest(subsuite) |
|---|
| 95 | if return_covermods and i == 0: |
|---|
| 96 | covermods.append(mod) |
|---|
| 97 | if return_covermods: |
|---|
| 98 | return suite, covermods |
|---|
| 99 | else: |
|---|
| 100 | return suite |
|---|
| 101 | |
|---|
| 102 | |
|---|
| 103 | def find_tp_modules(): |
|---|
| 104 | """Find all sub-modules of the zine package.""" |
|---|
| 105 | modules = [] |
|---|
| 106 | import zine |
|---|
| 107 | base = dirname(zine.__file__) |
|---|
| 108 | start = len(dirname(base)) |
|---|
| 109 | if base != 'zine': |
|---|
| 110 | start += 1 |
|---|
| 111 | |
|---|
| 112 | for path, dirnames, filenames in os.walk(base): |
|---|
| 113 | for filename in filenames: |
|---|
| 114 | if filename.endswith('.py'): |
|---|
| 115 | fullpath = join(path, filename) |
|---|
| 116 | if filename == '__init__.py': |
|---|
| 117 | stripped = fullpath[start:-12] |
|---|
| 118 | else: |
|---|
| 119 | stripped = fullpath[start:-3] |
|---|
| 120 | |
|---|
| 121 | modname = stripped.replace('/', '.') |
|---|
| 122 | modules.append(modname) |
|---|
| 123 | return modules |
|---|
| 124 | |
|---|
| 125 | |
|---|
| 126 | def main(): |
|---|
| 127 | from optparse import OptionParser |
|---|
| 128 | usage = ('Usage: %prog [option] [modules to be tested]\n' |
|---|
| 129 | 'Modules names have to be given in the form utils.mail (without ' |
|---|
| 130 | 'zine.)\nIf no module names are given, all tests are run') |
|---|
| 131 | parser = OptionParser(usage=usage) |
|---|
| 132 | parser.add_option('-c', '--coverage', action='store_true', dest='coverage', |
|---|
| 133 | help='show coverage information (slow!)') |
|---|
| 134 | parser.add_option('-v', '--verbose', action='store_true', dest='verbose', |
|---|
| 135 | default=False, help='show which tests are run') |
|---|
| 136 | |
|---|
| 137 | options, args = parser.parse_args(sys.argv[1:]) |
|---|
| 138 | modnames = ['zine.' + modname for modname in args] |
|---|
| 139 | if options.coverage: |
|---|
| 140 | if coverage is not None: |
|---|
| 141 | use_coverage = True |
|---|
| 142 | else: |
|---|
| 143 | sys.stderr.write("coverage information requires Ned Batchelder's " |
|---|
| 144 | "coverage.py to be installed!\n") |
|---|
| 145 | sys.exit(1) |
|---|
| 146 | else: |
|---|
| 147 | use_coverage = False |
|---|
| 148 | |
|---|
| 149 | if use_coverage: |
|---|
| 150 | coverage.erase() |
|---|
| 151 | coverage.start() |
|---|
| 152 | s, covermods = suite(modnames, True) |
|---|
| 153 | else: |
|---|
| 154 | s = suite(modnames) |
|---|
| 155 | TextTestRunner(verbosity=options.verbose + 1).run(s) |
|---|
| 156 | if use_coverage: |
|---|
| 157 | coverage.stop() |
|---|
| 158 | print '\n\n' + '=' * 25 + ' coverage information ' + '=' * 25 |
|---|
| 159 | coverage.report(covermods) |
|---|
| 160 | |
|---|
| 161 | |
|---|
| 162 | if __name__ == '__main__': |
|---|
| 163 | main() |
|---|