From: Simon Glass <simon.glass@canonical.com> Add comprehensive tests for the PXE/extlinux.conf parser APIs. The tests verify that config files can be parsed and label properties inspected without loading kernel/initrd/FDT files. The C test (test/boot/pxe.c) validates all struct pxe_label fields: - String fields: name, menu, kernel, kernel_label, config, append, initrd, fdt, fdtdir, fdtoverlays - Integer fields: ipappend, attempted, localboot, localboot_val, kaslrseed, num The test also verifies struct pxe_menu fields: title, default_label, fallback_label, bmp, timeout, prompt. Parser keywords exercised include: kernel, linux, fit (with #config syntax), append, initrd, fdt, fdtdir, fdtoverlays, localboot, ipappend, kaslrseed, menu title, timeout, prompt, default, fallback, background. The Python wrapper (test/py/tests/test_pxe_parser.py) creates a FAT filesystem image with an extlinux.conf file and passes it to the C test. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/boot/Makefile | 1 + test/boot/pxe.c | 197 +++++++++++++++++++++++++++++++ test/cmd_ut.c | 2 + test/py/tests/test_pxe_parser.py | 182 ++++++++++++++++++++++++++++ 4 files changed, 382 insertions(+) create mode 100644 test/boot/pxe.c create mode 100644 test/py/tests/test_pxe_parser.py diff --git a/test/boot/Makefile b/test/boot/Makefile index 21f533cdc4c..ceb863969dd 100644 --- a/test/boot/Makefile +++ b/test/boot/Makefile @@ -4,6 +4,7 @@ ifdef CONFIG_UT_BOOTSTD obj-$(CONFIG_BOOTSTD) += bootdev.o bootstd_common.o bootflow.o bootmeth.o +obj-$(CONFIG_CMDLINE) += pxe.o obj-$(CONFIG_FIT) += image.o obj-$(CONFIG_$(PHASE_)FIT_PRINT) += fit_print.o obj-$(CONFIG_BLK_LUKS) += luks.o diff --git a/test/boot/pxe.c b/test/boot/pxe.c new file mode 100644 index 00000000000..49d8d160389 --- /dev/null +++ b/test/boot/pxe.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * PXE parser tests - C implementation for Python wrapper + * + * Copyright 2026 Canonical Ltd + * + * These tests verify the extlinux.conf parser APIs. + */ + +#include <dm.h> +#include <env.h> +#include <fs_legacy.h> +#include <mapmem.h> +#include <pxe_utils.h> +#include <test/test.h> +#include <test/ut.h> + +/* Define test macro for pxe suite - no init function needed */ +#define PXE_TEST_ARGS(_name, _flags, ...) \ + UNIT_TEST_ARGS(_name, _flags, pxe, __VA_ARGS__) + +/* Argument indices */ +#define PXE_ARG_FS_IMAGE 0 /* Path to filesystem image */ +#define PXE_ARG_CFG_PATH 1 /* Path to config file within image */ + +/* Memory address for loading files */ +#define PXE_LOAD_ADDR 0x01000000 + +/** + * struct pxe_test_info - context for the test getfile callback + * + * @uts: Unit test state for assertions + */ +struct pxe_test_info { + struct unit_test_state *uts; +}; + +/** + * pxe_test_getfile() - Read a file from the host filesystem + * + * This callback is used by the PXE parser to read included files. + */ +static int pxe_test_getfile(struct pxe_context *ctx, const char *file_path, + ulong *addrp, ulong align, + enum bootflow_img_t type, ulong *sizep) +{ + loff_t len_read; + int ret; + + if (!*addrp) + return -ENOTSUPP; + + ret = fs_set_blk_dev("host", "0:0", FS_TYPE_ANY); + if (ret) + return ret; + ret = fs_legacy_read(file_path, *addrp, 0, 0, &len_read); + if (ret) + return ret; + *sizep = len_read; + + return 0; +} + +/** + * Test parsing an extlinux.conf file + * + * This test: + * 1. Binds a filesystem image containing extlinux.conf + * 2. Parses the config using parse_pxefile() + * 3. Verifies the parsed labels can be inspected + * 4. Verifies label properties are accessible + */ +static int pxe_test_parse_norun(struct unit_test_state *uts) +{ + const char *fs_image = ut_str(PXE_ARG_FS_IMAGE); + const char *cfg_path = ut_str(PXE_ARG_CFG_PATH); + ulong addr = PXE_LOAD_ADDR; + struct pxe_test_info info; + struct pxe_context ctx; + struct pxe_label *label; + struct pxe_menu *cfg; + int ret; + + ut_assertnonnull(fs_image); + ut_assertnonnull(cfg_path); + + info.uts = uts; + + /* Bind the filesystem image */ + ut_assertok(run_commandf("host bind 0 %s", fs_image)); + + /* Set up the PXE context */ + ut_assertok(pxe_setup_ctx(&ctx, pxe_test_getfile, &info, true, cfg_path, + false, false, NULL)); + + /* Read the config file into memory */ + ret = get_pxe_file(&ctx, cfg_path, addr); + ut_asserteq(1, ret); /* get_pxe_file returns 1 on success */ + + /* Parse the config file */ + cfg = parse_pxefile(&ctx, addr); + ut_assertnonnull(cfg); + + /* Verify menu properties */ + ut_asserteq_str("Test Boot Menu", cfg->title); + ut_asserteq_str("linux", cfg->default_label); + ut_asserteq_str("rescue", cfg->fallback_label); + ut_asserteq_str("/boot/background.bmp", cfg->bmp); + ut_asserteq(50, cfg->timeout); + ut_asserteq(1, cfg->prompt); + + /* Verify first label: linux (with fdt, fdtoverlays) */ + label = list_first_entry(&cfg->labels, struct pxe_label, list); + ut_asserteq_str("", label->num); /* only set when menu is built */ + ut_asserteq_str("linux", label->name); + ut_asserteq_str("Boot Linux", label->menu); + ut_asserteq_str("/vmlinuz", label->kernel_label); + ut_asserteq_str("/vmlinuz", label->kernel); + ut_assertnull(label->config); + ut_asserteq_str("root=/dev/sda1 quiet", label->append); + ut_asserteq_str("/initrd.img", label->initrd); + ut_asserteq_str("/dtb/board.dtb", label->fdt); + ut_assertnull(label->fdtdir); + ut_asserteq_str("/dtb/overlay1.dtbo /dtb/overlay2.dtbo", + label->fdtoverlays); + ut_asserteq(0, label->ipappend); + ut_asserteq(0, label->attempted); + ut_asserteq(0, label->localboot); + ut_asserteq(0, label->localboot_val); + ut_asserteq(1, label->kaslrseed); + + /* Verify second label: rescue (linux keyword, fdtdir, ipappend) */ + label = list_entry(label->list.next, struct pxe_label, list); + ut_asserteq_str("", label->num); + ut_asserteq_str("rescue", label->name); + ut_asserteq_str("Rescue Mode", label->menu); + ut_asserteq_str("/vmlinuz-rescue", label->kernel_label); + ut_asserteq_str("/vmlinuz-rescue", label->kernel); + ut_assertnull(label->config); + ut_asserteq_str("single", label->append); + ut_assertnull(label->initrd); + ut_assertnull(label->fdt); + ut_asserteq_str("/dtb/", label->fdtdir); + ut_assertnull(label->fdtoverlays); + ut_asserteq(3, label->ipappend); + ut_asserteq(0, label->attempted); + ut_asserteq(0, label->localboot); + ut_asserteq(0, label->localboot_val); + ut_asserteq(0, label->kaslrseed); + + /* Verify third label: local (localboot only) */ + label = list_entry(label->list.next, struct pxe_label, list); + ut_asserteq_str("", label->num); + ut_asserteq_str("local", label->name); + ut_asserteq_str("Local Boot", label->menu); + ut_assertnull(label->kernel_label); + ut_assertnull(label->kernel); + ut_assertnull(label->config); + ut_assertnull(label->append); + ut_assertnull(label->initrd); + ut_assertnull(label->fdt); + ut_assertnull(label->fdtdir); + ut_assertnull(label->fdtoverlays); + ut_asserteq(0, label->ipappend); + ut_asserteq(0, label->attempted); + ut_asserteq(1, label->localboot); + ut_asserteq(1, label->localboot_val); + ut_asserteq(0, label->kaslrseed); + + /* Verify fourth label: fitboot (fit keyword sets kernel and config) */ + label = list_entry(label->list.next, struct pxe_label, list); + ut_asserteq_str("", label->num); + ut_asserteq_str("fitboot", label->name); + ut_asserteq_str("FIT Boot", label->menu); + ut_asserteq_str("/boot/image.fit#config-1", label->kernel_label); + ut_asserteq_str("/boot/image.fit", label->kernel); + ut_asserteq_str("#config-1", label->config); + ut_asserteq_str("console=ttyS0", label->append); + ut_assertnull(label->initrd); + ut_assertnull(label->fdt); + ut_assertnull(label->fdtdir); + ut_assertnull(label->fdtoverlays); + ut_asserteq(0, label->ipappend); + ut_asserteq(0, label->attempted); + ut_asserteq(0, label->localboot); + ut_asserteq(0, label->localboot_val); + ut_asserteq(0, label->kaslrseed); + + /* Clean up */ + destroy_pxe_menu(cfg); + pxe_destroy_ctx(&ctx); + + return 0; +} +PXE_TEST_ARGS(pxe_test_parse_norun, UTF_CONSOLE | UTF_MANUAL, + { "fs_image", UT_ARG_STR }, + { "cfg_path", UT_ARG_STR }); diff --git a/test/cmd_ut.c b/test/cmd_ut.c index a35ac69434d..f8ff02bdbc7 100644 --- a/test/cmd_ut.c +++ b/test/cmd_ut.c @@ -71,6 +71,7 @@ SUITE_DECL(measurement); SUITE_DECL(mem); SUITE_DECL(optee); SUITE_DECL(pci_mps); +SUITE_DECL(pxe); SUITE_DECL(seama); SUITE_DECL(setexpr); SUITE_DECL(upl); @@ -100,6 +101,7 @@ static struct suite suites[] = { SUITE(mem, "memory-related commands"), SUITE(optee, "OP-TEE"), SUITE(pci_mps, "PCI Express Maximum Payload Size"), + SUITE(pxe, "PXE/extlinux parser tests"), SUITE(seama, "seama command parameters loading and decoding"), SUITE(setexpr, "setexpr command"), SUITE(upl, "Universal payload support"), diff --git a/test/py/tests/test_pxe_parser.py b/test/py/tests/test_pxe_parser.py new file mode 100644 index 00000000000..c9af135b17d --- /dev/null +++ b/test/py/tests/test_pxe_parser.py @@ -0,0 +1,182 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2026 Canonical Ltd + +""" +Test the PXE/extlinux parser APIs + +These tests verify that the extlinux.conf parser can be used independently +to inspect boot labels without loading kernel/initrd/FDT files. + +Tests are implemented in C (test/boot/pxe.c) and called from here. +Python handles filesystem image setup and configuration. +""" + +import os +import pytest + +from fs_helper import FsHelper + + +def create_extlinux_conf(srcdir, labels, menu_opts=None): + """Create an extlinux.conf file with the given labels + + Args: + srcdir (str): Directory to create the extlinux directory in + labels (list): List of dicts with label properties: + - name: Label name (required) + - menu: Menu label text (optional) + - kernel: Kernel path (optional) + - linux: Linux kernel path (alternative to kernel) + - initrd: Initrd path (optional) + - append: Kernel arguments (optional) + - fdt: Device tree path (optional) + - fdtdir: Device tree directory (optional) + - fdtoverlays: Device tree overlays (optional) + - localboot: Local boot flag (optional) + - ipappend: IP append flags (optional) + - fit: FIT config path (optional) + - kaslrseed: Enable KASLR seed (optional) + - default: If True, this is the default label (optional) + menu_opts (dict): Menu-level options: + - title: Menu title + - timeout: Timeout in tenths of a second + - prompt: Prompt flag + - fallback: Fallback label name + - ontimeout: Label to boot on timeout + - background: Background image path + - say: Message to print + - include: File to include + + Returns: + str: Path to the config file relative to srcdir + """ + if menu_opts is None: + menu_opts = {} + + extdir = os.path.join(srcdir, 'extlinux') + os.makedirs(extdir, exist_ok=True) + + conf_path = os.path.join(extdir, 'extlinux.conf') + with open(conf_path, 'w', encoding='ascii') as fd: + # Menu-level options + title = menu_opts.get('title', 'Test Boot Menu') + fd.write(f'menu title {title}\n') + fd.write(f"timeout {menu_opts.get('timeout', 1)}\n") + if 'prompt' in menu_opts: + fd.write(f"prompt {menu_opts['prompt']}\n") + if 'fallback' in menu_opts: + fd.write(f"fallback {menu_opts['fallback']}\n") + if 'ontimeout' in menu_opts: + fd.write(f"ontimeout {menu_opts['ontimeout']}\n") + if 'background' in menu_opts: + fd.write(f"menu background {menu_opts['background']}\n") + if 'say' in menu_opts: + fd.write(f"say {menu_opts['say']}\n") + if 'include' in menu_opts: + fd.write(f"include {menu_opts['include']}\n") + + for label in labels: + if label.get('default'): + fd.write(f"default {label['name']}\n") + + for label in labels: + fd.write(f"\nlabel {label['name']}\n") + if 'menu' in label: + fd.write(f" menu label {label['menu']}\n") + if 'kernel' in label: + fd.write(f" kernel {label['kernel']}\n") + if 'linux' in label: + fd.write(f" linux {label['linux']}\n") + if 'initrd' in label: + fd.write(f" initrd {label['initrd']}\n") + if 'append' in label: + fd.write(f" append {label['append']}\n") + if 'fdt' in label: + fd.write(f" fdt {label['fdt']}\n") + if 'fdtdir' in label: + fd.write(f" fdtdir {label['fdtdir']}\n") + if 'fdtoverlays' in label: + fd.write(f" fdtoverlays {label['fdtoverlays']}\n") + if 'localboot' in label: + fd.write(f" localboot {label['localboot']}\n") + if 'ipappend' in label: + fd.write(f" ipappend {label['ipappend']}\n") + if 'fit' in label: + fd.write(f" fit {label['fit']}\n") + if label.get('kaslrseed'): + fd.write(" kaslrseed\n") + + return '/extlinux/extlinux.conf' + + +@pytest.fixture +def pxe_image(u_boot_config): + """Create a filesystem image with an extlinux.conf file""" + fsh = FsHelper(u_boot_config, 'vfat', 4, prefix='pxe_test') + fsh.setup() + + # Create a simple extlinux.conf with multiple labels + labels = [ + { + 'name': 'linux', + 'menu': 'Boot Linux', + 'kernel': '/vmlinuz', + 'initrd': '/initrd.img', + 'append': 'root=/dev/sda1 quiet', + 'fdt': '/dtb/board.dtb', + 'fdtoverlays': '/dtb/overlay1.dtbo /dtb/overlay2.dtbo', + 'kaslrseed': True, + 'default': True, + }, + { + 'name': 'rescue', + 'menu': 'Rescue Mode', + 'linux': '/vmlinuz-rescue', # test 'linux' keyword + 'append': 'single', + 'fdtdir': '/dtb/', + 'ipappend': '3', + }, + { + 'name': 'local', + 'menu': 'Local Boot', + 'localboot': '1', + }, + { + 'name': 'fitboot', + 'menu': 'FIT Boot', + 'fit': '/boot/image.fit#config-1', + 'append': 'console=ttyS0', + }, + ] + + menu_opts = { + 'title': 'Test Boot Menu', + 'timeout': 50, + 'prompt': 1, + 'fallback': 'rescue', + 'ontimeout': 'linux', + 'background': '/boot/background.bmp', + } + + cfg_path = create_extlinux_conf(fsh.srcdir, labels, menu_opts) + + # Create the filesystem + fsh.mk_fs() + + yield fsh.fs_img, cfg_path + + # Cleanup + if not u_boot_config.persist: + fsh.cleanup() + + +@pytest.mark.boardspec('sandbox') +class TestPxeParser: + """Test PXE/extlinux parser APIs via C unit tests""" + + def test_pxe_parse(self, ubman, pxe_image): + """Test parsing an extlinux.conf and verifying label properties""" + fs_img, cfg_path = pxe_image + with ubman.log.section('Test PXE parse'): + ubman.run_ut('pxe', 'pxe_test_parse', + fs_image=fs_img, cfg_path=cfg_path) -- 2.43.0