Skip to content

Commit

Permalink
allure-robotframework adapter (via allure-framework#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
skhomuti authored and GilBecker-Anaplan committed Apr 10, 2023
1 parent 54dc03b commit d6e4f79
Show file tree
Hide file tree
Showing 39 changed files with 915 additions and 74 deletions.
1 change: 1 addition & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pipeline {
sh 'tox --workdir=/tmp -c allure-python-commons/tox.ini'
sh 'tox --workdir=/tmp -c allure-pytest/tox.ini'
sh 'tox --workdir=/tmp -c allure-behave/tox.ini'
sh 'tox --workdir=/tmp -c allure-robotframework/tox.ini'
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Allure [pytest](http://pytest.org) integration. It's developed as pytest plugin
Allure [behave](http://pythonhosted.org/behave/) integration. Just external formatter that produce test results in
allure2 format. This package is available on [pypi](https://pypi.python.org/pypi/allure-behave)

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

## Allure python commons
Common engine for all modules. It is useful for make integration with your homemade frameworks.
Expand Down
4 changes: 3 additions & 1 deletion allure-python-commons/src/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ def _last_executable(self):
def get_item(self, uuid):
return self._items.get(uuid)

def get_last_item(self, item_type):
def get_last_item(self, item_type=None):
for _uuid in reversed(self._items):
if item_type is None:
return self._items.get(_uuid)
if type(self._items[_uuid]) == item_type:
return self._items.get(_uuid)

Expand Down
18 changes: 1 addition & 17 deletions allure-robotframework/README.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
Allure Robot Framework Listener
===============================
.. image:: https://pypip.in/v/allure-robotframework/badge.png
:alt: Release Status
:target: https://pypi.python.org/pypi/allure-robotframework
.. image:: https://pypip.in/d/allure-robotframework/badge.png
:alt: Downloads
:target: https://pypi.python.org/pypi/allure-robotframework

- `Source <https://github.com/allure-framework/allure-python>`_

Expand Down Expand Up @@ -33,14 +27,4 @@ Listener support `robotframework-pabot library <https://pypi.python.org/pypi/rob

.. code:: bash
$ pabot --listener allure_robotframework ./my_robot_test
Advanced listener settings:

- ALLURE_MAX_STEP_MESSAGE_COUNT=5. If robotframework step contains less messages than specified in this setting, each message shows as substep. This reduces the number of attachments in large projects. The default value is zero - all messages are displayed as attachments.

Contributing to allure-robotframework
=====================================

This project exists thanks to all the people who contribute. Especially by `Megafon <https://corp.megafon.com>`_ and
`@skhomuti <https://github.com/skhomuti>`_ who started and maintaining allure-robotframework.
$ pabot --listener allure_robotframework ./my_robot_test
43 changes: 11 additions & 32 deletions allure-robotframework/setup.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@

import os
from setuptools import setup

PACKAGE = "allure-robotframework"

classifiers = [
'Development Status :: 5 - Production/Stable',
'Framework :: Robot Framework',
'Framework :: Robot Framework :: Tool',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Topic :: Software Development :: Quality Assurance',
'Topic :: Software Development :: Testing',
]

setup_requires = [
"setuptools_scm"
]

install_requires = [
"allure-python-commons==2.3.4b1",
]


def prepare_version():
from setuptools_scm import get_version
configuration = {"root": "..", "relative_to": __file__}
version = get_version(**configuration)
install_requires.append("allure-python-commons=={version}".format(version=version))
return configuration
PACKAGE = "allure-robotframework"
VERSION = "0.1.3"


def get_readme(fname):
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()


if __name__ == '__main__':
setup(
name=PACKAGE,
use_scm_version=prepare_version,
version=VERSION,
description="Allure Robot Framework integration",
license="Apache-2.0",
install_requires=install_requires,
setup_requires=setup_requires,
keywords="allure reporting robotframework",
packages=['allure_robotframework', 'AllureLibrary'],
package_dir={"allure_robotframework": "src/listener", 'AllureLibrary': 'src/library'},
packages=['allure_robotframework'],
package_dir={"allure_robotframework": "src"},
install_requires=install_requires,
py_modules=['allure_robotframework'],
url="https://github.com/allure-framework/allure-python",
url="https://github.com/skhomuti/allure-python",
author="Sergey Khomutinin",
author_email="[email protected]",
long_description=get_readme('README.rst'),
classifiers=classifiers,
long_description=read('README.rst'),
)
3 changes: 3 additions & 0 deletions allure-robotframework/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from allure_robotframework.listener import allure_robotframework

__all__ = ['allure_robotframework']
30 changes: 30 additions & 0 deletions allure-robotframework/src/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class RobotStatus(object):
FAILED = 'FAIL'
PASSED = 'PASS'


class RobotKeywordType(object):
SETUP = 'Setup'
TEARDOWN = 'Teardown'
KEYWORD = 'Keyword'
LOOP = 'FOR'
LOOP_ITEM = 'FOR ITEM'
FIXTURES = [SETUP, TEARDOWN]


class RobotLogLevel(object):
FAIL = 'FAIL'
ERROR = 'ERROR'
WARNING = 'WARN'
INFORMATION = 'INFO'
DEBUG = 'DEBUG'
TRACE = 'TRACE'

CRITICAL_LEVELS = [FAIL, ERROR]


class RobotBasicKeywords(object):
BUILTIN_LIB = 'BuiltIn'
NO_OPERATION = BUILTIN_LIB + '.No Operation'
FAIL = BUILTIN_LIB + '.Fail'
LOG = BUILTIN_LIB + '.Log'
173 changes: 173 additions & 0 deletions allure-robotframework/src/listener.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from __future__ import absolute_import

from collections import OrderedDict
from allure_commons.model2 import TestResultContainer, TestResult, TestStepResult, TestAfterResult, TestBeforeResult, \
StatusDetails, Label, Link
from allure_commons.reporter import AllureReporter
from allure_commons.utils import now, uuid4, md5, host_tag
from allure_commons.logger import AllureFileLogger
from allure_commons.types import AttachmentType, LabelType, LinkType
from allure_commons import plugin_manager
from robot.libraries.BuiltIn import BuiltIn
from allure_robotframework.constants import RobotKeywordType, RobotLogLevel
from allure_robotframework import utils
import os


# noinspection PyPep8Naming
class allure_robotframework(object):
ROBOT_LISTENER_API_VERSION = 2
DEFAULT_OUTPUT_PATH = os.path.join('output', 'allure')
LOG_MESSAGE_FORMAT = '{full_message}<p><b>[{level}]</b> {message}</p>'
FAIL_MESSAGE_FORMAT = '{full_message}<p style="color: red"><b>[{level}]</b> {message}</p>'

def __init__(self, logger_path=DEFAULT_OUTPUT_PATH):
self.reporter = AllureReporter()
self.logger = AllureFileLogger(logger_path)
self.stack = []
self.items_log = {}
self.pool_id = None
self.links = OrderedDict()
plugin_manager.register(self.reporter)
plugin_manager.register(self.logger)

def start_suite(self, name, attributes):
if not self.pool_id:
self.pool_id = BuiltIn().get_variable_value('${PABOTEXECUTIONPOOLID}')
self.pool_id = int(self.pool_id) if self.pool_id else 0
self.start_new_group(name, attributes)

def end_suite(self, name, attributes):
self.stop_current_group()

def start_test(self, name, attributes):
self.start_new_group(name, attributes)
self.start_new_test(name, attributes)

def end_test(self, name, attributes):
self.stop_current_test(name, attributes)
self.stop_current_group()

def start_keyword(self, name, attributes):
self.start_new_keyword(name, attributes)

def end_keyword(self, name, attributes):
self.end_current_keyword(name, attributes)

def log_message(self, message):
level = message.get('level')
if level == RobotLogLevel.FAIL:
self.reporter.get_item(self.stack[-1]).statusDetails = StatusDetails(message=message.get('message'))
self.append_message_to_last_item_log(message, level)

# listener event ends
def start_new_group(self, name, attributes):
uuid = uuid4()
self.set_suite_link(attributes.get('metadata'), uuid)
if self.stack:
parent_suite = self.reporter.get_last_item(TestResultContainer)
parent_suite.children.append(uuid)
self.stack.append(uuid)
suite = TestResultContainer(uuid=uuid,
name=name,
description=attributes.get('doc'),
start=now())
self.reporter.start_group(uuid, suite)

def stop_current_group(self):
uuid = self.stack.pop()
self.remove_suite_link(uuid)
self.reporter.stop_group(uuid, stop=now())

def start_new_test(self, name, attributes):
uuid = uuid4()
self.reporter.get_last_item(TestResultContainer).children.append(uuid)
self.stack.append(uuid)
test_case = TestResult(uuid=uuid,
historyId=md5(attributes.get('longname')),
name=name,
fullName=attributes.get('longname'),
start=now())
self.reporter.schedule_test(uuid, test_case)

def stop_current_test(self, name, attributes):
uuid = self.stack.pop()
test = self.reporter.get_test(uuid)
test.status = utils.get_allure_status(attributes.get('status'))
test.labels.extend(utils.get_allure_suites(attributes.get('longname')))
test.labels.extend(utils.get_allure_tags(attributes.get('tags')))
test.labels.append(utils.get_allure_thread(self.pool_id))
test.labels.append(Label(LabelType.HOST, value=host_tag()))
test.statusDetails = StatusDetails(message=attributes.get('message'))
test.description = attributes.get('doc')
last_link = list(self.links.values())[-1] if self.links else None
if last_link:
test.links.append(Link(LinkType.LINK, last_link, 'Link'))
test.stop = now()
self.reporter.close_test(uuid)

def start_new_keyword(self, name, attributes):
uuid = uuid4()
parent_uuid = self.stack[-1]
step_name = '{} = {}'.format(attributes.get('assign')[0], name) if attributes.get('assign') else name
args = {
'name': step_name,
'description': attributes.get('doc'),
'parameters': utils.get_allure_parameters(attributes.get('args')),
'start': now()
}
keyword_type = attributes.get('type')
last_item = self.reporter.get_last_item()
if keyword_type in RobotKeywordType.FIXTURES and not isinstance(last_item, TestStepResult):
if isinstance(last_item, TestResult):
parent_uuid = self.stack[-2]
if keyword_type == RobotKeywordType.SETUP:
self.reporter.start_before_fixture(parent_uuid, uuid, TestBeforeResult(**args))
elif keyword_type == RobotKeywordType.TEARDOWN:
self.reporter.start_after_fixture(parent_uuid, uuid, TestAfterResult(**args))
self.stack.append(uuid)
return
self.stack.append(uuid)
self.reporter.start_step(parent_uuid=parent_uuid,
uuid=uuid,
step=TestStepResult(**args))

def end_current_keyword(self, name, attributes):
uuid = self.stack.pop()
if uuid in self.items_log:
self.reporter.attach_data(uuid=uuid4(),
body=self.items_log.pop(uuid).replace('\n', '<br>'),
name='Keyword Log',
attachment_type=AttachmentType.HTML)
args = {
'uuid': uuid,
'status': utils.get_allure_status(attributes.get('status')),
'stop': now()
}
keyword_type = attributes.get('type')
parent_item = self.reporter.get_last_item()
if keyword_type in RobotKeywordType.FIXTURES and not isinstance(parent_item, TestStepResult):
if keyword_type == RobotKeywordType.SETUP:
self.reporter.stop_before_fixture(**args)
return
elif keyword_type == RobotKeywordType.TEARDOWN:
self.reporter.stop_after_fixture(**args)
return
self.reporter.stop_step(**args)

def append_message_to_last_item_log(self, message, level):
full_message = self.items_log[self.stack[-1]] if self.stack[-1] in self.items_log else ''
message_format = self.FAIL_MESSAGE_FORMAT if level in RobotLogLevel.CRITICAL_LEVELS else self.LOG_MESSAGE_FORMAT
self.items_log[self.stack[-1]] = message_format.format(full_message=full_message,
level=message.get('level'),
message=message.get('message'))

def set_suite_link(self, metadata, uuid):
if metadata:
link = metadata.get('Link')
if link:
self.links[uuid] = link

def remove_suite_link(self, uuid):
if self.links.get(uuid):
self.links.pop(uuid)
41 changes: 41 additions & 0 deletions allure-robotframework/src/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import absolute_import
from allure_commons.model2 import Status, Label, Parameter
from allure_commons.types import LabelType
from allure_robotframework.constants import RobotStatus


def get_allure_status(status):
return Status.PASSED if status == RobotStatus.PASSED else Status.FAILED


def get_allure_parameters(parameters):
return [Parameter(name="arg{}".format(i + 1), value=param) for i, param in enumerate(parameters)]


def get_allure_suites(longname):
"""
>>> get_allure_suites('Suite1.Test')
[Label(name='suite', value='Suite1')]
>>> get_allure_suites('Suite1.Suite2.Test')
[Label(name='suite', value='Suite1'), Label(name='subSuite', value='Suite2')]
>>> get_allure_suites('Suite1.Suite2.Suite3.Test') # doctest: +NORMALIZE_WHITESPACE
[Label(name='parentSuite', value='Suite1'),
Label(name='suite', value='Suite2'),
Label(name='subSuite', value='Suite3')]
"""
labels = []
suites = longname.split('.')
if len(suites) > 3:
labels.append(Label('parentSuite', suites.pop(0)))
labels.append(Label('suite', suites.pop(0)))
if len(suites) > 1:
labels.append(Label('subSuite', '.'.join(suites[:-1])))
return labels


def get_allure_tags(tags):
return [Label(LabelType.TAG, tag) for tag in tags]


def get_allure_thread(pool_id):
return Label(LabelType.THREAD, 'Thread #{number}'.format(number=pool_id))
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*** Test Cases ***
Case With Description
[Documentation] Case description
No Operation

Case With Dynamic Description
[Documentation] Start description
Set Test Documentation End description
Loading

0 comments on commit d6e4f79

Please sign in to comment.