Source code for pyquchk.checker

import sys
from warnings import warn
import six
from .arbitraries.arbitrary import get_arbitrary
from .arbitraries.sequences import dict_fixkeys


__all__ = ['for_all', 'Result', 'ReturnedFalse', 'CheckError',
           'AssumptionError', 'assume']


class AssumptionError(Exception):
    pass


def assume(condition):
    if not condition:
        raise AssumptionError


[docs]class CheckError(Exception): """ Raised when a test which uses :func:`.qc` decorator fails. It contains the original test data for which the test has failed, and the exception raised by the tested function. Actually, a :exc:`CheckError` instance is always also an instance of the exception raised by the tested function, so you can except it as usually. """ #: Data which caused the function to raise an exception test_data = None #: Exception raised by the function, or :exc:`ReturnedFalse` if function #: returned ``False`` cause = None
def get_CheckError(data, noshrink_data, cause): class CheckError_(CheckError, type(cause)): def __init__(self, data, noshrink_data, cause): self.test_data = data self.noshrink_data = noshrink_data self.cause = cause def __str__(self): msg = "test failed" if self.cause is not None: msg += " due to %s" % type(self.cause).__name__ cause = str(self.cause) if cause: msg += ": " + cause res = [msg] res.append('') res.append("Failure encountered for data:") res += [" %s = %r" % i for i in sorted(self.test_data.items())] res.append('') res.append("Before shrinking:") res += [" %s = %r" % i for i in sorted(self.noshrink_data.items())] return '\n'.join(res) CheckError_.__name__ = 'CheckError' return CheckError_(data, noshrink_data, cause)
[docs]class ReturnedFalse(Exception): """ Automatically raised when the tested function returns a value which evaluates to a :mod:`bool` ``False``. """ def __init__(self, retval): #: Exact value returned by the function (evaluates to a ``False`` :mod:`bool` value) self.retval = retval def __str__(self): return 'function returned %r' % self.retval
[docs]class Result(object): """ Represent the result of performed testing (returned by :func:`for_all`). """ def __init__(self, verdict, passed, datas, data=None, noshrink_data=None, exception=None, traceback=None): #: ``'OK'`` if passed and ``'FAIL'`` if not self.verdict = verdict #: ``True`` if passed and ``False`` if not (opposite to :attr:`failed`) self.passed = passed #: ``False`` if passed and ``True`` if not (opposite to :attr:`passed`) self.failed = not passed #: List of all the data values generated during the test run self.datas = datas #: Data on which the tested function failed (as a :mod:`dict`), #: or ``None`` if passed self.data = data #: Same as :attr:`data`, but before shrinking. self.noshrink_data = noshrink_data #: Exception raised by the function, or :exc:`ReturnedFalse` if function #: returned ``False``, or ``None`` if passed self.exception = exception #: Exception traceback, or ``None`` if passed self.traceback = traceback @classmethod def ok(cls, datas): return Result('OK', True, datas) @classmethod def fail(cls, datas, data, noshrink_data, exception, traceback): return Result('FAIL', False, datas, data, noshrink_data, exception, traceback) def reraise(self): exception = get_CheckError(self.data, self.noshrink_data, self.exception) six.reraise(type(exception), exception, self.traceback)
[docs] def __bool__(self): """ Return :attr:`passed`, used to interprete as a :mod:`bool` value. """ return self.passed
__nonzero__ = __bool__
class Checker(object): def __init__(self, func, args, ntests, nshrinks, nassume, ignore_return=False): self.func = func self.args = args self.ntests = ntests or 500 self.nshrinks = nshrinks or 10000 self.nassume = nassume or 10000 self.ignore_return = ignore_return arg_arbs = {argname: get_arbitrary(self.args[argname]) for argname in self.args} self.arb = dict_fixkeys(**arg_arbs) def eval_func(self, data): func_res = self.func(**data) if not self.ignore_return and not func_res: raise ReturnedFalse(func_res) def full_shrink(self, kwargs): n = 0 while n < self.nshrinks: for shr_kwargs in self.arb.shrink(kwargs): n += 1 try: self.eval_func(shr_kwargs) except AssumptionError: continue except Exception as e: kwargs = shr_kwargs break if n > self.nshrinks: break else: break else: warn('Shrink did not stop after %d iterations.' % self.nshrinks) return kwargs def for_all(self): gen_serial = self.arb.gen_serial() def generate(n): if n < self.ntests / 2: try: return next(gen_serial) except StopIteration: return self.arb.next_random() else: return self.arb.next_random() datas = [] n = 0 assume_fail_cnt = 0 while n < self.ntests: kwargs = generate(n) datas.append(kwargs) try: self.eval_func(kwargs) except AssumptionError: del datas[-1] assume_fail_cnt += 1 if assume_fail_cnt > self.nassume: raise RuntimeError( 'Could not satisfy assume() after %d of test cases.' % self.nassume) continue except Exception as e: shrinked_kwargs = self.full_shrink(kwargs) try: self.eval_func(shrinked_kwargs) except Exception as e: _, _, traceback = sys.exc_info() return Result.fail(datas, shrinked_kwargs, kwargs, e, traceback) n += 1 return Result.ok(datas)
[docs]def for_all(func, **kwargs): """ for_all(func, ntests=None, nshrinks=None, nassume=None) Test the function ``func`` on ``_ntests`` tests. For usage examples see :ref:`property-checking`. :param kwargs: function arguments specification :returns: information about the tests run :rtype: :class:`Result` """ ntests = kwargs.get('ntests', None) nshrinks = kwargs.get('nshrinks', None) nassume = kwargs.get('nassume', None) return Checker(func, kwargs, ntests, nshrinks, nassume).for_all()