gridfleet-testkit 0.4.0__tar.gz → 0.5.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.4.0 → gridfleet_testkit-0.5.0}/CHANGELOG.md +31 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/PKG-INFO +71 -11
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/README.md +69 -9
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/__init__.py +19 -9
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/allocation.py +11 -5
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/appium.py +21 -12
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/client.py +200 -46
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/pytest_plugin.py +37 -4
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/pyproject.toml +6 -3
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_allocation.py +51 -10
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_appium.py +16 -8
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_client.py +421 -31
- gridfleet_testkit-0.5.0/tests/test_client_test_data.py +64 -0
- gridfleet_testkit-0.5.0/tests/test_docs_contract.py +66 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_package_metadata.py +26 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_pytest_plugin.py +68 -11
- gridfleet_testkit-0.5.0/tests/test_pytest_plugin_test_data.py +20 -0
- gridfleet_testkit-0.5.0/uv.lock +648 -0
- gridfleet_testkit-0.4.0/uv.lock +0 -609
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/.gitignore +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/_example_helpers.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_android_mobile_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_android_tv_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_firetv_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_ios_simulator_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_roku_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_roku_sideload_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/examples/test_tvos_screenshot.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/sessions.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_driver_agnostic_guard.py +0 -0
- {gridfleet_testkit-0.4.0 → gridfleet_testkit-0.5.0}/tests/test_sessions.py +0 -0
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.4.0...gridfleet-testkit-v0.5.0) (2026-05-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* remove device_config secret masking ([#104](https://github.com/quidow/gridfleet/issues/104))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **backend:** escalate device to maintenance after N cooldowns in same run ([#121](https://github.com/quidow/gridfleet/issues/121)) ([7fe01f7](https://github.com/quidow/gridfleet/commit/7fe01f768ff70cd3ddb7f26aec1ab7210b49987f))
|
|
15
|
+
* **main:** split device test_data from device_config + modal portal ([b5d0fa0](https://github.com/quidow/gridfleet/commit/b5d0fa09a862af742b3a2462667a86b1d3a867b6))
|
|
16
|
+
* **testkit:** add allocated_device test_data and hydration ([e814e3d](https://github.com/quidow/gridfleet/commit/e814e3d0040b87a547ffc892ee2064305724d576))
|
|
17
|
+
* **testkit:** add device_test_data pytest fixture ([51eeefc](https://github.com/quidow/gridfleet/commit/51eeefc522989e827610f2b1103d2ded0cadda89))
|
|
18
|
+
* **testkit:** add gridfleetclient test_data methods ([b7f1124](https://github.com/quidow/gridfleet/commit/b7f1124e49618a7193ffc61f9db066f847d38522))
|
|
19
|
+
* **testkit:** align public surface, fix run cleanup, lazy env reads ([#128](https://github.com/quidow/gridfleet/issues/128)) ([ee85958](https://github.com/quidow/gridfleet/commit/ee859581f84f77f43c2d0bb627eeeaef1e2a99db))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Dependencies
|
|
23
|
+
|
|
24
|
+
* **deps:** bump appium-python-client in /testkit ([dc12591](https://github.com/quidow/gridfleet/commit/dc12591f5ebfba2361341132df546f6325750a61))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Documentation
|
|
28
|
+
|
|
29
|
+
* **docs:** document discriminated-union release-with-cooldown response ([7fe01f7](https://github.com/quidow/gridfleet/commit/7fe01f768ff70cd3ddb7f26aec1ab7210b49987f))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Code Refactoring
|
|
33
|
+
|
|
34
|
+
* remove device_config secret masking ([#104](https://github.com/quidow/gridfleet/issues/104)) ([7329a31](https://github.com/quidow/gridfleet/commit/7329a3107814f653b81b2753e519e271ec0dd8bd))
|
|
35
|
+
|
|
5
36
|
## [0.4.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.3.0...gridfleet-testkit-v0.4.0) (2026-05-06)
|
|
6
37
|
|
|
7
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridfleet-testkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Supported pytest and run-orchestration helpers for GridFleet integrations
|
|
5
5
|
Project-URL: Homepage, https://github.com/quidow/gridfleet
|
|
6
6
|
Project-URL: Repository, https://github.com/quidow/gridfleet
|
|
@@ -27,7 +27,7 @@ Requires-Dist: pytest<10,>=9.0.3
|
|
|
27
27
|
Provides-Extra: appium
|
|
28
28
|
Requires-Dist: appium-python-client<6,>=4.5; extra == 'appium'
|
|
29
29
|
Provides-Extra: dev
|
|
30
|
-
Requires-Dist: mypy<
|
|
30
|
+
Requires-Dist: mypy<3,>=1.20.2; extra == 'dev'
|
|
31
31
|
Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
|
|
32
32
|
Requires-Dist: ruff<1,>=0.15.12; extra == 'dev'
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
@@ -40,14 +40,28 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
- Stable import root: `gridfleet_testkit`
|
|
42
42
|
- Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
|
|
43
|
-
- Supported
|
|
43
|
+
- Supported pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `gridfleet_worker_id`
|
|
44
|
+
- Supported public Appium helpers:
|
|
44
45
|
- `build_appium_options`
|
|
45
46
|
- `create_appium_driver`
|
|
46
47
|
- `get_connection_target_from_driver`
|
|
47
48
|
- `get_device_config_for_driver`
|
|
49
|
+
- `get_device_test_data_for_driver`
|
|
50
|
+
- Supported public client helpers:
|
|
48
51
|
- `GridFleetClient`
|
|
49
52
|
- `HeartbeatThread`
|
|
50
53
|
- `register_run_cleanup`
|
|
54
|
+
- Supported public allocation/session helpers:
|
|
55
|
+
- `AllocatedDevice`
|
|
56
|
+
- `UnavailableInclude`
|
|
57
|
+
- `CooldownResult`
|
|
58
|
+
- `build_error_session_payload`
|
|
59
|
+
- `hydrate_allocated_device`
|
|
60
|
+
- `hydrate_allocated_device_from_driver`
|
|
61
|
+
- Supported public exceptions:
|
|
62
|
+
- `NoClaimableDevicesError`
|
|
63
|
+
- `UnknownIncludeError`
|
|
64
|
+
- `ReserveCapabilitiesUnsupportedError`
|
|
51
65
|
- Manual hardware examples under `testkit/examples/`
|
|
52
66
|
|
|
53
67
|
## What It Does Not Own
|
|
@@ -134,8 +148,12 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
|
|
|
134
148
|
- Injects `gridfleet:testName` with the pytest test name
|
|
135
149
|
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
136
150
|
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
151
|
+
- Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
|
|
152
|
+
- Exposes `gridfleet_worker_id` which returns the pytest-xdist worker id, or `"controller"` for non-worker processes
|
|
137
153
|
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
138
154
|
|
|
155
|
+
If Appium driver creation fails before a Grid session exists, the pytest fixture registers a device-less terminal error session with an `error-<uuid>` session id, attempted capabilities, requested pack/platform metadata when available, and exception details, then re-raises the original exception. These rows make setup failures visible in the GridFleet Sessions view.
|
|
156
|
+
|
|
139
157
|
## Direct Appium Usage
|
|
140
158
|
|
|
141
159
|
If you need to create a driver outside pytest, use the public Appium helpers:
|
|
@@ -162,15 +180,20 @@ finally:
|
|
|
162
180
|
|
|
163
181
|
| Helper | Purpose |
|
|
164
182
|
| --- | --- |
|
|
165
|
-
| `GridFleetClient.list_devices(
|
|
183
|
+
| `GridFleetClient.list_devices(*, pack_id=None, status=None, host_id=None, ...)` | List devices using backend keyword filters (pack_id, platform_id, status, host_id, connection_target, tags, ...) |
|
|
166
184
|
| `GridFleetClient.get_device(device_id)` | Fetch one full device detail row by backend device id |
|
|
167
|
-
| `GridFleetClient.get_device_config(connection_target
|
|
185
|
+
| `GridFleetClient.get_device_config(connection_target)` | Look up a device by runtime connection target and fetch its config |
|
|
186
|
+
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
187
|
+
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
188
|
+
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
189
|
+
| `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
|
|
190
|
+
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
168
191
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
169
192
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
170
193
|
| `GridFleetClient.claim_device(run_id, worker_id=...)` | Claim one reserved device for a worker |
|
|
171
194
|
| `GridFleetClient.claim_device_with_retry(run_id, worker_id=..., max_wait_sec=300)` | Claim one reserved device, sleeping according to server `Retry-After` responses |
|
|
172
195
|
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
173
|
-
| `GridFleetClient.release_device_safe(run_id, device_id=..., worker_id=...)` |
|
|
196
|
+
| `GridFleetClient.release_device_safe(run_id, device_id=..., worker_id=...)` | Return `True` when release is accepted, `False` when the run is gone or the claim is already unclaimed, and raise on wrong-worker conflicts or unsafe errors |
|
|
174
197
|
| `GridFleetClient.release_device_with_cooldown(run_id, device_id=..., worker_id=..., reason=..., ttl_seconds=...)` | Release a worker claim and keep that run from reclaiming the device until cooldown expires |
|
|
175
198
|
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
176
199
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
@@ -185,7 +208,8 @@ finally:
|
|
|
185
208
|
| `build_error_session_payload(fields)` | Build a `/api/sessions` payload for driver-creation failures without importing pytest |
|
|
186
209
|
| `hydrate_allocated_device(claim_response, run_id, client)` | Combine a claim response with optional device config and live capabilities |
|
|
187
210
|
| `hydrate_allocated_device_from_driver(allocated, driver, client)` | Return a new allocated-device object with capabilities from a running driver |
|
|
188
|
-
| `
|
|
211
|
+
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
212
|
+
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
|
|
189
213
|
|
|
190
214
|
### Worker Identity
|
|
191
215
|
|
|
@@ -216,7 +240,9 @@ run = client.reserve_devices(
|
|
|
216
240
|
run_id = run["id"]
|
|
217
241
|
worker_count = len(run["devices"])
|
|
218
242
|
heartbeat_thread = client.start_heartbeat(run_id, interval=30)
|
|
219
|
-
register_run_cleanup(client, run_id, heartbeat_thread)
|
|
243
|
+
cleanup = register_run_cleanup(client, run_id, heartbeat_thread)
|
|
244
|
+
# cleanup() runs at process exit; call client.complete_run(run_id) on success
|
|
245
|
+
# or client.cancel_run(run_id) on failure to set the run state explicitly.
|
|
220
246
|
|
|
221
247
|
# If one reserved device fails setup:
|
|
222
248
|
client.report_preparation_failure(
|
|
@@ -278,11 +304,45 @@ assert allocated.device_id == claim["device_id"]
|
|
|
278
304
|
assert allocated.platform_name in {"Android", "iOS", "tvOS", "Roku"}
|
|
279
305
|
```
|
|
280
306
|
|
|
281
|
-
The helper fetches static device config by default when `connection_target` is present. It fetches live capabilities only when `fetch_capabilities=True`.
|
|
307
|
+
The helper fetches static device config by default when `connection_target` is present. It fetches live capabilities only when `fetch_capabilities=True`. Pass `fetch_test_data=True` to also populate `allocated.test_data`. The `test_data` field is also available directly from the claim response when the manager inlines it.
|
|
308
|
+
|
|
309
|
+
### Run Cleanup Policy
|
|
310
|
+
|
|
311
|
+
`register_run_cleanup(...)` registers an atexit cleanup callable and returns it. By default it stops the heartbeat thread but does not complete or cancel the run, because process exit alone does not prove test success. Prefer explicit `client.complete_run(run_id)` after successful orchestration and `client.cancel_run(run_id)` for known failures. Pass `on_exit="complete"` or `on_exit="cancel"` only when that policy is correct for your script. Signal handlers are opt-in with `install_signal_handlers=True`; signal cleanup defaults to cancellation.
|
|
312
|
+
|
|
313
|
+
### Device Test Data
|
|
314
|
+
|
|
315
|
+
The `device_test_data` fixture returns the operator-attached free-form test_data for the device assigned to the current test:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
def test_uses_operator_data(appium_driver, device_test_data):
|
|
319
|
+
assert "account" in device_test_data
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Outside of pytest, use the client directly:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
test_data = client.get_device_test_data(device_id)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Or use the driver helper:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from gridfleet_testkit import get_device_test_data_for_driver
|
|
332
|
+
|
|
333
|
+
test_data = get_device_test_data_for_driver(driver)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Errors and Result Types
|
|
337
|
+
|
|
338
|
+
- `NoClaimableDevicesError(RuntimeError)`: raised when the manager reports no run devices are claimable yet. Exposes `run_id`, `retry_after_sec`, and `next_available_at`. The `RuntimeError` base is part of the contract — consumers can rely on it.
|
|
339
|
+
- `UnknownIncludeError(ValueError)`: raised when the backend rejects one or more `?include=` keys. Exposes `values` with the rejected key names. The `ValueError` base is part of the contract.
|
|
340
|
+
- `ReserveCapabilitiesUnsupportedError(ValueError)`: raised when a reserve-time `include` request contains `"capabilities"`, which is not supported at reserve time. The `ValueError` base is part of the contract.
|
|
341
|
+
- `CooldownResult`: union response type from `release_device_with_cooldown`, with `status` equal to `"cooldown_set"` or `"maintenance_escalated"`. `CooldownSetResult` and `CooldownEscalatedResult` are the concrete TypedDict variants.
|
|
282
342
|
|
|
283
343
|
### Reduced HTTP round-trips on claim
|
|
284
344
|
|
|
285
|
-
|
|
345
|
+
The manager can inline device config and live capabilities into the claim/reserve response, eliminating per-worker follow-up GETs.
|
|
286
346
|
|
|
287
347
|
```python
|
|
288
348
|
client = GridFleetClient()
|
|
@@ -291,7 +351,7 @@ allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
|
291
351
|
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
292
352
|
```
|
|
293
353
|
|
|
294
|
-
|
|
354
|
+
`device_config` and inline `config` payloads are returned verbatim from the manager. The testkit does not perform client-side secret masking or reveal toggles. Protect device config with manager authentication, operator access control, and your lab's secret-handling policy.
|
|
295
355
|
|
|
296
356
|
`reserve_devices` accepts `include=("config",)` only — `include=("capabilities",)` raises `ReserveCapabilitiesUnsupportedError` client-side because reserve-time capabilities are not yet device-bound. Pass `include=` on the per-worker `claim_device` call instead.
|
|
297
357
|
|
|
@@ -6,14 +6,28 @@
|
|
|
6
6
|
|
|
7
7
|
- Stable import root: `gridfleet_testkit`
|
|
8
8
|
- Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
|
|
9
|
-
- Supported
|
|
9
|
+
- Supported pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `gridfleet_worker_id`
|
|
10
|
+
- Supported public Appium helpers:
|
|
10
11
|
- `build_appium_options`
|
|
11
12
|
- `create_appium_driver`
|
|
12
13
|
- `get_connection_target_from_driver`
|
|
13
14
|
- `get_device_config_for_driver`
|
|
15
|
+
- `get_device_test_data_for_driver`
|
|
16
|
+
- Supported public client helpers:
|
|
14
17
|
- `GridFleetClient`
|
|
15
18
|
- `HeartbeatThread`
|
|
16
19
|
- `register_run_cleanup`
|
|
20
|
+
- Supported public allocation/session helpers:
|
|
21
|
+
- `AllocatedDevice`
|
|
22
|
+
- `UnavailableInclude`
|
|
23
|
+
- `CooldownResult`
|
|
24
|
+
- `build_error_session_payload`
|
|
25
|
+
- `hydrate_allocated_device`
|
|
26
|
+
- `hydrate_allocated_device_from_driver`
|
|
27
|
+
- Supported public exceptions:
|
|
28
|
+
- `NoClaimableDevicesError`
|
|
29
|
+
- `UnknownIncludeError`
|
|
30
|
+
- `ReserveCapabilitiesUnsupportedError`
|
|
17
31
|
- Manual hardware examples under `testkit/examples/`
|
|
18
32
|
|
|
19
33
|
## What It Does Not Own
|
|
@@ -100,8 +114,12 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
|
|
|
100
114
|
- Injects `gridfleet:testName` with the pytest test name
|
|
101
115
|
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
102
116
|
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
117
|
+
- Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
|
|
118
|
+
- Exposes `gridfleet_worker_id` which returns the pytest-xdist worker id, or `"controller"` for non-worker processes
|
|
103
119
|
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
104
120
|
|
|
121
|
+
If Appium driver creation fails before a Grid session exists, the pytest fixture registers a device-less terminal error session with an `error-<uuid>` session id, attempted capabilities, requested pack/platform metadata when available, and exception details, then re-raises the original exception. These rows make setup failures visible in the GridFleet Sessions view.
|
|
122
|
+
|
|
105
123
|
## Direct Appium Usage
|
|
106
124
|
|
|
107
125
|
If you need to create a driver outside pytest, use the public Appium helpers:
|
|
@@ -128,15 +146,20 @@ finally:
|
|
|
128
146
|
|
|
129
147
|
| Helper | Purpose |
|
|
130
148
|
| --- | --- |
|
|
131
|
-
| `GridFleetClient.list_devices(
|
|
149
|
+
| `GridFleetClient.list_devices(*, pack_id=None, status=None, host_id=None, ...)` | List devices using backend keyword filters (pack_id, platform_id, status, host_id, connection_target, tags, ...) |
|
|
132
150
|
| `GridFleetClient.get_device(device_id)` | Fetch one full device detail row by backend device id |
|
|
133
|
-
| `GridFleetClient.get_device_config(connection_target
|
|
151
|
+
| `GridFleetClient.get_device_config(connection_target)` | Look up a device by runtime connection target and fetch its config |
|
|
152
|
+
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
153
|
+
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
154
|
+
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
155
|
+
| `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
|
|
156
|
+
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
134
157
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
135
158
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
136
159
|
| `GridFleetClient.claim_device(run_id, worker_id=...)` | Claim one reserved device for a worker |
|
|
137
160
|
| `GridFleetClient.claim_device_with_retry(run_id, worker_id=..., max_wait_sec=300)` | Claim one reserved device, sleeping according to server `Retry-After` responses |
|
|
138
161
|
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
139
|
-
| `GridFleetClient.release_device_safe(run_id, device_id=..., worker_id=...)` |
|
|
162
|
+
| `GridFleetClient.release_device_safe(run_id, device_id=..., worker_id=...)` | Return `True` when release is accepted, `False` when the run is gone or the claim is already unclaimed, and raise on wrong-worker conflicts or unsafe errors |
|
|
140
163
|
| `GridFleetClient.release_device_with_cooldown(run_id, device_id=..., worker_id=..., reason=..., ttl_seconds=...)` | Release a worker claim and keep that run from reclaiming the device until cooldown expires |
|
|
141
164
|
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
142
165
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
@@ -151,7 +174,8 @@ finally:
|
|
|
151
174
|
| `build_error_session_payload(fields)` | Build a `/api/sessions` payload for driver-creation failures without importing pytest |
|
|
152
175
|
| `hydrate_allocated_device(claim_response, run_id, client)` | Combine a claim response with optional device config and live capabilities |
|
|
153
176
|
| `hydrate_allocated_device_from_driver(allocated, driver, client)` | Return a new allocated-device object with capabilities from a running driver |
|
|
154
|
-
| `
|
|
177
|
+
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
178
|
+
| `register_run_cleanup(client, run_id, heartbeat_thread=None)` | Register `atexit` cleanup callable and return it; stops the heartbeat thread on exit but does not complete or cancel the run by default |
|
|
155
179
|
|
|
156
180
|
### Worker Identity
|
|
157
181
|
|
|
@@ -182,7 +206,9 @@ run = client.reserve_devices(
|
|
|
182
206
|
run_id = run["id"]
|
|
183
207
|
worker_count = len(run["devices"])
|
|
184
208
|
heartbeat_thread = client.start_heartbeat(run_id, interval=30)
|
|
185
|
-
register_run_cleanup(client, run_id, heartbeat_thread)
|
|
209
|
+
cleanup = register_run_cleanup(client, run_id, heartbeat_thread)
|
|
210
|
+
# cleanup() runs at process exit; call client.complete_run(run_id) on success
|
|
211
|
+
# or client.cancel_run(run_id) on failure to set the run state explicitly.
|
|
186
212
|
|
|
187
213
|
# If one reserved device fails setup:
|
|
188
214
|
client.report_preparation_failure(
|
|
@@ -244,11 +270,45 @@ assert allocated.device_id == claim["device_id"]
|
|
|
244
270
|
assert allocated.platform_name in {"Android", "iOS", "tvOS", "Roku"}
|
|
245
271
|
```
|
|
246
272
|
|
|
247
|
-
The helper fetches static device config by default when `connection_target` is present. It fetches live capabilities only when `fetch_capabilities=True`.
|
|
273
|
+
The helper fetches static device config by default when `connection_target` is present. It fetches live capabilities only when `fetch_capabilities=True`. Pass `fetch_test_data=True` to also populate `allocated.test_data`. The `test_data` field is also available directly from the claim response when the manager inlines it.
|
|
274
|
+
|
|
275
|
+
### Run Cleanup Policy
|
|
276
|
+
|
|
277
|
+
`register_run_cleanup(...)` registers an atexit cleanup callable and returns it. By default it stops the heartbeat thread but does not complete or cancel the run, because process exit alone does not prove test success. Prefer explicit `client.complete_run(run_id)` after successful orchestration and `client.cancel_run(run_id)` for known failures. Pass `on_exit="complete"` or `on_exit="cancel"` only when that policy is correct for your script. Signal handlers are opt-in with `install_signal_handlers=True`; signal cleanup defaults to cancellation.
|
|
278
|
+
|
|
279
|
+
### Device Test Data
|
|
280
|
+
|
|
281
|
+
The `device_test_data` fixture returns the operator-attached free-form test_data for the device assigned to the current test:
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
def test_uses_operator_data(appium_driver, device_test_data):
|
|
285
|
+
assert "account" in device_test_data
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Outside of pytest, use the client directly:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
test_data = client.get_device_test_data(device_id)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Or use the driver helper:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from gridfleet_testkit import get_device_test_data_for_driver
|
|
298
|
+
|
|
299
|
+
test_data = get_device_test_data_for_driver(driver)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Errors and Result Types
|
|
303
|
+
|
|
304
|
+
- `NoClaimableDevicesError(RuntimeError)`: raised when the manager reports no run devices are claimable yet. Exposes `run_id`, `retry_after_sec`, and `next_available_at`. The `RuntimeError` base is part of the contract — consumers can rely on it.
|
|
305
|
+
- `UnknownIncludeError(ValueError)`: raised when the backend rejects one or more `?include=` keys. Exposes `values` with the rejected key names. The `ValueError` base is part of the contract.
|
|
306
|
+
- `ReserveCapabilitiesUnsupportedError(ValueError)`: raised when a reserve-time `include` request contains `"capabilities"`, which is not supported at reserve time. The `ValueError` base is part of the contract.
|
|
307
|
+
- `CooldownResult`: union response type from `release_device_with_cooldown`, with `status` equal to `"cooldown_set"` or `"maintenance_escalated"`. `CooldownSetResult` and `CooldownEscalatedResult` are the concrete TypedDict variants.
|
|
248
308
|
|
|
249
309
|
### Reduced HTTP round-trips on claim
|
|
250
310
|
|
|
251
|
-
|
|
311
|
+
The manager can inline device config and live capabilities into the claim/reserve response, eliminating per-worker follow-up GETs.
|
|
252
312
|
|
|
253
313
|
```python
|
|
254
314
|
client = GridFleetClient()
|
|
@@ -257,7 +317,7 @@ allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
|
257
317
|
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
258
318
|
```
|
|
259
319
|
|
|
260
|
-
|
|
320
|
+
`device_config` and inline `config` payloads are returned verbatim from the manager. The testkit does not perform client-side secret masking or reveal toggles. Protect device config with manager authentication, operator access control, and your lab's secret-handling policy.
|
|
261
321
|
|
|
262
322
|
`reserve_devices` accepts `include=("config",)` only — `include=("capabilities",)` raises `ReserveCapabilitiesUnsupportedError` client-side because reserve-time capabilities are not yet device-bound. Pass `include=` on the per-worker `claim_device` call instead.
|
|
263
323
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Supported Python integration helpers for GridFleet.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
reveal=True)` explicitly. `AllocatedDevice.config_is_masked` is `True` whenever
|
|
8
|
-
the inline path was taken.
|
|
3
|
+
`device_config` values returned by the manager are verbatim; the testkit no
|
|
4
|
+
longer distinguishes between masked and revealed payloads. Code that wants
|
|
5
|
+
the live Appium-side config can use either inline `config` from
|
|
6
|
+
`claim_device(include=("config",))` or `client.get_device_config(connection_target)`.
|
|
9
7
|
|
|
10
8
|
Environment variables read by the client:
|
|
11
9
|
|
|
@@ -31,15 +29,17 @@ from .appium import (
|
|
|
31
29
|
create_appium_driver,
|
|
32
30
|
get_connection_target_from_driver,
|
|
33
31
|
get_device_config_for_driver,
|
|
32
|
+
get_device_test_data_for_driver,
|
|
34
33
|
)
|
|
35
34
|
from .client import (
|
|
36
|
-
|
|
37
|
-
GRIDFLEET_API_URL,
|
|
35
|
+
CooldownResult,
|
|
38
36
|
GridFleetClient,
|
|
39
37
|
HeartbeatThread,
|
|
40
38
|
NoClaimableDevicesError,
|
|
41
39
|
ReserveCapabilitiesUnsupportedError,
|
|
42
40
|
UnknownIncludeError,
|
|
41
|
+
_default_api_url,
|
|
42
|
+
_default_grid_url,
|
|
43
43
|
register_run_cleanup,
|
|
44
44
|
)
|
|
45
45
|
from .sessions import build_error_session_payload
|
|
@@ -47,12 +47,13 @@ from .sessions import build_error_session_payload
|
|
|
47
47
|
try:
|
|
48
48
|
__version__ = version("gridfleet-testkit")
|
|
49
49
|
except PackageNotFoundError:
|
|
50
|
-
__version__ = "0.
|
|
50
|
+
__version__ = "0.5.0"
|
|
51
51
|
|
|
52
52
|
__all__ = [
|
|
53
53
|
"GRIDFLEET_API_URL",
|
|
54
54
|
"GRID_URL",
|
|
55
55
|
"AllocatedDevice",
|
|
56
|
+
"CooldownResult",
|
|
56
57
|
"GridFleetClient",
|
|
57
58
|
"HeartbeatThread",
|
|
58
59
|
"NoClaimableDevicesError",
|
|
@@ -65,7 +66,16 @@ __all__ = [
|
|
|
65
66
|
"create_appium_driver",
|
|
66
67
|
"get_connection_target_from_driver",
|
|
67
68
|
"get_device_config_for_driver",
|
|
69
|
+
"get_device_test_data_for_driver",
|
|
68
70
|
"hydrate_allocated_device",
|
|
69
71
|
"hydrate_allocated_device_from_driver",
|
|
70
72
|
"register_run_cleanup",
|
|
71
73
|
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def __getattr__(name: str) -> str:
|
|
77
|
+
if name == "GRID_URL":
|
|
78
|
+
return _default_grid_url()
|
|
79
|
+
if name == "GRIDFLEET_API_URL":
|
|
80
|
+
return _default_api_url()
|
|
81
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -39,8 +39,8 @@ class AllocatedDevice:
|
|
|
39
39
|
claimed_at: str
|
|
40
40
|
config: dict[str, Any] | None
|
|
41
41
|
live_capabilities: dict[str, Any] | None
|
|
42
|
+
test_data: dict[str, Any] | None = None
|
|
42
43
|
unavailable_includes: tuple[UnavailableInclude, ...] = ()
|
|
43
|
-
config_is_masked: bool = False
|
|
44
44
|
|
|
45
45
|
@property
|
|
46
46
|
def is_real_device(self) -> bool:
|
|
@@ -121,6 +121,7 @@ def hydrate_allocated_device(
|
|
|
121
121
|
client: GridFleetClient,
|
|
122
122
|
fetch_config: bool = True,
|
|
123
123
|
fetch_capabilities: bool = False,
|
|
124
|
+
fetch_test_data: bool = False,
|
|
124
125
|
) -> AllocatedDevice:
|
|
125
126
|
"""Combine a claim response with optional static config and live capabilities.
|
|
126
127
|
|
|
@@ -142,13 +143,10 @@ def hydrate_allocated_device(
|
|
|
142
143
|
inline_config = payload.get("config")
|
|
143
144
|
if isinstance(inline_config, dict):
|
|
144
145
|
config: dict[str, Any] | None = inline_config
|
|
145
|
-
config_is_masked = True
|
|
146
146
|
elif fetch_config and connection_target and "config" not in unavailable_set:
|
|
147
147
|
config = client.get_device_config(connection_target)
|
|
148
|
-
config_is_masked = False
|
|
149
148
|
else:
|
|
150
149
|
config = None
|
|
151
|
-
config_is_masked = False
|
|
152
150
|
inline_capabilities = payload.get("live_capabilities")
|
|
153
151
|
if isinstance(inline_capabilities, dict):
|
|
154
152
|
live_capabilities: dict[str, Any] | None = inline_capabilities
|
|
@@ -157,6 +155,14 @@ def hydrate_allocated_device(
|
|
|
157
155
|
else:
|
|
158
156
|
live_capabilities = None
|
|
159
157
|
|
|
158
|
+
inline_test_data = payload.get("test_data")
|
|
159
|
+
if isinstance(inline_test_data, dict):
|
|
160
|
+
test_data: dict[str, Any] | None = inline_test_data
|
|
161
|
+
elif fetch_test_data and "test_data" not in unavailable_set:
|
|
162
|
+
test_data = client.get_device_test_data(device_id)
|
|
163
|
+
else:
|
|
164
|
+
test_data = None
|
|
165
|
+
|
|
160
166
|
return AllocatedDevice(
|
|
161
167
|
run_id=run_id,
|
|
162
168
|
device_id=device_id,
|
|
@@ -175,8 +181,8 @@ def hydrate_allocated_device(
|
|
|
175
181
|
claimed_by=_string_value(payload, "claimed_by"),
|
|
176
182
|
claimed_at=_string_value(payload, "claimed_at"),
|
|
177
183
|
config=config,
|
|
178
|
-
config_is_masked=config_is_masked,
|
|
179
184
|
live_capabilities=live_capabilities,
|
|
185
|
+
test_data=test_data,
|
|
180
186
|
unavailable_includes=unavailable_includes,
|
|
181
187
|
)
|
|
182
188
|
|
|
@@ -8,15 +8,11 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from collections.abc import Mapping
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from .client import GRID_URL
|
|
11
|
+
from .client import GridFleetClient, _default_grid_url
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
def _catalog_payload(catalog_client: Any | None) -> dict[str, Any]:
|
|
17
15
|
if catalog_client is None:
|
|
18
|
-
from .client import GridFleetClient
|
|
19
|
-
|
|
20
16
|
catalog_client = GridFleetClient()
|
|
21
17
|
if hasattr(catalog_client, "get_driver_pack_catalog"):
|
|
22
18
|
payload = catalog_client.get_driver_pack_catalog()
|
|
@@ -97,7 +93,9 @@ def build_appium_options(
|
|
|
97
93
|
catalog_client: Any | None = None,
|
|
98
94
|
) -> Any:
|
|
99
95
|
"""Build Appium options from driver-pack catalog platform metadata."""
|
|
100
|
-
|
|
96
|
+
# appium is an optional dep (extra "appium"); imported lazily so consumers
|
|
97
|
+
# without the extra can still use the rest of testkit.
|
|
98
|
+
from appium.options.common import AppiumOptions # noqa: PLC0415
|
|
101
99
|
|
|
102
100
|
params = dict(capabilities or {})
|
|
103
101
|
explicit_platform_name = params.get("platformName")
|
|
@@ -129,11 +127,13 @@ def create_appium_driver(
|
|
|
129
127
|
platform_id: str | None = None,
|
|
130
128
|
capabilities: Mapping[str, Any] | None = None,
|
|
131
129
|
test_name: str | None = None,
|
|
132
|
-
grid_url: str =
|
|
130
|
+
grid_url: str | None = None,
|
|
133
131
|
catalog_client: Any | None = None,
|
|
134
132
|
) -> Any:
|
|
135
133
|
"""Create an Appium remote driver through Selenium Grid."""
|
|
136
|
-
|
|
134
|
+
# appium is an optional dep (extra "appium"); imported lazily so consumers
|
|
135
|
+
# without the extra can still use the rest of testkit.
|
|
136
|
+
from appium import webdriver # noqa: PLC0415
|
|
137
137
|
|
|
138
138
|
options = build_appium_options(
|
|
139
139
|
pack_id=pack_id,
|
|
@@ -142,7 +142,7 @@ def create_appium_driver(
|
|
|
142
142
|
test_name=test_name,
|
|
143
143
|
catalog_client=catalog_client,
|
|
144
144
|
)
|
|
145
|
-
return webdriver.Remote(grid_url, options=options)
|
|
145
|
+
return webdriver.Remote(grid_url or _default_grid_url(), options=options)
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
def get_connection_target_from_driver(driver: Any) -> str:
|
|
@@ -158,10 +158,19 @@ def get_device_config_for_driver(
|
|
|
158
158
|
driver: Any,
|
|
159
159
|
*,
|
|
160
160
|
gridfleet_client: GridFleetClient | None = None,
|
|
161
|
-
reveal: bool = True,
|
|
162
161
|
) -> dict[str, Any]:
|
|
163
162
|
"""Fetch device config for a live Appium driver using its runtime connection target."""
|
|
164
|
-
|
|
163
|
+
client = gridfleet_client or GridFleetClient()
|
|
164
|
+
return client.get_device_config(get_connection_target_from_driver(driver))
|
|
165
|
+
|
|
165
166
|
|
|
167
|
+
def get_device_test_data_for_driver(
|
|
168
|
+
driver: Any,
|
|
169
|
+
*,
|
|
170
|
+
gridfleet_client: GridFleetClient | None = None,
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""Fetch operator-attached test_data for a live Appium driver session."""
|
|
166
173
|
client = gridfleet_client or GridFleetClient()
|
|
167
|
-
|
|
174
|
+
connection_target = get_connection_target_from_driver(driver)
|
|
175
|
+
device_id = client.resolve_device_id_by_connection_target(connection_target)
|
|
176
|
+
return client.get_device_test_data(device_id)
|