# -*- coding: utf-8 -*- # # Copyright (C) <2010-2017> Gabriel Falcão # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from __future__ import unicode_literals import re import os import sys import difflib import inspect import traceback from functools import wraps, partial from datetime import datetime from six import string_types, text_type, PY2, get_function_code from six.moves import reduce from sure.old import AssertionHelper from sure.old import Iterable from sure.old import builtins from sure.core import DeepComparison from sure.core import DeepExplanation from sure.core import _get_file_name from sure.core import _get_line_number from sure.core import safe_repr from sure.core import anything # noqa from sure.magic import is_cpython, patchable_builtin from sure.registry import context as _registry if not PY2: basestring = str version = '1.4.11' not_here_error = \ 'you have tried to access the attribute %r from the context ' \ '(aka VariablesBag), but there is no such attribute assigned to it. ' \ 'Maybe you misspelled it ? Well, here are the options: %s' original_obj_attrs = dir(object) class VariablesBag(dict): __varnames__ = None __sure_actions_ran__ = None __sure_action_results__ = None __sure_providers_of__ = None def __init__(self, *args, **kw): self.__varnames__ = [] self.__sure_actions_ran__ = [] self.__sure_action_results__ = [] self.__sure_providers_of__ = {} return super(VariablesBag, self).__init__(*args, **kw) def __setattr__(self, attr, value): if attr not in dir(VariablesBag): self[attr] = value self.__varnames__.append(attr) return super(VariablesBag, self).__setattr__(attr, value) def __getattr__(self, attr): try: return super(VariablesBag, self).__getattribute__(attr) except AttributeError: if attr not in dir(VariablesBag): raise AssertionError(not_here_error % ( attr, safe_repr(self.__varnames__), )) def ensure_type(caller_name, cast, obj): try: return cast(obj) except TypeError: raise AssertionError('{0} tried to ') class CallBack(object): context_error = "the function %s defined at %s line %d, is being "\ "decorated by either @that_with_context or @scenario, so it should " \ "take at least 1 parameter, which is the test context" def __init__(self, cb, args, kwargs): self.callback = cb self.args = args or [] self.kwargs = kwargs or {} self.callback_name = cb.__name__ self.callback_filename = os.path.split(get_function_code(cb).co_filename)[-1] self.callback_lineno = get_function_code(cb).co_firstlineno + 1 def apply(self, *optional_args): args = list(optional_args) args.extend(self.args) try: return self.callback(*args, **self.kwargs) except Exception: exc_klass, exc_value, tb = sys.exc_info() err = traceback.format_exc().splitlines()[-1] err = err.replace('{0}:'.format(exc_klass.__name__), '').strip() if err.startswith(self.callback_name) and \ ('takes no arguments (1 given)' in err or 'takes 0 positional arguments but 1 was given' in err): raise TypeError(self.context_error % ( self.callback_name, self.callback_filename, self.callback_lineno, ) ) raise def that_with_context(setup=None, teardown=None): def dec(func): @wraps(func) def wrap(*args, **kw): context = VariablesBag() if callable(setup): cb = CallBack(setup, args, kw) cb.apply(context) elif isinstance(setup, Iterable): for s in setup: cb = CallBack(s, args, kw) cb.apply(context) test = CallBack(func, args, kw) try: res = test.apply(context) finally: if callable(teardown): cb = CallBack(teardown, args, kw) cb.apply(context) elif isinstance(teardown, Iterable): for s in teardown: cb = CallBack(s, args, kw) cb.apply(context) return res return wrap return dec scenario = that_with_context def within(**units): assert len(units) == 1, 'use within(number=unit). e.g.: within(one=second)' word, unit = list(units.items())[0] value = word_to_number(word) convert_from, convert_to = UNITS[unit] timeout = convert_from(value) exc = [] def dec(func): def wrap(*args, **kw): start = datetime.utcnow() try: func(start, *args, **kw) except TypeError as e: if PY2: # PY2 has different error message fmt = '{0}() takes no arguments' else: fmt = '{0}() takes 0 positional arguments but 1 was given' err = text_type(e) if fmt.format(func.__name__) in err: func(*args, **kw) else: exc.append(traceback.format_exc()) except Exception as e: exc.append(traceback.format_exc()) end = datetime.utcnow() delta = (end - start) took = convert_to(delta.microseconds) print(took, timeout) assert took < timeout, \ '%s did not run within %s %s' % (func.__name__, word, unit) if exc: raise AssertionError(exc.pop(0)) wrap.__name__ = func.__name__ wrap.__doc__ = func.__doc__ wrap.__dict__ = func.__dict__ return wrap return dec UNITS = { 'minutes': ( lambda from_num: from_num / 60.0, lambda to_num: to_num * 6000000, ), 'seconds': ( lambda from_num: from_num, lambda to_num: to_num / 100000, ), 'miliseconds': ( lambda from_num: from_num * 1000, lambda to_num: to_num / 100, ), 'microseconds': ( lambda from_num: from_num * 100000, lambda to_num: to_num, ), } milisecond = miliseconds = 'miliseconds' microsecond = microseconds = 'microseconds' second = seconds = 'seconds' minute = minutes = 'minutes' def word_to_number(word): basic = { 'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10, 'eleven': 11, 'twelve': 12, } try: return basic[word] except KeyError: raise AssertionError( 'sure supports only literal numbers from one to twelve, ' \ 'you tried the word "twenty"') def action_for(context, provides=None, depends_on=None): if not provides: provides = [] if not depends_on: depends_on = [] def register_providers(func, attr): if re.search(r'^[{]\d+[}]$', attr): return # ignore dinamically declared provides if not attr in context.__sure_providers_of__: context.__sure_providers_of__[attr] = [] context.__sure_providers_of__[attr].append(func) def register_dinamic_providers(func, attr, args, kwargs): found = re.search(r'^[{](\d+)[}]$', attr) if not found: return # ignore dinamically declared provides index = int(found.group(1)) assert index < len(args), \ 'the dinamic provider index: {%d} is bigger than %d, which is ' \ 'the length of the positional arguments passed to %s' % ( index, len(args), func.__name__) attr = args[index] if not attr in context.__sure_providers_of__: context.__sure_providers_of__[attr] = [] context.__sure_providers_of__[attr].append(func) def ensure_providers(func, attr, args, kwargs): found = re.search(r'^[{](\d+)[}]$', attr) if found: index = int(found.group(1)) attr = args[index] assert attr in context, \ 'the action "%s" was supposed to provide the attribute "%s" ' \ 'into the context, but it did not. Please double check its ' \ 'implementation' % (func.__name__, attr) dependency_error_lonely = 'the action "%s" defined at %s:%d ' \ 'depends on the attribute "%s" to be available in the' \ ' context. It turns out that there are no actions providing ' \ 'that. Please double-check the implementation' dependency_error_hints = 'the action "%s" defined at %s:%d ' \ 'depends on the attribute "%s" to be available in the context.'\ ' You need to call one of the following actions beforehand:\n' def check_dependencies(func): action = func.__name__ filename = _get_file_name(func) lineno = _get_line_number(func) for dependency in depends_on: if dependency in context.__sure_providers_of__: providers = context.__sure_providers_of__[dependency] err = dependency_error_hints % ( action, filename, lineno, dependency, ) err += '\n'.join([ ' -> %s at %s:%d' % ( p.__name__, _get_file_name(p), _get_line_number(p)) for p in providers]) else: err = dependency_error_lonely % ( action, filename, lineno, dependency, ) assert dependency in context, err def decorate_and_absorb(func): [register_providers(func, attr) for attr in provides] @wraps(func) def wrapper(*args, **kw): [register_dinamic_providers(func, attr, args, kw) for attr in provides] context.__sure_actions_ran__.append((func, args, kw)) check_dependencies(func) result = func(*args, **kw) [ensure_providers(func, attr, args, kw) for attr in provides] context.__sure_action_results__.append(result) return context setattr(context, func.__name__, wrapper) return wrapper return decorate_and_absorb def work_in_progress(func): @wraps(func) def wrapper(*args, **kwargs): _registry['is_running'] = True ret = func(*args, **kwargs) _registry['is_running'] = False return ret return wrapper def assertionmethod(func): @wraps(func) def wrapper(self, *args, **kw): try: value = func(self, *args, **kw) except AssertionError as e: raise AssertionError(e) msg = "{0}({1}) failed".format( func.__name__, ", ".join(map(safe_repr, args)), ", ".join(["{0}={1}".format(k, safe_repr(kw[k])) for k in kw]), ) if PY2: msg = text_type(msg) assert value, msg return value return wrapper def assertionproperty(func): return builtins.property(assertionmethod(func)) POSITIVES = [ 'should', 'does', 'do', 'must', 'when', ] NEGATIVES = [ 'shouldnt', 'dont', 'do_not', 'doesnt', 'does_not', 'doesnot', 'should_not', 'shouldnot', ] class IdentityAssertion(object): def __init__(self, assertion_builder): self._ab = assertion_builder def __call__(self, other): if self._ab.negative: assert self._ab.obj is not other, "{0} should not be the same object as {1}, but it is".format(self._ab.obj, other) return True assert self._ab.obj is other, "{0} should be the same object as {1}, but it is not".format(self._ab.obj, other) return True def __getattr__(self, name): return getattr(self._ab, name) class AssertionBuilder(object): def __init__(self, name=None, negative=False, obj=None, callable_args=None, callable_kw=None): self._name = name self.negative = negative self.obj = obj self._callable_args = callable_args or [] self._callable_kw = callable_kw or {} self._that = AssertionHelper(self.obj) def __call__(self, obj): self.obj = obj if isinstance(obj, self.__class__): self.obj = obj.obj self._callable_args = obj._callable_args self._callable_kw = obj._callable_kw self._that = AssertionHelper(self.obj) return self def __getattr__(self, attr): special_case = False special_case = attr in (POSITIVES + NEGATIVES) negative = attr in NEGATIVES if special_case: return AssertionBuilder(attr, negative=negative, obj=self.obj, callable_args=self._callable_args, callable_kw=self._callable_kw) return super(AssertionBuilder, self).__getattribute__(attr) @assertionproperty def callable(self): if self.negative: assert not callable(self.obj), ( 'expected `{0}` to not be callable but it is'.format(safe_repr(self.obj))) else: assert callable(self.obj), ( 'expected {0} to be callable'.format(safe_repr(self.obj))) return True @assertionproperty def be(self): return IdentityAssertion(self) being = be @assertionproperty def not_be(self): return IdentityAssertion(self.should_not) not_being = not_be @assertionproperty def not_have(self): return self.should_not @assertionproperty def to_not(self): return self.should_not @assertionproperty def to(self): return self which = to @assertionproperty def when(self): return self @assertionproperty def have(self): return self @assertionproperty def with_value(self): return self def property(self, name): has_it = hasattr(self.obj, name) if self.negative: assert not has_it, ( '%r should not have the property `%s`, ' 'but it is %r' % (self.obj, name, getattr(self.obj, name))) return True assert has_it, ( "%r should have the property `%s` but does not" % ( self.obj, name)) return expect(getattr(self.obj, name)) def key(self, name): has_it = name in self.obj if self.negative: assert not has_it, ( '%r should not have the key `%s`, ' 'but it is %r' % (self.obj, name, self.obj[name])) return True assert has_it, ( "%r should have the key `%s` but does not" % ( self.obj, name)) return expect(self.obj[name]) @assertionproperty def empty(self): representation = safe_repr(self.obj) length = len(self.obj) if self.negative: assert length > 0, ( "expected `{0}` to not be empty".format(representation)) else: assert length == 0, ( "expected `{0}` to be empty but it has {1} items".format(representation, length)) return True @assertionproperty def ok(self): if self.negative: msg = 'expected `{0}` to be falsy'.format(self.obj) assert not bool(self.obj), msg else: msg = 'expected `{0}` to be truthy'.format(self.obj) assert bool(self.obj), msg return True truthy = ok true = ok @assertionproperty def falsy(self): if self.negative: msg = 'expected `{0}` to be truthy'.format(self.obj) assert bool(self.obj), msg else: msg = 'expected `{0}` to be falsy'.format(self.obj) assert not bool(self.obj), msg return True false = falsy @assertionproperty def none(self): if self.negative: assert self.obj is not None, ( r"expected `{0}` to not be None".format(self.obj)) else: assert self.obj is None, ( r"expected `{0}` to be None".format(self.obj)) return True @assertionmethod def within_range(self, start, end): start = ensure_type('within_range', int, start) end = ensure_type('within_range', int, end) subject = ensure_type('within_range', int, self.obj) is_within_range = subject >= start and subject <= end if self.negative: if is_within_range: raise AssertionError('expected {0} to NOT be within {1} and {2}'.format(subject, start, end)) return not is_within_range else: if not is_within_range: raise AssertionError('expected {0} to be within {1} and {2}'.format(subject, start, end)) return is_within_range @assertionmethod def within(self, first, *rest): if isinstance(first, Iterable): collection_should = AssertionHelper(first) if self.negative: return collection_should.does_not_contain(self.obj) else: return collection_should.contains(self.obj) elif len(rest) == 1: return self.within_range(first, rest[0]) else: if self.negative: ppath = '{0}.should_not.be.within'.format(self.obj) else: ppath = '{0}.should.be.within'.format(self.obj) raise AssertionError(( '{0}({1}, {2}) must be called with either a iterable:\n' '{0}([1, 2, 3, 4])\n' 'or with a range of numbers:' '{0}(1, 3000)' ).format(ppath, first, ", ".join([repr(x) for x in rest]))) @assertionmethod def equal(self, what, epsilon=None): """compares given object ``X`` with an expected ``Y`` object. It primarily assures that the compared objects are absolute equal ``==``. :param what: the expected value :param epsilon: a delta to leverage upper-bound floating point permissiveness """ try: comparison = DeepComparison(self.obj, what, epsilon).compare() error = False except AssertionError as e: error = e comparison = None if isinstance(comparison, DeepExplanation): error = comparison.get_assertion(self.obj, what) if self.negative: if error: return True msg = '%s should differ from %s, but is the same thing' raise AssertionError(msg % (safe_repr(self.obj), safe_repr(what))) else: if not error: return True raise error eql = equal equals = equal equal_to = equal @assertionmethod def different_of(self, what): differ = difflib.Differ() source = self.obj.strip().splitlines(True) destination = what.strip().splitlines(True) result = differ.compare(source, destination) difference = "".join(result) if self.negative: if self.obj != what: assert not difference, "Difference:\n\n{0}".format(difference) else: if self.obj == what: raise AssertionError("{0} should be different of {1}".format(self.obj, what)) return True @assertionmethod def an(self, klass): if isinstance(klass, type): class_name = klass.__name__ elif isinstance(klass, string_types): class_name = klass.strip() else: class_name = text_type(klass) is_vowel = class_name[0] in 'aeiou' if isinstance(klass, string_types): if '.' in klass: items = klass.split('.') first = items.pop(0) if not items: items = [first] first = '_abcoll' else: if sys.version_info <= (3, 0, 0): first = '__builtin__' else: first = 'builtins' items = [klass] klass = reduce(getattr, items, __import__(first)) suffix = is_vowel and "n" or "" if self.negative: assert not isinstance(self.obj, klass), ( 'expected `{0}` to not be a{1} {2}'.format( self.obj, suffix, class_name)) else: assert isinstance(self.obj, klass), ( 'expected `{0}` to be a{1} {2}'.format( self.obj, suffix, class_name)) return True a = an @assertionmethod def greater_than(self, dest): if self.negative: msg = "expected `{0}` to not be greater than `{1}`".format( self.obj, dest) assert not self.obj > dest, msg else: msg = "expected `{0}` to be greater than `{1}`".format( self.obj, dest) assert self.obj > dest, msg return True @assertionmethod def greater_than_or_equal_to(self, dest): if self.negative: msg = "expected `{0}` to not be greater than or equal to `{1}`".format( self.obj, dest) assert not self.obj >= dest, msg else: msg = "expected `{0}` to be greater than or equal to `{1}`".format( self.obj, dest) assert self.obj >= dest, msg return True @assertionmethod def lower_than(self, dest): if self.negative: msg = "expected `{0}` to not be lower than `{1}`".format( self.obj, dest) assert not self.obj < dest, msg else: msg = "expected `{0}` to be lower than `{1}`".format( self.obj, dest) assert self.obj < dest, msg return True @assertionmethod def lower_than_or_equal_to(self, dest): if self.negative: msg = "expected `{0}` to not be lower than or equal to `{1}`".format( self.obj, dest) assert not self.obj <= dest, msg else: msg = "expected `{0}` to be lower than or equal to `{1}`".format( self.obj, dest) assert self.obj <= dest, msg return True @assertionmethod def below(self, num): if self.negative: msg = "{0} should not be below {1}".format(self.obj, num) assert not self.obj < num, msg else: msg = "{0} should be below {1}".format(self.obj, num) assert self.obj < num, msg return True @assertionmethod def above(self, num): if self.negative: msg = "{0} should not be above {1}".format(self.obj, num) assert not self.obj > num, msg else: msg = "{0} should be above {1}".format(self.obj, num) assert self.obj > num, msg return True @assertionmethod def length_of(self, num): if self.negative: return self._that.len_is_not(num) return self._that.len_is(num) def called_with(self, *args, **kw): self._callable_args = args self._callable_kw = kw return self called = builtins.property(called_with) @assertionmethod def throw(self, *args, **kw): _that = AssertionHelper(self.obj, with_args=self._callable_args, and_kwargs=self._callable_kw) if self.negative: msg = ("{0} called with args {1} and kwargs {2} should " "not raise {3} but raised {4}") exc = args and args[0] or Exception try: self.obj(*self._callable_args, **self._callable_kw) return True except Exception as e: err = msg.format( self.obj, self._that._callable_args, self._that._callable_kw, exc, e, ) raise AssertionError(err) return _that.raises(*args, **kw) thrown = throw raised = thrown @assertionmethod def return_value(self, value): return_value = self.obj(*self._callable_args, **self._callable_kw) return this(return_value).should.equal(value) returned_the_value = return_value @assertionmethod def look_like(self, value): if self.negative: try: self._that.looks_like(value) except AssertionError: return True else: msg = '%r should not look like %r but does' raise AssertionError(msg % (self.obj, value)) return self._that.looks_like(value) @assertionmethod def contain(self, what): obj = self.obj if self.negative: return expect(what).to.not_be.within(obj) else: return expect(what).to.be.within(obj) @assertionmethod def match(self, regex, *args): obj_repr = repr(self.obj) assert isinstance(self.obj, basestring), ( "{0} should be a string in order to compare using .match()".format(obj_repr) ) matched = re.search(regex, self.obj, *args) modifiers_map = { re.I: "i", re.L: "l", re.M: "m", re.S: "s", re.U: "u", } modifiers = "".join([modifiers_map.get(x, "") for x in args]) regex_representation = '/{0}/{1}'.format(regex, modifiers) if self.negative: assert matched is None, ( "{0} should not match the regular expression {1}".format( obj_repr, regex_representation)) else: assert matched is not None, ( "{0} doesn't match the regular expression {1}".format( obj_repr, regex_representation)) return True this = AssertionBuilder('this') the = AssertionBuilder('the') it = AssertionBuilder('it') these = AssertionBuilder('these') those = AssertionBuilder('those') expect = AssertionBuilder('expect') def assertion(func): """Extend sure with a custom assertion method.""" func = assertionmethod(func) setattr(AssertionBuilder, func.__name__, func) return func def chain(func): """Extend sure with a custom chaining method.""" setattr(AssertionBuilder, func.__name__, func) return func def chainproperty(func): """Extend sure with a custom chain property.""" func = assertionproperty(func) setattr(AssertionBuilder, func.fget.__name__, func) return func class ensure(object): """ Contextmanager to ensure that the given assertion message is printed upon a raised ``AssertionError`` exception. The ``args`` and ``kwargs`` are used to format the message using ``format()``. """ def __init__(self, msg, *args, **kwargs): self.msg = msg self.args = args self.kwargs = kwargs def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): """ Catch all ``AsertionError`` exceptions and reraise them with the message provided to the context manager. """ if exc_type is not AssertionError: return msg = self.msg.format(*self.args, **self.kwargs) raise AssertionError(msg) allows_new_syntax = not os.getenv('SURE_DISABLE_NEW_SYNTAX') if is_cpython and allows_new_syntax: def make_safe_property(method, name, should_be_property=True): if not should_be_property: return method(None) def deleter(method, self, *args, **kw): if isinstance(self, type): # if the attribute has to be deleted from a class object # we cannot use ``del self.__dict__[name]`` directly because we cannot # modify a mappingproxy object. Thus, we have to delete it in our # proxy __dict__. del overwritten_object_handlers[(id(self), method.__name__)] else: # if the attribute has to be deleted from an instance object # we are able to directly delete it from the object's __dict__. del self.__dict__[name] def setter(method, self, other): if isinstance(self, type): # if the attribute has to be set to a class object # we cannot use ``self.__dict__[name] = other`` directly because we cannot # modify a mappingproxy object. Thus, we have to set it in our # proxy __dict__. overwritten_object_handlers[(id(self), method.__name__)] = other else: # if the attribute has to be set to an instance object # we are able to directly set it in the object's __dict__. self.__dict__[name] = other return builtins.property( fget=method, fset=partial(setter, method), fdel=partial(deleter, method), ) def build_assertion_property(name, is_negative, prop=True): """Build assertion property This is the assertion property which is usually patched to the built-in ``object`` and ``NoneType``. """ def method(self): # check if the given object already has an attribute with the # given name. If yes return it instead of patching it. try: if name in self.__dict__: return self.__dict__[name] except AttributeError: # we do not have an object with __dict__, thus # it's safe to just continue and patch the `name`. pass overwritten_object_handler = overwritten_object_handlers.get((id(self), name), None) if overwritten_object_handler: return overwritten_object_handler builder = AssertionBuilder(name, negative=is_negative) instance = builder(self) callable_args = getattr(self, '_callable_args', ()) if callable_args: instance._callable_args = callable_args callable_kw = getattr(self, '_callable_kw', {}) if callable_kw: instance._callable_kw = callable_kw return instance method.__name__ = str(name) return make_safe_property(method, name, prop) object_handler = patchable_builtin(object) # We have to keep track of all objects which # should overwrite a ``POSITIVES`` or ``NEGATIVES`` # property. If we wouldn't do that in the # make_safe_property.setter method we would loose # the newly assigned object reference. overwritten_object_handlers = {} # None does not have a tp_dict associated to its PyObject, so this # is the only way we could make it work like we expected. none = patchable_builtin(None.__class__) for name in POSITIVES: object_handler[name] = build_assertion_property(name, is_negative=False) none[name] = build_assertion_property(name, is_negative=False, prop=False) for name in NEGATIVES: object_handler[name] = build_assertion_property(name, is_negative=True) none[name] = build_assertion_property(name, is_negative=True, prop=False) old_dir = dir def enable(): @wraps(builtins.dir) def _new_dir(*obj): if not obj: frame = inspect.currentframe() return sorted(frame.f_back.f_locals.keys()) if len(obj) > 1: raise TypeError('dir expected at most 1 arguments, got {0}'.format(len(obj))) patched = [x for x in old_dir(obj[0]) if isinstance(getattr(obj[0], x, None), AssertionBuilder)] return sorted(set(old_dir(obj[0])).difference(patched)) builtins.dir = _new_dir if allows_new_syntax: enable()