Skip to content

Commit

Permalink
presents more test coverage, changes feature name to feature title, e…
Browse files Browse the repository at this point in the history
…xpands coverage options at the CLI and updates documentation
  • Loading branch information
gabrielfalcao committed Jan 16, 2024
1 parent b131b5e commit f1adc76
Show file tree
Hide file tree
Showing 16 changed files with 169 additions and 82 deletions.
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -60,8 +60,8 @@ 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.core --cover-module=sure tests/runner
$(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
Expand Down
4 changes: 2 additions & 2 deletions docs/source/changelog.rst
Expand Up @@ -4,8 +4,8 @@ Change Log
All notable changes to this project will be documented in this file.
This project adheres to `Semantic Versioning <http://semver.org/>`__.

[v3.0.0]
--------
v3.0.0
------

- Presents better documentation
- Drops support to Python 2 obliterates the ``sure.compat`` module
Expand Down
6 changes: 3 additions & 3 deletions docs/source/conf.py
Expand Up @@ -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",
Expand All @@ -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"
Expand Down Expand Up @@ -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'
57 changes: 16 additions & 41 deletions sure/cli.py
Expand Up @@ -18,7 +18,6 @@

import os
import sys
import logging
from glob import glob
from itertools import chain as flatten
from functools import reduce
Expand Down Expand Up @@ -62,7 +61,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,
Expand All @@ -74,26 +77,32 @@ def entrypoint(
special_syntax,
with_coverage,
cover_branches,
cover_include,
cover_omit,
cover_module,
cover_erase,
cover_concurrency,
reap_warnings,
):
if not paths:
paths = glob("test*/**")
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,
}

cov = with_coverage and coverage.Coverage(**coverageopts) or None
if cov:
cov.erase()
cover_erase and cov.erase()
cov.load()
cov.start()

Expand All @@ -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)
4 changes: 3 additions & 1 deletion sure/doubles/stubs.py
Expand Up @@ -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)()
2 changes: 0 additions & 2 deletions sure/reporter.py
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion sure/reporters/feature.py
Expand Up @@ -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(" ")

Expand Down
4 changes: 2 additions & 2 deletions sure/reporters/test.py
Expand Up @@ -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):
Expand Down
29 changes: 18 additions & 11 deletions sure/runtime.py
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -623,9 +624,9 @@ def __init__(self, module):

def __repr__(self):
if self.description:
return f'<Feature "{self.description}" {self.name}>'
return f'<Feature "{self.description}" {self.title}>'
else:
return f'<Feature "{self.name}">'
return f'<Feature "{self.title}">'

def read_scenarios(self, suts):
self.scenarios = list(map((lambda e: Scenario(e, self)), suts))
Expand Down Expand Up @@ -654,6 +655,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)
Expand Down
2 changes: 1 addition & 1 deletion sure/version.py
@@ -1 +1 @@
version = "3.0a0"
version = "3.0a1"
16 changes: 8 additions & 8 deletions tests/functional/test_runner.py
Expand Up @@ -64,7 +64,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)
Expand Down Expand Up @@ -107,7 +107,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)
Expand All @@ -116,7 +116,7 @@ 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"

)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions tests/test_runtime/test_feature.py
@@ -0,0 +1,41 @@
# -*- 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/>.

"""tests for :class:`sure.runtime.Feature`"""

from sure import expects
from sure.runtime import Feature
from sure.doubles import stub


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('<Feature "description" title>')


def test_feature_without_description():
"repr(sure.runtime.Feature) with description"

feature = stub(Feature, title="title", description=None)

expects(repr(feature)).to.equal('<Feature "title">')
11 changes: 10 additions & 1 deletion tests/test_runtime/test_runtime_context.py
Expand Up @@ -14,6 +14,8 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"tests for :class:`sure.runtime.RuntimeContext`"

from mock import patch
from sure import expects
from sure.doubles import stub
Expand All @@ -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)

Expand All @@ -43,4 +47,9 @@ def test_runtime_context(WarningReaper):
"<RuntimeContext reporter=<ReporterStub> options=<RuntimeOptions immediate=False glob_pattern='**test*.py' reap_warnings=True>>"
)
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',
])

0 comments on commit f1adc76

Please sign in to comment.