Skip to content

Commit ae336f6

Browse files
gchwiercarlescufi
authored andcommitted
tests: mcuboot: pytest: Add image swap test with mcumgr
Added application based on SMP Server Sample. Application is built together with MCUboot using sysbuild and is flashed onto device in one step. Tests are automated with pytest - new harness of Twister. The image for upgrade is prepared using west sign command then is uploaded by mcumgr into device and tested. Automated scenarios to test upgrade (image upload, test, revert, confirm), to test downgrade prevention mechanism and to test upgrade with image, that is signed with an invalid key. Signed-off-by: Grzegorz Chwierut <[email protected]>
1 parent cf6bb28 commit ae336f6

File tree

13 files changed

+579
-0
lines changed

13 files changed

+579
-0
lines changed

scripts/pylib/pytest-twister-harness/src/twister_harness/helpers/shell.py

+51
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import re
99
import time
1010

11+
from dataclasses import dataclass, field
12+
from inspect import signature
13+
1114
from twister_harness.device.device_adapter import DeviceAdapter
1215
from twister_harness.exceptions import TwisterHarnessTimeoutException
1316

@@ -78,3 +81,51 @@ def get_filtered_output(self, command_lines: list[str]) -> list[str]:
7881
])
7982
)
8083
return list(filter(lambda l: not regex_filter.search(l), command_lines))
84+
85+
86+
@dataclass
87+
class ShellMCUbootArea:
88+
name: str
89+
version: str
90+
image_size: str
91+
magic: str = 'unset'
92+
swap_type: str = 'none'
93+
copy_done: str = 'unset'
94+
image_ok: str = 'unset'
95+
96+
@classmethod
97+
def from_kwargs(cls, **kwargs) -> ShellMCUbootArea:
98+
cls_fields = {field for field in signature(cls).parameters}
99+
native_args = {}
100+
for name, val in kwargs.items():
101+
if name in cls_fields:
102+
native_args[name] = val
103+
return cls(**native_args)
104+
105+
106+
@dataclass
107+
class ShellMCUbootCommandParsed:
108+
"""
109+
Helper class to keep data from `mcuboot` shell command.
110+
"""
111+
areas: list[ShellMCUbootArea] = field(default_factory=list)
112+
113+
@classmethod
114+
def create_from_cmd_output(cls, cmd_output: list[str]) -> ShellMCUbootCommandParsed:
115+
"""
116+
Factory to create class from the output of `mcuboot` shell command.
117+
"""
118+
areas: list[dict] = []
119+
re_area = re.compile(r'(.+ area.*):\s*$')
120+
re_key = re.compile(r'(?P<key>.+):(?P<val>.+)')
121+
for line in cmd_output:
122+
if m := re_area.search(line):
123+
areas.append({'name': m.group(1)})
124+
elif areas:
125+
if m := re_key.search(line):
126+
areas[-1][m.group('key').strip().replace(' ', '_')] = m.group('val').strip()
127+
data_areas: list[ShellMCUbootArea] = []
128+
for area in areas:
129+
data_areas.append(ShellMCUbootArea.from_kwargs(**area))
130+
131+
return cls(data_areas)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright (c) 2023 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
import textwrap
6+
7+
from twister_harness.helpers.shell import ShellMCUbootCommandParsed, ShellMCUbootArea
8+
9+
10+
def test_if_mcuboot_command_output_is_parsed_two_areas() -> None:
11+
cmd_output = textwrap.dedent("""
12+
\x1b[1;32muart:~$ \x1b[mmcuboot
13+
swap type: revert
14+
confirmed: 0
15+
primary area (1):
16+
version: 0.0.2+0
17+
image size: 68240
18+
magic: good
19+
swap type: test
20+
copy done: set
21+
image ok: unset
22+
secondary area (3):
23+
version: 0.0.0+0
24+
image size: 68240
25+
magic: unset
26+
swap type: none
27+
copy done: unset
28+
image ok: unset
29+
\x1b[1;32muart:~$ \x1b[m
30+
""")
31+
mcuboot_parsed = ShellMCUbootCommandParsed.create_from_cmd_output(cmd_output.splitlines())
32+
assert isinstance(mcuboot_parsed, ShellMCUbootCommandParsed)
33+
assert isinstance(mcuboot_parsed.areas[0], ShellMCUbootArea)
34+
assert len(mcuboot_parsed.areas) == 2
35+
assert mcuboot_parsed.areas[0].version == '0.0.2+0'
36+
assert mcuboot_parsed.areas[0].swap_type == 'test'
37+
38+
39+
def test_if_mcuboot_command_output_is_parsed_with_failed_area() -> None:
40+
cmd_output = textwrap.dedent("""
41+
\x1b[1;32muart:~$ \x1b[mmcuboot
42+
swap type: revert
43+
confirmed: 0
44+
primary area (1):
45+
version: 1.1.1+1
46+
image size: 68240
47+
magic: good
48+
swap type: test
49+
copy done: set
50+
image ok: unset
51+
failed to read secondary area (1) header: -5
52+
\x1b[1;32muart:~$ \x1b[m
53+
""")
54+
mcuboot_parsed = ShellMCUbootCommandParsed.create_from_cmd_output(cmd_output.splitlines())
55+
assert len(mcuboot_parsed.areas) == 1
56+
assert mcuboot_parsed.areas[0].version == '1.1.1+1'

tests/boot/with_mcumgr/CMakeLists.txt

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
cmake_minimum_required(VERSION 3.20.0)
4+
5+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
6+
project(with_mcumgr)
7+
8+
FILE(GLOB app_sources src/*.c)
9+
target_sources(app PRIVATE ${app_sources})

tests/boot/with_mcumgr/README.rst

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Upgrade testing with MCUmgr
2+
###########################
3+
4+
This application is based on :ref:`smp_svr_sample`. It is built
5+
using **sysbuild**. Tests are automated with pytest, a new harness of Twister
6+
(more information can be found here :ref:`integration-with-pytest`)
7+
8+
.. note::
9+
Pytest uses the MCUmgr fixture which requires the ``mcumgr`` available
10+
in the system PATH.
11+
More information about MCUmgr can be found here :ref:`mcu_mgr`.
12+
13+
To run tests with Twister on ``nrf52840dk_nrf52840`` platform,
14+
use following command:
15+
16+
.. code-block:: console
17+
18+
./zephyr/scripts/twister -vv --west-flash --enable-slow -T zephyr/tests/boot/with_mcumgr \
19+
-p nrf52840dk_nrf52840 --device-testing --device-serial /dev/ttyACM0
20+
21+
.. note::
22+
Twister requires ``--west-flash`` flag enabled (without additional parameters
23+
like ``erase``) to use sysbuild.
24+
25+
Test scripts can be found in ``pytest`` directory. To list available
26+
scenarios with described procedures, one can use a pytest command:
27+
28+
.. code-block:: console
29+
30+
pytest zephyr/tests/boot/with_mcumgr/pytest --collect-only -v

tests/boot/with_mcumgr/prj.conf

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Enable MCUmgr and dependencies.
2+
CONFIG_NET_BUF=y
3+
CONFIG_ZCBOR=y
4+
CONFIG_CRC=y
5+
CONFIG_MCUMGR=y
6+
CONFIG_STREAM_FLASH=y
7+
CONFIG_FLASH_MAP=y
8+
9+
# Enable the shell MCUmgr transport.
10+
CONFIG_BASE64=y
11+
CONFIG_SHELL=y
12+
CONFIG_SHELL_BACKEND_SERIAL=y
13+
CONFIG_MCUMGR_TRANSPORT_SHELL=y
14+
15+
# Enable most core commands.
16+
CONFIG_FLASH=y
17+
CONFIG_IMG_MANAGER=y
18+
CONFIG_MCUMGR_GRP_IMG=y
19+
CONFIG_MCUMGR_GRP_OS=y
20+
21+
# mcumgr-cli application doesn't accepts log in the channel it uses
22+
CONFIG_SHELL_LOG_BACKEND=n
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright (c) 2023 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
from __future__ import annotations
5+
6+
import logging
7+
8+
from pathlib import Path
9+
from twister_harness import DeviceAdapter, Shell, MCUmgr
10+
from utils import (
11+
find_in_config,
12+
match_lines,
13+
match_no_lines,
14+
check_with_shell_command,
15+
check_with_mcumgr_command,
16+
)
17+
from test_upgrade import create_signed_image, PROJECT_NAME
18+
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
def test_downgrade_prevention(dut: DeviceAdapter, shell: Shell, mcumgr: MCUmgr):
24+
"""
25+
Verify that the application is not downgraded
26+
1) Device flashed with MCUboot and an application that contains SMP server.
27+
Image version is 1.1.1+1
28+
2) Prepare an update of an application containing the SMP server, where
29+
image version is 0.0.0 (lower than version of the original app)
30+
3) Upload the application update to slot 1 using mcumgr
31+
4) Flag the application update in slot 1 as 'pending' by using mcumgr 'test'
32+
5) Restart the device, verify that downgrade prevention mechanism
33+
blocked the image swap
34+
6) Verify that the original application is booted (version 1.1.1)
35+
"""
36+
origin_version = find_in_config(
37+
Path(dut.device_config.build_dir) / PROJECT_NAME / 'zephyr' / '.config',
38+
'CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION'
39+
)
40+
check_with_shell_command(shell, origin_version)
41+
assert origin_version != '0.0.0+0'
42+
43+
logger.info('Prepare upgrade image with lower version')
44+
image_to_test = create_signed_image(dut.device_config.build_dir, '0.0.0+0')
45+
46+
logger.info('Upload image with mcumgr')
47+
dut.disconnect()
48+
mcumgr.image_upload(image_to_test)
49+
50+
logger.info('Test uploaded APP image')
51+
second_hash = mcumgr.get_hash_to_test()
52+
mcumgr.image_test(second_hash)
53+
mcumgr.reset_device()
54+
55+
dut.connect()
56+
output = dut.readlines_until('Launching primary slot application')
57+
match_no_lines(output, ['Starting swap using move algorithm'])
58+
match_lines(output, ['erased due to downgrade prevention'])
59+
logger.info('Verify that the original APP is booted')
60+
check_with_shell_command(shell, origin_version)
61+
dut.disconnect()
62+
check_with_mcumgr_command(mcumgr, origin_version)

0 commit comments

Comments
 (0)