diff --git a/Makefile b/Makefile
index 4ba0dec..a638609 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index 1011344..0ca70a5 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -4,8 +4,8 @@ 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
- Drops support to Python 2 obliterates the ``sure.compat`` module
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/sure/cli.py b/sure/cli.py
index f18c98b..370f0e9 100644
--- a/sure/cli.py
+++ b/sure/cli.py
@@ -18,7 +18,6 @@
import os
import sys
-import logging
from glob import glob
from itertools import chain as flatten
from functools import reduce
@@ -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,
@@ -74,7 +77,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,18 +89,20 @@ 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,
}
cov = with_coverage and coverage.Coverage(**coverageopts) or None
if cov:
- cov.erase()
+ cover_erase and cov.erase()
cov.load()
cov.start()
@@ -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/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/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..50bf076 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))
@@ -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)
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/test_runner.py b/tests/functional/test_runner.py
index 6c156e2..1950b00 100644
--- a/tests/functional/test_runner.py
+++ b/tests/functional/test_runner.py
@@ -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)
@@ -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)
@@ -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"
)
@@ -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(
diff --git a/tests/test_runtime/test_feature.py b/tests/test_runtime/test_feature.py
new file mode 100644
index 0000000..4762925
--- /dev/null
+++ b/tests/test_runtime/test_feature.py
@@ -0,0 +1,41 @@
+# -*- 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 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('')
+
+
+def test_feature_without_description():
+ "repr(sure.runtime.Feature) with description"
+
+ feature = stub(Feature, title="title", description=None)
+
+ expects(repr(feature)).to.equal('')
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_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/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_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