gridfleet-testkit 0.3.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.5.0/CHANGELOG.md +82 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/PKG-INFO +88 -9
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/README.md +86 -7
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/__init__.py +31 -4
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/allocation.py +61 -3
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/appium.py +21 -12
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/client.py +274 -49
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/pytest_plugin.py +37 -4
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/pyproject.toml +6 -3
- gridfleet_testkit-0.5.0/tests/test_allocation.py +371 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/tests/test_appium.py +16 -8
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/tests/test_client.py +705 -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.3.0 → gridfleet_testkit-0.5.0}/tests/test_package_metadata.py +26 -0
- {gridfleet_testkit-0.3.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.3.0/CHANGELOG.md +0 -44
- gridfleet_testkit-0.3.0/tests/test_allocation.py +0 -175
- gridfleet_testkit-0.3.0/uv.lock +0 -609
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/.gitignore +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/_example_helpers.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_android_mobile_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_android_tv_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_firetv_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_ios_simulator_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_roku_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_roku_sideload_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/examples/test_tvos_screenshot.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/gridfleet_testkit/sessions.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/tests/test_driver_agnostic_guard.py +0 -0
- {gridfleet_testkit-0.3.0 → gridfleet_testkit-0.5.0}/tests/test_sessions.py +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog — GridFleet Testkit
|
|
2
|
+
|
|
3
|
+
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
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
|
+
|
|
36
|
+
## [0.4.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.3.0...gridfleet-testkit-v0.4.0) (2026-05-06)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* **testkit:** wire ?include=config,capabilities through claim/reserve and hydrate inline ([#95](https://github.com/quidow/gridfleet/issues/95)) ([20ed20d](https://github.com/quidow/gridfleet/commit/20ed20d9ee362890923146e771ad8805b45e5bfa))
|
|
42
|
+
|
|
43
|
+
## [0.3.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.2.1...gridfleet-testkit-v0.3.0) (2026-05-05)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### ⚠ BREAKING CHANGES
|
|
47
|
+
|
|
48
|
+
* **testkit:** promote public api helpers ([#92](https://github.com/quidow/gridfleet/issues/92))
|
|
49
|
+
|
|
50
|
+
### Features
|
|
51
|
+
|
|
52
|
+
* **testkit:** add xdist recipe primitives ([#93](https://github.com/quidow/gridfleet/issues/93)) ([58fd3c3](https://github.com/quidow/gridfleet/commit/58fd3c3402ba7e735aae55e27abbe65a05c8ffe8))
|
|
53
|
+
* **testkit:** promote public api helpers ([#92](https://github.com/quidow/gridfleet/issues/92)) ([80d4483](https://github.com/quidow/gridfleet/commit/80d44832903f532de3da238d020b5dc27eb8b30e))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Bug Fixes
|
|
57
|
+
|
|
58
|
+
* **agent:** trigger release for port conflict cleanup ([6a561ca](https://github.com/quidow/gridfleet/commit/6a561ca480c62b9abb2d5141fa98fc4e1a7696b6))
|
|
59
|
+
|
|
60
|
+
## [0.2.1](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.2.0...gridfleet-testkit-v0.2.1) (2026-05-03)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
* **testkit:** bound supported python metadata ([c5fff86](https://github.com/quidow/gridfleet/commit/c5fff86cbb2a4897ac571c7c5b989f0361e49743))
|
|
66
|
+
|
|
67
|
+
## [0.2.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.1.0...gridfleet-testkit-v0.2.0) (2026-05-03)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### Features
|
|
71
|
+
|
|
72
|
+
* **testkit:** add run-scoped device cooldowns ([#54](https://github.com/quidow/gridfleet/issues/54)) ([6163dc9](https://github.com/quidow/gridfleet/commit/6163dc959334e933b43c20a99ad4edcbdae6c98b))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Bug Fixes
|
|
76
|
+
|
|
77
|
+
* idempotent device release after lifecycle cleanup ([#12](https://github.com/quidow/gridfleet/issues/12)) ([7a98a5d](https://github.com/quidow/gridfleet/commit/7a98a5d18330150aab0a852f6b894d1d53de257c))
|
|
78
|
+
|
|
79
|
+
## 0.1.0 — Initial Public Preview
|
|
80
|
+
|
|
81
|
+
- Initial public preview of the GridFleet testkit.
|
|
82
|
+
- Python pytest/Appium helper package with device reservation, capability injection, and session lifecycle fixtures.
|
|
@@ -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,7 +304,60 @@ 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.
|
|
342
|
+
|
|
343
|
+
### Reduced HTTP round-trips on claim
|
|
344
|
+
|
|
345
|
+
The manager can inline device config and live capabilities into the claim/reserve response, eliminating per-worker follow-up GETs.
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
client = GridFleetClient()
|
|
349
|
+
claim = client.claim_device(run_id, worker_id="w0", include=("config", "capabilities"))
|
|
350
|
+
allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
351
|
+
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
352
|
+
```
|
|
353
|
+
|
|
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.
|
|
355
|
+
|
|
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.
|
|
357
|
+
|
|
358
|
+
`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
|
+
|
|
360
|
+
`hydrate_allocated_device` accepts claim responses only. For multi-device reservations, iterate `reserve_response["devices"]`, call `claim_device` per worker, and hydrate each claim response.
|
|
282
361
|
|
|
283
362
|
## Examples
|
|
284
363
|
|
|
@@ -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,7 +270,60 @@ 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.
|
|
308
|
+
|
|
309
|
+
### Reduced HTTP round-trips on claim
|
|
310
|
+
|
|
311
|
+
The manager can inline device config and live capabilities into the claim/reserve response, eliminating per-worker follow-up GETs.
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
client = GridFleetClient()
|
|
315
|
+
claim = client.claim_device(run_id, worker_id="w0", include=("config", "capabilities"))
|
|
316
|
+
allocated = hydrate_allocated_device(claim, run_id=run_id, client=client)
|
|
317
|
+
# zero follow-up GETs; allocated.config / allocated.live_capabilities populated inline
|
|
318
|
+
```
|
|
319
|
+
|
|
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.
|
|
321
|
+
|
|
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.
|
|
323
|
+
|
|
324
|
+
`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
|
+
|
|
326
|
+
`hydrate_allocated_device` accepts claim responses only. For multi-device reservations, iterate `reserve_response["devices"]`, call `claim_device` per worker, and hydrate each claim response.
|
|
248
327
|
|
|
249
328
|
## Examples
|
|
250
329
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
"""Supported Python integration helpers for GridFleet.
|
|
2
2
|
|
|
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)`.
|
|
7
|
+
|
|
3
8
|
Environment variables read by the client:
|
|
4
9
|
|
|
5
10
|
- GRID_URL: Selenium Grid URL used by Appium helper defaults.
|
|
@@ -13,19 +18,28 @@ this package because run-state sharing is consumer policy.
|
|
|
13
18
|
|
|
14
19
|
from importlib.metadata import PackageNotFoundError, version
|
|
15
20
|
|
|
16
|
-
from .allocation import
|
|
21
|
+
from .allocation import (
|
|
22
|
+
AllocatedDevice,
|
|
23
|
+
UnavailableInclude,
|
|
24
|
+
hydrate_allocated_device,
|
|
25
|
+
hydrate_allocated_device_from_driver,
|
|
26
|
+
)
|
|
17
27
|
from .appium import (
|
|
18
28
|
build_appium_options,
|
|
19
29
|
create_appium_driver,
|
|
20
30
|
get_connection_target_from_driver,
|
|
21
31
|
get_device_config_for_driver,
|
|
32
|
+
get_device_test_data_for_driver,
|
|
22
33
|
)
|
|
23
34
|
from .client import (
|
|
24
|
-
|
|
25
|
-
GRIDFLEET_API_URL,
|
|
35
|
+
CooldownResult,
|
|
26
36
|
GridFleetClient,
|
|
27
37
|
HeartbeatThread,
|
|
28
38
|
NoClaimableDevicesError,
|
|
39
|
+
ReserveCapabilitiesUnsupportedError,
|
|
40
|
+
UnknownIncludeError,
|
|
41
|
+
_default_api_url,
|
|
42
|
+
_default_grid_url,
|
|
29
43
|
register_run_cleanup,
|
|
30
44
|
)
|
|
31
45
|
from .sessions import build_error_session_payload
|
|
@@ -33,22 +47,35 @@ from .sessions import build_error_session_payload
|
|
|
33
47
|
try:
|
|
34
48
|
__version__ = version("gridfleet-testkit")
|
|
35
49
|
except PackageNotFoundError:
|
|
36
|
-
__version__ = "0.
|
|
50
|
+
__version__ = "0.5.0"
|
|
37
51
|
|
|
38
52
|
__all__ = [
|
|
39
53
|
"GRIDFLEET_API_URL",
|
|
40
54
|
"GRID_URL",
|
|
41
55
|
"AllocatedDevice",
|
|
56
|
+
"CooldownResult",
|
|
42
57
|
"GridFleetClient",
|
|
43
58
|
"HeartbeatThread",
|
|
44
59
|
"NoClaimableDevicesError",
|
|
60
|
+
"ReserveCapabilitiesUnsupportedError",
|
|
61
|
+
"UnavailableInclude",
|
|
62
|
+
"UnknownIncludeError",
|
|
45
63
|
"__version__",
|
|
46
64
|
"build_appium_options",
|
|
47
65
|
"build_error_session_payload",
|
|
48
66
|
"create_appium_driver",
|
|
49
67
|
"get_connection_target_from_driver",
|
|
50
68
|
"get_device_config_for_driver",
|
|
69
|
+
"get_device_test_data_for_driver",
|
|
51
70
|
"hydrate_allocated_device",
|
|
52
71
|
"hydrate_allocated_device_from_driver",
|
|
53
72
|
"register_run_cleanup",
|
|
54
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}")
|
|
@@ -9,6 +9,14 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from .client import GridFleetClient
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class UnavailableInclude:
|
|
14
|
+
"""One include key the backend could not satisfy on this allocation."""
|
|
15
|
+
|
|
16
|
+
include: str
|
|
17
|
+
reason: str
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
@dataclass(frozen=True)
|
|
13
21
|
class AllocatedDevice:
|
|
14
22
|
"""Combined view of a claimed device, ready for driver creation."""
|
|
@@ -31,6 +39,8 @@ class AllocatedDevice:
|
|
|
31
39
|
claimed_at: str
|
|
32
40
|
config: dict[str, Any] | None
|
|
33
41
|
live_capabilities: dict[str, Any] | None
|
|
42
|
+
test_data: dict[str, Any] | None = None
|
|
43
|
+
unavailable_includes: tuple[UnavailableInclude, ...] = ()
|
|
34
44
|
|
|
35
45
|
@property
|
|
36
46
|
def is_real_device(self) -> bool:
|
|
@@ -89,6 +99,21 @@ def _merge_device_detail(payload: dict[str, Any], detail: dict[str, Any]) -> dic
|
|
|
89
99
|
return merged
|
|
90
100
|
|
|
91
101
|
|
|
102
|
+
def _parse_unavailable_includes(payload: dict[str, Any]) -> tuple[UnavailableInclude, ...]:
|
|
103
|
+
raw = payload.get("unavailable_includes")
|
|
104
|
+
if not isinstance(raw, list):
|
|
105
|
+
return ()
|
|
106
|
+
parsed: list[UnavailableInclude] = []
|
|
107
|
+
for entry in raw:
|
|
108
|
+
if not isinstance(entry, dict):
|
|
109
|
+
continue
|
|
110
|
+
include = entry.get("include")
|
|
111
|
+
reason = entry.get("reason")
|
|
112
|
+
if isinstance(include, str) and include and isinstance(reason, str) and reason:
|
|
113
|
+
parsed.append(UnavailableInclude(include=include, reason=reason))
|
|
114
|
+
return tuple(parsed)
|
|
115
|
+
|
|
116
|
+
|
|
92
117
|
def hydrate_allocated_device(
|
|
93
118
|
claim_response: dict[str, Any],
|
|
94
119
|
*,
|
|
@@ -96,16 +121,47 @@ def hydrate_allocated_device(
|
|
|
96
121
|
client: GridFleetClient,
|
|
97
122
|
fetch_config: bool = True,
|
|
98
123
|
fetch_capabilities: bool = False,
|
|
124
|
+
fetch_test_data: bool = False,
|
|
99
125
|
) -> AllocatedDevice:
|
|
100
|
-
"""Combine a claim response with optional static config and live capabilities.
|
|
126
|
+
"""Combine a claim response with optional static config and live capabilities.
|
|
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
|
+
"""
|
|
101
134
|
payload = dict(claim_response)
|
|
102
135
|
device_id = _string_value(payload, "device_id")
|
|
103
136
|
if _needs_device_detail(payload):
|
|
104
137
|
payload = _merge_device_detail(payload, client.get_device(device_id))
|
|
105
138
|
|
|
139
|
+
unavailable_includes = _parse_unavailable_includes(payload)
|
|
140
|
+
unavailable_set = {entry.include for entry in unavailable_includes}
|
|
141
|
+
|
|
106
142
|
connection_target = _optional_string_value(payload, "connection_target")
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
inline_config = payload.get("config")
|
|
144
|
+
if isinstance(inline_config, dict):
|
|
145
|
+
config: dict[str, Any] | None = inline_config
|
|
146
|
+
elif fetch_config and connection_target and "config" not in unavailable_set:
|
|
147
|
+
config = client.get_device_config(connection_target)
|
|
148
|
+
else:
|
|
149
|
+
config = None
|
|
150
|
+
inline_capabilities = payload.get("live_capabilities")
|
|
151
|
+
if isinstance(inline_capabilities, dict):
|
|
152
|
+
live_capabilities: dict[str, Any] | None = inline_capabilities
|
|
153
|
+
elif fetch_capabilities and "capabilities" not in unavailable_set:
|
|
154
|
+
live_capabilities = client.get_device_capabilities(device_id)
|
|
155
|
+
else:
|
|
156
|
+
live_capabilities = None
|
|
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
|
|
109
165
|
|
|
110
166
|
return AllocatedDevice(
|
|
111
167
|
run_id=run_id,
|
|
@@ -126,6 +182,8 @@ def hydrate_allocated_device(
|
|
|
126
182
|
claimed_at=_string_value(payload, "claimed_at"),
|
|
127
183
|
config=config,
|
|
128
184
|
live_capabilities=live_capabilities,
|
|
185
|
+
test_data=test_data,
|
|
186
|
+
unavailable_includes=unavailable_includes,
|
|
129
187
|
)
|
|
130
188
|
|
|
131
189
|
|
|
@@ -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)
|