gridfleet-testkit 0.1.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.
- gridfleet_testkit-0.1.0/.gitignore +52 -0
- gridfleet_testkit-0.1.0/PKG-INFO +253 -0
- gridfleet_testkit-0.1.0/README.md +219 -0
- gridfleet_testkit-0.1.0/examples/__init__.py +1 -0
- gridfleet_testkit-0.1.0/examples/_example_helpers.py +58 -0
- gridfleet_testkit-0.1.0/examples/assets/hello-world.zip +0 -0
- gridfleet_testkit-0.1.0/examples/test_android_mobile_screenshot.py +40 -0
- gridfleet_testkit-0.1.0/examples/test_android_tv_screenshot.py +40 -0
- gridfleet_testkit-0.1.0/examples/test_firetv_screenshot.py +42 -0
- gridfleet_testkit-0.1.0/examples/test_ios_simulator_screenshot.py +42 -0
- gridfleet_testkit-0.1.0/examples/test_roku_screenshot.py +46 -0
- gridfleet_testkit-0.1.0/examples/test_roku_sideload_screenshot.py +51 -0
- gridfleet_testkit-0.1.0/examples/test_tvos_screenshot.py +42 -0
- gridfleet_testkit-0.1.0/gridfleet_testkit/__init__.py +29 -0
- gridfleet_testkit-0.1.0/gridfleet_testkit/appium.py +167 -0
- gridfleet_testkit-0.1.0/gridfleet_testkit/client.py +242 -0
- gridfleet_testkit-0.1.0/gridfleet_testkit/py.typed +1 -0
- gridfleet_testkit-0.1.0/gridfleet_testkit/pytest_plugin.py +247 -0
- gridfleet_testkit-0.1.0/pyproject.toml +81 -0
- gridfleet_testkit-0.1.0/tests/test_appium.py +215 -0
- gridfleet_testkit-0.1.0/tests/test_client.py +478 -0
- gridfleet_testkit-0.1.0/tests/test_driver_agnostic_guard.py +29 -0
- gridfleet_testkit-0.1.0/tests/test_package_metadata.py +17 -0
- gridfleet_testkit-0.1.0/tests/test_pytest_plugin.py +318 -0
- gridfleet_testkit-0.1.0/uv.lock +613 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
*.egg
|
|
10
|
+
|
|
11
|
+
# Testing
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
htmlcov/
|
|
15
|
+
|
|
16
|
+
# Frontend
|
|
17
|
+
frontend/node_modules/
|
|
18
|
+
frontend/dist/
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# Environment
|
|
27
|
+
.env
|
|
28
|
+
.env.local
|
|
29
|
+
.env.*.local
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# Playwright
|
|
36
|
+
.playwright-mcp/
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Build output
|
|
40
|
+
dist/
|
|
41
|
+
|
|
42
|
+
# Misc
|
|
43
|
+
*.log
|
|
44
|
+
.idea
|
|
45
|
+
testing/screenshots
|
|
46
|
+
frontend/test-results
|
|
47
|
+
|
|
48
|
+
# Worktrees
|
|
49
|
+
.worktrees/
|
|
50
|
+
|
|
51
|
+
# Superpowers working docs
|
|
52
|
+
.superpowers/
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gridfleet-testkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
|
|
5
|
+
Project-URL: Homepage, https://github.com/quidow/gridfleet
|
|
6
|
+
Project-URL: Repository, https://github.com/quidow/gridfleet
|
|
7
|
+
Project-URL: Documentation, https://github.com/quidow/gridfleet/tree/main/docs/reference/testkit.md
|
|
8
|
+
Project-URL: Issues, https://github.com/quidow/gridfleet/issues
|
|
9
|
+
Project-URL: Security, https://github.com/quidow/gridfleet/security/advisories/new
|
|
10
|
+
Author: GridFleet contributors
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
Keywords: appium,gridfleet,pytest,selenium,testing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: Pytest
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27
|
|
26
|
+
Requires-Dist: pytest>=9.0.3
|
|
27
|
+
Provides-Extra: appium
|
|
28
|
+
Requires-Dist: appium-python-client>=4.5; extra == 'appium'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: mypy>=1.20.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=9.0.3; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.15.12; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# GridFleet Testkit
|
|
36
|
+
|
|
37
|
+
`testkit/` is the supported Python integration surface for external pytest/Appium suites that run through GridFleet.
|
|
38
|
+
|
|
39
|
+
## What This Package Owns
|
|
40
|
+
|
|
41
|
+
- Stable import root: `gridfleet_testkit`
|
|
42
|
+
- Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
|
|
43
|
+
- Supported public helpers:
|
|
44
|
+
- `build_appium_options`
|
|
45
|
+
- `create_appium_driver`
|
|
46
|
+
- `get_connection_target_from_driver`
|
|
47
|
+
- `get_device_config_for_driver`
|
|
48
|
+
- `GridFleetClient`
|
|
49
|
+
- `HeartbeatThread`
|
|
50
|
+
- `register_run_cleanup`
|
|
51
|
+
- Manual hardware examples under `testkit/examples/`
|
|
52
|
+
|
|
53
|
+
## What It Does Not Own
|
|
54
|
+
|
|
55
|
+
- Appium server installation or host-level driver setup
|
|
56
|
+
- Selenium Grid lifecycle
|
|
57
|
+
- Device registration, verification, or readiness setup
|
|
58
|
+
- CI orchestration beyond the documented client helpers
|
|
59
|
+
|
|
60
|
+
The supported contract is the installable package and documented import pattern. The example scripts are onboarding aids, not CI-backed conformance tests.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
From PyPI:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install "gridfleet-testkit[appium]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
From a local checkout:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv pip install -e ./testkit[appium]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
From a copied `testkit/` directory inside another repository:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv pip install -e ./testkit[appium]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
From a Git checkout or VCS URL that contains this package:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The package supports Python 3.10 and newer.
|
|
89
|
+
|
|
90
|
+
## Environment
|
|
91
|
+
|
|
92
|
+
| Variable | Default | Meaning |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
|
|
95
|
+
| `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
|
|
96
|
+
| `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
|
|
97
|
+
| `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
|
|
98
|
+
| `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
|
|
99
|
+
| `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
|
|
100
|
+
|
|
101
|
+
The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
|
|
102
|
+
|
|
103
|
+
## Pytest Plugin
|
|
104
|
+
|
|
105
|
+
Load the supported plugin from your test project:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Minimal usage:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import pytest
|
|
115
|
+
|
|
116
|
+
@pytest.mark.parametrize(
|
|
117
|
+
"appium_driver",
|
|
118
|
+
[{"pack_id": "appium-uiautomator2", "platform_id": "android_mobile"}],
|
|
119
|
+
indirect=True,
|
|
120
|
+
)
|
|
121
|
+
def test_session_starts(appium_driver):
|
|
122
|
+
assert appium_driver.session_id is not None
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The plugin resolves `pack_id` and `platform_id` against the enabled driver-pack catalog, then injects Appium `platformName`, `appium:automationName`, `appium:platform`, and `gridfleet:testName`.
|
|
126
|
+
|
|
127
|
+
When exactly one enabled pack provides a platform id, `platform_id` alone is accepted. For environment-portable tests, set `GRIDFLEET_TESTKIT_PACK_ID` and `GRIDFLEET_TESTKIT_PLATFORM_ID`, then parametrize with `{}`.
|
|
128
|
+
|
|
129
|
+
If you need raw Appium control instead, omit `pack_id` and `platform_id`, then pass `platformName` as a normal capability key.
|
|
130
|
+
|
|
131
|
+
### Plugin Lifecycle
|
|
132
|
+
|
|
133
|
+
- Creates an Appium session through `GRID_URL`
|
|
134
|
+
- Injects `gridfleet:testName` with the pytest test name
|
|
135
|
+
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
136
|
+
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
137
|
+
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
138
|
+
|
|
139
|
+
## Direct Appium Usage
|
|
140
|
+
|
|
141
|
+
If you need to create a driver outside pytest, use the public Appium helpers:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from gridfleet_testkit import create_appium_driver, get_device_config_for_driver
|
|
145
|
+
|
|
146
|
+
driver = create_appium_driver(
|
|
147
|
+
pack_id="appium-uiautomator2",
|
|
148
|
+
platform_id="firetv_real",
|
|
149
|
+
test_name="manual-smoke",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
assert driver.session_id is not None
|
|
154
|
+
device_config = get_device_config_for_driver(driver)
|
|
155
|
+
finally:
|
|
156
|
+
driver.quit()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`create_appium_driver(...)` reuses the same driver-pack catalog resolver as the pytest fixture. Managed nodes still get their host-scoped runtime allocations from the manager, so callers should not hard-code `systemPort`, `chromedriverPort`, `mjpegServerPort`, `wdaLocalPort`, or `derivedDataPath`. `get_device_config_for_driver(...)` is the non-pytest equivalent of the `device_config` fixture. If you only need the options object, use `build_appium_options(...)`.
|
|
160
|
+
|
|
161
|
+
## Client Helpers
|
|
162
|
+
|
|
163
|
+
| Helper | Purpose |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
|
|
166
|
+
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
167
|
+
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
168
|
+
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
169
|
+
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
170
|
+
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
171
|
+
| `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
|
|
172
|
+
| `GridFleetClient.complete_run(run_id)` | Complete a run |
|
|
173
|
+
| `GridFleetClient.cancel_run(run_id)` | Cancel a run |
|
|
174
|
+
| `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
|
|
175
|
+
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` and signal cleanup that completes or cancels a run |
|
|
176
|
+
|
|
177
|
+
### Reservation Flow
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from gridfleet_testkit import GridFleetClient, register_run_cleanup
|
|
181
|
+
|
|
182
|
+
client = GridFleetClient("http://manager-ip:8000/api")
|
|
183
|
+
|
|
184
|
+
run = client.reserve_devices(
|
|
185
|
+
name="my-test-run",
|
|
186
|
+
requirements=[
|
|
187
|
+
{
|
|
188
|
+
"pack_id": "appium-uiautomator2",
|
|
189
|
+
"platform_id": "firetv_real",
|
|
190
|
+
"os_version": "8",
|
|
191
|
+
"allocation": "all_available",
|
|
192
|
+
"min_count": 1,
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
ttl_minutes=45,
|
|
196
|
+
created_by="local-dev",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
run_id = run["id"]
|
|
200
|
+
worker_count = len(run["devices"])
|
|
201
|
+
heartbeat_thread = client.start_heartbeat(run_id, interval=30)
|
|
202
|
+
register_run_cleanup(client, run_id, heartbeat_thread)
|
|
203
|
+
|
|
204
|
+
# If one reserved device fails setup:
|
|
205
|
+
client.report_preparation_failure(
|
|
206
|
+
run_id,
|
|
207
|
+
device_id="device-123",
|
|
208
|
+
message="Driver bootstrap timed out during CI setup",
|
|
209
|
+
source="local-dev",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
client.signal_ready(run_id)
|
|
213
|
+
client.signal_active(run_id)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
|
|
217
|
+
|
|
218
|
+
## Examples
|
|
219
|
+
|
|
220
|
+
Baseline screenshot examples:
|
|
221
|
+
|
|
222
|
+
- `examples/test_android_mobile_screenshot.py`
|
|
223
|
+
- `examples/test_android_tv_screenshot.py`
|
|
224
|
+
- `examples/test_firetv_screenshot.py`
|
|
225
|
+
- `examples/test_ios_simulator_screenshot.py`
|
|
226
|
+
- `examples/test_tvos_screenshot.py`
|
|
227
|
+
- `examples/test_roku_screenshot.py`
|
|
228
|
+
|
|
229
|
+
Advanced example:
|
|
230
|
+
|
|
231
|
+
- `examples/test_roku_sideload_screenshot.py`
|
|
232
|
+
|
|
233
|
+
The baseline examples share the same flow:
|
|
234
|
+
|
|
235
|
+
1. Create a session through Selenium Grid
|
|
236
|
+
2. Print the resolved connection context
|
|
237
|
+
3. Save a screenshot
|
|
238
|
+
4. Assert that the screenshot file exists and is non-empty
|
|
239
|
+
|
|
240
|
+
## Platform Notes
|
|
241
|
+
|
|
242
|
+
- Android Mobile / Android TV / Fire TV:
|
|
243
|
+
- require the UiAutomator2 driver
|
|
244
|
+
- rely on Grid routing hints generated from GridFleet metadata
|
|
245
|
+
- Fire TV:
|
|
246
|
+
- baseline example supports optional `appium:os_version` filtering when you need a specific Fire OS release
|
|
247
|
+
- iOS simulator:
|
|
248
|
+
- baseline example intentionally targets the simulator lane with `appium:device_type=simulator`
|
|
249
|
+
- tvOS:
|
|
250
|
+
- baseline example intentionally targets a real device and assumes the host already satisfies XCUITest and WebDriverAgent prerequisites
|
|
251
|
+
- Roku:
|
|
252
|
+
- screenshot examples install and activate the bundled sample dev app before capture
|
|
253
|
+
- both Roku examples depend on Roku dev credentials
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# GridFleet Testkit
|
|
2
|
+
|
|
3
|
+
`testkit/` is the supported Python integration surface for external pytest/Appium suites that run through GridFleet.
|
|
4
|
+
|
|
5
|
+
## What This Package Owns
|
|
6
|
+
|
|
7
|
+
- Stable import root: `gridfleet_testkit`
|
|
8
|
+
- Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
|
|
9
|
+
- Supported public helpers:
|
|
10
|
+
- `build_appium_options`
|
|
11
|
+
- `create_appium_driver`
|
|
12
|
+
- `get_connection_target_from_driver`
|
|
13
|
+
- `get_device_config_for_driver`
|
|
14
|
+
- `GridFleetClient`
|
|
15
|
+
- `HeartbeatThread`
|
|
16
|
+
- `register_run_cleanup`
|
|
17
|
+
- Manual hardware examples under `testkit/examples/`
|
|
18
|
+
|
|
19
|
+
## What It Does Not Own
|
|
20
|
+
|
|
21
|
+
- Appium server installation or host-level driver setup
|
|
22
|
+
- Selenium Grid lifecycle
|
|
23
|
+
- Device registration, verification, or readiness setup
|
|
24
|
+
- CI orchestration beyond the documented client helpers
|
|
25
|
+
|
|
26
|
+
The supported contract is the installable package and documented import pattern. The example scripts are onboarding aids, not CI-backed conformance tests.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
From PyPI:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install "gridfleet-testkit[appium]"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
From a local checkout:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv pip install -e ./testkit[appium]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
From a copied `testkit/` directory inside another repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install -e ./testkit[appium]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
From a Git checkout or VCS URL that contains this package:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv pip install "git+https://github.com/<org>/<repo>.git#subdirectory=testkit"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The package supports Python 3.10 and newer.
|
|
55
|
+
|
|
56
|
+
## Environment
|
|
57
|
+
|
|
58
|
+
| Variable | Default | Meaning |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `GRID_URL` | `http://localhost:4444` | Selenium Grid hub URL used by the pytest Appium fixture |
|
|
61
|
+
| `GRIDFLEET_API_URL` | `http://localhost:8000/api` | GridFleet API base used for session reporting, config lookup, run helpers, and driver-pack catalog lookup |
|
|
62
|
+
| `GRIDFLEET_TESTKIT_USERNAME` | unset | Machine-auth username sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_USERNAME`. |
|
|
63
|
+
| `GRIDFLEET_TESTKIT_PASSWORD` | unset | Machine-auth password sent as HTTP Basic auth on every API call. Required when the manager runs with `GRIDFLEET_AUTH_ENABLED=true`. Use the same value as the manager's `GRIDFLEET_MACHINE_AUTH_PASSWORD`. |
|
|
64
|
+
| `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
|
|
65
|
+
| `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
|
|
66
|
+
|
|
67
|
+
The package assumes a running GridFleet API, a reachable Selenium Grid hub, and platform-specific Appium driver setup on the registered hosts. When auth is disabled on the manager, leave `GRIDFLEET_TESTKIT_USERNAME` / `GRIDFLEET_TESTKIT_PASSWORD` unset and the testkit will send no `Authorization` header.
|
|
68
|
+
|
|
69
|
+
## Pytest Plugin
|
|
70
|
+
|
|
71
|
+
Load the supported plugin from your test project:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Minimal usage:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import pytest
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize(
|
|
83
|
+
"appium_driver",
|
|
84
|
+
[{"pack_id": "appium-uiautomator2", "platform_id": "android_mobile"}],
|
|
85
|
+
indirect=True,
|
|
86
|
+
)
|
|
87
|
+
def test_session_starts(appium_driver):
|
|
88
|
+
assert appium_driver.session_id is not None
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The plugin resolves `pack_id` and `platform_id` against the enabled driver-pack catalog, then injects Appium `platformName`, `appium:automationName`, `appium:platform`, and `gridfleet:testName`.
|
|
92
|
+
|
|
93
|
+
When exactly one enabled pack provides a platform id, `platform_id` alone is accepted. For environment-portable tests, set `GRIDFLEET_TESTKIT_PACK_ID` and `GRIDFLEET_TESTKIT_PLATFORM_ID`, then parametrize with `{}`.
|
|
94
|
+
|
|
95
|
+
If you need raw Appium control instead, omit `pack_id` and `platform_id`, then pass `platformName` as a normal capability key.
|
|
96
|
+
|
|
97
|
+
### Plugin Lifecycle
|
|
98
|
+
|
|
99
|
+
- Creates an Appium session through `GRID_URL`
|
|
100
|
+
- Injects `gridfleet:testName` with the pytest test name
|
|
101
|
+
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
102
|
+
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
103
|
+
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
104
|
+
|
|
105
|
+
## Direct Appium Usage
|
|
106
|
+
|
|
107
|
+
If you need to create a driver outside pytest, use the public Appium helpers:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from gridfleet_testkit import create_appium_driver, get_device_config_for_driver
|
|
111
|
+
|
|
112
|
+
driver = create_appium_driver(
|
|
113
|
+
pack_id="appium-uiautomator2",
|
|
114
|
+
platform_id="firetv_real",
|
|
115
|
+
test_name="manual-smoke",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
assert driver.session_id is not None
|
|
120
|
+
device_config = get_device_config_for_driver(driver)
|
|
121
|
+
finally:
|
|
122
|
+
driver.quit()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`create_appium_driver(...)` reuses the same driver-pack catalog resolver as the pytest fixture. Managed nodes still get their host-scoped runtime allocations from the manager, so callers should not hard-code `systemPort`, `chromedriverPort`, `mjpegServerPort`, `wdaLocalPort`, or `derivedDataPath`. `get_device_config_for_driver(...)` is the non-pytest equivalent of the `device_config` fixture. If you only need the options object, use `build_appium_options(...)`.
|
|
126
|
+
|
|
127
|
+
## Client Helpers
|
|
128
|
+
|
|
129
|
+
| Helper | Purpose |
|
|
130
|
+
| --- | --- |
|
|
131
|
+
| `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
|
|
132
|
+
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
133
|
+
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
134
|
+
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
135
|
+
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
136
|
+
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
137
|
+
| `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
|
|
138
|
+
| `GridFleetClient.complete_run(run_id)` | Complete a run |
|
|
139
|
+
| `GridFleetClient.cancel_run(run_id)` | Cancel a run |
|
|
140
|
+
| `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
|
|
141
|
+
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` and signal cleanup that completes or cancels a run |
|
|
142
|
+
|
|
143
|
+
### Reservation Flow
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from gridfleet_testkit import GridFleetClient, register_run_cleanup
|
|
147
|
+
|
|
148
|
+
client = GridFleetClient("http://manager-ip:8000/api")
|
|
149
|
+
|
|
150
|
+
run = client.reserve_devices(
|
|
151
|
+
name="my-test-run",
|
|
152
|
+
requirements=[
|
|
153
|
+
{
|
|
154
|
+
"pack_id": "appium-uiautomator2",
|
|
155
|
+
"platform_id": "firetv_real",
|
|
156
|
+
"os_version": "8",
|
|
157
|
+
"allocation": "all_available",
|
|
158
|
+
"min_count": 1,
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
ttl_minutes=45,
|
|
162
|
+
created_by="local-dev",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
run_id = run["id"]
|
|
166
|
+
worker_count = len(run["devices"])
|
|
167
|
+
heartbeat_thread = client.start_heartbeat(run_id, interval=30)
|
|
168
|
+
register_run_cleanup(client, run_id, heartbeat_thread)
|
|
169
|
+
|
|
170
|
+
# If one reserved device fails setup:
|
|
171
|
+
client.report_preparation_failure(
|
|
172
|
+
run_id,
|
|
173
|
+
device_id="device-123",
|
|
174
|
+
message="Driver bootstrap timed out during CI setup",
|
|
175
|
+
source="local-dev",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
client.signal_ready(run_id)
|
|
179
|
+
client.signal_active(run_id)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
|
|
183
|
+
|
|
184
|
+
## Examples
|
|
185
|
+
|
|
186
|
+
Baseline screenshot examples:
|
|
187
|
+
|
|
188
|
+
- `examples/test_android_mobile_screenshot.py`
|
|
189
|
+
- `examples/test_android_tv_screenshot.py`
|
|
190
|
+
- `examples/test_firetv_screenshot.py`
|
|
191
|
+
- `examples/test_ios_simulator_screenshot.py`
|
|
192
|
+
- `examples/test_tvos_screenshot.py`
|
|
193
|
+
- `examples/test_roku_screenshot.py`
|
|
194
|
+
|
|
195
|
+
Advanced example:
|
|
196
|
+
|
|
197
|
+
- `examples/test_roku_sideload_screenshot.py`
|
|
198
|
+
|
|
199
|
+
The baseline examples share the same flow:
|
|
200
|
+
|
|
201
|
+
1. Create a session through Selenium Grid
|
|
202
|
+
2. Print the resolved connection context
|
|
203
|
+
3. Save a screenshot
|
|
204
|
+
4. Assert that the screenshot file exists and is non-empty
|
|
205
|
+
|
|
206
|
+
## Platform Notes
|
|
207
|
+
|
|
208
|
+
- Android Mobile / Android TV / Fire TV:
|
|
209
|
+
- require the UiAutomator2 driver
|
|
210
|
+
- rely on Grid routing hints generated from GridFleet metadata
|
|
211
|
+
- Fire TV:
|
|
212
|
+
- baseline example supports optional `appium:os_version` filtering when you need a specific Fire OS release
|
|
213
|
+
- iOS simulator:
|
|
214
|
+
- baseline example intentionally targets the simulator lane with `appium:device_type=simulator`
|
|
215
|
+
- tvOS:
|
|
216
|
+
- baseline example intentionally targets a real device and assumes the host already satisfies XCUITest and WebDriverAgent prerequisites
|
|
217
|
+
- Roku:
|
|
218
|
+
- screenshot examples install and activate the bundled sample dev app before capture
|
|
219
|
+
- both Roku examples depend on Roku dev credentials
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Manual hardware examples for the supported GridFleet testkit."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Shared helpers for copyable manual screenshot examples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
SCREENSHOT_DIR = Path(__file__).parent / "screenshots"
|
|
9
|
+
ROKU_HELLO_WORLD_APP = Path(__file__).parent / "assets" / "hello-world.zip"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _resolved_connection_target(capabilities: dict[str, Any]) -> str:
|
|
13
|
+
value = capabilities.get("appium:udid")
|
|
14
|
+
if isinstance(value, str) and value:
|
|
15
|
+
return value
|
|
16
|
+
session_id = capabilities.get("sessionId")
|
|
17
|
+
if isinstance(session_id, str) and session_id:
|
|
18
|
+
return session_id
|
|
19
|
+
return "session"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_connection_context(driver: Any) -> str:
|
|
23
|
+
"""Print the resolved session context and return the connection target string."""
|
|
24
|
+
caps = driver.capabilities
|
|
25
|
+
connection_target = _resolved_connection_target(caps)
|
|
26
|
+
platform_name = caps.get("platformName", "")
|
|
27
|
+
automation_name = caps.get("appium:automationName") or caps.get("automationName")
|
|
28
|
+
print(
|
|
29
|
+
"\nConnected to device: "
|
|
30
|
+
f"connection_target={connection_target}, "
|
|
31
|
+
f"platform={platform_name}, "
|
|
32
|
+
f"automationName={automation_name}"
|
|
33
|
+
)
|
|
34
|
+
print(f"Session ID: {driver.session_id}")
|
|
35
|
+
return connection_target
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_and_assert_screenshot(driver: Any, example_name: str) -> Path:
|
|
39
|
+
"""Save a screenshot and assert that the written file is non-empty."""
|
|
40
|
+
caps = driver.capabilities
|
|
41
|
+
connection_target = _resolved_connection_target(caps).replace(":", "_")
|
|
42
|
+
SCREENSHOT_DIR.mkdir(exist_ok=True)
|
|
43
|
+
screenshot_path = SCREENSHOT_DIR / f"{example_name}_{connection_target}.png"
|
|
44
|
+
saved = driver.save_screenshot(str(screenshot_path))
|
|
45
|
+
|
|
46
|
+
assert saved, "save_screenshot returned False"
|
|
47
|
+
assert screenshot_path.exists(), f"Screenshot file not found at {screenshot_path}"
|
|
48
|
+
assert screenshot_path.stat().st_size > 0, "Screenshot file is empty"
|
|
49
|
+
|
|
50
|
+
print(f"Screenshot saved: {screenshot_path} ({screenshot_path.stat().st_size} bytes)")
|
|
51
|
+
return screenshot_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def install_and_activate_roku_dev_app(driver: Any) -> None:
|
|
55
|
+
"""Install and activate the bundled Roku dev app used by screenshot examples."""
|
|
56
|
+
assert ROKU_HELLO_WORLD_APP.exists(), f"App package not found: {ROKU_HELLO_WORLD_APP}"
|
|
57
|
+
driver.install_app(str(ROKU_HELLO_WORLD_APP.resolve()))
|
|
58
|
+
driver.activate_app("dev")
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manual baseline example: connect to an Android mobile device through Selenium Grid and take a screenshot.
|
|
3
|
+
|
|
4
|
+
Requires:
|
|
5
|
+
- Selenium Grid hub running on localhost:4444
|
|
6
|
+
- An Android mobile device registered and its Appium node running
|
|
7
|
+
- The supported GridFleet testkit installed
|
|
8
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
cd testkit && python -m pytest examples/test_android_mobile_screenshot.py -v -s
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
19
|
+
|
|
20
|
+
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
"appium_driver",
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
"pack_id": "appium-uiautomator2",
|
|
28
|
+
"platform_id": "android_mobile",
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
indirect=True,
|
|
32
|
+
)
|
|
33
|
+
def test_android_mobile_take_screenshot(appium_driver: Any) -> None:
|
|
34
|
+
"""Connect to an Android mobile device through the Grid and take a screenshot."""
|
|
35
|
+
driver = appium_driver
|
|
36
|
+
|
|
37
|
+
assert driver.session_id is not None, "Failed to create Appium session"
|
|
38
|
+
|
|
39
|
+
print_connection_context(driver)
|
|
40
|
+
save_and_assert_screenshot(driver, "android_mobile")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manual baseline example: connect to an Android TV device through Selenium Grid and take a screenshot.
|
|
3
|
+
|
|
4
|
+
Requires:
|
|
5
|
+
- Selenium Grid hub running on localhost:4444
|
|
6
|
+
- An Android TV device registered and its Appium node running
|
|
7
|
+
- The supported GridFleet testkit installed
|
|
8
|
+
- Appium-Python-Client installed (`uv pip install -e ./testkit[appium]`)
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
cd testkit && python -m pytest examples/test_android_tv_screenshot.py -v -s
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from examples._example_helpers import print_connection_context, save_and_assert_screenshot
|
|
19
|
+
|
|
20
|
+
pytest_plugins = ["gridfleet_testkit.pytest_plugin"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
"appium_driver",
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
"pack_id": "appium-uiautomator2",
|
|
28
|
+
"platform_id": "android_tv",
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
indirect=True,
|
|
32
|
+
)
|
|
33
|
+
def test_android_tv_take_screenshot(appium_driver: Any) -> None:
|
|
34
|
+
"""Connect to an Android TV device through the Grid and take a screenshot."""
|
|
35
|
+
driver = appium_driver
|
|
36
|
+
|
|
37
|
+
assert driver.session_id is not None, "Failed to create Appium session"
|
|
38
|
+
|
|
39
|
+
print_connection_context(driver)
|
|
40
|
+
save_and_assert_screenshot(driver, "android_tv")
|