U-Boot Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] test/py: mtd: Add tests for mtd command
@ 2026-05-26  7:37 Love Kumar
  2026-05-26 16:30 ` Tom Rini
  2026-05-27 13:36 ` Heinrich Schuchardt
  0 siblings, 2 replies; 3+ messages in thread
From: Love Kumar @ 2026-05-26  7:37 UTC (permalink / raw)
  To: u-boot; +Cc: michal.simek, git, trini, sjg

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@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,
+}
+"""
+
+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
+
+@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}'
+            )
+
+@pytest.mark.buildconfigspec('cmd_mtd')
+@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')
+
+@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')
+
+@pytest.mark.buildconfigspec('cmd_mtd')
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_memory')
+@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')
+
+@pytest.mark.buildconfigspec('cmd_mtd')
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_memory')
+@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')
+
+@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')
+
+@pytest.mark.buildconfigspec('cmd_mtd')
+@pytest.mark.buildconfigspec('cmd_bdi')
+@pytest.mark.buildconfigspec('cmd_memory')
+@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'
+        )
-- 
2.23.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH] test/py: mtd: Add tests for mtd command
  2026-05-26  7:37 [PATCH] test/py: mtd: Add tests for mtd command Love Kumar
@ 2026-05-26 16:30 ` Tom Rini
  2026-05-27 13:36 ` Heinrich Schuchardt
  1 sibling, 0 replies; 3+ messages in thread
From: Tom Rini @ 2026-05-26 16:30 UTC (permalink / raw)
  To: Love Kumar; +Cc: u-boot, michal.simek, git, sjg

[-- Attachment #1: Type: text/plain, Size: 3020 bytes --]

On Tue, May 26, 2026 at 01:07:49PM +0530, 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@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

Have you built the documentation and looked at it to make sure it
renders correctly? I ask because:

[snip]
> +"""
> +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,
> +    },
> +]

I recall it being tricky to get this kind of markup to render correctly
and not produce a warning (which is fatal in CI) when doing some of the
other tests. It's possible, it's just a matter of indentation and some
other hinting to the tools that this is an example bit of code, in
essence. Thanks.

-- 
Tom

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH] test/py: mtd: Add tests for mtd command
  2026-05-26  7:37 [PATCH] test/py: mtd: Add tests for mtd command Love Kumar
  2026-05-26 16:30 ` Tom Rini
@ 2026-05-27 13:36 ` Heinrich Schuchardt
  1 sibling, 0 replies; 3+ messages in thread
From: Heinrich Schuchardt @ 2026-05-27 13:36 UTC (permalink / raw)
  To: Love Kumar; +Cc: U-Boot Mailing List, michal.simek, git, trini, sjg

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@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
> +
> +@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}'
> +            )
> +
> +@pytest.mark.buildconfigspec('cmd_mtd')
> +@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')
> +
> +@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')
> +
> +@pytest.mark.buildconfigspec('cmd_mtd')
> +@pytest.mark.buildconfigspec('cmd_bdi')
> +@pytest.mark.buildconfigspec('cmd_memory')
> +@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')
> +
> +@pytest.mark.buildconfigspec('cmd_mtd')
> +@pytest.mark.buildconfigspec('cmd_bdi')
> +@pytest.mark.buildconfigspec('cmd_memory')
> +@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')
> +
> +@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')
> +
> +@pytest.mark.buildconfigspec('cmd_mtd')
> +@pytest.mark.buildconfigspec('cmd_bdi')
> +@pytest.mark.buildconfigspec('cmd_memory')
> +@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'
> +        )


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-05-27 13:36 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-26  7:37 [PATCH] test/py: mtd: Add tests for mtd command Love Kumar
2026-05-26 16:30 ` Tom Rini
2026-05-27 13:36 ` Heinrich Schuchardt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox