[PATCH] test/py: mtd: Add tests for mtd command

Love Kumar love.kumar at amd.com
Tue May 26 09:37:49 CEST 2026


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,
+}
+"""
+
+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'
+        )
-- 
2.23.0



More information about the U-Boot mailing list