From d7e18c5bbd6a60d00ca7c344e73c25a917a908da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Falc=C3=A3o?= Date: Tue, 16 Jan 2024 03:36:53 +0000 Subject: [PATCH] presents a few changes and introduces bit more test coverage --- docs/source/changelog.rst | 4 +- docs/source/conf.py | 6 +- sure/cli.py | 57 ++++++------------- sure/doubles/stubs.py | 4 +- sure/reporter.py | 2 - sure/reporters/feature.py | 2 +- sure/reporters/test.py | 4 +- sure/runtime.py | 29 ++++++---- sure/version.py | 2 +- tests/functional/test_runner.py | 16 +++--- tests/test_runtime/test_feature.py | 41 +++++++++++++ tests/test_runtime/test_runtime_context.py | 11 +++- .../test_runtime/test_scenario_arrangement.py | 57 ++++++++++++++++++- tests/unit/reporters/test_feature_reporter.py | 10 ++-- tests/unit/test_runtime.py | 2 +- 15 files changed, 167 insertions(+), 80 deletions(-) create mode 100644 tests/test_runtime/test_feature.py 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