Skip to content

Commit 70f4e74

Browse files
authored
Merge pull request #421 from lucasssvaz/feat/wokwi_jtag
feat(wokwi): Add support for using the USB Serial JTAG
2 parents de4da94 + 8d584b3 commit 70f4e74

5 files changed

Lines changed: 190 additions & 0 deletions

File tree

pytest-embedded-wokwi/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ To run your tests with Wokwi, make sure to specify the `wokwi` service when runn
3030
pytest --embedded-services idf,wokwi
3131
```
3232

33+
#### USB Serial JTAG
34+
35+
By default, Wokwi diagrams use UART connections (`$serialMonitor:TX`/`$serialMonitor:RX`) for serial communication. Some targets (e.g. ESP32-P4) can use USB Serial JTAG instead. You can enable this with the `--wokwi-usb-serial-jtag` flag:
36+
37+
```
38+
pytest --embedded-services idf,wokwi --wokwi-usb-serial-jtag true
39+
```
40+
41+
This works for both auto-generated diagrams and diagrams loaded from disk (including those specified via `--wokwi-diagram`). When enabled, the flag will:
42+
43+
- Set the `serialInterface` attribute to `USB_SERIAL_JTAG` on the board part
44+
- Remove any `$serialMonitor` connections from the diagram
45+
3346
#### Writing Tests
3447

3548
When writing tests for your firmware, you can use the same pytest fixtures and assertions as you would for local testing. The main difference is that your tests will be executed in the Wokwi simulation environment and you have access to the Wokwi API for controlling the simulation through the `wokwi` fixture.

pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import sys
5+
import tempfile
56
import typing as t
67
from pathlib import Path
78

@@ -47,6 +48,7 @@ def __init__(
4748
msg_queue: MessageQueue,
4849
firmware_resolver: IDFFirmwareResolver,
4950
wokwi_diagram: str | None = None,
51+
wokwi_usb_serial_jtag: bool | None = None,
5052
app: t.Optional['IdfApp'] = None,
5153
meta: Meta | None = None,
5254
**kwargs,
@@ -55,6 +57,7 @@ def __init__(
5557
super().__init__(msg_queue=msg_queue, meta=meta, **kwargs)
5658

5759
self.app = app
60+
self._usb_serial_jtag = wokwi_usb_serial_jtag
5861

5962
# Get Wokwi API token
6063
token = os.getenv('WOKWI_CLI_TOKEN')
@@ -69,6 +72,9 @@ def __init__(
6972
self.create_diagram_json()
7073
wokwi_diagram = os.path.join(self.app.app_path, 'diagram.json')
7174

75+
if self._usb_serial_jtag:
76+
wokwi_diagram = self._apply_serial_interface_override(wokwi_diagram)
77+
7278
# Connect and start simulation
7379
try:
7480
firmware_path = Path(firmware_resolver.resolve_firmware(app)).as_posix()
@@ -319,6 +325,43 @@ def create_diagram_json(self):
319325
with open(diagram_json_path, 'w') as f:
320326
json.dump(diagram, f, indent=2)
321327

328+
@staticmethod
329+
def _apply_serial_interface_override(diagram_path: str) -> str:
330+
"""Override the serial interface to USB Serial JTAG in a diagram file.
331+
332+
Reads the diagram JSON, sets the ``serialInterface`` attribute to
333+
``USB_SERIAL_JTAG`` on the main board part and removes any
334+
``$serialMonitor`` connections. The modified diagram is written to a
335+
temporary file so the original is never mutated.
336+
337+
Returns the path to the modified diagram file.
338+
"""
339+
with open(diagram_path) as f:
340+
diagram = json.load(f)
341+
342+
for part in diagram.get('parts', []):
343+
if part.get('type', '').startswith('board-'):
344+
part.setdefault('attrs', {})
345+
part['attrs']['serialInterface'] = 'USB_SERIAL_JTAG'
346+
break
347+
348+
diagram['connections'] = [
349+
conn
350+
for conn in diagram.get('connections', [])
351+
if not any('$serialMonitor' in str(endpoint) for endpoint in conn)
352+
]
353+
354+
fd, tmp_path = tempfile.mkstemp(suffix='.json', prefix='wokwi_diagram_')
355+
try:
356+
with os.fdopen(fd, 'w') as f:
357+
json.dump(diagram, f, indent=2)
358+
except Exception:
359+
os.close(fd)
360+
raise
361+
362+
logging.info('Applied USB Serial JTAG interface override to diagram: %s', tmp_path)
363+
return tmp_path
364+
322365
def _hard_reset(self):
323366
"""Fake hard_reset to maintain API consistency."""
324367
raise NotImplementedError('Hard reset not supported in Wokwi simulation')

pytest-embedded-wokwi/tests/test_wokwi.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import json
12
import os
23

34
import pytest
5+
from pytest_embedded_wokwi.wokwi import Wokwi
46

57
wokwi_token_required = pytest.mark.skipif(
68
not os.getenv('WOKWI_CLI_TOKEN', None),
@@ -54,3 +56,115 @@ def test_pexpect_by_wokwi(dut):
5456
)
5557

5658
result.assert_outcomes(passed=1)
59+
60+
61+
class TestApplySerialInterfaceOverride:
62+
"""Unit tests for Wokwi._apply_serial_interface_override (no token needed)."""
63+
64+
def _write_diagram(self, tmp_path, diagram: dict) -> str:
65+
path = os.path.join(str(tmp_path), 'diagram.json')
66+
with open(path, 'w') as f:
67+
json.dump(diagram, f)
68+
return path
69+
70+
def test_adds_serial_interface_and_removes_serial_monitor(self, tmp_path):
71+
diagram = {
72+
'version': 1,
73+
'parts': [{'type': 'board-esp32-devkit-c-v4', 'id': 'esp'}],
74+
'connections': [
75+
['esp:TX', '$serialMonitor:RX', ''],
76+
['esp:RX', '$serialMonitor:TX', ''],
77+
],
78+
}
79+
src = self._write_diagram(tmp_path, diagram)
80+
result_path = Wokwi._apply_serial_interface_override(src)
81+
82+
try:
83+
with open(result_path) as f:
84+
result = json.load(f)
85+
86+
assert result['parts'][0]['attrs']['serialInterface'] == 'USB_SERIAL_JTAG'
87+
assert result['connections'] == []
88+
finally:
89+
os.unlink(result_path)
90+
91+
def test_preserves_non_serial_monitor_connections(self, tmp_path):
92+
diagram = {
93+
'version': 1,
94+
'parts': [{'type': 'board-esp32-s3-devkitc-1', 'id': 'esp32', 'attrs': {}}],
95+
'connections': [
96+
['esp32:RX', '$serialMonitor:TX', '', []],
97+
['esp32:TX', '$serialMonitor:RX', '', []],
98+
['btn1:1.l', 'esp32:14', 'blue', ['h-38.4', 'v105.78']],
99+
['esp32:4', 'led1:A', 'green', ['h0']],
100+
],
101+
}
102+
src = self._write_diagram(tmp_path, diagram)
103+
result_path = Wokwi._apply_serial_interface_override(src)
104+
105+
try:
106+
with open(result_path) as f:
107+
result = json.load(f)
108+
109+
assert len(result['connections']) == 2
110+
assert result['connections'][0] == ['btn1:1.l', 'esp32:14', 'blue', ['h-38.4', 'v105.78']]
111+
assert result['connections'][1] == ['esp32:4', 'led1:A', 'green', ['h0']]
112+
finally:
113+
os.unlink(result_path)
114+
115+
def test_does_not_mutate_original_file(self, tmp_path):
116+
diagram = {
117+
'version': 1,
118+
'parts': [{'type': 'board-esp32-devkit-c-v4', 'id': 'esp'}],
119+
'connections': [
120+
['esp:TX', '$serialMonitor:RX', ''],
121+
['esp:RX', '$serialMonitor:TX', ''],
122+
],
123+
}
124+
src = self._write_diagram(tmp_path, diagram)
125+
result_path = Wokwi._apply_serial_interface_override(src)
126+
127+
try:
128+
with open(src) as f:
129+
original = json.load(f)
130+
131+
assert 'serialInterface' not in original['parts'][0].get('attrs', {})
132+
assert len(original['connections']) == 2
133+
assert result_path != src
134+
finally:
135+
os.unlink(result_path)
136+
137+
def test_adds_attrs_when_missing(self, tmp_path):
138+
diagram = {
139+
'version': 1,
140+
'parts': [{'type': 'board-esp32-p4-function-ev', 'id': 'esp'}],
141+
'connections': [],
142+
}
143+
src = self._write_diagram(tmp_path, diagram)
144+
result_path = Wokwi._apply_serial_interface_override(src)
145+
146+
try:
147+
with open(result_path) as f:
148+
result = json.load(f)
149+
150+
assert result['parts'][0]['attrs'] == {'serialInterface': 'USB_SERIAL_JTAG'}
151+
assert result['connections'] == []
152+
finally:
153+
os.unlink(result_path)
154+
155+
def test_overrides_existing_serial_interface(self, tmp_path):
156+
diagram = {
157+
'version': 1,
158+
'parts': [{'type': 'board-esp32-devkit-c-v4', 'id': 'esp', 'attrs': {'serialInterface': 'UART'}}],
159+
'connections': [],
160+
}
161+
src = self._write_diagram(tmp_path, diagram)
162+
result_path = Wokwi._apply_serial_interface_override(src)
163+
164+
try:
165+
with open(result_path) as f:
166+
result = json.load(f)
167+
168+
assert result['parts'][0]['attrs']['serialInterface'] == 'USB_SERIAL_JTAG'
169+
finally:
170+
os.unlink(result_path)

pytest-embedded/pytest_embedded/dut_factory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ def _fixture_classes_and_options_fn(
157157
qemu_extra_args,
158158
qemu_efuse_path,
159159
wokwi_diagram,
160+
wokwi_usb_serial_jtag,
160161
skip_regenerate_image,
161162
encrypt,
162163
keyfile,
@@ -335,6 +336,7 @@ def _fixture_classes_and_options_fn(
335336
kwargs[fixture].update(
336337
{
337338
'wokwi_diagram': wokwi_diagram,
339+
'wokwi_usb_serial_jtag': wokwi_usb_serial_jtag,
338340
'msg_queue': msg_queue,
339341
'app': None,
340342
'meta': _meta,
@@ -696,6 +698,7 @@ def create(
696698
qemu_extra_args: str | None = None,
697699
qemu_efuse_path: str | None = None,
698700
wokwi_diagram: str | None = None,
701+
wokwi_usb_serial_jtag: bool | None = None,
699702
skip_regenerate_image: bool | None = None,
700703
encrypt: bool | None = None,
701704
keyfile: str | None = None,
@@ -743,6 +746,7 @@ def create(
743746
qemu_extra_args: Additional QEMU arguments.
744747
qemu_efuse_path: Efuse binary path.
745748
wokwi_diagram: Wokwi diagram path.
749+
wokwi_usb_serial_jtag: Use USB Serial JTAG instead of UART for Wokwi serial communication.
746750
skip_regenerate_image: Skip image regeneration flag.
747751
encrypt: Encryption flag.
748752
keyfile: Keyfile for encryption.
@@ -813,6 +817,7 @@ def create(
813817
'qemu_extra_args': qemu_extra_args,
814818
'qemu_efuse_path': qemu_efuse_path,
815819
'wokwi_diagram': wokwi_diagram,
820+
'wokwi_usb_serial_jtag': wokwi_usb_serial_jtag,
816821
'skip_regenerate_image': skip_regenerate_image,
817822
'encrypt': encrypt,
818823
'keyfile': keyfile,

pytest-embedded/pytest_embedded/plugin.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ def pytest_addoption(parser):
298298
'--wokwi-diagram',
299299
help='Path to the wokwi diagram file (Default: None)',
300300
)
301+
wokwi_group.addoption(
302+
'--wokwi-usb-serial-jtag',
303+
help='y/yes/true for True and n/no/false for False. '
304+
'Use USB Serial JTAG instead of UART for serial communication in the Wokwi diagram. '
305+
'When enabled, the diagram will use the USB_SERIAL_JTAG interface and remove $serialMonitor connections. '
306+
'(Default: False)',
307+
)
301308

302309

303310
###########
@@ -1076,6 +1083,13 @@ def wokwi_diagram(request: FixtureRequest) -> str | None:
10761083
return _request_param_or_config_option_or_default(request, 'wokwi_diagram', None)
10771084

10781085

1086+
@pytest.fixture
1087+
@multi_dut_argument
1088+
def wokwi_usb_serial_jtag(request: FixtureRequest) -> bool | None:
1089+
"""Enable parametrization for the same cli option"""
1090+
return _request_param_or_config_option_or_default(request, 'wokwi_usb_serial_jtag', None)
1091+
1092+
10791093
####################
10801094
# Private Fixtures #
10811095
####################
@@ -1134,6 +1148,7 @@ def parametrize_fixtures(
11341148
qemu_extra_args,
11351149
qemu_efuse_path,
11361150
wokwi_diagram,
1151+
wokwi_usb_serial_jtag,
11371152
skip_regenerate_image,
11381153
encrypt,
11391154
keyfile,

0 commit comments

Comments
 (0)