{"id":2197005,"url":"http://patchwork.ozlabs.org/api/1.0/patches/2197005/?format=json","project":{"id":18,"url":"http://patchwork.ozlabs.org/api/1.0/projects/18/?format=json","name":"U-Boot","link_name":"uboot","list_id":"u-boot.lists.denx.de","list_email":"u-boot@lists.denx.de","web_url":null,"scm_url":null,"webscm_url":null},"msgid":"<05cc2003b7e0951a80b0078b3824d14466979b3d.1771275704.git.daniel@makrotopia.org>","date":"2026-02-16T21:22:59","name":"[RFC,11/20] test: boot: add image_loader unit tests","commit_ref":null,"pull_url":null,"state":"rfc","archived":false,"hash":"8b1aec9c16d9870a4f85e3df4812a5e43a5bee30","submitter":{"id":64091,"url":"http://patchwork.ozlabs.org/api/1.0/people/64091/?format=json","name":"Daniel Golle","email":"daniel@makrotopia.org"},"delegate":{"id":3651,"url":"http://patchwork.ozlabs.org/api/1.0/users/3651/?format=json","username":"trini","first_name":"Tom","last_name":"Rini","email":"trini@ti.com"},"mbox":"http://patchwork.ozlabs.org/project/uboot/patch/05cc2003b7e0951a80b0078b3824d14466979b3d.1771275704.git.daniel@makrotopia.org/mbox/","series":[{"id":492351,"url":"http://patchwork.ozlabs.org/api/1.0/series/492351/?format=json","date":"2026-02-16T21:21:14","name":"boot: add OpenWrt boot method and on-demand FIT loading","version":1,"mbox":"http://patchwork.ozlabs.org/series/492351/mbox/"}],"check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2197005/checks/","tags":{},"headers":{"Return-Path":"<u-boot-bounces@lists.denx.de>","X-Original-To":"incoming@patchwork.ozlabs.org","Delivered-To":"patchwork-incoming@legolas.ozlabs.org","Authentication-Results":["legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=lists.denx.de\n (client-ip=85.214.62.61; helo=phobos.denx.de;\n envelope-from=u-boot-bounces@lists.denx.de; receiver=patchwork.ozlabs.org)","phobos.denx.de;\n dmarc=none (p=none dis=none) header.from=makrotopia.org","phobos.denx.de;\n spf=pass smtp.mailfrom=u-boot-bounces@lists.denx.de","phobos.denx.de; dmarc=none (p=none dis=none)\n header.from=makrotopia.org","phobos.denx.de;\n spf=pass smtp.mailfrom=daniel@makrotopia.org"],"Received":["from phobos.denx.de (phobos.denx.de [85.214.62.61])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fFG523jpTz1xwD\n\tfor <incoming@patchwork.ozlabs.org>; Tue, 17 Feb 2026 08:24:54 +1100 (AEDT)","from h2850616.stratoserver.net (localhost [IPv6:::1])\n\tby phobos.denx.de (Postfix) with ESMTP id 1799F83C72;\n\tMon, 16 Feb 2026 22:23:43 +0100 (CET)","by phobos.denx.de (Postfix, from userid 109)\n id A48B283D3D; Mon, 16 Feb 2026 22:23:22 +0100 (CET)","from pidgin.makrotopia.org (pidgin.makrotopia.org\n [IPv6:2a07:2ec0:3002::65])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits))\n (No client certificate requested)\n by phobos.denx.de (Postfix) with ESMTPS id 5F4C083DEB\n for <u-boot@lists.denx.de>; Mon, 16 Feb 2026 22:23:19 +0100 (CET)","from local\n by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256)\n (Exim 4.99) (envelope-from <daniel@makrotopia.org>)\n id 1vs63a-000000002kb-2j5o; Mon, 16 Feb 2026 21:23:02 +0000"],"X-Spam-Checker-Version":"SpamAssassin 3.4.2 (2018-09-13) on phobos.denx.de","X-Spam-Level":"","X-Spam-Status":"No, score=-1.9 required=5.0 tests=BAYES_00,\n RCVD_IN_DNSWL_BLOCKED,SPF_HELO_NONE,SPF_PASS autolearn=ham\n autolearn_force=no version=3.4.2","Date":"Mon, 16 Feb 2026 21:22:59 +0000","From":"Daniel Golle <daniel@makrotopia.org>","To":"Tom Rini <trini@konsulko.com>, Simon Glass <sjg@chromium.org>,\n Quentin Schulz <quentin.schulz@cherry.de>,\n Daniel Golle <daniel@makrotopia.org>,\n Kory Maincent <kory.maincent@bootlin.com>,\n Mattijs Korpershoek <mkorpershoek@kernel.org>,\n Martin Schwan <m.schwan@phytec.de>, Anshul Dalal <anshuld@ti.com>,\n Ilias Apalodimas <ilias.apalodimas@linaro.org>,\n Sughosh Ganu <sughosh.ganu@arm.com>, Aristo Chen <jj251510319013@gmail.com>,\n\t=?utf-8?b?54mbIOW/l+Wujw==?= <Zone.Niuzh@hotmail.com>,\n Marek Vasut <marek.vasut+renesas@mailbox.org>,\n Heinrich Schuchardt <xypron.glpk@gmx.de>,\n Wolfgang Wallner <wolfgang.wallner@at.abb.com>,\n Frank Wunderlich <frank-w@public-files.de>,\n David Lechner <dlechner@baylibre.com>,\n Osama Abdelkader <osama.abdelkader@gmail.com>,\n Mikhail Kshevetskiy <mikhail.kshevetskiy@iopsys.eu>,\n Michael Trimarchi <michael@amarulasolutions.com>,\n Miquel Raynal <miquel.raynal@bootlin.com>,\n Andrew Goodbody <andrew.goodbody@linaro.org>,\n Yegor Yefremov <yegorslists@googlemail.com>,\n Mike Looijmans <mike.looijmans@topic.nl>,\n Weijie Gao <weijie.gao@mediatek.com>,\n Alexander Stein <alexander.stein@ew.tq-group.com>,\n Neil Armstrong <neil.armstrong@linaro.org>,\n Mayuresh Chitale <mchitale@ventanamicro.com>,\n Paul HENRYS <paul.henrys_ext@softathome.com>, u-boot@lists.denx.de","Cc":"John Crispin <john@phrozen.org>, Paul Spooren <mail@aparcar.org>","Subject":"[RFC PATCH 11/20] test: boot: add image_loader unit tests","Message-ID":"\n <05cc2003b7e0951a80b0078b3824d14466979b3d.1771275704.git.daniel@makrotopia.org>","References":"<cover.1771275704.git.daniel@makrotopia.org>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<cover.1771275704.git.daniel@makrotopia.org>","X-Mailman-Approved-At":"Mon, 16 Feb 2026 22:23:42 +0100","X-BeenThere":"u-boot@lists.denx.de","X-Mailman-Version":"2.1.39","Precedence":"list","List-Id":"U-Boot discussion <u-boot.lists.denx.de>","List-Unsubscribe":"<https://lists.denx.de/options/u-boot>,\n <mailto:u-boot-request@lists.denx.de?subject=unsubscribe>","List-Archive":"<https://lists.denx.de/pipermail/u-boot/>","List-Post":"<mailto:u-boot@lists.denx.de>","List-Help":"<mailto:u-boot-request@lists.denx.de?subject=help>","List-Subscribe":"<https://lists.denx.de/listinfo/u-boot>,\n <mailto:u-boot-request@lists.denx.de?subject=subscribe>","Errors-To":"u-boot-bounces@lists.denx.de","Sender":"\"U-Boot\" <u-boot-bounces@lists.denx.de>","X-Virus-Scanned":"clamav-milter 0.103.8 at phobos.denx.de","X-Virus-Status":"Clean"},"content":"Add unit tests for the image_loader framework covering its core\nlogic with a mock storage backend:\n\n- map() allocates, reads and records a region\n- map() returns cached pointer for already-mapped range\n- map() returns correct offset within a larger region\n- map() re-reads when extending a region to a larger size\n- map_to() reads to a specified address and records it\n- lookup() returns NULL for unmapped ranges\n- alloc_ptr advances with correct alignment\n- map() returns NULL when the translation table is full\n- cleanup() calls backend and resets state\n- map() with multiple disjoint regions\n- read beyond image size returns error\n\nAlso fix IMAGE_LOADER_MAX_REGIONS Kconfig to depend on IMAGE_LOADER\nand default to 16 unconditionally (the previous 'default 0' fallback\ncaused the regions array to be zero-sized when IMAGE_LOADER was\nenabled after initial defconfig generation).\n\nRegister the new 'image_loader' test suite in test/cmd_ut.c so it\ncan be run via 'ut image_loader'.\n\nSigned-off-by: Daniel Golle <daniel@makrotopia.org>\n---\n boot/Kconfig             |   4 +-\n test/boot/Makefile       |   2 +\n test/boot/image_loader.c | 429 +++++++++++++++++++++++++++++++++++++++\n test/cmd_ut.c            |   2 +\n 4 files changed, 435 insertions(+), 2 deletions(-)\n create mode 100644 test/boot/image_loader.c","diff":"diff --git a/boot/Kconfig b/boot/Kconfig\nindex 1f870c7d251..efc06f3cd1a 100644\n--- a/boot/Kconfig\n+++ b/boot/Kconfig\n@@ -1179,8 +1179,8 @@ config IMAGE_LOADER\n \n config IMAGE_LOADER_MAX_REGIONS\n \tint \"Maximum number of mapped regions in image loader\"\n-\tdefault 16 if IMAGE_LOADER\n-\tdefault 0\n+\tdepends on IMAGE_LOADER\n+\tdefault 16\n \thelp\n \t  Maximum number of distinct image regions that can be mapped\n \t  into RAM simultaneously. 16 is sufficient for typical FIT\ndiff --git a/test/boot/Makefile b/test/boot/Makefile\nindex 89538d4f0a6..6fd349a65bc 100644\n--- a/test/boot/Makefile\n+++ b/test/boot/Makefile\n@@ -23,3 +23,5 @@ endif\n obj-$(CONFIG_BOOTMETH_VBE) += vbe_fixup.o\n \n obj-$(CONFIG_UPL) += upl.o\n+\n+obj-$(CONFIG_IMAGE_LOADER) += image_loader.o\ndiff --git a/test/boot/image_loader.c b/test/boot/image_loader.c\nnew file mode 100644\nindex 00000000000..dc4b0b4173a\n--- /dev/null\n+++ b/test/boot/image_loader.c\n@@ -0,0 +1,429 @@\n+// SPDX-License-Identifier: GPL-2.0+\n+/*\n+ * Tests for image_loader framework\n+ *\n+ * Copyright (C) 2026 Daniel Golle <daniel@makrotopia.org>\n+ */\n+\n+#include <image-loader.h>\n+#include <mapmem.h>\n+#include <malloc.h>\n+#include <asm/cache.h>\n+#include <test/test.h>\n+#include <test/ut.h>\n+\n+#define IMG_LOADER_TEST(_name, _flags) \\\n+\tUNIT_TEST(_name, _flags, image_loader)\n+\n+/* Synthetic image size used throughout the tests */\n+#define IMAGE_SIZE\t4096\n+\n+/**\n+ * struct mock_priv - private data for the mock storage backend\n+ *\n+ * @image:\tpointer to synthetic image data in RAM\n+ * @image_size:\tsize of the synthetic image\n+ * @read_count:\tnumber of times .read() was called\n+ * @last_off:\toffset from the most recent .read() call\n+ * @last_size:\tsize from the most recent .read() call\n+ */\n+struct mock_priv {\n+\tconst void *image;\n+\tsize_t image_size;\n+\tint read_count;\n+\tulong last_off;\n+\tulong last_size;\n+};\n+\n+static int mock_read(struct image_loader *ldr, ulong src, ulong size,\n+\t\t     void *dst)\n+{\n+\tstruct mock_priv *p = ldr->priv;\n+\n+\tif (src + size > p->image_size)\n+\t\treturn -EINVAL;\n+\n+\tmemcpy(dst, (const char *)p->image + src, size);\n+\tp->read_count++;\n+\tp->last_off = src;\n+\tp->last_size = size;\n+\n+\treturn 0;\n+}\n+\n+static void mock_cleanup(struct image_loader *ldr)\n+{\n+\t/* Nothing dynamic to free — just verify it's called */\n+}\n+\n+/**\n+ * init_mock_loader() - set up a loader with the mock backend\n+ *\n+ * @ldr:\tloader to initialise\n+ * @priv:\tmock private data (caller-allocated)\n+ * @image:\tsynthetic image buffer\n+ * @image_size:\tsize of @image\n+ * @alloc_base:\tRAM address to use as alloc_ptr base\n+ */\n+static void init_mock_loader(struct image_loader *ldr, struct mock_priv *priv,\n+\t\t\t     const void *image, size_t image_size,\n+\t\t\t     ulong alloc_base)\n+{\n+\tmemset(ldr, 0, sizeof(*ldr));\n+\tmemset(priv, 0, sizeof(*priv));\n+\n+\tpriv->image = image;\n+\tpriv->image_size = image_size;\n+\n+\tldr->read = mock_read;\n+\tldr->cleanup = mock_cleanup;\n+\tldr->priv = priv;\n+\tldr->alloc_ptr = alloc_base;\n+}\n+\n+/* Test: map() allocates, reads and records a region */\n+static int image_loader_test_map_basic(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p;\n+\n+\t/* Fill image with a recognisable pattern */\n+\tfor (int i = 0; i < IMAGE_SIZE; i++)\n+\t\timage[i] = (u8)(i & 0xff);\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Map a 64-byte region at offset 0 */\n+\tp = image_loader_map(&ldr, 0, 64);\n+\tut_assertnonnull(p);\n+\tut_asserteq_mem(image, p, 64);\n+\tut_asserteq(1, mock.read_count);\n+\tut_asserteq(1, ldr.nr_regions);\n+\tut_asserteq(0, (int)ldr.regions[0].img_offset);\n+\tut_asserteq(64, (int)ldr.regions[0].size);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_map_basic, 0);\n+\n+/* Test: map() returns cached pointer for already-mapped range */\n+static int image_loader_test_map_cached(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p1, *p2;\n+\n+\tmemset(image, 0xaa, IMAGE_SIZE);\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\tp1 = image_loader_map(&ldr, 0, 128);\n+\tut_assertnonnull(p1);\n+\tut_asserteq(1, mock.read_count);\n+\n+\t/* Same range — should return same pointer, no new read */\n+\tp2 = image_loader_map(&ldr, 0, 128);\n+\tut_asserteq_ptr(p1, p2);\n+\tut_asserteq(1, mock.read_count);\n+\n+\t/* Subset of the already-mapped range — still cached */\n+\tp2 = image_loader_map(&ldr, 0, 64);\n+\tut_asserteq_ptr(p1, p2);\n+\tut_asserteq(1, mock.read_count);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_map_cached, 0);\n+\n+/* Test: map() returns correct offset within a larger region */\n+static int image_loader_test_map_offset(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p1, *p2;\n+\n+\tfor (int i = 0; i < IMAGE_SIZE; i++)\n+\t\timage[i] = (u8)(i & 0xff);\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Map a 256-byte region starting at offset 0 */\n+\tp1 = image_loader_map(&ldr, 0, 256);\n+\tut_assertnonnull(p1);\n+\n+\t/* Request a sub-range within the previously mapped region */\n+\tp2 = image_loader_map(&ldr, 64, 64);\n+\tut_assertnonnull(p2);\n+\t/* p2 should point 64 bytes into p1 */\n+\tut_asserteq_ptr((char *)p1 + 64, p2);\n+\tut_asserteq_mem(image + 64, p2, 64);\n+\t/* Only one read should have occurred */\n+\tut_asserteq(1, mock.read_count);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_map_offset, 0);\n+\n+/* Test: map() re-reads when extending a region to a larger size */\n+static int image_loader_test_map_extend(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p1, *p2;\n+\n+\tfor (int i = 0; i < IMAGE_SIZE; i++)\n+\t\timage[i] = (u8)(i & 0xff);\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Initial small mapping */\n+\tp1 = image_loader_map(&ldr, 0, 64);\n+\tut_assertnonnull(p1);\n+\tut_asserteq(1, mock.read_count);\n+\n+\t/* Request larger range at same base — should re-read (extend) */\n+\tp2 = image_loader_map(&ldr, 0, 256);\n+\tut_assertnonnull(p2);\n+\tut_asserteq(2, mock.read_count);\n+\tut_asserteq_ptr(p1, p2);\t/* same RAM base */\n+\tut_asserteq_mem(image, p2, 256);\n+\n+\t/* Region count should still be 1 (updated, not added) */\n+\tut_asserteq(1, ldr.nr_regions);\n+\tut_asserteq(256, (int)ldr.regions[0].size);\n+\n+\t/* alloc_ptr must have advanced past the extended region */\n+\tut_assert(ldr.alloc_ptr >= 0x1000000 + 256);\n+\n+\t/*\n+\t * Map a new region after the extend — it must not overlap the\n+\t * extended first region. This is the exact pattern that bit us\n+\t * on real hardware: FIT header at offset 0 extended from 64 to\n+\t * 4096, then kernel payload at offset 4096 was allocated at\n+\t * alloc_ptr that hadn't been advanced, clobbering the header.\n+\t */\n+\t{\n+\t\tvoid *p3 = image_loader_map(&ldr, 256, 128);\n+\n+\t\tut_assertnonnull(p3);\n+\t\tut_asserteq(2, ldr.nr_regions);\n+\t\t/* New region must start at or after the extended region end */\n+\t\tut_assert((ulong)map_to_sysmem(p3) >= 0x1000000 + 256);\n+\t\tut_asserteq_mem(image + 256, p3, 128);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_map_extend, 0);\n+\n+/* Test: map_to() reads to a specified address and records it */\n+static int image_loader_test_map_to(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tu8 dst[256];\n+\tvoid *p;\n+\n+\tfor (int i = 0; i < IMAGE_SIZE; i++)\n+\t\timage[i] = (u8)(i & 0xff);\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\tp = image_loader_map_to(&ldr, 128, 256, dst);\n+\tut_asserteq_ptr(dst, p);\n+\tut_asserteq_mem(image + 128, dst, 256);\n+\tut_asserteq(1, mock.read_count);\n+\tut_asserteq(1, ldr.nr_regions);\n+\tut_asserteq(128, (int)ldr.regions[0].img_offset);\n+\tut_asserteq(256, (int)ldr.regions[0].size);\n+\tut_asserteq_ptr(dst, ldr.regions[0].ram);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_map_to, 0);\n+\n+/* Test: lookup() returns NULL for unmapped ranges */\n+static int image_loader_test_lookup_miss(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p;\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Nothing mapped yet — should return NULL */\n+\tp = image_loader_lookup(&ldr, 0, 64);\n+\tut_assertnull(p);\n+\n+\t/* Map a region at offset 0 */\n+\tut_assertnonnull(image_loader_map(&ldr, 0, 64));\n+\n+\t/* Lookup within the mapped region — should succeed */\n+\tp = image_loader_lookup(&ldr, 0, 32);\n+\tut_assertnonnull(p);\n+\n+\t/* Lookup at a different offset — should miss */\n+\tp = image_loader_lookup(&ldr, 128, 32);\n+\tut_assertnull(p);\n+\n+\t/* Lookup extending beyond the mapped region — should miss */\n+\tp = image_loader_lookup(&ldr, 0, 128);\n+\tut_assertnull(p);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_lookup_miss, 0);\n+\n+/* Test: alloc_ptr advances with correct alignment */\n+static int image_loader_test_alloc_advance(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tulong base = 0x1000000;\n+\tulong expected;\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, base);\n+\n+\t/* Map 100 bytes — alloc_ptr should advance to ALIGN(base + 100) */\n+\tut_assertnonnull(image_loader_map(&ldr, 0, 100));\n+\texpected = ALIGN(base + 100, ARCH_DMA_MINALIGN);\n+\tut_asserteq(expected, ldr.alloc_ptr);\n+\n+\t/* Map another 200 bytes at a different offset */\n+\tut_assertnonnull(image_loader_map(&ldr, 200, 200));\n+\texpected = ALIGN(expected + 200, ARCH_DMA_MINALIGN);\n+\tut_asserteq(expected, ldr.alloc_ptr);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_alloc_advance, 0);\n+\n+/* Test: map() returns NULL when the translation table is full */\n+static int image_loader_test_table_full(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p;\n+\tint i;\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Fill all region slots with distinct offsets */\n+\tfor (i = 0; i < CONFIG_IMAGE_LOADER_MAX_REGIONS; i++) {\n+\t\tp = image_loader_map(&ldr, i * 16, 8);\n+\t\tut_assertnonnull(p);\n+\t}\n+\n+\tut_asserteq(CONFIG_IMAGE_LOADER_MAX_REGIONS, ldr.nr_regions);\n+\n+\t/* Next map at a new offset should fail (table full) */\n+\tp = image_loader_map(&ldr, CONFIG_IMAGE_LOADER_MAX_REGIONS * 16, 8);\n+\tut_assertnull(p);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_table_full, 0);\n+\n+/* Test: cleanup() calls the backend and resets state */\n+static int image_loader_test_cleanup(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Map something so nr_regions > 0 */\n+\tut_assertnonnull(image_loader_map(&ldr, 0, 64));\n+\tut_asserteq(1, ldr.nr_regions);\n+\n+\timage_loader_cleanup(&ldr);\n+\n+\tut_assertnull(ldr.read);\n+\tut_assertnull(ldr.cleanup);\n+\tut_assertnull(ldr.priv);\n+\tut_asserteq(0, ldr.nr_regions);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_cleanup, 0);\n+\n+/* Test: map() with multiple disjoint regions */\n+static int image_loader_test_multi_region(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p1, *p2, *p3;\n+\n+\tfor (int i = 0; i < IMAGE_SIZE; i++)\n+\t\timage[i] = (u8)(i & 0xff);\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\tp1 = image_loader_map(&ldr, 0, 64);\n+\tut_assertnonnull(p1);\n+\tp2 = image_loader_map(&ldr, 512, 128);\n+\tut_assertnonnull(p2);\n+\tp3 = image_loader_map(&ldr, 1024, 256);\n+\tut_assertnonnull(p3);\n+\n+\tut_asserteq(3, ldr.nr_regions);\n+\tut_asserteq(3, mock.read_count);\n+\n+\t/* Verify data in each region */\n+\tut_asserteq_mem(image, p1, 64);\n+\tut_asserteq_mem(image + 512, p2, 128);\n+\tut_asserteq_mem(image + 1024, p3, 256);\n+\n+\t/* Lookup each region */\n+\tut_asserteq_ptr(p1, image_loader_lookup(&ldr, 0, 64));\n+\tut_asserteq_ptr(p2, image_loader_lookup(&ldr, 512, 128));\n+\tut_asserteq_ptr(p3, image_loader_lookup(&ldr, 1024, 256));\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_multi_region, 0);\n+\n+/* Test: read beyond image size returns error */\n+static int image_loader_test_read_oob(struct unit_test_state *uts)\n+{\n+\tstruct image_loader ldr;\n+\tstruct mock_priv mock;\n+\tu8 image[IMAGE_SIZE];\n+\tvoid *p;\n+\n+\tinit_mock_loader(&ldr, &mock, image, IMAGE_SIZE, 0x1000000);\n+\n+\t/* Attempt to map beyond the end of the image */\n+\tp = image_loader_map(&ldr, IMAGE_SIZE - 32, 64);\n+\tut_assertnull(p);\n+\n+\t/* map_to should also fail */\n+\tu8 dst[64];\n+\n+\tp = image_loader_map_to(&ldr, IMAGE_SIZE - 32, 64, dst);\n+\tut_assertnull(p);\n+\n+\treturn 0;\n+}\n+\n+IMG_LOADER_TEST(image_loader_test_read_oob, 0);\ndiff --git a/test/cmd_ut.c b/test/cmd_ut.c\nindex 44e5fdfdaa6..3b907a12e4e 100644\n--- a/test/cmd_ut.c\n+++ b/test/cmd_ut.c\n@@ -71,6 +71,7 @@ SUITE_DECL(optee);\n SUITE_DECL(pci_mps);\n SUITE_DECL(seama);\n SUITE_DECL(setexpr);\n+SUITE_DECL(image_loader);\n SUITE_DECL(upl);\n \n static struct suite suites[] = {\n@@ -98,6 +99,7 @@ static struct suite suites[] = {\n \tSUITE(pci_mps, \"PCI Express Maximum Payload Size\"),\n \tSUITE(seama, \"seama command parameters loading and decoding\"),\n \tSUITE(setexpr, \"setexpr command\"),\n+\tSUITE(image_loader, \"image_loader on-demand storage loading\"),\n \tSUITE(upl, \"Universal payload support\"),\n };\n \n","prefixes":["RFC","11/20"]}