Skip to content

Commit 31fd1d2

Browse files
authored
nose2 first version (via #513)
1 parent d6fd9f4 commit 31fd1d2

File tree

23 files changed

+719
-27
lines changed

23 files changed

+719
-27
lines changed

.github/workflows/build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jobs:
1010
package: [
1111
allure-python-commons-test,
1212
allure-python-commons,
13+
allure-nose2,
1314
allure-behave,
1415
allure-pytest,
1516
allure-pytest-bdd,

.github/workflows/release.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ jobs:
4040
twine upload dist/*
4141
popd
4242
43+
pushd allure-nose2
44+
python setup.py sdist bdist_wheel
45+
twine upload dist/*
46+
popd
47+
4348
pushd allure-pytest
4449
python setup.py sdist bdist_wheel
4550
twine upload dist/*

allure-nose2/README.rst

Whitespace-only changes.

allure-nose2/setup.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
from setuptools import setup
3+
4+
PACKAGE = "allure-nose2"
5+
6+
classifiers = [
7+
'Development Status :: 5 - Production/Stable',
8+
'Intended Audience :: Developers',
9+
'License :: OSI Approved :: Apache Software License',
10+
'Topic :: Software Development :: Quality Assurance',
11+
'Topic :: Software Development :: Testing',
12+
'Programming Language :: Python :: 3',
13+
'Programming Language :: Python :: 3.6',
14+
'Programming Language :: Python :: 3.7',
15+
]
16+
17+
setup_requires = [
18+
"setuptools_scm"
19+
]
20+
21+
install_requires = [
22+
"nose2"
23+
]
24+
25+
26+
def prepare_version():
27+
from setuptools_scm import get_version
28+
configuration = {"root": "..", "relative_to": __file__}
29+
version = get_version(**configuration)
30+
install_requires.append("allure-python-commons=={version}".format(version=version))
31+
return configuration
32+
33+
34+
def get_readme(fname):
35+
return open(os.path.join(os.path.dirname(__file__), fname)).read()
36+
37+
38+
def main():
39+
setup(
40+
name=PACKAGE,
41+
use_scm_version=prepare_version,
42+
description="Allure nose2 integration",
43+
url="https://github.com/allure-framework/allure-python",
44+
author="QAMetaSoftware, Stanislav Seliverstov",
45+
author_email="[email protected]",
46+
license="Apache-2.0",
47+
classifiers=classifiers,
48+
keywords="allure reporting nose2",
49+
long_description=get_readme('README.rst'),
50+
packages=["allure_nose2"],
51+
package_dir={"allure_nose2": "src"},
52+
setup_requires=setup_requires,
53+
install_requires=install_requires
54+
)
55+
56+
if __name__ == '__main__':
57+
main()
58+

allure-nose2/src/__init__.py

Whitespace-only changes.

allure-nose2/src/listener.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
# ToDo attaches
3+
class AllureListener(object):
4+
def __init__(self, lifecycle):
5+
self.lifecycle = lifecycle

allure-nose2/src/plugin.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from nose2.events import Plugin
2+
from allure_commons import plugin_manager
3+
from allure_commons.logger import AllureFileLogger
4+
from allure_nose2.listener import AllureListener
5+
from allure_commons.lifecycle import AllureLifecycle
6+
from nose2 import result
7+
from allure_commons.model2 import Status
8+
from allure_commons.model2 import StatusDetails
9+
from allure_commons.model2 import Label
10+
from allure_commons.types import LabelType
11+
from allure_commons.utils import host_tag, thread_tag
12+
13+
from allure_commons.utils import platform_label, md5
14+
15+
16+
from .utils import timestamp_millis, status_details, update_attrs, labels, name, fullname, params
17+
import allure_commons
18+
19+
20+
class DecoratorsHelper(object):
21+
@classmethod
22+
@allure_commons.hookimpl
23+
def decorate_as_label(cls, label_type, labels):
24+
# ToDo functools.update_wrapper
25+
def wrapper(test):
26+
update_attrs(test, label_type, labels)
27+
return test
28+
29+
return wrapper
30+
31+
@classmethod
32+
def register(cls):
33+
if cls not in plugin_manager.get_plugins():
34+
plugin_manager.register(cls)
35+
36+
@classmethod
37+
def unregister(cls):
38+
if cls in plugin_manager.get_plugins():
39+
plugin_manager.unregister(plugin=cls)
40+
41+
42+
DecoratorsHelper.register()
43+
44+
45+
class Allure(Plugin):
46+
configSection = 'allure'
47+
commandLineSwitch = (None, "allure", "Generate an Allure report")
48+
49+
def __init__(self, *args, **kwargs):
50+
super(Allure, self).__init__(*args, **kwargs)
51+
self._host = host_tag()
52+
self._thread = thread_tag()
53+
self.lifecycle = AllureLifecycle()
54+
self.logger = AllureFileLogger("allure-result")
55+
self.listener = AllureListener(self.lifecycle)
56+
57+
def registerInSubprocess(self, event):
58+
self.unregister_allure_plugins()
59+
event.pluginClasses.append(self.__class__)
60+
61+
def startSubprocess(self, event):
62+
self.register_allure_plugins()
63+
64+
def stopSubprocess(self, event):
65+
self.unregister_allure_plugins()
66+
67+
def register_allure_plugins(self):
68+
plugin_manager.register(self.listener)
69+
plugin_manager.register(self.logger)
70+
71+
def unregister_allure_plugins(self):
72+
plugin_manager.unregister(plugin=self.listener)
73+
plugin_manager.unregister(plugin=self.logger)
74+
75+
def is_registered(self):
76+
return all([plugin_manager.is_registered(self.listener),
77+
plugin_manager.is_registered(self.logger)])
78+
79+
def startTestRun(self, event):
80+
self.register_allure_plugins()
81+
82+
def afterTestRun(self, event):
83+
self.unregister_allure_plugins()
84+
85+
def startTest(self, event):
86+
if self.is_registered():
87+
with self.lifecycle.schedule_test_case() as test_result:
88+
test_result.name = name(event)
89+
test_result.start = timestamp_millis(event.startTime)
90+
test_result.fullName = fullname(event)
91+
test_result.testCaseId = md5(test_result.fullName)
92+
test_result.historyId = md5(event.test.id())
93+
test_result.labels.extend(labels(event.test))
94+
test_result.labels.append(Label(name=LabelType.HOST, value=self._host))
95+
test_result.labels.append(Label(name=LabelType.THREAD, value=self._thread))
96+
test_result.labels.append(Label(name=LabelType.FRAMEWORK, value='nose2'))
97+
test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label()))
98+
test_result.parameters = params(event)
99+
100+
def stopTest(self, event):
101+
if self.is_registered():
102+
with self.lifecycle.update_test_case() as test_result:
103+
test_result.stop = timestamp_millis(event.stopTime)
104+
self.lifecycle.write_test_case()
105+
106+
def testOutcome(self, event):
107+
if self.is_registered():
108+
with self.lifecycle.update_test_case() as test_result:
109+
if event.outcome == result.PASS and event.expected:
110+
test_result.status = Status.PASSED
111+
elif event.outcome == result.PASS and not event.expected:
112+
test_result.status = Status.PASSED
113+
test_result.statusDetails = StatusDetails(message="test passes unexpectedly")
114+
elif event.outcome == result.FAIL and not event.expected:
115+
test_result.status = Status.FAILED
116+
test_result.statusDetails = status_details(event)
117+
elif event.outcome == result.ERROR:
118+
test_result.status = Status.BROKEN
119+
test_result.statusDetails = status_details(event)
120+
elif event.outcome == result.SKIP:
121+
test_result.status = Status.SKIPPED
122+
test_result.statusDetails = status_details(event)
123+
# Todo default status and other cases
124+
# elif event.outcome == result.FAIL and event.expected:
125+
# pass
126+
# self.skipped += 1
127+
# skipped = ET.SubElement(testcase, 'skipped')
128+
# skipped.set('message', 'expected test failure')
129+
# skipped.text = msg

allure-nose2/src/utils.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from traceback import format_exception_only
2+
from allure_commons.model2 import StatusDetails, Label
3+
from allure_commons.model2 import Parameter
4+
from allure_commons.utils import represent
5+
from nose2 import util
6+
import inspect
7+
8+
# ToDo move to commons
9+
ALLURE_LABELS = [
10+
'epic',
11+
'feature',
12+
'story',
13+
]
14+
15+
16+
def timestamp_millis(timestamp):
17+
return int(timestamp * 1000)
18+
19+
20+
def status_details(event):
21+
message, trace = None, None
22+
if event.exc_info:
23+
exc_type, value, _ = event.exc_info
24+
message = '\n'.join(format_exception_only(exc_type, value)) if exc_type or value else None
25+
trace = ''.join(util.exc_info_to_string(event.exc_info, event.test))
26+
elif event.reason:
27+
message = event.reason
28+
29+
if message or trace:
30+
return StatusDetails(message=message, trace=trace)
31+
32+
33+
def update_attrs(test, name, values):
34+
if type(values) in (list, tuple, str) and name.isidentifier():
35+
attrib = getattr(test, name, values)
36+
if attrib and attrib != values:
37+
attrib = sum(
38+
[tuple(i) if type(i) in (tuple, list) else (i,) for i in (attrib, values)],
39+
()
40+
)
41+
setattr(test, name, attrib)
42+
43+
44+
def labels(test):
45+
46+
def _get_attrs(obj, keys):
47+
pairs = set()
48+
for key in keys:
49+
values = getattr(obj, key, ())
50+
for value in (values,) if type(values) == str else values:
51+
pairs.add((key, value))
52+
return pairs
53+
54+
keys = ALLURE_LABELS
55+
pairs = _get_attrs(test, keys)
56+
57+
if hasattr(test, "_testFunc"):
58+
pairs.update(_get_attrs(test._testFunc, keys))
59+
elif hasattr(test, "_testMethodName"):
60+
test_method = getattr(test, test._testMethodName)
61+
pairs.update(_get_attrs(test_method, keys))
62+
return [Label(name=name, value=value) for name, value in pairs]
63+
64+
65+
def name(event):
66+
full_name = fullname(event)
67+
return full_name.split(".")[-1]
68+
69+
70+
def fullname(event):
71+
if hasattr(event.test, "_testFunc"):
72+
test_module = event.test._testFunc.__module__
73+
test_name = event.test._testFunc.__name__
74+
return "{module}.{name}".format(module=test_module, name=test_name)
75+
test_id = event.test.id()
76+
return test_id.split(":")[0]
77+
78+
79+
def params(event):
80+
def _params(names, values):
81+
return [Parameter(name=name, value=represent(value)) for name, value in zip(names, values)]
82+
83+
test_id = event.test.id()
84+
85+
if len(test_id.split("\n")) > 1:
86+
if hasattr(event.test, "_testFunc"):
87+
wrapper_arg_spec = inspect.getfullargspec(event.test._testFunc)
88+
arg_set, obj = wrapper_arg_spec.defaults
89+
test_arg_spec = inspect.getfullargspec(obj)
90+
args = test_arg_spec.args
91+
return _params(args, arg_set)
92+
elif hasattr(event.test, "_testMethodName"):
93+
method = getattr(event.test, event.test._testMethodName)
94+
wrapper_arg_spec = inspect.getfullargspec(method)
95+
obj, arg_set = wrapper_arg_spec.defaults
96+
test_arg_spec = inspect.getfullargspec(obj)
97+
args = test_arg_spec.args
98+
return _params(args[1:], arg_set)

allure-nose2/test/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import sys
2+
import types
3+
from importlib import util
4+
from doctest import script_from_examples
5+
from nose2 import events
6+
7+
8+
class CurrentExample(events.Plugin):
9+
commandLineSwitch = (None, "current-example", "Method docstring to module")
10+
11+
def __init__(self, *args, **kwargs):
12+
super(CurrentExample, self).__init__(*args, **kwargs)
13+
self._current_docstring = ""
14+
15+
def startTest(self, event):
16+
if hasattr(event.test, "_testFunc"):
17+
self._current_docstring = event.test._testFunc.__doc__
18+
else:
19+
self._current_docstring = event.test._testMethodDoc
20+
21+
def get_example_module(self):
22+
module = types.ModuleType("stub")
23+
if self._current_docstring:
24+
code = script_from_examples(self._current_docstring)
25+
spec = util.spec_from_loader("example_module", origin="example_module", loader=None)
26+
module = util.module_from_spec(spec)
27+
exec(code, module.__dict__)
28+
sys.modules['example_module'] = module
29+
return module

0 commit comments

Comments
 (0)