Skip to content

Commit

Permalink
presents a bit more test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Jan 14, 2024
1 parent a07e155 commit 8858abc
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 156 deletions.
4 changes: 4 additions & 0 deletions docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ This project adheres to `Semantic Versioning <http://semver.org/>`__.
- Renames :class:`sure.AssertionBuilder` constructor parameters:
- ``with_kwargs`` to ``with_kws``
- ``and_kwargs`` to ``and_kws``
- Functions or methods decorated with the :func:`sure.within`
decorator no longer receive a :class:`datetime.datetime` object as
first argument.


[v2.0.0]
--------
Expand Down
60 changes: 17 additions & 43 deletions sure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@
from sure import registry
from sure.core import DeepComparison
from sure.core import Explanation
from sure.core import identify_caller_location
from sure.errors import SpecialSyntaxDisabledError
from sure.errors import (
WrongUsageError,
SpecialSyntaxDisabledError,
)
from sure.errors import WrongUsageError
from sure.errors import InternalRuntimeError
from sure.doubles.dummies import anything
from sure.loader import get_file_name
Expand All @@ -56,14 +54,6 @@
bugtracker = "https://github.com/gabrielfalcao/sure/issues"


def unwrap_assertion_helper(obj) -> object:
while isinstance(obj, AssertionHelper):
obj = obj.src
while isinstance(obj, AssertionBuilder):
obj = obj.actual
return obj


class StagingArea(dict):
"""A :external+python:ref:`mapping <mapping>` primarily designated for providing
a kind of "staging area" for test functions or methods decorated
Expand Down Expand Up @@ -112,13 +102,6 @@ def __setattr__(self, attr, value):
return super(StagingArea, self).__setattr__(attr, value)


def ensure_type(caller_name, cast, actual):
try:
return cast(actual)
except TypeError:
raise InternalRuntimeError(f"{caller_name} expects {cast} but received {actual} which is {type(actual)} instead")


class CallBack(object):
context_error = (
"the function %s defined at %s line %d, is being "
Expand Down Expand Up @@ -277,38 +260,30 @@ def within(**units):

convert_from, convert_to = UNITS[unit]
timeout = convert_from(value)
exc = []

def dec(func):
exc = []

@wraps(func)
def wrap(*args, **kw):
start = datetime.utcnow()

try:
func(start, *args, **kw)
except TypeError as e: # TODO: test
fmt = "{0}() takes 0 positional arguments but 1 was given"
err = str(e)
if fmt.format(func.__name__) in err:
func(*args, **kw)
else:
exc.append(traceback.format_exc())
func(*args, **kw)

except Exception:
exc.append(traceback.format_exc())
except Exception as e:
exc.append(e)

end = datetime.utcnow()
delta = end - start
took = convert_to(delta.microseconds)

if not took < timeout:
raise AssertionError(
"%s did not run within %s %s" % (
func.__name__,
word,
unit,
))
f"{identify_caller_location(func)} did not run within {word} {unit}"
)
if exc:
raise AssertionError(exc.pop(0))
raise exc.pop(0)

wrap.__name__ = func.__name__
wrap.__doc__ = func.__doc__
Expand Down Expand Up @@ -506,7 +481,6 @@ def assertionmethod(func):

@wraps(func)
def wrapper(self, *args, **kw):
self.actual = unwrap_assertion_helper(self.actual)
try:
value = func(self, *args, **kw)
except AssertionError as e:
Expand Down Expand Up @@ -543,7 +517,7 @@ def __init__(
self._name = name
self.negative = negative

self.actual = unwrap_assertion_helper(actual)
self.actual = actual
self._callable_args = []
self._callable_kw = {}
if isinstance(with_args, (list, tuple)):
Expand All @@ -570,7 +544,7 @@ def __call__(self,
self._callable_args = actual._callable_args
self._callable_kw = actual._callable_kw
else:
self.actual = unwrap_assertion_helper(actual)
self.actual = actual

self._callable_args = []
self._callable_kw = {}
Expand Down Expand Up @@ -801,10 +775,10 @@ def does_not_contain(self, expectation):
to_not_contain = does_not_contain

@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.actual)
def within_range(self, start: int, end: int):
start = int(start)
end = int(end)
subject = int(self.actual)
is_within_range = subject >= start and subject <= end

if self.negative:
Expand Down
26 changes: 15 additions & 11 deletions sure/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import types
from typing import Union, List, Dict, Tuple
from collections import OrderedDict
from functools import cache

from sure.terminal import yellow, red, green
from sure.doubles.dummies import Anything
from sure.loader import get_file_name
from sure.loader import get_line_number
from sure.loader import resolve_path

from unittest.mock import _CallList as UnitTestMockCallList

try: # TODO: document the coupling with :mod:`mock` or :mod:`unittest.mock`
from mock.mock import _CallList as MockCallList
except ImportError:
except ImportError: # pragma: no cover
MockCallList = None

MockCallListType = tuple(filter(bool, (UnitTestMockCallList, MockCallList)))
Expand All @@ -36,13 +42,7 @@ def get_header(self, X, Y, suffix):
return yellow(header).strip()

def get_assertion(self, X, Y, prefix=""):
if not isinstance(prefix, str):
raise TypeError(
f"Explanation.get_assertion() takes a {str} as "
f"its `prefix' argument but received {prefix} ({type(prefix)}) instead"
)
else:
prefix = f"{prefix.strip()}\n"
prefix = f"{str(prefix or '').strip()}\n"

return AssertionError(f"{prefix}{self.get_header(X, Y, self)}")

Expand Down Expand Up @@ -247,10 +247,14 @@ def safe_format_repr(string):

return exp

def explanation(self):
return self._explanation


def itemize_length(items):
length = len(items)
return '{0} item{1}'.format(length, length > 1 and "s" or "")


def identify_caller_location(caller: Union[types.FunctionType, types.MethodType]):
callable_name = caller.__name__
filename = resolve_path(get_file_name(caller), os.getcwd())
lineno = get_line_number(caller)
return f'{callable_name} [{filename} line {lineno}]'
18 changes: 4 additions & 14 deletions sure/doubles/stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,29 @@

'''The :mod:`sure.doubles.stubs` module provides test-doubles of the type "Stub"
**Stubs** provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
**Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.**
'''


def stub(base_class=None, metaclass=None, **attributes):
def stub(base_class=None, **attributes):
"""creates a python class "on-the-fly" with the given keyword-arguments
as class-attributes accessible with .attrname.
The new class inherits from ``base_class`` and defaults to ``object``
Use this to mock rather than stub in instances where such approach seems reasonable.
"""
if not isinstance(metaclass, type):
attributes['metaclass'] = metaclass

if not isinstance(base_class, type):
attributes['base_class'] = base_class
base_class = object
if base_class is None:
base_class = object

stub_name = f"{base_class.__name__}Stub"

members = {
"__init__": lambda self: None,
"__new__": lambda *args, **kw: object.__new__(
"__new__": lambda *args, **kw: base_class.__new__(
*args, *kw
),
"__repr__": lambda self: f"<{stub_name}>",
}
kwds = {}
if metaclass is not None:
kwds["metaclass"] = metaclass
members["__metaclass__"] = metaclass # TODO: remove this line

members.update(attributes)
return type(stub_name, (base_class,), members, **kwds)()
return type(stub_name, (base_class,), members)()
16 changes: 8 additions & 8 deletions sure/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Dict, List, Optional, Tuple, Union
from functools import reduce
from sure import registry
from sure.types import TestLocation
from sure.types import TestLocation, ScenarioResult


__sure_package_path__ = str(Path(__file__).parent)
Expand Down Expand Up @@ -72,10 +72,10 @@ def __init__(self, message):
super().__init__(message)

def __str__(self):
return self.message
return getattr(self, 'message', self.__class__.__name__)

def __repr__(self):
return self.message
return getattr(self, 'message', self.__class__.__name__)


class FileSystemError(IOError):
Expand All @@ -90,25 +90,25 @@ def __init__(self, code):


class RuntimeInterruption(BaseSureError):
def __init__(self, scenario_result):
def __init__(self, scenario_result: ScenarioResult):
self.result = scenario_result
self.scenario = scenario_result.scenario
self.context = scenario_result.context
super().__init__(f"{self.result}")


class ImmediateError(RuntimeInterruption):
def __init__(self, scenario_result):
def __init__(self, scenario_result: ScenarioResult):
super().__init__(scenario_result)
self.args = scenario_result.error.args
self.message = "".join(self.args)
super().__init__(scenario_result)


class ImmediateFailure(RuntimeInterruption):
def __init__(self, scenario_result):
def __init__(self, scenario_result: ScenarioResult):
super().__init__(scenario_result)
self.args = scenario_result.failure.args
self.message = scenario_result.succinct_failure
super().__init__(scenario_result)


class ExitError(ImmediateExit):
Expand Down
41 changes: 13 additions & 28 deletions sure/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,24 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from typing import List
from pathlib import Path

from sure.loader import loader

module_root = Path(__file__).parent.absolute()

REPORTERS = {}


def add_reporter(reporter: type) -> type:
if reporter.name == "__meta__":
return reporter
def register_class(cls, identifier):
cls.kind = identifier
cls.loader = loader
if len(cls.__mro__) > 2:
register = MODULE_REGISTERS[identifier]
return register(cls)
else:
return cls


def add_reporter(reporter: type) -> type:
REPORTERS[reporter.name] = reporter
return reporter

Expand All @@ -41,29 +44,11 @@ def gather_reporter_names() -> List[str]:
return list(filter(bool, REPORTERS.keys()))


def internal_module_name(name):
return __name__.replace(".meta", f".{name}.")


def register_class(cls, identifier):
cls.kind = identifier
cls.loader = loader
if len(cls.__mro__) > 2:
register = MODULE_REGISTERS[identifier]
return register(cls)
else:
return cls


MODULE_REGISTERS = dict(
(
("reporter", add_reporter),
)
)


class MetaReporter(type):
def __init__(cls, name, bases, attrs):
if cls.__module__ != __name__:
cls = register_class(cls, "reporter")
super(MetaReporter, cls).__init__(name, bases, attrs)


MODULE_REGISTERS = dict((("reporter", add_reporter),))
8 changes: 1 addition & 7 deletions sure/original.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,13 @@
from sure.core import Explanation
from sure.core import DeepComparison
from sure.core import itemize_length
from sure.core import identify_caller_location
from sure.errors import treat_error, CallerLocation
from sure.loader import get_file_name
from sure.loader import get_line_number
from sure.loader import resolve_path


def identify_caller_location(caller: Union[types.FunctionType, types.MethodType]):
callable_name = caller.__name__
filename = resolve_path(get_file_name(caller), os.getcwd())
lineno = get_line_number(caller)
return f'{callable_name} [{filename} line {lineno}]'


def is_iterable(obj):
"""returns ``True`` the given object is iterable
Expand Down

0 comments on commit 8858abc

Please sign in to comment.