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
@@ -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
new file mode 100644
@@ -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 });
@@ -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"),
new file mode 100644
@@ -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)