robotmk-bridge-testdatagenerator 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robotmk_bridge_testdatagenerator-0.3.0/.gitignore +16 -0
- robotmk_bridge_testdatagenerator-0.3.0/CHANGELOG.md +31 -0
- robotmk_bridge_testdatagenerator-0.3.0/LICENSE +21 -0
- robotmk_bridge_testdatagenerator-0.3.0/PKG-INFO +112 -0
- robotmk_bridge_testdatagenerator-0.3.0/README.md +89 -0
- robotmk_bridge_testdatagenerator-0.3.0/pyproject.toml +47 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/__init__.py +17 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/__main__.py +215 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/generator.py +167 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/handlers/__init__.py +11 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/handlers/gatling_generator.py +103 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/handlers/junit_generator.py +84 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/handlers/zap_generator.py +256 -0
- robotmk_bridge_testdatagenerator-0.3.0/src/robotmk_bridge_testdatagenerator/handlers.yaml +53 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/elabit/robotmk-bridge-testdatagenerator/compare/v0.2.0...v0.3.0) (2026-06-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* init commit, pypi init ([1d278e6](https://github.com/elabit/robotmk-bridge-testdatagenerator/commit/1d278e64b068e6b17a922cb3fe85ad330971e41c))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* deleted publish workflow ([a046128](https://github.com/elabit/robotmk-bridge-testdatagenerator/commit/a046128ed5e7f9fcc2c271b89676f35ee37ca45e))
|
|
14
|
+
|
|
15
|
+
## [0.2.0](https://github.com/elabit/robotmk-bridge-testdatagenerator/compare/v0.1.0...v0.2.0) (2026-06-24)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* init commit, pypi init ([1d278e6](https://github.com/elabit/robotmk-bridge-testdatagenerator/commit/1d278e64b068e6b17a922cb3fe85ad330971e41c))
|
|
21
|
+
|
|
22
|
+
## Changelog
|
|
23
|
+
|
|
24
|
+
All notable changes to this project will be documented in this file.
|
|
25
|
+
|
|
26
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
27
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
28
|
+
|
|
29
|
+
Changelog entries are managed automatically by [release-please](https://github.com/googleapis/release-please).
|
|
30
|
+
|
|
31
|
+
<!-- Release notes are appended above this line by release-please -->
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon Meggle
|
|
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.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotmk-bridge-testdatagenerator
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Generates synthetic test result files (JUnit, ZAP, Gatling) for robotmk-bridge-plugin testing
|
|
5
|
+
Project-URL: Homepage, https://github.com/simonmeggle/robotmk-bridge-testdatagenerator
|
|
6
|
+
Project-URL: Issues, https://github.com/simonmeggle/robotmk-bridge-testdatagenerator/issues
|
|
7
|
+
Author-email: Simon Meggle <simon.meggle@checkmk.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: checkmk,gatling,junit,robotmk,test-data,testing,zap
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: pyyaml>=5.4
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# robotmk-bridge-testdatagenerator
|
|
25
|
+
|
|
26
|
+
Generates synthetic test result files for [robotmk-bridge-plugin](https://github.com/simonmeggle/robotmk-bridge-plugin) testing. Produces realistic, randomized output in the formats consumed by the bridge plugin's handlers.
|
|
27
|
+
|
|
28
|
+
## Supported Formats
|
|
29
|
+
|
|
30
|
+
| Handler | Format | Extension |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `junit` | JUnit XML | `.xml` |
|
|
33
|
+
| `zaproxy` | OWASP ZAP XML v2.7.0 | `.xml` |
|
|
34
|
+
| `gatling` | Gatling simulation.log v2.0 | `.log` |
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install robotmk-bridge-testdatagenerator
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## CLI Usage
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Generate all handlers (output: tests/e2e/data/)
|
|
46
|
+
rmkb-testgen
|
|
47
|
+
|
|
48
|
+
# Specify output directory and status
|
|
49
|
+
rmkb-testgen --output-dir /tmp/test-data --status failed
|
|
50
|
+
|
|
51
|
+
# Generate specific handlers only
|
|
52
|
+
rmkb-testgen --handlers junit gatling
|
|
53
|
+
|
|
54
|
+
# Continuous mode (Ctrl+C to stop)
|
|
55
|
+
rmkb-testgen --continuous --interval 5
|
|
56
|
+
|
|
57
|
+
# List available handlers
|
|
58
|
+
rmkb-testgen --list
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Options
|
|
62
|
+
|
|
63
|
+
| Flag | Short | Default | Description |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `--output-dir` | `-o` | `tests/e2e/data` | Output directory |
|
|
66
|
+
| `--status` | `-s` | `passed` | `passed` / `failed` / `mixed` |
|
|
67
|
+
| `--handlers` | `-H` | all | Specific handlers to generate |
|
|
68
|
+
| `--pattern` | `-p` | `{handler}.{ext}` | Filename pattern |
|
|
69
|
+
| `--continuous` | `-c` | off | Regenerate on interval |
|
|
70
|
+
| `--interval` | `-i` | `5.0` | Seconds between generations |
|
|
71
|
+
| `--list` | `-l` | — | List handlers and exit |
|
|
72
|
+
| `--verbose` | `-v` | off | Verbose output |
|
|
73
|
+
|
|
74
|
+
## Library Usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from robotmk_bridge_testdatagenerator import (
|
|
78
|
+
generate_all_handler_files,
|
|
79
|
+
generate_handler_file,
|
|
80
|
+
get_supported_handlers,
|
|
81
|
+
)
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
|
|
84
|
+
# Generate all handlers
|
|
85
|
+
files = generate_all_handler_files(Path("/tmp/test-data"), test_status="mixed")
|
|
86
|
+
|
|
87
|
+
# Generate a single handler
|
|
88
|
+
generate_handler_file("junit", Path("/tmp/result.xml"), test_status="failed")
|
|
89
|
+
|
|
90
|
+
# List handlers
|
|
91
|
+
handlers = get_supported_handlers() # ['junit', 'gatling', 'zaproxy']
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Custom handlers.yaml
|
|
95
|
+
|
|
96
|
+
By default the bundled `handlers.yaml` is used. Override via environment variable:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
ROBOTMK_HANDLERS_YAML=/path/to/your/handlers.yaml rmkb-testgen
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Test Status Semantics
|
|
103
|
+
|
|
104
|
+
| Status | JUnit | ZAP | Gatling |
|
|
105
|
+
|---|---|---|---|
|
|
106
|
+
| `passed` | All pass | Low-risk alerts only | All requests OK |
|
|
107
|
+
| `failed` | All fail | High-risk alerts | All requests KO |
|
|
108
|
+
| `mixed` | Every 3rd fails | Low + medium risk | Every 4th KO |
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# robotmk-bridge-testdatagenerator
|
|
2
|
+
|
|
3
|
+
Generates synthetic test result files for [robotmk-bridge-plugin](https://github.com/simonmeggle/robotmk-bridge-plugin) testing. Produces realistic, randomized output in the formats consumed by the bridge plugin's handlers.
|
|
4
|
+
|
|
5
|
+
## Supported Formats
|
|
6
|
+
|
|
7
|
+
| Handler | Format | Extension |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `junit` | JUnit XML | `.xml` |
|
|
10
|
+
| `zaproxy` | OWASP ZAP XML v2.7.0 | `.xml` |
|
|
11
|
+
| `gatling` | Gatling simulation.log v2.0 | `.log` |
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install robotmk-bridge-testdatagenerator
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CLI Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Generate all handlers (output: tests/e2e/data/)
|
|
23
|
+
rmkb-testgen
|
|
24
|
+
|
|
25
|
+
# Specify output directory and status
|
|
26
|
+
rmkb-testgen --output-dir /tmp/test-data --status failed
|
|
27
|
+
|
|
28
|
+
# Generate specific handlers only
|
|
29
|
+
rmkb-testgen --handlers junit gatling
|
|
30
|
+
|
|
31
|
+
# Continuous mode (Ctrl+C to stop)
|
|
32
|
+
rmkb-testgen --continuous --interval 5
|
|
33
|
+
|
|
34
|
+
# List available handlers
|
|
35
|
+
rmkb-testgen --list
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Options
|
|
39
|
+
|
|
40
|
+
| Flag | Short | Default | Description |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| `--output-dir` | `-o` | `tests/e2e/data` | Output directory |
|
|
43
|
+
| `--status` | `-s` | `passed` | `passed` / `failed` / `mixed` |
|
|
44
|
+
| `--handlers` | `-H` | all | Specific handlers to generate |
|
|
45
|
+
| `--pattern` | `-p` | `{handler}.{ext}` | Filename pattern |
|
|
46
|
+
| `--continuous` | `-c` | off | Regenerate on interval |
|
|
47
|
+
| `--interval` | `-i` | `5.0` | Seconds between generations |
|
|
48
|
+
| `--list` | `-l` | — | List handlers and exit |
|
|
49
|
+
| `--verbose` | `-v` | off | Verbose output |
|
|
50
|
+
|
|
51
|
+
## Library Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from robotmk_bridge_testdatagenerator import (
|
|
55
|
+
generate_all_handler_files,
|
|
56
|
+
generate_handler_file,
|
|
57
|
+
get_supported_handlers,
|
|
58
|
+
)
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
# Generate all handlers
|
|
62
|
+
files = generate_all_handler_files(Path("/tmp/test-data"), test_status="mixed")
|
|
63
|
+
|
|
64
|
+
# Generate a single handler
|
|
65
|
+
generate_handler_file("junit", Path("/tmp/result.xml"), test_status="failed")
|
|
66
|
+
|
|
67
|
+
# List handlers
|
|
68
|
+
handlers = get_supported_handlers() # ['junit', 'gatling', 'zaproxy']
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Custom handlers.yaml
|
|
72
|
+
|
|
73
|
+
By default the bundled `handlers.yaml` is used. Override via environment variable:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
ROBOTMK_HANDLERS_YAML=/path/to/your/handlers.yaml rmkb-testgen
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Test Status Semantics
|
|
80
|
+
|
|
81
|
+
| Status | JUnit | ZAP | Gatling |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| `passed` | All pass | Low-risk alerts only | All requests OK |
|
|
84
|
+
| `failed` | All fail | High-risk alerts | All requests KO |
|
|
85
|
+
| `mixed` | Every 3rd fails | Low + medium risk | Every 4th KO |
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "robotmk-bridge-testdatagenerator"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Generates synthetic test result files (JUnit, ZAP, Gatling) for robotmk-bridge-plugin testing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Simon Meggle", email = "simon.meggle@checkmk.com" }]
|
|
13
|
+
keywords = ["robotmk", "checkmk", "testing", "junit", "gatling", "zap", "test-data"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["pyyaml>=5.4"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
rmkb-testgen = "robotmk_bridge_testdatagenerator.__main__:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/simonmeggle/robotmk-bridge-testdatagenerator"
|
|
32
|
+
Issues = "https://github.com/simonmeggle/robotmk-bridge-testdatagenerator/issues"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/robotmk_bridge_testdatagenerator"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel.shared-data]
|
|
38
|
+
# handlers.yaml is included as package data, not shared-data
|
|
39
|
+
# (it's inside the package directory, hatchling includes it automatically)
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = [
|
|
43
|
+
"src/",
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Test data generator for robotmk-bridge-plugin.
|
|
2
|
+
|
|
3
|
+
Generates realistic test result files for different handler types
|
|
4
|
+
(JUnit, ZAP, Gatling). Used by both e2e tests and unit test fixtures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .generator import (
|
|
8
|
+
generate_all_handler_files,
|
|
9
|
+
generate_handler_file,
|
|
10
|
+
get_supported_handlers,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"generate_all_handler_files",
|
|
15
|
+
"generate_handler_file",
|
|
16
|
+
"get_supported_handlers",
|
|
17
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""CLI entry point for the test data generator.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
rmkb-testgen [options]
|
|
5
|
+
python -m robotmk_bridge_testdatagenerator [options]
|
|
6
|
+
|
|
7
|
+
Examples:
|
|
8
|
+
# Generate all handler test files in default location
|
|
9
|
+
rmkb-testgen
|
|
10
|
+
|
|
11
|
+
# Generate in a specific directory
|
|
12
|
+
rmkb-testgen --output-dir /tmp/test-data
|
|
13
|
+
|
|
14
|
+
# Generate with failed test status
|
|
15
|
+
rmkb-testgen --status failed
|
|
16
|
+
|
|
17
|
+
# Generate only specific handlers
|
|
18
|
+
rmkb-testgen --handlers junit gatling
|
|
19
|
+
|
|
20
|
+
# Continuous mode: regenerate every 5 seconds
|
|
21
|
+
rmkb-testgen --continuous --interval 5
|
|
22
|
+
|
|
23
|
+
# Use a custom handlers.yaml
|
|
24
|
+
ROBOTMK_HANDLERS_YAML=/path/to/handlers.yaml rmkb-testgen
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import signal
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from .generator import (
|
|
34
|
+
generate_all_handler_files,
|
|
35
|
+
generate_handler_file,
|
|
36
|
+
get_supported_handlers,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_shutdown_requested = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def signal_handler(signum, frame):
|
|
43
|
+
global _shutdown_requested
|
|
44
|
+
_shutdown_requested = True
|
|
45
|
+
print("\n\nShutdown requested. Stopping after current generation...")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_files(args):
|
|
49
|
+
"""Generate test data files based on arguments."""
|
|
50
|
+
if args.handlers:
|
|
51
|
+
generated_files = {}
|
|
52
|
+
for handler in args.handlers:
|
|
53
|
+
from .generator import load_handlers_registry
|
|
54
|
+
handlers_def = load_handlers_registry()
|
|
55
|
+
handler_def = next(
|
|
56
|
+
(h for h in handlers_def if h["name"] == handler), None
|
|
57
|
+
)
|
|
58
|
+
if not handler_def:
|
|
59
|
+
print(f"Error: Handler '{handler}' not found", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
|
|
62
|
+
result_ext = handler_def.get("result_ext", "txt")
|
|
63
|
+
filename = args.pattern.format(handler=handler, ext=result_ext)
|
|
64
|
+
output_path = args.output_dir / filename
|
|
65
|
+
generate_handler_file(
|
|
66
|
+
handler_name=handler,
|
|
67
|
+
output_path=output_path,
|
|
68
|
+
test_status=args.status,
|
|
69
|
+
)
|
|
70
|
+
generated_files[handler] = output_path
|
|
71
|
+
else:
|
|
72
|
+
generated_files = generate_all_handler_files(
|
|
73
|
+
output_dir=args.output_dir,
|
|
74
|
+
test_status=args.status,
|
|
75
|
+
filename_pattern=args.pattern,
|
|
76
|
+
)
|
|
77
|
+
return generated_files
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
"""CLI entry point for test data generation."""
|
|
82
|
+
parser = argparse.ArgumentParser(
|
|
83
|
+
description="Generate test result files for Robotmk Bridge handlers",
|
|
84
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
85
|
+
epilog="""
|
|
86
|
+
Examples:
|
|
87
|
+
Generate all handlers:
|
|
88
|
+
rmkb-testgen
|
|
89
|
+
|
|
90
|
+
Specific output directory:
|
|
91
|
+
rmkb-testgen --output-dir /tmp/test-data
|
|
92
|
+
|
|
93
|
+
Failed status:
|
|
94
|
+
rmkb-testgen --status failed
|
|
95
|
+
|
|
96
|
+
Specific handlers:
|
|
97
|
+
rmkb-testgen --handlers junit gatling
|
|
98
|
+
|
|
99
|
+
Continuous mode (Ctrl+C to stop):
|
|
100
|
+
rmkb-testgen --continuous --interval 5
|
|
101
|
+
|
|
102
|
+
Custom handlers.yaml:
|
|
103
|
+
ROBOTMK_HANDLERS_YAML=/path/to/handlers.yaml rmkb-testgen
|
|
104
|
+
""",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"-o", "--output-dir",
|
|
109
|
+
type=Path,
|
|
110
|
+
default=Path("tests/e2e/data"),
|
|
111
|
+
help="Output directory for generated test files (default: tests/e2e/data)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"-s", "--status",
|
|
115
|
+
choices=["passed", "failed", "mixed"],
|
|
116
|
+
default="passed",
|
|
117
|
+
help="Test status for generated files (default: passed)",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"-H", "--handlers",
|
|
121
|
+
nargs="+",
|
|
122
|
+
choices=get_supported_handlers(),
|
|
123
|
+
help="Generate only specific handlers (default: all)",
|
|
124
|
+
)
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"-p", "--pattern",
|
|
127
|
+
default="{handler}.{ext}",
|
|
128
|
+
help="Filename pattern (default: {handler}.{ext})",
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"-c", "--continuous",
|
|
132
|
+
action="store_true",
|
|
133
|
+
help="Continuous mode: regenerate files at regular intervals",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"-i", "--interval",
|
|
137
|
+
type=float,
|
|
138
|
+
default=5.0,
|
|
139
|
+
help="Interval in seconds between generations in continuous mode (default: 5)",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"-l", "--list",
|
|
143
|
+
action="store_true",
|
|
144
|
+
help="List supported handlers and exit",
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"-v", "--verbose",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Enable verbose output",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
args = parser.parse_args()
|
|
153
|
+
|
|
154
|
+
if args.list:
|
|
155
|
+
print("Supported handlers:")
|
|
156
|
+
for handler in get_supported_handlers():
|
|
157
|
+
print(f" - {handler}")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
if args.interval <= 0:
|
|
161
|
+
print("Error: Interval must be greater than 0", file=sys.stderr)
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
166
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
167
|
+
|
|
168
|
+
if args.verbose:
|
|
169
|
+
print(f"Output directory: {args.output_dir.absolute()}")
|
|
170
|
+
print(f"Test status: {args.status}")
|
|
171
|
+
print(f"Filename pattern: {args.pattern}")
|
|
172
|
+
if args.continuous:
|
|
173
|
+
print(f"Continuous mode: regenerating every {args.interval}s")
|
|
174
|
+
print("Press Ctrl+C to stop\n")
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
if args.continuous:
|
|
178
|
+
generation_count = 0
|
|
179
|
+
while not _shutdown_requested:
|
|
180
|
+
generation_count += 1
|
|
181
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
182
|
+
if args.verbose:
|
|
183
|
+
print(f"[{timestamp}] Generation #{generation_count}")
|
|
184
|
+
generated_files = generate_files(args)
|
|
185
|
+
if generation_count == 1 or args.verbose:
|
|
186
|
+
print(f"Generated {len(generated_files)} test file(s):")
|
|
187
|
+
for handler, path in generated_files.items():
|
|
188
|
+
size = path.stat().st_size
|
|
189
|
+
print(f" ✓ {handler:12s} → {path.name:20s} ({size:,} bytes)")
|
|
190
|
+
else:
|
|
191
|
+
print(f"[{timestamp}] Generation #{generation_count}: "
|
|
192
|
+
f"{len(generated_files)} files updated")
|
|
193
|
+
if not _shutdown_requested:
|
|
194
|
+
if args.verbose:
|
|
195
|
+
print(f"Waiting {args.interval}s until next generation...\n")
|
|
196
|
+
time.sleep(args.interval)
|
|
197
|
+
print(f"\nStopped after {generation_count} generation(s)")
|
|
198
|
+
else:
|
|
199
|
+
generated_files = generate_files(args)
|
|
200
|
+
print(f"Generated {len(generated_files)} test file(s):")
|
|
201
|
+
for handler, path in generated_files.items():
|
|
202
|
+
size = path.stat().st_size
|
|
203
|
+
print(f" ✓ {handler:12s} → {path.name:20s} ({size:,} bytes)")
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
208
|
+
if args.verbose:
|
|
209
|
+
import traceback
|
|
210
|
+
traceback.print_exc()
|
|
211
|
+
return 1
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
sys.exit(main())
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Core test data generator module.
|
|
2
|
+
|
|
3
|
+
Reads the bundled handlers.yaml (or ROBOTMK_HANDLERS_YAML env override) and
|
|
4
|
+
provides a unified API for generating test result files for different handler types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _open_handlers_yaml():
|
|
16
|
+
"""Return an open file-like object for handlers.yaml.
|
|
17
|
+
|
|
18
|
+
Checks ROBOTMK_HANDLERS_YAML env var first; falls back to the file
|
|
19
|
+
bundled inside the package via importlib.resources.
|
|
20
|
+
"""
|
|
21
|
+
env_override = os.environ.get("ROBOTMK_HANDLERS_YAML")
|
|
22
|
+
if env_override:
|
|
23
|
+
return open(env_override, "r", encoding="utf-8")
|
|
24
|
+
ref = resources.files("robotmk_bridge_testdatagenerator").joinpath("handlers.yaml")
|
|
25
|
+
return ref.open("r", encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_handlers_registry() -> List[Dict]:
|
|
29
|
+
"""Load handler definitions from handlers.yaml.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
List of handler definitions, each with 'name', 'title', 'class_import',
|
|
33
|
+
'result_ext', and optional 'handler_params'.
|
|
34
|
+
"""
|
|
35
|
+
with _open_handlers_yaml() as f:
|
|
36
|
+
data = yaml.safe_load(f)
|
|
37
|
+
|
|
38
|
+
if not data or "handlers" not in data:
|
|
39
|
+
raise ValueError("handlers.yaml must contain a 'handlers' key")
|
|
40
|
+
|
|
41
|
+
return data["handlers"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_supported_handlers() -> List[str]:
|
|
45
|
+
"""Return list of handler names from handlers.yaml.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of handler names (e.g., ['junit', 'gatling', 'zaproxy'])
|
|
49
|
+
"""
|
|
50
|
+
return [h["name"] for h in load_handlers_registry()]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_handler_file(
|
|
54
|
+
handler_name: str,
|
|
55
|
+
output_path: Path,
|
|
56
|
+
test_status: str = "passed",
|
|
57
|
+
**kwargs
|
|
58
|
+
) -> Path:
|
|
59
|
+
"""Generate a test result file for the specified handler.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
handler_name: Name of handler (e.g., 'junit', 'zaproxy', 'gatling')
|
|
63
|
+
output_path: Path where the result file should be written
|
|
64
|
+
test_status: Test outcome - 'passed', 'failed', or 'mixed' (default: 'passed')
|
|
65
|
+
**kwargs: Handler-specific parameters (e.g., num_tests, duration_ms)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Path to the generated file
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If handler_name is not supported
|
|
72
|
+
RuntimeError: If handler generator fails
|
|
73
|
+
"""
|
|
74
|
+
handlers = load_handlers_registry()
|
|
75
|
+
handler_def = next((h for h in handlers if h["name"] == handler_name), None)
|
|
76
|
+
|
|
77
|
+
if not handler_def:
|
|
78
|
+
supported = [h["name"] for h in handlers]
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Handler '{handler_name}' not found in handlers.yaml. "
|
|
81
|
+
f"Supported handlers: {', '.join(supported)}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
handler_to_module = {
|
|
85
|
+
"zaproxy": "zap",
|
|
86
|
+
"junit": "junit",
|
|
87
|
+
"gatling": "gatling",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module_name = handler_to_module.get(handler_name, handler_name)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
from . import handlers as handler_module
|
|
94
|
+
generator_name = f"{module_name}_generator"
|
|
95
|
+
generator = getattr(handler_module, generator_name)
|
|
96
|
+
except (ImportError, AttributeError) as e:
|
|
97
|
+
raise RuntimeError(
|
|
98
|
+
f"Could not import generator for '{handler_name}'. "
|
|
99
|
+
f"Expected module: robotmk_bridge_testdatagenerator.handlers.{generator_name}"
|
|
100
|
+
) from e
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
generator.generate(
|
|
104
|
+
output_path=output_path,
|
|
105
|
+
test_status=test_status,
|
|
106
|
+
handler_def=handler_def,
|
|
107
|
+
**kwargs
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise RuntimeError(
|
|
111
|
+
f"Failed to generate test data for handler '{handler_name}': {e}"
|
|
112
|
+
) from e
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
if output_path.exists():
|
|
116
|
+
output_path.chmod(0o644)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
return output_path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_all_handler_files(
|
|
124
|
+
output_dir: Path,
|
|
125
|
+
test_status: str = "passed",
|
|
126
|
+
filename_pattern: str = "{handler}.{ext}"
|
|
127
|
+
) -> Dict[str, Path]:
|
|
128
|
+
"""Generate test result files for all supported handlers.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
output_dir: Directory where files should be written
|
|
132
|
+
test_status: Test outcome - 'passed', 'failed', or 'mixed'
|
|
133
|
+
filename_pattern: Pattern for output filenames (variables: {handler}, {ext})
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict mapping handler name to generated file path
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> files = generate_all_handler_files(Path("tests/e2e/data"))
|
|
140
|
+
>>> files
|
|
141
|
+
{'junit': Path('tests/e2e/data/junit.xml'),
|
|
142
|
+
'gatling': Path('tests/e2e/data/gatling.log'),
|
|
143
|
+
'zaproxy': Path('tests/e2e/data/zaproxy.xml')}
|
|
144
|
+
"""
|
|
145
|
+
output_dir = Path(output_dir)
|
|
146
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
try:
|
|
148
|
+
output_dir.chmod(0o755)
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
handlers = load_handlers_registry()
|
|
153
|
+
generated_files = {}
|
|
154
|
+
|
|
155
|
+
for handler_def in handlers:
|
|
156
|
+
handler_name = handler_def["name"]
|
|
157
|
+
result_ext = handler_def.get("result_ext", "txt")
|
|
158
|
+
filename = filename_pattern.format(handler=handler_name, ext=result_ext)
|
|
159
|
+
output_path = output_dir / filename
|
|
160
|
+
generate_handler_file(
|
|
161
|
+
handler_name=handler_name,
|
|
162
|
+
output_path=output_path,
|
|
163
|
+
test_status=test_status
|
|
164
|
+
)
|
|
165
|
+
generated_files[handler_name] = output_path
|
|
166
|
+
|
|
167
|
+
return generated_files
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Gatling simulation log test data generator."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate(
|
|
10
|
+
output_path: Path,
|
|
11
|
+
test_status: str = "passed",
|
|
12
|
+
handler_def: Dict = None,
|
|
13
|
+
num_requests: int = 10,
|
|
14
|
+
duration_ms: int = 5000,
|
|
15
|
+
**kwargs
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Generate a Gatling simulation.log file in v2.0 format.
|
|
18
|
+
|
|
19
|
+
Gatling v2.x logs use tab-separated format with different record types:
|
|
20
|
+
- RUN: simulation metadata (v2.0 format with FQCN)
|
|
21
|
+
- REQUEST: individual HTTP request with timing (no simulation name)
|
|
22
|
+
- USER: user session start/end (group name + numeric ID)
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
output_path: Path where the log file should be written
|
|
26
|
+
test_status: 'passed', 'failed', or 'mixed'
|
|
27
|
+
handler_def: Handler definition from handlers.yaml (unused here)
|
|
28
|
+
num_requests: Number of requests to simulate
|
|
29
|
+
duration_ms: Total simulation duration in milliseconds
|
|
30
|
+
**kwargs: Additional parameters (ignored)
|
|
31
|
+
"""
|
|
32
|
+
base_timestamp = int(time.time() * 1000) # Current time in milliseconds
|
|
33
|
+
simulation_fqcn = "computerdatabase.advanced.AdvancedSimulationStep05"
|
|
34
|
+
run_id = "simulation-001"
|
|
35
|
+
normalized_name = "advancedsimulationstep05"
|
|
36
|
+
user_count = max(1, num_requests // 3)
|
|
37
|
+
|
|
38
|
+
# Define user groups (mix of regular users and admins)
|
|
39
|
+
user_groups = ["Users", "Users", "Admins"] # More regular users than admins
|
|
40
|
+
|
|
41
|
+
lines = []
|
|
42
|
+
|
|
43
|
+
# RUN record - simulation start (v2.0 format)
|
|
44
|
+
# Format: RUN\tsimulation_fqcn\trun_id\tnormalized_name\ttimestamp\t \tversion
|
|
45
|
+
lines.append(f"RUN\t{simulation_fqcn}\t{run_id}\t{normalized_name}\t{base_timestamp}\t \t2.0")
|
|
46
|
+
|
|
47
|
+
# USER records - user sessions start
|
|
48
|
+
user_sessions = [] # Track (group, id) tuples
|
|
49
|
+
for user_id in range(user_count):
|
|
50
|
+
numeric_id = user_id + 1
|
|
51
|
+
user_group = user_groups[user_id % len(user_groups)]
|
|
52
|
+
start_time = base_timestamp + (user_id * duration_ms // user_count)
|
|
53
|
+
user_sessions.append((user_group, numeric_id, start_time))
|
|
54
|
+
lines.append(f"USER\t{user_group}\t{numeric_id}\tSTART\t{start_time}\t{start_time}")
|
|
55
|
+
|
|
56
|
+
# Generate varied request names
|
|
57
|
+
request_types = ["Home", "Home Redirect 1", "Search", "Select", "Page 0", "Page 1",
|
|
58
|
+
"Page 2", "Page 3", "Form", "Post", "Post Redirect 1"]
|
|
59
|
+
|
|
60
|
+
# REQUEST records
|
|
61
|
+
request_interval = duration_ms // num_requests if num_requests > 0 else 100
|
|
62
|
+
|
|
63
|
+
for i in range(num_requests):
|
|
64
|
+
user_idx = i % user_count
|
|
65
|
+
user_group, user_id, _ = user_sessions[user_idx]
|
|
66
|
+
request_name = request_types[i % len(request_types)]
|
|
67
|
+
|
|
68
|
+
request_start = base_timestamp + (i * request_interval)
|
|
69
|
+
# Add variance: +/- 40% of average duration
|
|
70
|
+
avg_duration = request_interval // 2
|
|
71
|
+
request_duration = int(avg_duration * random.uniform(0.6, 1.4))
|
|
72
|
+
request_end = request_start + request_duration
|
|
73
|
+
|
|
74
|
+
# Determine if this request should fail
|
|
75
|
+
should_fail = False
|
|
76
|
+
if test_status == "failed":
|
|
77
|
+
should_fail = True
|
|
78
|
+
elif test_status == "mixed" and i % 4 == 0:
|
|
79
|
+
should_fail = True
|
|
80
|
+
|
|
81
|
+
if should_fail:
|
|
82
|
+
status = "KO"
|
|
83
|
+
message = "status.find.is(201), but actually found 200"
|
|
84
|
+
else:
|
|
85
|
+
status = "OK"
|
|
86
|
+
message = " " # Single space for OK status
|
|
87
|
+
|
|
88
|
+
# REQUEST format (v2.0): REQUEST\tuser_group\tuser_id\t\trequest_name\tstart\tend\tstatus\tmessage
|
|
89
|
+
# Note: Empty field between user_id and request_name (double tab)
|
|
90
|
+
lines.append(
|
|
91
|
+
f"REQUEST\t{user_group}\t{user_id}\t\t{request_name}\t"
|
|
92
|
+
f"{request_start}\t{request_end}\t{status}\t{message}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# USER records - user sessions end
|
|
96
|
+
end_time = base_timestamp + duration_ms
|
|
97
|
+
for user_group, user_id, start_time in user_sessions:
|
|
98
|
+
lines.append(f"USER\t{user_group}\t{user_id}\tEND\t{start_time}\t{end_time}")
|
|
99
|
+
|
|
100
|
+
# Write to file
|
|
101
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
102
|
+
f.write("\n".join(lines))
|
|
103
|
+
f.write("\n") # Trailing newline
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""JUnit XML test data generator."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from xml.etree import ElementTree as ET
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate(
|
|
11
|
+
output_path: Path,
|
|
12
|
+
test_status: str = "passed",
|
|
13
|
+
handler_def: Dict = None,
|
|
14
|
+
num_tests: int = 5,
|
|
15
|
+
duration_s: float = 2.5,
|
|
16
|
+
**kwargs
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Generate a JUnit XML result file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
output_path: Path where the XML file should be written
|
|
22
|
+
test_status: 'passed', 'failed', or 'mixed'
|
|
23
|
+
handler_def: Handler definition from handlers.yaml (unused here)
|
|
24
|
+
num_tests: Number of test cases to generate
|
|
25
|
+
duration_s: Total duration in seconds
|
|
26
|
+
**kwargs: Additional parameters (ignored)
|
|
27
|
+
"""
|
|
28
|
+
# Create root testsuite element
|
|
29
|
+
testsuite = ET.Element("testsuite")
|
|
30
|
+
testsuite.set("name", "RobotmkBridgeTests")
|
|
31
|
+
testsuite.set("tests", str(num_tests))
|
|
32
|
+
testsuite.set("time", f"{duration_s:.3f}")
|
|
33
|
+
testsuite.set("timestamp", datetime.now().strftime("%Y-%m-%dT%H:%M:%S"))
|
|
34
|
+
|
|
35
|
+
failures = 0
|
|
36
|
+
errors = 0
|
|
37
|
+
|
|
38
|
+
# Generate test cases based on test_status
|
|
39
|
+
for i in range(num_tests):
|
|
40
|
+
testcase = ET.SubElement(testsuite, "testcase")
|
|
41
|
+
testcase.set("classname", f"tests.example.TestSuite{i // 3 + 1}")
|
|
42
|
+
testcase.set("name", f"test_example_{i + 1}")
|
|
43
|
+
# Add variance: +/- 30% of average time
|
|
44
|
+
avg_time = duration_s / num_tests
|
|
45
|
+
test_time = avg_time * random.uniform(0.7, 1.3)
|
|
46
|
+
testcase.set("time", f"{test_time:.3f}")
|
|
47
|
+
|
|
48
|
+
# Determine if this test should fail
|
|
49
|
+
should_fail = False
|
|
50
|
+
if test_status == "failed":
|
|
51
|
+
should_fail = True
|
|
52
|
+
elif test_status == "mixed" and i % 3 == 0:
|
|
53
|
+
should_fail = True
|
|
54
|
+
|
|
55
|
+
if should_fail:
|
|
56
|
+
if i % 2 == 0:
|
|
57
|
+
# Add failure
|
|
58
|
+
failure = ET.SubElement(testcase, "failure")
|
|
59
|
+
failure.set("message", f"Assertion failed: Expected value was incorrect")
|
|
60
|
+
failure.set("type", "AssertionError")
|
|
61
|
+
failure.text = f"AssertionError: Expected 'success' but got 'failure' at line {i * 10 + 42}"
|
|
62
|
+
failures += 1
|
|
63
|
+
else:
|
|
64
|
+
# Add error
|
|
65
|
+
error = ET.SubElement(testcase, "error")
|
|
66
|
+
error.set("message", f"Test runtime error")
|
|
67
|
+
error.set("type", "RuntimeError")
|
|
68
|
+
error.text = f"RuntimeError: Connection timeout after 30 seconds"
|
|
69
|
+
errors += 1
|
|
70
|
+
else:
|
|
71
|
+
# Optionally add system-out for passed tests
|
|
72
|
+
if i % 2 == 0:
|
|
73
|
+
system_out = ET.SubElement(testcase, "system-out")
|
|
74
|
+
system_out.text = f"Test {i + 1} executed successfully with no warnings"
|
|
75
|
+
|
|
76
|
+
# Update testsuite attributes
|
|
77
|
+
testsuite.set("failures", str(failures))
|
|
78
|
+
testsuite.set("errors", str(errors))
|
|
79
|
+
testsuite.set("skipped", "0")
|
|
80
|
+
|
|
81
|
+
# Write XML to file
|
|
82
|
+
tree = ET.ElementTree(testsuite)
|
|
83
|
+
ET.indent(tree, space=" ")
|
|
84
|
+
tree.write(output_path, encoding="utf-8", xml_declaration=True)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""OWASP ZAP XML test data generator."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate(
|
|
10
|
+
output_path: Path,
|
|
11
|
+
test_status: str = "passed",
|
|
12
|
+
handler_def: Dict = None,
|
|
13
|
+
num_sites: int = 3,
|
|
14
|
+
**kwargs
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Generate an OWASP ZAP XML report file matching v2.7.0 format.
|
|
17
|
+
|
|
18
|
+
ZAP reports contain alerts with risk levels (0=Info, 1=Low, 2=Medium, 3=High)
|
|
19
|
+
and confidence levels (0=Low, 1=Medium, 2=High, 3=Confirmed).
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
output_path: Path where the XML file should be written
|
|
23
|
+
test_status: 'passed' (low risk only), 'failed' (high risk), 'mixed' (varied)
|
|
24
|
+
handler_def: Handler definition from handlers.yaml (unused here)
|
|
25
|
+
num_sites: Number of sites to include in report
|
|
26
|
+
**kwargs: Additional parameters (ignored)
|
|
27
|
+
"""
|
|
28
|
+
# Generate timestamp in ZAP format: "Tue, 7 Aug 2018 13:17:56"
|
|
29
|
+
now = datetime.now()
|
|
30
|
+
timestamp = now.strftime("%a, %-d %b %Y %H:%M:%S")
|
|
31
|
+
|
|
32
|
+
lines = ['<?xml version="1.0"?>']
|
|
33
|
+
lines.append(f'<OWASPZAPReport version="2.7.0" generated="{timestamp}">')
|
|
34
|
+
|
|
35
|
+
# Define sites to scan with varied number
|
|
36
|
+
sites = [
|
|
37
|
+
{"name": "http://localhost:7272", "host": "localhost", "port": "7272", "ssl": "false"},
|
|
38
|
+
{"name": "http://127.0.0.1:7272", "host": "127.0.0.1", "port": "7272", "ssl": "false"},
|
|
39
|
+
{"name": "http://192.168.50.56:7272", "host": "192.168.50.56", "port": "7272", "ssl": "false"},
|
|
40
|
+
][:num_sites]
|
|
41
|
+
|
|
42
|
+
# Get all available alert types - only use low risk (passing tests)
|
|
43
|
+
low_alerts = get_low_risk_alerts()
|
|
44
|
+
|
|
45
|
+
# Generate sites with alerts (only low-risk = all passing)
|
|
46
|
+
for site_idx, site_config in enumerate(sites):
|
|
47
|
+
lines.append(f'<site name="{site_config["name"]}" host="{site_config["host"]}" '
|
|
48
|
+
f'port="{site_config["port"]}" ssl="{site_config["ssl"]}">')
|
|
49
|
+
lines.append('<alerts>')
|
|
50
|
+
|
|
51
|
+
# Randomize number of alerts per site (vary runtime simulation)
|
|
52
|
+
num_alerts = random.randint(2, 6)
|
|
53
|
+
site_alerts = []
|
|
54
|
+
|
|
55
|
+
for _ in range(num_alerts):
|
|
56
|
+
# Only choose low risk alerts (all tests pass)
|
|
57
|
+
alert = random.choice(low_alerts).copy()
|
|
58
|
+
|
|
59
|
+
# Vary the number of instances (affects processing time)
|
|
60
|
+
alert["max_instances"] = random.randint(1, 8)
|
|
61
|
+
site_alerts.append(alert)
|
|
62
|
+
|
|
63
|
+
for alert in site_alerts:
|
|
64
|
+
lines.extend(generate_alert_xml(alert, site_config, site_idx))
|
|
65
|
+
|
|
66
|
+
lines.append('</alerts>')
|
|
67
|
+
lines.append('</site>')
|
|
68
|
+
|
|
69
|
+
lines.append('</OWASPZAPReport>\n')
|
|
70
|
+
|
|
71
|
+
# Write to file
|
|
72
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
73
|
+
f.write("".join(lines))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def generate_alert_xml(alert_template: Dict, site_config: Dict, site_idx: int):
|
|
77
|
+
"""Generate XML lines for a single alert item."""
|
|
78
|
+
lines = ['<alertitem>\n']
|
|
79
|
+
|
|
80
|
+
# Core alert info
|
|
81
|
+
lines.append(f' <pluginid>{alert_template["pluginid"]}</pluginid>\n')
|
|
82
|
+
lines.append(f' <alert>{alert_template["name"]}</alert>\n')
|
|
83
|
+
lines.append(f' <name>{alert_template["name"]}</name>\n')
|
|
84
|
+
lines.append(f' <riskcode>{alert_template["riskcode"]}</riskcode>\n')
|
|
85
|
+
lines.append(f' <confidence>{alert_template["confidence"]}</confidence>\n')
|
|
86
|
+
lines.append(f' <riskdesc>{alert_template["riskdesc"]}</riskdesc>\n')
|
|
87
|
+
lines.append(f' <desc>{alert_template["desc"]}</desc>\n')
|
|
88
|
+
|
|
89
|
+
# Instances - vary count to simulate different scan complexities/runtimes
|
|
90
|
+
lines.append(' <instances>\n')
|
|
91
|
+
max_instances = alert_template.get("max_instances", 3)
|
|
92
|
+
num_instances = random.randint(1, max_instances)
|
|
93
|
+
|
|
94
|
+
for i in range(num_instances):
|
|
95
|
+
lines.append(' <instance>\n')
|
|
96
|
+
|
|
97
|
+
# Generate URI variations
|
|
98
|
+
base_url = site_config["name"]
|
|
99
|
+
paths = alert_template.get("paths", ["/", "/index.html", "/api/data"])
|
|
100
|
+
uri = f'{base_url}{random.choice(paths)}'
|
|
101
|
+
lines.append(f' <uri>{uri}</uri>\n')
|
|
102
|
+
|
|
103
|
+
method = random.choice(["GET", "POST"])
|
|
104
|
+
lines.append(f' <method>{method}</method>\n')
|
|
105
|
+
|
|
106
|
+
lines.append(f' <param>{alert_template["param"]}</param>\n')
|
|
107
|
+
|
|
108
|
+
if "evidence" in alert_template:
|
|
109
|
+
lines.append(f' <evidence>{alert_template["evidence"]}</evidence>\n')
|
|
110
|
+
|
|
111
|
+
lines.append(' </instance>\n')
|
|
112
|
+
|
|
113
|
+
lines.append(' </instances>\n')
|
|
114
|
+
lines.append(f' <count>{num_instances}</count>\n')
|
|
115
|
+
|
|
116
|
+
# Solutions and references
|
|
117
|
+
lines.append(f' <solution>{alert_template["solution"]}</solution>\n')
|
|
118
|
+
|
|
119
|
+
if "otherinfo" in alert_template:
|
|
120
|
+
lines.append(f' <otherinfo>{alert_template["otherinfo"]}</otherinfo>\n')
|
|
121
|
+
|
|
122
|
+
lines.append(f' <reference>{alert_template["reference"]}</reference>\n')
|
|
123
|
+
lines.append(f' <cweid>{alert_template["cweid"]}</cweid>\n')
|
|
124
|
+
lines.append(f' <wascid>{alert_template["wascid"]}</wascid>\n')
|
|
125
|
+
lines.append(f' <sourceid>3</sourceid>\n')
|
|
126
|
+
lines.append('</alertitem>\n')
|
|
127
|
+
|
|
128
|
+
return lines
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_low_risk_alerts():
|
|
132
|
+
"""Return low-risk security alerts."""
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
"pluginid": "10021",
|
|
136
|
+
"name": "X-Content-Type-Options Header Missing",
|
|
137
|
+
"riskcode": "1",
|
|
138
|
+
"confidence": "2",
|
|
139
|
+
"riskdesc": "Low (Medium)",
|
|
140
|
+
"desc": "<p>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>",
|
|
141
|
+
"param": "X-Content-Type-Options",
|
|
142
|
+
"max_instances": 5,
|
|
143
|
+
"paths": ["/", "/demo.css", "/welcome.html", "/error.html"],
|
|
144
|
+
"solution": "<p>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><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>",
|
|
145
|
+
"otherinfo": "<p>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><p>At "High" threshold this scanner will not alert on client or server error responses.</p>",
|
|
146
|
+
"reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
|
|
147
|
+
"cweid": "16",
|
|
148
|
+
"wascid": "15",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"pluginid": "10016",
|
|
152
|
+
"name": "Web Browser XSS Protection Not Enabled",
|
|
153
|
+
"riskcode": "1",
|
|
154
|
+
"confidence": "2",
|
|
155
|
+
"riskdesc": "Low (Medium)",
|
|
156
|
+
"desc": "<p>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>",
|
|
157
|
+
"param": "X-XSS-Protection",
|
|
158
|
+
"max_instances": 7,
|
|
159
|
+
"paths": ["/", "/welcome.html", "/error.html", "/favicon.ico", "/robots.txt", "/sitemap.xml"],
|
|
160
|
+
"solution": "<p>Ensure that the web browser's XSS filter is enabled, by setting the X-XSS-Protection HTTP response header to '1'.</p>",
|
|
161
|
+
"otherinfo": "<p>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><p>X-XSS-Protection: 1; mode=block</p><p>X-XSS-Protection: 1; report=http://www.example.com/xss</p><p>The following values would disable it:</p><p>X-XSS-Protection: 0</p><p>The X-XSS-Protection HTTP response header is currently supported on Internet Explorer, Chrome and Safari (WebKit).</p><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>",
|
|
162
|
+
"reference": "<p>https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet</p><p>https://blog.veracode.com/2014/03/guidelines-for-setting-security-headers/</p>",
|
|
163
|
+
"cweid": "933",
|
|
164
|
+
"wascid": "14",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"pluginid": "10012",
|
|
168
|
+
"name": "Password Autocomplete in Browser",
|
|
169
|
+
"riskcode": "1",
|
|
170
|
+
"confidence": "2",
|
|
171
|
+
"riskdesc": "Low (Medium)",
|
|
172
|
+
"desc": "<p>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>",
|
|
173
|
+
"param": "password_field",
|
|
174
|
+
"max_instances": 2,
|
|
175
|
+
"paths": ["/", "/login"],
|
|
176
|
+
"evidence": "<input id="password_field" size="30" type="password">",
|
|
177
|
+
"solution": "<p>Turn off the AUTOCOMPLETE attribute in forms or individual input elements containing password inputs by using AUTOCOMPLETE='OFF'.</p>",
|
|
178
|
+
"reference": "<p>http://www.w3schools.com/tags/att_input_autocomplete.asp</p><p>https://msdn.microsoft.com/en-us/library/ms533486%28v=vs.85%29.aspx</p>",
|
|
179
|
+
"cweid": "525",
|
|
180
|
+
"wascid": "15",
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_high_risk_alerts():
|
|
186
|
+
"""Return high-risk security alerts."""
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
"pluginid": "40018",
|
|
190
|
+
"name": "SQL Injection",
|
|
191
|
+
"riskcode": "3",
|
|
192
|
+
"confidence": "3",
|
|
193
|
+
"riskdesc": "High (High)",
|
|
194
|
+
"desc": "<p>SQL injection may be possible.</p>",
|
|
195
|
+
"param": "id",
|
|
196
|
+
"max_instances": 3,
|
|
197
|
+
"paths": ["/api/users", "/search", "/products"],
|
|
198
|
+
"evidence": "You have an error in your SQL syntax",
|
|
199
|
+
"solution": "<p>Use prepared statements and parameterized queries.</p>",
|
|
200
|
+
"reference": "<p>https://www.owasp.org/index.php/SQL_Injection</p>",
|
|
201
|
+
"cweid": "89",
|
|
202
|
+
"wascid": "19",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"pluginid": "40012",
|
|
206
|
+
"name": "Cross Site Scripting (Reflected)",
|
|
207
|
+
"riskcode": "3",
|
|
208
|
+
"confidence": "2",
|
|
209
|
+
"riskdesc": "High (Medium)",
|
|
210
|
+
"desc": "<p>Cross-site Scripting (XSS) is possible.</p>",
|
|
211
|
+
"param": "query",
|
|
212
|
+
"max_instances": 2,
|
|
213
|
+
"paths": ["/search", "/comment"],
|
|
214
|
+
"evidence": "<script>alert(1)</script>",
|
|
215
|
+
"solution": "<p>Validate all input and encode all output.</p>",
|
|
216
|
+
"reference": "<p>https://www.owasp.org/index.php/XSS</p>",
|
|
217
|
+
"cweid": "79",
|
|
218
|
+
"wascid": "8",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"pluginid": "10020",
|
|
222
|
+
"name": "X-Frame-Options Header Not Set",
|
|
223
|
+
"riskcode": "2",
|
|
224
|
+
"confidence": "2",
|
|
225
|
+
"riskdesc": "Medium (Medium)",
|
|
226
|
+
"desc": "<p>X-Frame-Options header is not included in the HTTP response to protect against 'ClickJacking' attacks.</p>",
|
|
227
|
+
"param": "X-Frame-Options",
|
|
228
|
+
"max_instances": 4,
|
|
229
|
+
"paths": ["/", "/welcome.html", "/error.html"],
|
|
230
|
+
"solution": "<p>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>",
|
|
231
|
+
"reference": "<p>http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx</p>",
|
|
232
|
+
"cweid": "16",
|
|
233
|
+
"wascid": "15",
|
|
234
|
+
},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_mixed_risk_alerts():
|
|
239
|
+
"""Return mix of risk levels."""
|
|
240
|
+
return get_low_risk_alerts() + [
|
|
241
|
+
{
|
|
242
|
+
"pluginid": "10020",
|
|
243
|
+
"name": "X-Frame-Options Header Not Set",
|
|
244
|
+
"riskcode": "2",
|
|
245
|
+
"confidence": "2",
|
|
246
|
+
"riskdesc": "Medium (Medium)",
|
|
247
|
+
"desc": "<p>X-Frame-Options header is not included in the HTTP response to protect against 'ClickJacking' attacks.</p>",
|
|
248
|
+
"param": "X-Frame-Options",
|
|
249
|
+
"max_instances": 4,
|
|
250
|
+
"paths": ["/", "/welcome.html", "/error.html"],
|
|
251
|
+
"solution": "<p>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>",
|
|
252
|
+
"reference": "<p>http://blogs.msdn.com/b/ieinternals/archive/2010/03/30/combating-clickjacking-with-x-frame-options.aspx</p>",
|
|
253
|
+
"cweid": "16",
|
|
254
|
+
"wascid": "15",
|
|
255
|
+
},
|
|
256
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# handlers.yaml — Central registry of supported Robotmk Bridge handlers.
|
|
2
|
+
#
|
|
3
|
+
# This file is the single source of truth for:
|
|
4
|
+
# 1. The Bakery UI (handler dropdown choices + per-handler parameter fields),
|
|
5
|
+
# generated by scripts/generate_bakery_handler_fields.py.
|
|
6
|
+
# 2. The test data generator (tests/data_generator/).
|
|
7
|
+
#
|
|
8
|
+
# Fields per handler:
|
|
9
|
+
# name: Internal key; must match the handler name expected by RobotmkBridgeCore.
|
|
10
|
+
# title: Human-readable name shown in the Bakery UI.
|
|
11
|
+
# class_import: Fully qualified Python class path (for introspection).
|
|
12
|
+
# result_ext: Typical file extension of result files produced by this tool.
|
|
13
|
+
# description: Short one-liner for documentation purposes.
|
|
14
|
+
|
|
15
|
+
handlers:
|
|
16
|
+
- name: junit
|
|
17
|
+
title: JUnit
|
|
18
|
+
class_import: rmkbridge.junit.JUnitHandler
|
|
19
|
+
result_ext: xml
|
|
20
|
+
description: >
|
|
21
|
+
Parses JUnit XML result files produced by any JUnit-compatible
|
|
22
|
+
test framework (pytest, Maven Surefire, NUnit, etc.).
|
|
23
|
+
|
|
24
|
+
- name: gatling
|
|
25
|
+
title: Gatling
|
|
26
|
+
class_import: rmkbridge.gatling.GatlingHandler
|
|
27
|
+
result_ext: log
|
|
28
|
+
description: >
|
|
29
|
+
Parses Gatling simulation log files (simulation.log) produced
|
|
30
|
+
by Gatling performance testing runs.
|
|
31
|
+
|
|
32
|
+
- name: zaproxy
|
|
33
|
+
title: OWASP ZAP (Zed Attack Proxy)
|
|
34
|
+
class_import: rmkbridge.zap.ZAProxyHandler
|
|
35
|
+
result_ext: xml
|
|
36
|
+
description: >
|
|
37
|
+
Parses OWASP ZAP security scan result files (XML or JSON format).
|
|
38
|
+
Supports filtering by accepted_risk_level and required_confidence_level.
|
|
39
|
+
handler_params:
|
|
40
|
+
- name: accepted_risk_level
|
|
41
|
+
type: int
|
|
42
|
+
required: false
|
|
43
|
+
default: null
|
|
44
|
+
description: >
|
|
45
|
+
Minimum risk level of alerts to include (0=Info, 1=Low, 2=Medium, 3=High).
|
|
46
|
+
Alerts below this level are ignored.
|
|
47
|
+
- name: required_confidence_level
|
|
48
|
+
type: int
|
|
49
|
+
required: false
|
|
50
|
+
default: null
|
|
51
|
+
description: >
|
|
52
|
+
Minimum confidence level of alerts to include (1=Low, 2=Medium, 3=High).
|
|
53
|
+
Alerts below this confidence are ignored.
|