Skip to content

Commit 4f31310

Browse files
gchwiernordicjm
authored andcommitted
tests: Moved upgrade scenarios from test repo
Moved tests from: https://projecttools.nordicsemi.no/bitbucket/projects/NCS-TEST/repos/test-sdk-mcuboot Signed-off-by: Grzegorz Chwierut <[email protected]>
1 parent 094afca commit 4f31310

File tree

87 files changed

+4018
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+4018
-0
lines changed

scripts/quarantine.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,16 @@
106106
platforms:
107107
- [email protected]/nrf54h20/cpuapp
108108
comment: "https://nordicsemi.atlassian.net/browse/NCSDK-31463"
109+
110+
- scenarios:
111+
- mcuboot.upgrade.encryption.ecdsa_p256
112+
platforms:
113+
- nrf52840dk/nrf52840
114+
comment: "https://nordicsemi.atlassian.net/browse/NCSDK-29460"
115+
116+
- scenarios:
117+
- mcuboot.nrf_compress.encryption_ed25519
118+
- mcuboot.upgrade.encryption.ed25519
119+
platforms:
120+
- nrf54lv10dk.*
121+
comment: "https://nordicsemi.atlassian.net/browse/NCSDK-35259"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Bootloader upgrade tests
2+
3+
The test suite includes various scenarios for testing bootloader functionality.
4+
The tests use pytest framework with Twister harness for automated execution on hardware.
5+
These tests applications are based on the SMP server from `zephyr/samples/subsys/mgmt/mcumgr/smp_svr`.
6+
7+
## Requirements
8+
9+
The tests require the `mcumgr` command-line tool to be installed and available in your system PATH.
10+
11+
Python packages listed in `requirements.txt` are also required. Install them with:
12+
13+
```console
14+
pip install -r requirements.txt
15+
```
16+
17+
Before running the tests, ensure that the `ZEPHYR_BASE` environment variable is set:
18+
19+
```console
20+
export ZEPHYR_BASE='<PATH_TO_NCS>/ncs/zephyr'
21+
```
22+
23+
## Running with Twister
24+
25+
To run a specific test scenario using Twister:
26+
27+
```console
28+
$ZEPHYR_BASE/scripts/twister -vv -ll debug --enable-slow \
29+
--west-flash="--recover" --device-testing -p nrf54l15dk/nrf54l15/cpuapp --device-serial /dev/ttyACM1 \
30+
-T $ZEPHYR_BASE/../nrf/tests/subsys/bootloader/upgrade \
31+
-s mcuboot.upgrade.basic --pytest-args="-k test_upgrade_with_revert"
32+
```
33+
34+
## Running without Twister
35+
36+
You can also run tests manually without Twister:
37+
38+
1. Build the test application:
39+
40+
```console
41+
cd tests/subsys/bootloader/upgrade
42+
west build -p -b nrf54l15dk/nrf54l15/cpuapp ref_smp_svr -T mcuboot.upgrade.basic --build-dir build-54l
43+
```
44+
45+
2. Run the specific test:
46+
47+
```console
48+
pytest --twister-harness -s -v --log-cli-level=DEBUG --device-type=hardware \
49+
--device-serial=/dev/ttyACM1 --west-flash-extra-args=--recover --build-dir=build-54l \
50+
-k test_upgrade_with_revert
51+
```
52+
53+
**Note:** The exact pytest command is displayed in the console when running Twister.
54+
Once the `twister-out` directory is generated, you can find the build directory in it.
55+
56+
## Manual testing workflow
57+
58+
For manual testing and debugging, you can prepare a second image and test the upgrade process step by step
59+
(it depends on the test scenario), e.g.:
60+
61+
1. Build a second version of the application:
62+
63+
```console
64+
west build -p -b nrf54l15dk/nrf54l15/cpuapp ref_smp_svr -T mcuboot.upgrade.basic \
65+
--build-dir build-54l-v2 -- '-DCONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="2.0.0+0"'
66+
```
67+
68+
2. Upload the image using mcumgr:
69+
70+
```console
71+
mcumgr --conntype serial --connstring=/dev/ttyACM1 image upload \
72+
build-54l-v2/ref_smp_svr/zephyr/zephyr.signed.bin
73+
```
74+
75+
3. List images to get the hash:
76+
77+
```console
78+
mcumgr --conntype serial --connstring=/dev/ttyACM1 image list
79+
```
80+
81+
4. Test the new image:
82+
83+
```console
84+
mcumgr --conntype serial --connstring=/dev/ttyACM1 image test <hash_of_second_image>
85+
```
86+
87+
5. Reset the device and check the console logs:
88+
89+
```console
90+
I: Image index: 0, Swap type: test
91+
I: Starting swap using offset algorithm.
92+
I: Bootloader chainload address offset: 0x11000
93+
I: Image version: v2.0.0
94+
```
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright (c) 2025 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
"""Pytest configuration, hooks, and fixtures for MCUboot test suite."""
6+
7+
import os
8+
import sys
9+
import textwrap
10+
11+
import pytest
12+
13+
# Add the directory to PYTHONPATH
14+
zephyr_base = os.getenv("ZEPHYR_BASE")
15+
if zephyr_base:
16+
sys.path.insert(0, os.path.join(zephyr_base, "scripts", "pylib", "pytest-twister-harness", "src"))
17+
else:
18+
raise EnvironmentError("ZEPHYR_BASE environment variable is not set")
19+
20+
pytest_plugins = [
21+
"twister_harness.plugin",
22+
]
23+
24+
USED_MARKERS = [
25+
# Test cycle:
26+
"commit: run on commit, every test without nightly or weekly marker get this marker",
27+
"nightly: use to skip tests in the on commit regression, tip: use -m 'nightly or commit' in regression",
28+
"weekly: for weekly run, tip: use -m 'weekly or nightly or commit' in regression to run all tests",
29+
textwrap.dedent("""
30+
add_markers_if(condition, markers): decorate test with given markers
31+
if the condition evaluate to True.
32+
Example: add_markers_if('"nsib" in device_config.build_dir.name', [pytest.mark.nightly])
33+
"""),
34+
# filtering:
35+
textwrap.dedent("""
36+
skip_if(condition, reason=...): skip the given test function if the condition evaluate to True.
37+
'DeviceConfig' object is available as 'device_config' variable in the condition.
38+
Example: skip_if('"nrf54l" in device_config.platform', reason='Filtered out for nrf54l family')
39+
"""),
40+
]
41+
42+
43+
def pytest_configure(config: pytest.Config):
44+
"""Configure pytest markers for the test session."""
45+
for marker in USED_MARKERS:
46+
config.addinivalue_line("markers", marker)
47+
48+
49+
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): # noqa: ARG001
50+
"""Modify collected test items before running tests."""
51+
for item in items:
52+
# Implementation for add_markers_if decorator
53+
for marker in item.iter_markers("add_markers_if"):
54+
try:
55+
condition = marker.args[0]
56+
markers = marker.args[1]
57+
if not isinstance(markers, list):
58+
markers = [markers]
59+
except (IndexError, KeyError):
60+
pytest.exit("Wrong usage of `add_markers_if` decorator")
61+
if _evaluate_condition(item, condition):
62+
for marker in markers:
63+
item.add_marker(marker)
64+
65+
# Add default markers if not any of the used markers are present
66+
if not any(marker.name in ["commit", "nightly", "weekly"] for marker in item.iter_markers()):
67+
item.add_marker(pytest.mark.commit)
68+
item.add_marker(pytest.mark.nightly)
69+
item.add_marker(pytest.mark.weekly)
70+
71+
# Implementation for skip_if decorator
72+
for marker in item.iter_markers("skip_if"):
73+
try:
74+
condition = marker.args[0]
75+
reason = marker.kwargs.get("reason", "")
76+
except (IndexError, KeyError):
77+
pytest.exit("Wrong usage of `skip_if` decorator")
78+
if _evaluate_condition(item, condition):
79+
item.add_marker(pytest.mark.skip(reason))
80+
81+
82+
def _evaluate_condition(item, condition: str) -> bool:
83+
assert isinstance(condition, str)
84+
if not condition:
85+
return True
86+
globals_ = {"__builtins__": {"sys": sys, "os": os, "any": any, "all": all}}
87+
try:
88+
device_config = item.config.twister_harness_config.devices[0]
89+
locals_ = {
90+
"device_config": device_config,
91+
}
92+
except (KeyError, AttributeError):
93+
return True
94+
try:
95+
return eval(condition, globals_, locals_) is True
96+
except Exception as exc:
97+
pytest.exit(f'Cannot evaluate expression: "{condition}"; {exc}')
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (c) 2025 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
"""Tool for verifying LZMA2 compression and decompression of MCUboot images."""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
import logging
11+
import sys
12+
import tempfile
13+
import textwrap
14+
from pathlib import Path
15+
16+
from helpers import run_command
17+
from intelhex import IntelHex
18+
from mcuboot_image_utils import ImageHeader
19+
20+
logger = logging.getLogger(__name__)
21+
22+
EXPECTED_MAGIC = 0x96F3B83D
23+
LZMA_HEADER_SIZE = 2
24+
FLAG_LZMA2 = 0x400
25+
FLAG_ARM_THUMB = 0x800
26+
27+
28+
class CheckCompressionError(Exception):
29+
"""Exception raised for errors in LZMA compression check."""
30+
31+
32+
def check_lzma_compression(
33+
signed_bin: str | Path,
34+
unsigned_bin: str | Path,
35+
workdir: str | Path | None = None,
36+
padding: int = 0,
37+
) -> None:
38+
"""Check if the image is compressed with LZMA2.
39+
40+
Verify if decompressed data is identical as before compression.
41+
"""
42+
logger.debug(f"check_lzma_compression: {signed_bin=}, {unsigned_bin=}")
43+
workdir = Path(workdir) if workdir else Path.cwd()
44+
ih_signed = IntelHex()
45+
ih_signed.loadbin(signed_bin)
46+
47+
logger.info("Check image header of signed image")
48+
header = ImageHeader.from_bytes(ih_signed.gets(0, 20))
49+
logger.debug(header)
50+
if header.magic != EXPECTED_MAGIC:
51+
raise CheckCompressionError("Invalid magic value")
52+
if header.flags & FLAG_LZMA2 == 0:
53+
raise CheckCompressionError("Not LZMA2 compression")
54+
55+
logger.info("Extracting LZMA stream from signed image")
56+
lzma_stream_size = header.img_size - LZMA_HEADER_SIZE
57+
lzma_stream_offset = header.hdr_size + LZMA_HEADER_SIZE
58+
ih_lzma = IntelHex()
59+
ih_lzma.frombytes(ih_signed.gets(lzma_stream_offset, lzma_stream_size))
60+
ih_lzma.tobinfile(workdir / "stream.lzma")
61+
62+
logger.info("Decompressing LZMA stream")
63+
unlzma_cmd = [
64+
"unlzma",
65+
"--armthumb" if header.flags & FLAG_ARM_THUMB else "",
66+
"--lzma2",
67+
"--format=raw",
68+
"--suffix=.lzma",
69+
str(workdir / "stream.lzma"),
70+
]
71+
run_command(unlzma_cmd)
72+
73+
logger.info("Verifying decompressed data")
74+
ih_unsigned = IntelHex()
75+
ih_unsigned.loadbin(unsigned_bin)
76+
if padding:
77+
# For platforms that don't use partition manager, the original image is offset by padding
78+
# which is later filled with the header. For comparison, this padding needs to be removed.
79+
ih_unsigned_without_padding = IntelHex()
80+
ih_unsigned_without_padding.frombytes(
81+
ih_unsigned.gets(header.hdr_size, ih_unsigned.maxaddr() - header.hdr_size + 1) # type: ignore
82+
)
83+
ih_unsigned = ih_unsigned_without_padding
84+
ih_unlzma = IntelHex()
85+
ih_unlzma.loadbin(workdir / "stream")
86+
87+
if ih_unsigned.maxaddr() != ih_unlzma.maxaddr():
88+
raise CheckCompressionError("Decompressed data length is not identical as before compression")
89+
if ih_unsigned.tobinarray() != ih_unlzma.tobinarray():
90+
raise CheckCompressionError("Decompressed data is not identical as before compression")
91+
logger.info("Decompressed data is identical as before compression")
92+
93+
94+
def create_parser() -> argparse.ArgumentParser:
95+
"""Create an argument parser for the LZMA compression check tool."""
96+
parser = argparse.ArgumentParser(
97+
formatter_class=argparse.RawDescriptionHelpFormatter,
98+
allow_abbrev=False,
99+
description="LZMA test",
100+
epilog=(
101+
textwrap.dedent("""
102+
This tool finds and extracts compressed stream, decompresses it and verifies if
103+
decompressed one is identical as before compression.
104+
""")
105+
),
106+
)
107+
parser.add_argument("signed_bin", metavar="path", help="Path to the signed binary file")
108+
parser.add_argument("unsigned_bin", metavar="path", help="Path to the unsigned binary file")
109+
110+
parser.add_argument(
111+
"--padding",
112+
type=int,
113+
default=0,
114+
help="Padding value for platforms that don't use partition manager (default: 0)",
115+
)
116+
parser.add_argument(
117+
"-ll", "--log-level", type=str.upper, default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
118+
)
119+
return parser
120+
121+
122+
def main():
123+
"""Run the LZMA compression check tool."""
124+
parser = create_parser()
125+
args = parser.parse_args()
126+
logging.basicConfig(level=args.log_level, format="%(levelname)-8s: %(message)s")
127+
128+
try:
129+
with tempfile.TemporaryDirectory() as tmpdir:
130+
check_lzma_compression(args.signed_bin, args.unsigned_bin, tmpdir, args.padding)
131+
except CheckCompressionError as e:
132+
logger.error(e)
133+
sys.exit(1)
134+
135+
136+
if __name__ == "__main__":
137+
main()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) 2025 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
"""Pytest configuration, hooks, and fixtures for MCUboot test suite."""
6+
7+
import logging
8+
9+
import pytest
10+
from helpers import reset_board
11+
from key_provisioning import provision_mcuboot, provision_nsib
12+
from twister_harness.device.device_adapter import DeviceAdapter
13+
from twister_harness.fixtures import determine_scope
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
@pytest.fixture(scope=determine_scope)
19+
def no_reset(device_object: DeviceAdapter):
20+
"""Do not reset after flashing."""
21+
device_object.device_config.west_flash_extra_args.append("--no-reset")
22+
yield
23+
device_object.device_config.west_flash_extra_args.remove("--no-reset")
24+
# Reset the command list to avoid side effects on other tests
25+
device_object.command = []
26+
27+
28+
@pytest.fixture(scope=determine_scope)
29+
def kmu_provision(no_reset, dut: DeviceAdapter): # noqa: ARG001
30+
"""Provision KMU keys using west ncs-provision upload command."""
31+
# only for sysbuild
32+
if dut.device_config.build_dir != dut.device_config.app_build_dir:
33+
provision_nsib(dut)
34+
provision_mcuboot(dut)
35+
reset_board(dut.device_config.id)

0 commit comments

Comments
 (0)