gridfleet-testkit 0.1.0__tar.gz → 0.2.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.2.0/CHANGELOG.md +20 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/PKG-INFO +33 -1
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/README.md +32 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/gridfleet_testkit/__init__.py +10 -2
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/gridfleet_testkit/client.py +74 -1
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/pyproject.toml +1 -1
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/tests/test_client.py +113 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/uv.lock +1 -1
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/.gitignore +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/_example_helpers.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_android_mobile_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_android_tv_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_firetv_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_ios_simulator_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_roku_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_roku_sideload_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_tvos_screenshot.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/gridfleet_testkit/appium.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/gridfleet_testkit/pytest_plugin.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/tests/test_appium.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/tests/test_driver_agnostic_guard.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/tests/test_package_metadata.py +0 -0
- {gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/tests/test_pytest_plugin.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog — GridFleet Testkit
|
|
2
|
+
|
|
3
|
+
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.2.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.1.0...gridfleet-testkit-v0.2.0) (2026-05-03)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **testkit:** add run-scoped device cooldowns ([#54](https://github.com/quidow/gridfleet/issues/54)) ([6163dc9](https://github.com/quidow/gridfleet/commit/6163dc959334e933b43c20a99ad4edcbdae6c98b))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* idempotent device release after lifecycle cleanup ([#12](https://github.com/quidow/gridfleet/issues/12)) ([7a98a5d](https://github.com/quidow/gridfleet/commit/7a98a5d18330150aab0a852f6b894d1d53de257c))
|
|
16
|
+
|
|
17
|
+
## 0.1.0 — Initial Public Preview
|
|
18
|
+
|
|
19
|
+
- Initial public preview of the GridFleet testkit.
|
|
20
|
+
- 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.2.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
|
|
@@ -165,6 +165,10 @@ finally:
|
|
|
165
165
|
| `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
|
|
166
166
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
167
167
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
168
|
+
| `GridFleetClient.claim_device(run_id, worker_id=...)` | Claim one reserved device for a worker |
|
|
169
|
+
| `GridFleetClient.claim_device_with_retry(run_id, worker_id=..., max_wait_sec=300)` | Claim one reserved device, sleeping according to server `Retry-After` responses |
|
|
170
|
+
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
171
|
+
| `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 |
|
|
168
172
|
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
169
173
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
170
174
|
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
@@ -215,6 +219,34 @@ client.signal_active(run_id)
|
|
|
215
219
|
|
|
216
220
|
Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
|
|
217
221
|
|
|
222
|
+
### Worker Claim With Cooldown
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from gridfleet_testkit import GridFleetClient
|
|
226
|
+
|
|
227
|
+
client = GridFleetClient("http://manager-ip:8000/api")
|
|
228
|
+
|
|
229
|
+
claim = client.claim_device_with_retry(run_id, worker_id="gw0", max_wait_sec=300)
|
|
230
|
+
device_id = claim["device_id"]
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Create the Appium session and run test setup for this worker.
|
|
234
|
+
...
|
|
235
|
+
except RuntimeError as exc:
|
|
236
|
+
client.release_device_with_cooldown(
|
|
237
|
+
run_id,
|
|
238
|
+
device_id=device_id,
|
|
239
|
+
worker_id="gw0",
|
|
240
|
+
reason=str(exc),
|
|
241
|
+
ttl_seconds=60,
|
|
242
|
+
)
|
|
243
|
+
raise
|
|
244
|
+
else:
|
|
245
|
+
client.release_device(run_id, device_id=device_id, worker_id="gw0")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Cooldowns are scoped to the active run. They prevent the same run from reclaiming the device until `ttl_seconds` expires, but completing or cancelling the run releases the physical device normally.
|
|
249
|
+
|
|
218
250
|
## Examples
|
|
219
251
|
|
|
220
252
|
Baseline screenshot examples:
|
|
@@ -131,6 +131,10 @@ finally:
|
|
|
131
131
|
| `GridFleetClient.get_device_config(connection_target, reveal=True)` | Look up a device by runtime connection target and fetch its config |
|
|
132
132
|
| `GridFleetClient.get_driver_pack_catalog()` | Fetch enabled driver-pack catalog data for Appium platform selection |
|
|
133
133
|
| `GridFleetClient.reserve_devices(...)` | Create a run/reservation and return the manager response |
|
|
134
|
+
| `GridFleetClient.claim_device(run_id, worker_id=...)` | Claim one reserved device for a worker |
|
|
135
|
+
| `GridFleetClient.claim_device_with_retry(run_id, worker_id=..., max_wait_sec=300)` | Claim one reserved device, sleeping according to server `Retry-After` responses |
|
|
136
|
+
| `GridFleetClient.release_device(run_id, device_id=..., worker_id=...)` | Release a worker claim without cooldown |
|
|
137
|
+
| `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 |
|
|
134
138
|
| `GridFleetClient.signal_ready(run_id)` | Move a run to `ready` |
|
|
135
139
|
| `GridFleetClient.signal_active(run_id)` | Move a run to `active` |
|
|
136
140
|
| `GridFleetClient.heartbeat(run_id)` | Send a run heartbeat and read current state |
|
|
@@ -181,6 +185,34 @@ client.signal_active(run_id)
|
|
|
181
185
|
|
|
182
186
|
Use `count` for exact reservations. Use `allocation: "all_available"` when CI should reserve every currently eligible matching device and size its worker pool from `len(run["devices"])`.
|
|
183
187
|
|
|
188
|
+
### Worker Claim With Cooldown
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from gridfleet_testkit import GridFleetClient
|
|
192
|
+
|
|
193
|
+
client = GridFleetClient("http://manager-ip:8000/api")
|
|
194
|
+
|
|
195
|
+
claim = client.claim_device_with_retry(run_id, worker_id="gw0", max_wait_sec=300)
|
|
196
|
+
device_id = claim["device_id"]
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
# Create the Appium session and run test setup for this worker.
|
|
200
|
+
...
|
|
201
|
+
except RuntimeError as exc:
|
|
202
|
+
client.release_device_with_cooldown(
|
|
203
|
+
run_id,
|
|
204
|
+
device_id=device_id,
|
|
205
|
+
worker_id="gw0",
|
|
206
|
+
reason=str(exc),
|
|
207
|
+
ttl_seconds=60,
|
|
208
|
+
)
|
|
209
|
+
raise
|
|
210
|
+
else:
|
|
211
|
+
client.release_device(run_id, device_id=device_id, worker_id="gw0")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Cooldowns are scoped to the active run. They prevent the same run from reclaiming the device until `ttl_seconds` expires, but completing or cancelling the run releases the physical device normally.
|
|
215
|
+
|
|
184
216
|
## Examples
|
|
185
217
|
|
|
186
218
|
Baseline screenshot examples:
|
|
@@ -8,18 +8,26 @@ from .appium import (
|
|
|
8
8
|
get_connection_target_from_driver,
|
|
9
9
|
get_device_config_for_driver,
|
|
10
10
|
)
|
|
11
|
-
from .client import
|
|
11
|
+
from .client import (
|
|
12
|
+
GRID_URL,
|
|
13
|
+
GRIDFLEET_API_URL,
|
|
14
|
+
GridFleetClient,
|
|
15
|
+
HeartbeatThread,
|
|
16
|
+
NoClaimableDevicesError,
|
|
17
|
+
register_run_cleanup,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
try:
|
|
14
21
|
__version__ = version("gridfleet-testkit")
|
|
15
22
|
except PackageNotFoundError:
|
|
16
|
-
__version__ = "0.
|
|
23
|
+
__version__ = "0.2.0"
|
|
17
24
|
|
|
18
25
|
__all__ = [
|
|
19
26
|
"GRIDFLEET_API_URL",
|
|
20
27
|
"GRID_URL",
|
|
21
28
|
"GridFleetClient",
|
|
22
29
|
"HeartbeatThread",
|
|
30
|
+
"NoClaimableDevicesError",
|
|
23
31
|
"__version__",
|
|
24
32
|
"build_appium_options",
|
|
25
33
|
"create_appium_driver",
|
|
@@ -8,6 +8,7 @@ import logging
|
|
|
8
8
|
import os
|
|
9
9
|
import signal
|
|
10
10
|
import threading
|
|
11
|
+
import time
|
|
11
12
|
from typing import Any, cast
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
@@ -29,6 +30,43 @@ def _default_auth() -> httpx.BasicAuth | None:
|
|
|
29
30
|
return httpx.BasicAuth(username, password)
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
class NoClaimableDevicesError(RuntimeError):
|
|
34
|
+
"""Raised when the manager reports that no run devices are claimable yet."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
message: str,
|
|
39
|
+
*,
|
|
40
|
+
retry_after_sec: int,
|
|
41
|
+
next_available_at: str | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.retry_after_sec = retry_after_sec
|
|
44
|
+
self.next_available_at = next_available_at
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _raise_for_status(resp: Any) -> None:
|
|
49
|
+
if resp.status_code == 409:
|
|
50
|
+
try:
|
|
51
|
+
payload = resp.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
payload = None
|
|
54
|
+
error = payload.get("error") if isinstance(payload, dict) else None
|
|
55
|
+
if isinstance(error, dict):
|
|
56
|
+
details = error.get("details")
|
|
57
|
+
if isinstance(details, dict) and details.get("error") == "no_claimable_devices":
|
|
58
|
+
retry_after = details.get("retry_after_sec")
|
|
59
|
+
if not isinstance(retry_after, int):
|
|
60
|
+
retry_after = 5
|
|
61
|
+
next_available_at = details.get("next_available_at")
|
|
62
|
+
raise NoClaimableDevicesError(
|
|
63
|
+
str(error.get("message") or "No unclaimed devices available in this run"),
|
|
64
|
+
retry_after_sec=retry_after,
|
|
65
|
+
next_available_at=next_available_at if isinstance(next_available_at, str) else None,
|
|
66
|
+
)
|
|
67
|
+
resp.raise_for_status()
|
|
68
|
+
|
|
69
|
+
|
|
32
70
|
class HeartbeatThread(threading.Thread):
|
|
33
71
|
"""Background thread that sends periodic heartbeat pings for an active test run."""
|
|
34
72
|
|
|
@@ -173,7 +211,7 @@ class GridFleetClient:
|
|
|
173
211
|
timeout=10,
|
|
174
212
|
auth=self._auth,
|
|
175
213
|
)
|
|
176
|
-
resp
|
|
214
|
+
_raise_for_status(resp)
|
|
177
215
|
return cast("dict[str, Any]", resp.json())
|
|
178
216
|
|
|
179
217
|
def release_device(self, run_id: str, *, device_id: str, worker_id: str) -> None:
|
|
@@ -185,6 +223,41 @@ class GridFleetClient:
|
|
|
185
223
|
)
|
|
186
224
|
resp.raise_for_status()
|
|
187
225
|
|
|
226
|
+
def release_device_with_cooldown(
|
|
227
|
+
self,
|
|
228
|
+
run_id: str,
|
|
229
|
+
*,
|
|
230
|
+
device_id: str,
|
|
231
|
+
worker_id: str,
|
|
232
|
+
reason: str,
|
|
233
|
+
ttl_seconds: int,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
resp = httpx.post(
|
|
236
|
+
f"{self.base_url}/runs/{run_id}/devices/{device_id}/release-with-cooldown",
|
|
237
|
+
json={"worker_id": worker_id, "reason": reason, "ttl_seconds": ttl_seconds},
|
|
238
|
+
timeout=10,
|
|
239
|
+
auth=self._auth,
|
|
240
|
+
)
|
|
241
|
+
resp.raise_for_status()
|
|
242
|
+
return cast("dict[str, Any]", resp.json())
|
|
243
|
+
|
|
244
|
+
def claim_device_with_retry(
|
|
245
|
+
self,
|
|
246
|
+
run_id: str,
|
|
247
|
+
*,
|
|
248
|
+
worker_id: str,
|
|
249
|
+
max_wait_sec: int = 300,
|
|
250
|
+
) -> dict[str, Any]:
|
|
251
|
+
deadline = time.monotonic() + max_wait_sec
|
|
252
|
+
while True:
|
|
253
|
+
try:
|
|
254
|
+
return self.claim_device(run_id, worker_id=worker_id)
|
|
255
|
+
except NoClaimableDevicesError as exc:
|
|
256
|
+
remaining = deadline - time.monotonic()
|
|
257
|
+
if remaining <= 0:
|
|
258
|
+
raise
|
|
259
|
+
time.sleep(min(exc.retry_after_sec, max(1, int(remaining))))
|
|
260
|
+
|
|
188
261
|
def report_preparation_failure(
|
|
189
262
|
self,
|
|
190
263
|
run_id: str,
|
|
@@ -9,6 +9,7 @@ import pytest
|
|
|
9
9
|
from gridfleet_testkit.client import (
|
|
10
10
|
GridFleetClient,
|
|
11
11
|
HeartbeatThread,
|
|
12
|
+
NoClaimableDevicesError,
|
|
12
13
|
_default_auth,
|
|
13
14
|
register_run_cleanup,
|
|
14
15
|
)
|
|
@@ -256,6 +257,84 @@ def test_claim_device_calls_api(monkeypatch):
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
|
|
260
|
+
def test_claim_device_raises_no_claimable_devices_with_retry_metadata(monkeypatch):
|
|
261
|
+
def fake_post(
|
|
262
|
+
url: str,
|
|
263
|
+
*,
|
|
264
|
+
json: dict[str, Any],
|
|
265
|
+
timeout: int,
|
|
266
|
+
auth: Any = None,
|
|
267
|
+
) -> DummyResponse:
|
|
268
|
+
return DummyResponse(
|
|
269
|
+
{
|
|
270
|
+
"error": {
|
|
271
|
+
"code": "CONFLICT",
|
|
272
|
+
"message": "No unclaimed devices available in this run",
|
|
273
|
+
"request_id": "req-1",
|
|
274
|
+
"details": {
|
|
275
|
+
"error": "no_claimable_devices",
|
|
276
|
+
"retry_after_sec": 7,
|
|
277
|
+
"next_available_at": "2026-05-03T20:00:00Z",
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
status_code=409,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
285
|
+
|
|
286
|
+
client = GridFleetClient("http://manager/api")
|
|
287
|
+
with pytest.raises(NoClaimableDevicesError) as exc_info:
|
|
288
|
+
client.claim_device("run-123", worker_id="gw0")
|
|
289
|
+
|
|
290
|
+
assert exc_info.value.retry_after_sec == 7
|
|
291
|
+
assert exc_info.value.next_available_at == "2026-05-03T20:00:00Z"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def test_claim_device_with_retry_sleeps_and_retries(monkeypatch):
|
|
295
|
+
calls: list[str] = []
|
|
296
|
+
sleeps: list[int] = []
|
|
297
|
+
responses = iter(
|
|
298
|
+
[
|
|
299
|
+
DummyResponse(
|
|
300
|
+
{
|
|
301
|
+
"error": {
|
|
302
|
+
"code": "CONFLICT",
|
|
303
|
+
"message": "No unclaimed devices available in this run",
|
|
304
|
+
"request_id": "req-1",
|
|
305
|
+
"details": {"error": "no_claimable_devices", "retry_after_sec": 2, "next_available_at": None},
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
status_code=409,
|
|
309
|
+
),
|
|
310
|
+
DummyResponse({"device_id": "dev-1", "claimed_by": "gw0", "claimed_at": "2026-05-03T20:00:00Z"}),
|
|
311
|
+
]
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def fake_post(
|
|
315
|
+
url: str,
|
|
316
|
+
*,
|
|
317
|
+
json: dict[str, Any],
|
|
318
|
+
timeout: int,
|
|
319
|
+
auth: Any = None,
|
|
320
|
+
) -> DummyResponse:
|
|
321
|
+
calls.append(url)
|
|
322
|
+
return next(responses)
|
|
323
|
+
|
|
324
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
325
|
+
monkeypatch.setattr("gridfleet_testkit.client.time.sleep", lambda seconds: sleeps.append(seconds))
|
|
326
|
+
|
|
327
|
+
client = GridFleetClient("http://manager/api")
|
|
328
|
+
result = client.claim_device_with_retry("run-123", worker_id="gw0", max_wait_sec=5)
|
|
329
|
+
|
|
330
|
+
assert result["device_id"] == "dev-1"
|
|
331
|
+
assert sleeps == [2]
|
|
332
|
+
assert calls == [
|
|
333
|
+
"http://manager/api/runs/run-123/claim",
|
|
334
|
+
"http://manager/api/runs/run-123/claim",
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
|
|
259
338
|
def test_release_device_calls_api(monkeypatch):
|
|
260
339
|
recorded: dict[str, Any] = {}
|
|
261
340
|
|
|
@@ -283,6 +362,40 @@ def test_release_device_calls_api(monkeypatch):
|
|
|
283
362
|
}
|
|
284
363
|
|
|
285
364
|
|
|
365
|
+
def test_release_device_with_cooldown_calls_api(monkeypatch):
|
|
366
|
+
recorded: dict[str, Any] = {}
|
|
367
|
+
|
|
368
|
+
def fake_post(
|
|
369
|
+
url: str,
|
|
370
|
+
*,
|
|
371
|
+
json: dict[str, Any],
|
|
372
|
+
timeout: int,
|
|
373
|
+
auth: Any = None,
|
|
374
|
+
) -> DummyResponse:
|
|
375
|
+
recorded["url"] = url
|
|
376
|
+
recorded["json"] = json
|
|
377
|
+
recorded["timeout"] = timeout
|
|
378
|
+
return DummyResponse({"status": "cooldown_set", "excluded_until": "2026-05-03T20:00:00Z"})
|
|
379
|
+
|
|
380
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
381
|
+
|
|
382
|
+
client = GridFleetClient("http://manager/api")
|
|
383
|
+
result = client.release_device_with_cooldown(
|
|
384
|
+
"run-123",
|
|
385
|
+
device_id="dev-1",
|
|
386
|
+
worker_id="gw0",
|
|
387
|
+
reason="appium launch timeout",
|
|
388
|
+
ttl_seconds=60,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert result["status"] == "cooldown_set"
|
|
392
|
+
assert recorded == {
|
|
393
|
+
"url": "http://manager/api/runs/run-123/devices/dev-1/release-with-cooldown",
|
|
394
|
+
"json": {"worker_id": "gw0", "reason": "appium launch timeout", "ttl_seconds": 60},
|
|
395
|
+
"timeout": 10,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
286
399
|
def test_release_device_raises_for_conflict(monkeypatch):
|
|
287
400
|
def fake_post(
|
|
288
401
|
url: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_android_mobile_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_ios_simulator_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.1.0 → gridfleet_testkit-0.2.0}/examples/test_roku_sideload_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|