embeddedci 2.7.0__tar.gz → 2.7.1__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 (115) hide show
  1. {embeddedci-2.7.0 → embeddedci-2.7.1}/CHANGELOG.md +7 -0
  2. {embeddedci-2.7.0 → embeddedci-2.7.1}/PKG-INFO +1 -1
  3. {embeddedci-2.7.0 → embeddedci-2.7.1}/pyproject.toml +1 -1
  4. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/cloud.py +16 -8
  5. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/tcp.py +13 -9
  6. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_cloud.py +62 -3
  7. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_transport_failures.py +4 -4
  8. {embeddedci-2.7.0 → embeddedci-2.7.1}/.gitignore +0 -0
  9. {embeddedci-2.7.0 → embeddedci-2.7.1}/LICENSE +0 -0
  10. {embeddedci-2.7.0 → embeddedci-2.7.1}/README.md +0 -0
  11. {embeddedci-2.7.0 → embeddedci-2.7.1}/docs/event-uart-design.md +0 -0
  12. {embeddedci-2.7.0 → embeddedci-2.7.1}/examples/README.md +0 -0
  13. {embeddedci-2.7.0 → embeddedci-2.7.1}/examples/test_bmp280.py +0 -0
  14. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/__init__.py +0 -0
  15. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/__init__.py +0 -0
  16. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/can.py +0 -0
  17. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/capabilities.py +0 -0
  18. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/capture.py +0 -0
  19. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/ci.py +0 -0
  20. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/client.py +0 -0
  21. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/cloud_auth.py +0 -0
  22. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/connection.py +0 -0
  23. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/constants.py +0 -0
  24. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/control_loop.py +0 -0
  25. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/decode.py +0 -0
  26. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/discovery.py +0 -0
  27. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/dsp.py +0 -0
  28. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/errors.py +0 -0
  29. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/flash.py +0 -0
  30. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/gpio.py +0 -0
  31. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/i2c.py +0 -0
  32. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/lease.py +0 -0
  33. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/lowlevel.py +0 -0
  34. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/motor_emulator.py +0 -0
  35. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/power.py +0 -0
  36. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/protocol.py +0 -0
  37. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/pytest_plugin.py +0 -0
  38. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/replay.py +0 -0
  39. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/results.py +0 -0
  40. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/sensor.py +0 -0
  41. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/server_api.py +0 -0
  42. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/spi.py +0 -0
  43. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/state.py +0 -0
  44. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/__init__.py +0 -0
  45. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/base.py +0 -0
  46. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/serial.py +0 -0
  47. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/uart.py +0 -0
  48. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/waveforms.py +0 -0
  49. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/wiring.py +0 -0
  50. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/py.typed +0 -0
  51. {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/upload_build.py +0 -0
  52. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/api_surface.json +0 -0
  53. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/conftest.py +0 -0
  54. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/README.md +0 -0
  55. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/conftest.py +0 -0
  56. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/e2e_helpers.py +0 -0
  57. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_cloud.py +0 -0
  58. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_dut.py +0 -0
  59. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_gpio.py +0 -0
  60. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_la13_14.py +0 -0
  61. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_pod.py +0 -0
  62. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_rev3.py +0 -0
  63. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_spi.py +0 -0
  64. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_usb.py +0 -0
  65. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_bmp280_hil.py +0 -0
  66. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_can_ecu_demo.py +0 -0
  67. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_can_loopback_hil.py +0 -0
  68. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_smoke.py +0 -0
  69. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_adc_fft_hw.py +0 -0
  70. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_api_surface.py +0 -0
  71. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_can.py +0 -0
  72. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capabilities.py +0 -0
  73. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capture.py +0 -0
  74. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capture_replay_hw.py +0 -0
  75. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_ci.py +0 -0
  76. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_client_v2.py +0 -0
  77. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_connection.py +0 -0
  78. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_control_loop.py +0 -0
  79. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_control_loop_hw.py +0 -0
  80. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_current_out_waveforms.py +0 -0
  81. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dac_limits_fixture.py +0 -0
  82. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dac_path_ranges.py +0 -0
  83. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_decode.py +0 -0
  84. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_decode_parity.py +0 -0
  85. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dsp.py +0 -0
  86. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_flash_bridge.py +0 -0
  87. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_gpio.py +0 -0
  88. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_i2c_decode.py +0 -0
  89. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_i2c_parity.py +0 -0
  90. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_image_switching.py +0 -0
  91. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_integration.py +0 -0
  92. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_la_voltage_fixture.py +0 -0
  93. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_lease.py +0 -0
  94. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_motor_emulator.py +0 -0
  95. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_pins_bias.py +0 -0
  96. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_protocol.py +0 -0
  97. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_refusal_errors.py +0 -0
  98. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_replay.py +0 -0
  99. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_sensor.py +0 -0
  100. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_autodetect.py +0 -0
  101. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_json.py +0 -0
  102. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_text_console.py +0 -0
  103. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_spi.py +0 -0
  104. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_tcp_transport.py +0 -0
  105. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_timed_power.py +0 -0
  106. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_timing.py +0 -0
  107. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_trigger_power.py +0 -0
  108. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_uart_capture.py +0 -0
  109. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_uart_session.py +0 -0
  110. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_upload_build.py +0 -0
  111. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_waveforms.py +0 -0
  112. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_wiring.py +0 -0
  113. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/testdata/i2c_decode_vectors.json +0 -0
  114. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/testdata/spi_decode_vectors.json +0 -0
  115. {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/testdata/uart_decode_vectors.json +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog — `embeddedci`
2
2
 
3
+ ## 2.7.1
4
+
5
+ - Over the cloud, a UART console (`open_uart`) can stay open while a capture, power profile or
6
+ DAC replay runs, as on the LAN. Each streaming tunnel tells the server what it is for
7
+ (`?kind=uart|capture|dac|flash`). A server without kind support still answers HTTP 409
8
+ ("device is busy") for the second tunnel, as before.
9
+
3
10
  ## 2.7.0
4
11
 
5
12
  - `UartSession.write` on a link that is gone (a reset connection, an unplugged pod, a closed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: embeddedci
3
- Version: 2.7.0
3
+ Version: 2.7.1
4
4
  Summary: BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing
5
5
  Project-URL: Homepage, https://embeddedci.com
6
6
  Project-URL: Repository, https://github.com/embeddedci-com/embeddedci-python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "embeddedci"
7
- version = "2.7.0"
7
+ version = "2.7.1"
8
8
  description = "BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -343,9 +343,13 @@ class CloudTransport(TcpTransport):
343
343
  self._expires_at = None
344
344
  return True
345
345
 
346
- def _ws_url(self) -> str:
346
+ def _ws_url(self, kind: Optional[str] = None) -> str:
347
347
  """The tunnel URL. It carries no credentials: those go in :meth:`_ws_headers`, so the
348
- token never reaches an access log or an error message."""
348
+ token never reaches an access log or an error message.
349
+
350
+ ``kind`` (``uart``/``capture``/``dac``/``flash``) tells the server what the tunnel is
351
+ for, so a UART console and a capture or DAC replay may be open together, as on the LAN.
352
+ Without it the server treats the tunnel as exclusive."""
349
353
  base = self.api_base
350
354
  if base.startswith("https://"):
351
355
  ws_base = "wss://" + base[len("https://"):]
@@ -353,7 +357,10 @@ class CloudTransport(TcpTransport):
353
357
  ws_base = "ws://" + base[len("http://"):]
354
358
  else:
355
359
  ws_base = base
356
- return f"{ws_base}/api/cloud/devices/ws?device={quote(self.device_name, safe='')}"
360
+ url = f"{ws_base}/api/cloud/devices/ws?device={quote(self.device_name, safe='')}"
361
+ if kind:
362
+ url += f"&kind={quote(kind, safe='')}"
363
+ return url
357
364
 
358
365
  def _ws_headers(self, token: str) -> "list[str]":
359
366
  """Headers for the tunnel's WebSocket upgrade: the session token as a Bearer header
@@ -364,18 +371,19 @@ class CloudTransport(TcpTransport):
364
371
  headers.append(f"X-Benchpod-Lease: {self.lease_id}")
365
372
  return headers
366
373
 
367
- def _open_tunnel(self) -> _WsTunnelSocket:
374
+ def _open_tunnel(self, kind: Optional[str] = None) -> _WsTunnelSocket:
368
375
  token = self._session_token()
369
- return _WsTunnelSocket(self._ws_url(), self.timeout, headers=self._ws_headers(token),
376
+ return _WsTunnelSocket(self._ws_url(kind), self.timeout, headers=self._ws_headers(token),
370
377
  secrets=(token,))
371
378
 
372
- def _dial(self, timeout: Optional[float] = None) -> _WsTunnelSocket: # type: ignore[override]
379
+ def _dial(self, timeout: Optional[float] = None, # type: ignore[override]
380
+ kind: Optional[str] = None) -> _WsTunnelSocket:
373
381
  try:
374
- sock = self._open_tunnel()
382
+ sock = self._open_tunnel(kind)
375
383
  except TransportError as exc:
376
384
  status = getattr(exc, "status", None)
377
385
  if status == 401 and self._invalidate_token():
378
- sock = self._open_tunnel()
386
+ sock = self._open_tunnel(kind)
379
387
  else:
380
388
  body = getattr(exc, "body", None) or b""
381
389
  if isinstance(body, bytes):
@@ -107,8 +107,12 @@ class TcpTransport(Transport):
107
107
  except ValueError:
108
108
  raise TransportError(f"invalid port in address {self.addr!r}") from None
109
109
 
110
- def _dial(self, timeout: Optional[float] = None) -> socket.socket:
111
- """Connect, with ``timeout`` (default: the transport's) on every later read."""
110
+ def _dial(self, timeout: Optional[float] = None, kind: Optional[str] = None) -> socket.socket:
111
+ """Connect, with ``timeout`` (default: the transport's) on every later read.
112
+
113
+ ``kind`` names what the connection is for (``"uart"``, ``"capture"``, ``"dac"``,
114
+ ``"flash"``). The cloud transport passes it to the server so a UART console and a capture
115
+ can share the pod as they do here; a LAN socket needs no such hint and ignores it."""
112
116
  host, port = self._split_addr()
113
117
  deadline = time.monotonic() + self.dial_timeout
114
118
  last: Optional[Exception] = None
@@ -194,7 +198,7 @@ class TcpTransport(Transport):
194
198
  def samples(self, req: dict) -> List[int]:
195
199
  """Send a command whose reply is a chunked sample array."""
196
200
  timeout = request_timeout(req, self.timeout)
197
- sock = self._dial(timeout)
201
+ sock = self._dial(timeout, kind="capture")
198
202
  buf = bytearray()
199
203
  out: List[int] = []
200
204
  try:
@@ -225,7 +229,7 @@ class TcpTransport(Transport):
225
229
  ``la_upto``) of a ``capture_dual`` — can see them. Raises on an error chunk.
226
230
  """
227
231
  timeout = request_timeout(req, self.timeout)
228
- sock = self._dial(timeout)
232
+ sock = self._dial(timeout, kind="capture")
229
233
  buf = bytearray()
230
234
  cmd = req.get("cmd")
231
235
  try:
@@ -264,7 +268,7 @@ class TcpTransport(Transport):
264
268
  v18). Returns the ``replay`` reply data.
265
269
  """
266
270
  timeout = max(self.timeout, _UPLOAD_TIMEOUT)
267
- sock = self._dial(timeout)
271
+ sock = self._dial(timeout, kind="dac")
268
272
  buf = bytearray()
269
273
  try:
270
274
  begin = {"cmd": "load_bin", "total": len(data)}
@@ -326,7 +330,7 @@ class TcpTransport(Transport):
326
330
  req["delay_ms"] = int(delay_ms)
327
331
  self.command(req)
328
332
 
329
- def _raw_handshake(self, req: dict, cmd: str) -> RawLink:
333
+ def _raw_handshake(self, req: dict, cmd: str, kind: str) -> RawLink:
330
334
  """Send a mode-switch command and hand back the raw byte link.
331
335
 
332
336
  Shared by ``dap_start`` and ``uart_proxy_start``: both ack one JSON line
@@ -337,7 +341,7 @@ class TcpTransport(Transport):
337
341
  hanging; only after it does the timeout come off.
338
342
  """
339
343
  timeout = request_timeout(req, self.timeout)
340
- sock = self._dial(timeout)
344
+ sock = self._dial(timeout, kind=kind)
341
345
  try:
342
346
  with _io_errors(cmd, timeout):
343
347
  sock.sendall(encode_request(req))
@@ -364,12 +368,12 @@ class TcpTransport(Transport):
364
368
  req["packet_count"] = packet_count
365
369
  if wait_ms:
366
370
  req["wait_ms"] = wait_ms
367
- return self._raw_handshake(req, "dap_start")
371
+ return self._raw_handshake(req, "dap_start", "flash")
368
372
 
369
373
  def uart_proxy_start(self, rx: int, tx: int, baud: int) -> RawLink:
370
374
  return self._raw_handshake(
371
375
  {"cmd": "uart_proxy_start", "rx": rx, "tx": tx, "baud": baud},
372
- "uart_proxy_start",
376
+ "uart_proxy_start", "uart",
373
377
  )
374
378
 
375
379
  def close(self) -> None:
@@ -39,6 +39,65 @@ def test_cloud_ws_url():
39
39
  t = CloudTransport("dev-a", api_base="https://example.test", token="abc")
40
40
  url = t._ws_url()
41
41
  assert url == "wss://example.test/api/cloud/devices/ws?device=dev-a"
42
+ assert t._ws_url("uart") == "wss://example.test/api/cloud/devices/ws?device=dev-a&kind=uart"
43
+
44
+
45
+ def test_tunnel_url_carries_the_kind_hint(monkeypatch):
46
+ calls = _fake_websocket_module(monkeypatch)
47
+ t = CloudTransport("dev-a", api_base="https://example.test", token="tok")
48
+ t._dial(kind="capture")
49
+ t._dial()
50
+ assert calls[0][0].endswith("?device=dev-a&kind=capture")
51
+ assert calls[1][0].endswith("?device=dev-a")
52
+
53
+
54
+ def _recording_dial(monkeypatch, t, sock):
55
+ kinds = []
56
+
57
+ def dial(timeout=None, kind=None):
58
+ kinds.append(kind)
59
+ return sock
60
+
61
+ monkeypatch.setattr(t, "_dial", dial)
62
+ return kinds
63
+
64
+
65
+ @pytest.mark.parametrize("call, reply, kind", [
66
+ (lambda t: t.uart_proxy_start(rx=1, tx=2, baud=115200), b'{"status":"ok"}\n', "uart"),
67
+ (lambda t: t.dap_start(swclk=11, swdio=12), b'{"status":"ok"}\n', "flash"),
68
+ (lambda t: t.samples({"cmd": "capture", "samples": 4}),
69
+ b'{"status":"ok","data":[1,2],"more":false}\n', "capture"),
70
+ (lambda t: list(t.stream_chunks({"cmd": "power_profile"})),
71
+ b'{"status":"ok","data":{},"more":false}\n', "capture"),
72
+ ])
73
+ def test_streaming_sessions_name_their_tunnel_kind(monkeypatch, call, reply, kind):
74
+ """Each streaming session tells the server what it is for, so a UART console and a capture
75
+ or power profile can share the pod over the cloud as they do on the LAN."""
76
+ t = CloudTransport("dev-a", api_base="https://example.test", token="x")
77
+ kinds = _recording_dial(monkeypatch, t, _FakeSock(reply))
78
+ call(t)
79
+ assert kinds == [kind]
80
+
81
+
82
+ def test_load_replay_names_the_dac_kind_and_staging_stays_exclusive(monkeypatch):
83
+ sock, _ = _sending_socket(monkeypatch)
84
+ sock._ws = _FakeWS([
85
+ b'{"status":"ok","data":{"ready":4}}\n',
86
+ b'{"status":"ok","data":{"total":4}}\n',
87
+ b'{"status":"ok","data":{}}\n',
88
+ ])
89
+ t = CloudTransport("dev-a", api_base="https://example.test", token="x")
90
+ kinds = _recording_dial(monkeypatch, t, sock)
91
+ t.load_replay(data=b"\x00" * 4, replay={"cmd": "replay"})
92
+ assert kinds == ["dac"]
93
+
94
+ sock._ws = _FakeWS([
95
+ b'{"status":"ok","data":{"ready":4}}\n',
96
+ b'{"status":"ok","data":{"total":4}}\n',
97
+ ])
98
+ t.stage_psram(b"\x00" * 4)
99
+ # PSRAM staging for an SPI session has no kind: the server keeps it exclusive.
100
+ assert kinds == ["dac", None]
42
101
 
43
102
 
44
103
  def _fake_websocket_module(monkeypatch, fail=None):
@@ -209,7 +268,7 @@ def test_cloud_transport_dap_handshake_returns_raw_link(monkeypatch):
209
268
  # dap_start acks one JSON line, then the same tunnel carries raw bytes.
210
269
  t = CloudTransport("dev-a", api_base="https://example.test", token="x")
211
270
  fake = _FakeSock(b'{"status":"ok"}\nRAWBYTES')
212
- monkeypatch.setattr(t, "_dial", lambda *_a: fake)
271
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: fake)
213
272
 
214
273
  link = t.dap_start(swclk=11, swdio=12)
215
274
  # Bytes after the ack newline are the raw CMSIS-DAP stream and must survive.
@@ -313,7 +372,7 @@ def test_cloud_load_replay_skips_the_tunnel_ack_lines(monkeypatch):
313
372
  b'{"status":"ok","data":{"replaying":true}}\n',
314
373
  ])
315
374
  t = CloudTransport("dev-a", api_base="https://example.test", token="abc")
316
- monkeypatch.setattr(t, "_dial", lambda *_a: sock)
375
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: sock)
317
376
  data = t.load_replay(data=b"\x00" * 20000, replay={"cmd": "replay"}, psram=True)
318
377
  assert data == {"replaying": True}
319
378
  frames = sock._ws.frames
@@ -613,7 +672,7 @@ def test_cloud_tunnel_command_times_out_as_transport_timeout(monkeypatch):
613
672
 
614
673
  t = CloudTransport("dev-a", api_base="https://example.test", token="x", timeout=5)
615
674
  sock = _tunnel_socket(_RaisingWS(websocket.WebSocketTimeoutException("timed out")))
616
- monkeypatch.setattr(t, "_dial", lambda *_a: sock)
675
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: sock)
617
676
  with pytest.raises(TransportTimeout, match="within 5 s"):
618
677
  list(t.stream_chunks({"cmd": "capture", "samples": 16}))
619
678
 
@@ -345,7 +345,7 @@ def test_a_tunnel_closing_mid_upload_is_connection_closed_through_load_replay(mo
345
345
  ws.fail_with = websocket.WebSocketConnectionClosedException("gone")
346
346
  sock, _ = _tunnel(monkeypatch, ws, clock)
347
347
  t = CloudTransport("dev-a", api_base="https://example.test", token="x")
348
- monkeypatch.setattr(t, "_dial", lambda *_a: sock)
348
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: sock)
349
349
  with pytest.raises(ConnectionClosedError, match="closed while sending"):
350
350
  t.load_replay(data=b"\x00" * 20000, replay={"cmd": "replay"})
351
351
  assert len(ws.sent) == 5 # the load_bin line and four data frames, then nothing
@@ -360,7 +360,7 @@ def test_a_tunnel_that_stops_taking_data_is_a_transport_timeout(monkeypatch):
360
360
  ws.fail_with = websocket.WebSocketTimeoutException("send timed out")
361
361
  sock, _ = _tunnel(monkeypatch, ws, clock)
362
362
  t = CloudTransport("dev-a", api_base="https://example.test", token="x", timeout=5)
363
- monkeypatch.setattr(t, "_dial", lambda *_a: sock)
363
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: sock)
364
364
  with pytest.raises(TransportTimeout):
365
365
  t.stage_psram(b"\x01" * 5000)
366
366
 
@@ -376,7 +376,7 @@ def test_a_large_download_frame_carrying_many_lines_is_split_correctly(monkeypat
376
376
  clock = [0.0]
377
377
  sock, _ = _tunnel(monkeypatch, _WS([blob[:cut], blob[cut:]]), clock)
378
378
  t = CloudTransport("dev-a", api_base="https://example.test", token="x")
379
- monkeypatch.setattr(t, "_dial", lambda *_a: sock)
379
+ monkeypatch.setattr(t, "_dial", lambda *_a, **_k: sock)
380
380
  assert t.samples({"cmd": "capture", "samples": 20000}) == list(range(20000))
381
381
 
382
382
 
@@ -451,7 +451,7 @@ def test_measure_power_over_the_cloud_tunnel_waits_for_its_duration(monkeypatch)
451
451
  t = CloudTransport("dev-a", api_base="https://example.test", token="x", timeout=30)
452
452
  ws = _WS([b'{"status":"ok","data":{"stats":{"n":1}},"more":false}\n'])
453
453
  sock, _ = _tunnel(monkeypatch, ws, [0.0])
454
- monkeypatch.setattr(t, "_open_tunnel", lambda: sock)
454
+ monkeypatch.setattr(t, "_open_tunnel", lambda *_a: sock)
455
455
  list(t.stream_chunks({"cmd": "power_profile", "efuse": 1, "duration_ms": 600_000}))
456
456
  assert ws.timeout == pytest.approx(630)
457
457
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes