[PATCH] test/py: mtd: Add tests for mtd command
Heinrich Schuchardt
heinrich.schuchardt at canonical.com
Wed May 27 15:36:23 CEST 2026
On 5/26/26 09:37, Love Kumar wrote:
> Add test cases for mtd commands to verify list, erase, write, read
> and dump operations on NOR flash and binary-file data integrity.
> This test relies on boardenv_* configurations to run it for single or
> multiple MTD partitions.
>
> Signed-off-by: Love Kumar <love.kumar at amd.com>
> ---
> doc/develop/pytest/test_mtd.rst | 10 +
> test/py/tests/test_mtd.py | 648 ++++++++++++++++++++++++++++++++
> 2 files changed, 658 insertions(+)
> create mode 100644 doc/develop/pytest/test_mtd.rst
> create mode 100644 test/py/tests/test_mtd.py
>
> diff --git a/doc/develop/pytest/test_mtd.rst b/doc/develop/pytest/test_mtd.rst
> new file mode 100644
> index 000000000000..70d469af5caa
> --- /dev/null
> +++ b/doc/develop/pytest/test_mtd.rst
> @@ -0,0 +1,10 @@
> +.. SPDX-License-Identifier: GPL-2.0+
> +
> +test_mtd
> +========
> +
> +.. automodule:: test_mtd
> + :synopsis:
> + :member-order: bysource
> + :members:
> + :undoc-members:
> diff --git a/test/py/tests/test_mtd.py b/test/py/tests/test_mtd.py
> new file mode 100644
> index 000000000000..947eba7426c6
> --- /dev/null
> +++ b/test/py/tests/test_mtd.py
> @@ -0,0 +1,648 @@
> +# SPDX-License-Identifier: GPL-2.0
> +# (C) Copyright 2026, Advanced Micro Devices, Inc.
> +
> +"""
> +Note: This test relies on boardenv_* containing configuration values to
> +define one or more MTD partitions on which to exercise the 'mtd' U-Boot
> +command. The test reads env__mtd_partitions itself and loops over every
> +entry. Without this configuration the test is automatically skipped.
> +
> +It exercises the 'mtd' subcommands (list, erase, write, read, dump) and
> +a binary-file data integrity round-trip (tftp + write + read + cmp.b).
> +The suite works for single and stacked flash configurations. Partitions
> +whose detected MTD type is not 'NOR flash' are logged and skipped
> +per-partition.
> +
> +For Example:
> +
> +# List of MTD partitions to test. partition_name is the MTD partition
> +# name passed to every 'mtd' subcommand; flash_part_name is optional
> +# and, when set, is verified against 'SF: Detected <name>' lines in
> +# 'mtd list' output.
> +#
> +# partition_name - MTD partition name (required)
> +# flash_part_name - expected flash chip name (optional)
> +# expected_size - partition size in bytes (optional cross-check)
> +# expected_erasesize - erase-block size in bytes (optional cross-check)
> +# writeable - bool; if False only non-destructive tests run
> +# timeout - per-partition command timeout for long ops
> +env__mtd_partitions = [
> + {
> + 'partition_name': 'qspi-fsbl-uboot',
> + 'flash_part_name': 'mt25qu512a',
> + 'expected_size': 0x1000000,
> + 'expected_erasesize': 0x10000,
> + 'writeable': True,
> + },
> + {
> + 'partition_name': 'rootfs_b',
> + 'flash_part_name': 'mt25qu512a',
> + 'expected_size': 0x1000000,
> + 'writeable': True,
> + },
> + {
> + 'partition_name': 'rootfs_a-rootfs_c-concat',
> + 'flash_part_name': 'mt25qu512a',
> + 'expected_size': 0x6000000,
> + 'writeable': True,
> + },
> +]
> +
> +# Binary-file data integrity test configuration. When set,
> +# test_mtd_bin_integrity fetches the file over TFTP once, then writes
> +# it to every writeable partition large enough to hold it and verifies
> +# byte-for-byte equality via 'cmp.b'. When unset, that test is skipped.
> +#
> +# bin_file - TFTP filename of the binary blob
> +# bin_size - byte size of the binary blob
> +# tftp_addr - RAM address used as the TFTP destination and source
> +# for 'mtd write'
> +# readback_addr - RAM address used to read the data back from flash
> +env__mtd_bin_test = {
> + 'bin_file': 'BOOT_SINGLE.BIN',
> + 'tftp_addr': 0x200000,
> + 'bin_size': 0x1E3300,
> + 'readback_addr': 0x5000000,
> +}
> +
> +# Optional. If omitted, tests use the defaults shown below.
> +# iteration - randomized iteration count per test (default 3)
> +# default_timeout - default per-command timeout in milliseconds
> +# (default 1000000)
> +env__mtd_test_settings = {
> + 'iteration': 3,
> + 'default_timeout': 1000000,
> +}
> +"""
Hello Love,
make htmldocs
fails with your patch:
/test/py/tests/test_mtd.py:docstring of test_mtd:26: ERROR: Unexpected
indentation. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:32: WARNING: Definition
list ends without a blank line; unexpected unindent. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:34: ERROR: Unexpected
indentation. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:38: WARNING: Block
quote ends without a blank line; unexpected unindent. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:40: ERROR: Unexpected
indentation. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:44: WARNING: Block
quote ends without a blank line; unexpected unindent. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:45: WARNING: Block
quote ends without a blank line; unexpected unindent. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:58: ERROR: Unexpected
indentation. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:62: WARNING: Block
quote ends without a blank line; unexpected unindent. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:69: ERROR: Unexpected
indentation. [docutils]
/test/py/tests/test_mtd.py:docstring of test_mtd:71: WARNING: Block
quote ends without a blank line; unexpected unindent. [docutils]
The suggestion below fixes the issue.
Best regards
Heinrich
# SPDX-License-Identifier: GPL-2.0
# (C) Copyright 2026, Advanced Micro Devices, Inc.
"""
This test relies on boardenv_* containing configuration values to
define one or more MTD partitions on which to exercise the 'mtd' U-Boot
command. The test reads env__mtd_partitions itself and loops over every
entry. Without this configuration the test is automatically skipped.
It exercises the 'mtd' subcommands (list, erase, write, read, dump) and
a binary-file data integrity round-trip (tftp + write + read + cmp.b).
The suite works for single and stacked flash configurations. Partitions
whose detected MTD type is not 'NOR flash' are logged and skipped
per-partition.
For Example:
.. code-block:: python
# List of MTD partitions to test. partition_name is the MTD partition
# name passed to every 'mtd' subcommand; flash_part_name is optional
# and, when set, is verified against 'SF: Detected <name>' lines in
# 'mtd list' output.
#
# partition_name - MTD partition name (required)
# flash_part_name - expected flash chip name (optional)
# expected_size - partition size in bytes (optional cross-check)
# expected_erasesize - erase-block size in bytes (optional
cross-check)
# writeable - bool; if False only non-destructive tests run
# timeout - per-partition command timeout for long ops
env__mtd_partitions = [
{
'partition_name': 'qspi-fsbl-uboot',
'flash_part_name': 'mt25qu512a',
'expected_size': 0x1000000,
'expected_erasesize': 0x10000,
'writeable': True,
},
{
'partition_name': 'rootfs_b',
'flash_part_name': 'mt25qu512a',
'expected_size': 0x1000000,
'writeable': True,
},
{
'partition_name': 'rootfs_a-rootfs_c-concat',
'flash_part_name': 'mt25qu512a',
'expected_size': 0x6000000,
'writeable': True,
},
]
# Binary-file data integrity test configuration. When set,
# test_mtd_bin_integrity fetches the file over TFTP once, then writes
# it to every writeable partition large enough to hold it and verifies
# byte-for-byte equality via 'cmp.b'. When unset, that test is skipped.
#
# bin_file - TFTP filename of the binary blob
# bin_size - byte size of the binary blob
# tftp_addr - RAM address used as the TFTP destination and source
# for 'mtd write'
# readback_addr - RAM address used to read the data back from flash
env__mtd_bin_test = {
'bin_file': 'BOOT_SINGLE.BIN',
'tftp_addr': 0x200000,
'bin_size': 0x1E3300,
'readback_addr': 0x5000000,
}
# Optional. If omitted, tests use the defaults shown below.
# iteration - randomized iteration count per test (default 3)
# default_timeout - default per-command timeout in milliseconds
# (default 1000000)
env__mtd_test_settings = {
'iteration': 3,
'default_timeout': 1000000,
}
"""
> +
> +import random
> +import re
> +import pytest
> +import test_net
> +import utils
> +
> +MTD_LIST_HEADER = 'List of MTD devices:'
> +SF_DETECTED = 'SF: Detected'
> +SUPPORTED_TYPE = 'NOR flash'
> +EXPECTED_ERASE = 'eraseblock(s)'
> +EXPECTED_WRITE = 'Writing'
> +WRITE_FAILURE = 'Failure while writing'
> +EXPECTED_READ = 'Reading'
> +READ_FAILURE = 'Failure while reading'
> +EXPECTED_DUMP = 'Dump'
> +EXPECTED_COMPARE = 'were the same'
> +TFTP_DONE = 'Bytes transferred = '
> +
> +DEFAULT_TIMEOUT = 1000000
> +DEFAULT_ITERATION = 3
> +
> +def parse_mtd_list(output):
> + """Parse 'mtd list' output into a dict keyed by device name.
> +
> + Each value has 'type', 'block_size', 'min_io' and a 'partitions'
> + dict mapping name -> (start, end) for the device-level span and
> + any nested partition lines.
> + """
> + devices = {}
> + current = None
> + for line in output.splitlines():
> + match = re.match(r'^\* (\S+)\s*$', line)
> + if match:
> + current = match.group(1)
> + devices[current] = {
> + 'type': None,
> + 'block_size': 0,
> + 'min_io': 0,
> + 'partitions': {},
> + }
> + continue
> + if current is None:
> + continue
> + match = re.match(r'^\s*- type: (.+?)\s*$', line)
> + if match:
> + devices[current]['type'] = match.group(1)
> + continue
> + match = re.match(r'^\s*- block size: (0x[0-9a-f]+) bytes\s*$', line)
> + if match:
> + devices[current]['block_size'] = int(match.group(1), 16)
> + continue
> + match = re.match(r'^\s*- min I/O: (0x[0-9a-f]+) bytes\s*$', line)
> + if match:
> + devices[current]['min_io'] = int(match.group(1), 16)
> + continue
> + match = re.search(r'0x([0-9a-f]+)-0x([0-9a-f]+) : "([^"]+)"', line)
> + if match:
> + start = int(match.group(1), 16)
> + end = int(match.group(2), 16)
> + name = match.group(3)
> + devices[current]['partitions'][name] = (start, end)
> + return devices
> +
> +def parse_flash_parts(output):
> + """Return the list of flash names from 'SF: Detected <name>' lines."""
> + return re.findall(rf'{SF_DETECTED} (\S+) with', output)
> +
> +def find_part_info(parsed, partition_name):
> + """Look up a partition across all parsed devices.
> +
> + Returns (dev_info, start, end) or (None, None, None) if not found.
> + """
> + for dev_info in parsed.values():
> + if partition_name in dev_info['partitions']:
> + start, end = dev_info['partitions'][partition_name]
> + return dev_info, start, end
> + return None, None, None
> +
> +def get_iterations(ubman):
> + """Per-test iteration count from env__mtd_test_settings (default 3)."""
> + settings = ubman.config.env.get('env__mtd_test_settings', {})
> + return settings.get('iteration', DEFAULT_ITERATION)
> +
> +def get_default_timeout(ubman):
> + """Default command timeout in ms from env__mtd_test_settings.
> +
> + Defaults to DEFAULT_TIMEOUT (1000000) if unset.
> + """
> + settings = ubman.config.env.get('env__mtd_test_settings', {})
> + return settings.get('default_timeout', DEFAULT_TIMEOUT)
> +
> +def mtd_prepare(ubman, config):
> + """Probe an MTD partition and return a parameter dict.
> +
> + Returns None (after logging via ubman.log.info) if the partition
> + is not present or its detected type is not 'NOR flash'. A missing
> + partition_name in the config triggers pytest.fail.
> + """
> + partition_name = config.get('partition_name')
> + if not partition_name:
> + pytest.fail(
> + "env__mtd_partitions entry missing required key "
> + "'partition_name'"
> + )
> +
> + output = do_list(ubman)
> + parsed = parse_mtd_list(output)
> + if not parsed:
> + pytest.fail(f'mtd list returned no devices; output was:\n{output}')
> +
> + dev_info, start, end = find_part_info(parsed, partition_name)
> + if dev_info is None:
> + ubman.log.info(f'skip {partition_name!r}: not present in mtd list')
> + return None
> +
> + mtd_type = dev_info['type']
> + if mtd_type != SUPPORTED_TYPE:
> + ubman.log.info(
> + f'skip {partition_name!r}: type {mtd_type!r} unsupported '
> + f'(only {SUPPORTED_TYPE!r} is supported)'
> + )
> + return None
> +
> + size = end - start
> + erasesize = dev_info['block_size']
> + writesize = dev_info['min_io']
> + if erasesize == 0 or writesize == 0:
> + pytest.fail(
> + f'mtd list reported invalid geometry for {partition_name!r}: '
> + f'erasesize={erasesize:#x} writesize={writesize:#x}'
> + )
> +
> + expected_size = config.get('expected_size')
> + if expected_size and expected_size != size:
> + pytest.fail(
> + f'Size mismatch for {partition_name!r}: expected '
> + f'{expected_size:#x}, got {size:#x}'
> + )
> + expected_erasesize = config.get('expected_erasesize')
> + if expected_erasesize and expected_erasesize != erasesize:
> + pytest.fail(
> + f'Erase size mismatch for {partition_name!r}: expected '
> + f'{expected_erasesize:#x}, got {erasesize:#x}'
> + )
> +
> + return {
> + 'name': partition_name,
> + 'size': size,
> + 'start': start,
> + 'erasesize': erasesize,
> + 'writesize': writesize,
> + 'type': mtd_type,
> + 'ram_base': utils.find_ram_base(ubman),
> + 'timeout': config.get('timeout', get_default_timeout(ubman)),
> + 'writeable': config.get('writeable', False),
> + 'flash_part_name': config.get('flash_part_name'),
> + }
> +
> +def rand_aligned_offset(size, align):
> + """Random offset that is a multiple of 'align' inside [0, size-align]."""
> + if size <= align:
> + return 0
> + return random.randrange(0, size - align + 1, align)
> +
> +def rand_aligned_range(part, kind):
> + """Random aligned (offset, length) inside the partition.
> +
> + kind is 'erase' (erase-block aligned) or 'io' (page aligned). The
> + caller must have verified the partition holds at least one unit.
> + """
> + if kind == 'erase':
> + align = part['erasesize']
> + elif kind == 'io':
> + align = max(part['writesize'], 1)
> + else:
> + raise ValueError(f'Unknown alignment kind: {kind!r}')
> +
> + size = part['size']
> + off = rand_aligned_offset(size, align)
> + remaining = size - off
> + if remaining <= align:
> + length = align
> + else:
> + length = random.randrange(align, remaining + 1, align)
> + return off, length
> +
> +def run_op(ubman, part, cmd, exp_str=None, not_exp_str=None, exp_rc=0):
> + """Run a U-Boot command and check its output and return code.
> +
> + exp_str / not_exp_str (when not None) must / must not appear in the
> + output. exp_rc is the expected echo $? value; pass -1 to skip the
> + rc check. 'part' provides the timeout; pass None to use the default.
> + Returns (output, rc_string).
> + """
> + timeout = part['timeout'] if part else DEFAULT_TIMEOUT
> + with ubman.temporary_timeout(timeout):
> + output = ubman.run_command(cmd)
> + if exp_str is not None:
> + assert exp_str in output, (
> + f'Expected {exp_str!r} in output of {cmd!r}, got:\n{output}'
> + )
> + if not_exp_str is not None:
> + assert not_exp_str not in output, (
> + f'Unexpected {not_exp_str!r} in output of {cmd!r}'
> + )
> + rc_str = ubman.run_command('echo $?')
> + if exp_rc >= 0:
> + assert rc_str.endswith(str(exp_rc)), (
> + f'Expected rc {exp_rc} for {cmd!r}, got {rc_str!r}'
> + )
> + return output, rc_str
> +
> +def do_list(ubman):
> + """Run 'mtd list' and return the raw output."""
> + timeout = get_default_timeout(ubman)
> + with ubman.temporary_timeout(timeout):
> + output = ubman.run_command('mtd list')
> + assert MTD_LIST_HEADER in output, (
> + f'Expected {MTD_LIST_HEADER!r} in mtd list output, got:\n{output}'
> + )
> + return output
> +
> +def do_erase(ubman, part, off, size, exp_rc=0):
> + """Run 'mtd erase <name> <off> <size>'. off/size must be aligned."""
> + cmd = f'mtd erase {part["name"]} {off:#x} {size:#x}'
> + exp = EXPECTED_ERASE if exp_rc == 0 else None
> + return run_op(ubman, part, cmd, exp_str=exp, exp_rc=exp_rc)
> +
> +def do_write(ubman, part, addr, off, size, exp_rc=0):
> + """Run 'mtd write <name> <addr> <off> <size>'."""
> + cmd = f'mtd write {part["name"]} {addr:#x} {off:#x} {size:#x}'
> + exp = EXPECTED_WRITE if exp_rc == 0 else None
> + not_exp = WRITE_FAILURE if exp_rc == 0 else None
> + return run_op(
> + ubman, part, cmd, exp_str=exp, not_exp_str=not_exp, exp_rc=exp_rc,
> + )
> +
> +def do_read(ubman, part, addr, off, size, exp_rc=0):
> + """Run 'mtd read <name> <addr> <off> <size>'."""
> + cmd = f'mtd read {part["name"]} {addr:#x} {off:#x} {size:#x}'
> + exp = EXPECTED_READ if exp_rc == 0 else None
> + not_exp = READ_FAILURE if exp_rc == 0 else None
> + return run_op(
> + ubman, part, cmd, exp_str=exp, not_exp_str=not_exp, exp_rc=exp_rc,
> + )
> +
> +def do_dump(ubman, part, off=None, size=None):
> + """Run 'mtd dump' and verify a hex dump line is produced."""
> + cmd = f'mtd dump {part["name"]}'
> + if off is not None:
> + cmd += f' {off:#x}'
> + if size is not None:
> + cmd += f' {size:#x}'
> + output, _ = run_op(ubman, part, cmd, exp_str=EXPECTED_DUMP)
> + assert re.search(
> + r'0x[0-9a-f]{8}:\s+(?:[0-9a-f]{2}\s+){8,}', output
> + ), (
> + f'Expected hex dump line, got:\n{output}'
> + )
> + return output
> +
> +def round_trip_verify(ubman, part, off, size, pattern=None):
> + """Erase, write a RAM pattern, read it back, and CRC-compare.
> +
> + The source pattern goes to part['ram_base'] and the readback area
> + is part['ram_base'] + part['size'] so the two never overlap. Both
> + off and size must be erase-block aligned. Returns the source CRC.
> + """
> + src = part['ram_base']
> + dst = part['ram_base'] + part['size']
> + if pattern is None:
> + pattern = random.randint(1, 0xfe)
> + ubman.run_command(f'mw.b {src:#x} {pattern:#x} {size:#x}')
> + src_crc = utils.crc32(ubman, src, size)
> + do_erase(ubman, part, off, size)
> + do_write(ubman, part, src, off, size)
> + do_read(ubman, part, dst, off, size)
> + dst_crc = utils.crc32(ubman, dst, size)
> + assert src_crc == dst_crc, (
> + f'CRC mismatch after round-trip on {part["name"]!r}: '
> + f'src={src_crc} dst={dst_crc} off={off:#x} size={size:#x}'
> + )
> + return src_crc
> +
> +def setup_network(ubman):
> + """Bring up the network (try DHCP, fall back to static), or skip."""
> + test_net.test_net_dhcp(ubman)
> + if not test_net.net_set_up:
> + test_net.test_net_setup_static(ubman)
> + if not test_net.net_set_up:
> + pytest.skip('Network setup failed; cannot fetch binary file')
> +
> +def get_configs(ubman):
> + """Return env__mtd_partitions or skip the test if not configured."""
> + configs = ubman.config.env.get('env__mtd_partitions', None)
> + if not configs:
> + pytest.skip('No MTD partitions configured')
> + return configs
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> +def test_mtd_list(ubman):
> + """Verify 'mtd list' shows every partition and expected flash part."""
> + configs = get_configs(ubman)
> + output = do_list(ubman)
> + parsed = parse_mtd_list(output)
> + detected_flash = parse_flash_parts(output)
> +
> + for config in configs:
> + partition_name = config.get('partition_name')
> + if not partition_name:
> + pytest.fail(
> + "env__mtd_partitions entry missing 'partition_name'"
> + )
> + dev_info, start, end = find_part_info(parsed, partition_name)
> + assert dev_info is not None, (
> + f'Partition {partition_name!r} not in mtd list output:\n'
> + f'{output}'
> + )
> + expected_erasesize = config.get('expected_erasesize')
> + if expected_erasesize:
> + assert dev_info['block_size'] == expected_erasesize, (
> + f'Erase size mismatch for {partition_name!r}: '
> + f'expected {expected_erasesize:#x}, got '
> + f'{dev_info["block_size"]:#x}'
> + )
> + expected_size = config.get('expected_size')
> + if expected_size:
> + assert (end - start) == expected_size, (
> + f'Size mismatch for {partition_name!r}: expected '
> + f'{expected_size:#x}, got {end - start:#x}'
> + )
> + flash_part_name = config.get('flash_part_name')
> + if flash_part_name:
> + assert flash_part_name in detected_flash, (
> + f'Expected flash part {flash_part_name!r} not found '
> + f'in SF: Detected lines: {detected_flash}'
> + )
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> + at pytest.mark.buildconfigspec('cmd_bdi')
> +def test_mtd_erase_block(ubman):
> + """Erase random aligned ranges in every writeable partition."""
> + configs = get_configs(ubman)
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + if not part['writeable']:
> + ubman.log.info(f'skip {part["name"]!r}: not marked writeable')
> + continue
> + if part['size'] < part['erasesize']:
> + ubman.log.info(
> + f'skip {part["name"]!r}: smaller than one erase block'
> + )
> + continue
> + for _ in range(get_iterations(ubman)):
> + off, size = rand_aligned_range(part, 'erase')
> + do_erase(ubman, part, off, size)
> + ran_any = True
> + if not ran_any:
> + pytest.skip('No writeable partition large enough for this test')
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> +def test_mtd_erase_all(ubman):
> + """Erase every writeable partition in full."""
> + configs = get_configs(ubman)
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + if not part['writeable']:
> + ubman.log.info(f'skip {part["name"]!r}: not marked writeable')
> + continue
> + if part['size'] < part['erasesize']:
> + ubman.log.info(
> + f'skip {part["name"]!r}: smaller than one erase block'
> + )
> + continue
> + # Round down to whole erase blocks: partition size is not
> + # guaranteed to be a multiple of the erase block.
> + full = (part['size'] // part['erasesize']) * part['erasesize']
> + for _ in range(get_iterations(ubman)):
> + do_erase(ubman, part, 0, full)
> + ran_any = True
> + if not ran_any:
> + pytest.skip('No writeable partition large enough for this test')
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> + at pytest.mark.buildconfigspec('cmd_bdi')
> + at pytest.mark.buildconfigspec('cmd_memory')
> + at pytest.mark.buildconfigspec('cmd_crc32')
> +def test_mtd_write_read_random(ubman):
> + """Round-trip a random pattern at random aligned ranges."""
> + configs = get_configs(ubman)
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + if not part['writeable']:
> + ubman.log.info(f'skip {part["name"]!r}: not marked writeable')
> + continue
> + if part['size'] < part['erasesize']:
> + ubman.log.info(
> + f'skip {part["name"]!r}: smaller than one erase block'
> + )
> + continue
> + for _ in range(get_iterations(ubman)):
> + off, size = rand_aligned_range(part, 'erase')
> + round_trip_verify(ubman, part, off, size)
> + ran_any = True
> + if not ran_any:
> + pytest.skip('No writeable partition large enough for this test')
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> + at pytest.mark.buildconfigspec('cmd_bdi')
> + at pytest.mark.buildconfigspec('cmd_memory')
> + at pytest.mark.buildconfigspec('cmd_crc32')
> +def test_mtd_write_twice(ubman):
> + """Round-trip small, medium, full, and a partition-midpoint write."""
> + configs = get_configs(ubman)
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + if not part['writeable']:
> + ubman.log.info(f'skip {part["name"]!r}: not marked writeable')
> + continue
> + if part['size'] < part['erasesize']:
> + ubman.log.info(
> + f'skip {part["name"]!r}: smaller than one erase block'
> + )
> + continue
> + erasesize = part['erasesize']
> + full = (part['size'] // erasesize) * erasesize
> + mid_upper = max(erasesize + 1, full // 2)
> + sizes = [
> + erasesize,
> + random.randrange(erasesize, mid_upper + 1, erasesize),
> + full,
> + ]
> + for chunk in sizes:
> + round_trip_verify(ubman, part, 0, chunk)
> +
> + # Write a short region centred on the partition midpoint. For
> + # concat partitions this straddles the inner boundary.
> + op_size = min(erasesize * 4, full)
> + op_size = (op_size // erasesize) * erasesize
> + half = (op_size // 2 // erasesize) * erasesize
> + if half == 0:
> + half = erasesize
> + midpoint = (full // 2 // erasesize) * erasesize
> + straddle_off = max(midpoint - half, 0)
> + if op_size >= erasesize and straddle_off + op_size <= full:
> + round_trip_verify(ubman, part, straddle_off, op_size)
> + ran_any = True
> + if not ran_any:
> + pytest.skip('No writeable partition large enough for this test')
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> +def test_mtd_dump(ubman):
> + """Verify 'mtd dump' produces a hex dump (default + explicit size)."""
> + configs = get_configs(ubman)
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + align = max(part['writesize'], 1)
> + units_total = part['size'] // align
> + if units_total == 0:
> + ubman.log.info(
> + f'skip {part["name"]!r}: smaller than one I/O unit'
> + )
> + continue
> +
> + # Default-size dump: writesize bytes from a random aligned
> + # offset.
> + off = rand_aligned_offset(part['size'], align)
> + do_dump(ubman, part, off=off)
> +
> + # Explicit-size dump: 1..4 aligned units, with the offset
> + # picked after the size so off + size stays within the
> + # partition.
> + units = random.randint(1, min(4, units_total))
> + size = units * align
> + off = random.randint(0, units_total - units) * align
> + do_dump(ubman, part, off=off, size=size)
> + ran_any = True
> + if not ran_any:
> + pytest.skip('No partition available for dump')
> +
> + at pytest.mark.buildconfigspec('cmd_mtd')
> + at pytest.mark.buildconfigspec('cmd_bdi')
> + at pytest.mark.buildconfigspec('cmd_memory')
> + at pytest.mark.buildconfigspec('net_legacy', 'net_lwip')
> +def test_mtd_bin_integrity(ubman):
> + """Write a binary blob to flash and byte-compare it back.
> +
> + Fetches the binary over TFTP once, then writes it to every
> + writeable partition that is large enough to hold it. Each write
> + is followed by reading the data back into a different RAM region
> + and comparing byte-for-byte via 'cmp.b'.
> + """
> + bin_cfg = ubman.config.env.get('env__mtd_bin_test')
> + if not bin_cfg:
> + pytest.skip('No env__mtd_bin_test configured')
> + configs = get_configs(ubman)
> +
> + bin_file = bin_cfg.get('bin_file')
> + bin_size = bin_cfg.get('bin_size')
> + tftp_addr = bin_cfg.get('tftp_addr')
> + readback_addr = bin_cfg.get('readback_addr')
> + missing = [
> + key for key, val in (
> + ('bin_file', bin_file),
> + ('bin_size', bin_size),
> + ('tftp_addr', tftp_addr),
> + ('readback_addr', readback_addr),
> + ) if val is None
> + ]
> + if missing:
> + pytest.fail(f'env__mtd_bin_test missing required keys: {missing}')
> +
> + setup_network(ubman)
> + cmd = f'tftpb {tftp_addr:#x} {bin_file}'
> + with ubman.temporary_timeout(get_default_timeout(ubman)):
> + output = ubman.run_command(cmd)
> + if 'TIMEOUT' in output:
> + pytest.fail(f'TFTP timed out fetching {bin_file!r}')
> + if TFTP_DONE not in output:
> + pytest.fail(
> + f'TFTP of {bin_file!r} did not complete, output:\n{output}'
> + )
> +
> + ran_any = False
> + for config in configs:
> + part = mtd_prepare(ubman, config)
> + if part is None:
> + continue
> + if not part['writeable']:
> + ubman.log.info(f'skip {part["name"]!r}: not marked writeable')
> + continue
> + erasesize = part['erasesize']
> + erase_size = ((bin_size + erasesize - 1) // erasesize) * erasesize
> + if erase_size > part['size']:
> + ubman.log.info(
> + f'skip {part["name"]!r}: binary ({bin_size:#x}) does '
> + f'not fit in partition ({part["size"]:#x})'
> + )
> + continue
> +
> + do_erase(ubman, part, 0, erase_size)
> + do_write(ubman, part, tftp_addr, 0, bin_size)
> + # Wipe the readback area so a stale match cannot pass as success.
> + ubman.run_command(f'mw.b {readback_addr:#x} 0x00 {bin_size:#x}')
> + do_read(ubman, part, readback_addr, 0, bin_size)
> + cmp_cmd = f'cmp.b {tftp_addr:#x} {readback_addr:#x} {bin_size:#x}'
> + cmp_out = ubman.run_command(cmp_cmd)
> + assert EXPECTED_COMPARE in cmp_out, (
> + f'Binary mismatch on {part["name"]!r}: cmp.b reported '
> + f'{cmp_out!r}'
> + )
> + ran_any = True
> + if not ran_any:
> + pytest.skip(
> + 'No writeable partition large enough for the binary file'
> + )
More information about the U-Boot
mailing list