pytest-embedded 2.7.0__tar.gz → 2.8.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.
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/PKG-INFO +1 -1
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/__init__.py +1 -1
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/dut_factory.py +34 -6
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/plugin.py +58 -1
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/tests/test_base.py +189 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/LICENSE +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/README.md +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pyproject.toml +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/app.py +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/dut.py +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/log.py +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/unity.py +0 -0
- {pytest_embedded-2.7.0 → pytest_embedded-2.8.1}/pytest_embedded/utils.py +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import datetime
|
|
2
3
|
import gc
|
|
3
4
|
import io
|
|
@@ -42,8 +43,19 @@ DUT_GLOBAL_INDEX = 0
|
|
|
42
43
|
PARAMETRIZED_FIXTURES_CACHE = {}
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
_STDOUT_LOCK = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_stdout_lock(lock) -> None:
|
|
50
|
+
global _STDOUT_LOCK
|
|
51
|
+
_STDOUT_LOCK = lock
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _listen(
|
|
55
|
+
q: MessageQueue, filepath: str, with_timestamp: bool = True, count: int = 1, total: int = 1, _stdout_lock=None
|
|
56
|
+
) -> None:
|
|
46
57
|
shall_add_prefix = True
|
|
58
|
+
_pending = ''
|
|
47
59
|
while True:
|
|
48
60
|
msg = q.get()
|
|
49
61
|
if not msg:
|
|
@@ -71,20 +83,25 @@ def _listen(q: MessageQueue, filepath: str, with_timestamp: bool = True, count:
|
|
|
71
83
|
if _s.endswith('\n'): # complete line
|
|
72
84
|
shall_add_prefix = True
|
|
73
85
|
_s = _s[:-1].replace('\n', '\n' + prefix) + '\n'
|
|
86
|
+
with _stdout_lock if _stdout_lock else contextlib.nullcontext():
|
|
87
|
+
_stdout.write(_pending + _s)
|
|
88
|
+
_stdout.flush()
|
|
89
|
+
_pending = ''
|
|
74
90
|
else:
|
|
75
91
|
shall_add_prefix = False
|
|
76
92
|
_s = _s.replace('\n', '\n' + prefix)
|
|
77
|
-
|
|
78
|
-
_stdout.write(_s)
|
|
79
|
-
_stdout.flush()
|
|
93
|
+
_pending += _s
|
|
80
94
|
|
|
81
95
|
|
|
82
|
-
def _listener_gn(
|
|
96
|
+
def _listener_gn(
|
|
97
|
+
msg_queue, _pexpect_logfile, with_timestamp, dut_index, dut_total, _stdout_lock=None
|
|
98
|
+
) -> multiprocessing.Process:
|
|
83
99
|
os.makedirs(os.path.dirname(_pexpect_logfile), exist_ok=True)
|
|
84
100
|
kwargs = {
|
|
85
101
|
'with_timestamp': with_timestamp,
|
|
86
102
|
'count': dut_index,
|
|
87
103
|
'total': dut_total,
|
|
104
|
+
'_stdout_lock': _stdout_lock,
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
return _ctx.Process(
|
|
@@ -124,6 +141,7 @@ def _fixture_classes_and_options_fn(
|
|
|
124
141
|
erase_all,
|
|
125
142
|
esptool_baud,
|
|
126
143
|
esp_flash_force,
|
|
144
|
+
no_fast_flash,
|
|
127
145
|
part_tool,
|
|
128
146
|
confirm_target_elf_sha256,
|
|
129
147
|
erase_nvs,
|
|
@@ -140,6 +158,7 @@ def _fixture_classes_and_options_fn(
|
|
|
140
158
|
qemu_extra_args,
|
|
141
159
|
qemu_efuse_path,
|
|
142
160
|
wokwi_diagram,
|
|
161
|
+
wokwi_usb_serial_jtag,
|
|
143
162
|
skip_regenerate_image,
|
|
144
163
|
encrypt,
|
|
145
164
|
keyfile,
|
|
@@ -236,6 +255,7 @@ def _fixture_classes_and_options_fn(
|
|
|
236
255
|
kwargs[fixture].update(
|
|
237
256
|
{
|
|
238
257
|
'app': None,
|
|
258
|
+
'fast_flash': not no_fast_flash if no_fast_flash is not None else True,
|
|
239
259
|
}
|
|
240
260
|
)
|
|
241
261
|
elif 'nuttx' in _services:
|
|
@@ -318,6 +338,7 @@ def _fixture_classes_and_options_fn(
|
|
|
318
338
|
kwargs[fixture].update(
|
|
319
339
|
{
|
|
320
340
|
'wokwi_diagram': wokwi_diagram,
|
|
341
|
+
'wokwi_usb_serial_jtag': wokwi_usb_serial_jtag,
|
|
321
342
|
'msg_queue': msg_queue,
|
|
322
343
|
'app': None,
|
|
323
344
|
'meta': _meta,
|
|
@@ -663,6 +684,7 @@ class DutFactory:
|
|
|
663
684
|
erase_all: bool | None = None,
|
|
664
685
|
esptool_baud: int | None = None,
|
|
665
686
|
esp_flash_force: bool | None = False,
|
|
687
|
+
no_fast_flash: bool | None = None,
|
|
666
688
|
part_tool: str | None = None,
|
|
667
689
|
confirm_target_elf_sha256: bool | None = None,
|
|
668
690
|
erase_nvs: bool | None = None,
|
|
@@ -679,6 +701,7 @@ class DutFactory:
|
|
|
679
701
|
qemu_extra_args: str | None = None,
|
|
680
702
|
qemu_efuse_path: str | None = None,
|
|
681
703
|
wokwi_diagram: str | None = None,
|
|
704
|
+
wokwi_usb_serial_jtag: bool | None = None,
|
|
682
705
|
skip_regenerate_image: bool | None = None,
|
|
683
706
|
encrypt: bool | None = None,
|
|
684
707
|
keyfile: str | None = None,
|
|
@@ -726,6 +749,7 @@ class DutFactory:
|
|
|
726
749
|
qemu_extra_args: Additional QEMU arguments.
|
|
727
750
|
qemu_efuse_path: Efuse binary path.
|
|
728
751
|
wokwi_diagram: Wokwi diagram path.
|
|
752
|
+
wokwi_usb_serial_jtag: Use USB Serial JTAG instead of UART for Wokwi serial communication.
|
|
729
753
|
skip_regenerate_image: Skip image regeneration flag.
|
|
730
754
|
encrypt: Encryption flag.
|
|
731
755
|
keyfile: Keyfile for encryption.
|
|
@@ -753,7 +777,9 @@ class DutFactory:
|
|
|
753
777
|
)
|
|
754
778
|
logging.debug('You can get your custom DUT log file at the following path: %s.', _pexpect_logfile)
|
|
755
779
|
|
|
756
|
-
_listener = _listener_gn(
|
|
780
|
+
_listener = _listener_gn(
|
|
781
|
+
msg_queue, _pexpect_logfile, True, DUT_GLOBAL_INDEX, DUT_GLOBAL_INDEX + 1, _stdout_lock=_STDOUT_LOCK
|
|
782
|
+
)
|
|
757
783
|
layout.append(_listener)
|
|
758
784
|
|
|
759
785
|
_pexpect_fr = _pexpect_fr_gn(_pexpect_logfile, _listener)
|
|
@@ -778,6 +804,7 @@ class DutFactory:
|
|
|
778
804
|
'erase_all': erase_all,
|
|
779
805
|
'esptool_baud': esptool_baud,
|
|
780
806
|
'esp_flash_force': esp_flash_force,
|
|
807
|
+
'no_fast_flash': no_fast_flash,
|
|
781
808
|
'part_tool': part_tool,
|
|
782
809
|
'confirm_target_elf_sha256': confirm_target_elf_sha256,
|
|
783
810
|
'erase_nvs': erase_nvs,
|
|
@@ -794,6 +821,7 @@ class DutFactory:
|
|
|
794
821
|
'qemu_extra_args': qemu_extra_args,
|
|
795
822
|
'qemu_efuse_path': qemu_efuse_path,
|
|
796
823
|
'wokwi_diagram': wokwi_diagram,
|
|
824
|
+
'wokwi_usb_serial_jtag': wokwi_usb_serial_jtag,
|
|
797
825
|
'skip_regenerate_image': skip_regenerate_image,
|
|
798
826
|
'encrypt': encrypt,
|
|
799
827
|
'keyfile': keyfile,
|
|
@@ -30,6 +30,7 @@ from .app import App
|
|
|
30
30
|
from .dut import Dut
|
|
31
31
|
from .dut_factory import (
|
|
32
32
|
DutFactory,
|
|
33
|
+
_ctx,
|
|
33
34
|
_fixture_classes_and_options_fn,
|
|
34
35
|
_listener_gn,
|
|
35
36
|
_pexpect_fr_gn,
|
|
@@ -41,6 +42,7 @@ from .dut_factory import (
|
|
|
41
42
|
qemu_gn,
|
|
42
43
|
serial_gn,
|
|
43
44
|
set_parametrized_fixtures_cache,
|
|
45
|
+
set_stdout_lock,
|
|
44
46
|
wokwi_gn,
|
|
45
47
|
)
|
|
46
48
|
from .log import MessageQueue, MessageQueueManager, PexpectProcess
|
|
@@ -207,6 +209,17 @@ def pytest_addoption(parser):
|
|
|
207
209
|
action='store_true',
|
|
208
210
|
help='force mode for esptool',
|
|
209
211
|
)
|
|
212
|
+
arduino_group = parser.getgroup('embedded-arduino')
|
|
213
|
+
arduino_group.addoption(
|
|
214
|
+
'--no-fast-flash',
|
|
215
|
+
help=(
|
|
216
|
+
'y/yes/true for True and n/no/false for False. '
|
|
217
|
+
'Set to True to disable fast reflashing (--diff-with) for Arduino. '
|
|
218
|
+
'Useful when flash state is unknown, e.g. after OTA updates. '
|
|
219
|
+
'(Default: False)'
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
|
|
210
223
|
idf_group = parser.getgroup('embedded-idf')
|
|
211
224
|
idf_group.addoption(
|
|
212
225
|
'--supported-targets', help='Comma-separated list of supported targets for the test case. (Default: None)'
|
|
@@ -296,6 +309,13 @@ def pytest_addoption(parser):
|
|
|
296
309
|
'--wokwi-diagram',
|
|
297
310
|
help='Path to the wokwi diagram file (Default: None)',
|
|
298
311
|
)
|
|
312
|
+
wokwi_group.addoption(
|
|
313
|
+
'--wokwi-usb-serial-jtag',
|
|
314
|
+
help='y/yes/true for True and n/no/false for False. '
|
|
315
|
+
'Use USB Serial JTAG instead of UART for serial communication in the Wokwi diagram. '
|
|
316
|
+
'When enabled, the diagram will use the USB_SERIAL_JTAG interface and remove $serialMonitor connections. '
|
|
317
|
+
'(Default: False)',
|
|
318
|
+
)
|
|
299
319
|
|
|
300
320
|
|
|
301
321
|
###########
|
|
@@ -695,6 +715,23 @@ def _mp_manager():
|
|
|
695
715
|
manager.shutdown()
|
|
696
716
|
|
|
697
717
|
|
|
718
|
+
@pytest.fixture(scope='session', autouse=True)
|
|
719
|
+
def _stdout_lock():
|
|
720
|
+
"""
|
|
721
|
+
A session-scoped multiprocessing lock used to serialize stdout writes across
|
|
722
|
+
all DUT listener processes, preventing garbled output when multiple DUTs
|
|
723
|
+
print to stdout simultaneously.
|
|
724
|
+
|
|
725
|
+
It is marked ``autouse=True`` so that the lock is created and registered
|
|
726
|
+
globally (via ``set_stdout_lock``) before any DUT fixture is instantiated,
|
|
727
|
+
ensuring every listener process receives a valid lock reference regardless
|
|
728
|
+
of test ordering.
|
|
729
|
+
"""
|
|
730
|
+
lock = _ctx.Lock()
|
|
731
|
+
set_stdout_lock(lock)
|
|
732
|
+
yield lock
|
|
733
|
+
|
|
734
|
+
|
|
698
735
|
@pytest.fixture
|
|
699
736
|
def test_case_tempdir(test_case_name: str, session_tempdir: str) -> str:
|
|
700
737
|
"""Function scoped temp dir for pytest-embedded"""
|
|
@@ -746,13 +783,17 @@ def with_timestamp(request: FixtureRequest) -> bool:
|
|
|
746
783
|
|
|
747
784
|
@pytest.fixture
|
|
748
785
|
@multi_dut_generator_fixture
|
|
749
|
-
def _listener(
|
|
786
|
+
def _listener(
|
|
787
|
+
msg_queue, _pexpect_logfile, with_timestamp, dut_index, dut_total, _stdout_lock
|
|
788
|
+
) -> multiprocessing.Process:
|
|
750
789
|
"""
|
|
751
790
|
The listener would create a `_listen` process. The `_listen` process would get the string from the message queue,
|
|
752
791
|
and do two things together:
|
|
753
792
|
|
|
754
793
|
1. print the string to `sys.stdout`
|
|
755
794
|
2. write the string to `_pexpect_logfile`
|
|
795
|
+
|
|
796
|
+
A shared lock (_stdout_lock) is used to prevent interleaved output when multiple DUTs print simultaneously.
|
|
756
797
|
"""
|
|
757
798
|
return _listener_gn(**locals())
|
|
758
799
|
|
|
@@ -819,6 +860,13 @@ def esp_flash_force(request: FixtureRequest) -> str | None:
|
|
|
819
860
|
return _request_param_or_config_option_or_default(request, 'esp_flash_force', False)
|
|
820
861
|
|
|
821
862
|
|
|
863
|
+
@pytest.fixture
|
|
864
|
+
@multi_dut_argument
|
|
865
|
+
def no_fast_flash(request: FixtureRequest) -> bool | None:
|
|
866
|
+
"""Enable parametrization for the same cli option"""
|
|
867
|
+
return _request_param_or_config_option_or_default(request, 'no_fast_flash', None)
|
|
868
|
+
|
|
869
|
+
|
|
822
870
|
@pytest.fixture
|
|
823
871
|
@multi_dut_argument
|
|
824
872
|
def build_dir(request: FixtureRequest) -> str | None:
|
|
@@ -1053,6 +1101,13 @@ def wokwi_diagram(request: FixtureRequest) -> str | None:
|
|
|
1053
1101
|
return _request_param_or_config_option_or_default(request, 'wokwi_diagram', None)
|
|
1054
1102
|
|
|
1055
1103
|
|
|
1104
|
+
@pytest.fixture
|
|
1105
|
+
@multi_dut_argument
|
|
1106
|
+
def wokwi_usb_serial_jtag(request: FixtureRequest) -> bool | None:
|
|
1107
|
+
"""Enable parametrization for the same cli option"""
|
|
1108
|
+
return _request_param_or_config_option_or_default(request, 'wokwi_usb_serial_jtag', None)
|
|
1109
|
+
|
|
1110
|
+
|
|
1056
1111
|
####################
|
|
1057
1112
|
# Private Fixtures #
|
|
1058
1113
|
####################
|
|
@@ -1095,6 +1150,7 @@ def parametrize_fixtures(
|
|
|
1095
1150
|
erase_all,
|
|
1096
1151
|
esptool_baud,
|
|
1097
1152
|
esp_flash_force,
|
|
1153
|
+
no_fast_flash,
|
|
1098
1154
|
part_tool,
|
|
1099
1155
|
confirm_target_elf_sha256,
|
|
1100
1156
|
erase_nvs,
|
|
@@ -1111,6 +1167,7 @@ def parametrize_fixtures(
|
|
|
1111
1167
|
qemu_extra_args,
|
|
1112
1168
|
qemu_efuse_path,
|
|
1113
1169
|
wokwi_diagram,
|
|
1170
|
+
wokwi_usb_serial_jtag,
|
|
1114
1171
|
skip_regenerate_image,
|
|
1115
1172
|
encrypt,
|
|
1116
1173
|
keyfile,
|
|
@@ -821,3 +821,192 @@ def test_log_metric_without_path(pytester):
|
|
|
821
821
|
|
|
822
822
|
result = pytester.runpytest()
|
|
823
823
|
result.assert_outcomes(passed=1)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
# ---------------------------------------------------------------------------
|
|
827
|
+
# Tests for the stdout-lock feature (_stdout_lock / set_stdout_lock / _listen)
|
|
828
|
+
# ---------------------------------------------------------------------------
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def test_set_stdout_lock():
|
|
832
|
+
"""set_stdout_lock updates the module-level _STDOUT_LOCK variable."""
|
|
833
|
+
import pytest_embedded.dut_factory as m
|
|
834
|
+
from pytest_embedded.dut_factory import set_stdout_lock
|
|
835
|
+
|
|
836
|
+
original = m._STDOUT_LOCK
|
|
837
|
+
try:
|
|
838
|
+
sentinel = object()
|
|
839
|
+
set_stdout_lock(sentinel)
|
|
840
|
+
assert m._STDOUT_LOCK is sentinel
|
|
841
|
+
|
|
842
|
+
set_stdout_lock(None)
|
|
843
|
+
assert m._STDOUT_LOCK is None
|
|
844
|
+
finally:
|
|
845
|
+
set_stdout_lock(original)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def test_listen_no_data_loss_without_lock(tmp_path):
|
|
849
|
+
"""_listen writes every queued message to the logfile when no lock is used."""
|
|
850
|
+
import time
|
|
851
|
+
|
|
852
|
+
from pytest_embedded.dut_factory import _ctx, _listen
|
|
853
|
+
from pytest_embedded.log import MessageQueue
|
|
854
|
+
|
|
855
|
+
logfile = str(tmp_path / 'test.log')
|
|
856
|
+
q = MessageQueue()
|
|
857
|
+
messages = [f'line_{i}\n'.encode() for i in range(20)]
|
|
858
|
+
|
|
859
|
+
p = _ctx.Process(target=_listen, args=(q, logfile), kwargs={'with_timestamp': False})
|
|
860
|
+
p.start()
|
|
861
|
+
try:
|
|
862
|
+
for msg in messages:
|
|
863
|
+
q.put(msg)
|
|
864
|
+
|
|
865
|
+
deadline = time.monotonic() + 10
|
|
866
|
+
while time.monotonic() < deadline:
|
|
867
|
+
try:
|
|
868
|
+
content = open(logfile, 'rb').read()
|
|
869
|
+
if all(msg in content for msg in messages):
|
|
870
|
+
break
|
|
871
|
+
except OSError:
|
|
872
|
+
pass
|
|
873
|
+
time.sleep(0.05)
|
|
874
|
+
finally:
|
|
875
|
+
p.terminate()
|
|
876
|
+
p.join(timeout=5)
|
|
877
|
+
assert p.exitcode is not None, 'listener process did not terminate'
|
|
878
|
+
|
|
879
|
+
content = open(logfile, 'rb').read()
|
|
880
|
+
for msg in messages:
|
|
881
|
+
assert msg in content, f'{msg!r} missing from logfile'
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def test_listen_no_data_loss_with_lock(tmp_path):
|
|
885
|
+
"""_listen writes every queued message to the logfile when a Manager lock is used."""
|
|
886
|
+
import multiprocessing
|
|
887
|
+
import time
|
|
888
|
+
|
|
889
|
+
from pytest_embedded.dut_factory import _ctx, _listen
|
|
890
|
+
from pytest_embedded.log import MessageQueue
|
|
891
|
+
|
|
892
|
+
logfile = str(tmp_path / 'test.log')
|
|
893
|
+
q = MessageQueue()
|
|
894
|
+
messages = [f'line_{i}\n'.encode() for i in range(20)]
|
|
895
|
+
|
|
896
|
+
manager = multiprocessing.Manager()
|
|
897
|
+
try:
|
|
898
|
+
lock = manager.Lock()
|
|
899
|
+
p = _ctx.Process(
|
|
900
|
+
target=_listen,
|
|
901
|
+
args=(q, logfile),
|
|
902
|
+
kwargs={'with_timestamp': False, '_stdout_lock': lock},
|
|
903
|
+
)
|
|
904
|
+
p.start()
|
|
905
|
+
try:
|
|
906
|
+
for msg in messages:
|
|
907
|
+
q.put(msg)
|
|
908
|
+
|
|
909
|
+
deadline = time.monotonic() + 10
|
|
910
|
+
while time.monotonic() < deadline:
|
|
911
|
+
try:
|
|
912
|
+
content = open(logfile, 'rb').read()
|
|
913
|
+
if all(msg in content for msg in messages):
|
|
914
|
+
break
|
|
915
|
+
except OSError:
|
|
916
|
+
pass
|
|
917
|
+
time.sleep(0.05)
|
|
918
|
+
finally:
|
|
919
|
+
p.terminate()
|
|
920
|
+
p.join(timeout=5)
|
|
921
|
+
assert p.exitcode is not None, 'listener process did not terminate'
|
|
922
|
+
finally:
|
|
923
|
+
manager.shutdown()
|
|
924
|
+
|
|
925
|
+
content = open(logfile, 'rb').read()
|
|
926
|
+
for msg in messages:
|
|
927
|
+
assert msg in content, f'{msg!r} missing from logfile'
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def test_stdout_lock_concurrent_no_data_loss(tmp_path):
|
|
931
|
+
"""Two concurrent _listen processes sharing a Manager lock both preserve all data."""
|
|
932
|
+
import multiprocessing
|
|
933
|
+
import time
|
|
934
|
+
|
|
935
|
+
from pytest_embedded.dut_factory import _ctx, _listen
|
|
936
|
+
from pytest_embedded.log import MessageQueue
|
|
937
|
+
|
|
938
|
+
logfile0 = str(tmp_path / 'dut0.log')
|
|
939
|
+
logfile1 = str(tmp_path / 'dut1.log')
|
|
940
|
+
q0 = MessageQueue()
|
|
941
|
+
q1 = MessageQueue()
|
|
942
|
+
messages0 = [f'dut0_line_{i}\n'.encode() for i in range(20)]
|
|
943
|
+
messages1 = [f'dut1_line_{i}\n'.encode() for i in range(20)]
|
|
944
|
+
|
|
945
|
+
manager = multiprocessing.Manager()
|
|
946
|
+
try:
|
|
947
|
+
lock = manager.Lock()
|
|
948
|
+
p0 = _ctx.Process(
|
|
949
|
+
target=_listen,
|
|
950
|
+
args=(q0, logfile0),
|
|
951
|
+
kwargs={'with_timestamp': False, 'count': 1, 'total': 2, '_stdout_lock': lock},
|
|
952
|
+
)
|
|
953
|
+
p1 = _ctx.Process(
|
|
954
|
+
target=_listen,
|
|
955
|
+
args=(q1, logfile1),
|
|
956
|
+
kwargs={'with_timestamp': False, 'count': 2, 'total': 2, '_stdout_lock': lock},
|
|
957
|
+
)
|
|
958
|
+
p0.start()
|
|
959
|
+
p1.start()
|
|
960
|
+
try:
|
|
961
|
+
# interleave writes from both DUTs to maximize lock contention
|
|
962
|
+
for msg0, msg1 in zip(messages0, messages1):
|
|
963
|
+
q0.put(msg0)
|
|
964
|
+
q1.put(msg1)
|
|
965
|
+
|
|
966
|
+
deadline = time.monotonic() + 15
|
|
967
|
+
while time.monotonic() < deadline:
|
|
968
|
+
try:
|
|
969
|
+
c0 = open(logfile0, 'rb').read()
|
|
970
|
+
c1 = open(logfile1, 'rb').read()
|
|
971
|
+
if all(m in c0 for m in messages0) and all(m in c1 for m in messages1):
|
|
972
|
+
break
|
|
973
|
+
except OSError:
|
|
974
|
+
pass
|
|
975
|
+
time.sleep(0.05)
|
|
976
|
+
finally:
|
|
977
|
+
p0.terminate()
|
|
978
|
+
p1.terminate()
|
|
979
|
+
p0.join(timeout=5)
|
|
980
|
+
p1.join(timeout=5)
|
|
981
|
+
assert p0.exitcode is not None, 'dut0 listener process did not terminate'
|
|
982
|
+
assert p1.exitcode is not None, 'dut1 listener process did not terminate'
|
|
983
|
+
finally:
|
|
984
|
+
manager.shutdown()
|
|
985
|
+
|
|
986
|
+
c0 = open(logfile0, 'rb').read()
|
|
987
|
+
c1 = open(logfile1, 'rb').read()
|
|
988
|
+
for msg in messages0:
|
|
989
|
+
assert msg in c0, f'{msg!r} missing from dut0 logfile'
|
|
990
|
+
for msg in messages1:
|
|
991
|
+
assert msg in c1, f'{msg!r} missing from dut1 logfile'
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def test_multi_dut_no_data_loss(testdir):
|
|
995
|
+
"""In a 2-DUT test, all messages written by each DUT can be expected - nothing is dropped."""
|
|
996
|
+
testdir.makepyfile(r"""
|
|
997
|
+
import pytest
|
|
998
|
+
|
|
999
|
+
@pytest.mark.parametrize('count', [2], indirect=True)
|
|
1000
|
+
def test_concurrent_dut_writes(dut):
|
|
1001
|
+
n = 15
|
|
1002
|
+
for i in range(n):
|
|
1003
|
+
dut[0].write(f'dut0_msg_{i}')
|
|
1004
|
+
dut[1].write(f'dut1_msg_{i}')
|
|
1005
|
+
|
|
1006
|
+
for i in range(n):
|
|
1007
|
+
dut[0].expect_exact(f'dut0_msg_{i}')
|
|
1008
|
+
dut[1].expect_exact(f'dut1_msg_{i}')
|
|
1009
|
+
""")
|
|
1010
|
+
|
|
1011
|
+
result = testdir.runpytest()
|
|
1012
|
+
result.assert_outcomes(passed=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|