Skip to content

Adding support for nested keywords #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Upload Python Package to PyPI when a Release is Created

on:
release:
types: [created]

jobs:
pypi-publish:
name: Publish release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/robotframework-oxygen
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel
- name: Build package
run: |
python setup.py sdist bdist_wheel # Could also be python -m build
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[metadata]
description-file = README.md
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@
setup(name='robotframework-oxygen',
author='Eficode Oy',
author_email='[email protected]',
download_url = 'https://github.com/kleclerccherwenka/robotframework-oxygen/archive/refs/tags/NestedKeywords.tar.gz',
url='',
license='MIT',
install_requires=[
378 changes: 378 additions & 0 deletions src/oxygen/robot5_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,378 @@
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from robot.result.model import (Keyword as RobotResultKeyword,
TestCase as RobotResultTest,
TestSuite as RobotResultSuite)

from robot.running.model import TestSuite as RobotRunningSuite


class RobotResultInterface(object):
def build_suites(self, starting_time, *suites):
'''Convert a given `suite` dictionaries into a Robot suite'''
finished_suites = []
current_time = starting_time

for suite in suites:
current_time, finished_suite = self.build_suite(current_time, suite)

if finished_suite:
finished_suites.append(finished_suite)

return current_time, finished_suites


def build_suite(self, starting_time, suite):
'''Convert a set of suite dicts into Robot suites and append them to the
given list-like object
target: List-like object to add the Robot suites to
suites: Set of suite dicts to build into Robot suites
Return: The updated list-like object
'''

if not suite:
return starting_time, None

updated_time = starting_time
name = suite.get('name') or 'Unknown Suite Name'
tags = suite.get('tags') or []
setup_keyword = suite.get('setup') or None
teardown_keyword = suite.get('teardown') or None
child_suites = suite.get('suites') or []
tests = suite.get('tests') or []
metadata = suite.get('metadata') or {}

updated_time, robot_setup = self.build_keyword(updated_time, setup_keyword, setup=True)
updated_time, robot_teardown = self.build_keyword(updated_time, teardown_keyword, teardown=True)
updated_time, robot_suites = self.build_suites(updated_time, *child_suites)
updated_time, robot_tests = self.build_tests(updated_time, *tests)

robot_suite = self.spawn_robot_suite(name,
starting_time,
updated_time,
tags,
robot_setup,
robot_teardown,
robot_suites,
robot_tests,
metadata)

return updated_time, robot_suite


def spawn_robot_suite(self,
name,
start_time,
end_time,
tags,
setup_keyword,
teardown_keyword,
suites,
tests,
metadata):
start_timestamp = self.ms_to_timestamp(start_time)
end_timestamp = self.ms_to_timestamp(end_time)

robot_suite = RobotResultSuite(name,
starttime=start_timestamp,
endtime=end_timestamp)
robot_suite.set_tags(add=tags, persist=True)

if setup_keyword:
robot_suite.setup = setup_keyword
if teardown_keyword:
robot_suite.teardown = teardown_keyword

for suite in filter(None, suites):
robot_suite.suites.append(suite)

for test in filter(None, tests):
robot_suite.tests.append(test)

return robot_suite


def build_tests(self, starting_time, *tests):
'''Convert a set of `tests` dicts and add to a Robot suite `target`'''
updated_time = starting_time
robot_tests = []
for test in tests:
updated_time, robot_test = self.build_test(updated_time, test)

if robot_test:
robot_tests.append(robot_test)

return updated_time, robot_tests


def build_test(self, starting_time, test):
'''Convert a set of `tests` dicts and add to a Robot suite `target`'''
if not test:
return starting_time, None

updated_time = starting_time
test_name = test.get('name') or 'Unknown Test Name'
tags = test.get('tags') or []
setup_keyword = test.get('setup') or None
keywords = test.get('keywords') or []
teardown_keyword = test.get('teardown') or None

updated_time, robot_setup = self.build_keyword(updated_time,
setup_keyword,
setup=True)
updated_time, robot_keywords = self.build_keywords(updated_time,
*keywords)
updated_time, robot_teardown = self.build_keyword(updated_time,
teardown_keyword,
teardown=True)

robot_test = self.spawn_robot_test(test_name,
starting_time,
updated_time,
tags,
robot_setup,
robot_teardown,
robot_keywords)

return updated_time, robot_test


def spawn_robot_test(self,
name,
start_time,
end_time,
tags,
setup_keyword,
teardown_keyword,
keywords):
start_timestamp = self.ms_to_timestamp(start_time)
end_timestamp = self.ms_to_timestamp(end_time)
status = self.get_keywords_status(setup_keyword, teardown_keyword, *(keywords or []))

robot_test = RobotResultTest(name,
tags=tags,
status=status,
starttime=start_timestamp,
endtime=end_timestamp)

if setup_keyword:
robot_test.setup = setup_keyword
for keyword in keywords:
if keyword:
robot_test.body.append(keyword)
if teardown_keyword:
robot_test.teardown = teardown_keyword

return robot_test


def build_keywords(self, starting_time, *keywords):
'''Convert `keywords` dicts, add them as sub-keywords to a `target`'''
updated_time = starting_time
robot_keywords = []
for keyword in keywords:
updated_time, robot_keyword = self.build_keyword(updated_time, keyword)

if robot_keyword:
robot_keywords.append(robot_keyword)

return updated_time, robot_keywords


def build_keyword(self, starting_time, keyword, setup=False, teardown=False):
if not keyword:
return starting_time, None

updated_time = starting_time
name = keyword.get('name') or 'Unknown Keyword Name'
status = keyword.get('pass')
elapsed = keyword.get('elapsed') or 0.0
tags = keyword.get('tags') or []
messages = keyword.get('messages') or []
teardown = keyword.get('teardown') or None
keywords = keyword.get('keywords') or []
keywords = tuple(keywords) # required for nesting keywords

updated_time, robot_teardown = self.build_keyword(updated_time, teardown)
updated_time, robot_keywords = self.build_keywords(updated_time, *keywords)

final_time = updated_time + elapsed

robot_keyword = self.spawn_robot_keyword(name,
tags,
status,
updated_time,
final_time,
teardown,
robot_keywords,
messages,
setup,
teardown)

return final_time, robot_keyword


def spawn_robot_keyword(self,
name,
tags,
status,
start_time,
end_time,
teardown_keyword,
keywords,
messages,
setup=False,
teardown=False):
start_timestamp = self.ms_to_timestamp(start_time)
end_timestamp = self.ms_to_timestamp(end_time)

# import this here so RF4 interface stays in parity with RF3
from robot.model import BodyItem
if setup:
keyword_type = BodyItem.SETUP
elif teardown:
keyword_type = BodyItem.TEARDOWN
else:
keyword_type = BodyItem.KEYWORD

if status is None:
keyword_status = 'NOT RUN'
elif status:
keyword_status = 'PASS'
else:
keyword_status = 'FAIL'

robot_keyword = RobotResultKeyword(name,
tags=tags,
status=keyword_status,
starttime=start_timestamp,
endtime=end_timestamp)

robot_keyword.type = keyword_type

for keyword in keywords:
if keyword:
robot_keyword.body.append(keyword)

if teardown_keyword:
robot_keyword.teardown = teardown_keyword

for message in messages:
if message:
ishtml = message.startswith('*HTML*')
if ishtml:
message = message[6:]
robot_keyword.body.create_message(message, html=ishtml)

return robot_keyword


def get_time_format(self):
'''Convenience to return the general Robot timestamp format.'''
return '%Y%m%d %H:%M:%S.%f'


def timestamp_to_ms(self, timestamp):
time_format = self.get_time_format()
time_object = datetime.strptime(
timestamp,
time_format,
)

tz_delta = self.get_timezone_delta()

milliseconds = ((time_object + tz_delta).timestamp() * 1000)

return milliseconds

def ms_to_timestamp(self, milliseconds):
tz_delta = self.get_timezone_delta()

time_object = datetime.fromtimestamp(int(milliseconds / 1000)) - tz_delta
if time_object.year < 1970:
time_object = datetime.fromtimestamp(0)
# fromtimestamp() loses milliseconds, add them back
milliseconds_delta = timedelta(milliseconds=(milliseconds % 1000))
time_object = (time_object + milliseconds_delta)

time_format = self.get_time_format()

return time_object.strftime(time_format)


def get_timezone_delta(self):
local_zone = datetime.now(timezone.utc).astimezone().tzinfo
return local_zone.utcoffset(None)


def get_keywords_status(self, *keywords):
'''
keywords: List of Robot keywords
Return: 'PASS' or 'FAIL'
'''
if sum(not kw.passed for kw in filter(None, keywords)):
return 'FAIL'
else:
return 'PASS'


def create_wrapper_keyword(self,
name,
start_timestamp,
end_timestamp,
setup,
*keywords):
status = self.get_keywords_status(*keywords)
start_time = self.timestamp_to_ms(start_timestamp)
end_time = self.timestamp_to_ms(end_timestamp)

robot_keyword = self.spawn_robot_keyword(name,
[],
status,
start_time,
end_time,
None,
keywords,
[],
setup,
(not setup))

return robot_keyword


class RobotRunningInterface(object):
def build_suite(self, parsed_results):
robot_root_suite = RobotRunningSuite(
parsed_results['name'],
metadata=parsed_results.get('metadata', {})
)
for parsed_suite in parsed_results.get('suites', []):
robot_suite = robot_root_suite.suites.create(
parsed_suite['name'],
metadata=parsed_suite.get('metadata', {})
)
for subsuite in parsed_suite.get('suites', []):
robot_subsuite = self.build_suite(subsuite)
robot_suite.suites.append(robot_subsuite)
self.build_tests(parsed_suite, robot_suite)
self.build_tests(parsed_results, robot_root_suite)
return robot_root_suite

def build_tests(self, oxygen_suite, robot_suite):
for parsed_test in oxygen_suite.get('tests', []):
name = parsed_test['name']
tags = parsed_test.get('tags', [])
kw = parsed_test['keywords'][0]
msg = '\n'.join(kw.get('messages', []))
test_robot_counterpart = robot_suite.tests.create(name, tags=tags)
if kw['pass']:
args = [msg if msg else 'Test passed :D']
test_robot_counterpart.body.create_keyword('Pass execution',
args=args)
else:
args = [msg if msg else 'Test failed D:']
test_robot_counterpart.body.create_keyword('Fail', args=args)
5 changes: 4 additions & 1 deletion src/oxygen/robot_interface.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,10 @@ class RobotInterface(object):
def __init__(self):
major_version = int(robot_version().split('.')[0])

if major_version > 3:
if major_version >= 5:
from .robot5_interface import (RobotResultInterface,
RobotRunningInterface)
elif major_version > 3:
from .robot4_interface import (RobotResultInterface,
RobotRunningInterface)
else: