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 5c7f7fb
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 56 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
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
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
28 changes: 18 additions & 10 deletions sure/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import re
import sys
Expand All @@ -27,9 +26,13 @@
from pathlib import Path
from functools import reduce
from typing import Dict, List, Optional, Any, Callable, Union
from sure import types as stypes
from mock import Mock

from sure.reporter import Reporter
from sure.errors import InternalRuntimeError
from sure.special import WarningReaper
from sure import types as stypes

from sure.errors import (
exit_code,
ExitError,
Expand All @@ -49,12 +52,8 @@
get_type_definition_filename_and_firstlineno,
object_belongs_to_sure,
)
from sure.reporter import Reporter
from sure.errors import InternalRuntimeError

self = sys.modules[__name__]


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -144,25 +143,29 @@ class RuntimeOptions(object):
- ``immediate`` - quit entire test-run session immediately after a failure
- ``ignore`` - optional list of paths to be ignored
- ``glob_pattern`` - optional string representing a valid :mod:`fnmatch` pattern to be matched against every "full" :class:`~pathlib.Path` in lookup paths of :meth:`~sure.runner.Runner.find_candidates` and :class:`~sure.loader.loader`. Defaults to "**test*.py"
- ``glob_pattern`` - optional string representing a valid :mod:`fnmatch` pattern to be matched against every "full" :class:`~pathlib.Path` in lookup paths of :meth:`~sure.runner.Runner.find_candidates` and :class:`~sure.loader.loader`. Defaults to ``**test*.py``
- ``reap_warnings`` - optional bool to flag that warnings should be reaped, captured during runtime and displayed by the chosen reporter at the end of the test execution session. Defaults to ``False``
"""

immediate: bool
ignore: Optional[List[Union[str, Path]]]
glob_pattern: str
reap_warnings: bool

def __init__(
self,
immediate: bool,
ignore: Optional[List[Union[str, Path]]] = None,
glob_pattern: str = "**test*.py",
reap_warnings: bool = False
):
self.immediate = bool(immediate)
self.ignore = ignore and list(ignore) or []
self.glob_pattern = glob_pattern
self.reap_warnings = bool(reap_warnings)

def __repr__(self):
return f"<RuntimeOptions immediate={self.immediate} glob_pattern={repr(self.glob_pattern)}>"
return f"<RuntimeOptions immediate={self.immediate} glob_pattern={repr(self.glob_pattern)} reap_warnings={repr(self.reap_warnings)}>"


class RuntimeContext(object):
Expand All @@ -185,10 +188,17 @@ def __init__(
self.reporter = reporter
self.options = options
self.unittest_testcase_method_name = unittest_testcase_method_name
self.warning_reaper = WarningReaper()
if options.reap_warnings:
self.warning_reaper.enable_capture()

def __repr__(self):
return f"<RuntimeContext reporter={self.reporter} options={self.options}>"

@property
def warnings(self):
return self.warning_reaper.warnings


class ErrorStack(object):
def __init__(
Expand Down Expand Up @@ -406,7 +416,6 @@ def from_generic_object(
nested_containers = []

if isinstance(some_object, type) and not object_belongs_to_sure(some_object):
some_object_type = some_object
# <unittest.TestCase.__init__>
# constructs instance of unittest.TestCase and filter out each instance_or_function
if issubclass(some_object, unittest.TestCase):
Expand All @@ -429,7 +438,6 @@ def from_generic_object(
)

elif isinstance(some_object, types.FunctionType):
some_object_type = type(some_object)
instance_or_function = some_object
# TODO: refactor :mod:`sure.runner` and
# :mod:`sure.runtime` to provide a test function's
Expand Down
55 changes: 51 additions & 4 deletions sure/special.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,55 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import platform
import warnings

from datetime import datetime
from typing import Dict, List, Optional


__captured_warnings__ = []


class WarningReaper(object):
"""Captures warnings for posterior analysis"""

builtin_showwarning = warnings.showwarning

@property
def warnings(self) -> List[Dict[str, object]]:
return list(__captured_warnings__)

def showwarning(
self,
message,
category: Warning,
filename: str,
lineno: int,
file: Optional[io.IOBase] = None,
line: Optional[str] = None,
):
occurrence = datetime.utcnow()
info = locals()
info.pop('self')
if file is not None:
return self.__class__.builtin_showwarning(message, category, filename, lineno, file, line)

__captured_warnings__.append(info)

def enable_capture(self):
if not self.is_enabled:
warnings.showwarning = self.showwarning
return self

def disable_capture(self):
warnings.showwarning = self.__class__.builtin_showwarning
return self

@property
def is_enabled(self) -> bool:
return warnings.showwarning == self.__class__.builtin_showwarning


def load_ctypes():
Expand All @@ -42,7 +90,7 @@ def runtime_is_cpython():

def get_py_ssize_t():
ctypes = load_ctypes()
pythonapi = getattr(ctypes, 'pythonapi', None)
pythonapi = getattr(ctypes, "pythonapi", None)
if hasattr(pythonapi, "Py_InitModule4_64"):
return ctypes.c_int64
else:
Expand Down Expand Up @@ -71,9 +119,8 @@ def patchable_builtin(klass):
target = getattr(klass, "__dict__", name)

class SlotsProxy(PyObject):
_fields_ = [
("dict", ctypes.POINTER(PyObject))
]
_fields_ = [("dict", ctypes.POINTER(PyObject))]

proxy_dict = SlotsProxy.from_address(id(target))
namespace = {}

Expand Down
4 changes: 2 additions & 2 deletions tests/functional/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
ScenarioResult,
ScenarioResultSet,
TestLocation,
FeatureResultSet,
FeatureResultSet, RuntimeContext
)
from sure.reporters import test

Expand Down Expand Up @@ -307,6 +307,6 @@ def test_runner_execute_success_tests():
"ok",
),
],
"on_finish": [(anything_of_type(float),)],
"on_finish": [(anything_of_type(float), anything_of_type(RuntimeContext))],
}
)
2 changes: 1 addition & 1 deletion tests/test_runtime/test_runtime_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ def test_runtime_context():
)

expects(repr(context)).to.equal(
"<RuntimeContext reporter=<ReporterStub> options=<RuntimeOptions immediate=False glob_pattern='**test*.py'>>"
"<RuntimeContext reporter=<ReporterStub> options=<RuntimeOptions immediate=False glob_pattern='**test*.py' reap_warnings=False>>"
)
4 changes: 2 additions & 2 deletions tests/test_runtime/test_runtime_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def test_runtime_options():
expects(RuntimeOptions(0).immediate).to.be.false
expects(RuntimeOptions(1).immediate).to.be.true
expects(repr(RuntimeOptions(1))).to.equal(
"<RuntimeOptions immediate=True glob_pattern='**test*.py'>"
"<RuntimeOptions immediate=True glob_pattern='**test*.py' reap_warnings=False>"
)
expects(repr(RuntimeOptions(0))).to.equal(
"<RuntimeOptions immediate=False glob_pattern='**test*.py'>"
"<RuntimeOptions immediate=False glob_pattern='**test*.py' reap_warnings=False>"
)

0 comments on commit 5c7f7fb

Please sign in to comment.