gridfleet-testkit 0.5.0__tar.gz → 0.6.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 → gridfleet_testkit-0.6.0}/CHANGELOG.md +11 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/PKG-INFO +1 -1
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/__init__.py +1 -1
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/client.py +53 -2
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/pyproject.toml +1 -1
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_client.py +120 -14
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/uv.lock +1 -1
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/.gitignore +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/README.md +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/__init__.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/_example_helpers.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/assets/hello-world.zip +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_android_mobile_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_android_tv_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_firetv_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_ios_simulator_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_roku_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_roku_sideload_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_tvos_screenshot.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/allocation.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/appium.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/py.typed +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/pytest_plugin.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/gridfleet_testkit/sessions.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_allocation.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_appium.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_client_test_data.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_docs_contract.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_driver_agnostic_guard.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_package_metadata.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_pytest_plugin.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_pytest_plugin_test_data.py +0 -0
- {gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/tests/test_sessions.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.5.0...gridfleet-testkit-v0.6.0) (2026-05-10)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* **backend:** clients sending {drain: true|false} to /api/devices/ {id}/maintenance, /api/devices/bulk/enter-maintenance, or the group bulk equivalent must drop the field. The enter-maintenance behaviour is unchanged from drain=false (always stop the node).
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **backend:** device state model drift fixes (D1-D6) ([#144](https://github.com/quidow/gridfleet/issues/144)) ([09556fd](https://github.com/quidow/gridfleet/commit/09556fdac8ddb458f1655f9001f25240443062fb))
|
|
15
|
+
|
|
5
16
|
## [0.5.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.4.0...gridfleet-testkit-v0.5.0) (2026-05-08)
|
|
6
17
|
|
|
7
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gridfleet-testkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -600,6 +600,27 @@ class GridFleetClient:
|
|
|
600
600
|
return None
|
|
601
601
|
return cast("dict[str, Any]", resp.json())
|
|
602
602
|
|
|
603
|
+
def notify_session_finished(
|
|
604
|
+
self,
|
|
605
|
+
session_id: str,
|
|
606
|
+
*,
|
|
607
|
+
suppress_errors: bool = True,
|
|
608
|
+
) -> None:
|
|
609
|
+
"""Tell the manager the WebDriver session has ended.
|
|
610
|
+
|
|
611
|
+
Idempotent on the backend — repeated calls are a no-op once the
|
|
612
|
+
Session row is marked ended.
|
|
613
|
+
"""
|
|
614
|
+
try:
|
|
615
|
+
resp = httpx.post(
|
|
616
|
+
f"{self.base_url}/sessions/{session_id}/finished",
|
|
617
|
+
timeout=5,
|
|
618
|
+
auth=self._auth,
|
|
619
|
+
)
|
|
620
|
+
resp.raise_for_status()
|
|
621
|
+
except (httpx.HTTPError, TypeError, ValueError) as exc:
|
|
622
|
+
_raise_or_warn("notify session finished", suppress_errors, exc)
|
|
623
|
+
|
|
603
624
|
def update_session_status(
|
|
604
625
|
self,
|
|
605
626
|
session_id: str,
|
|
@@ -629,7 +650,12 @@ class GridFleetClient:
|
|
|
629
650
|
run_id: str | None = None,
|
|
630
651
|
suppress_errors: bool = True,
|
|
631
652
|
) -> dict[str, Any] | None:
|
|
632
|
-
"""Extract session metadata from an Appium driver and register it.
|
|
653
|
+
"""Extract session metadata from an Appium driver and register it.
|
|
654
|
+
|
|
655
|
+
Also wraps ``driver.quit`` so that the first call after a successful
|
|
656
|
+
registration fires :meth:`notify_session_finished` automatically.
|
|
657
|
+
Errors from notify are suppressed — they must never break the caller.
|
|
658
|
+
"""
|
|
633
659
|
capabilities = getattr(driver, "capabilities", {})
|
|
634
660
|
if not isinstance(capabilities, dict):
|
|
635
661
|
capabilities = {}
|
|
@@ -638,7 +664,7 @@ class GridFleetClient:
|
|
|
638
664
|
raise RuntimeError("Created Appium driver did not expose a session ID")
|
|
639
665
|
device_id = capabilities.get("appium:gridfleet:deviceId") or capabilities.get("gridfleet:deviceId")
|
|
640
666
|
connection_target = capabilities.get("appium:udid") or capabilities.get("appium:deviceName")
|
|
641
|
-
|
|
667
|
+
result = self.register_session(
|
|
642
668
|
session_id=session_id,
|
|
643
669
|
test_name=test_name,
|
|
644
670
|
device_id=device_id if isinstance(device_id, str) and device_id else None,
|
|
@@ -647,6 +673,31 @@ class GridFleetClient:
|
|
|
647
673
|
run_id=run_id,
|
|
648
674
|
suppress_errors=suppress_errors,
|
|
649
675
|
)
|
|
676
|
+
self._wrap_quit_for_notify(driver, session_id)
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
def _wrap_quit_for_notify(self, driver: Any, session_id: str) -> None:
|
|
680
|
+
"""Replace ``driver.quit`` with a wrapper that also notifies the manager.
|
|
681
|
+
|
|
682
|
+
The notify fires at most once per registration: after the first quit
|
|
683
|
+
succeeds, subsequent quit() calls run the underlying quit but do
|
|
684
|
+
NOT post to /finished again. The underlying quit still runs every
|
|
685
|
+
call.
|
|
686
|
+
|
|
687
|
+
Raises AttributeError if the driver lacks a quit method.
|
|
688
|
+
"""
|
|
689
|
+
original_quit = driver.quit
|
|
690
|
+
notified: dict[str, bool] = {"done": False}
|
|
691
|
+
|
|
692
|
+
def wrapped_quit(*args: Any, **kwargs: Any) -> Any:
|
|
693
|
+
try:
|
|
694
|
+
return original_quit(*args, **kwargs)
|
|
695
|
+
finally:
|
|
696
|
+
if not notified["done"]:
|
|
697
|
+
notified["done"] = True
|
|
698
|
+
self.notify_session_finished(session_id, suppress_errors=True)
|
|
699
|
+
|
|
700
|
+
driver.quit = wrapped_quit
|
|
650
701
|
|
|
651
702
|
def complete_run(self, run_id: str) -> dict[str, Any]:
|
|
652
703
|
resp = httpx.post(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import signal
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, ClassVar
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
import pytest
|
|
@@ -806,19 +806,19 @@ def test_register_session_from_driver_extracts_gridfleet_capabilities(monkeypatc
|
|
|
806
806
|
|
|
807
807
|
monkeypatch.setattr(GridFleetClient, "register_session", fake_register_session)
|
|
808
808
|
|
|
809
|
-
|
|
810
|
-
"
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
809
|
+
class FakeDriver:
|
|
810
|
+
session_id = "sess-1"
|
|
811
|
+
capabilities: ClassVar[dict[str, object]] = {
|
|
812
|
+
"appium:gridfleet:deviceId": "dev-1",
|
|
813
|
+
"appium:udid": "SERIAL123",
|
|
814
|
+
"appium:platform": "android_mobile",
|
|
815
|
+
"platformName": "Android",
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
def quit(self) -> None:
|
|
819
|
+
return None
|
|
820
|
+
|
|
821
|
+
driver = FakeDriver()
|
|
822
822
|
client = GridFleetClient("http://manager/api")
|
|
823
823
|
|
|
824
824
|
assert client.register_session_from_driver(driver, test_name="test_login", run_id="run-1") == {"ok": True}
|
|
@@ -1643,3 +1643,109 @@ def test_register_run_cleanup_idempotent_under_explicit_double_call(monkeypatch)
|
|
|
1643
1643
|
cleanup_fn() # second invocation must be a no-op
|
|
1644
1644
|
|
|
1645
1645
|
assert client.calls == ["complete:run-double"]
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
# --- Task 7: notify_session_finished and driver.quit wrap ---
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def test_notify_session_finished_calls_endpoint(monkeypatch):
|
|
1652
|
+
"""notify_session_finished posts to /api/sessions/{id}/finished."""
|
|
1653
|
+
recorded: dict[str, Any] = {}
|
|
1654
|
+
|
|
1655
|
+
def fake_post(
|
|
1656
|
+
url: str,
|
|
1657
|
+
*,
|
|
1658
|
+
timeout: int,
|
|
1659
|
+
auth: Any = None,
|
|
1660
|
+
) -> DummyResponse:
|
|
1661
|
+
recorded["url"] = url
|
|
1662
|
+
recorded["timeout"] = timeout
|
|
1663
|
+
return DummyResponse(None, status_code=204)
|
|
1664
|
+
|
|
1665
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
1666
|
+
|
|
1667
|
+
client = GridFleetClient("http://localhost:8000/api")
|
|
1668
|
+
client.notify_session_finished("abc-123", suppress_errors=False)
|
|
1669
|
+
|
|
1670
|
+
assert recorded["url"] == "http://localhost:8000/api/sessions/abc-123/finished"
|
|
1671
|
+
assert recorded["timeout"] == 5
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def test_register_session_from_driver_wraps_quit_to_notify(monkeypatch):
|
|
1675
|
+
"""After register_session_from_driver, driver.quit() also fires notify."""
|
|
1676
|
+
post_calls: list[str] = []
|
|
1677
|
+
quit_called = {"n": 0}
|
|
1678
|
+
|
|
1679
|
+
def fake_post(
|
|
1680
|
+
url: str,
|
|
1681
|
+
*,
|
|
1682
|
+
json: dict[str, Any] | None = None,
|
|
1683
|
+
timeout: int = 5,
|
|
1684
|
+
auth: Any = None,
|
|
1685
|
+
) -> DummyResponse:
|
|
1686
|
+
post_calls.append(url)
|
|
1687
|
+
if url.endswith("/sessions"):
|
|
1688
|
+
return DummyResponse({"id": "session-xyz"}, status_code=201)
|
|
1689
|
+
if url.endswith("/finished"):
|
|
1690
|
+
return DummyResponse(None, status_code=204)
|
|
1691
|
+
return DummyResponse({})
|
|
1692
|
+
|
|
1693
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
1694
|
+
|
|
1695
|
+
class FakeDriver:
|
|
1696
|
+
session_id = "session-xyz"
|
|
1697
|
+
capabilities: ClassVar[dict[str, object]] = {}
|
|
1698
|
+
|
|
1699
|
+
def quit(self) -> None:
|
|
1700
|
+
quit_called["n"] += 1
|
|
1701
|
+
|
|
1702
|
+
driver = FakeDriver()
|
|
1703
|
+
client = GridFleetClient("http://localhost:8000/api")
|
|
1704
|
+
client.register_session_from_driver(driver, test_name="t", suppress_errors=False)
|
|
1705
|
+
|
|
1706
|
+
driver.quit()
|
|
1707
|
+
assert quit_called["n"] == 1
|
|
1708
|
+
finished_calls = [u for u in post_calls if u.endswith("/finished")]
|
|
1709
|
+
assert len(finished_calls) == 1
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
def test_register_session_from_driver_quit_wrap_is_idempotent(monkeypatch):
|
|
1713
|
+
"""Calling driver.quit() twice does not fire two notify requests, but underlying quit runs both times."""
|
|
1714
|
+
post_calls: list[str] = []
|
|
1715
|
+
|
|
1716
|
+
def fake_post(
|
|
1717
|
+
url: str,
|
|
1718
|
+
*,
|
|
1719
|
+
json: dict[str, Any] | None = None,
|
|
1720
|
+
timeout: int = 5,
|
|
1721
|
+
auth: Any = None,
|
|
1722
|
+
) -> DummyResponse:
|
|
1723
|
+
post_calls.append(url)
|
|
1724
|
+
if url.endswith("/sessions"):
|
|
1725
|
+
return DummyResponse({"id": "session-xyz"}, status_code=201)
|
|
1726
|
+
if url.endswith("/finished"):
|
|
1727
|
+
return DummyResponse(None, status_code=204)
|
|
1728
|
+
return DummyResponse({})
|
|
1729
|
+
|
|
1730
|
+
monkeypatch.setattr("gridfleet_testkit.client.httpx.post", fake_post)
|
|
1731
|
+
|
|
1732
|
+
class FakeDriver:
|
|
1733
|
+
session_id = "session-xyz"
|
|
1734
|
+
capabilities: ClassVar[dict[str, object]] = {}
|
|
1735
|
+
quit_count = 0
|
|
1736
|
+
|
|
1737
|
+
def quit(self) -> None:
|
|
1738
|
+
type(self).quit_count += 1
|
|
1739
|
+
|
|
1740
|
+
driver = FakeDriver()
|
|
1741
|
+
client = GridFleetClient("http://localhost:8000/api")
|
|
1742
|
+
client.register_session_from_driver(driver, test_name="t", suppress_errors=False)
|
|
1743
|
+
|
|
1744
|
+
driver.quit()
|
|
1745
|
+
driver.quit() # second call: must NOT post /finished again, but MUST run quit
|
|
1746
|
+
|
|
1747
|
+
# /finished posted exactly once
|
|
1748
|
+
finished_calls = [u for u in post_calls if u.endswith("/finished")]
|
|
1749
|
+
assert len(finished_calls) == 1
|
|
1750
|
+
# underlying quit ran twice
|
|
1751
|
+
assert FakeDriver.quit_count == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_android_mobile_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.0}/examples/test_ios_simulator_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
{gridfleet_testkit-0.5.0 → gridfleet_testkit-0.6.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|