Skip to content

Commit

Permalink
presents more coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Jan 15, 2024
1 parent dab03a3 commit c8c0910
Show file tree
Hide file tree
Showing 19 changed files with 373 additions and 107 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ test tests:

# runs main command-line tool
run: | $(LIBEXEC_PATH)
$(LIBEXEC_PATH) tests/crashes
$(LIBEXEC_PATH) --special-syntax --with-coverage --cover-branches --cover-module=sure.core --cover-module=sure tests/runner
$(LIBEXEC_PATH) --special-syntax --with-coverage --cover-branches --cover-module=sure --immediate --cover-module=sure --ignore tests/crashes tests
$(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

push-release: dist # pushes distribution tarballs of the current version
$(VENV)/bin/twine upload dist/*.tar.gz
Expand Down
32 changes: 14 additions & 18 deletions sure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ def action_for(context, provides=None, depends_on=None):
list of assets to the staging area and might declare a list of
dependencies expected to exist within a :class:`StagingArea`
"""
action_subaction_dependency = (
'the action "%s" defined at %s:%d '
'depends on the attribute "%s" to be available in the current context'
)
action_attribute_dependency = (
'the action "%s" defined at %s:%d '
'depends on the attribute "%s" to be available in the context.'
" Perhaps one of the following actions might provide that attribute:\n"
)

if not provides:
provides = []

Expand All @@ -375,9 +385,8 @@ def register_dynamic_providers(func, attr, args, kws):
index = int(found.group(1))
if index > len(args):
raise AssertionError(
"the dynamic provider index: {%d} is greater than %d, which is "
"the length of the positional arguments passed to %s"
% (index, len(args), func.__name__)
f"the dynamic provider index: {index} is greater than {len(args)}, which is "
f"the length of the positional arguments passed to {func.__name__}"
)

attr = args[index]
Expand All @@ -400,19 +409,6 @@ def ensure_providers(func, attr, args, kws):
f"implementation for correctness or, if there is a bug in Sure, consider reporting that at {bugtracker}"
)

dependency_error_lonely = (
'the action "%s" defined at %s:%d '
'depends on the attribute "%s" to be available in the'
" context. It turns out that there are no actions providing "
"that. Please double-check the implementation"
)

dependency_error_hints = (
'the action "%s" defined at %s:%d '
'depends on the attribute "%s" to be available in the context.'
" You need to call one of the following actions beforehand:\n"
)

def check_dependencies(func):
action = func.__name__
filename = get_file_name(func)
Expand All @@ -421,7 +417,7 @@ def check_dependencies(func):
for dependency in depends_on:
if dependency in context.__sure_providers_of__:
providers = context.__sure_providers_of__[dependency]
err = dependency_error_hints % (
err = action_attribute_dependency % (
action,
filename,
lineno,
Expand All @@ -436,7 +432,7 @@ def check_dependencies(func):
)

else:
err = dependency_error_lonely % (
err = action_subaction_dependency % (
action,
filename,
lineno,
Expand Down
4 changes: 3 additions & 1 deletion sure/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
)
@click.option("--cover-branches", is_flag=True)
@click.option("--cover-module", multiple=True, help="specify module names to cover")
@click.option("--reap-warnings", is_flag=True, help="reaps warnings during runtime and report only at the end of test session")
def entrypoint(
paths,
reporter,
Expand All @@ -74,6 +75,7 @@ def entrypoint(
with_coverage,
cover_branches,
cover_module,
reap_warnings,
):
if not paths:
paths = glob("test*/**")
Expand All @@ -98,7 +100,7 @@ def entrypoint(
if special_syntax:
sure.enable_special_syntax()

options = RuntimeOptions(immediate=immediate, ignore=ignore)
options = RuntimeOptions(immediate=immediate, ignore=ignore, reap_warnings=reap_warnings)
runner = Runner(resolve_path(os.getcwd()), reporter, options)
try:
result = runner.run(paths)
Expand Down
26 changes: 11 additions & 15 deletions sure/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,11 @@

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

from unittest.mock import _CallList as UnitTestMockCallList

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

MockCallListType = tuple(filter(bool, (UnitTestMockCallList, MockCallList)))


class Explanation(str):
def get_header(self, X, Y, suffix):
Expand All @@ -51,10 +43,7 @@ def as_assertion(self, X, Y, *args, **kw):


class DeepComparison(object):
"""Performs a deep comparison between Python objects in the sense
that complex or nested datastructures, such as :external+python:ref:`mappings <mapping>` of
:external+python:ref:`sequences <sequence>`, :external+python:ref:`sequences <sequence>` of :external+python:ref:`mappings <mapping>`, :external+python:ref:`mappings <mapping>` of :external+python:ref:`sequences <sequence>` containing
:external+python:ref:`mappings <mapping>` or sequences :external+python:ref:`sequences <sequence>` and so on, are recursively compared and reaching farthest accessible edges.
"""Performs a deep comparison between Python objects in the sense that complex or nested datastructures, such as :external+python:ref:`mappings <mapping>` of :external+python:ref:`sequences <sequence>`, :external+python:ref:`sequences <sequence>` of :external+python:ref:`mappings <mapping>`, :external+python:ref:`mappings <mapping>` of :external+python:ref:`sequences <sequence>` containing :external+python:ref:`mappings <mapping>` or sequences :external+python:ref:`sequences <sequence>` and so on, are recursively compared and reaching farthest accessible edges.
"""
def __init__(self, X, Y, epsilon=None, parent=None):
self.complex_cmp_funcs = {
Expand Down Expand Up @@ -183,10 +172,17 @@ def compare_iterables(self, X, Y):
c = self.get_context()
len_X, len_Y = map(len, (X, Y))
if len_X > len_Y:
msg = f"X{red(c.current_X_keys)} has {len_X} items whereas Y{green(c.current_Y_keys)} has only {len_Y}"
if len_Y == 0:
msg = f"X{red(c.current_X_keys)} has {len_X} items whereas Y{green(c.current_Y_keys)} is empty"
else:
msg = f"X{red(c.current_X_keys)} has {len_X} items whereas Y{green(c.current_Y_keys)} has only {len_Y}"
return Explanation(msg)
elif len_X < len_Y:
msg = f"Y{green(c.current_Y_keys)} has {len_Y} items whereas X{red(c.current_X_keys)} has only {len_X}"
if len_X == 0:
msg = f"Y{green(c.current_Y_keys)} has {len_Y} items whereas X{red(c.current_X_keys)} is empty"
else:
msg = f"Y{green(c.current_Y_keys)} has {len_Y} items whereas X{red(c.current_X_keys)} has only {len_X}"

return Explanation(msg)
elif X == Y:
return True
Expand Down
39 changes: 39 additions & 0 deletions sure/doubles/mocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# <sure - sophisticated automated test library and runner>
# Copyright (C) <2010-2024> Gabriel Falcão <gabriel@nacaolivre.org>
#
# 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 <http://www.gnu.org/licenses/>.

'''The :mod:`sure.doubles.mocks` module currently does not provide
"Mocks" per se, it nevertheless serves as a containment module to
hermetically isolate the types :class:`unittest.mock._CallList` and,
if available within the target Python runtime, the type
:class:`mock.mock._CallList` in a tuple that
:class:`sure.core.DeepComparison` uses for comparing lists of
:class:`unittest.mock.call` or :class:`mock.mock.call` somewhat
interchangeably
'''


from unittest.mock import _CallList as UnitTestMockCallList

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

MockCallListType = tuple(filter(bool, (UnitTestMockCallList, MockCallList)))


__all__ = ['MockCallListType']
10 changes: 6 additions & 4 deletions sure/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from pathlib import Path
from typing import Dict
from sure.meta import MetaReporter, get_reporter, gather_reporter_names
from sure.types import Runner, Feature, FeatureResult
from sure.types import Runner, Feature, FeatureResult, RuntimeContext

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

Expand Down Expand Up @@ -217,16 +217,18 @@ class error:
"""
raise NotImplementedError

def on_finish(self):
def on_finish(self, context: RuntimeContext):
"""Called as soon as `sure' finishes running.
.. code:: python
from sure.reporter import Reporter
from sure.runtime import RuntimeContext
class HelloReporter(Reporter):
def on_finish(self):
sys.stderr.write('Reporter.on_finish works')
def on_finish(self, context: RuntimeContext):
sys.stderr.write('Reporter.on_finish works')
HelloReporter('a <sure.runner.Runner()>').on_finish()
"""
Expand Down
16 changes: 14 additions & 2 deletions sure/reporters/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def on_internal_runtime_error(self, context: RuntimeContext, error: ErrorStack):
self.sh.bold_red(error.location_specific_error())
sys.exit(error.code)

def on_finish(self):
def on_finish(self, context: RuntimeContext):
failed = len(self.failures)
errors = len(self.errors)
successful = len(self.successes)
Expand All @@ -174,4 +174,16 @@ def on_finish(self):
self.sh.green(f"{successful} successful")
self.sh.reset("\n")

self.sh.reset(" ")
self.sh.reset("")

warning_count = len(context.warnings)
if warning_count == 0:
return

self.sh.yellow(f"{warning_count} warnings")
self.sh.reset("\n")
for warning in context.warnings:
self.sh.yellow(f"{warning['category'].__name__}: ")
self.sh.bold_black(f"{warning['message']}\n")

self.sh.reset("\n")
12 changes: 8 additions & 4 deletions sure/reporters/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,19 @@ def on_feature(self, feature: Feature):
events["on_feature"].append((time.time(), feature.name))

def on_feature_done(self, feature: Feature, result: FeatureResult):
events["on_feature_done"].append((time.time(), feature.name, result.label.lower()))
events["on_feature_done"].append(
(time.time(), feature.name, result.label.lower())
)

def on_scenario(self, scenario: Scenario):
events["on_scenario"].append((time.time(), scenario.name))

def on_scenario_done(
self, scenario: Scenario, result: Union[ScenarioResult, ScenarioResultSet]
):
events["on_scenario_done"].append((time.time(), scenario.name, result.label.lower()))
events["on_scenario_done"].append(
(time.time(), scenario.name, result.label.lower())
)

def on_failure(self, test: Scenario, result: ScenarioResult):
events["on_failure"].append((time.time(), test.name, result.label.lower()))
Expand All @@ -68,5 +72,5 @@ def on_error(self, test: Scenario, result: ScenarioResult):
def on_internal_runtime_error(self, context: RuntimeContext, error: ErrorStack):
events["on_internal_runtime_error"].append((time.time(), context, error))

def on_finish(self):
events["on_finish"].append((time.time(),))
def on_finish(self, context: RuntimeContext):
events["on_finish"].append((time.time(), context))
14 changes: 6 additions & 8 deletions sure/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ def find_candidates(
def is_runnable_test(self, item) -> bool:
if object_belongs_to_sure(item):
return False
try:
name = getattr(item, "__name__", None)
except RecursionError:
return False

name = getattr(item, "__name__", None)
if isinstance(item, type):
if not issubclass(item, unittest.TestCase):
return seem_to_indicate_test(name)
Expand Down Expand Up @@ -131,23 +129,23 @@ def execute(self, lookup_paths=Iterable[Union[Path, str]]) -> FeatureResultSet:
results = []
self.reporter.on_start()
lookup_paths = list(lookup_paths)

for feature in self.load_features(lookup_paths):
self.reporter.on_feature(feature)
context = RuntimeContext(self.reporter, self.options)

result = feature.run(self.reporter, runtime=self.options)
if self.options.immediate:
if result.is_failure:
raise ExitFailure(context, result)
raise ExitFailure(self.context, result)

if result.is_error:
raise ExitError(context, result)
raise ExitError(self.context, result)

results.append(result)

self.reporter.on_feature_done(feature, result)

self.reporter.on_finish()
self.reporter.on_finish(self.context)
return FeatureResultSet(results)

def run(self, *args, **kws):
Expand Down

0 comments on commit c8c0910

Please sign in to comment.