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.
- {embeddedci-2.7.0 → embeddedci-2.7.1}/CHANGELOG.md +7 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/PKG-INFO +1 -1
- {embeddedci-2.7.0 → embeddedci-2.7.1}/pyproject.toml +1 -1
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/cloud.py +16 -8
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/tcp.py +13 -9
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_cloud.py +62 -3
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_transport_failures.py +4 -4
- {embeddedci-2.7.0 → embeddedci-2.7.1}/.gitignore +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/LICENSE +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/README.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/docs/event-uart-design.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/examples/README.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/examples/test_bmp280.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/__init__.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/__init__.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/can.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/capabilities.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/ci.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/client.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/cloud_auth.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/connection.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/constants.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/control_loop.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/discovery.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/dsp.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/errors.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/flash.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/i2c.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/lease.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/lowlevel.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/motor_emulator.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/protocol.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/pytest_plugin.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/replay.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/results.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/sensor.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/server_api.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/state.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/__init__.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/base.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/transport/serial.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/uart.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/benchpod/wiring.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/py.typed +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/src/embeddedci/upload_build.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/api_surface.json +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/conftest.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/README.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/conftest.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/e2e_helpers.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_cloud.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_dut.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_la13_14.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_pod.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_rev3.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/e2e/test_e2e_usb.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_bmp280_hil.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_can_ecu_demo.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_can_loopback_hil.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/examples/test_smoke.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_adc_fft_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_api_surface.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_can.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capabilities.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_capture_replay_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_ci.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_client_v2.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_connection.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_control_loop.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_control_loop_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_current_out_waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dac_limits_fixture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dac_path_ranges.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_decode_parity.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_dsp.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_flash_bridge.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_i2c_decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_i2c_parity.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_image_switching.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_integration.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_la_voltage_fixture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_lease.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_motor_emulator.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_pins_bias.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_protocol.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_refusal_errors.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_replay.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_sensor.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_autodetect.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_json.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_serial_text_console.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_tcp_transport.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_timed_power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_timing.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_trigger_power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_uart_capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_uart_session.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_upload_build.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/test_wiring.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/testdata/i2c_decode_vectors.json +0 -0
- {embeddedci-2.7.0 → embeddedci-2.7.1}/tests/testdata/spi_decode_vectors.json +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|