gridfleet-testkit 0.6.0__tar.gz → 0.8.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.6.0 → gridfleet_testkit-0.8.0}/CHANGELOG.md +48 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/PKG-INFO +58 -49
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/README.md +57 -48
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/__init__.py +8 -6
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/allocation.py +4 -15
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/appium.py +1 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/client.py +43 -179
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/pytest_plugin.py +11 -1
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/sessions.py +16 -2
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/pyproject.toml +1 -1
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_allocation.py +20 -38
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_appium.py +1 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_client.py +3 -501
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_package_metadata.py +0 -2
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_pytest_plugin.py +1 -0
- gridfleet_testkit-0.8.0/tests/test_pytest_plugin_grid_run_id_injection.py +56 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_sessions.py +1 -1
- gridfleet_testkit-0.8.0/tests/test_sessions_resolve_device_handle.py +25 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/uv.lock +131 -131
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/.gitignore +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/_example_helpers.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_android_mobile_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_android_tv_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_firetv_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_ios_simulator_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_roku_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_roku_sideload_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/examples/test_tvos_screenshot.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_client_test_data.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_docs_contract.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_driver_agnostic_guard.py +0 -0
- {gridfleet_testkit-0.6.0 → gridfleet_testkit-0.8.0}/tests/test_pytest_plugin_test_data.py +0 -0
|
@@ -2,6 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.8.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.7.0...gridfleet-testkit-v0.8.0) (2026-05-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **backend,testkit:** recreate run device cooldown api ([fccfbc7](https://github.com/quidow/gridfleet/commit/fccfbc7bcf694f8c59cbaa394bb075d20e1b34f0))
|
|
11
|
+
* **testkit:** add cooldown_device client helper and result types ([584f411](https://github.com/quidow/gridfleet/commit/584f411f3b72f815e6f4055667c0eaccb1926bc5))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Dependencies
|
|
15
|
+
|
|
16
|
+
* **deps:** bump mypy in /agent ([#195](https://github.com/quidow/gridfleet/issues/195)) ([1317e59](https://github.com/quidow/gridfleet/commit/1317e59bbd4ae6969ed3c717c24b43dbfefec722))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
* **testkit:** correct cooldown_device ttl error in readme ([f768b6b](https://github.com/quidow/gridfleet/commit/f768b6b11b87ad3c4aa369ee2bcdebfeed5c1f86))
|
|
22
|
+
* **testkit:** document cooldown_device api and result types ([0e2418b](https://github.com/quidow/gridfleet/commit/0e2418b0ea3b952d6151a08e3f75116de7edcdd8))
|
|
23
|
+
|
|
24
|
+
## [0.7.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.6.0...gridfleet-testkit-v0.7.0) (2026-05-11)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### ⚠ BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* **testkit:** AllocatedDevice hydration now accepts device handles, not claim payloads.
|
|
30
|
+
* **testkit:** GridFleetClient claim/release helpers and NoClaimableDevicesError are removed.
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* **testkit:** drop claim release client api ([8d1f295](https://github.com/quidow/gridfleet/commit/8d1f29504e6d640ce9d85c70594a515868045be8))
|
|
35
|
+
* **testkit:** inject grid run id capability ([b4ae38c](https://github.com/quidow/gridfleet/commit/b4ae38ce9969ae325bffdb9716ba7d6c52a699ac))
|
|
36
|
+
* **testkit:** resolve device handle by connection target ([22b4299](https://github.com/quidow/gridfleet/commit/22b4299d6bc4302503a2c2b6f17018ceebc03084))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Bug Fixes
|
|
40
|
+
|
|
41
|
+
* **agent:** release adapter-owned doctor refactor ([#165](https://github.com/quidow/gridfleet/issues/165)) ([f3ae257](https://github.com/quidow/gridfleet/commit/f3ae25787e2c8ef926312f11d2313c6513f8bfa9))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Dependencies
|
|
45
|
+
|
|
46
|
+
* **deps:** bump urllib3 from 2.6.3 to 2.7.0 in /testkit ([#186](https://github.com/quidow/gridfleet/issues/186)) ([dd7a1df](https://github.com/quidow/gridfleet/commit/dd7a1df8fb29ae76f618abab947db2027471b536))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Code Refactoring
|
|
50
|
+
|
|
51
|
+
* **testkit:** drop claim response allocation metadata ([f0eec3e](https://github.com/quidow/gridfleet/commit/f0eec3e9c9f804439241ddbbb56b196bc467effd))
|
|
52
|
+
|
|
5
53
|
## [0.6.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.5.0...gridfleet-testkit-v0.6.0) (2026-05-10)
|
|
6
54
|
|
|
7
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridfleet-testkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -40,7 +40,7 @@ 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 pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `gridfleet_worker_id`
|
|
43
|
+
- Supported pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `device_handle`, `gridfleet_worker_id`
|
|
44
44
|
- Supported public Appium helpers:
|
|
45
45
|
- `build_appium_options`
|
|
46
46
|
- `create_appium_driver`
|
|
@@ -54,12 +54,15 @@ Description-Content-Type: text/markdown
|
|
|
54
54
|
- Supported public allocation/session helpers:
|
|
55
55
|
- `AllocatedDevice`
|
|
56
56
|
- `UnavailableInclude`
|
|
57
|
-
- `CooldownResult`
|
|
58
57
|
- `build_error_session_payload`
|
|
59
58
|
- `hydrate_allocated_device`
|
|
60
59
|
- `hydrate_allocated_device_from_driver`
|
|
60
|
+
- `resolve_device_handle_from_driver`
|
|
61
|
+
- Supported public result types:
|
|
62
|
+
- `CooldownResult`
|
|
63
|
+
- `CooldownSetResult`
|
|
64
|
+
- `CooldownEscalatedResult`
|
|
61
65
|
- Supported public exceptions:
|
|
62
|
-
- `NoClaimableDevicesError`
|
|
63
66
|
- `UnknownIncludeError`
|
|
64
67
|
- `ReserveCapabilitiesUnsupportedError`
|
|
65
68
|
- Manual hardware examples under `testkit/examples/`
|
|
@@ -111,6 +114,7 @@ The package supports Python 3.10 and newer.
|
|
|
111
114
|
| `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`. |
|
|
112
115
|
| `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
|
|
113
116
|
| `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
|
|
117
|
+
| `GRIDFLEET_RUN_ID` | `free` | Optional run id injected into Appium capabilities as `gridfleet:run_id`. The pytest plugin sets this automatically when sessions are created inside a reserved run. |
|
|
114
118
|
|
|
115
119
|
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.
|
|
116
120
|
|
|
@@ -146,9 +150,11 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
|
|
|
146
150
|
|
|
147
151
|
- Creates an Appium session through `GRID_URL`
|
|
148
152
|
- Injects `gridfleet:testName` with the pytest test name
|
|
153
|
+
- Injects `gridfleet:run_id` when a `GRIDFLEET_RUN_ID` environment variable is present (for example, inside a reserved run)
|
|
149
154
|
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
150
155
|
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
151
156
|
- Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
|
|
157
|
+
- Exposes `device_handle` for fetching the canonical manager device row using the runtime connection target
|
|
152
158
|
- Exposes `gridfleet_worker_id` which returns the pytest-xdist worker id, or `"controller"` for non-worker processes
|
|
153
159
|
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
154
160
|
|
|
@@ -183,6 +189,7 @@ finally:
|
|
|
183
189
|
| `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, ...) |
|
|
184
190
|
| `GridFleetClient.get_device(device_id)` | Fetch one full device detail row by backend device id |
|
|
185
191
|
| `GridFleetClient.get_device_config(connection_target)` | Look up a device by runtime connection target and fetch its config |
|
|
192
|
+
| `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
|
|
186
193
|
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
187
194
|
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
188
195
|
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
@@ -190,33 +197,33 @@ finally:
|
|
|
190
197
|
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
191
198
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
192
199
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
193
|
-
| `GridFleetClient.
|
|
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 |
|
|
195
|
-
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
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 |
|
|
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 |
|
|
198
|
-
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
200
|
+
| `GridFleetClient.signal_ready(run_id)` | Signal that a run is ready |
|
|
199
201
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
200
202
|
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
201
203
|
| `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
|
|
202
204
|
| `GridFleetClient.register_session(fields)` | Register a Grid/Appium session with optional requested capability metadata |
|
|
203
205
|
| `GridFleetClient.register_session_from_driver(driver, fields)` | Extract session id and capabilities from an Appium driver and register the session |
|
|
206
|
+
| `GridFleetClient.notify_session_finished(session_id)` | Tell the manager the WebDriver session has ended |
|
|
204
207
|
| `GridFleetClient.update_session_status(session_id, status)` | Report final session status |
|
|
205
208
|
| `GridFleetClient.complete_run(run_id)` | Complete a run |
|
|
206
209
|
| `GridFleetClient.cancel_run(run_id)` | Cancel a run |
|
|
210
|
+
| `GridFleetClient.cooldown_device(run_id, device_id, reason=..., ttl_seconds=...)` | Exclude a reserved device from the run with a cooldown TTL |
|
|
207
211
|
| `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
|
|
208
212
|
| `build_error_session_payload(fields)` | Build a `/api/sessions` payload for driver-creation failures without importing pytest |
|
|
209
|
-
| `hydrate_allocated_device(
|
|
213
|
+
| `hydrate_allocated_device(device_handle, run_id, client)` | Combine a device handle with optional device config and live capabilities |
|
|
210
214
|
| `hydrate_allocated_device_from_driver(allocated, driver, client)` | Return a new allocated-device object with capabilities from a running driver |
|
|
215
|
+
| `resolve_device_handle_from_driver(driver, client)` | Resolve the assigned manager device row from a running Appium session |
|
|
211
216
|
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
212
217
|
| `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 |
|
|
213
218
|
|
|
214
219
|
### Worker Identity
|
|
215
220
|
|
|
216
|
-
`worker_id` is an arbitrary string used for
|
|
221
|
+
`worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
|
|
217
222
|
|
|
218
223
|
### Reservation Flow
|
|
219
224
|
|
|
225
|
+
GridFleet runs are grid-routed: once devices are reserved, the manager tags matching Grid nodes with the run id, and Selenium Grid routes new Appium sessions to those nodes automatically via the `gridfleet:run_id` capability. There are no per-worker claim or release calls.
|
|
226
|
+
|
|
220
227
|
```python
|
|
221
228
|
from gridfleet_testkit import GridFleetClient, register_run_cleanup
|
|
222
229
|
|
|
@@ -258,53 +265,51 @@ client.signal_active(run_id)
|
|
|
258
265
|
|
|
259
266
|
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"])`.
|
|
260
267
|
|
|
261
|
-
|
|
268
|
+
For pytest-xdist controller/worker orchestration, see [Testkit xdist recipe](../docs/guides/testkit-xdist-recipe.md). The recipe is copyable guidance, not a public testkit abstraction.
|
|
269
|
+
|
|
270
|
+
### Cooling Down an Unstable Device
|
|
271
|
+
|
|
272
|
+
If a reserved device becomes unstable during a test, you can put it on cooldown so it is excluded from the run for a TTL. If the same device is cooled down too many times in the same run, it is escalated to maintenance automatically.
|
|
262
273
|
|
|
263
274
|
```python
|
|
264
275
|
from gridfleet_testkit import GridFleetClient
|
|
265
276
|
|
|
266
|
-
client = GridFleetClient(
|
|
277
|
+
client = GridFleetClient()
|
|
267
278
|
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
result = client.cooldown_device(
|
|
280
|
+
run_id="run-123",
|
|
281
|
+
device_id="device-456",
|
|
282
|
+
reason="Connection dropped mid-test",
|
|
283
|
+
ttl_seconds=120,
|
|
284
|
+
)
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
client.release_device_with_cooldown(
|
|
276
|
-
run_id,
|
|
277
|
-
device_id=device_id,
|
|
278
|
-
worker_id="gw0",
|
|
279
|
-
reason=str(exc),
|
|
280
|
-
ttl_seconds=60,
|
|
281
|
-
)
|
|
282
|
-
raise
|
|
283
|
-
else:
|
|
284
|
-
client.release_device(run_id, device_id=device_id, worker_id="gw0")
|
|
286
|
+
if result["status"] == "cooldown_set":
|
|
287
|
+
print(f"Device on cooldown until {result['excluded_until']}")
|
|
288
|
+
elif result["status"] == "maintenance_escalated":
|
|
289
|
+
print(f"Escalated after {result['cooldown_count']} cooldowns")
|
|
285
290
|
```
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
For pytest-xdist controller/worker orchestration, see [Testkit xdist recipe](../docs/guides/testkit-xdist-recipe.md). The recipe is copyable guidance, not a public testkit abstraction.
|
|
292
|
+
The manager enforces a maximum TTL via the `general.device_cooldown_max_sec` setting. The default is 3600 seconds. An `httpx.HTTPStatusError` with status 422 is raised if `ttl_seconds` exceeds the maximum.
|
|
290
293
|
|
|
291
294
|
### Allocated Device Hydration
|
|
292
295
|
|
|
293
|
-
Use `hydrate_allocated_device(
|
|
296
|
+
Use `hydrate_allocated_device(...)` immediately after reserving a run when a custom plugin needs a stable object instead of raw device-handle JSON.
|
|
294
297
|
|
|
295
298
|
```python
|
|
296
|
-
from gridfleet_testkit import GridFleetClient, hydrate_allocated_device
|
|
299
|
+
from gridfleet_testkit import GridFleetClient, hydrate_allocated_device, resolve_device_handle_from_driver
|
|
297
300
|
|
|
298
|
-
client = GridFleetClient(
|
|
301
|
+
client = GridFleetClient()
|
|
299
302
|
run_id = "run-123"
|
|
300
|
-
claim = client.claim_device_with_retry(run_id, worker_id="gw0", max_wait_sec=300)
|
|
301
|
-
allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
302
303
|
|
|
303
|
-
|
|
304
|
+
# After creating an Appium session, resolve the assigned device handle
|
|
305
|
+
device_handle = resolve_device_handle_from_driver(driver, client=client)
|
|
306
|
+
allocated = hydrate_allocated_device(device_handle, run_id=run_id, client=client)
|
|
307
|
+
|
|
308
|
+
assert allocated.device_id == device_handle["device_id"]
|
|
304
309
|
assert allocated.platform_name in {"Android", "iOS", "tvOS", "Roku"}
|
|
305
310
|
```
|
|
306
311
|
|
|
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
|
|
312
|
+
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 reserve response when the manager inlines it.
|
|
308
313
|
|
|
309
314
|
### Run Cleanup Policy
|
|
310
315
|
|
|
@@ -335,29 +340,33 @@ test_data = get_device_test_data_for_driver(driver)
|
|
|
335
340
|
|
|
336
341
|
### Errors and Result Types
|
|
337
342
|
|
|
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
343
|
- `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
344
|
- `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 `
|
|
345
|
+
- `CooldownResult`: union response type from `cooldown_device`, with `status` equal to `"cooldown_set"` or `"maintenance_escalated"`. `CooldownSetResult` and `CooldownEscalatedResult` are the concrete TypedDict variants.
|
|
342
346
|
|
|
343
|
-
### Reduced HTTP
|
|
347
|
+
### Reduced HTTP Round-trips on Reserve
|
|
344
348
|
|
|
345
|
-
The manager can inline device config and live capabilities into the
|
|
349
|
+
The manager can inline device config and live capabilities into the `reserve_devices` response, eliminating per-worker follow-up GETs.
|
|
346
350
|
|
|
347
351
|
```python
|
|
348
352
|
client = GridFleetClient()
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
353
|
+
run = client.reserve_devices(
|
|
354
|
+
name="my-test-run",
|
|
355
|
+
requirements=[...],
|
|
356
|
+
include=("config", "capabilities"),
|
|
357
|
+
)
|
|
358
|
+
for device_handle in run["devices"]:
|
|
359
|
+
allocated = hydrate_allocated_device(device_handle, run_id=run["id"], client=client)
|
|
360
|
+
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
352
361
|
```
|
|
353
362
|
|
|
354
363
|
`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.
|
|
355
364
|
|
|
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 `
|
|
365
|
+
`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 `hydrate_allocated_device` call instead if you need capabilities after sessions are running.
|
|
357
366
|
|
|
358
367
|
`include=` must be a sequence of strings (tuple or list) — order is preserved in the emitted query parameter. Passing a bare string like `include="config"` raises `TypeError` to avoid silently splitting the value into characters.
|
|
359
368
|
|
|
360
|
-
`hydrate_allocated_device` accepts
|
|
369
|
+
`hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries or rows returned by `get_device_by_connection_target`.
|
|
361
370
|
|
|
362
371
|
## Examples
|
|
363
372
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Stable import root: `gridfleet_testkit`
|
|
8
8
|
- Supported pytest plugin: `gridfleet_testkit.pytest_plugin`
|
|
9
|
-
- Supported pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `gridfleet_worker_id`
|
|
9
|
+
- Supported pytest fixtures: `appium_driver`, `gridfleet_client`, `device_config`, `device_test_data`, `device_handle`, `gridfleet_worker_id`
|
|
10
10
|
- Supported public Appium helpers:
|
|
11
11
|
- `build_appium_options`
|
|
12
12
|
- `create_appium_driver`
|
|
@@ -20,12 +20,15 @@
|
|
|
20
20
|
- Supported public allocation/session helpers:
|
|
21
21
|
- `AllocatedDevice`
|
|
22
22
|
- `UnavailableInclude`
|
|
23
|
-
- `CooldownResult`
|
|
24
23
|
- `build_error_session_payload`
|
|
25
24
|
- `hydrate_allocated_device`
|
|
26
25
|
- `hydrate_allocated_device_from_driver`
|
|
26
|
+
- `resolve_device_handle_from_driver`
|
|
27
|
+
- Supported public result types:
|
|
28
|
+
- `CooldownResult`
|
|
29
|
+
- `CooldownSetResult`
|
|
30
|
+
- `CooldownEscalatedResult`
|
|
27
31
|
- Supported public exceptions:
|
|
28
|
-
- `NoClaimableDevicesError`
|
|
29
32
|
- `UnknownIncludeError`
|
|
30
33
|
- `ReserveCapabilitiesUnsupportedError`
|
|
31
34
|
- Manual hardware examples under `testkit/examples/`
|
|
@@ -77,6 +80,7 @@ The package supports Python 3.10 and newer.
|
|
|
77
80
|
| `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`. |
|
|
78
81
|
| `GRIDFLEET_TESTKIT_PACK_ID` | unset | Optional default driver pack id for Appium option building |
|
|
79
82
|
| `GRIDFLEET_TESTKIT_PLATFORM_ID` | unset | Optional default platform id for Appium option building |
|
|
83
|
+
| `GRIDFLEET_RUN_ID` | `free` | Optional run id injected into Appium capabilities as `gridfleet:run_id`. The pytest plugin sets this automatically when sessions are created inside a reserved run. |
|
|
80
84
|
|
|
81
85
|
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.
|
|
82
86
|
|
|
@@ -112,9 +116,11 @@ If you need raw Appium control instead, omit `pack_id` and `platform_id`, then p
|
|
|
112
116
|
|
|
113
117
|
- Creates an Appium session through `GRID_URL`
|
|
114
118
|
- Injects `gridfleet:testName` with the pytest test name
|
|
119
|
+
- Injects `gridfleet:run_id` when a `GRIDFLEET_RUN_ID` environment variable is present (for example, inside a reserved run)
|
|
115
120
|
- Reports final session status back to `GRIDFLEET_API_URL`
|
|
116
121
|
- Exposes `device_config` for post-session config lookup using the runtime connection target
|
|
117
122
|
- Exposes `device_test_data` for post-session operator-attached test data using the runtime connection target
|
|
123
|
+
- Exposes `device_handle` for fetching the canonical manager device row using the runtime connection target
|
|
118
124
|
- Exposes `gridfleet_worker_id` which returns the pytest-xdist worker id, or `"controller"` for non-worker processes
|
|
119
125
|
- Relies on manager-owned runtime isolation for Appium driver sub-ports and XCUITest build paths
|
|
120
126
|
|
|
@@ -149,6 +155,7 @@ finally:
|
|
|
149
155
|
| `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, ...) |
|
|
150
156
|
| `GridFleetClient.get_device(device_id)` | Fetch one full device detail row by backend device id |
|
|
151
157
|
| `GridFleetClient.get_device_config(connection_target)` | Look up a device by runtime connection target and fetch its config |
|
|
158
|
+
| `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
|
|
152
159
|
| `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
|
|
153
160
|
| `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
|
|
154
161
|
| `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
|
|
@@ -156,33 +163,33 @@ finally:
|
|
|
156
163
|
| `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
|
|
157
164
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
158
165
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
159
|
-
| `GridFleetClient.
|
|
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 |
|
|
161
|
-
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
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 |
|
|
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 |
|
|
164
|
-
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
166
|
+
| `GridFleetClient.signal_ready(run_id)` | Signal that a run is ready |
|
|
165
167
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
166
168
|
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
167
169
|
| `GridFleetClient.report_preparation_failure(run_id, device_id, message, source="ci_preparation")` | Exclude one reserved device after setup fails |
|
|
168
170
|
| `GridFleetClient.register_session(fields)` | Register a Grid/Appium session with optional requested capability metadata |
|
|
169
171
|
| `GridFleetClient.register_session_from_driver(driver, fields)` | Extract session id and capabilities from an Appium driver and register the session |
|
|
172
|
+
| `GridFleetClient.notify_session_finished(session_id)` | Tell the manager the WebDriver session has ended |
|
|
170
173
|
| `GridFleetClient.update_session_status(session_id, status)` | Report final session status |
|
|
171
174
|
| `GridFleetClient.complete_run(run_id)` | Complete a run |
|
|
172
175
|
| `GridFleetClient.cancel_run(run_id)` | Cancel a run |
|
|
176
|
+
| `GridFleetClient.cooldown_device(run_id, device_id, reason=..., ttl_seconds=...)` | Exclude a reserved device from the run with a cooldown TTL |
|
|
173
177
|
| `GridFleetClient.start_heartbeat(run_id, interval=30)` | Start a background heartbeat thread |
|
|
174
178
|
| `build_error_session_payload(fields)` | Build a `/api/sessions` payload for driver-creation failures without importing pytest |
|
|
175
|
-
| `hydrate_allocated_device(
|
|
179
|
+
| `hydrate_allocated_device(device_handle, run_id, client)` | Combine a device handle with optional device config and live capabilities |
|
|
176
180
|
| `hydrate_allocated_device_from_driver(allocated, driver, client)` | Return a new allocated-device object with capabilities from a running driver |
|
|
181
|
+
| `resolve_device_handle_from_driver(driver, client)` | Resolve the assigned manager device row from a running Appium session |
|
|
177
182
|
| `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
|
|
178
183
|
| `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 |
|
|
179
184
|
|
|
180
185
|
### Worker Identity
|
|
181
186
|
|
|
182
|
-
`worker_id` is an arbitrary string used for
|
|
187
|
+
`worker_id` is an arbitrary string used for reservation telemetry and run attribution. For pytest-xdist, pass `request.config.workerinput["workerid"]` from worker processes; values are normally `gw0`, `gw1`, and so on. For controller-only flows, use `"controller"` or a stable hostname. For custom schedulers, use a UUID or job-specific worker name.
|
|
183
188
|
|
|
184
189
|
### Reservation Flow
|
|
185
190
|
|
|
191
|
+
GridFleet runs are grid-routed: once devices are reserved, the manager tags matching Grid nodes with the run id, and Selenium Grid routes new Appium sessions to those nodes automatically via the `gridfleet:run_id` capability. There are no per-worker claim or release calls.
|
|
192
|
+
|
|
186
193
|
```python
|
|
187
194
|
from gridfleet_testkit import GridFleetClient, register_run_cleanup
|
|
188
195
|
|
|
@@ -224,53 +231,51 @@ client.signal_active(run_id)
|
|
|
224
231
|
|
|
225
232
|
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"])`.
|
|
226
233
|
|
|
227
|
-
|
|
234
|
+
For pytest-xdist controller/worker orchestration, see [Testkit xdist recipe](../docs/guides/testkit-xdist-recipe.md). The recipe is copyable guidance, not a public testkit abstraction.
|
|
235
|
+
|
|
236
|
+
### Cooling Down an Unstable Device
|
|
237
|
+
|
|
238
|
+
If a reserved device becomes unstable during a test, you can put it on cooldown so it is excluded from the run for a TTL. If the same device is cooled down too many times in the same run, it is escalated to maintenance automatically.
|
|
228
239
|
|
|
229
240
|
```python
|
|
230
241
|
from gridfleet_testkit import GridFleetClient
|
|
231
242
|
|
|
232
|
-
client = GridFleetClient(
|
|
243
|
+
client = GridFleetClient()
|
|
233
244
|
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
result = client.cooldown_device(
|
|
246
|
+
run_id="run-123",
|
|
247
|
+
device_id="device-456",
|
|
248
|
+
reason="Connection dropped mid-test",
|
|
249
|
+
ttl_seconds=120,
|
|
250
|
+
)
|
|
236
251
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
client.release_device_with_cooldown(
|
|
242
|
-
run_id,
|
|
243
|
-
device_id=device_id,
|
|
244
|
-
worker_id="gw0",
|
|
245
|
-
reason=str(exc),
|
|
246
|
-
ttl_seconds=60,
|
|
247
|
-
)
|
|
248
|
-
raise
|
|
249
|
-
else:
|
|
250
|
-
client.release_device(run_id, device_id=device_id, worker_id="gw0")
|
|
252
|
+
if result["status"] == "cooldown_set":
|
|
253
|
+
print(f"Device on cooldown until {result['excluded_until']}")
|
|
254
|
+
elif result["status"] == "maintenance_escalated":
|
|
255
|
+
print(f"Escalated after {result['cooldown_count']} cooldowns")
|
|
251
256
|
```
|
|
252
257
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
For pytest-xdist controller/worker orchestration, see [Testkit xdist recipe](../docs/guides/testkit-xdist-recipe.md). The recipe is copyable guidance, not a public testkit abstraction.
|
|
258
|
+
The manager enforces a maximum TTL via the `general.device_cooldown_max_sec` setting. The default is 3600 seconds. An `httpx.HTTPStatusError` with status 422 is raised if `ttl_seconds` exceeds the maximum.
|
|
256
259
|
|
|
257
260
|
### Allocated Device Hydration
|
|
258
261
|
|
|
259
|
-
Use `hydrate_allocated_device(
|
|
262
|
+
Use `hydrate_allocated_device(...)` immediately after reserving a run when a custom plugin needs a stable object instead of raw device-handle JSON.
|
|
260
263
|
|
|
261
264
|
```python
|
|
262
|
-
from gridfleet_testkit import GridFleetClient, hydrate_allocated_device
|
|
265
|
+
from gridfleet_testkit import GridFleetClient, hydrate_allocated_device, resolve_device_handle_from_driver
|
|
263
266
|
|
|
264
|
-
client = GridFleetClient(
|
|
267
|
+
client = GridFleetClient()
|
|
265
268
|
run_id = "run-123"
|
|
266
|
-
claim = client.claim_device_with_retry(run_id, worker_id="gw0", max_wait_sec=300)
|
|
267
|
-
allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
# After creating an Appium session, resolve the assigned device handle
|
|
271
|
+
device_handle = resolve_device_handle_from_driver(driver, client=client)
|
|
272
|
+
allocated = hydrate_allocated_device(device_handle, run_id=run_id, client=client)
|
|
273
|
+
|
|
274
|
+
assert allocated.device_id == device_handle["device_id"]
|
|
270
275
|
assert allocated.platform_name in {"Android", "iOS", "tvOS", "Roku"}
|
|
271
276
|
```
|
|
272
277
|
|
|
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
|
|
278
|
+
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 reserve response when the manager inlines it.
|
|
274
279
|
|
|
275
280
|
### Run Cleanup Policy
|
|
276
281
|
|
|
@@ -301,29 +306,33 @@ test_data = get_device_test_data_for_driver(driver)
|
|
|
301
306
|
|
|
302
307
|
### Errors and Result Types
|
|
303
308
|
|
|
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
309
|
- `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
310
|
- `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 `
|
|
311
|
+
- `CooldownResult`: union response type from `cooldown_device`, with `status` equal to `"cooldown_set"` or `"maintenance_escalated"`. `CooldownSetResult` and `CooldownEscalatedResult` are the concrete TypedDict variants.
|
|
308
312
|
|
|
309
|
-
### Reduced HTTP
|
|
313
|
+
### Reduced HTTP Round-trips on Reserve
|
|
310
314
|
|
|
311
|
-
The manager can inline device config and live capabilities into the
|
|
315
|
+
The manager can inline device config and live capabilities into the `reserve_devices` response, eliminating per-worker follow-up GETs.
|
|
312
316
|
|
|
313
317
|
```python
|
|
314
318
|
client = GridFleetClient()
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
run = client.reserve_devices(
|
|
320
|
+
name="my-test-run",
|
|
321
|
+
requirements=[...],
|
|
322
|
+
include=("config", "capabilities"),
|
|
323
|
+
)
|
|
324
|
+
for device_handle in run["devices"]:
|
|
325
|
+
allocated = hydrate_allocated_device(device_handle, run_id=run["id"], client=client)
|
|
326
|
+
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
318
327
|
```
|
|
319
328
|
|
|
320
329
|
`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.
|
|
321
330
|
|
|
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 `
|
|
331
|
+
`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 `hydrate_allocated_device` call instead if you need capabilities after sessions are running.
|
|
323
332
|
|
|
324
333
|
`include=` must be a sequence of strings (tuple or list) — order is preserved in the emitted query parameter. Passing a bare string like `include="config"` raises `TypeError` to avoid silently splitting the value into characters.
|
|
325
334
|
|
|
326
|
-
`hydrate_allocated_device` accepts
|
|
335
|
+
`hydrate_allocated_device` accepts device-handle payloads such as `reserve_response["devices"]` entries or rows returned by `get_device_by_connection_target`.
|
|
327
336
|
|
|
328
337
|
## Examples
|
|
329
338
|
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`device_config` values returned by the manager are verbatim; the testkit no
|
|
4
4
|
longer distinguishes between masked and revealed payloads. Code that wants
|
|
5
|
-
the live Appium-side config can use
|
|
6
|
-
`claim_device(include=("config",))` or `client.get_device_config(connection_target)`.
|
|
5
|
+
the live Appium-side config can use `client.get_device_config(connection_target)`.
|
|
7
6
|
|
|
8
7
|
Environment variables read by the client:
|
|
9
8
|
|
|
@@ -32,31 +31,33 @@ from .appium import (
|
|
|
32
31
|
get_device_test_data_for_driver,
|
|
33
32
|
)
|
|
34
33
|
from .client import (
|
|
34
|
+
CooldownEscalatedResult,
|
|
35
35
|
CooldownResult,
|
|
36
|
+
CooldownSetResult,
|
|
36
37
|
GridFleetClient,
|
|
37
38
|
HeartbeatThread,
|
|
38
|
-
NoClaimableDevicesError,
|
|
39
39
|
ReserveCapabilitiesUnsupportedError,
|
|
40
40
|
UnknownIncludeError,
|
|
41
41
|
_default_api_url,
|
|
42
42
|
_default_grid_url,
|
|
43
43
|
register_run_cleanup,
|
|
44
44
|
)
|
|
45
|
-
from .sessions import build_error_session_payload
|
|
45
|
+
from .sessions import build_error_session_payload, resolve_device_handle_from_driver
|
|
46
46
|
|
|
47
47
|
try:
|
|
48
48
|
__version__ = version("gridfleet-testkit")
|
|
49
49
|
except PackageNotFoundError:
|
|
50
|
-
__version__ = "0.
|
|
50
|
+
__version__ = "0.8.0"
|
|
51
51
|
|
|
52
52
|
__all__ = [
|
|
53
53
|
"GRIDFLEET_API_URL",
|
|
54
54
|
"GRID_URL",
|
|
55
55
|
"AllocatedDevice",
|
|
56
|
+
"CooldownEscalatedResult",
|
|
56
57
|
"CooldownResult",
|
|
58
|
+
"CooldownSetResult",
|
|
57
59
|
"GridFleetClient",
|
|
58
60
|
"HeartbeatThread",
|
|
59
|
-
"NoClaimableDevicesError",
|
|
60
61
|
"ReserveCapabilitiesUnsupportedError",
|
|
61
62
|
"UnavailableInclude",
|
|
62
63
|
"UnknownIncludeError",
|
|
@@ -70,6 +71,7 @@ __all__ = [
|
|
|
70
71
|
"hydrate_allocated_device",
|
|
71
72
|
"hydrate_allocated_device_from_driver",
|
|
72
73
|
"register_run_cleanup",
|
|
74
|
+
"resolve_device_handle_from_driver",
|
|
73
75
|
]
|
|
74
76
|
|
|
75
77
|
|
|
@@ -19,7 +19,7 @@ class UnavailableInclude:
|
|
|
19
19
|
|
|
20
20
|
@dataclass(frozen=True)
|
|
21
21
|
class AllocatedDevice:
|
|
22
|
-
"""Combined view of
|
|
22
|
+
"""Combined view of an allocated device, ready for driver creation."""
|
|
23
23
|
|
|
24
24
|
run_id: str
|
|
25
25
|
device_id: str
|
|
@@ -35,8 +35,6 @@ class AllocatedDevice:
|
|
|
35
35
|
connection_type: str
|
|
36
36
|
manufacturer: str | None
|
|
37
37
|
model: str | None
|
|
38
|
-
claimed_by: str
|
|
39
|
-
claimed_at: str
|
|
40
38
|
config: dict[str, Any] | None
|
|
41
39
|
live_capabilities: dict[str, Any] | None
|
|
42
40
|
test_data: dict[str, Any] | None = None
|
|
@@ -115,7 +113,7 @@ def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInc
|
|
|
115
113
|
|
|
116
114
|
|
|
117
115
|
def hydrate_allocated_device(
|
|
118
|
-
|
|
116
|
+
device_handle: dict[str, Any],
|
|
119
117
|
*,
|
|
120
118
|
run_id: str,
|
|
121
119
|
client: GridFleetClient,
|
|
@@ -123,15 +121,8 @@ def hydrate_allocated_device(
|
|
|
123
121
|
fetch_capabilities: bool = False,
|
|
124
122
|
fetch_test_data: bool = False,
|
|
125
123
|
) -> AllocatedDevice:
|
|
126
|
-
"""Combine a
|
|
127
|
-
|
|
128
|
-
Accepts a ``ClaimResponse`` payload from ``GridFleetClient.claim_device`` only.
|
|
129
|
-
Reserve responses (``RunCreateResponse.devices`` entries before any worker
|
|
130
|
-
has claimed) lack ``claimed_by`` / ``claimed_at`` and will raise
|
|
131
|
-
``ValueError``. Iterate ``reserve_response['devices']`` and call
|
|
132
|
-
``claim_device`` per worker before hydrating.
|
|
133
|
-
"""
|
|
134
|
-
payload = dict(claim_response)
|
|
124
|
+
"""Combine a device handle with optional static config and live capabilities."""
|
|
125
|
+
payload = dict(device_handle)
|
|
135
126
|
device_id = _string_value(payload, "device_id")
|
|
136
127
|
if _needs_device_detail(payload):
|
|
137
128
|
payload = _merge_device_detail(payload, client.get_device(device_id))
|
|
@@ -178,8 +169,6 @@ def hydrate_allocated_device(
|
|
|
178
169
|
connection_type=_string_value(payload, "connection_type"),
|
|
179
170
|
manufacturer=_optional_string_value(payload, "manufacturer"),
|
|
180
171
|
model=_optional_string_value(payload, "model"),
|
|
181
|
-
claimed_by=_string_value(payload, "claimed_by"),
|
|
182
|
-
claimed_at=_string_value(payload, "claimed_at"),
|
|
183
172
|
config=config,
|
|
184
173
|
live_capabilities=live_capabilities,
|
|
185
174
|
test_data=test_data,
|
|
@@ -98,6 +98,7 @@ def build_appium_options(
|
|
|
98
98
|
from appium.options.common import AppiumOptions # noqa: PLC0415
|
|
99
99
|
|
|
100
100
|
params = dict(capabilities or {})
|
|
101
|
+
params.setdefault("gridfleet:run_id", os.environ.get("GRIDFLEET_RUN_ID", "free"))
|
|
101
102
|
explicit_platform_name = params.get("platformName")
|
|
102
103
|
if explicit_platform_name is not None and (pack_id is not None or platform_id is not None):
|
|
103
104
|
raise ValueError("Use either pack_id/platform_id or the raw platformName capability, not both.")
|