diff --git a/COPYING b/COPYING index a7c19df..41a4f93 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Copyright (C) <2010-2022> Gabriel Falcão +Copyright (C) <2010-2024> Gabriel Falcão GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 @@ -621,56 +621,3 @@ Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/MANIFEST.in b/MANIFEST.in index eeac635..8348c3d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ -include requirements.txt tox.ini +include requirements.txt include COPYING include README.rst -recursive-include tests *.py diff --git a/Makefile b/Makefile index 4ba0dec..d400c61 100644 --- a/Makefile +++ b/Makefile @@ -60,8 +60,7 @@ test tests: # runs main command-line tool run: | $(LIBEXEC_PATH) $(LIBEXEC_PATH) --reap-warnings tests/crashes - $(LIBEXEC_PATH) --reap-warnings --special-syntax --with-coverage --cover-branches --cover-module=sure.core --cover-module=sure tests/runner - $(LIBEXEC_PATH) --reap-warnings --special-syntax --with-coverage --cover-branches --cover-module=sure --immediate --cover-module=sure --ignore tests/crashes tests + $(LIBEXEC_PATH) --reap-warnings --special-syntax --with-coverage --cover-branches --cover-erase --cover-module=sure --immediate --cover-module=sure --ignore tests/crashes tests push-release: dist # pushes distribution tarballs of the current version $(VENV)/bin/twine upload dist/*.tar.gz diff --git a/README.rst b/README.rst index 7076e9b..e75819a 100644 --- a/README.rst +++ b/README.rst @@ -66,7 +66,7 @@ For More Information: Documentation ------------- -Available on the `website `_. +Available on `sure.readthedocs.io `_. To build locally run: diff --git a/development.txt b/development.txt index 064b62e..7300801 100644 --- a/development.txt +++ b/development.txt @@ -7,3 +7,4 @@ tox==4.1.1 twine==3.4.1 pytest==7.4.3 pytest-cov==4.1.0 +mock diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst index 69b8b07..64d8969 100644 --- a/docs/source/api-reference.rst +++ b/docs/source/api-reference.rst @@ -95,7 +95,10 @@ API Reference ------------------------ .. autoclass:: sure.doubles.dummies.Anything +.. autoclass:: sure.doubles.dummies.AnythingOfType .. autoattribute:: sure.doubles.dummies.anything +.. autofunction:: sure.doubles.dummies.anything_of_type + ``sure.doubles.fakes`` ---------------------- diff --git a/docs/source/assertion-reference.rst b/docs/source/assertion-reference.rst index c4e22de..ebf04c6 100644 --- a/docs/source/assertion-reference.rst +++ b/docs/source/assertion-reference.rst @@ -3,6 +3,17 @@ Assertion Builder Reference =========================== +Aliases +------- + +.. code:: python + + from sure import expects + + expects("text").to.equal("text") + expects.that("text").equals("text") + + Numerical Equality ------------------ diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 1011344..7d47bc1 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -4,10 +4,11 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to `Semantic Versioning `__. -[v3.0.0] --------- +v3.0.0 +------ -- Presents better documentation +- Pervasive test-coverage +- Presents better documentation, refactoring and bugfixes - Drops support to Python 2 obliterates the ``sure.compat`` module - Introduces the modules: - :mod:`sure.doubles` @@ -34,6 +35,14 @@ This project adheres to `Semantic Versioning `__. decorator no longer receive a :class:`datetime.datetime` object as first argument. +- Removes methods from :class:`sure.original.AssertionHelper`: + - :meth:`sure.original.AssertionHelper.differs` + - :meth:`sure.original.AssertionHelper.has` + - :meth:`sure.original.AssertionHelper.is_a` + - :meth:`sure.original.AssertionHelper.every_item_is` + - :meth:`sure.original.AssertionHelper.at` + - :meth:`sure.original.AssertionHelper.like` + - Feel free to open an issue requesting any of those methods to be added back to Sure's codebase. [v2.0.0] -------- @@ -52,7 +61,7 @@ Fixed Fixed ~~~~~ -- Reading the version dinamically was causing import errors that caused +- Reading the version dynamically was causing import errors that caused error when installing package. Refs #144 `v1.4.7 `__ diff --git a/docs/source/conf.py b/docs/source/conf.py index a8dee74..f276dfb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -31,6 +31,7 @@ except ImportError: sys.path.insert(0, Path(__file__).parent.parent.parent) +from sure import version extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", @@ -47,8 +48,7 @@ project = "sure" copyright = "2010-2024, Gabriel Falcão" author = "Gabriel Falcão" -version = "3.0a0" -release = "3.0a0" +release = version language = 'en' exclude_patterns = [] pygments_style = "sphinx" @@ -84,6 +84,6 @@ "python": ("https://docs.python.org/3/", None), "mock": ("https://mock.readthedocs.io/en/latest/", None), "psycopg2": ("https://www.psycopg.org/docs/", None), - "coverage": ("https://coverage.readthedocs.io/en/7.3.3/", None), + "coverage": ("https://coverage.readthedocs.io/en/7.4.0/", None), } pygments_style = 'xcode' diff --git a/setup.py b/setup.py index 1108600..eac4c70 100644 --- a/setup.py +++ b/setup.py @@ -46,24 +46,13 @@ PROJECT_ROOT = os.path.dirname(__file__) -class VersionFinder(ast.NodeVisitor): - VARIABLE_NAME = "version" - - def __init__(self): - self.version = None - - def visit_Assign(self, node): - try: - if node.targets[0].id == self.VARIABLE_NAME: - self.version = node.value.s - except Exception: - pass - - def read_version(): - finder = VersionFinder() - finder.visit(ast.parse(local_text_file("sure", "version.py"))) - return finder.version + mod = ast.parse(local_text_file("sure", "version.py")) + exp = mod.body[0] + tgt = exp.targets[0] + cst = exp.value + assert tgt.id == "version" + return cst.value def local_text_file(*f): @@ -86,9 +75,10 @@ def read_readme(): return __doc__ -install_requires = ["mock", "coverage==7.4.0", "click==8.1.7", "couleur==0.7.4"] -tests_require = [] +install_requires = ["coverage==7.4.0", "click==8.1.7", "couleur==0.7.4"] +tests_require = ["mock"] version = read_version() +packages = find_packages(exclude=["*tests*", "*examples*"]) if __name__ == "__main__": setup( @@ -102,7 +92,7 @@ def read_readme(): maintainer="Gabriel Falcao", maintainer_email="gabrielteratos@gmail.com", include_package_data=True, - packages=find_packages(exclude=["*tests*"]), + packages=packages, install_requires=install_requires, long_description_content_type='text/x-rst', entry_points={ @@ -118,11 +108,13 @@ def read_readme(): "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", diff --git a/sure/__init__.py b/sure/__init__.py index f744611..dd15784 100644 --- a/sure/__init__.py +++ b/sure/__init__.py @@ -20,14 +20,14 @@ import re import os import sys - import builtins import difflib import inspect import traceback - +import operator from functools import wraps, partial, reduce from datetime import datetime +from typing import Dict, List, Optional, Tuple, Union from sure.original import AssertionHelper from sure.original import Iterable @@ -336,18 +336,41 @@ def word_to_number(word): "fourteen": 14, "fifteen": 15, "sixteen": 16, + "seventeen": 17, + "eighteen": 18, + "nineteen": 19, + "twenty": 20, + "thirty": 30, + "fourty": 40, + "fifty": 50, + "sixty": 60, + "seventy": 70, + "eighty": 80, + "ninety": 90, + "hundred": 100, + "thousand": 1000, + "million": 1000000, } - # TODO: refactor - try: - return basic[word] - except KeyError: - raise AssertionError( - "sure supports only literal numbers from one to sixteen, " - f'you tried the word "{word}"' - ) + value = int(False) + words = word.split("_") + for p, word in enumerate(words): + number = basic.get(words[p], 0) + if len(words) > p + 1: + next_number = basic.get(words[p + 1], 0) + else: + next_number = 0 + if number <= 10 and next_number > 90: + value += number * next_number + elif number > 90: + continue + else: + value += number + + return value -def action_for(context, provides=None, depends_on=None): + +def action_for(context, provides=None, depends_on=None): # pragma: no cover # TODO: add test coverage """function decorator for defining functions which might provide a list of assets to the staging area and might declare a list of dependencies expected to exist within a :class:`StagingArea` @@ -478,20 +501,10 @@ def assertionmethod(func): @wraps(func) def wrapper(self, *args, **kw): try: - value = func(self, *args, **kw) + return func(self, *args, **kw) except AssertionError as e: raise e - if not value: - raise AssertionError( - f"{0}({1}{2}) failed".format( - func.__name__, - ", ".join(map(repr, args)), - ", ".join(["{0}={1}".format(k, repr(kw[k])) for k in kw]), - ) - ) - return value - return wrapper @@ -503,12 +516,12 @@ def assertionproperty(func): class AssertionBuilder(object): def __init__( self, - name=None, - negative=False, - actual=None, - with_args=None, - with_kws=None, - and_kws=None + name: str, + negative: bool = False, + actual: object = None, + with_args: Optional[Union[list, tuple]] = None, + with_kws: Optional[Dict[str, object]] = None, + and_kws: Optional[Dict[str, object]] = None, ): self._name = name self.negative = negative @@ -557,25 +570,10 @@ def __call__(self, 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, - actual=self.actual, - with_args=self._callable_args, - with_kws=self._callable_kw, - ) - try: return getattr(self._that, attr) except AttributeError: - return self.__getattribute__(attr) - return super(AssertionBuilder, self).__getattribute__(attr) + return super(AssertionBuilder, self).__getattribute__(attr) @assertionproperty def callable(self): @@ -619,23 +617,23 @@ def to_not(self): return self.should_not @assertionproperty - def to(self): + def have(self): return self @assertionproperty - def when(self): + def which(self): return self @assertionproperty - def which(self): + def to(self): return self @assertionproperty - def have(self): + def when(self): return self @assertionproperty - def with_value(self): + def that(self): return self @assertionmethod @@ -739,23 +737,12 @@ def none(self): return True - def __contains__(self, expectation): - if isinstance(self.actual, dict): - items = self.actual.keys() - - if isinstance(self.actual, Iterable): - items = self.actual - else: - items = dir(self.actual) - - return expectation in items - @assertionmethod def contains(self, expectation): if expectation in self.actual: return True else: - raise Explanation(f'{expectation} should be in {self.actual}').as_assertion(self.actual, expectation, "Content Verification Error") + raise Explanation(f"`{expectation}' should be in `{self.actual}'").as_assertion(self.actual, expectation, "Content Verification Error") contain = contains to_contain = contains @@ -765,7 +752,7 @@ def does_not_contain(self, expectation): if expectation not in self.actual: return True else: - raise Explanation(f'{expectation} should not be in {self.actual}').as_assertion(self.actual, expectation, "Content Verification Error") + raise Explanation(f"`{expectation}' should not be in `{self.actual}'").as_assertion(self.actual, expectation, "Content Verification Error") doesnt_contain = does_not_contain to_not_contain = does_not_contain @@ -807,22 +794,22 @@ def within(self, first, *rest): # instead of hardcoding ``.should_not.be.within`` and ``.should.be.within`` in the # variable assignments below if self.negative: - ppath = "{0}.should_not.be.within".format(self.actual) + ppath = f"({self.actual}).should_not.be.within" else: - ppath = "{0}.should.be.within".format(self.actual) + ppath = f"({self.actual}).should.be.within".format(self.actual) raise AssertionError( ( - "{0}({1}, {2}) must be called with either a iterable:\n" + "{0}({1}, {2}) must be called with either an iterable:\n" "{0}([1, 2, 3, 4])\n" - "or with a range of numbers:" - "{0}(1, 3000)" + "or with a range of numbers, i.e.: " + "`{0}(1, 3000)'" ).format(ppath, first, ", ".join([repr(x) for x in rest])) ) @assertionmethod def equal(self, expectation, epsilon=None): - """compares given object ``X'` with an expected '`Y'` object. + """compares given object ``X'` with an expected '`Y'` object. It primarily assures that the compared objects are absolute equal '`=='`. @@ -887,10 +874,8 @@ def different_of(self, expectation): def a(self, klass): if isinstance(klass, type): class_name = klass.__name__ - elif isinstance(klass, (str, )): - class_name = klass.strip() else: - class_name = str(klass) + class_name = str(klass).strip() is_vowel = class_name.lower()[0] in "aeiou" @@ -898,14 +883,8 @@ def a(self, klass): 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" + first = "builtins" items = [klass] klass = reduce(getattr, items, __import__(first)) @@ -1023,13 +1002,9 @@ def called_with(self, *args, **kw): @assertionmethod def throw(self, *args, **kw): - _that = AssertionHelper( - self.actual, with_args=self._callable_args, and_kws=self._callable_kw - ) - if self.negative: msg = ( - "{0} called with args {1} and keyword-args {2} should " + "`{0}' called with args {1} and keyword-args {2} should " "not raise {3} but raised {4}" ) @@ -1039,15 +1014,17 @@ def throw(self, *args, **kw): return True except Exception as e: err = msg.format( - self.actual, - self._that._callable_args, - self._that._callable_kw, + self.actual.__name__, + self._callable_args, + self._callable_kw, exc, e, ) raise AssertionError(err) - return _that.raises(*args, **kw) + return AssertionHelper( + self.actual, with_args=self._callable_args, and_kws=self._callable_kw + ).raises(*args, **kw) thrown = throw raises = thrown @@ -1086,7 +1063,7 @@ def match(self, regex, *args): if not isinstance( self.actual, str ): - raise f"{repr(self.actual)} should be a string in order to compare using .match()" + raise WrongUsageError(f"{repr(self.actual)} should be a string in order to compare using .match()") matched = re.search(regex, self.actual, *args) modifiers_map = { @@ -1147,9 +1124,6 @@ def assure_identical(self, identical): def __getattr__(self, name): return getattr(self.assertion_builder, name) - def __repr__(self): - return f"" - assert_that = AssertionBuilder("assert_that") it = AssertionBuilder("it") @@ -1218,7 +1192,7 @@ def __exit__(self, exc_type, exc_value, traceback): old_dir = dir -def enable_special_syntax(): +def enable_special_syntax(): # pragma: no cover """enables :mod:`sure`'s :ref:`Special Syntax` .. danger:: Enabling the special syntax in production code may cause unintended consequences. diff --git a/sure/astuneval.py b/sure/astuneval.py new file mode 100644 index 0000000..9b81e31 --- /dev/null +++ b/sure/astuneval.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""astuneval (Abstract Syntax-Tree Unevaluation) - safe substitution for unsafe :func:`eval` +""" +import ast + + +class Accessor(object): + """base class for object element accessors""" + + def __init__(self, astbody): + self.body = astbody + + def __call__(self, object: object, *args, **kw) -> object: + return self.access(object, *args, **kw) + + def access(self, object: object) -> object: + raise NotImplementedError(f"support to {type(self.body)} is not implemented") + + +class NameAccessor(Accessor): + """Accesses an object's attributes through name""" + + def access(self, object: object) -> object: + return getattr(object, self.body.id) + + +class SliceAccessor(Accessor): + """Accesses an object's attributes through slice""" + + def access(self, object: object) -> object: + return object[self.body.value] + + +class SubsAccessor(Accessor): + """Accesses an object's attributes through subscript""" + + def access(self, object: object) -> object: + get_value = NameAccessor(self.body.value) + get_slice = SliceAccessor(self.body.slice) + return get_slice(get_value(object)) + + +class AttributeAccessor(Accessor): + """Accesses an object's attributes through chained attribute""" + + def access(self, object: object) -> object: + attr_name = self.body.attr + access = resolve_accessor(self.body.value) + value = access(object) + return getattr(value, attr_name) + + +def resolve_accessor(body): + return { + ast.Name: NameAccessor, + ast.Subscript: SubsAccessor, + ast.Attribute: AttributeAccessor, + }.get(type(body), Accessor)(body) + + +def parse_accessor(value: str) -> Accessor: + body = parse_body(value) + return resolve_accessor(body) + + +def parse_body(value: str) -> ast.stmt: + bodies = ast.parse(value).body + if len(bodies) > int(True): + raise SyntaxError(f"{repr(value)} exceeds the maximum body count for ast nodes") + + return bodies[0].value diff --git a/sure/cli.py b/sure/cli.py index f18c98b..cb5a100 100644 --- a/sure/cli.py +++ b/sure/cli.py @@ -15,28 +15,26 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import os -import sys -import logging -from glob import glob -from itertools import chain as flatten -from functools import reduce -from pathlib import Path - -import click -import coverage -import threading -import sure.reporters - -from sure.loader import resolve_path -from sure.runner import Runner -from sure.runtime import RuntimeOptions -from sure.reporters import gather_reporter_names -from sure.errors import ExitError, ExitFailure, InternalRuntimeError, treat_error - - -@click.command(no_args_is_help=True) +import os # pragma: no cover +import sys # pragma: no cover +from glob import glob # pragma: no cover +from itertools import chain as flatten # pragma: no cover +from functools import reduce # pragma: no cover +from pathlib import Path # pragma: no cover + +import click # pragma: no cover +import coverage # pragma: no cover +import threading # pragma: no cover +import sure.reporters # pragma: no cover + +from sure.loader import resolve_path # pragma: no cover +from sure.runner import Runner # pragma: no cover +from sure.runtime import RuntimeOptions # pragma: no cover +from sure.reporters import gather_reporter_names # pragma: no cover +from sure.errors import ExitError, ExitFailure, InternalRuntimeError, treat_error # pragma: no cover + + +@click.command(no_args_is_help=True) # pragma: no cover @click.argument("paths", nargs=-1) @click.option("-c", "--with-coverage", is_flag=True) @click.option("-s", "--special-syntax", is_flag=True) @@ -62,7 +60,11 @@ type=click.Choice(gather_reporter_names()), ) @click.option("--cover-branches", is_flag=True) +@click.option("--cover-include", multiple=True, help="includes paths or patterns in the coverage") +@click.option("--cover-omit", multiple=True, help="omits paths or patterns from the coverage") @click.option("--cover-module", multiple=True, help="specify module names to cover") +@click.option("--cover-erase", is_flag=True, help="erases coverage data prior to running tests") +@click.option("--cover-concurrency", help="indicates the concurrency library used in measured code", type=click.Choice(["greenlet", "eventlet", "gevent", "multiprocessing", "thread"]), default="thread") @click.option("--reap-warnings", is_flag=True, help="reaps warnings during runtime and report only at the end of test session") def entrypoint( paths, @@ -74,7 +76,11 @@ def entrypoint( special_syntax, with_coverage, cover_branches, + cover_include, + cover_omit, cover_module, + cover_erase, + cover_concurrency, reap_warnings, ): if not paths: @@ -82,26 +88,29 @@ def entrypoint( else: paths = flatten(*list(map(glob, paths))) - configure_logging(log_level, log_file) coverageopts = { - "auto_data": True, - "cover_pylib": False, - "source": cover_module, + "auto_data": not False, "branch": cover_branches, - "config_file": True, + "include": cover_include, + "concurrency": cover_concurrency, + "omit": cover_omit, + "config_file": not False, + "cover_pylib": not False, + "source": cover_module, } + options = RuntimeOptions(immediate=immediate, ignore=ignore, reap_warnings=reap_warnings) + runner = Runner(resolve_path(os.getcwd()), reporter, options) + cov = with_coverage and coverage.Coverage(**coverageopts) or None if cov: - cov.erase() + cover_erase and cov.erase() cov.load() cov.start() if special_syntax: sure.enable_special_syntax() - options = RuntimeOptions(immediate=immediate, ignore=ignore, reap_warnings=reap_warnings) - runner = Runner(resolve_path(os.getcwd()), reporter, options) try: result = runner.run(paths) except Exception as e: @@ -120,37 +129,3 @@ def entrypoint( cov.stop() cov.save() cov.report() - - -def configure_logging(log_level: str, log_file: str): - if not log_level: - log_level = "none" - - if not isinstance(log_level, str): - raise TypeError( - f"log_level should be a string but is {log_level}({type(log_level)}) instead" - ) - log_level = log_level.lower() == "none" and "info" or log_level - - level = getattr(logging, log_level.upper()) - - if log_file: - log_directory = Path(log_file).parent() - if log_directory.exists(): - raise RuntimeError( - f"the log path {log_directory} exists but is not a directory" - ) - log_directory.mkdir(parents=True, exists_ok=True) - - handler = logging.FileHandler(log_file) - else: - handler = logging.NullHandler() - - handler.setLevel(level) - - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - - handler.setFormatter(formatter) - logging.getLogger().addHandler(handler) diff --git a/sure/core.py b/sure/core.py index 391ae0a..f0da38d 100644 --- a/sure/core.py +++ b/sure/core.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os import types +from pprint import pformat from typing import Union, List, Dict, Tuple from collections import OrderedDict from functools import cache @@ -30,7 +31,7 @@ class Explanation(str): def get_header(self, X, Y, suffix): - header = f"X = {repr(X)}\n and\nY = {repr(Y)}\n{str(suffix)}" + header = f"X = {pformat(X, sort_dicts=False, compact=False)}\n and\nY = {pformat(Y, sort_dicts=False, compact=False)}\n{str(suffix)}" return yellow(header).strip() def get_assertion(self, X, Y, prefix=""): diff --git a/sure/doubles/dummies.py b/sure/doubles/dummies.py index 343d540..3a5624c 100644 --- a/sure/doubles/dummies.py +++ b/sure/doubles/dummies.py @@ -77,7 +77,7 @@ def __eq__(self, given: object): given_type = type(given) module_name = given_type.__module__ type_name = given_type.__name__ - return isinstance(given, self.__expected_type__) and super().__eq__(f"typed:{module_name}.{type_name}") + return isinstance(given, (self.__expected_type__, self.__class__)) and super().__eq__(f"typed:{module_name}.{type_name}") def __repr__(self): return f'' diff --git a/sure/doubles/stubs.py b/sure/doubles/stubs.py index 57c66b3..1b377e3 100644 --- a/sure/doubles/stubs.py +++ b/sure/doubles/stubs.py @@ -39,7 +39,9 @@ def stub(base_class=None, **attributes): "__new__": lambda *args, **kw: base_class.__new__( *args, *kw ), - "__repr__": lambda self: f"<{stub_name}>", } + if base_class.__repr__ == object.__repr__: + members["__repr__"] = lambda self: f"<{stub_name}>" + members.update(attributes) return type(stub_name, (base_class,), members)() diff --git a/sure/errors.py b/sure/errors.py index 5a8f834..9fd4186 100644 --- a/sure/errors.py +++ b/sure/errors.py @@ -112,8 +112,9 @@ def __init__(self, scenario_result: ScenarioResult): class ExitError(ImmediateExit): - def __init__(self, context: RuntimeContext, result: ScenarioResult): - context.reporter.on_error(context, result) + def __init__(self, context: RuntimeContext, result: ScenarioResult, report:bool = True): + if report: + context.reporter.on_error(context, result) return super().__init__(exit_code("ERROR")) diff --git a/sure/loader/__init__.py b/sure/loader/__init__.py index 5edaabc..2480e15 100644 --- a/sure/loader/__init__.py +++ b/sure/loader/__init__.py @@ -14,7 +14,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import os import sys import ast @@ -33,12 +32,12 @@ FileSystemError, CallerLocation, collapse_path, - send_runtime_warning + send_runtime_warning, ) - from .astutil import gather_class_definitions_from_module_path __MODULES__ = {} +__MODULE_SPECS__ = {} __ROOTS__ = {} __TEST_CLASSES__ = {} @@ -61,7 +60,6 @@ def resolve_path(path, relative_to="~") -> Path: def get_package(path: Union[str, Path]) -> Path: path = Path(path).expanduser().absolute() - if not path.is_dir(): path = path.parent @@ -100,7 +98,9 @@ def from_function_or_method(cls, target): name = target.__name__ else: name = target.__class__.__name__ - path, lineno = get_type_definition_filename_and_firstlineno(target.__class__) + path, lineno = get_type_definition_filename_and_firstlineno( + target.__class__ + ) return cls( filename=path, @@ -120,7 +120,7 @@ def get_type_definition_filename_and_firstlineno(type_object: type) -> Tuple[Pat raise RuntimeError( f"module `{module_name}' does not appear within `sys.modules'. Perhaps Sure is not being used the right way or there is a bug in the current version", ) - if not hasattr(module, '__file__'): + if not hasattr(module, "__file__"): return f"<{module_name}>", -1 path = Path(module.__file__) @@ -145,7 +145,9 @@ def load_recursive( modules = [] excludes = excludes or [] if not isinstance(excludes, list): - raise TypeError(f"sure.loader.load_recursive() param `excludes' must be a {list} but is {repr(excludes)} ({type(excludes)}) instead") + raise TypeError( + f"sure.loader.load_recursive() param `excludes' must be a {list} but is {repr(excludes)} ({type(excludes)}) instead" + ) path = Path(path) if path.is_file(): if fnmatch(path, glob_pattern): @@ -185,6 +187,11 @@ def load_python_path(cls, path: Union[str, Path]) -> List[types.ModuleType]: send_runtime_warning(f"ignoring {path} for seeming to be a __dunder__ file") return [] + if path.is_symlink() and not path.resolve().exists(): + # avoid loading symlinks such as Emacs temporary files .i.e: `.#*' + send_runtime_warning(f"parsing skipped of irregular file `{path.absolute()}'") + return [] + module, root = cls.load_package(path) return [module] @@ -202,6 +209,7 @@ def load_module( module = importlib.util.module_from_spec(spec) __MODULES__[fqdn] = module + __MODULE_SPECS__[module] = spec cdfs = {} for name, metadata in gather_class_definitions_from_module_path( path, None @@ -226,14 +234,11 @@ def object_belongs_to_sure(object: object) -> bool: :param object: an :class:`object` object :returns: ``True`` if the given ``object`` passes this function heuristics to verify that the object belongs to :mod:`sure` """ - module_name = ( - getattr( - object, - "__module__", - getattr(getattr(object, "__class__", object), "__module__", ""), - ) - or "" - ) + module_name = getattr( + object, + "__module__", + getattr(getattr(object, "__class__", object), "__module__", ""), + ) or "" heuristics = [ lambda: module_name == "sure", lambda: module_name.startswith("sure."), diff --git a/sure/loader/astutil.py b/sure/loader/astutil.py index 49ba007..a295fb7 100644 --- a/sure/loader/astutil.py +++ b/sure/loader/astutil.py @@ -19,6 +19,7 @@ from typing import Dict, List, Optional, Tuple, Union from pathlib import Path +from sure.errors import send_runtime_warning def is_classdef(node: ast.stmt) -> bool: @@ -75,7 +76,13 @@ def gather_class_definitions_from_module_path( class is defined and a tuple with the names of its base classes. """ - with Path(path).open() as f: + path = Path(path) + + if path.is_symlink() and not path.resolve().exists(): # avoid loading broken symlinks + send_runtime_warning(f"parsing skipped of irregular file `{path.absolute()}'") + return {} + + with path.open() as f: node = ast.parse(f.read()) return gather_class_definitions_node(node, {}, nearest_line=nearest_line) diff --git a/sure/original.py b/sure/original.py index eedfcbb..9d95283 100644 --- a/sure/original.py +++ b/sure/original.py @@ -31,7 +31,7 @@ from typing import Union from collections.abc import Iterable - +from sure.astuneval import parse_accessor from sure.core import Explanation from sure.core import DeepComparison from sure.core import itemize_length @@ -65,26 +65,6 @@ def all_integers(obj: typing.Iterable) -> bool: return True -def explanation(msg: str) -> typing.Callable: - """Decorator for methods of :class:`~sure.original.AssertionHelper`. - - :param msg: message to be interpolated with the operands of the comparison taking place within the decorated method. - :returns: a decorator function - """ - def dec(func): - @wraps(func) - def wrap(self, expectation): - ret = func(self, expectation) - if bool(ret) is True: - return ret - else: - raise AssertionError(msg % (self.actual, expectation)) - - return wrap - - return dec - - class AssertionHelper(object): """Accompanies :class:`~sure.AssertionBuilder` in performing assertions. @@ -97,12 +77,12 @@ def __init__(self, src, self.actual = src self._attribute = None - self._eval = None + self.__element_access_expr__ = None self._range = None if all_integers(within_range): if len(within_range) != 2: raise TypeError( - 'within_range parameter must be a tuple with 2 objects', + f"within_range parameter must be a tuple with 2 objects, received a `{type(within_range).__name__}' with {len(within_range)} objects instead", ) self._range = within_range @@ -135,7 +115,7 @@ def match(self, *args, **kw): def raises(self, exc, msg=None): if not callable(self.actual): - raise TypeError('%r is not callable' % self.actual) + raise TypeError(f'{repr(self.actual)} is not callable') try: self.actual(*self._callable_args, **self._callable_kw) @@ -167,18 +147,6 @@ def raises(self, exc, msg=None): f'Expected to match regex: {repr(msg.pattern)}\n against:\n {repr(str(err))}' ) - elif isinstance(msg, (str, )) and msg not in str(err): - raise AssertionError( - 'When calling %r the exception message does not match. ' \ - 'Expected: %r\n got:\n %r' % (self.actual, msg, err) - ) - - elif isinstance(msg, re.Pattern) and not msg.search(err): - raise AssertionError( - 'When calling %r the exception message does not match. ' \ - 'Expected to match regex: %r\n against:\n %r' % (identify_caller_location(self.actual), msg.pattern, err) - ) - else: raise e else: @@ -197,12 +165,7 @@ def raises(self, exc, msg=None): self._callable_kw, exc)) else: raise AssertionError( - 'at %s:\ncalling %s() with args %r and kws %r did not raise %r' % ( - _src_filename, - self.actual.__name__, - self._callable_args, - self._callable_kw, exc - ) + f'at {_src_filename}:\ncalling {self.actual.__name__}() with args {repr(self._callable_args)} and kws {repr(self._callable_kw)} did not raise {repr(exc)}' ) return True @@ -235,7 +198,6 @@ def equals(self, expectation): return True def looks_like(self, expectation): - comp = DeepComparison(self.actual, expectation) old_src = pformat(self.actual) old_dst = pformat(expectation) self.actual = re.sub(r'\s', '', self.actual).lower() @@ -246,41 +208,6 @@ def looks_like(self, expectation): else: raise AssertionError(error) - def every_item_is(self, expectation): - msg = 'all members of %r should be %r, but the %dth is %r' - for index, item in enumerate(self.actual): - if self._range: - if index < self._range[0] or index > self._range[1]: - continue - - error = msg % (self.actual, expectation, index, item) - if item != expectation: - raise AssertionError(error) - - return True - - @explanation('%r should differ from %r, but is the same thing') - def differs(self, expectation): - return self.actual != expectation - - @explanation('%r should be a instance of %r, but is not') - def is_a(self, expectation): - return isinstance(self.actual, expectation) - - def at(self, key): - if not self.has(key): - raise AssertionError(f"key {key} not present in {self.actual}") - - if isinstance(self.actual, dict): - return AssertionHelper(self.actual[key]) - - else: - return AssertionHelper(getattr(self.actual, key)) - - @explanation('%r should have %r, but have not') - def has(self, that): - return that in self - def _get_int_or_length(self, obj: Union[int, typing.Iterable]): if isinstance(obj, Iterable): return len(obj) @@ -376,24 +303,20 @@ def len_is_not(self, that: Union[int, typing.Iterable]): return True - def like(self, that): - return self.has(that) - def the_attribute(self, attr): self._attribute = attr return self def in_each(self, attr): - self._eval = attr + self.__element_access_expr__ = attr return self def matches(self, items): msg = '%r[%d].%s should be %r, but is %r' - get_eval = lambda item: eval( - "%s.%s" % ('current', self._eval), {}, {'current': item}, - ) - if self._eval and is_iterable(self.actual): + get_eval = self.__element_access_expr__ and parse_accessor(self.__element_access_expr__) or (lambda x: None) + + if bool(self.__element_access_expr__) and is_iterable(self.actual): if isinstance(items, (str, )): items = [items for x in range(len(items))] else: @@ -408,13 +331,9 @@ def matches(self, items): ) for index, (item, other) in enumerate(zip(self.actual, items)): - if self._range: - if index < self._range[0] or index > self._range[1]: - continue - value = get_eval(item) - error = msg % (self.actual, index, self._eval, other, value) + error = msg % (self.actual, index, self.__element_access_expr__, other, value) if other != value: raise AssertionError(error) else: @@ -442,17 +361,6 @@ def is_empty(self): def are_empty(self): return self.is_empty - def __contains__(self, expectation): - if isinstance(self.actual, dict): - items = self.actual.keys() - - if isinstance(self.actual, Iterable): - items = self.actual - else: - items = dir(self.actual) - - return expectation in items - def contains(self, expectation): if expectation in self.actual: return True diff --git a/sure/reporter.py b/sure/reporter.py index a541c5f..b7be12d 100644 --- a/sure/reporter.py +++ b/sure/reporter.py @@ -19,8 +19,6 @@ from sure.meta import MetaReporter, get_reporter, gather_reporter_names from sure.types import Runner, Feature, FeatureResult, RuntimeContext -__path__ = Path(__file__).absolute().parent - class Reporter(object, metaclass=MetaReporter): """Base class for reporters. diff --git a/sure/reporters/feature.py b/sure/reporters/feature.py index 077de9e..fba581e 100644 --- a/sure/reporters/feature.py +++ b/sure/reporters/feature.py @@ -58,7 +58,7 @@ def on_feature(self, feature: Feature): self.sh.reset(" " * self.indentation) self.sh.bold_blue("Feature: ") self.sh.yellow("'") - self.sh.green(feature.name) + self.sh.green(feature.title) self.sh.yellow("'") self.sh.reset(" ") diff --git a/sure/reporters/test.py b/sure/reporters/test.py index 8ba1d19..07d1c1a 100644 --- a/sure/reporters/test.py +++ b/sure/reporters/test.py @@ -43,11 +43,11 @@ def on_start(self): events["on_start"].append((time.time(),)) def on_feature(self, feature: Feature): - events["on_feature"].append((time.time(), feature.name)) + events["on_feature"].append((time.time(), feature.title)) def on_feature_done(self, feature: Feature, result: FeatureResult): events["on_feature_done"].append( - (time.time(), feature.name, result.label.lower()) + (time.time(), feature.title, result.label.lower()) ) def on_scenario(self, scenario: Scenario): diff --git a/sure/runtime.py b/sure/runtime.py index 7fec440..076b67e 100644 --- a/sure/runtime.py +++ b/sure/runtime.py @@ -585,11 +585,6 @@ def invoke_contextualized(self, container, context): :param name: :class:`str` :param location: :class:`~sure.runtime.TestLocation` """ - if not isinstance(container, BaseContainer): - raise InternalRuntimeError( - f"expected {container} to be an instance of BaseContainer in this instance" - ) - try: return_value = container.unit() return ScenarioResult( @@ -604,17 +599,23 @@ def invoke_contextualized(self, container, context): class Feature(object): - def __init__(self, module): - name = getattr( + title: str + description: Optional[str] + ready: bool + module: Union[types.ModuleType, unittest.TestCase] + location: stypes.TestLocation + + def __init__(self, module: Union[types.ModuleType, unittest.TestCase]): + title = getattr( module, "suite_name", - getattr(module, "feature", getattr(module, "name", module.__name__)), + getattr(module, "feature", getattr(module, "title", module.__name__)), ) description = getattr( module, "suite_description", getattr(module, "description", "") ) - self.name = stripped(name) + self.title = stripped(title) self.description = stripped(description) self.module = module @@ -623,9 +624,9 @@ def __init__(self, module): def __repr__(self): if self.description: - return f'' + return f'' else: - return f'' + return f'' def read_scenarios(self, suts): self.scenarios = list(map((lambda e: Scenario(e, self)), suts)) @@ -634,9 +635,8 @@ def read_scenarios(self, suts): def run(self, reporter: Reporter, runtime: RuntimeOptions) -> stypes.FeatureResult: results = [] + context = RuntimeContext(reporter, runtime) for scenario in self.scenarios: - context = RuntimeContext(reporter, runtime) - result = scenario.run(context) results.append(result) @@ -654,6 +654,12 @@ def run(self, reporter: Reporter, runtime: RuntimeOptions) -> stypes.FeatureResu class Scenario(object): + name: str + description: Optional[str] + ready: bool + module: Union[types.ModuleType, unittest.TestCase] + location: stypes.TestLocation + def __init__(self, class_or_callable, feature): self.name = class_or_callable.__name__ self.log = logging.getLogger(self.name) @@ -674,14 +680,15 @@ def run(self, context: RuntimeContext): self, ).uncollapse_nested() results = [] + for collector in collectors: collector_results = [] context.reporter.on_scenario(collector.scenario) - for result, role in collector.run(context): if role == RuntimeRole.Unit: collector_results.append(result) - elif not result.is_success: + + if not result.is_success: if result.is_failure: context.reporter.on_failure(result.scenario, result) if context.options.immediate: @@ -690,11 +697,11 @@ def run(self, context: RuntimeContext): elif result.is_error: context.reporter.on_error(result.scenario, result) if context.options.immediate: - raise ExitError(context, result) + raise ExitError(context, result, report=False) - context.reporter.on_scenario_done( - collector.scenario, ScenarioResultSet(collector_results, context) - ) + context.reporter.on_scenario_done( + collector.scenario, result + ) results.extend(collector_results) return ScenarioResultSet(results, context) @@ -704,6 +711,12 @@ class BaseResult: purpose is to allow for distinguishing result-containing objects.""" def __repr__(self): + if not hasattr(self, 'label'): + raise NotImplementedError(f"{self.__class__} MUST define a `label' property or attribute which must be a string") + + if not isinstance(self.label, str): + raise NotImplementedError(f"{self.__class__}.label must be a string but is a {type(self.label)} instead") + return repr(self.label.lower()) @@ -735,9 +748,6 @@ def __init__( else: self.__error__ = treat_error(error, self.location) - def tb(self): - return traceback.format_exception(*self.exc_info) - @property def label(self) -> str: if self.ok: @@ -755,12 +765,7 @@ def __str__(self): ) def printable(self): - prelude = f"{self.location}" - hook = "" - if callable(getattr(self.error, "printable", None)): - hook = self.error.printable() - - return " ".join(filter(bool, [prelude, hook])) + return f"{self.location}" @property def is_error(self): @@ -771,9 +776,6 @@ def error(self) -> Optional[Exception]: if not isinstance(self.__error__, AssertionError): return self.__error__ - def set_error(self, error: Optional[Exception]): - self.__error__ = error - @property def is_failure(self): return isinstance(self.__failure__, AssertionError) @@ -793,9 +795,6 @@ def ok(self): @property def succinct_failure(self) -> str: - if not self.is_failure: - return "" - return self.stack.location_specific_error() @@ -808,17 +807,17 @@ def __init__(self, scenario_results: List[ScenarioResult], context: RuntimeConte self.failed_scenarios = [] self.errored_scenarios = [] - for scenario in scenario_results: - if scenario.is_failure: - self.failed_scenarios.append(scenario) - if scenario.is_error: - self.errored_scenarios.append(scenario) + for result in scenario_results: + if result.is_failure: + self.failed_scenarios.append(result) + if result.is_error: + self.errored_scenarios.append(result) def printable(self): if self.failure is not None: - return self.failure + return f"{self.failure.__class__.__name__}: {self.failure}" if self.error: - return self.error.printable() + return f"{self.error.__class__.__name__}: {self.error}" return "" @@ -832,17 +831,6 @@ def error(self) -> Optional[Exception]: if scenario.is_error: return scenario.error - @property - def stack(self) -> Optional[ErrorStack]: - for scenario in self.errored_scenarios: - return scenario.stack - - def __getattr__(self, attr): - try: - return self.__getattribute__(attr) - except AttributeError: - return getattr(self.scenario_results[-1], attr) - @property def is_failure(self): return len(self.failed_scenarios) > 0 @@ -853,35 +841,13 @@ def failure(self) -> Optional[Exception]: if scenario.is_failure: return scenario.failure - @property - def succinct_failure(self) -> Optional[str]: - for scenario in self.failed_scenarios: - if scenario.is_failure: - return scenario.succinct_failure - - @property - def first_scenario_result_error(self) -> Optional[ScenarioResult]: - for scenario in self.errored_scenarios: - if scenario.is_error: - return scenario - - @property - def first_scenario_result_fail(self) -> Optional[ScenarioResult]: - for scenario in self.failed_scenarios: - if scenario.is_failure: - return scenario - - @property - def first_nonsuccessful_result(self) -> Optional[ScenarioResult]: - return self.first_scenario_result_error or self.first_scenario_result_fail - -class FeatureResult(BaseResult): +class FeatureResult(ScenarioResultSet): scenario_results: ScenarioResultSet error: Optional[Exception] failure: Optional[AssertionError] - def __init__(self, scenario_results, error=None): + def __init__(self, scenario_results): self.scenario_results = scenario_results self.failed_scenarios = [] self.errored_scenarios = [] @@ -892,35 +858,6 @@ def __init__(self, scenario_results, error=None): if scenario.is_failure: self.failed_scenarios.append(scenario) - def printable(self): - if self.failure is not None: - return self.failure - if self.error: - return self.error.printable() - - return "" - - @property - def is_error(self): - return len(self.errored_scenarios) > 0 - - @property - def error(self) -> Optional[Exception]: - for scenario in self.errored_scenarios: - if scenario.is_error: - return scenario.error - - @property - def stack(self) -> Optional[ErrorStack]: - for scenario in self.errored_scenarios: - return scenario.stack - - def __getattr__(self, attr): - try: - return self.__getattribute__(attr) - except AttributeError: - return getattr(self.scenario_results[-1], attr) - @property def is_failure(self): return len(self.failed_scenarios) > 0 @@ -932,103 +869,31 @@ def failure(self) -> Optional[Exception]: return scenario.failure @property - def succinct_failure(self) -> Optional[str]: - for scenario in self.failed_scenarios: - if scenario.is_failure: - return scenario.succinct_failure + def is_error(self): + return len(self.errored_scenarios) > 0 @property - def first_scenario_result_error(self) -> Optional[ScenarioResult]: + def error(self) -> Optional[Exception]: for scenario in self.errored_scenarios: if scenario.is_error: - return scenario - - @property - def first_scenario_result_fail(self) -> Optional[ScenarioResult]: - for scenario in self.failed_scenarios: - if scenario.is_failure: - return scenario - - @property - def first_nonsuccessful_result(self) -> Optional[ScenarioResult]: - return self.first_scenario_result_error or self.first_scenario_result_fail + return scenario.error -class FeatureResultSet(BaseResult): +class FeatureResultSet(FeatureResult): error: Optional[Exception] failure: Optional[AssertionError] - def __init__(self, feature_results, error=None): - self.feature_results = feature_results - self.failed_features = [] - self.errored_features = [] - - for feature in feature_results: - if feature.is_error: - self.errored_features.append(feature) - if feature.is_failure: - self.failed_features.append(feature) - - def printable(self): - if self.failure is not None: - return self.failure - if self.error: - return self.error.printable() - - return "" - - @property - def is_error(self): - return len(self.errored_features) > 0 - - @property - def error(self) -> Optional[Exception]: - for feature in self.errored_features: - if feature.is_error: - return collapse_path(feature.error) - - @property - def stack(self) -> Optional[ErrorStack]: - for feature in self.errored_features: - return feature.stack - - def __getattr__(self, attr): - try: - return self.__getattribute__(attr) - except AttributeError: - return getattr(self.feature_results[-1], attr) - - @property - def is_failure(self): - return len(self.failed_features) > 0 - - @property - def failure(self) -> Optional[Exception]: - for feature in self.failed_features: - if feature.is_failure: - return feature.failure - - @property - def succinct_failure(self) -> Optional[str]: - for feature in self.failed_features: - if feature.is_failure: - return feature.succinct_failure - @property - def first_scenario_result_error(self) -> Optional[FeatureResult]: - for feature_result in self.errored_features: - if feature_result.is_error: - return feature_result.first_nonsuccessful_result + def feature_results(self): + return self.scenario_results @property - def first_scenario_result_fail(self) -> Optional[FeatureResult]: - for feature_result in self.failed_features: - if feature_result.is_failure: - return feature_result.first_nonsuccessful_result + def failed_features(self): + return self.failed_scenarios @property - def first_nonsuccessful_result(self) -> Optional[FeatureResult]: - return self.first_scenario_result_error or self.first_scenario_result_fail + def errored_features(self): + return self.errored_scenarios def stripped(string): diff --git a/sure/special.py b/sure/special.py index af2aebc..b598a6c 100644 --- a/sure/special.py +++ b/sure/special.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) <2012-2023> Gabriel Falcão +# Copyright (C) <2012-2024> Gabriel Falcão # Copyright (C) <2012> Lincoln Clarete # # This program is free software: you can redistribute it and/or modify diff --git a/sure/version.py b/sure/version.py index 9f8ea32..3477736 100644 --- a/sure/version.py +++ b/sure/version.py @@ -1 +1 @@ -version = "3.0a0" +version = "3.0a1" diff --git a/tests/functional/loader/test_loader.py b/tests/functional/loader/test_loader.py index 8c5ab03..074dead 100644 --- a/tests/functional/loader/test_loader.py +++ b/tests/functional/loader/test_loader.py @@ -29,7 +29,7 @@ loader, ) -fake_packages_path = Path(__file__).parent.joinpath("fake_packages") +fake_packages_path = Path(__file__).parent.joinpath("fake_packages").absolute() def test_get_package_upmost__init__containing(): @@ -178,6 +178,24 @@ def test_loader_load_python_path_returns_empty_list_when_given_path_is_a_directo send_runtime_warning.assert_called_once_with(f"ignoring {fake_packages_path} for being a directory") +@patch('sure.loader.send_runtime_warning') +@patch('sure.loader.Path') +def test_loader_load_python_path_returns_empty_list_when_given_path_is_a_broken_symlink(Path, send_runtime_warning): + "sure.loader.loader.load_python_path() should return empty list when receiving a :class:`pathlib.Path` that points to a broken symlink" + + path = Path.return_value + path.is_dir.return_value = False + path.is_symlink.return_value = True + path.resolve.return_value.exists.return_value = False + path.absolute.return_value = "absolute-path-dummy" + modules = loader.load_python_path(fake_packages_path) + + expects(modules).to.be.a(list) + expects(modules).to.be.empty + + send_runtime_warning.assert_called_once_with("parsing skipped of irregular file `absolute-path-dummy'") + + @patch('sure.loader.send_runtime_warning') def test_loader_load_python_path_returns_empty_list_when_given_path_seems_to_be_a_dunder_file(send_runtime_warning): "sure.loader.loader.load_python_path() should return empty list when receiving a :class:`pathlib.Path` that is a directory" diff --git a/tests/functional/test_runner.py b/tests/functional/test_runner.py index 6c156e2..7bffb47 100644 --- a/tests/functional/test_runner.py +++ b/tests/functional/test_runner.py @@ -31,7 +31,8 @@ ScenarioResult, ScenarioResultSet, TestLocation, - FeatureResultSet, RuntimeContext + FeatureResultSet, + RuntimeContext, ) from sure.reporters import test @@ -64,7 +65,7 @@ def test_runner_load_features_from_module_containing_unittest_cases(): expects(feature).to.have.property("description").being.equal( "Module with :class:`unittest.TestCase` subclasses" ) - expects(feature).to.have.property("name").being.equal( + expects(feature).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_unittest_test_cases" ) expects(feature).to.have.property("ready").being.equal(True) @@ -107,7 +108,7 @@ def test_runner_load_features_from_module_path_recursively(): expects(featureA).to.equal expects(featureA).to.be.a(Feature) - expects(featureA).to.have.property("name").being.equal( + expects(featureA).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_function_members" ) expects(featureA).to.have.property("ready").being.equal(True) @@ -116,9 +117,8 @@ def test_runner_load_features_from_module_path_recursively(): expects(featureB).to.equal expects(featureB).to.be.a(Feature) - expects(featureB).to.have.property("name").being.equal( + expects(featureB).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_members" - ) expects(featureB).to.have.property("ready").being.equal(True) expects(featureB).to.have.property("scenarios").being.a(list) @@ -126,7 +126,7 @@ def test_runner_load_features_from_module_path_recursively(): expects(featureC).to.equal expects(featureC).to.be.a(Feature) - expects(featureC).to.have.property("name").being.equal( + expects(featureC).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_nonunittest_test_cases" ) expects(featureC).to.have.property("ready").being.equal(True) @@ -135,7 +135,7 @@ def test_runner_load_features_from_module_path_recursively(): expects(featureX).to.equal expects(featureX).to.be.a(Feature) - expects(featureX).to.have.property("name").being.equal( + expects(featureX).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_unittest_test_cases" ) expects(featureX).to.have.property("ready").being.equal(True) @@ -166,7 +166,7 @@ def test_runner_load_features_from_directory_with_python_files(): expects(feature).to.have.property("description").being.equal( "Module with :class:`unittest.TestCase` subclasses" ) - expects(feature).to.have.property("name").being.equal( + expects(feature).to.have.property("title").being.equal( "tests.functional.modules.success.module_with_unittest_test_cases" ) expects(feature).to.have.property("ready").being.equal(True) @@ -176,14 +176,14 @@ def test_runner_load_features_from_directory_with_python_files(): expects(scenarioA).to.be.a(Scenario) expects(scenarioB).to.be.a(Scenario) - expects(scenarioA.name).to.equal("TestCaseA") + expects(scenarioA).should.have.property("name").to.equal("TestCaseA") expects(scenarioA.description).to.equal("Description of TestCaseA") expects(scenarioA.location).to.be.a(TestLocation) expects(scenarioA.location.path_and_lineno).to.equal( f"{collapse_path(unittest_testcases_module_path)}:23" ) - expects(scenarioB.name).to.equal("TestCaseB") + expects(scenarioB).should.have.property("name").to.equal("TestCaseB") expects(scenarioB.description).to.be.empty expects(scenarioB.location).to.be.a(TestLocation) expects(scenarioB.location.path_and_lineno).to.equal( @@ -204,7 +204,7 @@ def test_runner_execute_success_tests(): expects(feature_result_set).to.be.a(FeatureResultSet) expects(feature_result_set).to.have.property("feature_results").being.length_of(4) expects(feature_result_set).to.have.property("failed_features").being.empty - expects(feature_result_set).to.have.property("errored_scenarios").being.empty + expects(feature_result_set).to.have.property("errored_features").being.empty expects(dict(test.events)).to.equal( { @@ -265,6 +265,8 @@ def test_runner_execute_success_tests(): (anything_of_type(float), "test_function_Z", "ok"), (anything_of_type(float), "TestCase", "ok"), (anything_of_type(float), "UnitCase", "ok"), + (anything_of_type(float), "UnitCase", "ok"), + (anything_of_type(float), "UnitCase", "ok"), (anything_of_type(float), "test_function_A", "ok"), (anything_of_type(float), "test_function_B", "ok"), (anything_of_type(float), "test_function_C", "ok"), @@ -272,6 +274,10 @@ def test_runner_execute_success_tests(): (anything_of_type(float), "TestCaseA", "ok"), (anything_of_type(float), "TestCaseA", "ok"), (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), @@ -280,6 +286,14 @@ def test_runner_execute_success_tests(): (anything_of_type(float), "TestCaseA", "ok"), (anything_of_type(float), "TestCaseA", "ok"), (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseA", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), + (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), (anything_of_type(float), "TestCaseB", "ok"), diff --git a/tests/test_assertion_builder.py b/tests/test_assertion_builder.py index 90842b8..26105c2 100644 --- a/tests/test_assertion_builder.py +++ b/tests/test_assertion_builder.py @@ -35,7 +35,7 @@ def test_4_equal_2p2(): - "this(4).should.equal(2 + 2)" + "expects(4).should.equal(2 + 2)" time = datetime.now() - timedelta(0, 60) @@ -57,7 +57,7 @@ def incorrect_negative_expectation(): def test_2_within_0a2(): - "this(1).should.be.within(0, 2)" + "expects(1).should.be.within(0, 2)" expect(1).should.be.within(0, 2) expect(4).should_not.be.within(0, 2) @@ -76,7 +76,7 @@ def opposite_not(): def test_true_to_be_ok(): - "this(True).should.be.ok" + "expects(True).should.be.ok" expect(True).should.be.ok expect(False).should_not.be.ok @@ -95,7 +95,7 @@ def opposite_not(): def test_falsy(): - "this(False).should.be.false" + "expects(False).should.be.false" expect(False).should.be.falsy expect(True).should_not.be.falsy @@ -114,7 +114,7 @@ def opposite_not(): def test_none(): - "this(None).should.be.none" + "expects(None).should.be.none" expect(None).should.be.none expect(not None).should_not.be.none @@ -133,7 +133,7 @@ def opposite_not(): def test_should_be_a(): - "this(None).should.be.none" + "expects(None).should.be.none" expect(1).should.be.an(int) expect([]).should.be.a('collections.abc.Iterable') @@ -153,7 +153,7 @@ def opposite_not(): def test_should_be_callable(): - "this(function).should.be.callable" + "expects(function).should.be.callable" expect(lambda: None).should.be.callable expect("aa").should_not.be.callable @@ -175,7 +175,7 @@ def opposite_not(): def test_iterable_should_be_empty(): - "this(iterable).should.be.empty" + "expects(iterable).should.be.empty" expect([]).should.be.empty expect([1, 2, 3]).should_not.be.empty @@ -196,7 +196,7 @@ def opposite_not(): def test_iterable_should_have_length_of(): - "this(iterable).should.have.length_of(N)" + "expects(iterable).should.have.length_of(N)" expect({'foo': 'bar', 'a': 'b'}).should.have.length_of(2) expect([1, 2, 3]).should_not.have.length_of(4) @@ -218,7 +218,7 @@ def opposite_not(): def test_greater_than(): - "this(X).should.be.greater_than(Y)" + "expects(X).should.be.greater_than(Y)" expect(5).should.be.greater_than(4) expect(1).should_not.be.greater_than(2) @@ -239,7 +239,7 @@ def opposite_not(): def test_greater_than_or_equal_to(): - "this(X).should.be.greater_than_or_equal_to(Y)" + "expects(X).should.be.greater_than_or_equal_to(Y)" expect(4).should.be.greater_than_or_equal_to(4) expect(1).should_not.be.greater_than_or_equal_to(2) @@ -260,7 +260,7 @@ def opposite_not(): def test_lower_than(): - "this(X).should.be.lower_than(Y)" + "expects(X).should.be.lower_than(Y)" expect(4).should.be.lower_than(5) expect(2).should_not.be.lower_than(1) @@ -281,7 +281,7 @@ def opposite_not(): def test_lower_than_or_equal_to(): - "this(X).should.be.lower_than_or_equal_to(Y)" + "expects(X).should.be.lower_than_or_equal_to(Y)" expect(5).should.be.lower_than_or_equal_to(5) expect(2).should_not.be.lower_than_or_equal_to(1) @@ -302,21 +302,21 @@ def opposite_not(): def test_assertion_builder_be__call__(): - "this(ACTUAL).should.be(EXPECTED) where ACTUAL and EXPECTED are evaluated as identical in Python" + "expects(ACTUAL).should.be(EXPECTED) where ACTUAL and EXPECTED are evaluated as identical in Python" d1 = {} d2 = d1 d3 = {} - assert isinstance(this(d2).should.be(d1), bool) + assert isinstance(expects(d2).should.be(d1), bool) expect(d2).should.be(d1) expect(d3).should_not.be(d1) def wrong_should(): - return this(d3).should.be(d1) + return expects(d3).should.be(d1) def wrong_should_not(): - return this(d2).should_not.be(d1) + return expects(d2).should_not.be(d1) expect(wrong_should_not).when.called.should.throw( AssertionError, @@ -329,7 +329,7 @@ def wrong_should_not(): def test_have_property(): - "this(instance).should.have.property(property_name)" + "expects(instance).should.have.property(property_name)" class ChemicalElement(object): name = "Uranium" @@ -360,7 +360,7 @@ def opposite_not(): def test_have_property_with_value(): - ("this(instance).should.have.property(property_name).being or " + ("expects(instance).should.have.property(property_name).being or " ".with_value should allow chain up") class ChemicalElement(object): @@ -394,7 +394,7 @@ def opposite_not(): def test_have_key(): - "this(dictionary).should.have.key(key_data)" + "expects(dictionary).should.have.key(key_data)" data_structure = {'data': "binary blob"} @@ -420,7 +420,7 @@ def opposite_not(): def test_have_key_with_value(): - ("this(dictionary).should.have.key(key_name).being or " + ("expects(dictionary).should.have.key(key_name).being or " ".with_value should allow chain up") chemical_element = dict(name="Uranium") @@ -450,7 +450,7 @@ def opposite_not(): def test_look_like(): - "this(' aa \n ').should.look_like('aa')" + "expects(' aa \n ').should.look_like('aa')" expect(' \n aa \n ').should.look_like('AA') expect(' \n bb \n ').should_not.look_like('aa') @@ -861,3 +861,69 @@ def trigger(): AssertionError, "test [tests/test_assertion_builder.py line 853] did not run within one microseconds" ) + + +def test_assertion_builder_with_args(): + "AssertionBuilder() should accept `with_args' option" + + assertion_builder = AssertionBuilder("ab", with_args=['a', 'b']) + assert isinstance(getattr(assertion_builder, "_callable_args", None), list) + assert getattr(assertion_builder, "_callable_args", None) == ["a", "b"] + + assertion_builder = AssertionBuilder("ab", with_args=('a', 'b')) + assert isinstance(getattr(assertion_builder, "_callable_args", None), list) + assert getattr(assertion_builder, "_callable_args", None) == ["a", "b"] + + +def test_assertion_builder_with_kws(): + "AssertionBuilder() should accept `with_kws' option" + + assertion_builder = AssertionBuilder("test", with_kws={"foo": "bar"}) + assert isinstance(getattr(assertion_builder, "_callable_kw", None), dict) + assert getattr(assertion_builder, "_callable_kw", None) == {"foo": "bar"} + + assertion_builder = AssertionBuilder("test", with_kws={"foo": "bar"}) + assert isinstance(getattr(assertion_builder, "_callable_kw", None), dict) + assert getattr(assertion_builder, "_callable_kw", None) == {"foo": "bar"} + + +def test_assertion_builder_and_kws(): + "AssertionBuilder() should accept `and_kws' option" + + assertion_builder = AssertionBuilder("test", and_kws={"foo": "bar"}) + assert isinstance(getattr(assertion_builder, "_callable_kw", None), dict) + assert getattr(assertion_builder, "_callable_kw", None) == {"foo": "bar"} + + assertion_builder = AssertionBuilder("test", and_kws={"foo": "bar"}) + assert isinstance(getattr(assertion_builder, "_callable_kw", None), dict) + assert getattr(assertion_builder, "_callable_kw", None) == {"foo": "bar"} + + +def test_list_should_equal_list_empty_or_different_size(): + "expects(iterableA).to.equal(iterableZ)" + + expects([1, 2, 3]).to.equal([1, 2, 3]) + + def compare_with_empty_list(): + expects([1, 2, 3]).to.equal([]) + + def compare_with_shorter_length(): + expects([1, 2, 3]).to.equal([1, 2]) + + def compare_with_longer_length(): + expects([1, 2, 3]).to.equal([1, 2, 4, 6]) + + expect(compare_with_empty_list).when.called.to.throw(AssertionError) + expect(compare_with_empty_list).when.called.to.throw( + "X has 3 items whereas Y is empty" + ) + + expect(compare_with_shorter_length).when.called.to.throw(AssertionError) + expect(compare_with_shorter_length).when.called.to.throw( + "X has 3 items whereas Y has only 2" + ) + + expect(compare_with_longer_length).when.called.to.throw(AssertionError) + expect(compare_with_longer_length).when.called.to.throw( + "Y has 4 items whereas X has only 3" + ) diff --git a/tests/test_assertion_builder_assertion_methods.py b/tests/test_assertion_builder_assertion_methods.py new file mode 100644 index 0000000..5c550b3 --- /dev/null +++ b/tests/test_assertion_builder_assertion_methods.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""tests for :class:`sure.AssertionBuilder` properties defined with the +decorator :func:`sure.assertionmethod`""" +import re +import sys +from sure import expects +from sure.doubles import anything_of_type + + +def test_contains_and_to_contain(): + "expects.that().contains and expects().to_contain" + + expects.that(set(range(8))).contains(7) + expects(set(range(13))).to.contain(8) + expects(set(range(33))).to_contain(3) + expects(set()).to_contain.when.called_with("art").should.have.raised("`art' should be in `set()'") + + +def test_does_not_contain_and_to_not_contain(): + "expects().contains and expects().to_not_contain" + + expects(list()).to_not_contain("speculations") + expects(set(range(33))).to_not_contain.when.called_with(3).should.have.raised("`3' should not be in `{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}'") + + +def test_within(): + "expects().to.be.within()" + + expects(3).to.be.within(set(range(33))) + expects(7).to.be.within.when.called_with(0, 0, 6).should.have.raised( + "(7).should.be.within(0, 0, 6) must be called with either an iterable:\n" + "(7).should.be.within([1, 2, 3, 4])\n" + "or with a range of numbers, i.e.: `(7).should.be.within(1, 3000)'" + ) + expects(3).to.be.within(0, 7) + expects(1).to_not.be.within.when.called_with(0, 0, 7).should.have.raised( + "(1).should_not.be.within(0, 0, 7) must be called with either an iterable:\n" + ) + expects(3).to.not_be.within.when.called_with(set(range(33))).should.have.raised( + "`3' should not be in `{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}'" + ) + expects("art").to.be.within.when.called_with({"set"}).should.have.raised( + "`art' should be in `{'set'}'" + ) + + +def test_different_of(): + "expects().to.be.different_of()" + + expects("").to.be.different_of.when.called_with([]).should.have.raised( + ".different_of only works for string comparison but in this case is expecting [] () instead" + ) + + expects([]).to.be.different_of.when.called_with("").should.have.raised( + ".different_of only works for string comparison but in this case the actual source comparison object is [] () instead" + ) + + +def test_is_a(): + "expects().to.be.a()" + + expects(b"a").to.be.a(bytes) + expects(sys.stdout).to.be.a.when.called_with("io.StringIO").to.have.raised( + "expects(sys.stdout).to.be.a.when.called_with(\"io.StringIO\").to.have.raised( expects `<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>' to be an `io.StringIO'" + ) + expects(sys.stdout).to.not_be.a.when.called_with("io.TextIOWrapper").to.have.raised( + "expects(sys.stdout).to.not_be.a.when.called_with(\"io.TextIOWrapper\").to.have.raised( expects `<_io.TextIOWrapper name='' mode='w' encoding='utf-8'>' to not be an `io.TextIOWrapper'" + ) + + +def test_to_be_below(): + "expects().to.be.below()" + + expects(b"A").to.be.below(b"a") + expects(b"a").to.below.when.called_with(b"A").should.have.raised( + "b'a' should be below b'A'" + ) + expects(70).to_not.be.below.when.called_with(83).should.have.raised( + "70 should not be below 83" + ) + expects(b"a").to.below.when.called_with(b"A").should_not.have.raised.when.called_with( + "b'a' should be below b'A'" + ).should.have.thrown.when.called_with("`below' called with args (b'A',) and keyword-args {} should not raise b'a' should be below b'A' but raised b'a' should be below b'A'").should.return_value(not False) + + +def test_to_be_above(): + "expects().to.be.above()" + + expects(b"S").to.be.above(b"B") + expects(b"D").to.be.above.when.called_with(b"S").should.have.raised( + "b'D' should be above b'S'" + ) + expects(115).to.not_be.above.when.called_with(102).to.have.raised( + "115 should not be above 102" + ) + + +def test_to_match(): + "expects().to.match() REGEX" + + expects("ROBSON").to.match(r"(^RO|.OB.{3})") + expects("robson").to.match(r"(^RO|.OB.{3})", re.I) + expects("OM").to.match(r"S?[OU][NM]") + expects("ON").to.match(r"S?[OU][NM]") + expects("SOM").to.match(r"S?[OU][NM]") + expects("SON").to.match(r"S?[OU][NM]") + expects("SUM").to.match(r"S?[OU][MN]") + expects("SUN").to.match(r"S?[OU][MN]") + expects("UM").to.match(r"S?[OU][MN]") + expects("UN").to.match(r"S?[OU][MN]") + expects("NOS").to_not.match(r"S?[OU][MN]") + expects(list("OHMS")).to.match.when.called_with("Ω").should.have.raised( + "['O', 'H', 'M', 'S'] should be a string in order to compare using .match()" + ) diff --git a/tests/test_assertion_builder_assertion_properties.py b/tests/test_assertion_builder_assertion_properties.py new file mode 100644 index 0000000..9b1af1a --- /dev/null +++ b/tests/test_assertion_builder_assertion_properties.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""tests for :class:`sure.AssertionBuilder` properties defined with the +decorator :func:`sure.assertionproperty`""" + +from sure import expects +from sure.doubles import anything_of_type + + +def test_not_have(): + "expects().to.not_have" + + class WaveFunctionParameters: + period = anything_of_type(float) + amplitude = anything_of_type(float) + frequency = anything_of_type(float) + + expects(WaveFunctionParameters).to.not_have.property("unrequested_phase_change") + expects(WaveFunctionParameters).to.have.property("frequency").which.should.equal(anything_of_type(float)) diff --git a/tests/test_loader_astutil.py b/tests/test_loader_astutil.py index 26df8b7..b8a59d8 100644 --- a/tests/test_loader_astutil.py +++ b/tests/test_loader_astutil.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import unittest from unittest import TestCase +from unittest.mock import patch from sure import expects from sure.loader.astutil import gather_class_definitions_from_module_path, gather_class_definitions_node @@ -24,7 +25,7 @@ class TestLoaderAstUtilBaseClassName(TestCase): def test_gather_class_definitions_from_module_path(self): classes = gather_class_definitions_from_module_path(__file__) expects(classes).to.equal( - {'TestLoaderAstUtilBaseClassName': (23, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (31, ('unittest.TestCase',))} + {'TestLoaderAstUtilBaseClassName': (24, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (32, ('unittest.TestCase',))} ) @@ -32,7 +33,7 @@ class TestLoaderAstUtilBaseClassAttributeAndName(unittest.TestCase): def test_gather_class_definitions_from_module_path(self): classes = gather_class_definitions_from_module_path(__file__) expects(classes).to.equal( - {'TestLoaderAstUtilBaseClassName': (23, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (31, ('unittest.TestCase',))} + {'TestLoaderAstUtilBaseClassName': (24, ('TestCase',)), 'TestLoaderAstUtilBaseClassAttributeAndName': (32, ('unittest.TestCase',))} ) @@ -40,3 +41,17 @@ def test_gather_class_definitions_node_with_string(): "sure.laoder.astutil.gather_class_definitions_node() with a string" expects(gather_class_definitions_node("string", classes={})).to.equal({}) + + +@patch('sure.loader.astutil.send_runtime_warning') +@patch('sure.loader.astutil.Path') +def test_gather_class_definitions_from_module_path_symlink(Path, send_runtime_warning): + "sure.laoder.astutil.gather_class_definitions_from_module_path() with a symlink" + + path = Path.return_value + path.is_symlink.return_value = True + path.resolve.return_value.exists.return_value = False + path.absolute.return_value = "absolute-path-dummy" + expects(gather_class_definitions_from_module_path("path")).to.equal({}) + Path.assert_called_once_with("path") + send_runtime_warning.assert_called_once_with("parsing skipped of irregular file `absolute-path-dummy'") diff --git a/tests/test_original_api.py b/tests/test_original_api.py index 5c679a3..acacb21 100644 --- a/tests/test_original_api.py +++ b/tests/test_original_api.py @@ -17,9 +17,7 @@ import os import sure import time - from datetime import datetime - from sure import that, this from sure import expects from sure import action_for @@ -27,10 +25,11 @@ from sure import within from sure import second, miliseconds from sure import StagingArea +from sure.doubles import Dummy, anything from sure.errors import WrongUsageError from sure.special import is_cpython from sure.loader import collapse_path -from sure.original import all_integers +from sure.original import all_integers, AssertionHelper def test_setup_with_context(): @@ -63,7 +62,7 @@ def it_crashes(): assert that(it_crashes).raises( TypeError, ( - "the function it_crashes defined at tests/test_original_api.py line 60, is being " + "the function it_crashes defined at tests/test_original_api.py line 59, is being " "decorated by either @that_with_context or @scenario, so it should " "take at least 1 parameter, which is the test context" ), @@ -104,15 +103,6 @@ def data_structure_was_modified(context): assert not hasattr(data_structure, "modified") -def test_that_is_a(): - "that() is_a(object)" - - data_structure = "data_structure" - - assert that(data_structure).is_a(str) - assert isinstance(data_structure, str) - - def test_that_equals(): "that() equals(string)" @@ -122,69 +112,6 @@ def test_that_equals(): assert data_structure == "data_structure" -def test_that_differs(): - "that() differs(object)" - - data_structure = "data_structure" - - assert that(data_structure).differs("23123%FYTUGIHOfdf") - assert data_structure != "23123%FYTUGIHOfdf" - - -def test_that_has(): - "that() has(object)" - - class Class: - value = "some class" - - Object = Class() - dictionary = { - "value": "Value", - } - value = "value" - - assert hasattr(Class, "value") - expects(Class).has("value") - expects(Class).to.be.like("value") - assert "value" in that(Class) - - assert hasattr(Object, "value") - expects(Object).has("value") - expects(Object).to.be.like("value") - assert "value" in that(Object) - - assert "value" in dictionary - expects(dictionary).has("value") - expects(dictionary).to.be.like("value") - assert "value" in that(dictionary) - - expects(value).has("value") - expects(value).to.be.like("value") - assert "value" in that(value) - expects(value).has("va") - expects(value).to.be.like("va") - assert "val" in that(value) - expects(value).has("val") - expects(value).to.be.like("ue") - assert "ue" in that(value) - - -def test_that_at_key_equals(): - "that().at(object).equals(object)" - - class Class: - attribute = "some class" - - Object = Class() - dictionary = { - "attribute": "data_structure", - } - - assert that(Class).at("attribute").equals("some class") - assert that(Object).at("attribute").equals("some class") - assert that(dictionary).at("attribute").equals("data_structure") - - def test_that_len_is(): "that() len_is(number)" @@ -339,28 +266,11 @@ def __repr__(self): assert that(shapes, within_range=(1, 2)).the_attribute("name").equals("square") -def test_that_checking_all_elements(): - "that(iterable).every_item_is('value')" - shapes = [ - "cube", - "ball", - "ball", - "piramid", - ] - - assert shapes[0] != "ball" - assert shapes[3] != "ball" - - assert shapes[1] == "ball" - assert shapes[2] == "ball" - - assert that(shapes, within_range=(1, 2)).every_item_is("ball") - - def test_that_checking_each_matches(): "that(iterable).in_each('').equals('value')" class animal(object): + def __init__(self, kind): self.attributes = { "class": "mammal", @@ -377,7 +287,6 @@ def __init__(self, kind): assert animals[0].attributes["kind"] != "cow" assert animals[1].attributes["kind"] != "cow" - assert animals[2].attributes["kind"] == "cow" assert animals[3].attributes["kind"] == "cow" assert animals[4].attributes["kind"] == "cow" @@ -401,17 +310,9 @@ def __init__(self, kind): .matches(["dog", "cat", "cow", "cow", "cow"]) ) - try: - assert that(animals).in_each("attributes['kind']").matches(["dog"]) - assert False, "should not reach here" - except AssertionError as e: - assert that(str(e)).equals( - "%r has 5 items, but the matching list has 1: %r" - % ( - ["dog", "cat", "cow", "cow", "cow"], - ["dog"], - ) - ) + expects(that(animals).in_each("attributes['kind']").matches).when.called_with(["dog"]).should.have.raised( + f"{repr(['dog', 'cat', 'cow', 'cow', 'cow'])} has 5 items, but the matching list has 1: {repr(['dog'])}" + ) def test_that_raises(): @@ -457,9 +358,7 @@ def function(arg1=None, arg2=None): assert called called = False - assert that(function, with_args=[1], and_kws={"arg2": 2}).raises( - "yeah, it failed" - ) + assert that(function, with_args=[1], and_kws={"arg2": 2}).raises("yeah, it failed") assert called called = False @@ -650,9 +549,7 @@ def __call__(self): range_name = range.__name__ assert that(fail_1).raises("X is a list and Y is a {0} instead".format(range_name)) assert that(Fail2).raises("X is a {0} and Y is a list instead".format(range_name)) - assert that(Fail3()).raises( - "X is a {0} and Y is a list instead".format(range_name) - ) + assert that(Fail3()).raises("X is a {0} and Y is a list instead".format(range_name)) def test_within_pass(): @@ -663,111 +560,11 @@ def test_within_pass(): def test_within_five_milicesonds_fails_when_function_takes_six_miliseconds(): "within(five=miliseconds) should fail when the decorated function takes six miliseconds to run" - def sleepy(*a): time.sleep(0.6) - failed = False - try: - within(five=miliseconds)(sleepy)() - except AssertionError as e: - failed = True - expects("sleepy [tests/test_original_api.py line 667] did not run within five miliseconds").to.equal(str(e)) - - assert failed, "within(five=miliseconds)(sleepy) did not fail" - - -def test_word_to_number(): - expects(sure.word_to_number("one")).to.equal(1) - expects(sure.word_to_number("two")).to.equal(2) - expects(sure.word_to_number("three")).to.equal(3) - expects(sure.word_to_number("four")).to.equal(4) - expects(sure.word_to_number("five")).to.equal(5) - expects(sure.word_to_number("six")).to.equal(6) - expects(sure.word_to_number("seven")).to.equal(7) - expects(sure.word_to_number("eight")).to.equal(8) - expects(sure.word_to_number("nine")).to.equal(9) - expects(sure.word_to_number("ten")).to.equal(10) - expects(sure.word_to_number("eleven")).to.equal(11) - expects(sure.word_to_number("twelve")).to.equal(12) - expects(sure.word_to_number("thirteen")).to.equal(13) - expects(sure.word_to_number("fourteen")).to.equal(14) - expects(sure.word_to_number("fifteen")).to.equal(15) - expects(sure.word_to_number("sixteen")).to.equal(16) - - -def test_word_to_number_fail(): - failed = False - try: - sure.word_to_number("twenty") - except AssertionError as e: - failed = True - expects(str(e)).to.equal( - "sure supports only literal numbers from one " - 'to sixteen, you tried the word "twenty"' - ) - - assert failed, "should raise assertion error" - - -def test_microsecond_unit(): - "testing microseconds convertion" - cfrom, cto = sure.UNITS[sure.microsecond] - - expects(cfrom(1)).to.equal(100000) - expects(cto(1)).to.equal(1) - - cfrom, cto = sure.UNITS[sure.microseconds] - - expects(cfrom(1)).to.equal(100000) - expects(cto(1)).to.equal(1) - - -def test_milisecond_unit(): - "testing miliseconds convertion" - cfrom, cto = sure.UNITS[sure.milisecond] - - expects(cfrom(1)).to.equal(1000) - expects(cto(100)).to.equal(1) - - cfrom, cto = sure.UNITS[sure.miliseconds] - - expects(cfrom(1)).to.equal(1000) - expects(cto(100)).to.equal(1) - - -def test_second_unit(): - "testing seconds convertion" - cfrom, cto = sure.UNITS[sure.second] - - expects(cfrom(1)).to.equal(1) - expects(cto(100000)).to.equal(1) - - cfrom, cto = sure.UNITS[sure.seconds] - - expects(cfrom(1)).to.equal(1) - expects(cto(100000)).to.equal(1) - - -def test_minute_unit(): - "testing minutes convertion" - cfrom, cto = sure.UNITS[sure.minute] - - expects(cfrom(60)).to.equal(1) - expects(cto(1)).to.equal(6000000) - - cfrom, cto = sure.UNITS[sure.minutes] - - expects(cfrom(60)).to.equal(1) - expects(cto(1)).to.equal(6000000) - - -def test_within_wrong_usage(): - "within(three=miliseconds, one=second) should raise WrongUsageError" - - expects(within).when.called_with(three=miliseconds, one=second).to.have.raised( - WrongUsageError, - "within() takes a single keyword argument where the argument must be a numerical description from one to eighteen and the value. For example: within(eighteen=miliseconds)" + expects(within(five=miliseconds)(sleepy)).when.called.to.have.raised( + "sleepy [tests/test_original_api.py line 563] did not run within five miliseconds" ) @@ -910,9 +707,9 @@ def test_fails_when_action_doesnt_fulfill_the_agreement_of_its_provides_argument error = ( 'the action "unreasonable_action" is supposed to provide the ' 'attribute "two" into the context but does not. ' - 'Check its implementation for correctness or, if ' - 'there is a bug in Sure, consider reporting that at ' - 'https://github.com/gabrielfalcao/sure/issues' + "Check its implementation for correctness or, if " + "there is a bug in Sure, consider reporting that at " + "https://github.com/gabrielfalcao/sure/issues" ) def with_setup(context): @@ -923,9 +720,11 @@ def unreasonable_action(): @scenario(with_setup) def reasoning_of_an_unreasonable_action(context): expects(context.unreasonable_action).to.have.raised(AssertionError, error) - return 'relativist' + return "relativist" - expects(reasoning_of_an_unreasonable_action).when.called.to.return_value('relativist') + expects(reasoning_of_an_unreasonable_action).when.called.to.return_value( + "relativist" + ) def test_depends_on_failing_due_to_lack_of_attribute_in_context(): @@ -933,7 +732,7 @@ def test_depends_on_failing_due_to_lack_of_attribute_in_context(): fullpath = collapse_path(os.path.abspath(__file__)) error = ( - f'the action "variant_action" defined at {fullpath}:942 ' + f'the action "variant_action" defined at {fullpath}:741 ' 'depends on the attribute "data_structure" to be available in the' " current context" ) @@ -955,11 +754,12 @@ def test_depends_on_failing_due_not_calling_a_previous_action(): "it fails when an action depends on some attribute that is being " "provided by other actions" fullpath = collapse_path(os.path.abspath(__file__)) + error = ( - 'the action "my_action" defined at {0}:970 ' + 'the action "my_action" defined at {0}:770 ' 'depends on the attribute "some_attr" to be available in the context.' " Perhaps one of the following actions might provide that attribute:\n" - " -> dependency_action at {0}:966".replace("{0}", fullpath) + " -> dependency_action at {0}:766".replace("{0}", fullpath) ) def with_setup(context): @@ -1035,7 +835,7 @@ def access_nonexisting_attribute(): ) -def test_actions_providing_dinamically_named_variables(): +def test_actions_providing_dynamically_named_variables(): "the actions should be able to declare the variables they provide" def with_setup(context): @@ -1156,7 +956,7 @@ def assertions(): "X = ['one', 'yeah']\n" " and\n" "Y = ['one', 'yeah', 'damn']\n" - "Y has 3 items whereas X has only 2" + "Y has 3 items whereas X has only 2", ) @@ -1179,7 +979,7 @@ def assertions(): "X = {'three': 'value'}\n" " and\n" "Y = {'two': 'value'}\n" - "X has the key \"'three'\" whereas Y does not" + "X has the key \"'three'\" whereas Y does not", ) @@ -1459,7 +1259,7 @@ def assertions(): "X = {'index': [{'age': 33, 'name': 'JC'}]}\n" " and\n" "Y = {'index': [{'age': 31, 'foo': 'bar', 'name': 'JC'}]}\n" - "X['index'][0] does not have the key \"'foo'\" whereas Y['index'][0] has it" + "X['index'][0] does not have the key \"'foo'\" whereas Y['index'][0] has it", ) @@ -1486,7 +1286,7 @@ def assertions(): "X = {'index': [{'age': 33, 'foo': 'bar', 'name': 'JC'}]}\n" " and\n" "Y = {'index': [{'age': 31, 'name': 'JC'}]}\n" - "X['index'][0] has the key \"'foo'\" whereas Y['index'][0] does not" + "X['index'][0] has the key \"'foo'\" whereas Y['index'][0] does not", ) @@ -1513,7 +1313,7 @@ def assertions(): "X = {'index': [{'age': 33, 'foo': 'bar', 'name': 'JC'}]}\n" " and\n" "Y = {'index': [{'age': 33, 'bar': 'foo', 'name': 'JC'}]}\n" - "X['index'][0] has the key \"'foo'\" whereas Y['index'][0] does not" + "X['index'][0] has the key \"'foo'\" whereas Y['index'][0] does not", ) @@ -1583,6 +1383,7 @@ def fail(): def test_raises_with_string(): "that(callable).raises('message') should compare the message" + def it_fails(): raise AssertionError("should fail with this exception") @@ -1644,12 +1445,46 @@ def test_deep_comparison_sequences_of_sequences(): try: expects(part1).equals(part2) except AssertionError as e: - expects(str(e)).to_not.be.different_of("""Equality Error -X = [('Bootstraping Redis role', []), ('Restart scalarizr', []), ('Rebundle server', ['rebundle']), ('Use new role', ['rebundle']), ('Restart scalarizr after bundling', ['rebundle']), ('Bundling data', []), ('Modifying data', []), ('Reboot server', []), ('Backuping data on Master', []), ('Setup replication', []), ('Restart scalarizr in slave', []), ('Slave force termination', []), ('Slave delete EBS', ['ec2']), ('Setup replication for EBS test', ['ec2']), ('Writing on Master, reading on Slave', []), ('Slave -> Master promotion', []), ('Restart farm', ['restart_farm'])] + expects(str(e)).to_not.be.different_of( + """Equality Error +X = [('Bootstraping Redis role', []), + ('Restart scalarizr', []), + ('Rebundle server', ['rebundle']), + ('Use new role', ['rebundle']), + ('Restart scalarizr after bundling', ['rebundle']), + ('Bundling data', []), + ('Modifying data', []), + ('Reboot server', []), + ('Backuping data on Master', []), + ('Setup replication', []), + ('Restart scalarizr in slave', []), + ('Slave force termination', []), + ('Slave delete EBS', ['ec2']), + ('Setup replication for EBS test', ['ec2']), + ('Writing on Master, reading on Slave', []), + ('Slave -> Master promotion', []), + ('Restart farm', ['restart_farm'])] and -Y = [('Bootstraping Redis role', ['rebundle', 'rebundle', 'rebundle']), ('Restart scalarizr', []), ('Rebundle server', ['rebundle']), ('Use new role', ['rebundle']), ('Restart scalarizr after bundling', ['rebundle']), ('Bundling data', []), ('Modifying data', []), ('Reboot server', []), ('Backuping data on Master', []), ('Setup replication', []), ('Restart scalarizr in slave', []), ('Slave force termination', []), ('Slave delete EBS', ['ec2']), ('Setup replication for EBS test', ['ec2']), ('Writing on Master, reading on Slave', []), ('Slave -> Master promotion', []), ('Restart farm', ['restart_farm'])] +Y = [('Bootstraping Redis role', ['rebundle', 'rebundle', 'rebundle']), + ('Restart scalarizr', []), + ('Rebundle server', ['rebundle']), + ('Use new role', ['rebundle']), + ('Restart scalarizr after bundling', ['rebundle']), + ('Bundling data', []), + ('Modifying data', []), + ('Reboot server', []), + ('Backuping data on Master', []), + ('Setup replication', []), + ('Restart scalarizr in slave', []), + ('Slave force termination', []), + ('Slave delete EBS', ['ec2']), + ('Setup replication for EBS test', ['ec2']), + ('Writing on Master, reading on Slave', []), + ('Slave -> Master promotion', []), + ('Restart farm', ['restart_farm'])] Y[0][1] has 3 items whereas X[0][1] is empty -""".strip()) +""".strip() + ) def test_within_failing_due_to_internally_raised_exception(): @@ -1657,11 +1492,10 @@ def test_within_failing_due_to_internally_raised_exception(): def crash(*a): time.sleep(0.1) - raise RuntimeError('unrelated exception') + raise RuntimeError("unrelated exception") expects(within(five=miliseconds)(crash)).when.called.to.have.raised( - RuntimeError, - "unrelated exception" + RuntimeError, "unrelated exception" ) @@ -1675,3 +1509,138 @@ def test_all_integers_not_iterable(): ":func:`sure.original.all_integers` returns False when receiving a non-iterable param" expects(all_integers(9)).to.be.false + + +def test_word_to_number(): + expects(sure.word_to_number("one")).to.equal(1) + expects(sure.word_to_number("two")).to.equal(2) + expects(sure.word_to_number("three")).to.equal(3) + expects(sure.word_to_number("four")).to.equal(4) + expects(sure.word_to_number("five")).to.equal(5) + expects(sure.word_to_number("six")).to.equal(6) + expects(sure.word_to_number("seven")).to.equal(7) + expects(sure.word_to_number("eight")).to.equal(8) + expects(sure.word_to_number("nine")).to.equal(9) + expects(sure.word_to_number("ten")).to.equal(10) + expects(sure.word_to_number("eleven")).to.equal(11) + expects(sure.word_to_number("twelve")).to.equal(12) + expects(sure.word_to_number("thirteen")).to.equal(13) + expects(sure.word_to_number("fourteen")).to.equal(14) + expects(sure.word_to_number("fifteen")).to.equal(15) + expects(sure.word_to_number("sixteen")).to.equal(16) + expects(sure.word_to_number("seventeen")).to.equal(17) + expects(sure.word_to_number("eighteen")).to.equal(18) + expects(sure.word_to_number("nineteen")).to.equal(19) + expects(sure.word_to_number("twenty")).to.equal(20) + expects(sure.word_to_number("twenty_one")).to.equal(21) + expects(sure.word_to_number("fourty_two")).to.equal(42) + expects(sure.word_to_number("seventy_one")).to.equal(71) + expects(sure.word_to_number("ninety_four")).to.equal(94) + expects(sure.word_to_number("one_hundred")).to.equal(100) + expects(sure.word_to_number("one_thousand")).to.equal(1000) + expects(sure.word_to_number("one_million")).to.equal(1000000) + expects(sure.word_to_number("one_thousand_three_hundred_thirty_seven")).to.equal(1337) + + +def test_microsecond_unit(): + "testing microseconds convertion" + cfrom, cto = sure.UNITS[sure.microsecond] + + expects(cfrom(1)).to.equal(100000) + expects(cto(1)).to.equal(1) + + cfrom, cto = sure.UNITS[sure.microseconds] + + expects(cfrom(1)).to.equal(100000) + expects(cto(1)).to.equal(1) + + +def test_milisecond_unit(): + "testing miliseconds convertion" + cfrom, cto = sure.UNITS[sure.milisecond] + + expects(cfrom(1)).to.equal(1000) + expects(cto(100)).to.equal(1) + + cfrom, cto = sure.UNITS[sure.miliseconds] + + expects(cfrom(1)).to.equal(1000) + expects(cto(100)).to.equal(1) + + +def test_second_unit(): + "testing seconds convertion" + cfrom, cto = sure.UNITS[sure.second] + + expects(cfrom(1)).to.equal(1) + expects(cto(100000)).to.equal(1) + + cfrom, cto = sure.UNITS[sure.seconds] + + expects(cfrom(1)).to.equal(1) + expects(cto(100000)).to.equal(1) + + +def test_minute_unit(): + "testing minutes convertion" + cfrom, cto = sure.UNITS[sure.minute] + + expects(cfrom(60)).to.equal(1) + expects(cto(1)).to.equal(6000000) + + cfrom, cto = sure.UNITS[sure.minutes] + + expects(cfrom(60)).to.equal(1) + expects(cto(1)).to.equal(6000000) + + +def test_within_wrong_usage(): + "within(three=miliseconds, one=second) should raise WrongUsageError" + + expects(within).when.called_with(three=miliseconds, one=second).to.have.raised( + WrongUsageError, + "within() takes a single keyword argument where the argument must be a numerical description from one to eighteen and the value. For example: within(eighteen=miliseconds)", + ) + + +def test_assertion_helper_within_range_wrong_number_of_elements(): + expects(AssertionHelper).when.called_with(object, within_range=set(range(3))).should.have.raised( + TypeError, + "within_range parameter must be a tuple with 2 objects, received a `set' with 3 objects instead" + ) + + +def test_assertion_helper_with_kws(): + src = Dummy('assertion_helper.src') + assertion_helper = AssertionHelper(src, with_args=("z", "y"), with_kws={"a": "b"}) + expects(assertion_helper).to.have.property("_callable_args").being.a(list) + expects(assertion_helper).to.have.property("_callable_args").being.equal(["z", "y"]) + expects(assertion_helper).to.have.property("_callable_kw").being.a(dict) + expects(assertion_helper).to.have.property("_callable_kw").being.equal({"a": "b"}) + expects(assertion_helper).to.have.property("src").being.a(Dummy) + expects(assertion_helper).to.have.property("src").being.equal(src) + + +def test_assertion_helper_raises_raises_type_error_noncallable(): + src = Dummy('assertion_helper.src') + assertion_helper = AssertionHelper(src) + expects(assertion_helper.raises).when.called_with("dummy").to.have.raised( + TypeError, + " is not callable" + ) + + +def test_assertion_helper_raises_fails_when_the_expected_error_does_not_happen_given_function(): + assertion_helper = AssertionHelper(lambda: None) + + expects(assertion_helper.raises).when.called_with("error").to.have.raised( + f'calling function ({collapse_path(__file__)} at line: "1634") with args [] and kws {{}} did not raise {repr("error")}' + ) + + +def test_assertion_helper_raises_fails_when_the_expected_error_does_not_happen_builtin_function(): + assertion_helper = AssertionHelper(vars) + + expects(assertion_helper.raises).when.called_with("error").to.have.raised( + "at :\ncalling vars() with args [] and kws {} did not raise 'error'" + ) diff --git a/tests/test_runtime/test_base_result.py b/tests/test_runtime/test_base_result.py new file mode 100644 index 0000000..fc67128 --- /dev/null +++ b/tests/test_runtime/test_base_result.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . + +"""tests for :class:`sure.runtime.BaseResult`""" +from sure import expects +from sure.runtime import BaseResult + + +description = "tests for :class:`sure.runtime.BaseResult`" + + +def test_base_result___repr___not_implemented_error_missing_label_property(): + "BaseResult.__repr__ should raise :exc:`NotImplementederror` when missing a `label' property" + + expects(repr).when.called_with(BaseResult()).to.have.raised( + NotImplementedError, + " MUST define a `label' property or attribute which must be a string" + ) + + +def test_base_result___repr___not_implemented_error_nonstring_label_property(): + "calling :func:`repr` on a subclass of :class:`sure.runtime.BaseResult` whose label property returns something other than a :class:`str` instance should raise :exc:`NotImplementedError`" + + class FakeResultDummyLabelNonString(BaseResult): + @property + def label(self): + return () + + expects(repr).when.called_with(FakeResultDummyLabelNonString()).to.have.raised( + NotImplementedError, + ".FakeResultDummyLabelNonString'>.label must be a string but is a instead" + ) + + +def test_base_result___repr___returns_lowercase_label(): + "the builtin implementation of :meth:`sure.runtime.BaseResult.__repr__` should return the value of its `label' property as a lower-case string" + + class FakeResultDummyLabel(BaseResult): + @property + def label(self): + return "LABEL" + + repr(FakeResultDummyLabel()).should.equal("'label'") diff --git a/tests/test_runtime/test_feature.py b/tests/test_runtime/test_feature.py new file mode 100644 index 0000000..ce38f50 --- /dev/null +++ b/tests/test_runtime/test_feature.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . + +"""tests for :class:`sure.runtime.Feature`""" + +from unittest.mock import patch, call +from unittest.mock import Mock as Spy +from sure import expects +from sure.runtime import Feature, RuntimeOptions, ScenarioResult, Scenario +from sure.doubles import stub +from sure.errors import ExitFailure, ExitError + + +description = "tests for :class:`sure.runtime.Feature`" + + +def test_feature_with_description(): + "repr(sure.runtime.Feature) with description" + + feature = stub(Feature, title="title", description="description") + + expects(repr(feature)).to.equal('') + + +def test_feature_without_description(): + "repr(sure.runtime.Feature) with description" + + feature = stub(Feature, title="title", description=None) + + expects(repr(feature)).to.equal('') + + +@patch('sure.errors.sys.exit') +@patch('sure.runtime.RuntimeContext') +def test_feature_run_is_failure(RuntimeContext, exit): + 'Feature.run() should raise :class:`sure.errors.ExitFailure` at the occurrence of failure within an "immediate" failure context' + + reporter_spy = Spy(name='Reporter') + scenario_result = stub(ScenarioResult, is_failure=True, __failure__=AssertionError('contrived failure'), __error__=None) + scenario_run_spy = Spy(name="Scenario.run", return_value=scenario_result) + + scenario_stub = stub(Scenario, run=scenario_run_spy) + feature_stub = stub( + Feature, + title="failure feature test", + description=None, + scenarios=[scenario_stub] + ) + + expects(feature_stub.run).when.called_with(reporter=reporter_spy, runtime=RuntimeOptions(immediate=True)).to.have.raised( + ExitFailure, + 'ExitFailure' + ) + expects(reporter_spy.mock_calls).to.equal([ + call.on_failure(scenario_stub, scenario_result) + ]) + + +@patch('sure.errors.sys.exit') +@patch('sure.runtime.RuntimeContext') +def test_feature_run_is_error(RuntimeContext, exit): + 'Feature.run() should raise :class:`sure.errors.ExitError` at the occurrence of error within an "immediate" error context' + + reporter_spy = Spy(name='Reporter') + scenario_run_spy = Spy(name="Scenario.run") + scenario_stub = stub(Scenario, run=scenario_run_spy) + feature_stub = stub( + Feature, + title="error feature test", + description=None, + scenarios=[scenario_stub] + ) + scenario_result = stub(ScenarioResult, is_error=True, __error__=ValueError('contrived error'), __failure__=None, is_failure=False, scenario=scenario_stub) + scenario_run_spy.return_value = scenario_result + + expects(feature_stub.run).when.called_with(reporter=reporter_spy, runtime=RuntimeOptions(immediate=True)).to.have.raised( + ExitError, + 'ExitError' + ) + expects(reporter_spy.mock_calls).to.equal([ + call.on_error(scenario_stub, scenario_result) + ]) diff --git a/tests/test_runtime/test_feature_result.py b/tests/test_runtime/test_feature_result.py new file mode 100644 index 0000000..f99c85e --- /dev/null +++ b/tests/test_runtime/test_feature_result.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""tests for :class:`sure.runtime.FeatureResult`""" + +import sys +from sure import expects +from sure.doubles import stub +from sure.loader import collapse_path +from sure.runtime import ( + ErrorStack, + RuntimeContext, + Scenario, + ScenarioResult, + ScenarioResultSet, + FeatureResult, + TestLocation, +) + +description = "tests for :class:`sure.runtime.FeatureResult`" + + +def test_feature_result(): + "FeatureResult discerns types of :class:`sure.runtime.ScenarioResult` instances" + + context = stub(RuntimeContext) + scenario_result_sets = [ + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ] + + feature_result = FeatureResult(scenario_result_sets) + + expects(feature_result).to.have.property('failed_scenarios').being.length_of(3) + expects(feature_result).to.have.property('errored_scenarios').being.length_of(3) + expects(feature_result).to.have.property('scenario_results').being.length_of(7) + + +def test_feature_result_printable_with_failure(): + "Feature.printable presents reference to first failure occurrence" + + context = stub(RuntimeContext) + scenario_result_sets = [ + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=AssertionError('dummy'))], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ] + + feature_result = FeatureResult(scenario_result_sets) + + expects(feature_result.printable()).to.equal("AssertionError: dummy") + + +def test_feature_result_printable_with_error(): + "Feature.printable presents reference to first error occurrence" + + context = stub(RuntimeContext) + scenario_result_sets = [ + ScenarioResultSet([stub(ScenarioResult, __error__=None, __failure__=None)], context=context), + ScenarioResultSet([stub(ScenarioResult, __error__=ValueError('dummy'), __failure__=None)], context=context), + ] + + feature_result = FeatureResult(scenario_result_sets) + + expects(feature_result.printable()).to.equal("ValueError: dummy") diff --git a/tests/test_runtime/test_runtime_context.py b/tests/test_runtime/test_runtime_context.py index e8fc720..c3aaa53 100644 --- a/tests/test_runtime/test_runtime_context.py +++ b/tests/test_runtime/test_runtime_context.py @@ -14,6 +14,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +"tests for :class:`sure.runtime.RuntimeContext`" + from mock import patch from sure import expects from sure.doubles import stub @@ -28,6 +30,8 @@ def test_runtime_context(WarningReaper): """sure.runtime.RuntimeContext""" + warning_reaper = WarningReaper.return_value + warning_reaper.warnings = ['dummy-warning-a', 'dummy-warning-b'] options_dummy = RuntimeOptions(immediate=False, reap_warnings=True) reporter_stub = stub(Reporter) @@ -43,4 +47,9 @@ def test_runtime_context(WarningReaper): " options=>" ) WarningReaper.assert_called_once_with() - WarningReaper.return_value.enable_capture.assert_called_once_with() + warning_reaper.enable_capture.assert_called_once_with() + + expects(context).to.have.property('warnings').being.equal([ + 'dummy-warning-a', + 'dummy-warning-b', + ]) diff --git a/tests/test_runtime/test_scenario.py b/tests/test_runtime/test_scenario.py new file mode 100644 index 0000000..377335c --- /dev/null +++ b/tests/test_runtime/test_scenario.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . + +"""tests for :class:`sure.runtime.Scenario`""" + + +from unittest.mock import patch, call +from unittest.mock import Mock as Spy +from sure import expects +from sure.runtime import ( + Scenario, + RuntimeOptions, + ScenarioResult, + RuntimeRole, + RuntimeContext, + ScenarioResultSet, +) +from sure.doubles import stub, Dummy, anything +from sure.errors import ExitFailure, ExitError + + +description = "tests for :class:`sure.runtime.Scenario`" + + +@patch("sure.errors.sys.exit") +@patch("sure.runtime.ScenarioArrangement") +def test_scenario_run_when_result_is_failure(ScenarioArrangement, exit): + "Scenario.run() should raise :class:`sure.errors.ExitError` when a failure occurs" + + scenario_stub = stub( + Scenario, + object=Dummy("scenario.object"), + ) + reporter_spy = Spy(name="Reporter") + scenario_arrangement = ScenarioArrangement.return_value + scenario_arrangement.scenario = scenario_stub + ScenarioArrangement.from_generic_object.return_value = scenario_arrangement + scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] + scenario_result = stub( + ScenarioResult, + is_failure=True, + is_success=False, + is_error=False, + scenario=scenario_stub, + ) + scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] + + context = stub( + RuntimeContext, options=RuntimeOptions(immediate=False), reporter=reporter_spy + ) + scenario_result_set = scenario_stub.run(context) + + expects(scenario_result_set).to.be.a(ScenarioResultSet) + expects(reporter_spy.mock_calls).to.equal( + [ + call.on_scenario(scenario_arrangement.scenario), + call.on_failure(scenario_stub, scenario_result), + call.on_scenario_done(scenario_arrangement.scenario, scenario_result), + ] + ) + + +@patch("sure.errors.sys.exit") +@patch("sure.runtime.ScenarioArrangement") +def test_scenario_run_when_result_is_failure_and_runtime_options_immediate( + ScenarioArrangement, exit +): + 'Scenario.run() should raise :class:`sure.errors.ExitError` when a failure occurs and the runtime context is configured to "fail immediately"' + + reporter_spy = Spy(name="Reporter") + scenario_arrangement = ScenarioArrangement.return_value + ScenarioArrangement.from_generic_object.return_value = scenario_arrangement + scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] + scenario_stub = stub( + Scenario, + object=Dummy("scenario.object"), + ) + scenario_result = stub( + ScenarioResult, + is_failure=True, + is_success=False, + is_error=False, + scenario=scenario_stub, + ) + scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] + + context = stub( + RuntimeContext, options=RuntimeOptions(immediate=True), reporter=reporter_spy + ) + expects(scenario_stub.run).when.called_with(context).should.have.raised(ExitFailure) + + expects(reporter_spy.mock_calls).to.equal( + [ + call.on_scenario(scenario_arrangement.scenario), + call.on_failure(scenario_stub, scenario_result), + ] + ) + + +@patch("sure.errors.sys.exit") +@patch("sure.runtime.ScenarioArrangement") +def test_scenario_run_when_result_is_error_and_runtime_options_immediate( + ScenarioArrangement, exit +): + 'Scenario.run() should raise :class:`sure.errors.ExitError` when an error occurs and the runtime context is configured to "error immediately"' + + reporter_spy = Spy(name="Reporter") + scenario_arrangement = ScenarioArrangement.return_value + ScenarioArrangement.from_generic_object.return_value = scenario_arrangement + scenario_arrangement.uncollapse_nested.return_value = [scenario_arrangement] + scenario_stub = stub( + Scenario, + object=Dummy("scenario.object"), + ) + scenario_result = stub( + ScenarioResult, + is_error=True, + is_success=False, + is_failure=False, + scenario=scenario_stub, + ) + scenario_arrangement.run.return_value = [(scenario_result, RuntimeRole.Unit)] + + context = stub( + RuntimeContext, options=RuntimeOptions(immediate=True), reporter=reporter_spy + ) + expects(scenario_stub.run).when.called_with(context).should.have.raised(ExitError) + + expects(reporter_spy.mock_calls).to.equal( + [ + call.on_scenario(scenario_arrangement.scenario), + call.on_error(scenario_stub, scenario_result), + ] + ) diff --git a/tests/test_runtime/test_scenario_arrangement.py b/tests/test_runtime/test_scenario_arrangement.py index 6a521c5..95bfd9c 100644 --- a/tests/test_runtime/test_scenario_arrangement.py +++ b/tests/test_runtime/test_scenario_arrangement.py @@ -20,6 +20,7 @@ from sure.loader import collapse_path from sure.runner import Runner from sure.reporter import Reporter +from sure.errors import ImmediateFailure, ImmediateError, InternalRuntimeError from sure.runtime import ( RuntimeContext, TestLocation, @@ -91,7 +92,7 @@ def test_method_Z(self): expects(scenario_arrangement).to.have.property("source_instance").being.a(TestCaseA) expects(repr(scenario_arrangement)).to.equal( - f'' + f'' ) @@ -243,3 +244,57 @@ def test_method_Z(self): scenario_result, role = return_value expects(scenario_result).to.be.a(ScenarioResult) expects(role).to.equal(RuntimeRole.Unit) + + +def test_scenario_arrangement_run_container_failure_and_immediate_context_option(): + "sure.runtime.ScenarioArrangement.run_container() on failure with `context.option.immediate=True` should raise :exc:`sure.errors.ImmediateFailure`" + + class TestCaseRunContainerFailure: + def test_failure(self): + raise AssertionError("failure") + + runner = stub(Runner) + reporter = Reporter.from_name_and_runner("test", runner) + context = RuntimeContext( + reporter=reporter, + options=RuntimeOptions( + immediate=True, + ) + ) + scenario_arrangement = ScenarioArrangement.from_generic_object( + TestCaseRunContainerFailure, + context=context, + scenario=stub(Scenario), + ) + + container = scenario_arrangement.test_methods[0] + expects(list).when.called_with(scenario_arrangement.run_container(container, context)).to.throw( + ImmediateFailure, "failure" + ) + + +def test_scenario_arrangement_run_container_error_and_immediate_context_option(): + "sure.runtime.ScenarioArrangement.run_container() on error with `context.option.immediate=True` should raise :exc:`sure.errors.ImmediateError`" + + class TestCaseRunContainerError: + def test_error(self): + raise RuntimeError("error") + + runner = stub(Runner) + reporter = Reporter.from_name_and_runner("test", runner) + context = RuntimeContext( + reporter=reporter, + options=RuntimeOptions( + immediate=True, + ) + ) + scenario_arrangement = ScenarioArrangement.from_generic_object( + TestCaseRunContainerError, + context=context, + scenario=stub(Scenario), + ) + + container = scenario_arrangement.test_methods[0] + expects(list).when.called_with(scenario_arrangement.run_container(container, context)).to.throw( + ImmediateError, "error" + ) diff --git a/tests/test_runtime/test_scenario_result.py b/tests/test_runtime/test_scenario_result.py new file mode 100644 index 0000000..dfcd87e --- /dev/null +++ b/tests/test_runtime/test_scenario_result.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""tests for :class:`sure.runtime.ScenarioResult`""" + +import sys +from sure import expects +from sure.doubles import stub +from sure.loader import collapse_path +from sure.runtime import ( + ScenarioResult, + Scenario, + TestLocation, + RuntimeContext, + ErrorStack, +) + + +description = "tests for :class:`sure.runtime.ScenarioResult`" + + +def test_scenario_result_printable(): + "meth:`ScenarioResult.printable` returns its location as string" + + location = TestLocation(test_scenario_result_printable) + scenario = stub(Scenario) + context = stub(RuntimeContext) + scenario_result = ScenarioResult( + scenario=scenario, location=location, context=context, error=None + ) + + expects(scenario_result.printable()).to.equal( + ( + 'scenario "meth:`ScenarioResult.printable` returns its location as string" \n' + "defined at " + f"{collapse_path(__file__)}:35" + ) + ) diff --git a/tests/test_runtime/test_scenario_result_set.py b/tests/test_runtime/test_scenario_result_set.py new file mode 100644 index 0000000..779674e --- /dev/null +++ b/tests/test_runtime/test_scenario_result_set.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +"""tests for :class:`sure.runtime.ScenarioResultSet`""" + +import sys +from sure import expects +from sure.doubles import stub +from sure.loader import collapse_path +from sure.runtime import ( + ErrorStack, + RuntimeContext, + Scenario, + ScenarioResult, + ScenarioResultSet, + TestLocation, +) + +description = "tests for :class:`sure.runtime.ScenarioResultSet`" + + +def test_scenario_result_set(): + "ScenarioResultSet discerns types of :class:`sure.runtime.ScenarioResult` instances" + + scenario_results = [ + stub(ScenarioResult, __error__=None, __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=AssertionError('y')), + stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=AssertionError('Y')), + stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=None), + ] + + scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) + + expects(scenario_result_set).to.have.property('failed_scenarios').being.length_of(2) + expects(scenario_result_set).to.have.property('errored_scenarios').being.length_of(2) + expects(scenario_result_set).to.have.property('scenario_results').being.length_of(6) + + +def test_scenario_result_set_printable_error(): + "ScenarioResultSet.printable presents reference to first error occurrence" + + scenario_results = [ + stub(ScenarioResult, __error__=None, __failure__=None), + stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), + stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=None), + ] + + scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) + + expects(scenario_result_set.printable()).to.equal("InterruptedError: x") + + +def test_scenario_result_set_printable_failure(): + "ScenarioResultSet.printable presents reference to first failure occurrence" + + scenario_results = [ + stub(ScenarioResult, __error__=None, __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=AssertionError('Y')), + stub(ScenarioResult, __error__=InterruptedError('x'), __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=AssertionError('y')), + stub(ScenarioResult, __error__=InterruptedError('X'), __failure__=None), + stub(ScenarioResult, __error__=None, __failure__=None), + ] + + scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) + + expects(scenario_result_set.printable()).to.equal("AssertionError: Y") + + +def test_scenario_result_set_printable_no_errors_or_failures(): + "ScenarioResultSet.printable presents empty string when there are no errors or failures" + + scenario_results = [ + stub(ScenarioResult, __error__=None, __failure__=None), + ] + + scenario_result_set = ScenarioResultSet(scenario_results, context=stub(RuntimeContext)) + + expects(scenario_result_set.printable()).to.be.a(str) + expects(scenario_result_set.printable()).to.be.empty diff --git a/tests/unit/reporters/test_feature_reporter.py b/tests/unit/reporters/test_feature_reporter.py index f0d9377..945d5cd 100644 --- a/tests/unit/reporters/test_feature_reporter.py +++ b/tests/unit/reporters/test_feature_reporter.py @@ -47,11 +47,11 @@ def test_feature_reporter_on_start(): def test_feature_reporter_on_feature(): "FeatureReporter.on_feature" - sh = Spy(name="Shell") + sh = Spy(title="Shell") reporter = FeatureReporter(stub(Runner)) reporter.sh = sh - reporter.on_feature(stub(Feature, name="Conflicts")) + reporter.on_feature(stub(Feature, title="Conflicts")) expects(sh.mock_calls).to.equal( [ @@ -67,13 +67,13 @@ def test_feature_reporter_on_feature(): def test_feature_reporter_on_feature_done(): "FeatureReporter.on_feature_done" - sh = Spy(name="Shell") + sh = Spy(title="Shell") reporter = FeatureReporter(stub(Runner)) reporter.sh = sh reporter.on_feature_done( - stub(Feature, name="stubbed feature"), - Spy(name="feature_result"), + stub(Feature, title="stubbed feature"), + Spy(title="feature_result"), ) expects(sh.mock_calls).to.equal( diff --git a/tests/unit/test_astuneval.py b/tests/unit/test_astuneval.py new file mode 100644 index 0000000..906a5b1 --- /dev/null +++ b/tests/unit/test_astuneval.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) <2010-2024> 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 . +import ast +from sure import expects +from sure.astuneval import parse_body +from sure.astuneval import parse_accessor +from sure.astuneval import Accessor, NameAccessor, SubsAccessor, AttributeAccessor + + +def test_parse_body_against_several_kinds(): + expects(parse_body("atomic_bonds[3:7]")).to.be.an(ast.Subscript) + expects(parse_body("children[6]")).to.be.an(ast.Subscript) + expects(parse_body("hippolytus")).to.be.an(ast.Name) + expects(parse_body("zone[4].damage")).to.be.an(ast.Attribute) + + +def test_parse_accessor_name_accessor(): + class Tragedy: + telemachus = "♒️" + + expects(parse_accessor("telemachus")).to.be.a(NameAccessor) + get_character = parse_accessor("telemachus") + expects(get_character(Tragedy)).to.equal('♒️') + + +def test_parse_accessor_subscript_accessor(): + class MonacoGrandPrix1990: + classification = [ + "Ayrton Senna", + "Alain Prost", + "Jean Alesi", + ] + expects(parse_accessor("classification[2]")).to.be.a(SubsAccessor) + get_position = parse_accessor("classification[2]") + expects(get_position(MonacoGrandPrix1990)).to.equal("Jean Alesi") + + +def test_parse_accessor_attr_accessor(): + class Event: + def __init__(self, description: str): + self.tag = description + + class LogBook: + events = [ + Event("occurrenceA"), + Event("occurrenceB"), + Event("occurrenceC"), + Event("occurrenceD"), + Event("occurrenceE"), + Event("occurrenceF"), + ] + + expects(parse_accessor("events[3].description")).to.be.a(AttributeAccessor) + + access_description = parse_accessor("events[3].tag") + expects(access_description(LogBook)).to.equal("occurrenceD") + + +def test_accessor_access_not_implemented(): + accessor = Accessor(parse_body("attribute")) + expects(accessor.access).when.called_with(object).to.throw( + NotImplementedError + ) + + +def test_parse_body_syntax_error(): + parse_body.when.called_with("substance = collect()\nsubstance.reuse()").to.throw( + SyntaxError, + "'substance = collect()\\nsubstance.reuse()' exceeds the maximum body count for ast nodes" + ) diff --git a/tests/unit/test_runtime.py b/tests/unit/test_runtime.py index 83967ca..88dd7f6 100644 --- a/tests/unit/test_runtime.py +++ b/tests/unit/test_runtime.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"sure.runtime" +"unit tests for :mod:`sure.runtime`" from collections.abc import Awaitable from sure.runtime import object_name