Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion .github/workflows/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ runs:
uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- name: Install dependencies
shell: ${{ inputs.terminal }}
run: |
Expand Down
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,75 @@ $ python -m oxygen oxygen.gatling path/to/results.log

Then `results_robot_output.xml` will be created under `path/to/`.

## Extending Oxygen: writing your own handler

### [Read the developer guide on how to write your own handler](DEVGUIDE.md)

### Configuring your handler to Oxygen

Oxygen knows about different handlers based on the [`config.yml`](https://github.com/eficode/robotframework-oxygen/blob/master/config.yml) file. This configuration file can be interacted with through Oxygen's command line.

The configuration has the following parts:
```yml
oxygen.junit: # Python module. Oxygen will use this key to try to import the handler
handler: JUnitHandler # Class that Oxygen will initiate after the handler is imported
keyword: run_junit # Keyword that should be used to run the other test tool
tags: # List of tags that by default should be added to the test cases converted with this handler
- oxygen-junit
oxygen.zap:
handler: ZAProxyHandler
keyword: run_zap
tags: oxygen-zap
accepted_risk_level: 2 # Handlers can have their own command line arguments
required_confidence_level: 1 # See [the development guide](DEVGUIDE.md) for more information
```

#### `--add-config`

This argument is used to add new handler configuration to Oxygen:

```bash
$ python -m oxygen --add-config path/to/your_handler_config.yml
```

This file is read and appended to the Oxygen's `config.yml`. Based on the key, Oxygen will try to import you handler.

### `--reset-config`

This argument is used to return Oxygen's `config.yml` back to the state it was when the tool was installed:

```bash
$ python -m oxygen --reset-config
```

The command **does not** verify the operation from the user, so be careful.

### `--print-config`

This argument prints the current configuration of Oxygen:
```bash
$ python -m oxygen --print-config
Using config file: /path/to/oxygen/src/oxygen/config.yml
oxygen.gatling:
handler: GatlingHandler
keyword: run_gatling
tags: oxygen-gatling
oxygen.junit:
handler: JUnitHandler
keyword: run_junit
tags:
- oxygen-junit
oxygen.zap:
accepted_risk_level: 2
handler: ZAProxyHandler
keyword: run_zap
required_confidence_level: 1
tags: oxygen-zap

$
```
Because you can add the configuration to the same handler multiple times, note that only the last entry is in effect.

# Developing Oxygen

Clone the Oxygen repository to the environment where you want to the run the tool.
Expand All @@ -107,7 +176,6 @@ $ invoke --list

and the task file [`tasks.py`](https://github.com/eficode/robotframework-oxygen/blob/master/tasks.py).

[Read the developer guide on how to write your own handler](DEVGUIDE.md)

# License

Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ mock>=2.0.0
invoke>=1.1.1
coverage>=5.1
testfixtures>=6.14.1 # needed for large dict comparisons to make sense of them
green>=3.1.3 # unit test runner
pytest>=7.4.2
pytest-cov>=4.1.0
docutils>=0.16 # needed to generate library documentation with libdoc
Pygments>=2.6.1 # this one too
twine>=3.1.1 # needed for releasing to pypi
Expand Down
4 changes: 2 additions & 2 deletions src/oxygen/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .oxygen import OxygenCLI
from .oxygen import OxygenCLI, main

if __name__ == '__main__':
OxygenCLI().run()
main()
5 changes: 3 additions & 2 deletions src/oxygen/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from os.path import abspath, dirname, join
from pathlib import Path

CONFIG_FILE = join(abspath(dirname(__file__)), 'config.yml')
CONFIG_FILE = Path(__file__).resolve().parent / 'config.yml'
ORIGINAL_CONFIG_FILE = Path(__file__).resolve().parent / 'config_original.yml'
1 change: 1 addition & 0 deletions src/oxygen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ oxygen.zap:
tags: oxygen-zap
accepted_risk_level: 2
required_confidence_level: 1

16 changes: 16 additions & 0 deletions src/oxygen/config_original.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
oxygen.junit:
handler: JUnitHandler
keyword: run_junit
tags:
- oxygen-junit
oxygen.gatling:
handler: GatlingHandler
keyword: run_gatling
tags: oxygen-gatling
oxygen.zap:
handler: ZAProxyHandler
keyword: run_zap
tags: oxygen-zap
accepted_risk_level: 2
required_confidence_level: 1

4 changes: 4 additions & 0 deletions src/oxygen/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ class ResultFileIsNotAFileException(Exception):

class MismatchArgumentException(Exception):
pass


class InvalidConfigurationException(Exception):
pass
147 changes: 121 additions & 26 deletions src/oxygen/oxygen.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import sys

from argparse import ArgumentParser
from datetime import datetime, timedelta
from inspect import getdoc, signature
from io import StringIO
from pathlib import Path
from shutil import copy as copy_file
from traceback import format_exception

from robot.api import ExecutionResult, ResultVisitor, ResultWriter
from robot.libraries.BuiltIn import BuiltIn
from robot.errors import DataError
from yaml import load, FullLoader
from yaml import load, FullLoader, dump as dump_yaml

from .config import CONFIG_FILE
from .errors import OxygenException
from .config import CONFIG_FILE, ORIGINAL_CONFIG_FILE
from .errors import (OxygenException,
InvalidConfigurationException,
ResultFileNotFoundException)
from .robot_interface import RobotInterface
from .version import VERSION

Expand All @@ -25,17 +29,35 @@ class OxygenCore(object):


def __init__(self):
with open(CONFIG_FILE, 'r') as infile:
self._config = None
self._handlers = None

@property
def config(self):
if self._config is None:
self.load_config(CONFIG_FILE)
return self._config

def load_config(self, config_file):
with open(config_file, 'r') as infile:
self._config = load(infile, Loader=FullLoader)
self._handlers = {}
self._register_handlers()

@property
def handlers(self):
if self._handlers is None:
self._handlers = {}
self._register_handlers()
return self._handlers

def _register_handlers(self):
for tool_name, config in self._config.items():
handler_class = getattr(__import__(tool_name,
fromlist=[config['handler']]),
config['handler'])
handler = handler_class(config)
for tool_name, handler_config in self.config.items():
try:
handler_class = getattr(
__import__(tool_name, fromlist=[handler_config['handler']]),
handler_config['handler'])
except ModuleNotFoundError as e:
raise InvalidConfigurationException(e)
handler = handler_class(handler_config)
self._handlers[tool_name] = handler


Expand All @@ -53,7 +75,7 @@ def __init__(self, data):

def visit_test(self, test):
failures = []
for handler_type, handler in self._handlers.items():
for handler_type, handler in self.handlers.items():
try:
handler.check_for_keyword(test, self.data)
except Exception as e:
Expand Down Expand Up @@ -182,12 +204,12 @@ def __init__(self):
def _fetch_handler(self, name):
try:
return next(filter(lambda h: h.keyword == name,
self._handlers.values()))
self.handlers.values()))
except StopIteration:
raise OxygenException('No handler for keyword "{}"'.format(name))

def get_keyword_names(self):
return list(handler.keyword for handler in self._handlers.values())
return list(handler.keyword for handler in self.handlers.values())

def run_keyword(self, name, args, kwargs):
handler = self._fetch_handler(name)
Expand All @@ -210,31 +232,72 @@ class OxygenCLI(OxygenCore):
OxygenCLI is a command line interface to transform one test result file to
corresponding Robot Framework output.xml
'''
def parse_args(self, parser):
MAIN_LEVEL_CLI_ARGS = {
# we intentionally define `dest` here so we can filter arguments later
'--version': {'action': 'version',
'dest': 'version'},
'--add-config': {'type': Path,
'metavar': 'FILE',
'dest': 'add_config',
'help': ('path to YAML file whose content is '
'appended to existing Oxygen handler '
'configuration')},
'--reset-config': {'action': 'store_true',
'dest': 'reset_config',
'help': ('resets the Oxygen handler '
'configuration to a pristine, '
'as-freshly-installed version')},
'--print-config': {'action': 'store_true',
'dest': 'print_config',
'help': ('prints current Oxygen handler '
'configuration')}
}
def add_arguments(self, parser):
# Add version number here to the arguments as it depends on OxygenCLI
# being initiated already
self.MAIN_LEVEL_CLI_ARGS['--version']['version'] = \
f'%(prog)s {self.__version__}'
for flag, params in self.MAIN_LEVEL_CLI_ARGS.items():
parser.add_argument(flag, **params)

subcommands = parser.add_subparsers()
for tool_name, tool_handler in self._handlers.items():
for tool_name, tool_handler in self.handlers.items():
subcommand_parser = subcommands.add_parser(tool_name)
for flags, params in tool_handler.cli().items():
subcommand_parser.add_argument(*flags, **params)
subcommand_parser.set_defaults(func=tool_handler.parse_results)

def parse_args(self, parser):
return vars(parser.parse_args()) # returns a dictionary

def get_output_filename(self, result_file):
if result_file is None:
raise ResultFileNotFoundException('You did not give any result '
'file to convert')
filename = Path(result_file)
filename = filename.with_suffix('.xml')
robot_name = filename.stem + '_robot_output' + filename.suffix
filename = filename.with_name(robot_name)
return str(filename)

def run(self):
parser = ArgumentParser(prog='oxygen')
parser.add_argument('--version',
action='version',
version=f'%(prog)s {self.__version__}')
args = self.parse_args(parser)
if not args:
parser.error('No arguments given')
output_filename = self.get_output_filename(args['result_file'])
def append_config(self, new_config_path):
with open(new_config_path, 'r') as new_config:
with open(CONFIG_FILE, 'a') as old_config:
old_config.write(new_config.read())
self.load_config(CONFIG_FILE)

@staticmethod
def reset_config():
copy_file(ORIGINAL_CONFIG_FILE, CONFIG_FILE)
OxygenCLI().load_config(CONFIG_FILE)
print('Oxygen handler configuration reset!')

def print_config(self):
print(f'Using config file: {CONFIG_FILE}')
print(dump_yaml(self.config))

def convert_to_robot_result(self, args):
output_filename = self.get_output_filename(args.get('result_file'))
parsed_results = args['func'](
**{k: v for (k, v) in args.items() if not callable(v)})
robot_suite = RobotInterface().running.build_suite(parsed_results)
Expand All @@ -243,6 +306,38 @@ def run(self):
report=None,
stdout=StringIO())

def run(self):
parser = ArgumentParser(prog='oxygen')
self.add_arguments(parser)
args = self.parse_args(parser)
match args:
case {'add_config': new_config_path} if new_config_path is not None:
return self.append_config(new_config_path)
case {'print_config': should_print} if should_print:
return self.print_config()
case {'add_config': _,
'reset_config': _,
'print_config': _,
**rest} if not rest: # user is not trying to invoke main-level arguments, but do not provide other arguments either
parser.error('No arguments given')
case _:
# filter out arguments meant for other cases so that downstream
# handler does not need to know about them
filter_list = [v['dest'] for v in
self.MAIN_LEVEL_CLI_ARGS.values()]
filtered_args = {k: v for k, v in args.items()
if k not in filter_list}
return self.convert_to_robot_result(filtered_args)

def main():
'''Main CLI entrypoint

Also used in __main__.py
'''
if '--reset-config' in sys.argv:
OxygenCLI.reset_config()
sys.exit(0)
OxygenCLI().run()

if __name__ == '__main__':
OxygenCLI().run()
main()
7 changes: 3 additions & 4 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,16 @@ def install(context, package=None):
@task(iterable=['test'],
help={
'test': 'Limit unit test execution to specific tests. Must be given '
'multiple times to select several targets. See more: '
'https://github.com/CleanCut/green/blob/master/cli-options.txt#L5',
'multiple times to select several targets.'
})
def utest(context, test=None):
run(f'green {" ".join(test) if test else UNIT_TESTS}',
run(f'pytest {" ".join(test) if test else UNIT_TESTS} -q --disable-warnings',
env={'PYTHONPATH': str(SRCPATH)},
pty=(not system() == 'Windows'))

@task
def coverage(context):
run(f'green -r {str(UNIT_TESTS)}',
run(f'pytest --cov {UNIT_TESTS}',
env={'PYTHONPATH': str(SRCPATH)},
pty=(not system() == 'Windows'))
run('coverage html')
Expand Down
4 changes: 2 additions & 2 deletions tests/atest/oxygen_junit_tests.robot
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Oxygen's unit tests should pass
[Tags] oxygen-own-junit
Remove file ${JUNIT XML FILE}
File should not exist ${JUNIT XML FILE}
${green}= Get command green
${pytest}= Get command pytest
Run JUnit ${JUNIT XML FILE}
... ${green} -j ${JUNIT XML FILE} ${EXECDIR}
... ${pytest} --junit-xml\=${JUNIT XML FILE} ${EXECDIR}
File should exist ${JUNIT XML FILE}

*** Keywords ***
Expand Down
Loading