robotframework-velo-cli 0.1.7__py3-none-any.whl
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.
- robotframework_velo_cli-0.1.7.dist-info/METADATA +293 -0
- robotframework_velo_cli-0.1.7.dist-info/RECORD +15 -0
- robotframework_velo_cli-0.1.7.dist-info/WHEEL +5 -0
- robotframework_velo_cli-0.1.7.dist-info/entry_points.txt +3 -0
- robotframework_velo_cli-0.1.7.dist-info/licenses/LICENSE +49 -0
- robotframework_velo_cli-0.1.7.dist-info/top_level.txt +1 -0
- velo/__init__.py +6 -0
- velo/cli.py +29 -0
- velo/constants.py +6 -0
- velo/debug.py +143 -0
- velo/ignore.py +41 -0
- velo/py.typed +0 -0
- velo/runner.py +124 -0
- velo/sap_client.py +18 -0
- velo/streamer.py +63 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robotframework-velo-cli
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Run Robot Framework test suites on the Velo cloud platform
|
|
5
|
+
Author-email: Velo <support@velo.com>
|
|
6
|
+
License: Copyright (c) 2026 Velo. All rights reserved.
|
|
7
|
+
|
|
8
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
9
|
+
|
|
10
|
+
This software and its source code, documentation, and associated files
|
|
11
|
+
(collectively, the "Software") are the exclusive property of Velo and are
|
|
12
|
+
protected by copyright law and international treaties.
|
|
13
|
+
|
|
14
|
+
GRANT OF LICENSE
|
|
15
|
+
|
|
16
|
+
Velo grants you a limited, non-exclusive, non-transferable, non-sub licensable
|
|
17
|
+
license to use the Software solely for your internal business purposes,
|
|
18
|
+
strictly in accordance with any agreement entered into with Velo.
|
|
19
|
+
|
|
20
|
+
RESTRICTIONS
|
|
21
|
+
|
|
22
|
+
You may not, and you may not permit any third party to:
|
|
23
|
+
|
|
24
|
+
1. Copy, modify, adapt, translate, or create derivative works of the Software;
|
|
25
|
+
2. Reverse engineer, disassemble, decompile, or otherwise attempt to derive
|
|
26
|
+
the source code of the Software;
|
|
27
|
+
3. Sell, sublicense, rent, lease, transfer, or otherwise make the Software
|
|
28
|
+
available to any third party;
|
|
29
|
+
4. Remove or alter any proprietary notices, labels, or marks on the Software;
|
|
30
|
+
5. Use the Software for any purpose other than as expressly permitted
|
|
31
|
+
under this license.
|
|
32
|
+
|
|
33
|
+
NO WARRANTY
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
36
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
37
|
+
FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL VELO BE
|
|
38
|
+
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
|
|
39
|
+
THE SOFTWARE.
|
|
40
|
+
|
|
41
|
+
TERMINATION
|
|
42
|
+
|
|
43
|
+
This license is effective until terminated. It will terminate automatically
|
|
44
|
+
if you fail to comply with any of its terms. Upon termination, you must
|
|
45
|
+
immediately cease all use of the Software and destroy any copies in your
|
|
46
|
+
possession.
|
|
47
|
+
|
|
48
|
+
GOVERNING LAW
|
|
49
|
+
|
|
50
|
+
This license shall be governed by and construed in accordance with applicable
|
|
51
|
+
law. Any disputes arising under this license shall be subject to the exclusive
|
|
52
|
+
jurisdiction of the competent courts.
|
|
53
|
+
|
|
54
|
+
For licensing inquiries, contact: legal@velo.com
|
|
55
|
+
|
|
56
|
+
Project-URL: Homepage, https://velo.com
|
|
57
|
+
Project-URL: Bug Tracker, https://github.com/velo/robotframework-velo-cli/issues
|
|
58
|
+
Keywords: robotframework,robot,testing,automation,velo,sap
|
|
59
|
+
Classifier: Development Status :: 3 - Alpha
|
|
60
|
+
Classifier: Framework :: Robot Framework
|
|
61
|
+
Classifier: Framework :: Robot Framework :: Tool
|
|
62
|
+
Classifier: Intended Audience :: Developers
|
|
63
|
+
Classifier: Intended Audience :: Information Technology
|
|
64
|
+
Classifier: Programming Language :: Python :: 3
|
|
65
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
66
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
67
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
68
|
+
Classifier: Operating System :: OS Independent
|
|
69
|
+
Classifier: Topic :: Software Development :: Testing
|
|
70
|
+
Requires-Python: >=3.10
|
|
71
|
+
Description-Content-Type: text/markdown
|
|
72
|
+
License-File: LICENSE
|
|
73
|
+
Requires-Dist: robotframework>=6.0
|
|
74
|
+
Requires-Dist: requests>=2.31
|
|
75
|
+
Requires-Dist: pathspec>=0.12
|
|
76
|
+
Provides-Extra: dev
|
|
77
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
78
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
79
|
+
Requires-Dist: ruff; extra == "dev"
|
|
80
|
+
Requires-Dist: mypy; extra == "dev"
|
|
81
|
+
Dynamic: license-file
|
|
82
|
+
|
|
83
|
+
# robotframework-velo-cli
|
|
84
|
+
|
|
85
|
+
Run Robot Framework test suites on the Velo cloud platform — no SAP GUI setup required on your machine.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Installation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install robotframework-velo-cli
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Note:** This package intentionally installs a `robot` command that wraps and replaces the standard Robot Framework entry point. `robotframework` is a declared dependency and will be installed automatically. Because `robotframework-velo-cli` is installed after `robotframework`, its `robot` script takes precedence. When `VELO_REMOTE` is not set the wrapper passes all arguments through to Robot Framework unchanged — your existing workflow is unaffected.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quick start
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 1. Set your workspace credentials
|
|
103
|
+
export VELO_API_KEY=<your-api-key>
|
|
104
|
+
export VELO_API_BASE=http://<velo-api-host>:8000 # default: http://localhost:8000
|
|
105
|
+
|
|
106
|
+
# 2. Run your tests remotely — same command you always use
|
|
107
|
+
VELO_REMOTE=1 robot ./tests
|
|
108
|
+
|
|
109
|
+
# 3. With tag filtering — all standard RF flags pass through unchanged
|
|
110
|
+
VELO_REMOTE=1 robot --include smoke --exclude wip ./tests
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Execution modes
|
|
116
|
+
|
|
117
|
+
| Variable | Value | Behaviour |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `VELO_REMOTE` | `0` (default) | Package is inert — `robot` runs locally as normal |
|
|
120
|
+
| `VELO_REMOTE` | `1` | Suite is packaged, uploaded, and executed on the Velo platform |
|
|
121
|
+
| `VELO_DEBUG` | `1` | Local debug: native SAP GUI for Windows, or Docker Java elsewhere |
|
|
122
|
+
| `VELO_SAP_CLIENT` | `auto` | SAP backend: `auto`, `java`, or `windows` (see debug mode below) |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Environment variables
|
|
127
|
+
|
|
128
|
+
| Variable | Required | Default | Description |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| `VELO_API_KEY` | Yes (remote) | — | Workspace API key |
|
|
131
|
+
| `VELO_API_BASE` | No | `http://localhost:8000` | Velo API base URL |
|
|
132
|
+
| `VELO_REMOTE` | No | `0` | Set to `1` to enable remote execution |
|
|
133
|
+
| `VELO_DEBUG` | No | `0` | Set to `1` for local debug (Windows native or Docker Java) |
|
|
134
|
+
| `VELO_SAP_CLIENT` | No | `auto` | SAP client: `auto`, `java`, or `windows` |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Remote execution flow
|
|
139
|
+
|
|
140
|
+
When `VELO_REMOTE=1`, the following happens automatically:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
1. Scan working directory and apply .veloignore rules
|
|
144
|
+
2. Package directory into a .zip archive
|
|
145
|
+
3. Upload archive to the Velo API
|
|
146
|
+
4. Trigger a remote run (optionally with --include / --exclude tags)
|
|
147
|
+
5. Stream Robot Framework log output to your terminal in real time
|
|
148
|
+
6. On completion: download log.html + report.html to ./results/
|
|
149
|
+
7. Exit with the standard RF exit code
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The terminal experience is identical to a local RF run — log lines appear as tests execute, not buffered.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## What gets uploaded
|
|
157
|
+
|
|
158
|
+
The entire current working directory is packaged into a `.zip` archive when you run `robot`. The archive is created from the directory you run the command in, preserving the full folder structure.
|
|
159
|
+
|
|
160
|
+
**Typical archive contents:**
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
tests/
|
|
164
|
+
smoke/
|
|
165
|
+
login.robot
|
|
166
|
+
regression/
|
|
167
|
+
sales_order.robot
|
|
168
|
+
resources/
|
|
169
|
+
keywords.robot
|
|
170
|
+
variables/
|
|
171
|
+
common.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Only files are included — empty directories are omitted. The archive is uploaded to the API, extracted into the execution container, and `robot` is run against the entire directory.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## .veloignore
|
|
179
|
+
|
|
180
|
+
A `.veloignore` file in the project root controls which files are excluded from the uploaded archive. It uses the same syntax as `.gitignore` (glob patterns, `#` comments, negation with `!`).
|
|
181
|
+
|
|
182
|
+
Place it at the root of your test project:
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
your-project/
|
|
186
|
+
├── .veloignore ← here
|
|
187
|
+
├── tests/
|
|
188
|
+
├── resources/
|
|
189
|
+
└── ...
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Default exclusions
|
|
193
|
+
|
|
194
|
+
When **no `.veloignore` file is present**, the following are excluded automatically:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
.git
|
|
198
|
+
__pycache__
|
|
199
|
+
*.pyc
|
|
200
|
+
*.pyo
|
|
201
|
+
venv
|
|
202
|
+
.venv
|
|
203
|
+
node_modules
|
|
204
|
+
.env
|
|
205
|
+
.env.*
|
|
206
|
+
results
|
|
207
|
+
*.log
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Important: defaults are replaced, not merged
|
|
211
|
+
|
|
212
|
+
If a `.veloignore` file exists, **it completely replaces the default list** — the defaults above are no longer applied. Include any defaults you still want in your `.veloignore`.
|
|
213
|
+
|
|
214
|
+
### Example .veloignore
|
|
215
|
+
|
|
216
|
+
```gitignore
|
|
217
|
+
# Re-include sensible defaults
|
|
218
|
+
.git
|
|
219
|
+
__pycache__
|
|
220
|
+
*.pyc
|
|
221
|
+
venv
|
|
222
|
+
.venv
|
|
223
|
+
.env
|
|
224
|
+
.env.*
|
|
225
|
+
results
|
|
226
|
+
*.log
|
|
227
|
+
|
|
228
|
+
# Project-specific exclusions
|
|
229
|
+
data/sensitive/
|
|
230
|
+
config/secrets.yaml
|
|
231
|
+
*.csv
|
|
232
|
+
docs/
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Verifying exclusions
|
|
236
|
+
|
|
237
|
+
After a run, inspect the uploaded archive on the API server:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
unzip -l <storage_root>/suites/<suite_id>.zip
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Artifacts
|
|
246
|
+
|
|
247
|
+
When the run completes, the following are downloaded to your local `--outputdir` (default: `./results/`):
|
|
248
|
+
|
|
249
|
+
| File | Description |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `log.html` | Full Robot Framework execution log with keyword-level detail |
|
|
252
|
+
| `report.html` | Test suite summary report |
|
|
253
|
+
|
|
254
|
+
The video recording (`recording.mp4`) is stored on the server and accessible via the API at `GET /api/runs/{run_id}/artifacts/video`.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Exit codes
|
|
259
|
+
|
|
260
|
+
The package preserves standard RF exit codes so existing CI scripts and Makefiles work without modification:
|
|
261
|
+
|
|
262
|
+
| Code | Meaning |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `0` | All tests passed |
|
|
265
|
+
| `1` | One or more tests failed |
|
|
266
|
+
| `2` | Invalid RF options or arguments |
|
|
267
|
+
| `3` | Test execution stopped by user |
|
|
268
|
+
| `252` | Help or version info printed |
|
|
269
|
+
| `253` | Platform error (upload failed, API unreachable, run did not start) |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## RF flag pass-through
|
|
274
|
+
|
|
275
|
+
All standard `robot` flags are forwarded to the remote execution environment:
|
|
276
|
+
|
|
277
|
+
| Flag | Behaviour |
|
|
278
|
+
|---|---|
|
|
279
|
+
| `--include TAG` / `--exclude TAG` | Passed to remote RF runner |
|
|
280
|
+
| `--variable KEY:VALUE` | Passed to remote RF runner |
|
|
281
|
+
| `--suite SUITE` | Passed to remote RF runner |
|
|
282
|
+
| `--outputdir PATH` | Controls local download destination for results |
|
|
283
|
+
| `--dryrun` | Executes locally — remote is not triggered |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Development install (editable)
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
git clone <repo>
|
|
291
|
+
cd velo
|
|
292
|
+
pip install -e packages/robotframework-velo-cli
|
|
293
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
robotframework_velo_cli-0.1.7.dist-info/licenses/LICENSE,sha256=Qx-di54idhaOnrKKU-L9H2p-8JwkT_G157WyhxR-SZg,1906
|
|
2
|
+
velo/__init__.py,sha256=FOkEvPiy2cSVFxVUDAnajrFLEOWbkwHRcqn9ElxoGBk,177
|
|
3
|
+
velo/cli.py,sha256=4XkgsHx5zP7VZBUGK42ZkgZsqwVjFwOXLfwmdFYNgls,720
|
|
4
|
+
velo/constants.py,sha256=u9f5PRzF85ZG3dBTSdBj0sKHh9yrThOWqYxCo4S19T4,161
|
|
5
|
+
velo/debug.py,sha256=vofojwNkS4qFGqp5rBAvEyrIxtYV74zhAO0EwdfA4xs,4176
|
|
6
|
+
velo/ignore.py,sha256=FqUAkEMth2uxuvscUZhBn67MgFwysNSntXQrpLKU3BA,1042
|
|
7
|
+
velo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
velo/runner.py,sha256=qe8f8pslPDHEwBz0v501X5HNAAZmgsXTLoqJWNgpC_A,4053
|
|
9
|
+
velo/sap_client.py,sha256=aEOh43sK_aXvoCAHz-ygBn4zRpl9USGbS_Ex-qu4OFc,592
|
|
10
|
+
velo/streamer.py,sha256=3sQG3mQswS9xSSWHKo2PgsBtOWcAZwNm7hPz19s5xIo,2083
|
|
11
|
+
robotframework_velo_cli-0.1.7.dist-info/METADATA,sha256=DsFbPgdwP7yIWIG0sKl0_W0en-XxzCV4dwYibOi4-hg,9298
|
|
12
|
+
robotframework_velo_cli-0.1.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
13
|
+
robotframework_velo_cli-0.1.7.dist-info/entry_points.txt,sha256=RhLfsXbSOVkzZJ82pF5BNCnUc7AaGjnI-Nt8TfF2v84,66
|
|
14
|
+
robotframework_velo_cli-0.1.7.dist-info/top_level.txt,sha256=DvWsWbYteXtPv_CrlE7HAff1eE_Ayg1c3pLECTGAcQs,5
|
|
15
|
+
robotframework_velo_cli-0.1.7.dist-info/RECORD,,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Copyright (c) 2026 Velo. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
4
|
+
|
|
5
|
+
This software and its source code, documentation, and associated files
|
|
6
|
+
(collectively, the "Software") are the exclusive property of Velo and are
|
|
7
|
+
protected by copyright law and international treaties.
|
|
8
|
+
|
|
9
|
+
GRANT OF LICENSE
|
|
10
|
+
|
|
11
|
+
Velo grants you a limited, non-exclusive, non-transferable, non-sub licensable
|
|
12
|
+
license to use the Software solely for your internal business purposes,
|
|
13
|
+
strictly in accordance with any agreement entered into with Velo.
|
|
14
|
+
|
|
15
|
+
RESTRICTIONS
|
|
16
|
+
|
|
17
|
+
You may not, and you may not permit any third party to:
|
|
18
|
+
|
|
19
|
+
1. Copy, modify, adapt, translate, or create derivative works of the Software;
|
|
20
|
+
2. Reverse engineer, disassemble, decompile, or otherwise attempt to derive
|
|
21
|
+
the source code of the Software;
|
|
22
|
+
3. Sell, sublicense, rent, lease, transfer, or otherwise make the Software
|
|
23
|
+
available to any third party;
|
|
24
|
+
4. Remove or alter any proprietary notices, labels, or marks on the Software;
|
|
25
|
+
5. Use the Software for any purpose other than as expressly permitted
|
|
26
|
+
under this license.
|
|
27
|
+
|
|
28
|
+
NO WARRANTY
|
|
29
|
+
|
|
30
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
31
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
32
|
+
FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL VELO BE
|
|
33
|
+
LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF
|
|
34
|
+
THE SOFTWARE.
|
|
35
|
+
|
|
36
|
+
TERMINATION
|
|
37
|
+
|
|
38
|
+
This license is effective until terminated. It will terminate automatically
|
|
39
|
+
if you fail to comply with any of its terms. Upon termination, you must
|
|
40
|
+
immediately cease all use of the Software and destroy any copies in your
|
|
41
|
+
possession.
|
|
42
|
+
|
|
43
|
+
GOVERNING LAW
|
|
44
|
+
|
|
45
|
+
This license shall be governed by and construed in accordance with applicable
|
|
46
|
+
law. Any disputes arising under this license shall be subject to the exclusive
|
|
47
|
+
jurisdiction of the competent courts.
|
|
48
|
+
|
|
49
|
+
For licensing inquiries, contact: legal@velo.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
velo
|
velo/__init__.py
ADDED
velo/cli.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main() -> None:
|
|
6
|
+
"""Shadows the system `robot` command. Gate 2 implementation pending."""
|
|
7
|
+
args = sys.argv[1:]
|
|
8
|
+
|
|
9
|
+
if os.getenv("VELO_REMOTE") == "1":
|
|
10
|
+
from velo.runner import run_remote
|
|
11
|
+
|
|
12
|
+
sys.exit(run_remote(args))
|
|
13
|
+
|
|
14
|
+
elif os.getenv("VELO_DEBUG") == "1":
|
|
15
|
+
from velo.debug import run_debug
|
|
16
|
+
|
|
17
|
+
sys.exit(run_debug(args))
|
|
18
|
+
|
|
19
|
+
else:
|
|
20
|
+
import subprocess
|
|
21
|
+
|
|
22
|
+
result = subprocess.run([sys.executable, "-m", "robot"] + args)
|
|
23
|
+
sys.exit(result.returncode)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def velo_main() -> None:
|
|
27
|
+
"""Entry point for `velo` CLI subcommands (e.g. velo agent start). Gate 2+."""
|
|
28
|
+
print("velo agent commands are not available in this build.")
|
|
29
|
+
sys.exit(1)
|
velo/constants.py
ADDED
velo/debug.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""VELO_DEBUG=1 — local debug router (native Windows or Docker Java)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from velo.constants import RF_EXIT_PLATFORM_ERROR
|
|
12
|
+
from velo.sap_client import resolve_sap_client
|
|
13
|
+
|
|
14
|
+
DOCKER_DESKTOP_URL = "https://www.docker.com/products/docker-desktop"
|
|
15
|
+
EXECUTION_IMAGE = os.getenv("VELO_EXECUTION_IMAGE", "velo-execution")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_debug(rf_args: list[str]) -> int:
|
|
19
|
+
client = resolve_sap_client()
|
|
20
|
+
if client == "windows":
|
|
21
|
+
return _run_native_windows(rf_args)
|
|
22
|
+
return _run_docker_java(rf_args)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _run_native_windows(rf_args: list[str]) -> int:
|
|
26
|
+
if sys.platform != "win32":
|
|
27
|
+
print(
|
|
28
|
+
"[velo] VELO_DEBUG with SAP GUI for Windows requires a Windows host.",
|
|
29
|
+
file=sys.stderr,
|
|
30
|
+
)
|
|
31
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
32
|
+
|
|
33
|
+
err = _check_windows_scripting()
|
|
34
|
+
if err:
|
|
35
|
+
print(f"[velo] {err}", file=sys.stderr)
|
|
36
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
37
|
+
|
|
38
|
+
env = {**os.environ, "VELO_SAP_CLIENT": "windows"}
|
|
39
|
+
return subprocess.run([sys.executable, "-m", "robot", *rf_args], env=env).returncode
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check_windows_scripting() -> str | None:
|
|
43
|
+
try:
|
|
44
|
+
import win32com.client # type: ignore[import-untyped]
|
|
45
|
+
except ImportError:
|
|
46
|
+
return (
|
|
47
|
+
"pywin32 is required for Windows SAP GUI debug runs. "
|
|
48
|
+
"Install with: pip install pywin32"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
sap_gui = win32com.client.GetObject("SAPGUI")
|
|
53
|
+
engine = sap_gui.GetScriptingEngine
|
|
54
|
+
except Exception:
|
|
55
|
+
return (
|
|
56
|
+
"SAP GUI for Windows is not available. Install SAP GUI, start a session, "
|
|
57
|
+
"and enable scripting in SAP GUI options."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if engine is None:
|
|
61
|
+
return (
|
|
62
|
+
"SAP GUI Scripting is disabled. Enable it in SAP GUI settings and verify "
|
|
63
|
+
"server parameter sapgui/user_scripting."
|
|
64
|
+
)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_docker_java(rf_args: list[str]) -> int:
|
|
69
|
+
if shutil.which("docker") is None:
|
|
70
|
+
print(
|
|
71
|
+
"[velo] Docker is required for Java SAP GUI debug runs on this platform.",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
print(f"[velo] Install Docker Desktop: {DOCKER_DESKTOP_URL}", file=sys.stderr)
|
|
75
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
76
|
+
|
|
77
|
+
cwd = Path.cwd().resolve()
|
|
78
|
+
results_dir = _parse_outputdir(rf_args)
|
|
79
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
docker_cmd = [
|
|
82
|
+
"docker",
|
|
83
|
+
"run",
|
|
84
|
+
"--rm",
|
|
85
|
+
"-v",
|
|
86
|
+
f"{cwd}:/suite:ro",
|
|
87
|
+
"-v",
|
|
88
|
+
f"{results_dir}:/results",
|
|
89
|
+
"-e",
|
|
90
|
+
"VELO_SAP_CLIENT=java",
|
|
91
|
+
"-e",
|
|
92
|
+
"SUITE_DIR=/suite",
|
|
93
|
+
"-e",
|
|
94
|
+
"RESULTS_DIR=/results",
|
|
95
|
+
EXECUTION_IMAGE,
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
include, exclude = _parse_tags(rf_args)
|
|
99
|
+
if include:
|
|
100
|
+
docker_cmd.extend(["-e", f"ROBOT_INCLUDE={include}"])
|
|
101
|
+
if exclude:
|
|
102
|
+
docker_cmd.extend(["-e", f"ROBOT_EXCLUDE={exclude}"])
|
|
103
|
+
|
|
104
|
+
print(f"[velo] Starting Docker debug run ({EXECUTION_IMAGE})…", flush=True)
|
|
105
|
+
return subprocess.run(docker_cmd).returncode
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_outputdir(rf_args: list[str]) -> Path:
|
|
109
|
+
outputdir = "./results"
|
|
110
|
+
args = list(rf_args)
|
|
111
|
+
idx = 0
|
|
112
|
+
while idx < len(args):
|
|
113
|
+
arg = args[idx]
|
|
114
|
+
if arg in ("--outputdir", "-d") and idx + 1 < len(args):
|
|
115
|
+
outputdir = args[idx + 1]
|
|
116
|
+
idx += 2
|
|
117
|
+
continue
|
|
118
|
+
if arg.startswith("--outputdir="):
|
|
119
|
+
outputdir = arg.split("=", 1)[1]
|
|
120
|
+
idx += 1
|
|
121
|
+
path = Path(outputdir)
|
|
122
|
+
if not path.is_absolute():
|
|
123
|
+
path = Path.cwd() / path
|
|
124
|
+
return path.resolve()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_tags(rf_args: list[str]) -> tuple[str | None, str | None]:
|
|
128
|
+
include: str | None = None
|
|
129
|
+
exclude: str | None = None
|
|
130
|
+
args = list(rf_args)
|
|
131
|
+
idx = 0
|
|
132
|
+
while idx < len(args):
|
|
133
|
+
arg = args[idx]
|
|
134
|
+
if arg in ("--include", "-i") and idx + 1 < len(args):
|
|
135
|
+
include = args[idx + 1]
|
|
136
|
+
idx += 2
|
|
137
|
+
continue
|
|
138
|
+
if arg in ("--exclude", "-e") and idx + 1 < len(args):
|
|
139
|
+
exclude = args[idx + 1]
|
|
140
|
+
idx += 2
|
|
141
|
+
continue
|
|
142
|
+
idx += 1
|
|
143
|
+
return include, exclude
|
velo/ignore.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import zipfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pathspec
|
|
6
|
+
|
|
7
|
+
DEFAULT_IGNORE = [
|
|
8
|
+
".git",
|
|
9
|
+
"__pycache__",
|
|
10
|
+
"*.pyc",
|
|
11
|
+
"*.pyo",
|
|
12
|
+
"venv",
|
|
13
|
+
".venv",
|
|
14
|
+
"node_modules",
|
|
15
|
+
".env",
|
|
16
|
+
".env.*",
|
|
17
|
+
"results",
|
|
18
|
+
"*.log",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_ignore_spec(root: Path) -> pathspec.PathSpec[Any]:
|
|
23
|
+
veloignore = root / ".veloignore"
|
|
24
|
+
if veloignore.exists():
|
|
25
|
+
patterns = veloignore.read_text().splitlines()
|
|
26
|
+
else:
|
|
27
|
+
patterns = DEFAULT_IGNORE.copy()
|
|
28
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_archive(root: Path, output: Path) -> None:
|
|
32
|
+
"""Package root into a zip archive at output, respecting .veloignore rules."""
|
|
33
|
+
spec = load_ignore_spec(root)
|
|
34
|
+
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
35
|
+
for path in sorted(root.rglob("*")):
|
|
36
|
+
if not path.is_file():
|
|
37
|
+
continue
|
|
38
|
+
rel = path.relative_to(root)
|
|
39
|
+
if spec.match_file(str(rel)):
|
|
40
|
+
continue
|
|
41
|
+
zf.write(path, rel)
|
velo/py.typed
ADDED
|
File without changes
|
velo/runner.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from velo.constants import RF_EXIT_PLATFORM_ERROR
|
|
9
|
+
from velo.ignore import build_archive
|
|
10
|
+
from velo.streamer import stream_logs_to_terminal
|
|
11
|
+
|
|
12
|
+
DEFAULT_API_BASE = "https://velo.aster.pro"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_remote(rf_args: list[str]) -> int:
|
|
16
|
+
api_base = os.getenv("VELO_API_BASE", DEFAULT_API_BASE)
|
|
17
|
+
api_key = os.getenv("VELO_API_KEY", "")
|
|
18
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
19
|
+
|
|
20
|
+
root = Path.cwd()
|
|
21
|
+
|
|
22
|
+
# 1. Build archive respecting .veloignore
|
|
23
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
|
24
|
+
archive_path = Path(tmp.name)
|
|
25
|
+
build_archive(root, archive_path)
|
|
26
|
+
size_kb = archive_path.stat().st_size // 1024
|
|
27
|
+
print(f"[velo] uploading suite ({size_kb} KB)...", flush=True)
|
|
28
|
+
|
|
29
|
+
# 2. Upload suite
|
|
30
|
+
try:
|
|
31
|
+
with open(archive_path, "rb") as f:
|
|
32
|
+
r = requests.post(
|
|
33
|
+
f"{api_base}/cli/api/suites",
|
|
34
|
+
files={"archive": (root.name + ".zip", f, "application/zip")},
|
|
35
|
+
headers=headers,
|
|
36
|
+
)
|
|
37
|
+
r.raise_for_status()
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
print(f"[velo] suite upload failed: {exc}", file=sys.stderr, flush=True)
|
|
40
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
41
|
+
finally:
|
|
42
|
+
archive_path.unlink(missing_ok=True)
|
|
43
|
+
|
|
44
|
+
suite_id = r.json()["suite_id"]
|
|
45
|
+
|
|
46
|
+
# 3. Parse RF flags for include/exclude tags and output dir
|
|
47
|
+
include = _extract_flag(rf_args, "--include")
|
|
48
|
+
exclude = _extract_flag(rf_args, "--exclude")
|
|
49
|
+
output_dir = _extract_flag(rf_args, "--outputdir") or "results"
|
|
50
|
+
|
|
51
|
+
# 4. Trigger run
|
|
52
|
+
try:
|
|
53
|
+
r = requests.post(
|
|
54
|
+
f"{api_base}/cli/api/runs",
|
|
55
|
+
json={"suite_id": suite_id, "include": include, "exclude": exclude},
|
|
56
|
+
headers=headers,
|
|
57
|
+
)
|
|
58
|
+
r.raise_for_status()
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
print(f"[velo] run trigger failed: {exc}", file=sys.stderr, flush=True)
|
|
61
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
62
|
+
|
|
63
|
+
run_id = r.json()["run_id"]
|
|
64
|
+
print(f"[velo] run started: {run_id}", flush=True)
|
|
65
|
+
|
|
66
|
+
# 5. Stream logs to terminal
|
|
67
|
+
stream_logs_to_terminal(run_id, api_base, api_key)
|
|
68
|
+
|
|
69
|
+
# 6. Poll for final status + exit code
|
|
70
|
+
try:
|
|
71
|
+
r = requests.get(f"{api_base}/cli/api/runs/{run_id}", headers=headers)
|
|
72
|
+
r.raise_for_status()
|
|
73
|
+
run = r.json()
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
print(f"[velo] failed to fetch run result: {exc}", file=sys.stderr, flush=True)
|
|
76
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
77
|
+
|
|
78
|
+
# 7. Download artifacts to local output directory
|
|
79
|
+
_download_artifacts(run_id, output_dir, api_base, api_key)
|
|
80
|
+
|
|
81
|
+
exit_code = run.get("exit_code")
|
|
82
|
+
if exit_code is None:
|
|
83
|
+
return RF_EXIT_PLATFORM_ERROR
|
|
84
|
+
return int(exit_code)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _extract_flag(args: list[str], flag: str) -> str | None:
|
|
88
|
+
"""Return the value following `flag` in args, or None if not present."""
|
|
89
|
+
for i, arg in enumerate(args):
|
|
90
|
+
if arg == flag and i + 1 < len(args):
|
|
91
|
+
return args[i + 1]
|
|
92
|
+
if arg.startswith(f"{flag}="):
|
|
93
|
+
return arg.split("=", 1)[1]
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _download_artifacts(
|
|
98
|
+
run_id: str,
|
|
99
|
+
output_dir: str,
|
|
100
|
+
api_base: str,
|
|
101
|
+
api_key: str,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Download log.html and report.html into output_dir."""
|
|
104
|
+
dest = Path(output_dir)
|
|
105
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
107
|
+
|
|
108
|
+
artifacts = {
|
|
109
|
+
"log.html": f"{api_base}/cli/api/runs/{run_id}/artifacts/log",
|
|
110
|
+
"report.html": f"{api_base}/cli/api/runs/{run_id}/artifacts/report",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for filename, url in artifacts.items():
|
|
114
|
+
try:
|
|
115
|
+
r = requests.get(url, headers=headers, timeout=60)
|
|
116
|
+
r.raise_for_status()
|
|
117
|
+
(dest / filename).write_bytes(r.content)
|
|
118
|
+
print(f"[velo] downloaded {filename} → {dest / filename}", flush=True)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
print(
|
|
121
|
+
f"[velo] warning: could not download {filename}: {exc}",
|
|
122
|
+
file=sys.stderr,
|
|
123
|
+
flush=True,
|
|
124
|
+
)
|
velo/sap_client.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""SAP client type resolution for VELO_DEBUG and local runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_sap_client(client: str | None = None) -> str:
|
|
10
|
+
"""Return ``java`` or ``windows`` for the active SAP GUI backend."""
|
|
11
|
+
raw = (client or os.getenv("VELO_SAP_CLIENT") or "auto").strip().lower()
|
|
12
|
+
if raw not in ("auto", "java", "windows"):
|
|
13
|
+
raise ValueError(
|
|
14
|
+
f"Invalid VELO_SAP_CLIENT '{raw}'. Use auto, java, or windows."
|
|
15
|
+
)
|
|
16
|
+
if raw == "auto":
|
|
17
|
+
return "windows" if sys.platform == "win32" else "java"
|
|
18
|
+
return raw
|
velo/streamer.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def stream_logs_to_terminal(run_id: str, api_base: str, api_key: str) -> None:
|
|
8
|
+
"""
|
|
9
|
+
Connect to the SSE log stream for a run and print lines to stdout as they arrive.
|
|
10
|
+
Reconnects up to 5 times with exponential backoff on connection errors,
|
|
11
|
+
but stops immediately if the run has already reached a terminal state.
|
|
12
|
+
"""
|
|
13
|
+
headers = {
|
|
14
|
+
"Authorization": f"Bearer {api_key}",
|
|
15
|
+
"Accept": "text/event-stream",
|
|
16
|
+
}
|
|
17
|
+
retries = 0
|
|
18
|
+
max_retries = 5
|
|
19
|
+
|
|
20
|
+
while retries <= max_retries:
|
|
21
|
+
try:
|
|
22
|
+
with requests.get(
|
|
23
|
+
f"{api_base}/cli/api/runs/{run_id}/stream",
|
|
24
|
+
headers=headers,
|
|
25
|
+
stream=True,
|
|
26
|
+
timeout=300,
|
|
27
|
+
) as resp:
|
|
28
|
+
resp.raise_for_status()
|
|
29
|
+
retries = 0
|
|
30
|
+
for raw in resp.iter_lines():
|
|
31
|
+
if raw and raw.startswith(b"data: "):
|
|
32
|
+
print(raw[6:].decode(), flush=True)
|
|
33
|
+
return # stream closed cleanly — run ended
|
|
34
|
+
except Exception:
|
|
35
|
+
retries += 1
|
|
36
|
+
if retries > max_retries:
|
|
37
|
+
print("[velo] lost contact with platform", file=sys.stderr, flush=True)
|
|
38
|
+
return
|
|
39
|
+
if _run_is_terminal(run_id, api_base, api_key):
|
|
40
|
+
return
|
|
41
|
+
wait = 2**retries
|
|
42
|
+
print(
|
|
43
|
+
f"[velo] reconnecting to log stream... ({retries}/{max_retries})",
|
|
44
|
+
file=sys.stderr,
|
|
45
|
+
flush=True,
|
|
46
|
+
)
|
|
47
|
+
time.sleep(wait)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_is_terminal(run_id: str, api_base: str, api_key: str) -> bool:
|
|
51
|
+
"""Return True if the run has already completed or failed."""
|
|
52
|
+
try:
|
|
53
|
+
r = requests.get(
|
|
54
|
+
f"{api_base}/cli/api/runs/{run_id}",
|
|
55
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
56
|
+
timeout=10,
|
|
57
|
+
)
|
|
58
|
+
if r.status_code == 200:
|
|
59
|
+
status = r.json().get("status", "")
|
|
60
|
+
return status in ("completed", "failed")
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
return False
|