├── .github └── workflows │ ├── run-tests │ └── action.yml │ └── test.yml ├── .gitignore ├── DEVGUIDE.md ├── LICENSE ├── README.md ├── docs ├── OxygenLibrary-0.1.html ├── OxygenLibrary-0.2.html ├── OxygenLibrary-0.3.html └── index.html ├── example ├── __init__.py ├── my_parser.py └── my_parser_tests.robot ├── handler_result_specification.md ├── requirements.txt ├── setup.py ├── src └── oxygen │ ├── __init__.py │ ├── __main__.py │ ├── base_handler.py │ ├── config.py │ ├── config.yml │ ├── config_original.yml │ ├── errors.py │ ├── gatling.py │ ├── junit.py │ ├── oxygen.py │ ├── oxygen_handler_result.py │ ├── robot3_interface.py │ ├── robot4_interface.py │ ├── robot_interface.py │ ├── utils.py │ ├── version.py │ └── zap.py ├── tasks.py ├── tests ├── __init__.py ├── atest │ ├── metadata.robot │ ├── oxygen_junit_tests.robot │ ├── test.robot │ ├── test_with_import.robot │ └── test_without_import.robot ├── resources │ ├── big.xml │ ├── example_robot_output.xml │ ├── gatling-example-simulation.log │ ├── green-junit-example.xml │ ├── green-junit-expected-robot-output.xml │ ├── junit-single-testsuite.xml │ ├── junit.xml │ ├── my_dummy_handlers │ │ ├── __init__.py │ │ ├── dummy_handler_default_params.py │ │ ├── dummy_handler_metadata.py │ │ ├── dummy_handler_multiple_args.py │ │ ├── dummy_handler_multiple_args_too_few.py │ │ └── dummy_handler_single_arg.py │ └── zap │ │ ├── zap.json │ │ ├── zap.md │ │ ├── zap.xml │ │ ├── zap.xml.lol │ │ └── zap_pp.json └── utest │ ├── __init__.py │ ├── base_handler │ ├── __init__.py │ ├── test_errors.py │ ├── test_inject_suite_report.py │ ├── test_normalize_keyword_name.py │ └── test_set_suite_tags.py │ ├── gatling │ ├── __init__.py │ └── test_basic_functionality.py │ ├── helpers.py │ ├── junit │ ├── __init__.py │ └── test_basic_functionality.py │ ├── my_dummy_handler │ ├── __init__.py │ └── test_basic_functionality.py │ ├── oxygen │ ├── __init__.py │ ├── test_oxygen_cli.py │ ├── test_oxygen_config_file.py │ ├── test_oxygen_core.py │ ├── test_oxygen_listener.py │ ├── test_oxygen_visitor.py │ └── test_oxygenlibrary.py │ ├── oxygen_handler_result │ ├── __init__.py │ ├── shared_tests.py │ ├── test_OxygenKeywordDict.py │ ├── test_OxygenSuiteDict.py │ ├── test_OxygenTestCaseDict.py │ └── test_deprecation_warning.py │ ├── robot_interface │ ├── __init__.py │ ├── test_robot_interface_basic_usage.py │ └── test_time_conversions.py │ └── zap │ ├── __init__.py │ ├── test_basic_functionality.py │ ├── test_parse_zap_alert_dict.py │ ├── test_parse_zap_dict.py │ ├── test_parse_zap_instance.py │ ├── test_parse_zap_site_dict.py │ ├── test_xml_to_dict.py │ └── test_zap_cli.py └── tox.ini /.github/workflows/run-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Run tests' 2 | inputs: 3 | python-version: 4 | required: true 5 | rf-version: 6 | required: true 7 | terminal: 8 | required: true 9 | runs: 10 | using: "composite" 11 | steps: 12 | - name: Install Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: ${{ inputs.python-version }} 16 | - name: Install dependencies 17 | shell: ${{ inputs.terminal }} 18 | run: | 19 | pip install -r requirements.txt 20 | pip install robotframework==${{ inputs.rf-version }} 21 | - name: Run tests 22 | shell: ${{ inputs.terminal }} 23 | run: | 24 | invoke test 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | 11 | generate-matrix: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | python-versions: ${{ steps.generate-matrix.outputs.PYTHONS }} 15 | rf-versions: ${{ steps.generate-matrix.outputs.RF_VERSIONS }} 16 | steps: 17 | - name: "Generate Matrix" 18 | id: generate-matrix 19 | run: | 20 | echo 'PYTHONS=["3.10.11", "3.11.6", "3.12.0"]' >> $GITHUB_OUTPUT 21 | echo 'RF_VERSIONS=["3.2.2", "4.1.3", "5.0.1", "6.1.1"]' >> $GITHUB_OUTPUT 22 | 23 | 24 | windows: 25 | runs-on: windows-latest 26 | needs: 27 | - generate-matrix 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | python: ${{ fromJSON(needs.generate-matrix.outputs.python-versions) }} 32 | rf-version: ${{ fromJSON(needs.generate-matrix.outputs.rf-versions) }} 33 | name: Windows (python-${{ matrix.python }}, robotframework-${{ matrix.rf-version }}) 34 | steps: 35 | - name: Checkout the repository 36 | uses: actions/checkout@v4 37 | - name: Run tests 38 | uses: ./.github/workflows/run-tests 39 | with: 40 | python-version: ${{ matrix.python }} 41 | rf-version: ${{ matrix.rf-version }} 42 | terminal: "pwsh" 43 | 44 | linux: 45 | runs-on: ubuntu-latest 46 | needs: 47 | - generate-matrix 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | python: ${{ fromJSON(needs.generate-matrix.outputs.python-versions) }} 52 | rf-version: ${{ fromJSON(needs.generate-matrix.outputs.rf-versions) }} 53 | name: Linux (python-${{ matrix.python }}, robotframework-${{ matrix.rf-version }}) 54 | steps: 55 | - name: Checkout the repository 56 | uses: actions/checkout@v4 57 | - name: Run tests 58 | uses: ./.github/workflows/run-tests 59 | with: 60 | python-version: ${{ matrix.python }} 61 | rf-version: ${{ matrix.rf-version }} 62 | terminal: "bash" 63 | macos: 64 | runs-on: macos-latest 65 | needs: 66 | - generate-matrix 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | python: ${{ fromJSON(needs.generate-matrix.outputs.python-versions) }} 71 | rf-version: ${{ fromJSON(needs.generate-matrix.outputs.rf-versions) }} 72 | name: MacOS (python-${{ matrix.python }}, robotframework-${{ matrix.rf-version }}) 73 | steps: 74 | - name: Checkout the repository 75 | uses: actions/checkout@v4 76 | - name: Run tests 77 | uses: ./.github/workflows/run-tests 78 | with: 79 | python-version: ${{ matrix.python }} 80 | rf-version: ${{ matrix.rf-version }} 81 | terminal: "bash" 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.bak 3 | report.html 4 | log.html 5 | output.xml 6 | *.pyc 7 | .python-version 8 | .tox 9 | *.egg-info 10 | build 11 | dist 12 | .idea 13 | venv 14 | .coverage 15 | htmlcov 16 | green-junit.xml 17 | tests/resources/green-junit-example_robot_output.xml 18 | example/results_robot_output.xml 19 | example/results.json 20 | .vscode 21 | /result 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Eficode Oy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oxygen 2 | 3 | Oxygen is a [Robot Framework](https://robotframework.org/) tool that empowers the user to convert the results of any testing tool or framework to [Robot Framework's reporting](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#screenshots). This consolidates all test reporting together regardless of tools used. 4 | 5 | Oxygen has built-in support for three testing frameworks: [JUnit](https://junit.org/junit5/), [Gatling](https://gatling.io/), and [Zed Attack Proxy (ZAP)](https://www.zaproxy.org/). 6 | 7 | Oxygen is designed to be extensible. Users can create their own *handlers* for other testing framework or tools to transform their reporting into the Robot Framework's `log.html` and `report.html`. 8 | 9 | # Table of Contents 10 | 1. [Installation](#installation) 11 | 1. [Keyword documentation](#keyword-documentation) 12 | 1. [Usage](#usage) 13 | 1. [Developing Oxygen](#developing-oxygen) 14 | 1. [License](#license) 15 | 1. [Acknowledgements](#acknowledgments) 16 | 17 | # Installation 18 | 19 | To install Oxygen, run the following: 20 | ``` 21 | $ pip install robotframework-oxygen 22 | ``` 23 | 24 | ## Pre-requisites 25 | 26 | - Oxygen is supported on Windows, Linux and MacOS 27 | - [Python 3.10](http://python.org) or above 28 | - [pip](https://pypi.python.org/pypi/pip) for easy installation 29 | - [Robot Framework](http://robotframework.org) 30 | - [additional dependencies](requirements.txt) 31 | 32 | To check the Python version on the command line, run: 33 | ``` 34 | $ python --version 35 | ``` 36 | 37 | # Keyword documentation 38 | 39 | [Keyword Documentation](http://eficode.github.io/robotframework-oxygen/) 40 | 41 | # Usage 42 | 43 | ## Example: Robot Framework running other test tools 44 | 45 | Main usage scenario for Oxygen is the ability to write acceptance test cases that run your tests in other test tools and integrate the resulting test report as part of Robot Framework's. This means you are able to run all of your testing from Robot Framework and thus having all test reporting consolidated together. 46 | 47 | After installing Oxygen, it can be used in the Robot Framework suite to write test cases. For example, to build acceptance tests that run different sets of JUnit tests: 48 | 49 | ``` RobotFramework 50 | *** Settings *** 51 | Library oxygen.OxygenLibrary 52 | 53 | *** Test cases *** 54 | 55 | JUnit unit tests should pass 56 | [Tags] testset-1 57 | Run JUnit path/to/mydir/results.xml java -jar junit.jar --reports-dir=path/to/mydir 58 | 59 | JUnit integration tests should pass 60 | [Tags] testset-2 61 | Run JUnit path/to/anotherdir/results.xml java -jar junit.jar --reports-dir=path/to/anotherdir 62 | ``` 63 | 64 | Then, run the suite by providing Oxygen as [a listener](http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#listener-interface): 65 | 66 | ``` 67 | $ robot --listener oxygen.listener my_tests.robot 68 | ``` 69 | 70 | Opening the Robot Framework `log.html` and `report.html`, you should see that test case `JUnit unt tests should pass` has been replaced by Oxygen with test cases matching with what is in the `path/to/mydir/results.xml` JUnit report file. Similarly, test case `JUnit integration tests should pass` has been replaced with results from `path/to/anotherdir/results.xml`; each JUnit test case with its relevant information has a counterpart in the `log.html`. Each JUnit test case is also tagged with the tags from the original Robot Framework test case. 71 | 72 | The example above, for the brevity, shows incomplete commands to run JUnit tool from command line. Please refer to [keyword documentation](#keyword-documentation) for more detailed documentation about keyword's arguments, as well as documentation for [Gatling](https://gatling.io/) and [ZAP](https://www.zaproxy.org/) related keywords. And, of course, refer to the particular tool documentation as well. 73 | 74 | ## Using from command line 75 | 76 | In case where you want to run your other testing tools separately, but yet combine results into unified Robot Framework `log.html` and `report.html`, you can use Oxygen's command line interface to convert single result file to single corresponding Robot Framework `output.xml`: 77 | 78 | ``` 79 | $ python -m oxygen oxygen.junit my_junit_results.xml 80 | ``` 81 | 82 | As a convention, the resulting Robot Framework xml file will be named by adding a suffix to the end. In the example above, the resulting Robot Framework xml file would be named `my_junit_results_robot_output.xml`. 83 | 84 | **Note** that resulting xml file will also be created at the same location as the original result file. Therefore, when original result files are in another directory: 85 | 86 | ``` 87 | $ python -m oxygen oxygen.gatling path/to/results.log 88 | ``` 89 | 90 | Then `results_robot_output.xml` will be created under `path/to/`. 91 | 92 | ## Extending Oxygen: writing your own handler 93 | 94 | ### [Read the developer guide on how to write your own handler](DEVGUIDE.md) 95 | 96 | You might also want to look at [specification for handler results](handler_result_specification.md) 97 | 98 | ### Configuring your handler to Oxygen 99 | 100 | 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. 101 | 102 | The configuration has the following parts: 103 | ```yml 104 | oxygen.junit: # Python module. Oxygen will use this key to try to import the handler 105 | handler: JUnitHandler # Class that Oxygen will initiate after the handler is imported 106 | keyword: run_junit # Keyword that should be used to run the other test tool 107 | tags: # List of tags that by default should be added to the test cases converted with this handler 108 | - oxygen-junit 109 | oxygen.zap: 110 | handler: ZAProxyHandler 111 | keyword: run_zap 112 | tags: oxygen-zap 113 | accepted_risk_level: 2 # Handlers can have their own command line arguments 114 | required_confidence_level: 1 # See https://github.com/eficode/robotframework-oxygen/blob/master/DEVGUIDE.md for more information 115 | ``` 116 | 117 | #### `--add-config` 118 | 119 | This argument is used to add new handler configuration to Oxygen: 120 | 121 | ```bash 122 | $ python -m oxygen --add-config path/to/your_handler_config.yml 123 | ``` 124 | 125 | This file is read and appended to the Oxygen's `config.yml`. Based on the key, Oxygen will try to import you handler. 126 | 127 | ### `--reset-config` 128 | 129 | This argument is used to return Oxygen's `config.yml` back to the state it was when the tool was installed: 130 | 131 | ```bash 132 | $ python -m oxygen --reset-config 133 | ``` 134 | 135 | The command **does not** verify the operation from the user, so be careful. 136 | 137 | ### `--print-config` 138 | 139 | This argument prints the current configuration of Oxygen: 140 | ```bash 141 | $ python -m oxygen --print-config 142 | Using config file: /path/to/oxygen/src/oxygen/config.yml 143 | oxygen.gatling: 144 | handler: GatlingHandler 145 | keyword: run_gatling 146 | tags: oxygen-gatling 147 | oxygen.junit: 148 | handler: JUnitHandler 149 | keyword: run_junit 150 | tags: 151 | - oxygen-junit 152 | oxygen.zap: 153 | accepted_risk_level: 2 154 | handler: ZAProxyHandler 155 | keyword: run_zap 156 | required_confidence_level: 1 157 | tags: oxygen-zap 158 | 159 | $ 160 | ``` 161 | Because you can add the configuration to the same handler multiple times, note that only the last entry is in effect. 162 | 163 | ## `utils` module 164 | 165 | In [utils module](https://github.com/eficode/robotframework-oxygen/blob/master/src/oxygen/utils.py), you will find assortment of functionalities that you might want to leverage when writing your own handler. 166 | 167 | ### `run_command_line()` 168 | 169 | Most of the time, handlers want to run the other test tool through command line. For this, `utils` provides `run_command_line()` that wraps Python's [`subprocess`](https://docs.python.org/3/library/subprocess.html) module for more easier to use when writing your handler. 170 | 171 | `run_command_line()` takes following arguments: 172 | - `cmd`: the command to be executed in a subprocess 173 | - `check_return_code`: if set to `True`, will raise an exception if the `cmd` fails in the subprocess. **Note** that this fails the keyword and, thus, the execution of the test case is stopped. If you want to enable test case to continue even after `run_command_line()` has failed, you should disable it by setting `False`. It is often a good idea to allow user using your handler's keyword to decide how they want the command line execution to affect the test case 174 | - `env`: a dictionary of environment variables that should be passed to the subprocess. By default, `run_command_line()` inherits the environment from the current Python process as well as from modifications done by the Robot Framework command line arguments (ie. `--pythonpath`) 175 | 176 | # Developing Oxygen 177 | 178 | Clone the Oxygen repository to the environment where you want to the run the tool. 179 | 180 | Oxygen requires a set of dependencies to be installed. Dependencies are listed in the `requirements.txt` file: 181 | ``` 182 | $ pip install -r requirements.txt 183 | ``` 184 | 185 | Oxygen uses task runner tool [`invoke`](http://www.pyinvoke.org/) to run tests, build the project, etc. 186 | 187 | Please refer to the available tasks for the project: 188 | ``` 189 | $ invoke --list 190 | ``` 191 | 192 | and the task file [`tasks.py`](https://github.com/eficode/robotframework-oxygen/blob/master/tasks.py). 193 | 194 | 195 | # License 196 | 197 | Details of project licensing can be found in the [LICENSE](LICENSE) file in the project repository. 198 | 199 | # Acknowledgments 200 | 201 | Oxygen tool was developed by Eficode Oy as part of [Testomat project](https://www.testomatproject.eu/) with funding by [Business Finland](https://www.businessfinland.fi/). 202 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/example/__init__.py -------------------------------------------------------------------------------- /example/my_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from oxygen import BaseHandler 4 | 5 | 6 | class MyParser(BaseHandler): 7 | ''' 8 | MyParser is an example how to extend Oxygen by writing your own handler 9 | ''' 10 | RESULTS = {'tests': [ 11 | {'name': 'My test 1', 'passed': True, 'msg': ''}, 12 | {'name': 'My Test 2', 'passed': False, 'msg': 'Error text D:'}] 13 | } 14 | 15 | def run_my_tests(self, result_file): 16 | with open(result_file, 'w') as f: 17 | json.dump(self.RESULTS, f) 18 | return result_file 19 | 20 | def parse_results(self, result_file): 21 | with open(result_file, 'r') as f: 22 | results = json.load(f) 23 | return { 24 | 'name': result_file, 25 | 'tags': [], 26 | 'setup': None, 27 | 'teardown': None, 28 | 'suites': [], 29 | 'tests': [{ 30 | 'name': test['name'], 31 | 'tags': [], 32 | 'setup': None, 33 | 'teardown': None, 34 | 'keywords': [{ 35 | 'name': test['name'] + ' result', 36 | 'pass': test['passed'], 37 | 'elapsed': 0.0, 38 | 'tags': [], 39 | 'messages': [test['msg']], 40 | 'teardown': None, 41 | 'keywords': [] 42 | }] 43 | } for test in results['tests']] 44 | } 45 | -------------------------------------------------------------------------------- /example/my_parser_tests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library oxygen.OxygenLibrary 3 | 4 | *** Test Cases *** 5 | My parser's tests should succeed 6 | Run my tests ${CURDIR}/results.json 7 | -------------------------------------------------------------------------------- /handler_result_specification.md: -------------------------------------------------------------------------------- 1 | # Oxygen handler result specification 2 | ``` 3 | { 4 | "$defs": { 5 | "OxygenKeywordDict": { 6 | "properties": { 7 | "pass": { 8 | "title": "Pass", 9 | "type": "boolean" 10 | }, 11 | "name": { 12 | "title": "Name", 13 | "type": "string" 14 | }, 15 | "elapsed": { 16 | "title": "Elapsed", 17 | "type": "number" 18 | }, 19 | "tags": { 20 | "items": { 21 | "type": "string" 22 | }, 23 | "title": "Tags", 24 | "type": "array" 25 | }, 26 | "messages": { 27 | "items": { 28 | "type": "string" 29 | }, 30 | "title": "Messages", 31 | "type": "array" 32 | }, 33 | "teardown": { 34 | "$ref": "#/$defs/OxygenKeywordDict" 35 | }, 36 | "keywords": { 37 | "items": { 38 | "$ref": "#/$defs/OxygenKeywordDict" 39 | }, 40 | "title": "Keywords", 41 | "type": "array" 42 | } 43 | }, 44 | "required": [ 45 | "pass", 46 | "name" 47 | ], 48 | "title": "OxygenKeywordDict", 49 | "type": "object" 50 | }, 51 | "OxygenSuiteDict": { 52 | "properties": { 53 | "name": { 54 | "title": "Name", 55 | "type": "string" 56 | }, 57 | "tags": { 58 | "items": { 59 | "type": "string" 60 | }, 61 | "title": "Tags", 62 | "type": "array" 63 | }, 64 | "setup": { 65 | "$ref": "#/$defs/OxygenKeywordDict" 66 | }, 67 | "teardown": { 68 | "$ref": "#/$defs/OxygenKeywordDict" 69 | }, 70 | "metadata": { 71 | "additionalProperties": { 72 | "type": "string" 73 | }, 74 | "title": "Metadata", 75 | "type": "object" 76 | }, 77 | "suites": { 78 | "items": { 79 | "$ref": "#/$defs/OxygenSuiteDict" 80 | }, 81 | "title": "Suites", 82 | "type": "array" 83 | }, 84 | "tests": { 85 | "items": { 86 | "$ref": "#/$defs/OxygenTestCaseDict" 87 | }, 88 | "title": "Tests", 89 | "type": "array" 90 | } 91 | }, 92 | "required": [ 93 | "name" 94 | ], 95 | "title": "OxygenSuiteDict", 96 | "type": "object" 97 | }, 98 | "OxygenTestCaseDict": { 99 | "properties": { 100 | "name": { 101 | "title": "Name", 102 | "type": "string" 103 | }, 104 | "keywords": { 105 | "items": { 106 | "$ref": "#/$defs/OxygenKeywordDict" 107 | }, 108 | "title": "Keywords", 109 | "type": "array" 110 | }, 111 | "tags": { 112 | "items": { 113 | "type": "string" 114 | }, 115 | "title": "Tags", 116 | "type": "array" 117 | }, 118 | "setup": { 119 | "$ref": "#/$defs/OxygenKeywordDict" 120 | }, 121 | "teardown": { 122 | "$ref": "#/$defs/OxygenKeywordDict" 123 | } 124 | }, 125 | "required": [ 126 | "name", 127 | "keywords" 128 | ], 129 | "title": "OxygenTestCaseDict", 130 | "type": "object" 131 | } 132 | }, 133 | "allOf": [ 134 | { 135 | "$ref": "#/$defs/OxygenSuiteDict" 136 | } 137 | ] 138 | } 139 | ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework>=3.0.4 2 | junitparser==2.0 3 | PyYAML>=3.13 4 | pydantic>=2.4.2 5 | 6 | ### Dev 7 | mock>=2.0.0 8 | invoke>=1.1.1 9 | coverage>=5.1 10 | testfixtures>=6.14.1 # needed for large dict comparisons to make sense of them 11 | pytest>=7.4.2 12 | pytest-cov>=4.1.0 13 | docutils>=0.16 # needed to generate library documentation with libdoc 14 | Pygments>=2.6.1 # this one too 15 | twine>=3.1.1 # needed for releasing to pypi 16 | build>=0.6.0 # needed for building the distribution 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from os.path import abspath, dirname, join as path_join 3 | from setuptools import find_packages, setup 4 | 5 | CURDIR = abspath(dirname(__file__)) 6 | SRC = path_join(CURDIR, 'src') 7 | 8 | with open(path_join(SRC, 'oxygen', 'version.py')) as f: 9 | exec(f.read()) 10 | 11 | KEYWORDS = ('robotframework testing testautomation acceptancetesting atdd bdd' 12 | 'reporting testreporting') 13 | 14 | SHORT_DESC = ('Oxygen is an extensible tool for Robot Framework that ' 15 | 'enables you to integrate running other testing tools and their ' 16 | 'reports as part of Robot Framework\'s reporting.') 17 | 18 | with open(path_join(CURDIR, 'README.md'), 'r') as readme: 19 | LONG_DESCRIPTION = readme.read() 20 | 21 | CLASSIFIERS = ''' 22 | Development Status :: 5 - Production/Stable 23 | Programming Language :: Python :: 3 :: Only 24 | Operating System :: OS Independent 25 | Topic :: Software Development :: Testing 26 | License :: OSI Approved :: MIT License 27 | '''.strip().splitlines() 28 | 29 | setup(name='robotframework-oxygen', 30 | author='Eficode Oy', 31 | author_email='info@eficode.com', 32 | url='', 33 | license='MIT', 34 | install_requires=[ 35 | 'robotframework>=3.0.4', 36 | 'junitparser==2.0', 37 | 'PyYAML>=3.13', 38 | 'pydantic>=2.4.2' 39 | ], 40 | packages=find_packages(SRC), 41 | package_dir={'': 'src'}, 42 | package_data={'oxygen': ['*.yml']}, 43 | keywords=KEYWORDS, 44 | classifiers=CLASSIFIERS, 45 | version=VERSION, 46 | description=SHORT_DESC, 47 | long_description=LONG_DESCRIPTION, 48 | long_description_content_type="text/markdown") 49 | -------------------------------------------------------------------------------- /src/oxygen/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import VERSION 2 | from .base_handler import BaseHandler 3 | from .oxygen import listener, OxygenLibrary 4 | 5 | __all__ = ['BaseHandler', 'listener', 'OxygenLibrary'] 6 | __version__ = VERSION 7 | -------------------------------------------------------------------------------- /src/oxygen/__main__.py: -------------------------------------------------------------------------------- 1 | from .oxygen import OxygenCLI, main 2 | 3 | if __name__ == '__main__': 4 | main() 5 | -------------------------------------------------------------------------------- /src/oxygen/base_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from inspect import signature, Parameter 4 | 5 | from .errors import MismatchArgumentException 6 | from .robot_interface import (RobotInterface, get_keywords_from, 7 | set_special_keyword) 8 | from .utils import validate_with_deprecation_warning 9 | 10 | class BaseHandler(object): 11 | DEFAULT_CLI = {tuple(['result_file']): {}} 12 | 13 | def __init__(self, config): 14 | ''' 15 | Set up the handler with the given configuration 16 | 17 | config: A dict including the 'keyword' node 18 | ''' 19 | self._interface = RobotInterface() 20 | self._config = config 21 | 22 | tags = self._config.get('tags', []) 23 | if not isinstance(tags, list): 24 | tags = [tags] 25 | self._tags = tags 26 | self.keyword = self._normalize_keyword_name(self._config['keyword']) 27 | self.run_time_data = None 28 | 29 | def cli(self): 30 | ''' 31 | augment in subclasses 32 | 33 | def cli(self): 34 | cli_interface = self.DEFAULT_CLI.copy() 35 | cli_interface[('-e', '--example')] = {'help': 'use this like that'} 36 | cli_interface[('-f', '--flag')] = {'action': 'store_true'} 37 | return cli_interface 38 | ''' 39 | return self.DEFAULT_CLI 40 | 41 | def parse_results(self, kw_args): 42 | raise NotImplementedError('Actual handler implementation should override ' 43 | 'this with proper implementation!') 44 | 45 | def check_for_keyword(self, test, data): 46 | '''Check if any of the keywords directly under this test trigger test 47 | execution 48 | 49 | test: A Robot test 50 | ''' 51 | test_keywords = get_keywords_from(test) 52 | 53 | for curr, keyword in enumerate(test_keywords): 54 | keyword_name = self._normalize_keyword_name(keyword.name) 55 | if not (keyword_name == self.keyword): 56 | continue 57 | 58 | self.run_time_data = data[test.longname] 59 | # ALL keywords, setup or not, preceding the trigger will be treated 60 | # as setup keywords later. Same goes for keywords succeeding the 61 | # trigger; they will become teardown keywords. 62 | setup_keywords = test_keywords[:curr] 63 | teardown_keywords = test_keywords[(curr+1):] 64 | 65 | self._report_oxygen_run(keyword, setup_keywords, teardown_keywords) 66 | 67 | 68 | def _report_oxygen_run(self, keyword, setup_keywords, teardown_keywords): 69 | ''' 70 | keyword: The trigger keyword for this handler 71 | setup_keywords: The keywords preceding the trigger 72 | teardown_keywords: The keywords succeeding the trigger 73 | ''' 74 | # Wrap setup- and teardown keywords as a single keyword 75 | setup_keyword = None 76 | teardown_keyword = None 77 | 78 | if setup_keywords: 79 | setup_start = setup_keywords[0].starttime 80 | setup_end = setup_keywords[-1].endtime 81 | setup_keyword = self._interface.result.create_wrapper_keyword( 82 | 'Oxygen Setup', 83 | setup_start, 84 | setup_end, 85 | True, 86 | *setup_keywords) 87 | 88 | if teardown_keywords: 89 | teardown_start = teardown_keywords[0].starttime 90 | teardown_end = teardown_keywords[-1].endtime 91 | teardown_keyword = self._interface.result.create_wrapper_keyword( 92 | 'Oxygen Teardown', 93 | teardown_start, 94 | teardown_end, 95 | False, 96 | *teardown_keywords) 97 | 98 | self._build_results(keyword, setup_keyword, teardown_keyword) 99 | 100 | def _build_results(self, keyword, setup_keyword, teardown_keyword): 101 | ''' 102 | keyword: The trigger keyword 103 | setup_keyword: The special oxygen setup wrapper 104 | teardown_keyword: The special oxygen teardown wrapper 105 | ''' 106 | accepted_params = signature(self.parse_results).parameters 107 | accepted_params_max = len(accepted_params) 108 | accepted_params_min = len([ 109 | n for n, v in accepted_params.items() 110 | if v.default == Parameter.empty]) 111 | is_multiple_inputs = isinstance(self.run_time_data, tuple) 112 | 113 | # there are multiple inputs and in the range of accepted min and max 114 | if is_multiple_inputs and (accepted_params_min <= len( 115 | self.run_time_data) <= accepted_params_max): 116 | test_results = self.parse_results(*self.run_time_data) 117 | 118 | # there is single input and one required, also can be more non-required 119 | elif not is_multiple_inputs and accepted_params_min == 1: 120 | test_results = self.parse_results(self.run_time_data) 121 | 122 | # else if there are multiple inputs and not in the range of accepted 123 | elif is_multiple_inputs: 124 | raise MismatchArgumentException( 125 | f'parse_results expects at least {accepted_params_min} and' 126 | f' at most {accepted_params_max} arguments ' 127 | f'but got {len(self.run_time_data)}') 128 | 129 | # at this point there could be only multiple required and single input 130 | else: 131 | raise MismatchArgumentException( 132 | f'parse_results expects at least {accepted_params_min} ' 133 | 'arguments but got 1') 134 | 135 | self._validate(test_results) 136 | 137 | _, result_suite = self._interface.result.build_suite( 138 | 100000, test_results) 139 | 140 | if not result_suite: 141 | return 142 | 143 | test = keyword.parent 144 | self._set_suite_tags(result_suite, *(self._tags + list(test.tags))) 145 | 146 | if setup_keyword: 147 | set_special_keyword(result_suite, 'setup', setup_keyword) 148 | 149 | if teardown_keyword: 150 | set_special_keyword(result_suite, 'teardown', teardown_keyword) 151 | 152 | self._inject_suite_report(test, result_suite) 153 | 154 | def _validate(self, oxygen_result_dict): 155 | validate_with_deprecation_warning(oxygen_result_dict, self) 156 | 157 | def _inject_suite_report(self, test, result_suite): 158 | '''Add the given suite to the parent suite of the test case. 159 | 160 | This also filters out the test case from the parent suite test cases. 161 | 162 | test: Any Robot object part of the current test execution 163 | result_suite: Robot suite to report on 164 | ''' 165 | suite = test.parent 166 | new_tests = [t for t in suite.tests if t is not test] 167 | suite.suites.append(result_suite) 168 | suite.tests = new_tests 169 | 170 | def _normalize_keyword_name(self, keyword_name): 171 | ''' 172 | keyword_name: The raw keyword name (suite.subsuite.test.My Keyword) 173 | ''' 174 | short_name = str(keyword_name).split('.')[-1].strip() 175 | underscored = re.sub(r' +', '_', short_name) 176 | return underscored.lower() 177 | 178 | def _set_suite_tags(self, suite, *tags): 179 | suite.set_tags(tags) 180 | return suite 181 | 182 | -------------------------------------------------------------------------------- /src/oxygen/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | CONFIG_FILE = Path(__file__).resolve().parent / 'config.yml' 4 | ORIGINAL_CONFIG_FILE = Path(__file__).resolve().parent / 'config_original.yml' 5 | -------------------------------------------------------------------------------- /src/oxygen/config.yml: -------------------------------------------------------------------------------- 1 | oxygen.junit: 2 | handler: JUnitHandler 3 | keyword: run_junit 4 | tags: 5 | - oxygen-junit 6 | oxygen.gatling: 7 | handler: GatlingHandler 8 | keyword: run_gatling 9 | tags: oxygen-gatling 10 | oxygen.zap: 11 | handler: ZAProxyHandler 12 | keyword: run_zap 13 | tags: oxygen-zap 14 | accepted_risk_level: 2 15 | required_confidence_level: 1 16 | 17 | -------------------------------------------------------------------------------- /src/oxygen/config_original.yml: -------------------------------------------------------------------------------- 1 | oxygen.junit: 2 | handler: JUnitHandler 3 | keyword: run_junit 4 | tags: 5 | - oxygen-junit 6 | oxygen.gatling: 7 | handler: GatlingHandler 8 | keyword: run_gatling 9 | tags: oxygen-gatling 10 | oxygen.zap: 11 | handler: ZAProxyHandler 12 | keyword: run_zap 13 | tags: oxygen-zap 14 | accepted_risk_level: 2 15 | required_confidence_level: 1 16 | 17 | -------------------------------------------------------------------------------- /src/oxygen/errors.py: -------------------------------------------------------------------------------- 1 | class GatlingHandlerException(Exception): 2 | pass 3 | 4 | 5 | class JUnitHandlerException(Exception): 6 | pass 7 | 8 | 9 | class OxygenException(Exception): 10 | pass 11 | 12 | 13 | class SubprocessException(Exception): 14 | pass 15 | 16 | 17 | class ZAProxyHandlerException(Exception): 18 | pass 19 | 20 | 21 | class ResultFileNotFoundException(Exception): 22 | pass 23 | 24 | 25 | class ResultFileIsNotAFileException(Exception): 26 | pass 27 | 28 | 29 | class MismatchArgumentException(Exception): 30 | pass 31 | 32 | 33 | class InvalidConfigurationException(Exception): 34 | pass 35 | 36 | 37 | class InvalidOxygenResultException(Exception): 38 | pass 39 | -------------------------------------------------------------------------------- /src/oxygen/gatling.py: -------------------------------------------------------------------------------- 1 | from robot.api import logger 2 | 3 | from .base_handler import BaseHandler 4 | from .errors import GatlingHandlerException, SubprocessException 5 | from .utils import run_command_line, validate_path 6 | 7 | class GatlingHandler(BaseHandler): 8 | 9 | def run_gatling(self, result_file, command, check_return_code=False, **env): 10 | '''Run Gatling performance testing tool specified with ``command``. 11 | 12 | ``result_file`` is path to the file ``oxygen`` uses to parse the results. 13 | It is important you craft your `command` to produce the file 14 | `result_file` argument expects — otherwise Oxygen will not be able to 15 | parse the results later on. 16 | 17 | ``command`` is used to run the test tool. It is a single string which is 18 | run in a terminal subshell. 19 | 20 | ``check_return_code`` checks that ``command`` returns with exit code zero 21 | (0). By default, return code is not checked as failing test execution 22 | generally is reported with non-zero return code. However, it is useful 23 | to set ``check_return_code`` to ``True`` temporarily when you want to 24 | debug why the test tool is failing for other reasons than failing test 25 | execution. 26 | 27 | ``env`` is used to pass environment variables that are set in the subshell 28 | the ``command`` is run in. 29 | ''' 30 | try: 31 | output = run_command_line(command, check_return_code, **env) 32 | except SubprocessException as e: 33 | raise GatlingHandlerException(e) 34 | logger.info(output) 35 | logger.info('Result file: {}'.format(result_file)) 36 | return result_file 37 | 38 | def parse_results(self, result_file): 39 | return self._transform_tests(validate_path(result_file).resolve()) 40 | 41 | def _transform_tests(self, result_file): 42 | '''Given the result_file path, open the test results and get a suite 43 | dict. 44 | 45 | The whole Gatling format is jank 3000. 46 | Here be dragons. 47 | 48 | result_file: The path to the Gatling results 49 | ''' 50 | test_cases = [] 51 | with open(result_file) as results: 52 | result_contents = results.readlines() 53 | for line in result_contents: 54 | columns = line.strip().split('\t') 55 | if len(columns) < 8: 56 | continue 57 | step_name = columns[4] 58 | status = columns[7] 59 | if status not in ['OK', 'KO']: 60 | continue 61 | message = '' 62 | if len(columns) > 8: 63 | message = columns[8] 64 | 65 | keyword = { 66 | 'name': ' | '.join(columns), 67 | 'pass': True, 68 | 'messages': [], 69 | } 70 | 71 | if status == 'KO': 72 | keyword['pass'] = False 73 | keyword['messages'].append(message) 74 | 75 | test_case = { 76 | 'name': step_name, 77 | 'keywords': [keyword] 78 | } 79 | 80 | test_cases.append(test_case) 81 | 82 | test_suite = { 83 | 'name': 'Gatling Scenario', 84 | 'tags': self._tags, 85 | 'tests': test_cases, 86 | } 87 | 88 | return test_suite 89 | -------------------------------------------------------------------------------- /src/oxygen/junit.py: -------------------------------------------------------------------------------- 1 | from robot.api import logger 2 | from junitparser import Error, Failure, JUnitXml 3 | from junitparser.junitparser import TestSuite as JUnitXmlTestSuite 4 | 5 | from .base_handler import BaseHandler 6 | from .errors import JUnitHandlerException, SubprocessException 7 | from .utils import run_command_line, validate_path 8 | 9 | 10 | class JUnitHandler(BaseHandler): 11 | 12 | def run_junit(self, result_file, command, check_return_code=False, **env): 13 | '''Run JUnit unit testing tool specified with ``command``. 14 | 15 | See documentation for other arguments in \`Run Gatling\`. 16 | ''' 17 | logger.debug(f'Command: {command}') 18 | try: 19 | output = run_command_line(command, check_return_code, **env) 20 | except SubprocessException as e: 21 | raise JUnitHandlerException(e) 22 | logger.info(output) 23 | logger.info('Result file: {}'.format(result_file)) 24 | return result_file 25 | 26 | def parse_results(self, result_file): 27 | result_file = validate_path(result_file) 28 | xml = JUnitXml.fromfile(result_file) 29 | return self._transform_tests(xml) 30 | 31 | def _transform_tests(self, node): 32 | '''Convert the given xml object into a test suite dict 33 | 34 | node: An xml object from JUnitXml.fromfile() 35 | 36 | Return: The test suite dict 37 | ''' 38 | suite_dict = { 39 | 'name': 'JUnit Execution', 40 | 'tags': self._tags, 41 | 'suites': [], 42 | } 43 | 44 | if isinstance(node, JUnitXmlTestSuite): 45 | node = [node] 46 | 47 | for xunit_suite in node: 48 | suite = self._transform_test_suite(xunit_suite) 49 | suite_dict['suites'].append(suite) 50 | 51 | return suite_dict 52 | 53 | def _transform_test_suite(self, test_suite): 54 | '''Convert the given suite xml object into a suite dict 55 | 56 | test_suite: A JUnit suite xml object 57 | 58 | Return: A suite dict 59 | ''' 60 | suite_dict = { 61 | 'name': test_suite.name, 62 | 'tags': [], 63 | 'suites': [], 64 | 'tests': [], 65 | } 66 | 67 | # For child suites 68 | for xunit_suite in test_suite.testsuites(): 69 | suite = self._transform_test_suite(xunit_suite) 70 | suite_dict['suites'].append(suite) 71 | 72 | # For test cases 73 | for xunit_case in test_suite: 74 | case = self._transform_test_case(xunit_case) 75 | suite_dict['tests'].append(case) 76 | 77 | return suite_dict 78 | 79 | def _transform_test_case(self, test_case): 80 | '''Convert the given test case xml object into a test case dict 81 | 82 | test_case: A JUnit case xml object 83 | 84 | Return: A test case dict 85 | ''' 86 | test_dict = { 87 | 'name': '{} (Execution)'.format(test_case.name), 88 | 'pass': True, 89 | 'messages': [], 90 | 'keywords': [], 91 | } 92 | 93 | errors = test_case.iterchildren(Error) 94 | failures = test_case.iterchildren(Failure) 95 | 96 | if errors: 97 | for error in errors: 98 | error_name = 'ERROR: {} ({})'.format( 99 | error.message, 100 | error.type, 101 | ) 102 | test_dict['messages'].append(error_name) 103 | 104 | if failures: 105 | for failure in failures: 106 | failure_name = 'FAIL: {} ({})'.format( 107 | failure.message, 108 | failure.type, 109 | ) 110 | test_dict['messages'].append(failure_name) 111 | 112 | # If we had errors/failures, it's not a PASS 113 | if test_dict['messages']: 114 | test_dict['pass'] = False 115 | 116 | execution_time = (test_case.time or 0.0) * 1000 117 | test_dict['elapsed'] = execution_time 118 | 119 | test_case_dict = { 120 | 'name': test_case.name, 121 | 'tags': [], 122 | 'keywords': [ 123 | test_dict, 124 | ], 125 | } 126 | 127 | # This is really unreliable but at least we can find the trouble spots 128 | if not test_case.time: 129 | test_case_dict['tags'].append( 130 | 'oxygen-junit-unknown-execution-time') 131 | 132 | return test_case_dict 133 | -------------------------------------------------------------------------------- /src/oxygen/oxygen_handler_result.py: -------------------------------------------------------------------------------- 1 | ''' IMPORTANT 2 | 3 | OxygenKeywordDict is defined like this since key `pass` is reserved 4 | word in Python, and thus raises SyntaxError if defined like a class. 5 | However, in the functional style you cannot refer to the TypedDict itself 6 | recursively, like you can with with class style. Oh bother. 7 | 8 | See more: 9 | - https://docs.python.org/3/library/typing.html?highlight=typeddict#typing.TypedDict 10 | - https://stackoverflow.com/a/72460065 11 | ''' 12 | 13 | import functools 14 | 15 | from typing import List, Dict 16 | # TODO FIXME: Python 3.10 requires these to be imported from here 17 | # Python 3.10 EOL is in 2026 18 | from typing_extensions import TypedDict, Required 19 | 20 | from pydantic import TypeAdapter, ValidationError 21 | 22 | from .errors import InvalidOxygenResultException 23 | 24 | 25 | _Pass = TypedDict('_Pass', { 'pass': Required[bool], 'name': Required[str] }) 26 | # define required fields in this one above 27 | class OxygenKeywordDict(_Pass, total=False): 28 | elapsed: float # milliseconds 29 | tags: List[str] 30 | messages: List[str] 31 | teardown: 'OxygenKeywordDict' # in RF, keywords do not have setup kw; just put it as first kw in `keywords` 32 | keywords: List['OxygenKeywordDict'] 33 | 34 | 35 | class OxygenTestCaseDict(TypedDict, total=False): 36 | name: Required[str] 37 | keywords: Required[List[OxygenKeywordDict]] 38 | tags: List[str] 39 | setup: OxygenKeywordDict 40 | teardown: OxygenKeywordDict 41 | 42 | 43 | class OxygenSuiteDict(TypedDict, total=False): 44 | name: Required[str] 45 | tags: List[str] 46 | setup: OxygenKeywordDict 47 | teardown: OxygenKeywordDict 48 | metadata: Dict[str, str] 49 | suites: List['OxygenSuiteDict'] 50 | tests: List[OxygenTestCaseDict] 51 | 52 | 53 | def _change_validationerror_to_oxygenexception(func): 54 | @functools.wraps(func) 55 | def wrapper(*args, **kwargs): 56 | try: 57 | return func(*args, **kwargs) 58 | except ValidationError as e: 59 | raise InvalidOxygenResultException(e) 60 | return wrapper 61 | 62 | @_change_validationerror_to_oxygenexception 63 | def validate_oxygen_suite(oxygen_result_dict): 64 | return TypeAdapter(OxygenSuiteDict).validate_python(oxygen_result_dict) 65 | 66 | @_change_validationerror_to_oxygenexception 67 | def validate_oxygen_test_case(oxygen_test_case_dict): 68 | return TypeAdapter(OxygenTestCaseDict).validate_python(oxygen_test_case_dict) 69 | 70 | @_change_validationerror_to_oxygenexception 71 | def validate_oxygen_keyword(oxygen_kw_dict): 72 | return TypeAdapter(OxygenKeywordDict).validate_python(oxygen_kw_dict) 73 | -------------------------------------------------------------------------------- /src/oxygen/robot_interface.py: -------------------------------------------------------------------------------- 1 | from robot.version import get_version as robot_version 2 | 3 | 4 | class RobotInterface(object): 5 | def __init__(self): 6 | major_version = int(robot_version().split('.')[0]) 7 | 8 | if major_version > 3: 9 | from .robot4_interface import (RobotResultInterface, 10 | RobotRunningInterface) 11 | else: 12 | from .robot3_interface import (RobotResultInterface, 13 | RobotRunningInterface) 14 | 15 | self.result = RobotResultInterface() 16 | self.running = RobotRunningInterface() 17 | 18 | 19 | def get_keywords_from(test): 20 | if hasattr(test, 'body'): 21 | return test.body.filter(keywords=True) 22 | return test.keywords 23 | 24 | 25 | def set_special_keyword(suite, keyword_type, keyword): 26 | if hasattr(suite, keyword_type): 27 | if keyword_type == 'setup': 28 | suite.setup = keyword 29 | elif keyword_type == 'teardown': 30 | suite.teardown = keyword 31 | else: 32 | suite.keywords.append(keyword) 33 | -------------------------------------------------------------------------------- /src/oxygen/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | from pathlib import Path 6 | 7 | from .errors import (InvalidOxygenResultException, 8 | ResultFileIsNotAFileException, 9 | ResultFileNotFoundException, 10 | SubprocessException) 11 | from .oxygen_handler_result import validate_oxygen_suite 12 | 13 | def run_command_line(command, check_return_code=True, **env): 14 | new_env = os.environ.copy() 15 | # When user uses 'robot --pythonpath' we need to update PYTHONPATH in the 16 | # subprocess 17 | updated_pythonpath = {'PYTHONPATH': os.pathsep.join([new_env.get( 18 | 'PYTHONPATH', '')] + sys.path)} 19 | new_env.update(updated_pythonpath) 20 | new_env.update(env) 21 | 22 | try: 23 | proc = subprocess.run(command, capture_output=True, env=new_env, shell=True) 24 | except IndexError: 25 | raise SubprocessException('Command "{}" was empty'.format(command)) 26 | if check_return_code and proc.returncode != 0: 27 | raise SubprocessException(f'Command "{command}" failed with return ' 28 | f'code {proc.returncode}:\n"{proc.stdout.decode("utf-8")}"') 29 | return proc.stdout 30 | 31 | def validate_path(filepath): 32 | try: 33 | path = Path(filepath) 34 | except TypeError as e: 35 | raise ResultFileIsNotAFileException(f'File "{filepath}" is not a file') 36 | if not path.exists(): 37 | raise ResultFileNotFoundException(f'File "{path}" does not exits') 38 | if path.is_dir(): 39 | raise ResultFileIsNotAFileException(f'File "{path}" is not a file, ' 40 | 'but a directory') 41 | return path 42 | 43 | def validate_with_deprecation_warning(oxygen_result_dict, handler): 44 | try: 45 | validate_oxygen_suite(oxygen_result_dict) 46 | except InvalidOxygenResultException as e: 47 | import warnings 48 | # this is not done with triple quotes intentionally 49 | # to get sensible formatting to output 50 | msg = (f'\n{handler.__module__} is producing invalid results:\n' 51 | f'{e}\n\n' 52 | 'In Oxygen 1.0, handlers will need to produce valid ' 53 | 'results.\nSee: ' 54 | 'https://github.com/eficode/robotframework-oxygen/blob/master/parser_specification.md') 55 | warnings.warn(msg) 56 | -------------------------------------------------------------------------------- /src/oxygen/version.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.4.dev' 2 | -------------------------------------------------------------------------------- /src/oxygen/zap.py: -------------------------------------------------------------------------------- 1 | import json 2 | import xml.etree.ElementTree as ETree 3 | 4 | from collections import defaultdict 5 | 6 | from robot.api import logger 7 | 8 | from .base_handler import BaseHandler 9 | from .errors import SubprocessException, ZAProxyHandlerException 10 | from .utils import run_command_line, validate_path 11 | 12 | 13 | class ZAProxyHandler(BaseHandler): 14 | def run_zap(self, result_file, command, check_return_code=False, **env): 15 | '''Run Zed Attack Proxy security testing tool specified with 16 | ``command``. 17 | 18 | See documentation for other arguments in \`Run Gatling\`. 19 | ''' 20 | try: 21 | output = run_command_line(command, check_return_code, **env) 22 | except SubprocessException as e: 23 | raise ZAProxyHandlerException(e) 24 | logger.info(output) 25 | logger.info('Result file: {}'.format(result_file)) 26 | return result_file 27 | 28 | def cli(self): 29 | cli_interface = self.DEFAULT_CLI.copy() 30 | cli_interface[('--accepted-risk-level',)] = { 31 | 'help': 'Set accepted risk level', 32 | 'type': int 33 | } 34 | cli_interface[('--required-confidence-level',)] = { 35 | 'help': 'Set required confidence level', 36 | 'type': int 37 | } 38 | return cli_interface 39 | 40 | def parse_results(self, result_file, accepted_risk_level=None, 41 | required_confidence_level=None): 42 | if accepted_risk_level is not None: 43 | self._config['accepted_risk_level'] = accepted_risk_level 44 | if required_confidence_level is not None: 45 | self._config['required_confidence_level'] = \ 46 | required_confidence_level 47 | zap_dict = self._read_results(validate_path(result_file).resolve()) 48 | return self._parse_zap_dict(zap_dict) 49 | 50 | def _read_results(self, file_name): 51 | with open(file_name) as test_file: 52 | result_contents = test_file.read() 53 | 54 | try: 55 | json_dict = self._xml_to_dict(ETree.XML(result_contents)) 56 | if 'OWASPZAPReport' in json_dict.keys(): 57 | json_dict = json_dict['OWASPZAPReport'] 58 | except: 59 | print('Oxygen: Loading {} as XML failed, falling ' 60 | 'back to JSON.'.format(file_name)) 61 | json_dict = json.loads(result_contents) 62 | return json_dict 63 | 64 | def _xml_to_dict(self, xml): 65 | d = {xml.tag: {} if xml.attrib else None} 66 | children = list(xml) 67 | if children: 68 | dd = defaultdict(list) 69 | for dc in map(self._xml_to_dict, children): 70 | for k, v in dc.items(): 71 | dd[k].append(v) 72 | d = {xml.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} 73 | if xml.attrib: 74 | d[xml.tag].update(('@' + k, v) for k, v in xml.attrib.items()) 75 | if xml.text: 76 | text = xml.text.strip() 77 | if children or xml.attrib: 78 | if text: 79 | d[xml.tag]['#text'] = text 80 | else: 81 | d[xml.tag] = text 82 | return d 83 | 84 | 85 | def _parse_zap_dict(self, zap_dict): 86 | zap_version = self._get_parameter(zap_dict, 'version', 'Unknown ZAProxy Version') 87 | zap_run_date = self._get_parameter(zap_dict, 'generated', 'Unknown ZAProxy Run Time') 88 | 89 | return_dict = {} 90 | return_dict['name'] = 'Oxygen ZAProxy Report ({}, {})'.format( 91 | zap_version, 92 | zap_run_date, 93 | ) 94 | 95 | zap_sites = zap_dict.get('site', []) 96 | 97 | return_dict['tags'] = self._tags 98 | return_dict['suites'] = [] 99 | 100 | for zap_site in zap_sites: 101 | parsed_site = self._parse_zap_site_dict(zap_site) 102 | return_dict['suites'].append(parsed_site) 103 | 104 | return return_dict 105 | 106 | 107 | def _parse_zap_site_dict(self, zap_site_dict): 108 | site_name = self._get_parameter(zap_site_dict, 'name', 'Unknown Site Name') 109 | 110 | return_dict = {} 111 | return_dict['name'] = 'Site: {}'.format(site_name) 112 | return_dict['tests'] = [] 113 | 114 | zap_alerts = zap_site_dict.get('alerts', []) 115 | zap_alerts = zap_alerts or [] 116 | 117 | if isinstance(zap_alerts, dict): 118 | zap_alerts = zap_alerts.get('alertitem', []) 119 | if not isinstance(zap_alerts, list): 120 | zap_alerts = [zap_alerts] 121 | for zap_alert in zap_alerts: 122 | parsed_alert = self._parse_zap_alert_dict(zap_alert) 123 | return_dict['tests'].append(parsed_alert) 124 | 125 | return return_dict 126 | 127 | 128 | def _parse_zap_alert_dict(self, zap_alert_dict): 129 | plugin_id = zap_alert_dict.get('pluginid', '[Unknown Plugin ID]') 130 | alert_name = zap_alert_dict.get('name', 'Unknown Alert Name') 131 | zap_instances = zap_alert_dict.get('instances', []) 132 | tags = [] 133 | 134 | return_dict = {} 135 | return_dict['name'] = '{} {}'.format(plugin_id, alert_name) 136 | return_dict['tags'] = tags 137 | return_dict['keywords'] = [] 138 | 139 | considered_risk = self._is_considered_risk(zap_alert_dict) 140 | is_confident = self._is_considered_confident(zap_alert_dict) 141 | 142 | if isinstance(zap_instances, dict): 143 | zap_instances = zap_instances.get('instance', None) 144 | if not isinstance(zap_instances, list): 145 | zap_instances = filter(None, [zap_instances]) 146 | 147 | for zap_instance in zap_instances: 148 | parsed_instance = self._parse_zap_instance(zap_instance, 149 | considered_risk, 150 | is_confident, 151 | ) 152 | return_dict['keywords'].append(parsed_instance) 153 | 154 | return return_dict 155 | 156 | 157 | def _is_considered_risk(self, zap_alert_dict): 158 | risk_level = zap_alert_dict.get('riskcode', -1) 159 | risk_level = int(risk_level) 160 | 161 | if risk_level < 0: 162 | risk_level = 3 163 | 164 | acceptable_risk = self._get_treshold_risk_level() 165 | considered_risk = (risk_level >= acceptable_risk) 166 | 167 | return considered_risk 168 | 169 | 170 | def _is_considered_confident(self, zap_alert_dict): 171 | confidence_level = zap_alert_dict.get('confidence', -1) 172 | confidence_level = int(confidence_level) 173 | 174 | if confidence_level < 0: 175 | confidence_level = 3 176 | 177 | required_confidence = self._get_required_confidence_level() 178 | is_confident = (confidence_level >= required_confidence) 179 | 180 | return is_confident 181 | 182 | 183 | def _parse_zap_instance(self, zap_instance_dict, risk, confident): 184 | zap_uri = zap_instance_dict.get('uri', '[Unknown Target URI]') 185 | zap_method = zap_instance_dict.get('method', '[Unknown HTTP Method]') 186 | zap_param = zap_instance_dict.get('param', '[Unknown Target Parameter]') 187 | zap_evidence = zap_instance_dict.get('evidence', '[No Evidence Provided]') 188 | 189 | return_dict = {} 190 | return_dict['name'] = '{} {}: {}'.format(zap_method, zap_uri, zap_param) 191 | return_dict['pass'] = not (risk and confident) 192 | return_dict['elapsed'] = 0.0 193 | return_dict['messages'] = [] 194 | return_dict['messages'].append('Evidence: {}'.format(zap_evidence)) 195 | 196 | return return_dict 197 | 198 | 199 | def _get_parameter(self, zap_dict, name, default_text): 200 | special_name = '@' + name 201 | 202 | return_value = zap_dict.get(name, None) 203 | if not return_value: 204 | return_value = zap_dict.get(special_name, default_text) 205 | 206 | return return_value 207 | 208 | 209 | def _get_treshold_risk_level(self): 210 | risk_level = self._config.get('accepted_risk_level', None) 211 | 212 | if risk_level is None: 213 | print('No acceptable risk level configured, defaulting to 0') 214 | return 0 215 | 216 | risk_level = int(risk_level) 217 | 218 | if risk_level > 3: 219 | print('Risk level is configured too high, maximizing at 3') 220 | return 3 221 | 222 | return risk_level 223 | 224 | 225 | def _get_required_confidence_level(self): 226 | confidence_level = self._config.get('required_confidence_level', None) 227 | 228 | if confidence_level is None: 229 | print('No required confidence level configured, defaulting to 0') 230 | return 0 231 | 232 | confidence_level = int(confidence_level) 233 | 234 | if confidence_level > 3: 235 | print('Confidence level is configured too high, maximizing at 3') 236 | return 3 237 | 238 | return confidence_level 239 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pathlib import Path 4 | from platform import system 5 | from tempfile import mkstemp 6 | from textwrap import dedent 7 | 8 | from invoke import run, task 9 | 10 | 11 | CURDIR = Path.cwd() 12 | SRCPATH = CURDIR / 'src' 13 | UNIT_TESTS = CURDIR / 'tests' 14 | DUMMYHANDLERS = UNIT_TESTS / 'resources' / 'my_dummy_handlers' 15 | 16 | # If you want colored output for the tasks, use `run()` with `pty=True` 17 | # Not on Windows, though -- it'll fail if you have `pty=True` 18 | 19 | @task 20 | def clean(context): 21 | for path in (f'{SRCPATH / "robotframework_oxygen.egg-info"}', 22 | f'{CURDIR / "build"}', 23 | f'{CURDIR / "dist"}', 24 | f'{CURDIR / ".tox"}', 25 | f'{CURDIR / "htmlcov"}', 26 | f'{CURDIR / "log.html"}', 27 | f'{CURDIR / "report.html"}', 28 | f'{CURDIR / "output.xml"}', 29 | f'{CURDIR / "green-junit.xml"}'): 30 | run(f'rm -rf {path}') 31 | run(f'python {CURDIR / "setup.py"} clean') 32 | 33 | @task(pre=[clean]) 34 | def install(context, package=None): 35 | run(f'pip install -r {CURDIR / "requirements.txt"}') 36 | if package: 37 | run(f'pip install {package}') 38 | 39 | @task(iterable=['test'], 40 | help={ 41 | 'test': 'Limit unit test execution to specific tests. Must be given ' 42 | 'multiple times to select several targets.' 43 | }) 44 | def utest(context, test=None): 45 | run(f'pytest {" -k".join(test) if test else UNIT_TESTS} -q --disable-warnings', 46 | env={'PYTHONPATH': str(SRCPATH)}, 47 | pty=(not system() == 'Windows')) 48 | 49 | @task 50 | def coverage(context): 51 | run(f'pytest --cov {UNIT_TESTS}', 52 | env={'PYTHONPATH': str(SRCPATH)}, 53 | pty=(not system() == 'Windows')) 54 | run('coverage html') 55 | 56 | def _setup_atest(): 57 | _, tempconf = mkstemp() 58 | with open(tempconf, 'w') as f: 59 | f.write('''dummy_handler_metadata: 60 | handler: MyDummyMetadataHandler 61 | keyword: run_metadata_dummy_handler 62 | tags: oxygen-metadata''') 63 | return (tempconf, 64 | os.pathsep.join([str(SRCPATH), 65 | str(DUMMYHANDLERS)])) 66 | 67 | @task(help={ 68 | 'rf': 'Additional command-line arguments for Robot Framework as ' 69 | 'single string. E.g: invoke atest --rf "--name my_suite"' 70 | }) 71 | def atest(context, rf=''): 72 | tempconf, pythonpath = _setup_atest() 73 | run(f'python -m oxygen --add-config {tempconf}', 74 | env={'PYTHONPATH': pythonpath}) 75 | try: 76 | run(f'robot ' 77 | f'--pythonpath {str(SRCPATH)} ' 78 | f'--pythonpath {str(DUMMYHANDLERS)} ' 79 | f'--dotted ' 80 | f'{rf} ' 81 | f'--listener oxygen.listener ' 82 | f'{str(CURDIR / "tests" / "atest")}', 83 | pty=(not system() == 'Windows')) 84 | finally: 85 | run('python -m oxygen --reset-config', env={'PYTHONPATH': pythonpath}) 86 | 87 | @task 88 | def test(context): 89 | utest(context) 90 | atest(context) 91 | 92 | @task 93 | def update_oxygen_schema(context): 94 | import sys 95 | import json 96 | from pydantic import TypeAdapter 97 | 98 | sys.path.insert(0, str(SRCPATH)) 99 | from oxygen.oxygen_handler_result import OxygenSuiteDict 100 | 101 | schema = TypeAdapter(OxygenSuiteDict).json_schema() 102 | out = json.dumps(schema, indent=2) 103 | with open(CURDIR / 'handler_result_specification.md', 'r+') as f: 104 | header = f.readline() 105 | f.seek(0) 106 | f.write(header) 107 | f.write(f'```\n{out}\n```') 108 | f.truncate() 109 | print('Updated schema') 110 | 111 | @task(pre=[update_oxygen_schema]) 112 | def doc(context): 113 | doc_path = CURDIR / 'docs' 114 | if not doc_path.exists(): 115 | run(f'mkdir {doc_path}') 116 | version = run('python -c "import oxygen; print(oxygen.__version__)"', 117 | env={'PYTHONPATH': str(SRCPATH)}) 118 | version = version.stdout.strip() 119 | target = doc_path / f'OxygenLibrary-{version}.html' 120 | run(f'python -m robot.libdoc oxygen.OxygenLibrary {target}', 121 | env={'PYTHONPATH': str(SRCPATH)}) 122 | run(f'cp {target} {doc_path / "index.html"}') 123 | 124 | @task(pre=[clean]) 125 | def build(context): 126 | run(f'python -m build --wheel') 127 | 128 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/__init__.py -------------------------------------------------------------------------------- /tests/atest/metadata.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library oxygen.OxygenLibrary 3 | Metadata RF suite This metadata comes from the suite file itself 4 | 5 | *** Test Cases *** 6 | Metadata returned by handler should be visible 7 | [Documentation] This test is replaced with fix results that have metadata in them 8 | Run Metadata Dummy Handler doesentmatter 9 | 10 | -------------------------------------------------------------------------------- /tests/atest/oxygen_junit_tests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library oxygen.OxygenLibrary 3 | Library OperatingSystem 4 | 5 | *** Variables *** 6 | ${JUNIT XML FILE}= ${EXECDIR}${/}green-junit.xml 7 | 8 | *** Test Cases *** 9 | Oxygen's unit tests should pass 10 | [Documentation] Tests in test_oxygen_cli that run things in subprocesses 11 | ... *SHOULD* fail; Since `Run JUnit` runs things in 12 | ... subprocess itself, this is essentially running 13 | ... subprocesses within subprocesses all the way down. 14 | ... 15 | ... We can also inspect errors are reported correctly. 16 | [Tags] oxygen-own-junit 17 | Remove file ${JUNIT XML FILE} 18 | File should not exist ${JUNIT XML FILE} 19 | ${pytest}= Get command pytest 20 | Run JUnit ${JUNIT XML FILE} 21 | ... ${pytest} --junit-xml\=${JUNIT XML FILE} ${EXECDIR} 22 | File should exist ${JUNIT XML FILE} 23 | 24 | *** Keywords *** 25 | Get command 26 | [Arguments] ${program} 27 | ${locate}= Set variable if '${:}' == ';' where which 28 | Run keyword and return Run ${locate} ${program} 29 | -------------------------------------------------------------------------------- /tests/atest/test.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library oxygen.OxygenLibrary 3 | 4 | *** Variables *** 5 | ${RESOURCES} ${CURDIR}/../resources 6 | 7 | *** Test Cases *** 8 | My First Test 9 | [Tags] JUNIT_ROBOT_TAG 10 | Log Junit Setup Here 1 11 | Log Junit Setup Here 2 12 | run_junit 13 | ... ${RESOURCES}/junit.xml 14 | ... echo JUNIT_TEST_STRING 15 | Log Junit Teardown Here 1 16 | Log Junit Teardown Here 2 17 | 18 | My First Test Director's Cut 19 | [Tags] JUNIT_ROBOT_TAG 20 | Sleep 2 21 | Log Junit Setup Here 2 22 | run junit 23 | ... ${RESOURCES}/big.xml 24 | ... echo JUNIT_TEST_STRING_BIG 25 | Log Junit Teardown Here 1 26 | Log Junit Teardown Here 2 27 | 28 | My Second Test 29 | [Tags] GATLING_ROBOT_TAG 30 | Log Gatling Setup Here 1 31 | Log Gatling Setup Here 2 32 | run_gatling 33 | ... ${RESOURCES}/gatling-example-simulation.log 34 | ... echo GATLING TEST STRING 35 | Log Gatling Teardown Here 2 36 | 37 | My Third Test 38 | [Tags] ZAP_ROBOT_TAG 39 | Log ZAP Setup Here 1 40 | Log ZAP Setup Here 2 41 | run_zap 42 | ... ${RESOURCES}/zap/zap.xml.lol 43 | ... echo ZAP TEST STRING 2 44 | Log ZAP Teardown Here 1 45 | Log ZAP Teardown Here 2 46 | 47 | My Three Point Fifth Test 48 | [Tags] ZAP_ROBOT_TAG 49 | Log ZAP Setup Here 1 50 | Log ZAP Setup Here 2 51 | run_zap 52 | ... ${RESOURCES}/zap/zap_pp.json 53 | ... echo ZAP TEST STRING 3 54 | Log ZAP Teardown Here 1 55 | Log ZAP Teardown Here 2 56 | 57 | My Fourth Test 58 | [Tags] NO_OXYGEN_HERE 59 | Log Just a normal test 60 | -------------------------------------------------------------------------------- /tests/atest/test_with_import.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library oxygen.OxygenLibrary 3 | Library OperatingSystem 4 | 5 | *** Variables *** 6 | ${JUNIT XML FILE}= ${CURDIR}${/}..${/}resources${/}junit-single-testsuite.xml 7 | 8 | *** Test Cases *** 9 | Oxygen's unit test with dynamic import 10 | Import Library oxygen.OxygenLibrary 11 | Run JUnit ${JUNIT XML FILE} 12 | ... echo Run JUnit Dynamically Imported 13 | 14 | Oxygen's unit test with global import 15 | Run JUnit ${JUNIT XML FILE} 16 | ... echo Run JUnit Globally Imported 17 | -------------------------------------------------------------------------------- /tests/atest/test_without_import.robot: -------------------------------------------------------------------------------- 1 | *** Test Cases *** 2 | Oxygen's unit test without import 3 | Log Some text 4 | -------------------------------------------------------------------------------- /tests/resources/example_robot_output.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Logs the given message with the given level. 8 | 9 | Junit Setup Here 1 10 | 11 | Junit Setup Here 1 12 | 13 | 14 | 15 | Logs the given message with the given level. 16 | 17 | Junit Setup Here 2 18 | 19 | Junit Setup Here 2 20 | 21 | 22 | 23 | Run JUnit tool specified with ``command``. 24 | 25 | ${RESOURCES}/junit.xml 26 | echo 27 | JUNIT_TEST_STRING 28 | 29 | JUNIT_TEST_STRING 30 | 31 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/junit.xml 32 | 33 | 34 | 35 | Logs the given message with the given level. 36 | 37 | Junit Teardown Here 1 38 | 39 | Junit Teardown Here 1 40 | 41 | 42 | 43 | Logs the given message with the given level. 44 | 45 | Junit Teardown Here 2 46 | 47 | Junit Teardown Here 2 48 | 49 | 50 | 51 | JUNIT_ROBOT_TAG 52 | 53 | 54 | 55 | 56 | 57 | Pauses the test executed for the given time. 58 | 59 | 2 60 | 61 | Slept 2 seconds 62 | 63 | 64 | 65 | Logs the given message with the given level. 66 | 67 | Junit Setup Here 2 68 | 69 | Junit Setup Here 2 70 | 71 | 72 | 73 | Run JUnit tool specified with ``command``. 74 | 75 | ${RESOURCES}/big.xml 76 | echo 77 | JUNIT_TEST_STRING_BIG 78 | 79 | JUNIT_TEST_STRING_BIG 80 | 81 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/big.xml 82 | 83 | 84 | 85 | Logs the given message with the given level. 86 | 87 | Junit Teardown Here 1 88 | 89 | Junit Teardown Here 1 90 | 91 | 92 | 93 | Logs the given message with the given level. 94 | 95 | Junit Teardown Here 2 96 | 97 | Junit Teardown Here 2 98 | 99 | 100 | 101 | JUNIT_ROBOT_TAG 102 | 103 | 104 | 105 | 106 | 107 | Logs the given message with the given level. 108 | 109 | Gatling Setup Here 1 110 | 111 | Gatling Setup Here 1 112 | 113 | 114 | 115 | Logs the given message with the given level. 116 | 117 | Gatling Setup Here 2 118 | 119 | Gatling Setup Here 2 120 | 121 | 122 | 123 | Run Gatling tool specified with ``command``. 124 | 125 | ${RESOURCES}/gatling-example-simulation.log 126 | echo 127 | GATLING TEST STRING 128 | 129 | GATLING TEST STRING 130 | 131 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/gatling-example-simulation.log 132 | 133 | 134 | 135 | Logs the given message with the given level. 136 | 137 | Gatling Teardown Here 2 138 | 139 | Gatling Teardown Here 2 140 | 141 | 142 | 143 | GATLING_ROBOT_TAG 144 | 145 | 146 | 147 | 148 | 149 | Logs the given message with the given level. 150 | 151 | ZAP Setup Here 1 152 | 153 | ZAP Setup Here 1 154 | 155 | 156 | 157 | Logs the given message with the given level. 158 | 159 | ZAP Setup Here 2 160 | 161 | ZAP Setup Here 2 162 | 163 | 164 | 165 | Run Zed Attack Proxy tool specified with ``command``. 166 | 167 | ${RESOURCES}/zap/zap.xml.lol 168 | echo 169 | ZAP TEST STRING 2 170 | 171 | ZAP TEST STRING 2 172 | 173 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/zap/zap.xml.lol 174 | 175 | 176 | 177 | Logs the given message with the given level. 178 | 179 | ZAP Teardown Here 1 180 | 181 | ZAP Teardown Here 1 182 | 183 | 184 | 185 | Logs the given message with the given level. 186 | 187 | ZAP Teardown Here 2 188 | 189 | ZAP Teardown Here 2 190 | 191 | 192 | 193 | ZAP_ROBOT_TAG 194 | 195 | 196 | 197 | 198 | 199 | Logs the given message with the given level. 200 | 201 | ZAP Setup Here 1 202 | 203 | ZAP Setup Here 1 204 | 205 | 206 | 207 | Logs the given message with the given level. 208 | 209 | ZAP Setup Here 2 210 | 211 | ZAP Setup Here 2 212 | 213 | 214 | 215 | Run Zed Attack Proxy tool specified with ``command``. 216 | 217 | ${RESOURCES}/zap/zap_pp.json 218 | echo 219 | ZAP TEST STRING 3 220 | 221 | ZAP TEST STRING 3 222 | 223 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/zap/zap_pp.json 224 | 225 | 226 | 227 | Logs the given message with the given level. 228 | 229 | ZAP Teardown Here 1 230 | 231 | ZAP Teardown Here 1 232 | 233 | 234 | 235 | Logs the given message with the given level. 236 | 237 | ZAP Teardown Here 2 238 | 239 | ZAP Teardown Here 2 240 | 241 | 242 | 243 | ZAP_ROBOT_TAG 244 | 245 | 246 | 247 | 248 | 249 | Logs the given message with the given level. 250 | 251 | Just a normal test 252 | 253 | Just a normal test 254 | 255 | 256 | 257 | NO_OXYGEN_HERE 258 | 259 | 260 | 261 | 262 | 263 | Logs the given message with the given level. 264 | 265 | My Dummy Handler Setup Here 1 266 | 267 | My Dummy Handler Setup Here 1 268 | 269 | 270 | 271 | Logs the given message with the given level. 272 | 273 | My Dummy Handler Setup Here 2 274 | 275 | My Dummy Handler Setup Here 2 276 | 277 | 278 | 279 | Run My Dummy Handler tool specified with ``command``. 280 | 281 | ${RESOURCES}/my_dummy_handler.xml 282 | echo 283 | MY_DUMMY_HANDLER_TEST_STRING 284 | 285 | MY_DUMMY_HANDLER_TEST_STRING 286 | 287 | Result file: /Users/tkairi/Coding/oxygen/tests/atest/../resources/my_dummy_handler.xml 288 | 289 | 290 | 291 | Logs the given message with the given level. 292 | 293 | My Dummy Handler Teardown Here 1 294 | 295 | My Dummy Handler Teardown Here 1 296 | 297 | 298 | 299 | Logs the given message with the given level. 300 | 301 | My Dummy Handler Teardown Here 2 302 | 303 | My Dummy Handler Teardown Here 2 304 | 305 | 306 | 307 | MY_DUMMY_HANDLER_ROBOT_TAG 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | Critical Tests 318 | All Tests 319 | 320 | 321 | GATLING_ROBOT_TAG 322 | JUNIT_ROBOT_TAG 323 | NO_OXYGEN_HERE 324 | ZAP_ROBOT_TAG 325 | MY_DUMMY_HANDLER_ROBOT_TAG 326 | 327 | 328 | Atest 329 | Atest.Test 330 | 331 | 332 | 333 | 334 | 335 | -------------------------------------------------------------------------------- /tests/resources/gatling-example-simulation.log: -------------------------------------------------------------------------------- 1 | RUN computerdatabase.advanced.AdvancedSimulationStep05 qwert advancedsimulationstep05 1533120477681 2.0 2 | USER Users 1 START 1533120479078 1533120479078 3 | USER Admins 4 START 1533120479220 1533120479220 4 | REQUEST Admins 4 Home 1533120479221 1533120479313 OK 5 | REQUEST Users 1 Home 1533120479124 1533120479298 OK 6 | REQUEST Users 1 Home Redirect 1 1533120479321 1533120479368 OK 7 | REQUEST Admins 4 Home Redirect 1 1533120479321 1533120479367 OK 8 | USER Users 2 START 1533120480235 1533120480235 9 | REQUEST Users 2 Home 1533120480235 1533120480421 OK 10 | REQUEST Users 2 Home Redirect 1 1533120480422 1533120480466 OK 11 | REQUEST Users 1 Search 1533120480410 1533120480744 OK 12 | REQUEST Admins 4 Search 1533120480407 1533120480768 OK 13 | USER Users 3 START 1533120481233 1533120481233 14 | REQUEST Users 3 Home 1533120481234 1533120481322 OK 15 | REQUEST Users 3 Home Redirect 1 1533120481323 1533120481368 OK 16 | REQUEST Users 2 Search 1533120481486 1533120481533 OK 17 | REQUEST Users 1 Select 1533120481736 1533120481783 OK 18 | REQUEST Admins 4 Select 1533120481756 1533120481800 OK 19 | USER Users 5 START 1533120482095 1533120482095 20 | REQUEST Users 5 Home 1533120482095 1533120482187 OK 21 | REQUEST Users 5 Home Redirect 1 1533120482188 1533120482235 OK 22 | REQUEST Users 3 Search 1533120482385 1533120482431 OK 23 | REQUEST Users 2 Select 1533120482536 1533120482582 OK 24 | REQUEST Users 1 Page 0 1533120482801 1533120482848 OK 25 | REQUEST Admins 4 Page 0 1533120482804 1533120482848 OK 26 | USER Users 6 START 1533120483095 1533120483095 27 | REQUEST Users 6 Home 1533120483096 1533120483190 OK 28 | REQUEST Users 6 Home Redirect 1 1533120483191 1533120483238 OK 29 | REQUEST Users 5 Search 1533120483256 1533120483303 OK 30 | REQUEST Users 3 Select 1533120483436 1533120483482 OK 31 | REQUEST Users 2 Page 0 1533120483585 1533120483631 OK 32 | REQUEST Users 1 Page 1 1533120483835 1533120483881 OK 33 | REQUEST Admins 4 Page 1 1533120483845 1533120483889 OK 34 | USER Admins 8 START 1533120484086 1533120484086 35 | USER Users 7 START 1533120484095 1533120484095 36 | REQUEST Admins 8 Home 1533120484086 1533120484182 OK 37 | REQUEST Users 7 Home 1533120484095 1533120484221 OK 38 | REQUEST Admins 8 Home Redirect 1 1533120484183 1533120484228 OK 39 | REQUEST Users 7 Home Redirect 1 1533120484222 1533120484269 OK 40 | REQUEST Users 6 Search 1533120484255 1533120484303 OK 41 | REQUEST Users 5 Select 1533120484305 1533120484352 OK 42 | REQUEST Users 3 Page 0 1533120484476 1533120484523 OK 43 | REQUEST Users 2 Page 1 1533120484635 1533120484679 OK 44 | REQUEST Users 1 Page 2 1533120484887 1533120484933 OK 45 | REQUEST Admins 4 Page 2 1533120484896 1533120484940 OK 46 | USER Users 9 START 1533120485084 1533120485084 47 | REQUEST Users 9 Home 1533120485084 1533120485170 OK 48 | REQUEST Users 9 Home Redirect 1 1533120485171 1533120485225 OK 49 | REQUEST Admins 8 Search 1533120485246 1533120485291 OK 50 | REQUEST Users 7 Search 1533120485286 1533120485331 OK 51 | REQUEST Users 6 Select 1533120485306 1533120485357 OK 52 | REQUEST Users 5 Page 0 1533120485345 1533120485408 OK 53 | REQUEST Users 3 Page 1 1533120485536 1533120485583 OK 54 | REQUEST Users 2 Page 2 1533120485676 1533120485720 OK 55 | REQUEST Admins 4 Page 3 1533120485936 1533120485981 OK 56 | REQUEST Users 1 Page 3 1533120485936 1533120485982 OK 57 | USER Users 10 START 1533120486084 1533120486084 58 | REQUEST Users 10 Home 1533120486085 1533120486166 OK 59 | REQUEST Users 10 Home Redirect 1 1533120486167 1533120486212 OK 60 | REQUEST Users 9 Search 1533120486247 1533120486291 OK 61 | REQUEST Admins 8 Select 1533120486295 1533120486340 OK 62 | REQUEST Users 7 Select 1533120486335 1533120486380 OK 63 | REQUEST Users 6 Page 0 1533120486355 1533120486403 OK 64 | REQUEST Users 5 Page 1 1533120486414 1533120486460 OK 65 | REQUEST Users 3 Page 2 1533120486586 1533120486631 OK 66 | REQUEST Users 2 Page 3 1533120486716 1533120486761 OK 67 | USER Users 1 END 1533120479078 1533120486992 68 | REQUEST Admins 4 Form 1533120486987 1533120487033 OK 69 | USER Users 11 START 1533120487094 1533120487094 70 | REQUEST Users 11 Home 1533120487094 1533120487191 OK 71 | REQUEST Users 11 Home Redirect 1 1533120487192 1533120487237 OK 72 | REQUEST Users 10 Search 1533120487236 1533120487280 OK 73 | REQUEST Users 9 Select 1533120487284 1533120487332 OK 74 | REQUEST Admins 8 Page 0 1533120487334 1533120487378 OK 75 | REQUEST Users 7 Page 0 1533120487375 1533120487420 OK 76 | REQUEST Users 6 Page 1 1533120487416 1533120487464 OK 77 | REQUEST Users 5 Page 2 1533120487455 1533120487502 OK 78 | REQUEST Users 3 Page 3 1533120487615 1533120487662 OK 79 | USER Users 2 END 1533120480235 1533120487765 80 | USER Users 12 START 1533120488095 1533120488095 81 | REQUEST Admins 4 Post 1533120488053 1533120488103 OK 82 | REQUEST Admins 4 Post Redirect 1 1533120488113 1533120488157 OK 83 | USER Admins 4 END 1533120479220 1533120488174 84 | REQUEST Users 12 Home 1533120488096 1533120488181 OK 85 | REQUEST Users 12 Home Redirect 1 1533120488182 1533120488249 OK 86 | REQUEST Users 11 Search 1533120488256 1533120488300 OK 87 | REQUEST Users 10 Select 1533120488276 1533120488319 OK 88 | REQUEST Users 9 Page 0 1533120488334 1533120488390 OK 89 | REQUEST Admins 8 Page 1 1533120488384 1533120488429 OK 90 | REQUEST Users 7 Page 1 1533120488425 1533120488471 OK 91 | REQUEST Users 6 Page 2 1533120488465 1533120488513 OK 92 | REQUEST Users 5 Page 3 1533120488504 1533120488549 OK 93 | USER Users 3 END 1533120481233 1533120488665 94 | REQUEST Users 12 Search 1533120489266 1533120489311 OK 95 | REQUEST Users 11 Select 1533120489306 1533120489348 OK 96 | REQUEST Users 10 Page 0 1533120489316 1533120489358 OK 97 | REQUEST Users 9 Page 1 1533120489395 1533120489441 OK 98 | REQUEST Admins 8 Page 2 1533120489425 1533120489470 OK 99 | REQUEST Users 7 Page 2 1533120489463 1533120489509 OK 100 | USER Users 5 END 1533120482095 1533120489555 101 | REQUEST Users 6 Page 3 1533120489504 1533120489577 OK 102 | REQUEST Users 12 Select 1533120490304 1533120490353 OK 103 | REQUEST Users 11 Page 0 1533120490344 1533120490386 OK 104 | REQUEST Users 10 Page 1 1533120490365 1533120490407 OK 105 | REQUEST Users 9 Page 2 1533120490436 1533120490481 OK 106 | REQUEST Admins 8 Page 3 1533120490475 1533120490525 OK 107 | REQUEST Users 7 Page 3 1533120490515 1533120490560 OK 108 | USER Users 6 END 1533120483095 1533120490583 109 | REQUEST Users 12 Page 0 1533120491366 1533120491413 OK 110 | REQUEST Users 11 Page 1 1533120491386 1533120491430 OK 111 | REQUEST Users 10 Page 2 1533120491405 1533120491447 OK 112 | REQUEST Users 9 Page 3 1533120491486 1533120491531 OK 113 | REQUEST Admins 8 Form 1533120491515 1533120491560 OK 114 | USER Users 7 END 1533120484095 1533120491565 115 | REQUEST Users 12 Page 1 1533120492404 1533120492449 OK 116 | REQUEST Users 11 Page 2 1533120492436 1533120492479 OK 117 | REQUEST Users 10 Page 3 1533120492446 1533120492488 OK 118 | USER Users 9 END 1533120485084 1533120492523 119 | REQUEST Admins 8 Post 1533120492564 1533120492610 OK 120 | REQUEST Admins 8 Post Redirect 1 1533120492611 1533120492655 KO status.find.is(201), but actually found 200 121 | REQUEST Admins 8 Form 1533120492674 1533120492719 OK 122 | USER Users 10 END 1533120486084 1533120493495 123 | REQUEST Users 12 Page 2 1533120493454 1533120493499 OK 124 | REQUEST Users 11 Page 3 1533120493476 1533120493518 OK 125 | REQUEST Admins 8 Post 1533120493706 1533120493750 OK 126 | REQUEST Admins 8 Post Redirect 1 1533120493751 1533120493797 KO status.find.is(201), but actually found 200 127 | USER Admins 8 END 1533120484086 1533120493801 128 | USER Users 11 END 1533120487094 1533120494515 129 | REQUEST Users 12 Page 3 1533120494505 1533120494552 OK 130 | USER Users 12 END 1533120488095 1533120495555 131 | -------------------------------------------------------------------------------- /tests/resources/green-junit-example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Traceback (most recent call last): 111 | 112 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor 113 | yield 114 | 115 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 628, in run 116 | testMethod() 117 | 118 | File "/Users/tkairi/Coding/oxygen/tests/utest/oxygen/test_oxygen_cli.py", line 28, in test_cli_with_no_args 119 | self.assertEqual(proc.returncode, 2) 120 | 121 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 852, in assertEqual 122 | assertion_func(first, second, msg=msg) 123 | 124 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 845, in _baseAssertEqual 125 | raise self.failureException(msg) 126 | 127 | AssertionError: 1 != 2 128 | 129 | 130 | 131 | Traceback (most recent call last): 132 | 133 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor 134 | yield 135 | 136 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 628, in run 137 | testMethod() 138 | 139 | File "/Users/tkairi/Coding/oxygen/tests/utest/oxygen/test_oxygen_cli.py", line 20, in test_direct_module_entrypoint 140 | self.verify_cli_help_text('python -m oxygen.oxygen --help') 141 | 142 | File "/Users/tkairi/Coding/oxygen/tests/utest/oxygen/test_oxygen_cli.py", line 32, in verify_cli_help_text 143 | out = check_output(cmd, text=True, shell=True) 144 | 145 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 411, in check_output 146 | **kwargs).stdout 147 | 148 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 512, in run 149 | output=stdout, stderr=stderr) 150 | 151 | subprocess.CalledProcessError: Command 'python -m oxygen.oxygen --help' returned non-zero exit status 1. 152 | 153 | 154 | 155 | Traceback (most recent call last): 156 | 157 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 59, in testPartExecutor 158 | yield 159 | 160 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py", line 628, in run 161 | testMethod() 162 | 163 | File "/Users/tkairi/Coding/oxygen/tests/utest/oxygen/test_oxygen_cli.py", line 16, in test_main_level_entrypoint 164 | self.verify_cli_help_text('python -m oxygen --help') 165 | 166 | File "/Users/tkairi/Coding/oxygen/tests/utest/oxygen/test_oxygen_cli.py", line 32, in verify_cli_help_text 167 | out = check_output(cmd, text=True, shell=True) 168 | 169 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 411, in check_output 170 | **kwargs).stdout 171 | 172 | File "/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 512, in run 173 | output=stdout, stderr=stderr) 174 | 175 | subprocess.CalledProcessError: Command 'python -m oxygen --help' returned non-zero exit status 1. 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /tests/resources/junit-single-testsuite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/resources/junit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/resources/my_dummy_handlers/__init__.py -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/dummy_handler_default_params.py: -------------------------------------------------------------------------------- 1 | from oxygen import BaseHandler 2 | 3 | 4 | class MyDummyHandler(BaseHandler): 5 | ''' 6 | A test handler that throws mismatch argument exception if single argument 7 | is passed with multiple accepted 8 | ''' 9 | 10 | def run_my_dummy_handler(self, result_file): 11 | return result_file 12 | 13 | def parse_results(self, result_file, foo='foo'): 14 | return { 15 | 'name': result_file, 16 | 'foo': foo 17 | } 18 | -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/dummy_handler_metadata.py: -------------------------------------------------------------------------------- 1 | from oxygen import BaseHandler 2 | 3 | class MyDummyMetadataHandler(BaseHandler): 4 | def run_metadata_dummy_handler(self, result_file): 5 | return result_file 6 | 7 | def parse_results(self, result_file): 8 | return { 9 | 'name': 'Minimal Suite', 10 | 'metadata': { 11 | 'Main-level metadata': 'This should come *from handler*' 12 | }, 13 | 'suites': [{ 14 | 'name': 'Minimal Subsuite', 15 | 'metadata': { 'Sub-level metadata': '_Inner_ metadata' }, 16 | 'tests': [{ 17 | 'name': 'Minimal TC', 18 | 'keywords': [{ 'name': 'someKeyword', 'pass': True }] 19 | }] 20 | }] 21 | } 22 | -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/dummy_handler_multiple_args.py: -------------------------------------------------------------------------------- 1 | from oxygen import BaseHandler 2 | 3 | 4 | class MyDummyHandler(BaseHandler): 5 | ''' 6 | A test handler for unfolding parse_results arguments 7 | if it has multiple parameters 8 | ''' 9 | 10 | def run_my_dummy_handler(self, result_file): 11 | return result_file, 'foo' 12 | 13 | def parse_results(self, result_file, foo): 14 | return { 15 | 'name': result_file, 16 | 'foo': foo 17 | } 18 | -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/dummy_handler_multiple_args_too_few.py: -------------------------------------------------------------------------------- 1 | from oxygen import BaseHandler 2 | 3 | 4 | class MyDummyHandler(BaseHandler): 5 | ''' 6 | A test handler that throws mismatch argument exception because 7 | parse_results expects too many arguments 8 | ''' 9 | 10 | def run_my_dummy_handler(self, result_file): 11 | return result_file, 'foo' 12 | 13 | def parse_results(self, result_file, foo, bar): 14 | return { 15 | 'name': result_file, 16 | 'foo': foo, 17 | 'bar': bar 18 | } 19 | -------------------------------------------------------------------------------- /tests/resources/my_dummy_handlers/dummy_handler_single_arg.py: -------------------------------------------------------------------------------- 1 | from oxygen import BaseHandler 2 | 3 | 4 | class MyDummyHandler(BaseHandler): 5 | ''' 6 | A test handler for passing tuple if parse_results accepts one parameter 7 | ''' 8 | 9 | def run_my_dummy_handler(self, result_file): 10 | return result_file, 'foo' 11 | 12 | def parse_results(self, result_file): 13 | return { 14 | 'name': result_file 15 | } 16 | -------------------------------------------------------------------------------- /tests/resources/zap/zap.json: -------------------------------------------------------------------------------- 1 | {"@version":"2.7.0","@generated":"Tue, 7 Aug 2018 13:18:23","site":[{"@name":"http://192.168.50.56:7272","@host":"192.168.50.56","@port":"7272","@ssl":"false","alerts":[]},{"@name":"http://localhost:7272","@host":"localhost","@port":"7272","@ssl":"false","alerts":[{"pluginid":"10012","alert":"Password Autocomplete in Browser","name":"Password Autocomplete in Browser","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

The AUTOCOMPLETE attribute is not disabled on an HTML FORM/INPUT element containing password type input. Passwords may be stored in browsers and retrieved.<\/p>","instances":[{"uri":"http://localhost:7272/","method":"GET","param":"password_field","evidence":""}],"count":"1","solution":"

Turn off the AUTOCOMPLETE attribute in forms or individual input elements containing password inputs by using AUTOCOMPLETE='OFF'.<\/p>","reference":"

http://www.w3schools.com/tags/att_input_autocomplete.asp<\/p>

https://msdn.microsoft.com/en-us/library/ms533486%28v=vs.85%29.aspx<\/p>","cweid":"525","wascid":"15","sourceid":"3"},{"pluginid":"10020","alert":"X-Frame-Options Header Not Set","name":"X-Frame-Options Header Not Set","riskcode":"2","confidence":"2","riskdesc":"Medium (Medium)","desc":"

X-Frame-Options header is not included in the HTTP response to protect against 'ClickJacking' attacks.<\/p>","instances":[{"uri":"http://localhost:7272/","method":"GET","param":"X-Frame-Options"}],"count":"1","solution":"

Most modern Web browsers support the X-Frame-Options HTTP header. Ensure it's set on all web pages returned by your site (if you expect the page to be framed only by pages on your server (e.g. it's part of a FRAMESET) then you'll want to use SAMEORIGIN, otherwise if you never expect the page to be framed, you should use DENY. ALLOW-FROM allows specific websites to frame the web page in supported web browsers).<\/p>","reference":"

http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx<\/p>","cweid":"16","wascid":"15","sourceid":"3"},{"pluginid":"10016","alert":"Web Browser XSS Protection Not Enabled","name":"Web Browser XSS Protection Not Enabled","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

Web Browser XSS Protection is not enabled, or is disabled by the configuration of the 'X-XSS-Protection' HTTP response header on the web server<\/p>","instances":[{"uri":"http://localhost:7272/","method":"GET","param":"X-XSS-Protection"}],"count":"1","solution":"

Ensure that the web browser's XSS filter is enabled, by setting the X-XSS-Protection HTTP response header to '1'.<\/p>","otherinfo":"

The X-XSS-Protection HTTP response header allows the web server to enable or disable the web browser's XSS protection mechanism. The following values would attempt to enable it: <\/p>

X-XSS-Protection: 1; mode=block<\/p>

X-XSS-Protection: 1; report=http://www.example.com/xss<\/p>

The following values would disable it:<\/p>

X-XSS-Protection: 0<\/p>

The X-XSS-Protection HTTP response header is currently supported on Internet Explorer, Chrome and Safari (WebKit).<\/p>

Note that this alert is only raised if the response body could potentially contain an XSS payload (with a text-based content type, with a non-zero length).<\/p>","reference":"

https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet<\/p>

https://blog.veracode.com/2014/03/guidelines-for-setting-security-headers/<\/p>","cweid":"933","wascid":"14","sourceid":"3"},{"pluginid":"10021","alert":"X-Content-Type-Options Header Missing","name":"X-Content-Type-Options Header Missing","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.<\/p>","instances":[{"uri":"http://localhost:7272/","method":"GET","param":"X-Content-Type-Options"},{"uri":"http://localhost:7272/demo.css","method":"GET","param":"X-Content-Type-Options"}],"count":"2","solution":"

Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.<\/p>

If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.<\/p>","otherinfo":"

This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.<\/p>

At \"High\" threshold this scanner will not alert on client or server error responses.<\/p>","reference":"

http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx<\/p>

https://www.owasp.org/index.php/List_of_useful_HTTP_headers<\/p>","cweid":"16","wascid":"15","sourceid":"3"}]},{"@name":"http://127.0.0.1:7272","@host":"127.0.0.1","@port":"7272","@ssl":"false","alerts":[{"pluginid":"10021","alert":"X-Content-Type-Options Header Missing","name":"X-Content-Type-Options Header Missing","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.<\/p>","instances":[{"uri":"http://127.0.0.1:7272/demo.css","method":"GET","param":"X-Content-Type-Options"},{"uri":"http://127.0.0.1:7272/welcome.html","method":"GET","param":"X-Content-Type-Options"},{"uri":"http://127.0.0.1:7272/","method":"GET","param":"X-Content-Type-Options"},{"uri":"http://127.0.0.1:7272/error.html","method":"GET","param":"X-Content-Type-Options"},{"uri":"http://127.0.0.1:7272","method":"GET","param":"X-Content-Type-Options"}],"count":"5","solution":"

Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.<\/p>

If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.<\/p>","otherinfo":"

This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.<\/p>

At \"High\" threshold this scanner will not alert on client or server error responses.<\/p>","reference":"

http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx<\/p>

https://www.owasp.org/index.php/List_of_useful_HTTP_headers<\/p>","cweid":"16","wascid":"15","sourceid":"3"},{"pluginid":"10016","alert":"Web Browser XSS Protection Not Enabled","name":"Web Browser XSS Protection Not Enabled","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

Web Browser XSS Protection is not enabled, or is disabled by the configuration of the 'X-XSS-Protection' HTTP response header on the web server<\/p>","instances":[{"uri":"http://127.0.0.1:7272/favicon.ico","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272/","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272/sitemap.xml","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272/welcome.html","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272/error.html","method":"GET","param":"X-XSS-Protection"},{"uri":"http://127.0.0.1:7272/robots.txt","method":"GET","param":"X-XSS-Protection"}],"count":"7","solution":"

Ensure that the web browser's XSS filter is enabled, by setting the X-XSS-Protection HTTP response header to '1'.<\/p>","otherinfo":"

The X-XSS-Protection HTTP response header allows the web server to enable or disable the web browser's XSS protection mechanism. The following values would attempt to enable it: <\/p>

X-XSS-Protection: 1; mode=block<\/p>

X-XSS-Protection: 1; report=http://www.example.com/xss<\/p>

The following values would disable it:<\/p>

X-XSS-Protection: 0<\/p>

The X-XSS-Protection HTTP response header is currently supported on Internet Explorer, Chrome and Safari (WebKit).<\/p>

Note that this alert is only raised if the response body could potentially contain an XSS payload (with a text-based content type, with a non-zero length).<\/p>","reference":"

https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet<\/p>

https://blog.veracode.com/2014/03/guidelines-for-setting-security-headers/<\/p>","cweid":"933","wascid":"14","sourceid":"3"},{"pluginid":"10020","alert":"X-Frame-Options Header Not Set","name":"X-Frame-Options Header Not Set","riskcode":"2","confidence":"2","riskdesc":"Medium (Medium)","desc":"

X-Frame-Options header is not included in the HTTP response to protect against 'ClickJacking' attacks.<\/p>","instances":[{"uri":"http://127.0.0.1:7272/","method":"GET","param":"X-Frame-Options"},{"uri":"http://127.0.0.1:7272","method":"GET","param":"X-Frame-Options"},{"uri":"http://127.0.0.1:7272/welcome.html","method":"GET","param":"X-Frame-Options"},{"uri":"http://127.0.0.1:7272/error.html","method":"GET","param":"X-Frame-Options"}],"count":"4","solution":"

Most modern Web browsers support the X-Frame-Options HTTP header. Ensure it's set on all web pages returned by your site (if you expect the page to be framed only by pages on your server (e.g. it's part of a FRAMESET) then you'll want to use SAMEORIGIN, otherwise if you never expect the page to be framed, you should use DENY. ALLOW-FROM allows specific websites to frame the web page in supported web browsers).<\/p>","reference":"

http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx<\/p>","cweid":"16","wascid":"15","sourceid":"3"},{"pluginid":"10012","alert":"Password Autocomplete in Browser","name":"Password Autocomplete in Browser","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

The AUTOCOMPLETE attribute is not disabled on an HTML FORM/INPUT element containing password type input. Passwords may be stored in browsers and retrieved.<\/p>","instances":[{"uri":"http://127.0.0.1:7272","method":"GET","param":"password_field","evidence":""},{"uri":"http://127.0.0.1:7272/","method":"GET","param":"password_field","evidence":""}],"count":"2","solution":"

Turn off the AUTOCOMPLETE attribute in forms or individual input elements containing password inputs by using AUTOCOMPLETE='OFF'.<\/p>","reference":"

http://www.w3schools.com/tags/att_input_autocomplete.asp<\/p>

https://msdn.microsoft.com/en-us/library/ms533486%28v=vs.85%29.aspx<\/p>","cweid":"525","wascid":"15","sourceid":"3"}]},{"@name":"http://detectportal.firefox.com","@host":"detectportal.firefox.com","@port":"80","@ssl":"false","alerts":[{"pluginid":"10021","alert":"X-Content-Type-Options Header Missing","name":"X-Content-Type-Options Header Missing","riskcode":"1","confidence":"2","riskdesc":"Low (Medium)","desc":"

The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.<\/p>","instances":[{"uri":"http://detectportal.firefox.com/success.txt","method":"GET","param":"X-Content-Type-Options"}],"count":"1","solution":"

Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.<\/p>

If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.<\/p>","otherinfo":"

This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.<\/p>

At \"High\" threshold this scanner will not alert on client or server error responses.<\/p>","reference":"

http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx<\/p>

https://www.owasp.org/index.php/List_of_useful_HTTP_headers<\/p>","cweid":"16","wascid":"15","sourceid":"3"}]}]} -------------------------------------------------------------------------------- /tests/utest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/__init__.py -------------------------------------------------------------------------------- /tests/utest/base_handler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/base_handler/__init__.py -------------------------------------------------------------------------------- /tests/utest/base_handler/test_errors.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.base_handler import BaseHandler 4 | from ..helpers import get_config 5 | 6 | class TestBaseHandlerErrors(TestCase): 7 | def test_parse_results_raises_an_error(self): 8 | '''It is intentional that parse_results() raises an error. 9 | 10 | It should never be directly invoked from BaseHandler, as it should 11 | always be specific to implementing plugin. 12 | ''' 13 | with self.assertRaises(NotImplementedError): 14 | BaseHandler(get_config()['oxygen.junit']).parse_results('whatever') 15 | -------------------------------------------------------------------------------- /tests/utest/base_handler/test_inject_suite_report.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.base_handler import BaseHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestInjectSuiteReport(TestCase): 9 | def setUp(self): 10 | self.handler = BaseHandler(get_config()['oxygen.junit']) 11 | 12 | self.test = MagicMock() 13 | self.parent = MagicMock() 14 | self.traveller = MagicMock() 15 | self.suites = MagicMock() 16 | self.suites.append = MagicMock() 17 | self.suite = MagicMock() 18 | 19 | self.test.parent = self.parent 20 | self.parent.tests = [1, 2, self.test, 3] 21 | self.parent.parent = self.traveller 22 | self.traveller.parent = None 23 | self.traveller.suites = self.suites 24 | 25 | def test_finds_and_appends(self): 26 | self.handler._inject_suite_report(self.test, self.suite) 27 | self.test.parent.suites.append.assert_called_once_with(self.suite) 28 | 29 | def test_finds_and_filters(self): 30 | self.handler._inject_suite_report(self.test, self.suite) 31 | self.assertEqual(self.parent.tests, [1, 2, 3]) 32 | -------------------------------------------------------------------------------- /tests/utest/base_handler/test_normalize_keyword_name.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.base_handler import BaseHandler 4 | from ..helpers import get_config 5 | 6 | 7 | class TestNormalizeKeywordName(TestCase): 8 | 9 | def test_normalize(self): 10 | self.bh = BaseHandler(get_config()['oxygen.gatling']) 11 | unnormalized = 'Suite 1 . Suite 2 . My KeyWord NAME ' 12 | self.assertEqual(self.bh._normalize_keyword_name(unnormalized), 13 | 'my_keyword_name') 14 | -------------------------------------------------------------------------------- /tests/utest/base_handler/test_set_suite_tags.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.base_handler import BaseHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestSetSuiteTags(TestCase): 9 | def setUp(self): 10 | self.object = BaseHandler(get_config()['oxygen.junit']) 11 | 12 | self.suite = MagicMock() 13 | self.suite.set_tags = MagicMock() 14 | self.tags = (MagicMock(), MagicMock()) 15 | 16 | def test_tags_are_set(self): 17 | self.object._set_suite_tags(self.suite, *self.tags) 18 | self.suite.set_tags.assert_called_once_with(self.tags) 19 | -------------------------------------------------------------------------------- /tests/utest/gatling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/gatling/__init__.py -------------------------------------------------------------------------------- /tests/utest/gatling/test_basic_functionality.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest import skip, TestCase 3 | from unittest.mock import ANY, create_autospec, Mock, patch 4 | 5 | from testfixtures import compare 6 | 7 | from oxygen.base_handler import BaseHandler 8 | from oxygen.gatling import GatlingHandler 9 | from oxygen.errors import GatlingHandlerException 10 | from oxygen.oxygen_handler_result import validate_oxygen_suite 11 | from ..helpers import (example_robot_output, 12 | GATLING_EXPECTED_OUTPUT, 13 | get_config, 14 | RESOURCES_PATH) 15 | 16 | class GatlingBasicTests(TestCase): 17 | 18 | def setUp(self): 19 | self.handler = GatlingHandler(get_config()['oxygen.gatling']) 20 | 21 | def test_initialization(self): 22 | self.assertEqual(self.handler.keyword, 'run_gatling') 23 | self.assertEqual(self.handler._tags, ['GATLING']) 24 | 25 | @patch('oxygen.gatling.GatlingHandler._transform_tests') 26 | @patch('oxygen.gatling.validate_path') 27 | def test_parsing(self, mock_validate_path, mock_transform): 28 | m = create_autospec(Path) 29 | mock_validate_path.return_value = m 30 | self.handler.parse_results('some/file/path.ext') 31 | mock_validate_path.assert_called_once_with('some/file/path.ext') 32 | mock_transform.assert_called_once_with(m.resolve()) 33 | 34 | @patch('oxygen.utils.subprocess') 35 | def test_running(self, mock_subprocess): 36 | mock_subprocess.run.return_value = Mock(returncode=0) 37 | self.handler.run_gatling('somefile', 'some command') 38 | mock_subprocess.run.assert_called_once_with('some command', 39 | capture_output=True, 40 | shell=True, 41 | env=ANY) 42 | 43 | def subdict_in_parent_dict(self, parent_dict, subdict): 44 | return all( 45 | subitem in parent_dict.items() for subitem in subdict.items()) 46 | 47 | 48 | @patch('oxygen.utils.subprocess') 49 | def test_running_with_passing_environment_variables(self, mock_subprocess): 50 | mock_subprocess.run.return_value = Mock(returncode=0) 51 | self.handler.run_gatling('somefile', 'some command', ENV_VAR='hello') 52 | mock_subprocess.run.assert_called_once_with('some command', 53 | capture_output=True, 54 | shell=True, 55 | env=ANY) 56 | passed_full_env = mock_subprocess.run.call_args[-1]['env'] 57 | self.assertTrue(self.subdict_in_parent_dict(passed_full_env, 58 | {'ENV_VAR': 'hello'})) 59 | 60 | @patch('oxygen.utils.subprocess') 61 | def test_running_fails_correctly(self, mock_subprocess): 62 | mock_subprocess.run.return_value = Mock(returncode=255) 63 | with self.assertRaises(GatlingHandlerException): 64 | self.handler.run_gatling('somefile', 65 | 'some command', 66 | check_return_code=True) 67 | 68 | @patch('oxygen.utils.subprocess') 69 | def test_running_does_not_fail_by_default(self, mock_subprocess): 70 | mock_subprocess.run.return_value = Mock(returncode=255) 71 | retval = self.handler.run_gatling('somefile', 'some command') 72 | self.assertEqual(retval, 'somefile') 73 | 74 | def test_cli(self): 75 | self.assertEqual(self.handler.cli(), BaseHandler.DEFAULT_CLI) 76 | 77 | @patch('oxygen.gatling.GatlingHandler._report_oxygen_run') 78 | def test_check_for_keyword(self, mock_report): 79 | fake_test = example_robot_output().suite.suites[0].tests[2] 80 | expected_data = {'Atest.Test.My Second Test': 'somefile.lol'} 81 | 82 | self.handler.check_for_keyword(fake_test, expected_data) 83 | 84 | self.assertEqual(mock_report.call_args[0][0].name, 85 | 'oxygen.OxygenLibrary.Run Gatling') 86 | self.assertEqual(self.handler.run_time_data, 'somefile.lol') 87 | 88 | def test_gatling_parsing(self): 89 | example_file = RESOURCES_PATH / 'gatling-example-simulation.log' 90 | retval = self.handler._transform_tests(example_file) 91 | compare(retval, GATLING_EXPECTED_OUTPUT) 92 | self.assertTrue(validate_oxygen_suite(retval)) 93 | -------------------------------------------------------------------------------- /tests/utest/junit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/junit/__init__.py -------------------------------------------------------------------------------- /tests/utest/junit/test_basic_functionality.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest import skip, TestCase 3 | from unittest.mock import ANY, create_autospec, Mock, patch 4 | 5 | from junitparser import JUnitXml 6 | from testfixtures import compare 7 | 8 | from oxygen.base_handler import BaseHandler 9 | from oxygen.errors import JUnitHandlerException, ResultFileIsNotAFileException 10 | from oxygen.junit import JUnitHandler 11 | from oxygen.oxygen_handler_result import validate_oxygen_suite 12 | from ..helpers import example_robot_output, get_config, RESOURCES_PATH 13 | 14 | class JUnitBasicTests(TestCase): 15 | 16 | def setUp(self): 17 | self.handler = JUnitHandler(get_config()['oxygen.junit']) 18 | 19 | def test_initialization(self): 20 | self.assertEqual(self.handler.keyword, 'run_junit') 21 | self.assertEqual(self.handler._tags, ['JUNIT', 'EXTRA_JUNIT_CASE']) 22 | 23 | @patch('oxygen.junit.JUnitXml') 24 | @patch('oxygen.junit.JUnitHandler._transform_tests') 25 | @patch('oxygen.junit.validate_path') 26 | def test_parsing(self, mock_validate_path, mock_transform, mock_junitxml): 27 | m = create_autospec(Path) 28 | mock_validate_path.return_value = m 29 | mock_junitxml.fromfile.return_value = 'some junit' 30 | 31 | self.handler.parse_results('some/file/path.ext') 32 | 33 | mock_validate_path.assert_called_once_with('some/file/path.ext') 34 | mock_junitxml.fromfile.assert_called_once_with(m) 35 | mock_transform.assert_called_once_with('some junit') 36 | 37 | def test_result_file_is_not_a_string(self): 38 | with self.assertRaises(ResultFileIsNotAFileException) as ex: 39 | self.handler.parse_results(None) 40 | 41 | ex = str(ex.exception) 42 | self.assertIn('File "None" is not a file', ex) 43 | 44 | @patch('oxygen.utils.subprocess') 45 | def test_running(self, mock_subprocess): 46 | mock_subprocess.run.return_value = Mock(returncode=0) 47 | self.handler.run_junit('somefile', 'some command') 48 | mock_subprocess.run.assert_called_once_with('some command', 49 | capture_output=True, 50 | shell=True, 51 | env=ANY) 52 | 53 | def subdict_in_parent_dict(self, parent_dict, subdict): 54 | return all( 55 | subitem in parent_dict.items() for subitem in subdict.items()) 56 | 57 | @patch('oxygen.utils.subprocess') 58 | def test_running_with_passing_environment_variables(self, mock_subprocess): 59 | mock_subprocess.run.return_value = Mock(returncode=0) 60 | self.handler.run_junit('somefile', 'some command', env_var='value') 61 | mock_subprocess.run.assert_called_once_with('some command', 62 | capture_output=True, 63 | shell=True, 64 | env=ANY) 65 | passed_full_env = mock_subprocess.run.call_args[-1]['env'] 66 | self.assertTrue(self.subdict_in_parent_dict(passed_full_env, 67 | {'env_var': 'value'})) 68 | 69 | @patch('oxygen.utils.subprocess') 70 | def test_running_fails_correctly(self, mock_subprocess): 71 | mock_subprocess.run.return_value = Mock(returncode=255) 72 | with self.assertRaises(JUnitHandlerException): 73 | self.handler.run_junit('somefile', 74 | 'some command', 75 | check_return_code=True) 76 | 77 | @patch('oxygen.utils.subprocess') 78 | def test_running_does_not_fail_by_default(self, mock_subprocess): 79 | mock_subprocess.run.return_value = Mock(returncode=255) 80 | retval = self.handler.run_junit('somefile', 'some command') 81 | self.assertEqual(retval, 'somefile') 82 | 83 | def test_cli(self): 84 | self.assertEqual(self.handler.cli(), BaseHandler.DEFAULT_CLI) 85 | 86 | @patch('oxygen.junit.JUnitHandler._report_oxygen_run') 87 | def test_check_for_keyword(self, mock_report): 88 | fake_test = example_robot_output().suite.suites[0].tests[0] 89 | expected_data = {'Atest.Test.My First Test': '/some/path/to.ext'} 90 | 91 | self.handler.check_for_keyword(fake_test, expected_data) 92 | 93 | self.assertEqual(mock_report.call_args[0][0].name, 94 | 'oxygen.OxygenLibrary.Run Junit') 95 | self.assertEqual(self.handler.run_time_data, '/some/path/to.ext') 96 | 97 | def test_transform_tests_with_single_test_suite(self): 98 | expected_output = { 99 | 'name': 'JUnit Execution', 100 | 'suites': [{'name': 'com.example.demo.DemoApplicationTests', 101 | 'suites': [], 102 | 'tags': [], 103 | 'tests': [{'keywords': [{'elapsed': 454.0, 104 | 'keywords': [], 105 | 'messages': [], 106 | 'name': 'contextLoads() (Execution)', 107 | 'pass': True}], 108 | 'name': 'contextLoads()', 109 | 'tags': []}]}], 110 | 'tags': ['JUNIT', 'EXTRA_JUNIT_CASE'], 111 | } 112 | xml = JUnitXml.fromfile(str(RESOURCES_PATH / 'junit-single-testsuite.xml')) 113 | retval = self.handler._transform_tests(xml) 114 | compare(retval, expected_output) 115 | 116 | def test_transform_tests_with_multiple_suites(self): 117 | expected_output = { 118 | 'name': 'JUnit Execution', 119 | 'tags': ['JUNIT', 'EXTRA_JUNIT_CASE'], 120 | 'suites': [{ 121 | 'name': 'suite1', 122 | 'tags': [], 123 | 'suites': [{ 124 | 'name': 'suite2', 125 | 'tags': [], 126 | 'suites': [], 127 | 'tests': [{ 128 | 'name': 'casea', 129 | 'tags': ['oxygen-junit-unknown-execution-time'], 130 | 'keywords': [{'name': 'casea (Execution)', 131 | 'pass': True, 132 | 'messages': [], 133 | 'keywords': [], 134 | 'elapsed': 0.0}] 135 | }, { 136 | 'name': 'caseb', 137 | 'tags': ['oxygen-junit-unknown-execution-time'], 138 | 'keywords': [{ 139 | 'name': 'caseb (Execution)', 140 | 'pass': True, 141 | 'messages': [], 142 | 'elapsed': 0.0, 143 | 'keywords': [] 144 | }] 145 | }] 146 | }], 147 | 'tests': [{ 148 | 'name': 'case1', 149 | 'tags': ['oxygen-junit-unknown-execution-time'], 150 | 'keywords': [{ 151 | 'name': 'case1 (Execution)', 152 | 'pass': True, 153 | 'messages': [], 154 | 'keywords': [], 155 | 'elapsed': 0.0 156 | }] 157 | }, { 158 | 'name': 'case2', 159 | 'tags': ['oxygen-junit-unknown-execution-time'], 160 | 'keywords': [{ 161 | 'name': 'case2 (Execution)', 162 | 'pass': False, 163 | 'messages': [ 164 | 'ERROR: Example error message (the_error_type)' 165 | ], 166 | 'keywords': [], 167 | 'elapsed': 0.0 168 | }] 169 | }, { 170 | 'name': 'case3', 171 | 'tags': ['oxygen-junit-unknown-execution-time'], 172 | 'keywords': [{ 173 | 'name': 'case3 (Execution)', 174 | 'pass': False, 175 | 'messages': [ 176 | 'FAIL: Example failure message (the_failure_type)' 177 | ], 178 | 'keywords': [], 179 | 'elapsed': 0.0 180 | }] 181 | }] 182 | }], 183 | } 184 | xml = JUnitXml.fromfile(RESOURCES_PATH / 'junit.xml') 185 | retval = self.handler._transform_tests(xml) 186 | compare(retval, expected_output) 187 | self.assertTrue(validate_oxygen_suite(retval)) 188 | -------------------------------------------------------------------------------- /tests/utest/my_dummy_handler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/my_dummy_handler/__init__.py -------------------------------------------------------------------------------- /tests/utest/my_dummy_handler/test_basic_functionality.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import TestCase 3 | from oxygen.errors import MismatchArgumentException 4 | from ..helpers import RESOURCES_PATH, get_config, example_robot_output 5 | 6 | sys.path.append(str(RESOURCES_PATH / 'my_dummy_handlers')) 7 | 8 | from dummy_handler_single_arg import ( 9 | MyDummyHandler as DummyHandlerSingleArg, 10 | ) 11 | from dummy_handler_multiple_args import ( 12 | MyDummyHandler as DummyHandlerMultipleArgs, 13 | ) 14 | from dummy_handler_multiple_args_too_few import ( 15 | MyDummyHandler as DummyHandlerMultipleArgsTooFew, 16 | ) 17 | from dummy_handler_default_params import ( 18 | MyDummyHandler as DummyHandlerDefaultParams, 19 | ) 20 | 21 | 22 | class DummyHandlerSingleArgTests(TestCase): 23 | ''' 24 | A test for passing tuple if parse_results accepts one parameter 25 | ''' 26 | 27 | def setUp(self): 28 | self.handler = DummyHandlerSingleArg( 29 | get_config()['oxygen.my_dummy_handler'] 30 | ) 31 | 32 | def test_run_my_dummy_handler(self): 33 | return_value = self.handler.run_my_dummy_handler('/some/path/to.ext') 34 | self.assertTupleEqual(return_value, ('/some/path/to.ext', 'foo')) 35 | 36 | def test_parse_results(self): 37 | fake_test = example_robot_output().suite.suites[0].tests[6] 38 | expected_data = {'Atest.Test.My Fifth Test': '/some/path/to.ext'} 39 | 40 | self.handler.check_for_keyword(fake_test, expected_data) 41 | 42 | self.assertEqual(self.handler.run_time_data, '/some/path/to.ext') 43 | 44 | 45 | class DummyHandlerMultipleArgsTests(TestCase): 46 | ''' 47 | A test for unfolding parse_results arguments 48 | if it has multiple parameters 49 | ''' 50 | 51 | def setUp(self): 52 | self.handler = DummyHandlerMultipleArgs( 53 | get_config()['oxygen.my_dummy_handler'] 54 | ) 55 | 56 | def test_parse_results(self): 57 | fake_test = example_robot_output().suite.suites[0].tests[6] 58 | expected_data = { 59 | 'Atest.Test.My Fifth Test': ('/some/path/to.ext', 'foo') 60 | } 61 | 62 | self.handler.check_for_keyword(fake_test, expected_data) 63 | 64 | self.assertEqual( 65 | self.handler.run_time_data, ('/some/path/to.ext', 'foo') 66 | ) 67 | 68 | 69 | class DummyHandlerMultipleArgsTooFewTests(TestCase): 70 | ''' 71 | A test for testing if it throws mismatch argument exception because 72 | parse_results expects too many arguments 73 | ''' 74 | 75 | def setUp(self): 76 | self.handler = DummyHandlerMultipleArgsTooFew( 77 | get_config()['oxygen.my_dummy_handler'] 78 | ) 79 | 80 | def test_parse_results(self): 81 | fake_test = example_robot_output().suite.suites[0].tests[6] 82 | expected_data = { 83 | 'Atest.Test.My Fifth Test': ('/some/path/to.ext', 'foo') 84 | } 85 | 86 | self.assertRaises( 87 | MismatchArgumentException, 88 | self.handler.check_for_keyword, 89 | fake_test, 90 | expected_data, 91 | ) 92 | 93 | 94 | class DummyHandlerMultipleArgsSingleTests(TestCase): 95 | ''' 96 | A test for testing if it throws mismatch argument exception because 97 | parse_results expects multiple arguments but we do not pass multiple 98 | ''' 99 | 100 | def setUp(self): 101 | self.handler = DummyHandlerMultipleArgsTooFew( 102 | get_config()['oxygen.my_dummy_handler'] 103 | ) 104 | 105 | def test_parse_results(self): 106 | fake_test = example_robot_output().suite.suites[0].tests[6] 107 | expected_data = {'Atest.Test.My Fifth Test': 'some/path/to.ext'} 108 | 109 | self.assertRaises( 110 | MismatchArgumentException, 111 | self.handler.check_for_keyword, 112 | fake_test, 113 | expected_data, 114 | ) 115 | 116 | 117 | class DummyHandlerDefaultParamsTests(TestCase): 118 | ''' 119 | A test for testing arguments with defaults 120 | ''' 121 | 122 | def setUp(self): 123 | self.handler = DummyHandlerDefaultParams( 124 | get_config()['oxygen.my_dummy_handler'] 125 | ) 126 | 127 | def test_parse_results_with_one(self): 128 | fake_test = example_robot_output().suite.suites[0].tests[6] 129 | expected_data = {'Atest.Test.My Fifth Test': 'some/path/to.ext'} 130 | self.handler.check_for_keyword(fake_test, expected_data) 131 | self.assertEqual(self.handler.run_time_data, 'some/path/to.ext') 132 | 133 | def test_parse_results_with_multiple(self): 134 | fake_test = example_robot_output().suite.suites[0].tests[6] 135 | expected_data = { 136 | 'Atest.Test.My Fifth Test': ('some/path/to.ext', 'foo')} 137 | self.handler.check_for_keyword(fake_test, expected_data) 138 | self.assertTupleEqual( 139 | self.handler.run_time_data, ('some/path/to.ext', 'foo')) 140 | 141 | def test_parse_results_with_too_many(self): 142 | fake_test = example_robot_output().suite.suites[0].tests[6] 143 | expected_data = { 144 | 'Atest.Test.My Fifth Test': ('some/path/to.ext', 'foo', 'bar')} 145 | self.assertRaises( 146 | MismatchArgumentException, 147 | self.handler.check_for_keyword, 148 | fake_test, 149 | expected_data, 150 | ) 151 | -------------------------------------------------------------------------------- /tests/utest/oxygen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/oxygen/__init__.py -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygen_cli.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser, Namespace 2 | from pathlib import Path 3 | from subprocess import check_output, run, STDOUT, CalledProcessError 4 | from tempfile import mkstemp 5 | from unittest import TestCase 6 | from unittest.mock import ANY, create_autospec, patch, Mock 7 | from xml.etree import ElementTree 8 | 9 | from robot.running.model import TestSuite 10 | 11 | from oxygen.oxygen import OxygenCLI, OxygenCore 12 | from oxygen.config import CONFIG_FILE, ORIGINAL_CONFIG_FILE 13 | 14 | from ..helpers import RESOURCES_PATH 15 | 16 | 17 | class TestOxygenCLIEntryPoints(TestCase): 18 | '''Coverage does not measure coverage correctly for these tests. 19 | 20 | We have tests for __main__ entrypoint below, but Coverage is unable to 21 | do its measurements as they are running in subprocesses. They are run in 22 | subprocesses to simulate real command line usage. 23 | 24 | Setting up Coverage to see subprocesses as well seems a lot of work and 25 | quite a hack: https://coverage.readthedocs.io/en/latest/subprocess.html 26 | ''' 27 | 28 | @classmethod 29 | def tearDownClass(cls): 30 | with open(ORIGINAL_CONFIG_FILE, 'r') as og: 31 | with open(CONFIG_FILE, 'w') as config: 32 | config.write(og.read()) 33 | 34 | def tearDown(self): 35 | self.tearDownClass() 36 | 37 | def test_main_level_entrypoint(self): 38 | self.verify_cli_help_text('python -m oxygen --help') 39 | self.verify_cli_help_text('python -m oxygen -h') 40 | 41 | def test_direct_module_entrypoint(self): 42 | self.verify_cli_help_text('python -m oxygen.oxygen --help') 43 | self.verify_cli_help_text('python -m oxygen.oxygen -h') 44 | 45 | def test_cli_with_no_args(self): 46 | proc = run('python -m oxygen', 47 | shell=True, 48 | text=True, 49 | capture_output=True) 50 | self.assertEqual(proc.returncode, 2) 51 | self.assertIn('usage: oxygen', proc.stderr) 52 | 53 | def _run(self, cmd): 54 | try: 55 | return check_output(cmd, text=True, shell=True, stderr=STDOUT) 56 | except CalledProcessError as e: 57 | print(e.output) # with this, you can actually see the command 58 | raise # output, ie. why it failed 59 | 60 | def verify_cli_help_text(self, cmd): 61 | out = self._run(cmd) 62 | self.assertIn('usage: oxygen', out) 63 | self.assertIn('-h, --help', out) 64 | 65 | def test_junit_works_on_cli(self): 66 | target = RESOURCES_PATH / 'green-junit-example.xml' 67 | example = target.with_name('green-junit-expected-robot-output.xml') 68 | actual = target.with_name('green-junit-example_robot_output.xml') 69 | if actual.exists(): 70 | actual.unlink() # delete file if exists 71 | 72 | self._run(f'python -m oxygen oxygen.junit {target}') 73 | 74 | example_xml = ElementTree.parse(example).getroot() 75 | actual_xml = ElementTree.parse(actual).getroot() 76 | 77 | # RF<4 has multiple `stat` elements therefore we use the double slash 78 | # and a filter to single out the `stat` element with text "All Tests" 79 | all_tests_stat_block = './statistics/total//stat[.="All Tests"]' 80 | 81 | example_stat = example_xml.find(all_tests_stat_block) 82 | actual_stat = actual_xml.find(all_tests_stat_block) 83 | 84 | self.assertEqual(example_stat.get('pass'), actual_stat.get('pass')) 85 | self.assertEqual(example_stat.get('fail'), actual_stat.get('fail')) 86 | 87 | def _validate_handler_names(self, text): 88 | for handler in ('JUnitHandler', 'GatlingHandler', 'ZAProxyHandler'): 89 | self.assertIn(handler, text) 90 | 91 | def test_reset_config(self): 92 | with open(CONFIG_FILE, 'w') as f: 93 | f.write('complete: gibberish') 94 | 95 | self._run(f'python -m oxygen --reset-config') 96 | 97 | with open(CONFIG_FILE, 'r') as f: 98 | config_content = f.read() 99 | self.assertNotIn('complete: gibberish', config_content) 100 | self._validate_handler_names(config_content) 101 | 102 | def test_print_config(self): 103 | out = self._run('python -m oxygen --print-config') 104 | 105 | self.assertIn('Using config file', out) 106 | self._validate_handler_names(out) 107 | 108 | def _make_test_config(self): 109 | _, filepath = mkstemp() 110 | with open(filepath, 'w') as f: 111 | f.write('complete: gibberish') 112 | return filepath 113 | 114 | def test_add_config(self): 115 | filepath = self._make_test_config() 116 | 117 | self._run(f'python -m oxygen --add-config {filepath}') 118 | 119 | with open(CONFIG_FILE, 'r') as f: 120 | config_content = f.read() 121 | self._validate_handler_names(config_content) 122 | self.assertIn('complete: gibberish', config_content) 123 | 124 | def _is_file_content(self, filepath, text): 125 | with open(filepath, 'r') as f: 126 | return bool(text in f.read()) 127 | 128 | def test_main_level_args_override_handler_args(self): 129 | filepath = self._make_test_config() 130 | 131 | cmd = ('python -m oxygen {main_level_arg} ' 132 | f'oxygen.junit {RESOURCES_PATH / "green-junit-example.xml"}') 133 | 134 | self._run(cmd.format(main_level_arg=f'--add-config {filepath}')) 135 | self.assertTrue(self._is_file_content(CONFIG_FILE, 'complete: gibberish')) 136 | 137 | self._run(cmd.format(main_level_arg='--reset-config')) 138 | self.assertFalse(self._is_file_content(CONFIG_FILE, 139 | 'complete: gibberish')) 140 | 141 | 142 | out = self._run(cmd.format(main_level_arg='--print-config')) 143 | self._validate_handler_names(out) 144 | self.assertNotIn('gibberish', out) 145 | 146 | 147 | class TestOxygenCLI(TestCase): 148 | 149 | def setUp(self): 150 | self.cli = OxygenCLI() 151 | 152 | @patch('oxygen.oxygen.RobotInterface') 153 | @patch('oxygen.oxygen.OxygenCLI.parse_args') 154 | def test_run(self, mock_parse_args, mock_robot_iface): 155 | mock_parse_args.return_value = { 156 | 'result_file': 'path/to/file.xml', 157 | 'func': lambda *_, **__: {'some': 'results'}} 158 | expected_suite = create_autospec(TestSuite) 159 | mock = Mock() 160 | mock.running.build_suite = Mock(return_value=expected_suite) 161 | mock_robot_iface.return_value = mock 162 | 163 | self.cli.run() 164 | 165 | mock.running.build_suite.assert_called_once_with({'some': 'results'}) 166 | expected_suite.run.assert_called_once_with( 167 | output=str(Path('path/to/file_robot_output.xml')), 168 | log=None, 169 | report=None, 170 | stdout=ANY 171 | ) 172 | 173 | def test_parse_args(self): 174 | '''verifies that `parse_args()` returns a dictionary''' 175 | p = create_autospec(ArgumentParser) 176 | p.parse_args.return_value = create_autospec(Namespace) 177 | 178 | retval = self.cli.parse_args(p) 179 | 180 | self.assertIsInstance(retval, dict) 181 | 182 | def test_add_arguments(self): 183 | mock_parser = create_autospec(ArgumentParser) 184 | m = Mock() 185 | mock_parser.add_subparsers.return_value = m 186 | 187 | self.cli.add_arguments(mock_parser) 188 | 189 | # verify all main-level cli arguments were added 190 | self.assertEqual(len(mock_parser.add_argument.call_args_list), 4) 191 | # verify all built-in handlers were added 192 | self.assertEqual(len(m.add_parser.call_args_list), 3) 193 | 194 | def _actual(self, path): 195 | return self.cli.get_output_filename(path) 196 | 197 | def _expected(self, path): 198 | return str(Path(path)) 199 | 200 | def test_get_output_filename(self): 201 | for act, exp in ((self._actual('/absolute/path/to.file'), 202 | self._expected('/absolute/path/to_robot_output.xml')), 203 | 204 | (self._actual('path/to/file.xml'), 205 | self._expected('path/to/file_robot_output.xml')), 206 | 207 | (self._actual('file.extension'), 208 | self._expected('file_robot_output.xml'))): 209 | self.assertEqual(act, exp) 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygen_config_file.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.config import CONFIG_FILE, ORIGINAL_CONFIG_FILE 4 | 5 | class TestOxygenCLIEntryPoints(TestCase): 6 | def test_config_and_config_original_match(self): 7 | with open(CONFIG_FILE, 'r') as config: 8 | with open(ORIGINAL_CONFIG_FILE, 'r') as original_config: 9 | self.assertEqual(config.read(), original_config.read()) 10 | -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygen_core.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from oxygen.oxygen import OxygenCore 3 | 4 | 5 | class TestOxygenInitialization(TestCase): 6 | def test_oxygen_core_initializes_without_loading_config(self): 7 | ''' 8 | OxygenCore and all it's subclasses lazy-load the configuration and, 9 | consequently, the handlers. This test makes sure that is not 10 | accidentally changed at some point 11 | ''' 12 | core = OxygenCore() 13 | self.assertEqual(core._config, None) 14 | self.assertEqual(core._handlers, None) 15 | 16 | -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygen_listener.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import Mock, patch 3 | 4 | from oxygen import listener 5 | 6 | class OxygenListenerBasicTests(TestCase): 7 | def setUp(self): 8 | self.listener = listener() 9 | 10 | def test_listener_api_version_is_not_changed_accidentally(self): 11 | self.assertEqual(self.listener.ROBOT_LISTENER_API_VERSION, 2) 12 | 13 | def mock_lib_instance(self, mock_builtin, return_value): 14 | m_builtin = Mock() 15 | m_get_context = Mock() 16 | m_builtin._get_context.return_value = m_get_context 17 | m_get_context.namespace.get_library_instance.return_value = \ 18 | return_value 19 | mock_builtin.return_value = m_builtin 20 | return m_builtin 21 | 22 | @patch('oxygen.oxygen.BuiltIn') 23 | def test_end_test_when_library_was_not_used(self, mock_builtin): 24 | m = self.mock_lib_instance(mock_builtin, None) 25 | 26 | self.listener.end_test('foo', {}) 27 | 28 | m._get_context().namespace.get_library_instance.assert_called_once_with('oxygen.OxygenLibrary') 29 | self.assertEqual(self.listener.run_time_data, {}) 30 | 31 | @patch('oxygen.oxygen.BuiltIn') 32 | def test_end_test_when_library_was_used(self, mock_builtin): 33 | o = lambda: None 34 | o.data = 'I do not have a solution, but I do admire the problem' 35 | m = self.mock_lib_instance(mock_builtin, o) 36 | 37 | self.listener.end_test('oxygen.OxygenLibrary', {'longname': 'hello'}) 38 | 39 | m._get_context().namespace.get_library_instance.assert_called_once_with('oxygen.OxygenLibrary') 40 | self.assertEqual(self.listener.run_time_data, 41 | {'hello': ('I do not have a solution, but I do ' 42 | 'admire the problem')}) 43 | -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygen_visitor.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import Mock 3 | 4 | from oxygen.oxygen import OxygenVisitor 5 | from oxygen import errors as oxygen_errors 6 | 7 | 8 | class CustomUserException(Exception): 9 | pass 10 | 11 | 12 | class TestOxygen(TestCase): 13 | def test_multiple_errors_are_reported(self): 14 | m = Mock() 15 | m.check_for_keyword.side_effect = [ 16 | TypeError('different'), 17 | oxygen_errors.JUnitHandlerException('kinds of'), 18 | oxygen_errors.OxygenException('exceptions'), 19 | CustomUserException('for fun and profit') 20 | ] 21 | oxy = OxygenVisitor('fakedata') 22 | oxy._handlers = { 23 | 'fake_handler': m, 24 | 'another_fake': m, 25 | 'third': m, 26 | 'and fourth': m 27 | } 28 | 29 | with self.assertRaises(oxygen_errors.OxygenException) as ex: 30 | oxy.visit_test(Mock()) 31 | 32 | exception_message = str(ex.exception) 33 | for expected in ('different', 34 | 'kinds of', 35 | 'exceptions', 36 | 'for fun and profit'): 37 | self.assertIn(expected, exception_message) 38 | 39 | def test_single_exception_raised_directly(self): 40 | m = Mock() 41 | m.check_for_keyword.side_effect = [CustomUserException('single')] 42 | oxy = OxygenVisitor('fakedata') 43 | oxy._handlers = {'fake_handler': m} 44 | 45 | with self.assertRaises(CustomUserException) as ex: 46 | oxy.visit_test(Mock()) 47 | 48 | self.assertIn('single', str(ex.exception)) 49 | self.assertEqual(oxy.data, 'fakedata') 50 | -------------------------------------------------------------------------------- /tests/utest/oxygen/test_oxygenlibrary.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, Mock 3 | 4 | from oxygen import BaseHandler, OxygenLibrary 5 | from oxygen.errors import OxygenException 6 | 7 | from ..helpers import get_config_as_file 8 | 9 | class TestOxygenLibrary(TestCase): 10 | EXPECTED_KEYWORDS = ('run_junit', 'run_gatling', 'run_zap') 11 | 12 | def setUp(self): 13 | self.lib = OxygenLibrary() 14 | 15 | 16 | def test_initialization(self): 17 | for kw in self.EXPECTED_KEYWORDS: 18 | self.assertIn(kw, self.lib.get_keyword_names()) 19 | 20 | @patch('oxygen.config.CONFIG_FILE') 21 | def test_config_is_correct(self, mock_config): 22 | mock_config.return_value = get_config_as_file() 23 | 24 | self.assertGreater(len(self.lib.handlers), 1) 25 | for handler in self.lib.handlers.values(): 26 | self.assertTrue(isinstance(handler, BaseHandler)) 27 | self.assertTrue(any(hasattr(handler, kw) 28 | for kw in self.EXPECTED_KEYWORDS), 29 | 'handler "{}" did not have any of the ' 30 | 'following {}'.format(handler.__class__, 31 | self.EXPECTED_KEYWORDS)) 32 | 33 | def test_documentation(self): 34 | for kw in self.EXPECTED_KEYWORDS: 35 | self.assertNotNoneOrEmpty(self.lib.get_keyword_documentation(kw)) 36 | # Libdoc specific values 37 | self.assertNotNoneOrEmpty(self.lib.get_keyword_documentation('__intro__')) 38 | 39 | def assertNotNoneOrEmpty(self, value): 40 | assert value != None or value == '', \ 41 | 'Value "{}" is None or empty'.format(value) 42 | 43 | def test_arguments(self): 44 | for kw in self.EXPECTED_KEYWORDS: 45 | args = self.lib.get_keyword_arguments(kw) 46 | self.assertIsInstance(args, list) 47 | self.assertGreater(len(args), 0) 48 | 49 | @patch('oxygen.OxygenLibrary._fetch_handler') 50 | def test_run_keyword(self, mock_fetch_handler): 51 | expected_data = {f'{attr}.return_value': 'somefile.ext' 52 | for attr in self.EXPECTED_KEYWORDS} 53 | m = Mock() 54 | m.configure_mock(**expected_data) 55 | mock_fetch_handler.side_effect = [m]*3 56 | 57 | for kw in self.EXPECTED_KEYWORDS: 58 | ret = self.lib.run_keyword(kw, ['somefile.ext'], {}) 59 | self.assertEqual(ret, 'somefile.ext') 60 | self.assertNotNoneOrEmpty(self.lib.data) 61 | 62 | def test_run_keyword_should_fail_if_nonexistent_kw_is_called(self): 63 | with self.assertRaises(OxygenException) as ex: 64 | self.lib.run_keyword('nonexistent', [], {}) 65 | 66 | self.assertIn('No handler for keyword', str(ex.exception)) 67 | -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/oxygen_handler_result/__init__.py -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/shared_tests.py: -------------------------------------------------------------------------------- 1 | from ..helpers import (MINIMAL_KEYWORD_DICT, 2 | _ListSubclass, 3 | _StrSubclass, 4 | _KwSubclass) 5 | 6 | class SharedTestsForName(object): 7 | def shared_test_for_name(self): 8 | valid_inherited = _StrSubclass('someKeyword') 9 | this_is_not_None = _StrSubclass(None) 10 | 11 | self.valid_inputs_for('name', 12 | '', 13 | 'someKeyword', 14 | b'someKeyword', 15 | valid_inherited, 16 | this_is_not_None) 17 | 18 | self.invalid_inputs_for('name', None) 19 | 20 | 21 | class SharedTestsForTags(object): 22 | def shared_test_for_tags(self): 23 | self.valid_inputs_for('tags', 24 | [], 25 | ['some-tag', 'another-tag'], 26 | _ListSubclass()) 27 | 28 | invalid_inherited = _ListSubclass() 29 | invalid_inherited.append(123) 30 | 31 | self.invalid_inputs_for('tags', [123], None, {'foo': 'bar'}, object()) 32 | 33 | 34 | class SharedTestsForKeywordField(object): 35 | def shared_test_for_keyword_field(self, attribute): 36 | valid_inherited = _KwSubclass(**MINIMAL_KEYWORD_DICT) 37 | 38 | self.valid_inputs_for(attribute, 39 | MINIMAL_KEYWORD_DICT, 40 | valid_inherited, 41 | {**MINIMAL_KEYWORD_DICT, 42 | 'something_random': 'will-be-ignored'}) 43 | 44 | self.invalid_inputs_for(attribute, None, {}) 45 | -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/test_OxygenKeywordDict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.errors import InvalidOxygenResultException 4 | from oxygen.oxygen_handler_result import (validate_oxygen_keyword, 5 | OxygenKeywordDict) 6 | 7 | from ..helpers import (MINIMAL_KEYWORD_DICT, 8 | _ListSubclass, 9 | _KwSubclass) 10 | from .shared_tests import (SharedTestsForKeywordField, 11 | SharedTestsForName, 12 | SharedTestsForTags) 13 | 14 | 15 | class TestOxygenKeywordDict(TestCase, 16 | SharedTestsForName, 17 | SharedTestsForTags, 18 | SharedTestsForKeywordField): 19 | def setUp(self): 20 | self.minimal = MINIMAL_KEYWORD_DICT 21 | 22 | def test_validate_oxygen_keyword_validates_correctly(self): 23 | with self.assertRaises(InvalidOxygenResultException): 24 | validate_oxygen_keyword({}) 25 | 26 | def test_validate_oxygen_keyword_with_minimal_valid(self): 27 | minimal1 = { 'name': 'somename', 'pass': True } 28 | minimal2 = { 'name': 'somename', 'pass': False } 29 | 30 | self.assertEqual(validate_oxygen_keyword(minimal1), minimal1) 31 | self.assertEqual(validate_oxygen_keyword(minimal2), minimal2) 32 | 33 | def valid_inputs_for(self, attribute, *valid_inputs): 34 | for valid_input in valid_inputs: 35 | self.assertTrue(validate_oxygen_keyword({**self.minimal, 36 | attribute: valid_input})) 37 | 38 | def invalid_inputs_for(self, attribute, *invalid_inputs): 39 | for invalid_input in invalid_inputs: 40 | with self.assertRaises(InvalidOxygenResultException): 41 | validate_oxygen_keyword({**self.minimal, 42 | attribute: invalid_input}) 43 | 44 | def test_validate_oxygen_keyword_validates_name(self): 45 | self.shared_test_for_name() 46 | 47 | def test_validate_oxygen_keyword_validates_pass(self): 48 | ''' 49 | Due note that boolean cannot be subclassed in Python: 50 | https://mail.python.org/pipermail/python-dev/2002-March/020822.html 51 | ''' 52 | self.valid_inputs_for('pass', True, False, 0, 1, 0.0, 1.0) 53 | self.invalid_inputs_for('pass', [], {}, None, object(), -999, -99.9) 54 | 55 | def test_validate_oxygen_keyword_validates_tags(self): 56 | self.shared_test_for_tags() 57 | 58 | def test_validate_oxygen_keyword_validates_elapsed(self): 59 | class FloatSubclass(float): 60 | pass 61 | 62 | self.valid_inputs_for('elapsed', 63 | 123.4, 64 | -123.0, 65 | '123.4', 66 | '-999.999', 67 | 123, 68 | FloatSubclass()) 69 | 70 | self.invalid_inputs_for('elapsed', '', None, object()) 71 | 72 | def test_validate_oxygen_keyword_validates_messages(self): 73 | valid_inherited = _ListSubclass() 74 | valid_inherited.append('message') 75 | 76 | self.valid_inputs_for('messages', 77 | [], 78 | ['message'], 79 | _ListSubclass(), 80 | valid_inherited) 81 | 82 | invalid_inherited = _ListSubclass() 83 | invalid_inherited.append('message') 84 | invalid_inherited.append(123) 85 | 86 | self.invalid_inputs_for('messages', 87 | 'some,messages', 88 | None, 89 | invalid_inherited) 90 | 91 | def test_validate_oxygen_keyword_validates_teardown(self): 92 | self.shared_test_for_keyword_field('teardown') 93 | 94 | def test_validate_oxygen_keyword_validates_keywords(self): 95 | valid_inherited = _ListSubclass() 96 | valid_inherited.append(_KwSubclass(**self.minimal)) 97 | 98 | self.valid_inputs_for('keywords', 99 | [], 100 | [self.minimal, {**self.minimal, 101 | 'something_random': 'will-be-ignored'}], 102 | _ListSubclass(), # empty inherited list 103 | valid_inherited) 104 | 105 | invalid_inherited = _ListSubclass() 106 | invalid_inherited.append(_KwSubclass(**self.minimal)) 107 | invalid_inherited.append(123) 108 | self.invalid_inputs_for('keywords', None, invalid_inherited) 109 | 110 | def test_validate_oxygen_keyword_with_maximal_valid(self): 111 | expected = { 112 | 'name': 'keyword', 113 | 'pass': True, 114 | 'tags': ['some-tag'], 115 | 'messages': ['some message'], 116 | 'teardown': { 117 | 'name': 'teardownKeyword', 118 | 'pass': True, 119 | 'tags': ['teardown-kw'], 120 | 'messages': ['Teardown passed'], 121 | 'keywords': [] 122 | }, 123 | 'keywords': [{ 124 | 'name': 'subKeyword', 125 | 'pass': False, 126 | # tags missing intentionally 127 | 'messages': ['This particular kw failed'], 128 | 'teardown': { 129 | 'name': 'anotherTeardownKw', 130 | 'pass': True, 131 | 'tags': ['teardown-kw'], 132 | 'messages': ['message from anotherTeardownKw'], 133 | # teardown missing intentionally 134 | 'keywords': [] 135 | }, 136 | 'keywords': [{ 137 | 'name': 'subsubKeyword', 138 | 'pass': True, 139 | }] 140 | },{ 141 | 'name': 'anotherSubKeyword', 142 | 'pass': True, 143 | 'tags': [], 144 | 'messages': [], 145 | 'keywords': [] 146 | }] 147 | } 148 | 149 | self.assertEqual(validate_oxygen_keyword(expected), expected) 150 | -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/test_OxygenSuiteDict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.errors import InvalidOxygenResultException 4 | from oxygen.oxygen_handler_result import OxygenSuiteDict, validate_oxygen_suite 5 | 6 | from ..helpers import (MINIMAL_TC_DICT, 7 | MINIMAL_SUITE_DICT, 8 | _ListSubclass, 9 | _StrSubclass, 10 | _TCSubclass) 11 | from .shared_tests import (SharedTestsForName, 12 | SharedTestsForKeywordField, 13 | SharedTestsForTags) 14 | 15 | class TestOxygenSuiteDict(TestCase, 16 | SharedTestsForName, 17 | SharedTestsForKeywordField, 18 | SharedTestsForTags): 19 | def setUp(self): 20 | self.minimal = MINIMAL_SUITE_DICT 21 | 22 | def test_validate_oxygen_suite_validates_correctly(self): 23 | with self.assertRaises(InvalidOxygenResultException): 24 | validate_oxygen_suite({}) 25 | 26 | def test_validate_oxygen_suite_with_minimal_valid(self): 27 | expected = { 28 | 'name': 'My Suite' 29 | } 30 | 31 | self.assertEqual(validate_oxygen_suite(expected), expected) 32 | self.assertEqual(validate_oxygen_suite(self.minimal), self.minimal) 33 | 34 | def valid_inputs_for(self, attribute, *valid_inputs): 35 | for valid_input in valid_inputs: 36 | self.assertTrue(validate_oxygen_suite({**self.minimal, 37 | attribute: valid_input})) 38 | 39 | def invalid_inputs_for(self, attribute, *invalid_inputs): 40 | for invalid_input in invalid_inputs: 41 | with self.assertRaises(InvalidOxygenResultException): 42 | validate_oxygen_suite({**self.minimal, attribute: invalid_input}) 43 | 44 | def test_validate_oxygen_suite_validates_name(self): 45 | self.shared_test_for_name() 46 | 47 | def test_validate_oxygen_suite_validates_tags(self): 48 | self.shared_test_for_tags() 49 | 50 | def test_validate_oxygen_suite_validates_setup(self): 51 | self.shared_test_for_keyword_field('setup') 52 | 53 | def test_validate_oxygen_suite_validates_teardown(self): 54 | self.shared_test_for_keyword_field('teardown') 55 | 56 | def test_validate_oxygen_suite_validates_suites(self): 57 | class OxygenSuiteDictSubclass(OxygenSuiteDict): 58 | pass 59 | valid_inherited = _ListSubclass() 60 | valid_inherited.append(OxygenSuiteDictSubclass(**self.minimal)) 61 | 62 | self.valid_inputs_for('suites', 63 | [], 64 | [self.minimal], 65 | valid_inherited, 66 | [ OxygenSuiteDictSubclass(**self.minimal) ]) 67 | 68 | self.invalid_inputs_for('suites', None, [ {} ]) 69 | 70 | def test_validate_oxygen_suite_validates_tests(self): 71 | valid_inherited = _ListSubclass() 72 | valid_inherited.append(_TCSubclass(**MINIMAL_TC_DICT)) 73 | valid_inherited.append(MINIMAL_TC_DICT) 74 | 75 | self.valid_inputs_for('tests', 76 | [], 77 | [ MINIMAL_TC_DICT ], 78 | valid_inherited, 79 | [ _TCSubclass(**MINIMAL_TC_DICT) ]) 80 | 81 | 82 | self.invalid_inputs_for('tests', None, [ {} ]) 83 | 84 | def test_validate_oxygen_suite_validates_metadata(self): 85 | class DictSubclass(dict): 86 | pass 87 | inherited_key = _StrSubclass('key') 88 | 89 | this_is_not_None = _StrSubclass(None) 90 | 91 | self.valid_inputs_for('metadata', 92 | {}, 93 | {'': ''}, 94 | {'key': 'value'}, 95 | {_StrSubclass('key'): _StrSubclass('value')}, 96 | DictSubclass(inherited_key=_StrSubclass('value')), 97 | {this_is_not_None: 'value'}) 98 | 99 | self.invalid_inputs_for('metadata', 100 | {'key': None}, 101 | {'key': 1234},) 102 | -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/test_OxygenTestCaseDict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from oxygen.errors import InvalidOxygenResultException 4 | from oxygen.oxygen_handler_result import validate_oxygen_test_case 5 | 6 | from ..helpers import _ListSubclass, MINIMAL_KEYWORD_DICT, MINIMAL_TC_DICT 7 | from .shared_tests import (SharedTestsForKeywordField, 8 | SharedTestsForName, 9 | SharedTestsForTags) 10 | 11 | class TestOxygenTestCaseDict(TestCase, 12 | SharedTestsForName, 13 | SharedTestsForTags, 14 | SharedTestsForKeywordField): 15 | def setUp(self): 16 | self.minimal = MINIMAL_TC_DICT 17 | 18 | def test_validate_oxygen_tc_validates_correctly(self): 19 | with self.assertRaises(InvalidOxygenResultException): 20 | validate_oxygen_test_case({}) 21 | 22 | def test_validate_oxygen_tc_with_minimal_valid(self): 23 | expected = { 24 | 'name': 'My TC', 25 | 'keywords': [] 26 | } 27 | self.assertEqual(validate_oxygen_test_case(expected), expected) 28 | self.assertEqual(validate_oxygen_test_case(self.minimal), self. minimal) 29 | 30 | def valid_inputs_for(self, attribute, *valid_inputs): 31 | for valid_input in valid_inputs: 32 | self.assertTrue(validate_oxygen_test_case({**self.minimal, 33 | attribute: valid_input})) 34 | 35 | def invalid_inputs_for(self, attribute, *invalid_inputs): 36 | for invalid_input in invalid_inputs: 37 | with self.assertRaises(InvalidOxygenResultException): 38 | validate_oxygen_test_case({**self.minimal, 39 | attribute: invalid_input}) 40 | 41 | def test_validate_oxygen_tc_validates_name(self): 42 | self.shared_test_for_name() 43 | 44 | def test_validate_oxygen_tc_validates_keywords(self): 45 | valid_inherited = _ListSubclass() 46 | valid_inherited.append(MINIMAL_KEYWORD_DICT) 47 | 48 | self.valid_inputs_for('keywords', 49 | [], 50 | [ MINIMAL_KEYWORD_DICT ], 51 | _ListSubclass(), 52 | valid_inherited) 53 | 54 | invalid_inherited = _ListSubclass() 55 | invalid_inherited.append( {} ) 56 | 57 | self.invalid_inputs_for('keywords', None, invalid_inherited) 58 | 59 | def test_validate_oxygen_tc_validates_tags(self): 60 | self.shared_test_for_tags() 61 | 62 | def test_validate_oxygen_tc_validates_setup(self): 63 | self.shared_test_for_keyword_field('setup') 64 | 65 | def test_validate_oxygen_tc_validates_teardown(self): 66 | self.shared_test_for_keyword_field('teardown') 67 | -------------------------------------------------------------------------------- /tests/utest/oxygen_handler_result/test_deprecation_warning.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Currently, the introduction of pydantic[1] to validate result dictionaries that 3 | handlers return is planned to just raise a deprecation warning. 4 | 5 | After 1.0 -- ie. a backwards-incompatible release -- we should turn deprecation 6 | warning to actually start failing. See issue [2]. 7 | 8 | [1| https://github.com/eficode/robotframework-oxygen/issues/43 9 | [2] https://github.com/eficode/robotframework-oxygen/issues/45 10 | ''' 11 | from unittest import TestCase 12 | from unittest.mock import patch 13 | from oxygen.base_handler import BaseHandler 14 | from oxygen.oxygen import OxygenCLI 15 | 16 | from ..helpers import get_config, MINIMAL_SUITE_DICT 17 | 18 | class TestDeprecationWarningWhenValidating(TestCase): 19 | def setUp(self): 20 | self.cli = OxygenCLI() 21 | 22 | def _validate_warning_msg(self, warning, module_name): 23 | warning_message = str(warning.warning) 24 | for expected in (module_name, 25 | 'validation error for typed-dict', 26 | 'In Oxygen 1.0, handlers will need to produce valid results.'): 27 | self.assertIn(expected, warning_message) 28 | 29 | def test_warning_about_invalid_result(self): 30 | handler = BaseHandler(get_config()['oxygen.junit']) 31 | 32 | with self.assertWarns(UserWarning) as warning: 33 | handler._validate({}) 34 | 35 | self._validate_warning_msg(warning, 'oxygen.base_handler') 36 | 37 | @patch('oxygen.oxygen.RobotInterface') 38 | def test_warning_about_invalid_result_in_CLI(self, mock_iface): 39 | with self.assertWarns(UserWarning) as warning: 40 | self.cli.convert_to_robot_result({ 41 | 'result_file': 'doesentmatter', 42 | 'func': lambda **_: {**MINIMAL_SUITE_DICT, 'setup': []} 43 | }) 44 | 45 | mock_iface.assert_any_call() 46 | # this one has weird name because we fake `func` with lambda 47 | self._validate_warning_msg(warning, 'test_deprecation_warning') 48 | 49 | def test_deprecation_was_removed(self): 50 | '''Remove this test once deprecation warning has been removed''' 51 | if self.cli.__version__.startswith('1'): 52 | self.fail('Deprecation warning should have been removed in 1.0') 53 | -------------------------------------------------------------------------------- /tests/utest/robot_interface/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/robot_interface/__init__.py -------------------------------------------------------------------------------- /tests/utest/robot_interface/test_robot_interface_basic_usage.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | from unittest import TestCase 3 | 4 | from robot.result.model import (Keyword as RobotKeyword, 5 | Message as RobotMessage, 6 | TestCase as RobotTest, 7 | TestSuite as RobotSuite) 8 | 9 | from robot.running.model import TestSuite as RobotRunningSuite 10 | 11 | from oxygen.robot_interface import RobotInterface, get_keywords_from 12 | 13 | EXAMPLE_SUITES = [{ 14 | 'name': 'suite1', 15 | 'setup': [], 16 | 'metadata': {'metadata-key': 'metadata-value'}, 17 | 'suites': [{'name': 'suite2', 18 | 'setup': {'elapsed': 0.0, 19 | 'keywords': [], 20 | 'messages': [], 21 | 'name': 'Suite Setup keyword', 22 | 'pass': True, 23 | 'tags': [], 24 | 'teardown': []}, 25 | 'suites': [], 26 | 'tags': [], 27 | 'teardown': {'elapsed': 0.0, 28 | 'keywords': [], 29 | 'messages': [], 30 | 'name': 'Suite Teardown keyword', 31 | 'pass': True, 32 | 'tags': [], 33 | 'teardown': []}, 34 | 'tests': [{'keywords': [{'elapsed': 0.0, 35 | 'keywords': [], 36 | 'messages': [], 37 | 'name': 'casea (Execution)', 38 | 'pass': True, 39 | 'tags': [], 40 | 'teardown': []}], 41 | 'name': 'casea', 42 | 'setup': {'elapsed': 0.0, 43 | 'keywords': [], 44 | 'messages': [], 45 | 'name': 'Test Setup keyword', 46 | 'pass': True, 47 | 'tags': [], 48 | 'teardown': []}, 49 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 50 | 'teardown': {'elapsed': 0.0, 51 | 'keywords': [], 52 | 'messages': [], 53 | 'name': 'Suite Teardown keyword', 54 | 'pass': True, 55 | 'tags': [], 56 | 'teardown': []}}, 57 | {'keywords': [{'elapsed': 0.0, 58 | 'keywords': [], 59 | 'messages': [], 60 | 'name': 'caseb (Execution)', 61 | 'pass': True, 62 | 'tags': [], 63 | 'teardown': []}], 64 | 'name': 'caseb', 65 | 'setup': [], 66 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 67 | 'teardown': []}]}], 68 | 'tags': [], 69 | 'teardown': [], 70 | 'tests': [{'keywords': [{'elapsed': 0.0, 71 | 'keywords': [], 72 | 'messages': [], 73 | 'name': 'case1 (Execution)', 74 | 'pass': True, 75 | 'tags': [], 76 | 'teardown': []}], 77 | 'name': 'case1', 78 | 'setup': [], 79 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 80 | 'teardown': []}, 81 | {'keywords': [{'elapsed': 0.0, 82 | 'keywords': [], 83 | 'messages': ['ERROR: Example error message ' 84 | '(the_error_type)'], 85 | 'name': 'case2 (Execution)', 86 | 'pass': False, 87 | 'tags': [], 88 | 'teardown': []}], 89 | 'name': 'case2', 90 | 'setup': [], 91 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 92 | 'teardown': []}, 93 | {'keywords': [{'elapsed': 0.0, 94 | 'keywords': [], 95 | 'messages': ['FAIL: Example failure message ' 96 | '(the_failure_type)'], 97 | 'name': 'case3 (Execution)', 98 | 'pass': False, 99 | 'tags': [], 100 | 'teardown': []}], 101 | 'name': 'case3', 102 | 'setup': [], 103 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 104 | 'teardown': []}, 105 | {'keywords': [{'elapsed': 0.0, 106 | 'keywords': [], 107 | 'messages': ['*HTML* Robot Framework'], 108 | 'name': 'case3 (Execution)', 109 | 'pass': False, 110 | 'tags': [], 111 | 'teardown': []}], 112 | 'name': 'case3', 113 | 'setup': [], 114 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 115 | 'teardown': []}] 116 | }, { 117 | 'name': 'suite2', 118 | 'setup': [], 119 | 'suites': [], 120 | 'tags': [], 121 | 'teardown': [], 122 | 'tests': [{'keywords': [{'elapsed': 0.0, 123 | 'keywords': [], 124 | 'messages': [], 125 | 'name': 'casea (Execution)', 126 | 'pass': True, 127 | 'tags': [], 128 | 'teardown': []}], 129 | 'name': 'casea', 130 | 'setup': [], 131 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 132 | 'teardown': []}, 133 | {'keywords': [{'elapsed': 0.0, 134 | 'keywords': [], 135 | 'messages': [], 136 | 'name': 'caseb (Execution)', 137 | 'pass': False, 138 | 'tags': [], 139 | 'teardown': []}], 140 | 'name': 'caseb', 141 | 'setup': [], 142 | 'tags': ['OXYGEN_JUNIT_UNKNOWN_EXECUTION_TIME'], 143 | 'teardown': []}] 144 | }] 145 | 146 | 147 | class RobotInterfaceBasicTests(TestCase): 148 | ''' 149 | This tests only two methods of RobotInterface, but since they internally use 150 | all the other functions, pretty much everything is covered. 151 | ''' 152 | 153 | def setUp(self): 154 | self.iface = RobotInterface() 155 | 156 | def now(self): 157 | return int(round(time() * 1000)) 158 | 159 | def test_result_build_suites(self): 160 | _, converted = self.iface.result.build_suites(self.now(), 161 | *EXAMPLE_SUITES) 162 | 163 | self.assertIsInstance(converted, list) 164 | self.assertEqual(len(converted), 2) 165 | 166 | for converted_suite in converted: 167 | self.assertIsInstance(converted_suite, RobotSuite) 168 | 169 | for subsuite in converted[0].suites: 170 | self.assertIsInstance(subsuite, RobotSuite) 171 | 172 | for test in converted[1].tests: 173 | self.assertIsInstance(test, RobotTest) 174 | 175 | for kw in get_keywords_from(converted[0].tests[1]): 176 | self.assertIsInstance(kw, RobotKeyword) 177 | 178 | for message in get_keywords_from(converted[0].tests[1])[0].messages: 179 | self.assertIsInstance(message, RobotMessage) 180 | self.assertEqual(get_keywords_from(converted[0].tests[3])[0].messages[0].html, True) 181 | self.assertEqual(get_keywords_from(converted[0].tests[3])[0].messages[0].message, ' Robot Framework') 182 | self.assertEqual(get_keywords_from(converted[0].tests[2])[0].messages[0].message,'FAIL: Example failure message ' 183 | '(the_failure_type)') 184 | self.assertEqual(get_keywords_from(converted[0].tests[2])[0].messages[0].html, False) 185 | 186 | def test_result_create_wrapper_keyword_for_setup(self): 187 | ret = self.iface.result.create_wrapper_keyword('My Wrapper', 188 | '20200507 13:42:50.001', 189 | '20200507 14:59:01.999', 190 | True, 191 | RobotKeyword(), 192 | RobotKeyword()) 193 | 194 | self.assertIsInstance(ret, RobotKeyword) 195 | self.assertEqual(ret.name, 'My Wrapper') 196 | self.assertEqual(len(get_keywords_from(ret)), 2) 197 | try: 198 | # Robot Framework < 4.0 199 | from robot.result.model import Keyword 200 | self.assertEqual(ret.type, Keyword.SETUP_TYPE) 201 | except AttributeError: 202 | # Robot Framework >= 4.0 203 | from robot.model import BodyItem 204 | self.assertEqual(ret.type, BodyItem.SETUP) 205 | 206 | def validate_metadata(self, actual): 207 | self.assertEqual(actual.name, EXAMPLE_SUITES[0]['name']) 208 | self.assertEqual(dict(actual.metadata), EXAMPLE_SUITES[0]['metadata']) 209 | 210 | def test_result_build_suite_with_metadata(self): 211 | _, ret = self.iface.result.build_suite(self.now(), EXAMPLE_SUITES[0]) 212 | self.validate_metadata(ret) 213 | 214 | def test_result_create_wrapper_keyword_for_teardown(self): 215 | ret = self.iface.result.create_wrapper_keyword('My Wrapper', 216 | '20200507 13:42:50.001', 217 | '20200507 14:59:01.999', 218 | False, 219 | RobotKeyword()) 220 | try: 221 | # Robot Framework < 4.0 222 | from robot.result.model import Keyword 223 | self.assertEqual(ret.type, Keyword.TEARDOWN_TYPE) 224 | except AttributeError: 225 | # Robot Framework >= 4.0 226 | from robot.model import BodyItem 227 | self.assertEqual(ret.type, BodyItem.TEARDOWN) 228 | 229 | def test_running_build_suite(self): 230 | ret = self.iface.running.build_suite(EXAMPLE_SUITES[1]) 231 | 232 | self.assertIsInstance(ret, RobotRunningSuite) 233 | self.assertEqual(ret.name, EXAMPLE_SUITES[1]['name']) 234 | 235 | def test_running_build_suite_with_metadata(self): 236 | ret = self.iface.running.build_suite(EXAMPLE_SUITES[0]) 237 | self.validate_metadata(ret) 238 | -------------------------------------------------------------------------------- /tests/utest/robot_interface/test_time_conversions.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from unittest import TestCase 3 | 4 | from mock import patch 5 | 6 | from oxygen.robot_interface import RobotInterface 7 | 8 | 9 | class TestMsToTimestamp(TestCase): 10 | def setUp(self): 11 | self.interface = RobotInterface() 12 | 13 | def test_should_be_correct(self): 14 | timestamp = self.interface.result.ms_to_timestamp(1533625284100.0) 15 | self.assertEqual(timestamp, '20180807 07:01:24.100000') 16 | 17 | timestamp = self.interface.result.ms_to_timestamp(1533625284451.0) 18 | self.assertEqual(timestamp, '20180807 07:01:24.451000') 19 | 20 | def test_should_be_associative(self): 21 | timestamp = '20180807 07:01:24.300000' 22 | 23 | milliseconds = self.interface.result.timestamp_to_ms(timestamp) 24 | self.assertEqual(milliseconds, 1533625284300.0) 25 | 26 | timestamp = self.interface.result.ms_to_timestamp(milliseconds) 27 | self.assertEqual(timestamp, '20180807 07:01:24.300000') 28 | 29 | milliseconds = self.interface.result.timestamp_to_ms(timestamp) 30 | self.assertEqual(milliseconds, 1533625284300.0) 31 | 32 | timestamp = self.interface.result.ms_to_timestamp(milliseconds) 33 | self.assertEqual(timestamp, '20180807 07:01:24.300000') 34 | 35 | def _validate_timestamp(self, interface): 36 | timestamp = interface.ms_to_timestamp(-10) 37 | expected = '19700101 00:00:00.990000' 38 | 39 | self.assertEqual(timestamp, expected) 40 | 41 | def test_ms_before_epoch_are_reset_to_epoch(self): 42 | from oxygen.robot4_interface import RobotResultInterface as RF4ResultIface 43 | with patch.object(RF4ResultIface, 'get_timezone_delta') as m: 44 | m.return_value = timedelta(seconds=7200) 45 | self._validate_timestamp(RF4ResultIface()) 46 | 47 | from oxygen.robot3_interface import RobotResultInterface as RF3ResultIface 48 | with patch.object(RF3ResultIface, 'get_timezone_delta') as m: 49 | m.return_value = timedelta(seconds=7200) 50 | self._validate_timestamp(RF3ResultIface()) 51 | 52 | 53 | class TestTimestampToMs(TestCase): 54 | def setUp(self): 55 | self.iface = RobotInterface() 56 | 57 | def test_should_be_correct(self): 58 | milliseconds = self.iface.result.timestamp_to_ms('20180807 07:01:24.000') 59 | self.assertEqual(milliseconds, 1533625284000.0) 60 | 61 | milliseconds = self.iface.result.timestamp_to_ms('20180807 07:01:24.555') 62 | self.assertEqual(milliseconds, 1533625284555.0) 63 | 64 | def test_should_be_associative(self): 65 | milliseconds = 1533625284300.0 66 | 67 | timestamp = self.iface.result.ms_to_timestamp(milliseconds) 68 | self.assertEqual(timestamp, '20180807 07:01:24.300000') 69 | 70 | milliseconds = self.iface.result.timestamp_to_ms(timestamp) 71 | self.assertEqual(milliseconds, 1533625284300.0) 72 | 73 | timestamp = self.iface.result.ms_to_timestamp(milliseconds) 74 | self.assertEqual(timestamp, '20180807 07:01:24.300000') 75 | -------------------------------------------------------------------------------- /tests/utest/zap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eficode/robotframework-oxygen/93bc908a3d38006086d7b9832a63c54549a8d1f8/tests/utest/zap/__init__.py -------------------------------------------------------------------------------- /tests/utest/zap/test_parse_zap_alert_dict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.zap import ZAProxyHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestParseZapAlertDict(TestCase): 9 | def setUp(self): 10 | self.object = ZAProxyHandler(get_config()['oxygen.zap']) 11 | self._params = { 12 | 'instances': [True, False], 13 | } 14 | self.parser_mock = MagicMock(return_value=None) 15 | self.object._parse_zap_instance = self.parser_mock 16 | 17 | def test_has_defaults(self): 18 | return_dict = self.object._parse_zap_alert_dict(self._params) 19 | assert('name' in return_dict) 20 | assert(return_dict['name'] == '[Unknown Plugin ID] Unknown Alert Name') 21 | 22 | def test_keeps_params(self): 23 | self._params['pluginid'] = 'my plugin' 24 | self._params['name'] = 'my name' 25 | return_dict = self.object._parse_zap_alert_dict(self._params) 26 | assert('name' in return_dict) 27 | assert(return_dict['name'] == 'my plugin my name') 28 | 29 | def test_passes_down(self): 30 | return_dict = self.object._parse_zap_alert_dict(self._params) 31 | self.parser_mock.assert_any_call(True, True, True) 32 | self.parser_mock.assert_any_call(False, True, True) 33 | assert(self.parser_mock.call_count == 2) 34 | -------------------------------------------------------------------------------- /tests/utest/zap/test_parse_zap_dict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.zap import ZAProxyHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestParseZapDict(TestCase): 9 | def setUp(self): 10 | self.object = ZAProxyHandler(get_config()['oxygen.zap']) 11 | self.params = { 12 | 'version': None, 13 | 'generated': None, 14 | 'site': [True, False], 15 | } 16 | self.parser_mock = MagicMock(return_value=None) 17 | self.object._parse_zap_site_dict = self.parser_mock 18 | 19 | def test_has_defaults(self): 20 | return_dict = self.object._parse_zap_dict(self.params) 21 | expected_name = ('Oxygen ZAProxy Report (Unknown ZAProxy Version, ' 22 | 'Unknown ZAProxy Run Time)') 23 | assert('name' in return_dict) 24 | assert(return_dict['name'] == expected_name) 25 | 26 | def test_keepsparams(self): 27 | self.params['version'] = 'my version' 28 | self.params['generated'] = 'when' 29 | return_dict = self.object._parse_zap_dict(self.params) 30 | assert('name' in return_dict) 31 | assert(return_dict['name'] == ('Oxygen ZAProxy Report ' 32 | '(my version, when)')) 33 | 34 | def test_handles_oddparams(self): 35 | self.params['@version'] = 'my version' 36 | self.params['@generated'] = 'when' 37 | return_dict = self.object._parse_zap_dict(self.params) 38 | assert('name' in return_dict) 39 | assert(return_dict['name'] == ('Oxygen ZAProxy Report ' 40 | '(my version, when)')) 41 | 42 | def test_calls_down(self): 43 | return_dict = self.object._parse_zap_dict(self.params) 44 | self.parser_mock.assert_any_call(True) 45 | self.parser_mock.assert_any_call(False) 46 | assert(self.parser_mock.call_count == 2) 47 | -------------------------------------------------------------------------------- /tests/utest/zap/test_parse_zap_instance.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.zap import ZAProxyHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestParseZapInstance(TestCase): 9 | def setUp(self): 10 | self.object = ZAProxyHandler(get_config()['oxygen.zap']) 11 | self.object._parse_zap_site_dict = MagicMock(return_value=None) 12 | self.params = {} 13 | 14 | def test_has_defaults(self): 15 | return_dict = self.object._parse_zap_instance(self.params, True, True) 16 | expected_name = ('[Unknown HTTP Method] [Unknown Target URI]: ' 17 | '[Unknown Target Parameter]') 18 | assert('name' in return_dict) 19 | assert(return_dict['name'] == expected_name) 20 | 21 | def test_keepsparams(self): 22 | self.params = { 23 | 'uri': 'foo', 24 | 'method': 'bar', 25 | 'param': 'baz', 26 | } 27 | return_dict = self.object._parse_zap_instance(self.params, True, True) 28 | assert('name' in return_dict) 29 | assert(return_dict['name'] == 'bar foo: baz') 30 | 31 | def test_is_fail_if_risk_and_confident(self): 32 | return_dict = self.object._parse_zap_instance(self.params, True, True) 33 | assert('pass' in return_dict) 34 | assert(return_dict['pass'] == False) 35 | 36 | def test_is_pass_if_risk_and_not_confident(self): 37 | return_dict = self.object._parse_zap_instance(self.params, True, False) 38 | assert('pass' in return_dict) 39 | assert(return_dict['pass'] == True) 40 | 41 | def test_is_pass_if_not_risk_and_confident(self): 42 | return_dict = self.object._parse_zap_instance(self.params, False, True) 43 | assert('pass' in return_dict) 44 | assert(return_dict['pass'] == True) 45 | 46 | def test_is_pass_if_not_risk_and_not_confident(self): 47 | return_dict = self.object._parse_zap_instance( 48 | self.params, False, False) 49 | assert('pass' in return_dict) 50 | assert(return_dict['pass'] == True) 51 | -------------------------------------------------------------------------------- /tests/utest/zap/test_parse_zap_site_dict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock 3 | 4 | from oxygen.zap import ZAProxyHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestParseZapSiteDict(TestCase): 9 | def setUp(self): 10 | self.object = ZAProxyHandler(get_config()['oxygen.zap']) 11 | self._params = { 12 | 'alerts': [True, False], 13 | } 14 | self._parser = MagicMock(return_value=None) 15 | self.object._parse_zap_alert_dict = self._parser 16 | 17 | def test_has_defaults(self): 18 | return_dict = self.object._parse_zap_site_dict(self._params) 19 | assert('name' in return_dict) 20 | assert(return_dict['name'] == 'Site: Unknown Site Name') 21 | assert('tests' in return_dict) 22 | self._parser.assert_any_call(True) 23 | self._parser.assert_any_call(False) 24 | assert(self._parser.call_count == 2) 25 | 26 | def test_reads_prefixed(self): 27 | self._params['@name'] = 'My Site Name' 28 | return_dict = self.object._parse_zap_site_dict(self._params) 29 | assert('name' in return_dict) 30 | assert(return_dict['name'] == 'Site: My Site Name') 31 | assert('tests' in return_dict) 32 | self._parser.assert_any_call(True) 33 | self._parser.assert_any_call(False) 34 | assert(self._parser.call_count == 2) 35 | -------------------------------------------------------------------------------- /tests/utest/zap/test_xml_to_dict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from xml.etree import ElementTree as ET 3 | 4 | from oxygen.zap import ZAProxyHandler 5 | from ..helpers import get_config 6 | 7 | 8 | class TestXmlToDict(TestCase): 9 | def _create_example_xml(self): 10 | xml_head = ET.Element('xml_head') 11 | xml_container = ET.SubElement(xml_head, 'xml_container') 12 | ET.SubElement(xml_head, 'second_child') 13 | ET.SubElement(xml_container, 'first_contained') 14 | ET.SubElement(xml_container, 'second_contained') 15 | return xml_head 16 | 17 | def setUp(self): 18 | self.object = ZAProxyHandler(get_config()['oxygen.zap']) 19 | self._xml = self._create_example_xml() 20 | 21 | def test_converts_xml(self): 22 | returned_dict = self.object._xml_to_dict(self._xml) 23 | assert('xml_head' in returned_dict) 24 | assert('xml_container' in returned_dict['xml_head']) 25 | assert('second_child' in returned_dict['xml_head']) 26 | assert('first_contained' in returned_dict['xml_head']['xml_container']) 27 | assert('second_contained' in returned_dict['xml_head']['xml_container']) 28 | -------------------------------------------------------------------------------- /tests/utest/zap/test_zap_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from unittest import TestCase 4 | from unittest.mock import ANY, Mock, create_autospec, patch 5 | 6 | from robot.running.model import TestSuite 7 | 8 | from oxygen.oxygen import OxygenCLI 9 | from ..helpers import RESOURCES_PATH 10 | 11 | 12 | class TestOxygenZapCLI(TestCase): 13 | ZAP_XML = str(RESOURCES_PATH / "zap" / "zap.xml") 14 | 15 | def setUp(self): 16 | self.cli = OxygenCLI() 17 | self.handler = self.cli.handlers["oxygen.zap"] 18 | self.expected_suite = create_autospec(TestSuite) 19 | self.mock = Mock() 20 | self.mock.running.build_suite = Mock(return_value=self.expected_suite) 21 | 22 | def tearDown(self): 23 | self.cli = None 24 | self.handler = None 25 | self.expected_suite = None 26 | self.mock = None 27 | 28 | def test_cli(self): 29 | self.assertEqual( 30 | self.handler.cli(), 31 | { 32 | ("--accepted-risk-level",): { 33 | "help": "Set accepted risk level", 34 | "type": int, 35 | }, 36 | ("--required-confidence-level",): { 37 | "help": "Set required confidence level", 38 | "type": int, 39 | }, 40 | ("result_file",): {}, 41 | }, 42 | ) 43 | 44 | @patch("oxygen.oxygen.RobotInterface") 45 | def test_cli_run(self, mock_robot_iface): 46 | mock_robot_iface.return_value = self.mock 47 | 48 | cmd_args = f"oxygen oxygen.zap {self.ZAP_XML}" 49 | with patch.object(sys, "argv", cmd_args.split()): 50 | self.cli.run() 51 | 52 | self.assertEqual(self.handler._config["accepted_risk_level"], 2) 53 | self.assertEqual(self.handler._config["required_confidence_level"], 1) 54 | 55 | self.mock.running.build_suite.assert_called_once() 56 | 57 | self.expected_suite.run.assert_called_once_with( 58 | output=str(RESOURCES_PATH / "zap" / "zap_robot_output.xml"), 59 | log=None, 60 | report=None, 61 | stdout=ANY, 62 | ) 63 | 64 | @patch("oxygen.oxygen.RobotInterface") 65 | def test_cli_run_with_levels(self, mock_robot_iface): 66 | mock_robot_iface.return_value = self.mock 67 | 68 | cmd_args = ( 69 | f"oxygen oxygen.zap {self.ZAP_XML} --accepted-risk-level 3" 70 | " --required-confidence-level 3" 71 | ) 72 | with patch.object(sys, "argv", cmd_args.split()): 73 | self.cli.run() 74 | 75 | self.assertEqual(self.handler._config["accepted_risk_level"], 3) 76 | self.assertEqual(self.handler._config["required_confidence_level"], 3) 77 | 78 | @patch("oxygen.oxygen.RobotInterface") 79 | def test_cli_run_with_accepted_risk_level(self, mock_robot_iface): 80 | mock_robot_iface.return_value = self.mock 81 | 82 | cmd_args = f"oxygen oxygen.zap {self.ZAP_XML} --accepted-risk-level 3" 83 | with patch.object(sys, "argv", cmd_args.split()): 84 | self.cli.run() 85 | 86 | self.assertEqual(self.handler._config["accepted_risk_level"], 3) 87 | self.assertEqual(self.handler._config["required_confidence_level"], 1) 88 | 89 | @patch("oxygen.oxygen.RobotInterface") 90 | def test_cli_run_with_required_confidence_level(self, mock_robot_iface): 91 | mock_robot_iface.return_value = self.mock 92 | 93 | cmd_args = f"oxygen oxygen.zap {self.ZAP_XML} --required-confidence-level 3" 94 | with patch.object(sys, "argv", cmd_args.split()): 95 | self.cli.run() 96 | 97 | self.assertEqual(self.handler._config["accepted_risk_level"], 2) 98 | self.assertEqual(self.handler._config["required_confidence_level"], 3) 99 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37 3 | 4 | [testenv] 5 | whitelist_externals = invoke 6 | install_command = invoke install -p {packages} 7 | install = 8 | commands = 9 | invoke test 10 | --------------------------------------------------------------------------------