Skip to content

Commit

Permalink
presents a few changes and introduces bit more test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Jan 16, 2024
1 parent b131b5e commit d7e18c5
Show file tree
Hide file tree
Showing 15 changed files with 167 additions and 80 deletions.
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 d7e18c5

Please sign in to comment.