Skip to content

Commit

Permalink
presents test-runner with basic failure reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielfalcao committed Aug 14, 2023
1 parent 21335b2 commit 48e8973
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 56 deletions.
4 changes: 2 additions & 2 deletions sure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
if not PY2:
basestring = str

version = "1.4.11"
version = "2.0.0"


not_here_error = (
Expand Down Expand Up @@ -206,7 +206,7 @@ def wrap(*args, **kw):
else:
exc.append(traceback.format_exc())

except Exception as e:
except Exception:
exc.append(traceback.format_exc())

end = datetime.utcnow()
Expand Down
2 changes: 1 addition & 1 deletion sure/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@click.command()
@click.argument("paths", nargs=-1)
@click.option("-r", "--reporter", default="spec")
@click.option("-r", "--reporter", default="feature")
def entrypoint(paths, reporter):
runner = Runner(resolve_path(os.getcwd()), reporter)
runner.run(paths)
1 change: 1 addition & 0 deletions sure/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Anything(object):
def __eq__(self, _):
return True


anything = Anything()


Expand Down
21 changes: 21 additions & 0 deletions sure/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# <sure - utility belt for automated testing in python>
# Copyright (C) <2010-2023> 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/>.
from __future__ import unicode_literals


class NonValidTest(Exception):
"""raised when a non-compatible test appears within the test-run session"""
6 changes: 5 additions & 1 deletion sure/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals

from typing import List
from pathlib import Path
from sure.importer import importer

Expand All @@ -34,6 +34,10 @@ def get_reporter(name: str) -> type:
return REPORTERS.get(name)


def gather_reporter_names() -> List[str]:
return list(filter(bool, REPORTERS.keys()))


class MetaReporter(type):
def __init__(cls, name, bases, attrs):
if cls.__module__ != __name__:
Expand Down
4 changes: 2 additions & 2 deletions sure/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# 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
from sure.meta import get_reporter, MetaReporter
from sure.meta import get_reporter, MetaReporter, gather_reporter_names

__path__ = os.path.abspath(os.path.dirname(__file__))

Expand Down Expand Up @@ -232,7 +232,7 @@ def from_name(cls, name):
raise RuntimeError(
'no Reporter found for name {}, options are: {}'.format(
name,
',\n'.join(_registry.keys())
',\n'.join(gather_reporter_names())
))

return found
Expand Down
27 changes: 14 additions & 13 deletions sure/reporters/spec.py → sure/reporters/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,39 @@
ballot = '✗'


class SpecReporter(Reporter):
name = 'spec'
class FeatureReporter(Reporter):
name = 'feature'

def on_start(self):
self.indentation = 0
# sh.bold_white("Running sure version ")
# sh.bold_yellow(sure.version)
# sh.white("Running sure version ")
# sh.yellow(sure.version)
sh.reset("\n")

def on_suite(self, suite):
self.indentation += 2

sh.reset(" " * self.indentation)
sh.bold_white("Scenario: '")
sh.bold_yellow(suite.name)
sh.bold_white("'")
sh.blue("Feature: ")
sh.yellow("'")
sh.green(suite.name)
sh.yellow("'")
sh.reset("\n")

def on_suite_done(self, suite, result):
# sh.reset(" " * self.indentation)
# sh.bold_white("[")
# sh.bold_black(suite.name)
# sh.bold_white("]")
# sh.bold_white(checkmark)
# sh.white("[")
# sh.normal(suite.name)
# sh.white("]")
# sh.white(checkmark)
sh.reset("\n\n")
self.indentation = 0

def on_test(self, test):
self.indentation += 2
sh.reset(" " * self.indentation)
sh.bold_white("Spec: ")
sh.bold_black(test.description)
sh.green("Scenario: ")
sh.normal(test.description)
sh.reset(" ")

def on_test_done(self, test, result):
Expand Down
94 changes: 61 additions & 33 deletions sure/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
import os
import re
import sys
import types
import inspect
import traceback

import unittest
from typing import Optional
from sure.errors import NonValidTest
from sure.importer import importer
from sure.reporter import Reporter
from mock import Mock
Expand All @@ -31,12 +34,18 @@ def stripped(string):


class SpecContext(object):
reporter: Reporter
current_test: Optional[object]

def __init__(self, reporter):
self.mock = Mock()
self.reporter = reporter
self.current_test = None

def set_current_test(self, test):
self.current_test = test

class Result(object):

class BaseResult(object):
def __init__(self, results):
self.results = results

Expand All @@ -48,7 +57,7 @@ def ok(self):
return all([x.ok for x in self.results])


class TestCaseResult(Result):
class ScenarioResult(BaseResult):
def __init__(self, case, error=None):
self.case = case
self.error = error
Expand All @@ -57,7 +66,7 @@ def printable(self):
if self.is_error:
return self.error.printable()
elif self.is_failure:
return "\n".join(traceback.format_exception(*self.error))
return str(self.error)
else:
return ""

Expand All @@ -67,7 +76,7 @@ def is_error(self):

@property
def is_failure(self):
return isinstance(self.error, tuple)
return isinstance(self.error, AssertionError)

@property
def is_success(self):
Expand All @@ -78,17 +87,17 @@ def ok(self):
return self.is_success


class TestSuiteResult(Result):
class TestSuiteResult(BaseResult):
pass


class FinalTestSuiteResult(Result):
class FinalTestSuiteResult(BaseResult):
pass


class TestSuite(object):
class Feature(object):
def __init__(self, module):
name = getattr(module, 'suite_name', getattr(module, 'scenario', getattr(module, 'name', module.__name__)))
name = getattr(module, 'suite_name', getattr(module, 'feature', getattr(module, 'name', module.__name__)))
description = getattr(module, 'suite_description', getattr(module, 'description', ""))

self.name = stripped(name)
Expand All @@ -98,8 +107,8 @@ def __init__(self, module):
self.ready = False
self.testcases = []

def load_cases(self, executables):
self.testcases = list(map((lambda e: TestCase(e, self)), executables))
def read_scenarios(self, executables):
self.testcases = list(map((lambda e: Scenario(e, self)), executables))
self.ready = True
return self.testcases

Expand Down Expand Up @@ -145,37 +154,47 @@ def printable(self):
return "\n".join(self.traceback)


class TestCase(object):
class Scenario(object):
def __init__(self, class_or_callable, suite):
fallback = class_or_callable.__name__

self.description = stripped(class_or_callable.__doc__ or fallback)

self.object = class_or_callable
if isinstance(class_or_callable, type):
if issubclass(class_or_callable, unittest.TestCase):
self.object = class_or_callable()

self.suite = suite

def run_object(self, context):
# TODO classes must be handled here
def run_unittesttestcase(self, context):
for name, member in inspect.getmembers(self.object):
if isinstance(member, types.MethodType):
# XXX: log debug else
self.run_single_test(member, context)

# maybe sure should have a `Callable` class that just takes a
# context and abstracts the way to call the callable.
def run_single_test(self, test, context):
argcount = test.__code__.co_argcount
try:
if argcount == 0:
test()
elif argcount == 1:
test(context)
else:
raise RuntimeError(f'it appears that the test function {self.object} takes more than one argument')

if self.object.__code__.co_argcount == 1:
return self.object(context)
elif self.object.__code__.co_argcount == 0:
return self.object()
else:
raise RuntimeError(f'it appears that the test function {self.object} takes more than one argument')
except AssertionError as failure:
return ScenarioResult(self, failure)
except Exception:
return ScenarioResult(self, ErrorStack(sys.exc_info()))

return ScenarioResult(self)

def run(self, context):
try:
self.run_object(context)
except AssertionError:
return TestCaseResult(self, sys.exc_info())
except:
return TestCaseResult(self, ErrorStack(sys.exc_info()))
else:
return TestCaseResult(self)
if isinstance(self.object, unittest.TestCase):
return self.run_unittesttestcase(context)

return self.run_single_test(self.object, context)


class Runner(object):
Expand Down Expand Up @@ -206,6 +225,15 @@ def find_candidates(self, lookup_paths):
return candidate_modules

def is_runnable_test(self, item):
if isinstance(item, type):
if not issubclass(item, unittest.TestCase):
return
if item == unittest.TestCase:
return

elif not isinstance(item, types.FunctionType):
return

try:
name = item.__name__
except AttributeError:
Expand All @@ -223,8 +251,8 @@ def load_suites(self, lookup_paths):
cases = []
candidates = self.find_candidates(lookup_paths)
for module, executables in map(self.extract_members, candidates):
suite = TestSuite(module)
cases.extend(suite.load_cases(executables))
suite = Feature(module)
cases.extend(suite.read_scenarios(executables))
suites.append(suite)

return suites
Expand Down
30 changes: 26 additions & 4 deletions tests/runner/test_eins.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
# -*- coding: utf-8 -*-
from unittest import TestCase

scenario = "test one"
feature = "test sure runner"


def test_assert():
"testing a simple assertion with sure runner"
assert True
# def test_function_ok():
# "testing successful function with sure runner"
# assert True


def test_function_fail():
"testing failing function with sure runner"
assert False, 'the failure appears to be right'


# class TestClass(TestCase):
# "`sure' should work seamlessly with a unittest.TestCase"

# def setUp(self):
# self.one_attribute = {
# 'question': 'does it work for us?'
# }

# def tearDown(self):
# self.one_attribute.pop('question')

# def test_expected_attribute_exists(self):
# "the setUp should work in our favor or else everything is unambiguously lost"
# assert hasattr(self, 'one_attribute'), f'{self} should have one_attribute but does not appear so'

0 comments on commit 48e8973

Please sign in to comment.