gridfleet-testkit 0.8.0__tar.gz → 0.9.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.
Files changed (35) hide show
  1. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/CHANGELOG.md +14 -0
  2. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/PKG-INFO +34 -1
  3. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/README.md +33 -0
  4. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/__init__.py +1 -1
  5. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/allocation.py +7 -0
  6. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/client.py +10 -0
  7. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/pyproject.toml +1 -1
  8. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_allocation.py +11 -0
  9. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_client.py +21 -0
  10. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_pytest_plugin.py +27 -0
  11. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/uv.lock +1 -1
  12. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/.gitignore +0 -0
  13. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/__init__.py +0 -0
  14. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/_example_helpers.py +0 -0
  15. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/assets/hello-world.zip +0 -0
  16. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_android_mobile_screenshot.py +0 -0
  17. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_android_tv_screenshot.py +0 -0
  18. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_firetv_screenshot.py +0 -0
  19. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_ios_simulator_screenshot.py +0 -0
  20. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_roku_screenshot.py +0 -0
  21. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_roku_sideload_screenshot.py +0 -0
  22. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/examples/test_tvos_screenshot.py +0 -0
  23. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/appium.py +0 -0
  24. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/py.typed +0 -0
  25. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/pytest_plugin.py +0 -0
  26. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/gridfleet_testkit/sessions.py +0 -0
  27. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_appium.py +0 -0
  28. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_client_test_data.py +0 -0
  29. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_docs_contract.py +0 -0
  30. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_driver_agnostic_guard.py +0 -0
  31. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_package_metadata.py +0 -0
  32. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_pytest_plugin_grid_run_id_injection.py +0 -0
  33. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_pytest_plugin_test_data.py +0 -0
  34. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_sessions.py +0 -0
  35. {gridfleet_testkit-0.8.0 → gridfleet_testkit-0.9.0}/tests/test_sessions_resolve_device_handle.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to the GridFleet testkit (`gridfleet-testkit` on PyPI) are documented here.
4
4
 
5
+ ## [0.9.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.8.0...gridfleet-testkit-v0.9.0) (2026-05-12)
6
+
7
+
8
+ ### Features
9
+
10
+ * **testkit:** add run detail client helper ([c80a8cc](https://github.com/quidow/gridfleet/commit/c80a8cc95093c2f46a7e714c96ff0b33018af5ba))
11
+ * **testkit:** expose allocation device tags ([0bf5e2e](https://github.com/quidow/gridfleet/commit/0bf5e2e04b806fadd0afcd3c95073828d0c2414e))
12
+ * **testkit:** support tag-based device targeting ([db0d0e3](https://github.com/quidow/gridfleet/commit/db0d0e3d3d1231828bb22a707d3bdcab6c0ec717))
13
+
14
+
15
+ ### Documentation
16
+
17
+ * **testkit:** document tag-based device targeting ([096841b](https://github.com/quidow/gridfleet/commit/096841b737dec71524d0edfa4c538d9cc69e7c2c))
18
+
5
19
  ## [0.8.0](https://github.com/quidow/gridfleet/compare/gridfleet-testkit-v0.7.0...gridfleet-testkit-v0.8.0) (2026-05-12)
6
20
 
7
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gridfleet-testkit
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -192,6 +192,7 @@ finally:
192
192
  | `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
193
193
  | `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
194
194
  | `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
195
+ | `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
195
196
  | `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
196
197
  | `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
197
198
  | `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
@@ -216,6 +217,38 @@ finally:
216
217
  | `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
217
218
  | `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 |
218
219
 
220
+ ### Targeting Devices by Tag
221
+
222
+ GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
223
+
224
+ ```python
225
+ @pytest.mark.parametrize(
226
+ "appium_driver",
227
+ [
228
+ {
229
+ "pack_id": "appium-uiautomator2",
230
+ "platform_id": "android_mobile",
231
+ "appium:gridfleet:tag:screen_type": "4k",
232
+ }
233
+ ],
234
+ indirect=True,
235
+ )
236
+ def test_4k_display(appium_driver):
237
+ ...
238
+ ```
239
+
240
+ The same capability works for free sessions:
241
+
242
+ ```python
243
+ driver = create_appium_driver(
244
+ pack_id="appium-uiautomator2",
245
+ platform_id="android_mobile",
246
+ capabilities={"appium:gridfleet:tag:screen_type": "4k"},
247
+ )
248
+ ```
249
+
250
+ When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
251
+
219
252
  ### Worker Identity
220
253
 
221
254
  `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.
@@ -158,6 +158,7 @@ finally:
158
158
  | `GridFleetClient.get_device_by_connection_target(connection_target)` | Fetch one device detail row by runtime connection target |
159
159
  | `GridFleetClient.get_device_capabilities(device_id)` | Fetch current Appium capability metadata for a device |
160
160
  | `GridFleetClient.get_device_test_data(device_id)` | Fetch operator-attached free-form test_data for a device |
161
+ | `GridFleetClient.get_run(run_id)` | Fetch one run detail row by backend run id |
161
162
  | `GridFleetClient.replace_device_test_data(device_id, body)` | Replace test_data with the supplied object |
162
163
  | `GridFleetClient.merge_device_test_data(device_id, body)` | Deep-merge into device test_data |
163
164
  | `GridFleetClient.resolve_device_id_by_connection_target(connection_target)` | Resolve the backend device id for a runtime connection target |
@@ -182,6 +183,38 @@ finally:
182
183
  | `get_device_test_data_for_driver(driver, gridfleet_client=None)` | Fetch test_data for a live Appium driver |
183
184
  | `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 |
184
185
 
186
+ ### Targeting Devices by Tag
187
+
188
+ GridFleet injects device tags into Grid node stereotypes as `appium:gridfleet:tag:<key>` capabilities, so Selenium Grid can route sessions to devices matching specific tags.
189
+
190
+ ```python
191
+ @pytest.mark.parametrize(
192
+ "appium_driver",
193
+ [
194
+ {
195
+ "pack_id": "appium-uiautomator2",
196
+ "platform_id": "android_mobile",
197
+ "appium:gridfleet:tag:screen_type": "4k",
198
+ }
199
+ ],
200
+ indirect=True,
201
+ )
202
+ def test_4k_display(appium_driver):
203
+ ...
204
+ ```
205
+
206
+ The same capability works for free sessions:
207
+
208
+ ```python
209
+ driver = create_appium_driver(
210
+ pack_id="appium-uiautomator2",
211
+ platform_id="android_mobile",
212
+ capabilities={"appium:gridfleet:tag:screen_type": "4k"},
213
+ )
214
+ ```
215
+
216
+ When an operator edits device tags, GridFleet marks the device for re-verification. The next verification restarts the Appium node and re-registers it with the updated Grid stereotype.
217
+
185
218
  ### Worker Identity
186
219
 
187
220
  `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.
@@ -47,7 +47,7 @@ from .sessions import build_error_session_payload, resolve_device_handle_from_dr
47
47
  try:
48
48
  __version__ = version("gridfleet-testkit")
49
49
  except PackageNotFoundError:
50
- __version__ = "0.8.0"
50
+ __version__ = "0.9.0"
51
51
 
52
52
  __all__ = [
53
53
  "GRIDFLEET_API_URL",
@@ -39,6 +39,7 @@ class AllocatedDevice:
39
39
  live_capabilities: dict[str, Any] | None
40
40
  test_data: dict[str, Any] | None = None
41
41
  unavailable_includes: tuple[UnavailableInclude, ...] = ()
42
+ tags: dict[str, str] | None = None
42
43
 
43
44
  @property
44
45
  def is_real_device(self) -> bool:
@@ -153,6 +154,11 @@ def hydrate_allocated_device(
153
154
  test_data = client.get_device_test_data(device_id)
154
155
  else:
155
156
  test_data = None
157
+ inline_tags = payload.get("tags")
158
+ if isinstance(inline_tags, dict):
159
+ tags: dict[str, str] | None = inline_tags
160
+ else:
161
+ tags = None
156
162
 
157
163
  return AllocatedDevice(
158
164
  run_id=run_id,
@@ -173,6 +179,7 @@ def hydrate_allocated_device(
173
179
  live_capabilities=live_capabilities,
174
180
  test_data=test_data,
175
181
  unavailable_includes=unavailable_includes,
182
+ tags=tags,
176
183
  )
177
184
 
178
185
 
@@ -290,6 +290,16 @@ class GridFleetClient:
290
290
  resp.raise_for_status()
291
291
  return cast("dict[str, Any]", resp.json())
292
292
 
293
+ def get_run(self, run_id: str) -> dict[str, Any]:
294
+ """Fetch one run detail row by backend run id."""
295
+ resp = httpx.get(
296
+ f"{self.base_url}/runs/{run_id}",
297
+ timeout=10,
298
+ auth=self._auth,
299
+ )
300
+ resp.raise_for_status()
301
+ return cast("dict[str, Any]", resp.json())
302
+
293
303
  def replace_device_test_data(self, device_id: str, body: dict[str, Any]) -> dict[str, Any]:
294
304
  """Replace device test_data with the supplied object."""
295
305
  resp = httpx.put(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gridfleet-testkit"
7
- version = "0.8.0"
7
+ version = "0.9.0"
8
8
  description = "Supported pytest and run-orchestration helpers for GridFleet integrations"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -328,6 +328,17 @@ def test_hydrate_uses_inline_test_data() -> None:
328
328
  assert client.test_data_calls == []
329
329
 
330
330
 
331
+ def test_hydrate_allocated_device_populates_inline_tags() -> None:
332
+ client = FakeClient()
333
+ allocated = hydrate_allocated_device(
334
+ device_handle(tags={"screen_type": "4k"}),
335
+ run_id="run-1",
336
+ client=client,
337
+ fetch_config=False,
338
+ )
339
+ assert allocated.tags == {"screen_type": "4k"}
340
+
341
+
331
342
  def test_hydrate_defaults_test_data_to_none_when_absent() -> None:
332
343
  client = FakeClient()
333
344
  allocated = hydrate_allocated_device(
@@ -187,6 +187,27 @@ def test_get_device_fetches_device_detail_by_id(monkeypatch):
187
187
  assert calls == [("http://manager/api/devices/dev-1", 10)]
188
188
 
189
189
 
190
+ def test_get_run_fetches_run_endpoint(monkeypatch):
191
+ calls: list[tuple[str, int | None]] = []
192
+
193
+ def fake_get(
194
+ url: str,
195
+ *,
196
+ timeout: int | None = None,
197
+ auth: Any = None,
198
+ ) -> DummyResponse:
199
+ calls.append((url, timeout))
200
+ return DummyResponse({"id": "run-1", "name": "smoke"})
201
+
202
+ monkeypatch.setattr("gridfleet_testkit.client.httpx.get", fake_get)
203
+
204
+ client = GridFleetClient("http://manager/api")
205
+ run = client.get_run("run-1")
206
+
207
+ assert run == {"id": "run-1", "name": "smoke"}
208
+ assert calls == [("http://manager/api/runs/run-1", 10)]
209
+
210
+
190
211
  def test_get_driver_pack_catalog_fetches_catalog_endpoint(monkeypatch):
191
212
  calls: list[tuple[str, str, dict[str, Any] | None, int | None]] = []
192
213
 
@@ -188,6 +188,33 @@ def test_appium_driver_builds_capabilities_and_reports_status(monkeypatch, repor
188
188
  assert gridfleet_client.reported_statuses == [("sess-1", expected_status, True)]
189
189
 
190
190
 
191
+ def test_appium_driver_passes_tag_capabilities_through(monkeypatch):
192
+ created_drivers = []
193
+ install_fake_appium(monkeypatch, created_drivers)
194
+ RecordingClient.instances.clear()
195
+ gridfleet_client = RecordingClient()
196
+
197
+ request = FakeRequest(
198
+ {
199
+ "pack_id": "appium-uiautomator2",
200
+ "platform_id": "android_mobile",
201
+ "appium:gridfleet:tag:screen_type": "4k",
202
+ },
203
+ test_name="test_tag_cap",
204
+ )
205
+
206
+ fixture_fn = pytest_plugin.appium_driver.__wrapped__
207
+ generator = fixture_fn(request, gridfleet_client)
208
+ driver = next(generator)
209
+
210
+ assert created_drivers[0][1]["appium:gridfleet:tag:screen_type"] == "4k"
211
+ assert created_drivers[0][1]["gridfleet:testName"] == "test_tag_cap"
212
+
213
+ driver.quit()
214
+ with pytest.raises(StopIteration):
215
+ next(generator)
216
+
217
+
191
218
  def test_device_config_uses_runtime_connection_target():
192
219
  gridfleet_client = types.SimpleNamespace(get_device_config=lambda target: {"target": target})
193
220
  driver = types.SimpleNamespace(capabilities={"appium:udid": "SERIAL123"})
@@ -136,7 +136,7 @@ wheels = [
136
136
 
137
137
  [[package]]
138
138
  name = "gridfleet-testkit"
139
- version = "0.8.0"
139
+ version = "0.9.0"
140
140
  source = { editable = "." }
141
141
  dependencies = [
142
142
  { name = "httpx" },