Skip to content

Commit 10a0cbb

Browse files
skhomutisseliverstov
authored andcommitted
allure-robotframework adapter (via #214)
1 parent 7515396 commit 10a0cbb

39 files changed

+981
-1
lines changed

Jenkinsfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pipeline {
1010
sh 'tox --workdir=/tmp -c allure-python-commons/tox.ini'
1111
sh 'tox --workdir=/tmp -c allure-pytest/tox.ini'
1212
sh 'tox --workdir=/tmp -c allure-behave/tox.ini'
13+
sh 'tox --workdir=/tmp -c allure-robotframework/tox.ini'
1314
}
1415
}
1516
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Allure [pytest](http://pytest.org) integration. It's developed as pytest plugin
1414
Allure [behave](http://pythonhosted.org/behave/) integration. Just external formatter that produce test results in
1515
allure2 format. This package is available on [pypi](https://pypi.python.org/pypi/allure-behave)
1616

17+
## Robot Framework
18+
Allure [RobotFramework](http://robotframework.org/) integration. This integration is a
19+
[Listener](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#listener-interface)
20+
and does not require changing autotests. Available on [pypi](https://pypi.python.org/pypi/allure-robotframework)
1721

1822
## Allure python commons
1923
Common engine for all modules. It is useful for make integration with your homemade frameworks.

allure-python-commons/src/reporter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ def _last_executable(self):
3030
def get_item(self, uuid):
3131
return self._items.get(uuid)
3232

33-
def get_last_item(self, item_type):
33+
def get_last_item(self, item_type=None):
3434
for _uuid in reversed(self._items):
35+
if item_type is None:
36+
return self._items.get(_uuid)
3537
if type(self._items[_uuid]) == item_type:
3638
return self._items.get(_uuid)
3739

allure-robotframework/README.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Allure Robot Framework Listener
2+
===============================
3+
4+
- `Source <https://github.com/allure-framework/allure-python>`_
5+
6+
- `Documentation <https://docs.qameta.io/allure/2.0>`_
7+
8+
- `Gitter <https://gitter.im/allure-framework/allure-core>`_
9+
10+
Installation and Usage
11+
======================
12+
13+
.. code:: bash
14+
15+
$ pip install allure-robotframework
16+
$ robot --listener allure_robotframework ./my_robot_test
17+
18+
Optional argument sets output directory. Example:
19+
20+
.. code:: bash
21+
22+
$ robot --listener allure_robotframework;/set/your/path/here ./my_robot_test
23+
24+
Default output directory is `output/allure`.
25+
26+
Listener support `robotframework-pabot library <https://pypi.python.org/pypi/robotframework-pabot>`_:
27+
28+
.. code:: bash
29+
30+
$ pabot --listener allure_robotframework ./my_robot_test

allure-robotframework/setup.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
import os
3+
from setuptools import setup
4+
5+
install_requires = [
6+
"allure-python-commons==2.3.4b1",
7+
]
8+
9+
PACKAGE = "allure-robotframework"
10+
VERSION = "0.1.3"
11+
12+
13+
def read(fname):
14+
return open(os.path.join(os.path.dirname(__file__), fname)).read()
15+
16+
17+
if __name__ == '__main__':
18+
setup(
19+
name=PACKAGE,
20+
version=VERSION,
21+
description="Allure Robot Framework integration",
22+
license="Apache-2.0",
23+
keywords="allure reporting robotframework",
24+
packages=['allure_robotframework'],
25+
package_dir={"allure_robotframework": "src"},
26+
install_requires=install_requires,
27+
py_modules=['allure_robotframework'],
28+
url="https://github.com/skhomuti/allure-python",
29+
author="Sergey Khomutinin",
30+
author_email="[email protected]",
31+
long_description=read('README.rst'),
32+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from allure_robotframework.listener import allure_robotframework
2+
3+
__all__ = ['allure_robotframework']
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class RobotStatus(object):
2+
FAILED = 'FAIL'
3+
PASSED = 'PASS'
4+
5+
6+
class RobotKeywordType(object):
7+
SETUP = 'Setup'
8+
TEARDOWN = 'Teardown'
9+
KEYWORD = 'Keyword'
10+
LOOP = 'FOR'
11+
LOOP_ITEM = 'FOR ITEM'
12+
FIXTURES = [SETUP, TEARDOWN]
13+
14+
15+
class RobotLogLevel(object):
16+
FAIL = 'FAIL'
17+
ERROR = 'ERROR'
18+
WARNING = 'WARN'
19+
INFORMATION = 'INFO'
20+
DEBUG = 'DEBUG'
21+
TRACE = 'TRACE'
22+
23+
CRITICAL_LEVELS = [FAIL, ERROR]
24+
25+
26+
class RobotBasicKeywords(object):
27+
BUILTIN_LIB = 'BuiltIn'
28+
NO_OPERATION = BUILTIN_LIB + '.No Operation'
29+
FAIL = BUILTIN_LIB + '.Fail'
30+
LOG = BUILTIN_LIB + '.Log'
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
from __future__ import absolute_import
2+
3+
from collections import OrderedDict
4+
from allure_commons.model2 import TestResultContainer, TestResult, TestStepResult, TestAfterResult, TestBeforeResult, \
5+
StatusDetails, Label, Link
6+
from allure_commons.reporter import AllureReporter
7+
from allure_commons.utils import now, uuid4, md5, host_tag
8+
from allure_commons.logger import AllureFileLogger
9+
from allure_commons.types import AttachmentType, LabelType, LinkType
10+
from allure_commons import plugin_manager
11+
from robot.libraries.BuiltIn import BuiltIn
12+
from allure_robotframework.constants import RobotKeywordType, RobotLogLevel
13+
from allure_robotframework import utils
14+
import os
15+
16+
17+
# noinspection PyPep8Naming
18+
class allure_robotframework(object):
19+
ROBOT_LISTENER_API_VERSION = 2
20+
DEFAULT_OUTPUT_PATH = os.path.join('output', 'allure')
21+
LOG_MESSAGE_FORMAT = '{full_message}<p><b>[{level}]</b> {message}</p>'
22+
FAIL_MESSAGE_FORMAT = '{full_message}<p style="color: red"><b>[{level}]</b> {message}</p>'
23+
24+
def __init__(self, logger_path=DEFAULT_OUTPUT_PATH):
25+
self.reporter = AllureReporter()
26+
self.logger = AllureFileLogger(logger_path)
27+
self.stack = []
28+
self.items_log = {}
29+
self.pool_id = None
30+
self.links = OrderedDict()
31+
plugin_manager.register(self.reporter)
32+
plugin_manager.register(self.logger)
33+
34+
def start_suite(self, name, attributes):
35+
if not self.pool_id:
36+
self.pool_id = BuiltIn().get_variable_value('${PABOTEXECUTIONPOOLID}')
37+
self.pool_id = int(self.pool_id) if self.pool_id else 0
38+
self.start_new_group(name, attributes)
39+
40+
def end_suite(self, name, attributes):
41+
self.stop_current_group()
42+
43+
def start_test(self, name, attributes):
44+
self.start_new_group(name, attributes)
45+
self.start_new_test(name, attributes)
46+
47+
def end_test(self, name, attributes):
48+
self.stop_current_test(name, attributes)
49+
self.stop_current_group()
50+
51+
def start_keyword(self, name, attributes):
52+
self.start_new_keyword(name, attributes)
53+
54+
def end_keyword(self, name, attributes):
55+
self.end_current_keyword(name, attributes)
56+
57+
def log_message(self, message):
58+
level = message.get('level')
59+
if level == RobotLogLevel.FAIL:
60+
self.reporter.get_item(self.stack[-1]).statusDetails = StatusDetails(message=message.get('message'))
61+
self.append_message_to_last_item_log(message, level)
62+
63+
# listener event ends
64+
def start_new_group(self, name, attributes):
65+
uuid = uuid4()
66+
self.set_suite_link(attributes.get('metadata'), uuid)
67+
if self.stack:
68+
parent_suite = self.reporter.get_last_item(TestResultContainer)
69+
parent_suite.children.append(uuid)
70+
self.stack.append(uuid)
71+
suite = TestResultContainer(uuid=uuid,
72+
name=name,
73+
description=attributes.get('doc'),
74+
start=now())
75+
self.reporter.start_group(uuid, suite)
76+
77+
def stop_current_group(self):
78+
uuid = self.stack.pop()
79+
self.remove_suite_link(uuid)
80+
self.reporter.stop_group(uuid, stop=now())
81+
82+
def start_new_test(self, name, attributes):
83+
uuid = uuid4()
84+
self.reporter.get_last_item(TestResultContainer).children.append(uuid)
85+
self.stack.append(uuid)
86+
test_case = TestResult(uuid=uuid,
87+
historyId=md5(attributes.get('longname')),
88+
name=name,
89+
fullName=attributes.get('longname'),
90+
start=now())
91+
self.reporter.schedule_test(uuid, test_case)
92+
93+
def stop_current_test(self, name, attributes):
94+
uuid = self.stack.pop()
95+
test = self.reporter.get_test(uuid)
96+
test.status = utils.get_allure_status(attributes.get('status'))
97+
test.labels.extend(utils.get_allure_suites(attributes.get('longname')))
98+
test.labels.extend(utils.get_allure_tags(attributes.get('tags')))
99+
test.labels.append(utils.get_allure_thread(self.pool_id))
100+
test.labels.append(Label(LabelType.HOST, value=host_tag()))
101+
test.statusDetails = StatusDetails(message=attributes.get('message'))
102+
test.description = attributes.get('doc')
103+
last_link = list(self.links.values())[-1] if self.links else None
104+
if last_link:
105+
test.links.append(Link(LinkType.LINK, last_link, 'Link'))
106+
test.stop = now()
107+
self.reporter.close_test(uuid)
108+
109+
def start_new_keyword(self, name, attributes):
110+
uuid = uuid4()
111+
parent_uuid = self.stack[-1]
112+
step_name = '{} = {}'.format(attributes.get('assign')[0], name) if attributes.get('assign') else name
113+
args = {
114+
'name': step_name,
115+
'description': attributes.get('doc'),
116+
'parameters': utils.get_allure_parameters(attributes.get('args')),
117+
'start': now()
118+
}
119+
keyword_type = attributes.get('type')
120+
last_item = self.reporter.get_last_item()
121+
if keyword_type in RobotKeywordType.FIXTURES and not isinstance(last_item, TestStepResult):
122+
if isinstance(last_item, TestResult):
123+
parent_uuid = self.stack[-2]
124+
if keyword_type == RobotKeywordType.SETUP:
125+
self.reporter.start_before_fixture(parent_uuid, uuid, TestBeforeResult(**args))
126+
elif keyword_type == RobotKeywordType.TEARDOWN:
127+
self.reporter.start_after_fixture(parent_uuid, uuid, TestAfterResult(**args))
128+
self.stack.append(uuid)
129+
return
130+
self.stack.append(uuid)
131+
self.reporter.start_step(parent_uuid=parent_uuid,
132+
uuid=uuid,
133+
step=TestStepResult(**args))
134+
135+
def end_current_keyword(self, name, attributes):
136+
uuid = self.stack.pop()
137+
if uuid in self.items_log:
138+
self.reporter.attach_data(uuid=uuid4(),
139+
body=self.items_log.pop(uuid).replace('\n', '<br>'),
140+
name='Keyword Log',
141+
attachment_type=AttachmentType.HTML)
142+
args = {
143+
'uuid': uuid,
144+
'status': utils.get_allure_status(attributes.get('status')),
145+
'stop': now()
146+
}
147+
keyword_type = attributes.get('type')
148+
parent_item = self.reporter.get_last_item()
149+
if keyword_type in RobotKeywordType.FIXTURES and not isinstance(parent_item, TestStepResult):
150+
if keyword_type == RobotKeywordType.SETUP:
151+
self.reporter.stop_before_fixture(**args)
152+
return
153+
elif keyword_type == RobotKeywordType.TEARDOWN:
154+
self.reporter.stop_after_fixture(**args)
155+
return
156+
self.reporter.stop_step(**args)
157+
158+
def append_message_to_last_item_log(self, message, level):
159+
full_message = self.items_log[self.stack[-1]] if self.stack[-1] in self.items_log else ''
160+
message_format = self.FAIL_MESSAGE_FORMAT if level in RobotLogLevel.CRITICAL_LEVELS else self.LOG_MESSAGE_FORMAT
161+
self.items_log[self.stack[-1]] = message_format.format(full_message=full_message,
162+
level=message.get('level'),
163+
message=message.get('message'))
164+
165+
def set_suite_link(self, metadata, uuid):
166+
if metadata:
167+
link = metadata.get('Link')
168+
if link:
169+
self.links[uuid] = link
170+
171+
def remove_suite_link(self, uuid):
172+
if self.links.get(uuid):
173+
self.links.pop(uuid)

allure-robotframework/src/utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import absolute_import
2+
from allure_commons.model2 import Status, Label, Parameter
3+
from allure_commons.types import LabelType
4+
from allure_robotframework.constants import RobotStatus
5+
6+
7+
def get_allure_status(status):
8+
return Status.PASSED if status == RobotStatus.PASSED else Status.FAILED
9+
10+
11+
def get_allure_parameters(parameters):
12+
return [Parameter(name="arg{}".format(i + 1), value=param) for i, param in enumerate(parameters)]
13+
14+
15+
def get_allure_suites(longname):
16+
"""
17+
>>> get_allure_suites('Suite1.Test')
18+
[Label(name='suite', value='Suite1')]
19+
>>> get_allure_suites('Suite1.Suite2.Test')
20+
[Label(name='suite', value='Suite1'), Label(name='subSuite', value='Suite2')]
21+
>>> get_allure_suites('Suite1.Suite2.Suite3.Test') # doctest: +NORMALIZE_WHITESPACE
22+
[Label(name='parentSuite', value='Suite1'),
23+
Label(name='suite', value='Suite2'),
24+
Label(name='subSuite', value='Suite3')]
25+
"""
26+
labels = []
27+
suites = longname.split('.')
28+
if len(suites) > 3:
29+
labels.append(Label('parentSuite', suites.pop(0)))
30+
labels.append(Label('suite', suites.pop(0)))
31+
if len(suites) > 1:
32+
labels.append(Label('subSuite', '.'.join(suites[:-1])))
33+
return labels
34+
35+
36+
def get_allure_tags(tags):
37+
return [Label(LabelType.TAG, tag) for tag in tags]
38+
39+
40+
def get_allure_thread(pool_id):
41+
return Label(LabelType.THREAD, 'Thread #{number}'.format(number=pool_id))

allure-robotframework/test/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)