Fork me on GitHub

Source code for attest.collectors

# coding:utf-8
from __future__ import with_statement

import inspect
import sys

from contextlib import contextmanager
from functools  import wraps

from attest           import statistics
from attest.contexts  import capture_output
from attest.reporters import auto_reporter, AbstractReporter, TestResult
from attest.utils     import import_dotted_name, deep_get_members, nested


__all__ = (
    'Tests',
    'test_if',
    'test',
    'TestBase',
)


[docs]class Tests(object): """Collection of test functions. :param tests: String, or iterable of values, suitable as argument(s) to :meth:`register`. :param contexts: Iterable of callables that take no arguments and return a context manager. :param replace_tests: If true, :meth:`test` returns the wrapper function rather than the original. This option is available for backwards-compatibility. :param replace_contexts: If true, :meth:`context` returns the context manager rather than the original generator function. Provided for backwards-compatibility. .. versionadded:: 0.6 Pass a single string to `tests` without wrapping it in an iterable. .. versionadded:: 0.6 The `replace_tests` and `replace_contexts` parameters. The decorator methods now default to simply registering functions and leaving the original in place. This allows functions to be decorated and registered with multiple collections easily. """ def __init__(self, tests=(), contexts=None, replace_tests=False, replace_contexts=False): self._tests = [] if isinstance(tests, basestring): self.register(tests) else: for collection in tests: self.register(collection) self._contexts = [] if contexts is not None: self._contexts.extend(contexts) self.replace_tests = replace_tests self.replace_contexts = replace_contexts def __iter__(self): return iter(self._tests) def __len__(self): return len(self._tests)
[docs] def test_if(self, condition): """Returns :meth:`test` if the `condition` is ``True``. .. versionadded:: 0.4 """ if condition: return self.test return lambda x: x
[docs] def test(self, func): """Decorate a function as a test belonging to this collection.""" @wraps(func) def wrapper(): with nested(self._contexts) as context: context = [c for c in context if c is not None] argc = len(inspect.getargspec(func)[0]) args = [] for arg in context: if type(arg) is tuple: # type() is intentional args.extend(arg) else: args.append(arg) func(*args[:argc]) wrapper.__wrapped__ = func self._tests.append(wrapper) if self.replace_tests: return wrapper return func
[docs] def context(self, func): """Decorate a function as a :func:`~contextlib.contextmanager` for running the tests in this collection in. Corresponds to setup and teardown in other testing libraries. :: db = Tests() @db.context def connect(): con = connect_db() try: yield con finally: con.disconnect() @db.test def using_connection(con): assert con is not None The above corresponds to:: db = Tests() @contextmanager def connect(): con = connect_db() try: yield con finally: con.disconnect() @db.test def using_connection(): with connect() as con: assert con is not None The difference is that this decorator applies the context to all tests defined in its collection, so it's less repetitive. Yielding :const:`None` or nothing passes no arguments to the test, yielding a single value other than a tuple passes that value as the sole argument to the test, yielding a tuple splats the tuple as the arguments to the test. If you want to yield a tuple as the sole argument, wrap it in a one-tuple or unsplat the args in the test. You can have more than one context, which will be run in order using :func:`contextlib.nested`, and their yields will be passed in order to the test functions. .. versionadded:: 0.2 Nested contexts. .. versionchanged:: 0.5 Tests will gets as many arguments as they ask for. """ context = contextmanager(func) context.__wrapped__ = func self._contexts.append(context) if self.replace_contexts: return context return func
[docs] def register_if(self, condition): """Returns :meth:`register` if the `condition` is ``True``. .. versionadded:: 0.4 """ if condition: return self.register return lambda x: x
[docs] def register(self, tests): """Merge in other tests. :param tests: * A class, which is then instantiated and return allowing it to be used as a decorator for :class:`TestBase` classes. * A string, representing the dotted name to one of: * a module or package, which is recursively scanned for :class:`Tests` instances that are not private * an iterable yielding tests * Otherwise any iterable object is assumed to yield tests. Any of these can be passed in a list to the :class:`Tests` constructor. .. versionadded:: 0.2 Refer to collections by import path as a string .. versionadded:: 0.6 Recursive scanning of modules and packages .. versionchanged:: 0.6 Tests are only added if not already added """ if inspect.isclass(tests): self._tests.extend(tests()) return tests elif isinstance(tests, basestring): def istests(obj): return isinstance(obj, Tests) obj = import_dotted_name(tests) if inspect.ismodule(obj): for tests in deep_get_members(tests, istests): self.register(tests) return tests = obj for test in tests: if not test in self._tests: self._tests.append(test)
[docs] def test_suite(self): """Create a :class:`unittest.TestSuite` from this collection.""" from unittest import TestSuite, FunctionTestCase suite = TestSuite() for test in self: suite.addTest(FunctionTestCase(test)) return suite
[docs] def run(self, reporter=auto_reporter, full_tracebacks=False, fail_fast=False, debugger=False): """Run all tests in this collection. :param reporter: An instance of :class:`~attest.reporters.AbstractReporter` or a callable returning something implementing that API (not enforced). :param full_tracebacks: Control if the call stack of Attest is hidden in tracebacks. :param fail_fast: Stop after the first failure. :param debugger: Enter PDB when tests fail. .. versionchanged:: 0.6 Added `full_tracebacks` and `fail_fast`. """ assertions, statistics.assertions = statistics.assertions, 0 if not isinstance(reporter, AbstractReporter): reporter = reporter() reporter.begin(self._tests) for test in self: result = TestResult(test=test, full_tracebacks=full_tracebacks, debugger=debugger) try: with capture_output() as (out, err): if test() is False: raise AssertionError('test() is False') except BaseException, e: if isinstance(e, KeyboardInterrupt): break result.error = e result.stdout, result.stderr = out, err result.exc_info = sys.exc_info() reporter.failure(result) if fail_fast: break else: result.stdout, result.stderr = out, err reporter.success(result) try: reporter.finished() finally: statistics.assertions = assertions
[docs] def main(self): """Interface to :meth:`run` with command-line options. ``-h``, ``--help`` Show a help message ``-r NAME``, ``--reporter NAME`` Select reporter by name with :func:`~attest.reporters.get_reporter_by_name` ``--full-tracebacks`` Show complete tracebacks without hiding Attest's own call stack ``-l``, ``--list-reporters`` List the names of all installed reporters Remaining arguments are passed to the reporter. .. versionadded:: 0.2 .. versionchanged:: 0.4 ``--list-reporters`` was added. .. versionchanged:: 0.6 ``--full-tracebacks`` was added. """ from attest.run import main main(self)
[docs]def test_if(condition): """Returns :func:`test` if the `condition` is ``True``. .. versionadded:: 0.4 """ if condition: return test return lambda x: x
[docs]def test(meth): """Mark a :class:`TestBase` method as a test and wrap it to run in the :meth:`TestBase.__context__` of the subclass. """ @wraps(meth) def wrapper(self): with contextmanager(self.__context__)(): meth(self) wrapper.__test__ = True return wrapper
[docs]class TestBase(object): """Base for test classes. Decorate test methods with :func:`test`. Needs to be registered with a :class:`Tests` collection to be run. For setup and teardown, override :meth:`__context__` like a :func:`~contextlib.contextmanager` (without the decorator). :: class Math(TestBase): def __context__(self): self.two = 1 + 1 yield del self.two @test def arithmetics(self): assert self.two == 2 suite = Tests([Math()]) suite.run() """ def __context__(self): yield def __iter__(self): for name in dir(self): attr = getattr(self, name) if getattr(attr, '__test__', False) and callable(attr): yield attr