Skip to content

Commit ce92fb5

Browse files
committed
scripts: twister: add support for device testing via RTT
Add support for using RTT as one of the possible communication transports (besides UART and serial pty), when executing tests on hardware with Twister. Users can enable this by: - Adding both --device-testing and --device-rtt flags to the Twister command line, or - Setting 'rtt' to True in the hardware map YAML file. Implementation details: - RTT support uses the west rtt command to connect to the device. - If west rtt: should use a different runner, then --rtt-runner flag or rtt_runner field can be used. - Renaming changes in handlers.py were done to remove the ser/serial prefix from the variables and functions related to pty, since they are now shared by both serial pty and RTT. The current approach using the west rtt command has one limitation: it is currently not possible to test multiple devices that use RTT in parallel. west rtt command will always try a specific set of ports, so running this command while another one is executing doesn't work. Closes: #57120 Signed-off-by: Marko Sagadin <[email protected]>
1 parent 812a2e3 commit ce92fb5

File tree

10 files changed

+225
-83
lines changed

10 files changed

+225
-83
lines changed

doc/develop/test/twister.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,25 @@ In this case you can run twister with the following options:
12851285
The script is user-defined and handles delivering the messages which can be
12861286
used by twister to determine the test execution status.
12871287

1288+
To support devices that communicate via RTT, use the ``--device-rtt`` option. Twister
1289+
will connect to the device with ``west rtt`` command and capture the log messages.
1290+
In this case you can run twister with the following options:
1291+
1292+
.. tabs::
1293+
1294+
.. group-tab:: Linux
1295+
1296+
.. code-block:: bash
1297+
1298+
scripts/twister --device-testing --device-rtt \
1299+
-p nrf7002dk/nrf5340/cpuapp -T tests/kernel
1300+
1301+
.. group-tab:: Windows
1302+
1303+
.. note::
1304+
1305+
Not supported on Windows OS
1306+
12881307
The ``--device-flash-timeout`` option allows to set explicit timeout on the
12891308
device flash operation, for example when device flashing takes significantly
12901309
large time.
@@ -1480,6 +1499,23 @@ work. It is equivalent to following west and twister commands.
14801499
manually according to above example. This is because the serial port
14811500
of the PTY is not fixed and being allocated in the system at runtime.
14821501

1502+
RTT support using ``--device-rtt`` can also be used in the
1503+
hardware map:
1504+
1505+
.. code-block:: yaml
1506+
1507+
- connected: true
1508+
id: 001050795550
1509+
platform: nrf7002dk/nrf5340/cpuapp
1510+
product: J-Link
1511+
runner: nrfutil
1512+
rtt: true
1513+
rtt_runner: jlink
1514+
1515+
If a different runner should be used for RTT connection than for flashing, specify
1516+
``rtt_runner`` field, like shown above. If a different runner is not needed, then
1517+
``rtt_runner`` can be omitted.
1518+
14831519
If west is not available or does not know how to flash your system, a custom
14841520
flash command can be specified using the ``flash-command`` flag. The script is
14851521
called with a ``--build-dir`` with the path of the current build, as well as a

scripts/pylib/pytest-twister-harness/README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ Run exemplary test shell application by Twister:
2626
# hardware
2727
./scripts/twister -p nrf52840dk/nrf52840 --device-testing --device-serial /dev/ttyACM0 -T samples/subsys/testsuite/pytest/shell
2828
29+
# hardware over RTT
30+
./scripts/twister -p nrf52840dk/nrf52840 --device-testing --device-rtt -T samples/subsys/testsuite/pytest/shell --test sample.pytest.rtt
31+
2932
or build shell application by west and call pytest directly:
3033

3134
.. code-block:: sh

scripts/pylib/twister/twisterlib/environment.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
174174

175175
run_group_option.add_argument(
176176
"--device-testing", action="store_true",
177-
help="Test on device directly. Specify the serial device to "
178-
"use with the --device-serial option.")
177+
help="Test on device directly. Specify the serial device to use with "
178+
"the --device-serial option or specify script to use with the "
179+
"--device-serial-pty or specify to use with --device-rtt option.")
179180

180181
run_group_option.add_argument("--generate-hardware-map",
181182
help="""Probe serial devices connected to this platform
@@ -204,6 +205,15 @@ def add_parse_arguments(parser = None) -> argparse.ArgumentParser:
204205
--device-serial-pty <script>
205206
""")
206207

208+
device.add_argument("--device-rtt", action="store_true", default=False,
209+
help="""Use RTT as communication transport.
210+
Instead of using serial console, Twister will connect to the
211+
device with "west rtt" command to capture logs and interact with
212+
the test application. Setting this option automatically sets
213+
--flash-before option, as we can connect to the device only
214+
after it was flashed.
215+
""")
216+
207217
device.add_argument("--hardware-map",
208218
help="""Load hardware map from a file. This will be used
209219
for testing on hardware that is listed in the file.
@@ -959,20 +969,23 @@ def parse_arguments(
959969

960970
if (
961971
(not options.device_testing)
962-
and (options.device_serial or options.device_serial_pty or options.hardware_map)
972+
and (options.device_serial or options.device_serial_pty or options.device_rtt or
973+
options.hardware_map)
963974
):
964975
logger.error(
965-
"Use --device-testing with --device-serial, or --device-serial-pty, or --hardware-map."
976+
"Use --device-testing with --device-serial, or --device-serial-pty, or "
977+
"--device-rtt, or --hardware-map."
966978
)
967979
sys.exit(1)
968980

969981
if (
970982
options.device_testing
971-
and (options.device_serial or options.device_serial_pty) and len(options.platform) != 1
983+
and (options.device_serial or options.device_serial_pty or
984+
options.device_rtt) and len(options.platform) != 1
972985
):
973986
logger.error("When --device-testing is used with --device-serial "
974-
"or --device-serial-pty, exactly one platform must "
975-
"be specified")
987+
"or --device-serial-pty, or --device-rtt, "
988+
"exactly one platform must be specified")
976989
sys.exit(1)
977990

978991
if options.device_flash_with_test and not options.device_testing:

scripts/pylib/twister/twisterlib/handlers.py

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,8 @@ def device_is_available(self, instance):
515515
for d in self.duts:
516516
if fixture and fixture not in map(lambda f: f.split(sep=':')[0], d.fixtures):
517517
continue
518-
if d.platform != device or (d.serial is None and d.serial_pty is None):
518+
if d.platform != device or (d.serial is None and d.serial_pty is None and
519+
d.use_rtt is False):
519520
continue
520521
duts_found.append(d)
521522

@@ -565,7 +566,7 @@ def run_custom_script(script, timeout):
565566
proc.communicate()
566567
logger.error(f"{script} timed out")
567568

568-
def _create_flash_command(self, hardware):
569+
def _create_flash_command_from_option(self, hardware):
569570
flash_command = next(csv.reader([self.options.flash_command]))
570571

571572
command = [flash_command[0]]
@@ -579,9 +580,28 @@ def _create_flash_command(self, hardware):
579580

580581
return command
581582

582-
def _create_command(self, runner, hardware):
583+
def _create_rtt_command(self, hardware):
584+
585+
command = ["west", "rtt", "--skip-rebuild", "-d", self.build_dir]
586+
rtt_runner = hardware.rtt_runner
587+
588+
if rtt_runner:
589+
command.append("--runner")
590+
command.append(rtt_runner)
591+
592+
if rtt_runner in ("jlink", "pyocd"):
593+
board_id = hardware.probe_id or hardware.id
594+
595+
command.append("--dev-id")
596+
command.append(board_id)
597+
598+
# Since _start_pty expects a string, we need to convert the command list into
599+
# one.
600+
return " ".join(command)
601+
602+
def _create_flash_command(self, runner, hardware):
583603
if self.options.flash_command:
584-
return self._create_flash_command(hardware)
604+
return self._create_flash_command_from_option(hardware)
585605

586606
command = ["west"]
587607
if self.options.verbose > 2:
@@ -648,18 +668,18 @@ def _create_command(self, runner, hardware):
648668

649669
return command
650670

651-
def _terminate_pty(self, ser_pty, ser_pty_process):
652-
logger.debug(f"Terminating serial-pty:'{ser_pty}'")
653-
terminate_process(ser_pty_process)
671+
def _terminate_pty(self, pty_name, pty_process):
672+
logger.debug(f"Terminating pty:'{pty_name}'")
673+
terminate_process(pty_process)
654674
try:
655-
(stdout, stderr) = ser_pty_process.communicate(timeout=self.get_test_timeout())
656-
logger.debug(f"Terminated serial-pty:'{ser_pty}', stdout:'{stdout}', stderr:'{stderr}'")
675+
(stdout, stderr) = pty_process.communicate(timeout=self.get_test_timeout())
676+
logger.debug(f"Terminated pty:'{pty_name}', stdout:'{stdout}', stderr:'{stderr}'")
657677
except subprocess.TimeoutExpired:
658-
logger.debug(f"Terminated serial-pty:'{ser_pty}'")
659-
#
678+
logger.debug(f"Terminated pty:'{pty_name}'")
679+
660680

661681
def _create_serial_connection(self, dut, serial_device, hardware_baud,
662-
flash_timeout, serial_pty, ser_pty_process):
682+
flash_timeout, pty_name, pty_process):
663683
try:
664684
ser = serial.Serial(
665685
serial_device,
@@ -671,20 +691,20 @@ def _create_serial_connection(self, dut, serial_device, hardware_baud,
671691
timeout=max(flash_timeout, self.get_test_timeout())
672692
)
673693
except serial.SerialException as e:
674-
self._handle_serial_exception(e, dut, serial_pty, ser_pty_process)
694+
self._handle_serial_exception(e, dut, pty_name, pty_process)
675695
raise
676696

677697
return ser
678698

679699

680-
def _handle_serial_exception(self, exception, dut, serial_pty, ser_pty_process):
700+
def _handle_serial_exception(self, exception, dut, pty_name, pty_process):
681701
self.instance.status = TwisterStatus.FAIL
682702
self.instance.reason = "Serial Device Error"
683703
logger.error(f"Serial device error: {exception!s}")
684704

685705
self.instance.add_missing_case_status(TwisterStatus.BLOCK, "Serial Device Error")
686-
if serial_pty and ser_pty_process:
687-
self._terminate_pty(serial_pty, ser_pty_process)
706+
if pty_name and pty_process:
707+
self._terminate_pty(pty_name, pty_process)
688708

689709
self.make_dut_available(dut)
690710

@@ -706,21 +726,21 @@ def get_hardware(self):
706726
logger.error(self.instance.reason)
707727
return hardware
708728

709-
def _start_serial_pty(self, serial_pty, serial_pty_master):
710-
ser_pty_process = None
729+
def _start_pty(self, command, pty_master):
730+
pty_process = None
711731
try:
712-
ser_pty_process = subprocess.Popen(
713-
re.split('[, ]', serial_pty),
714-
stdout=serial_pty_master,
715-
stdin=serial_pty_master,
716-
stderr=serial_pty_master
732+
pty_process = subprocess.Popen(
733+
re.split('[, ]', command),
734+
stdout=pty_master,
735+
stdin=pty_master,
736+
stderr=pty_master
717737
)
718738
except subprocess.CalledProcessError as error:
719739
logger.error(
720-
f"Failed to run subprocess {serial_pty}, error {error.output}"
740+
f"Failed to run subprocess {command}, error {error.output}"
721741
)
722742

723-
return ser_pty_process
743+
return pty_process
724744

725745
def handle(self, harness):
726746
runner = None
@@ -732,16 +752,19 @@ def handle(self, harness):
732752

733753
runner = hardware.runner or self.options.west_runner
734754
serial_pty = hardware.serial_pty
735-
736-
if not serial_pty:
737-
serial_device = hardware.serial
755+
use_rtt = hardware.use_rtt
756+
pty_name = None
757+
758+
if serial_pty or use_rtt:
759+
pty_master, slave = pty.openpty()
760+
pty_name = os.ttyname(slave)
761+
reason = "RTT" if use_rtt else "serial PTY"
762+
logger.debug(f"Using pty {pty_name} for {reason}")
738763
else:
739-
ser_pty_master, slave = pty.openpty()
740-
serial_device = os.ttyname(slave)
741-
742-
logger.debug(f"Using serial device {serial_device} @ {hardware.baud} baud")
764+
serial_device = hardware.serial
765+
logger.debug(f"Using serial device {serial_device} @ {hardware.baud} baud")
743766

744-
command = self._create_command(runner, hardware)
767+
flash_command = self._create_flash_command(runner, hardware)
745768

746769
pre_script = hardware.pre_script
747770
post_flash_script = hardware.post_flash_script
@@ -759,20 +782,24 @@ def handle(self, harness):
759782
flash_timeout += self.get_test_timeout()
760783

761784
serial_port = None
762-
ser_pty_process = None
785+
pty_process = None
763786
if hardware.flash_before is False:
764787
if serial_pty:
765-
ser_pty_process = self._start_serial_pty(serial_pty, ser_pty_master)
766-
serial_port = serial_device
788+
pty_process = self._start_pty(serial_pty, pty_master)
789+
serial_port = pty_name
790+
else:
791+
serial_port = serial_device
767792

793+
# Open connection to serial device or if using serial pty just create ser object
794+
# and open it later
768795
try:
769796
ser = self._create_serial_connection(
770797
hardware,
771798
serial_port,
772799
hardware.baud,
773800
flash_timeout,
774801
serial_pty,
775-
ser_pty_process
802+
pty_process
776803
)
777804
except serial.SerialException:
778805
return
@@ -785,11 +812,13 @@ def handle(self, harness):
785812
t.start()
786813

787814
d_log = f"{self.instance.build_dir}/device.log"
788-
logger.debug(f'Flash command: {command}', )
815+
logger.debug(f'Flash command: {flash_command}')
789816
failure_type = Handler.FailureType.NONE
790817
try:
791818
stdout = stderr = None
792-
with subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
819+
with subprocess.Popen(flash_command,
820+
stderr=subprocess.PIPE,
821+
stdout=subprocess.PIPE) as proc:
793822
try:
794823
(stdout, stderr) = proc.communicate(timeout=flash_timeout)
795824
# ignore unencodable unicode chars
@@ -828,10 +857,18 @@ def handle(self, harness):
828857
# Connect to device after flashing it
829858
if hardware.flash_before:
830859
try:
831-
if serial_pty:
832-
ser_pty_process = self._start_serial_pty(serial_pty, ser_pty_master)
833-
logger.debug(f"Attach serial device {serial_device} @ {hardware.baud} baud")
834-
ser.port = serial_device
860+
if use_rtt:
861+
rtt_command = self._create_rtt_command(hardware)
862+
pty_process = self._start_pty(rtt_command, pty_master)
863+
logger.debug(f"Start RTT connection on pty {pty_name}")
864+
ser.port = pty_name
865+
elif serial_pty:
866+
pty_process = self._start_pty(serial_pty, pty_master)
867+
logger.debug(f"Start serial connection on pty {pty_name}")
868+
ser.port = pty_name
869+
else:
870+
logger.debug(f"Attach serial device {serial_device} @ {hardware.baud} baud")
871+
ser.port = serial_device
835872

836873
# Apply ESP32-specific RTS/DTR reset logic
837874
if runner == "esp32":
@@ -854,7 +891,7 @@ def handle(self, harness):
854891
ser.open()
855892

856893
except serial.SerialException as e:
857-
self._handle_serial_exception(e, hardware, serial_pty, ser_pty_process)
894+
self._handle_serial_exception(e, hardware, pty_name, pty_process)
858895
return
859896

860897
if failure_type != Handler.FailureType.FLASH:
@@ -876,8 +913,8 @@ def handle(self, harness):
876913
if ser.isOpen():
877914
ser.close()
878915

879-
if serial_pty:
880-
self._terminate_pty(serial_pty, ser_pty_process)
916+
if pty_name:
917+
self._terminate_pty(pty_name, pty_process)
881918

882919
self.execution_time = time.time() - start_time
883920

0 commit comments

Comments
 (0)