[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH 1/4] tools/tests/alloc: Unit and Integration Test Framework for page_alloc.c



Add a test framefork for unit and integration test suites testing
the Xen page allocator module xen/common/page_alloc.c in isolation.

It enables test suites to verify the behaviour of the page allocator
in situations that are easier to create and validate in isolation,
with full control over a synthetic Xen heap state and visibility
into the allocator and domain state.

Signed-off-by: Bernhard Kaindl <bernhard.kaindl@xxxxxxxxxx>
---
 tools/tests/Makefile                   |   1 +
 tools/tests/alloc/.gitignore           |   6 +
 tools/tests/alloc/Makefile             | 141 ++++++++
 tools/tests/alloc/README.rst           |  31 ++
 tools/tests/alloc/check-asserts.h      | 347 ++++++++++++++++++
 tools/tests/alloc/harness.h            |  69 ++++
 tools/tests/alloc/hypervisor-macros.h  | 101 ++++++
 tools/tests/alloc/libtest-page_alloc.h | 356 +++++++++++++++++++
 tools/tests/alloc/mock-page_list.h     | 307 ++++++++++++++++
 tools/tests/alloc/page_alloc-wrapper.h | 465 +++++++++++++++++++++++++
 tools/tests/alloc/page_alloc_shim.h    | 433 +++++++++++++++++++++++
 11 files changed, 2257 insertions(+)
 create mode 100644 tools/tests/alloc/.gitignore
 create mode 100644 tools/tests/alloc/Makefile
 create mode 100644 tools/tests/alloc/README.rst
 create mode 100644 tools/tests/alloc/check-asserts.h
 create mode 100644 tools/tests/alloc/harness.h
 create mode 100644 tools/tests/alloc/hypervisor-macros.h
 create mode 100644 tools/tests/alloc/libtest-page_alloc.h
 create mode 100644 tools/tests/alloc/mock-page_list.h
 create mode 100644 tools/tests/alloc/page_alloc-wrapper.h
 create mode 100644 tools/tests/alloc/page_alloc_shim.h

diff --git a/tools/tests/Makefile b/tools/tests/Makefile
index 6477a4386dda..ca3de4c7b54a 100644
--- a/tools/tests/Makefile
+++ b/tools/tests/Makefile
@@ -2,6 +2,7 @@ XEN_ROOT = $(CURDIR)/../..
 include $(XEN_ROOT)/tools/Rules.mk
 
 SUBDIRS-y :=
+SUBDIRS-y += alloc
 SUBDIRS-y += domid
 SUBDIRS-y += mem-claim
 SUBDIRS-y += paging-mempool
diff --git a/tools/tests/alloc/.gitignore b/tools/tests/alloc/.gitignore
new file mode 100644
index 000000000000..9f77c5879772
--- /dev/null
+++ b/tools/tests/alloc/.gitignore
@@ -0,0 +1,6 @@
+/test-claims_basic
+/test-claims_numa_install
+/test-claims_numa_redeem
+/test-online_page
+/test-offlining-claims
+/test-reserve_offline_page
diff --git a/tools/tests/alloc/Makefile b/tools/tests/alloc/Makefile
new file mode 100644
index 000000000000..f5724aa3f699
--- /dev/null
+++ b/tools/tests/alloc/Makefile
@@ -0,0 +1,141 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Makefile for tools/tests/alloc
+
+XEN_ROOT := $(abspath $(CURDIR)/../../..)
+include $(XEN_ROOT)/tools/Rules.mk
+RELDIR := $(subst $(XEN_ROOT)/,,$(CURDIR))
+
+TEST_SOURCES := $(notdir $(wildcard test-*.c))
+TARGETS := $(TEST_SOURCES:.c=)
+
+.PHONY: all
+all: $(TARGETS)
+
+define RUN_TESTS
+       @echo "Build configuration:"
+       @echo "CC=$(CC)"
+       @echo "CFLAGS='$(CFLAGS)'"
+       @for test in $? ; do \
+               echo;echo "$(RELDIR): RUN_TESTS: $$test...";echo; \
+               ./$$test ; EXIT_CODE=$$? ; \
+               if [ $$EXIT_CODE -ne 0 ]; then \
+                       echo "Test $$test failed with exit code $$EXIT_CODE"; \
+                       exit 1; \
+               fi; \
+       done
+       @echo
+       @echo "Tests executed successfully:"
+       @for test in $? ; do \
+               echo "  - $$test"; \
+       done
+endef
+
+# Run the tests if possible, otherwise print a warning and skip them.
+.PHONY: run
+# Determine if the tests can be run on the build host. If CC and HOSTCC
+# are the same, we can run the tests directly. If they differ, we check
+# if binfmt-support and qemu-binfmt are available to run the tests under
+# using binfmt-misc using qemu-user-static.
+ifeq ($(CC),$(HOSTCC))
+    TESTS_RUNNABLE=yes
+else
+    BINFMT_SUPP := $(if $(wildcard /etc/init.d/binfmt-support),1,0)
+    QEMU_BINFMT := $(if $(wildcard /usr/libexec/qemu-binfmt),1,0)
+    ifeq ($(BINFMT_SUPP)$(QEMU_BINFMT),11)
+        # Running static binaries doesn't need extra setup besides qemu-binfmt
+        CFLAGS += -static
+        TESTS_RUNNABLE=yes
+    else
+        TESTS_RUNNABLE=no
+    endif
+endif
+
+run: $(TARGETS)
+ifeq ($(TESTS_RUNNABLE),yes)
+       $(RUN_TESTS)
+else
+       $(warning HOSTCC != CC, and qemu-binfmt not detected, skipping alloc 
tests)
+endif
+
+# Run the tests  binfmt-misc set up
+BINFMT_SUPP := $(if $(wildcard /etc/init.d/binfmt-support),1,0)
+QEMU_BINFMT := $(if $(wildcard /usr/libexec/qemu-binfmt),1,0)
+.PHONY: run-tests
+run-tests: $(TARGETS)
+ifeq ($(CC),$(HOSTCC))
+       $(RUN_TESTS)
+else ifeq ($(BINFMT_SUPP)$(QEMU_BINFMT),11)
+       $(RUN_TESTS)
+else
+       $(warning Note: binfmt-support or qemu-user not found, skipping 
run-tests)
+endif
+
+#
+# Build and run the tests for multiple architectures,
+# skipping if the appropriate cross-compiler is not found.
+# The default XEN_TARGET_ARCH is always built and tested as well.
+# This is gcc-specific, but can be adapted for other toolchains.
+#
+ARCHS := arm64-aarch64-linux-gnu arm32-arm-linux-gnueabihf
+ARCHS += x86_32-i686-linux-gnu x86_64-x86_64-linux-gnu
+ARCHS += ppc64-powerpc64le-linux-gnu riscv64-riscv64-linux-gnu
+.PHONY: run-archs
+run-archs: $(TARGETS)
+ifneq ($(CC),gcc)
+       $(warning run-archs target is only supported with CC=gcc for now, 
skipping)
+else
+       @set -e;PASSES=;SKIPPED_ARCHS=; \
+       MAKEFLAGS="$$MAKEFLAGS --no-print-directory"; \
+
+       for t in $(ARCHS); do \
+               A=$${t%%-*}; C=$${t#*-}; \
+           [ $$A != $(XEN_TARGET_ARCH) ] || continue; \
+               if ! type "$${C}-gcc" >/dev/null 2>&1; then \
+               echo " $${C}-gcc not found, skipping $${A}"; \
+                       SKIPPED_ARCHS="$${SKIPPED_ARCHS} $${A}"; continue; \
+           fi; \
+               if [ $${A} = $(XEN_TARGET_ARCH) ]; then C=$(CROSS_COMPILE); fi; 
\
+               make XEN_TARGET_ARCH="$${A}" CROSS_COMPILE=$$C- clean 
run-tests; \
+               PASSES="$${PASSES} $${A}"; \
+       done;\
+       echo "$@ successful for:$${PASSES} $(XEN_TARGET_ARCH)";\
+       [ -z "$${SKIPPED_ARCHS}" ] || echo "Skipped 
architectures:$${SKIPPED_ARCHS}"
+endif
+
+.PHONY: clean
+.NOTPARALLEL: clean
+clean:
+       $(RM) -- *.o $(TARGETS) $(DEPS_RM)
+
+.PHONY: distclean
+distclean: clean
+       $(RM) -- *~
+
+.PHONY: install
+install: all
+       $(INSTALL_DIR) $(DESTDIR)$(LIBEXEC)/tests
+       $(INSTALL_PROG) $(TARGETS) $(DESTDIR)$(LIBEXEC)/tests
+
+.PHONY: uninstall
+uninstall:
+       $(RM) -- $(patsubst %,$(DESTDIR)$(LIBEXEC)/tests/%,$(TARGETS))
+
+# CFLAGS for building the tests
+XEN_INCLUDE_ARCH := $(subst x86_64,x86,$(XEN_COMPILE_ARCH))
+CFLAGS += -D__XEN_TOOLS__
+CFLAGS += $(APPEND_CFLAGS)
+CFLAGS += -I$(XEN_ROOT)/xen/include
+CFLAGS += -I$(XEN_ROOT)/xen/arch/$(XEN_INCLUDE_ARCH)/include
+CFLAGS += $(CFLAGS_xeninclude)
+
+# Enable sanitizers to catch memory errors and undefined behavior in the code
+# for x86_64. Other architectures do not support -fstatic with it.
+ifeq ($(XEN_TARGET_ARCH),x86_64)
+CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
+endif
+
+# Build rules for the tests
+$(TARGETS): %: %.o $(LIB_OBJ)
+       $(CC) -o $@ $^ $(LDFLAGS) $(CPPFLAGS) $(CFLAGS) $(APPEND_CFLAGS)
+
+-include $(DEPS_INCLUDE)
diff --git a/tools/tests/alloc/README.rst b/tools/tests/alloc/README.rst
new file mode 100644
index 000000000000..3ed362598bb3
--- /dev/null
+++ b/tools/tests/alloc/README.rst
@@ -0,0 +1,31 @@
+.. SPDX-License-Identifier: CC-BY-4.0
+
+Unit and Integration test suite for the page allocator
+======================================================
+
+The tests in ``tools/tests/alloc`` contain unit tests for the
+Xen page allocator in ``xen/common/page_alloc.c`` and are
+built as standalone executables.
+
+They are not intended to be run in a Xen environment, but rather to test
+the allocator logic in isolation and can be run on any compatible host
+system at build time and do not use any installed libraries or require
+any special setup or dependencies beyond the standard C library.
+
+The tests use a shim as a substitute for Xen hypervisor code that would
+conflict with running the page allocator as a host executable, and they
+use helper functions to initialize and assert the status of the data
+structures of the allocator such as the page lists and zones.
+
+The tests can be run with the ``run`` target of the ``Makefile``, which
+will execute all the test executables and report their results unless
+you override the TARGETS variable to run a specific test:
+
+.. code:: shell
+
+    make -C tools/tests/alloc clean all run \
+            TARGETS=test-reserve_offline_page-uma
+
+To add a new test, simply create a new C file with a name starting with
+``test-``, implement the test logic, and it will be automatically included
+in the build and run targets by default.
diff --git a/tools/tests/alloc/check-asserts.h 
b/tools/tests/alloc/check-asserts.h
new file mode 100644
index 000000000000..04a254c0999d
--- /dev/null
+++ b/tools/tests/alloc/check-asserts.h
@@ -0,0 +1,347 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Header-only library for check assertions in unit tests.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _CHECK_ASSERTS_H_
+#define _CHECK_ASSERTS_H_
+
+#include <assert.h>
+#include <execinfo.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/utsname.h>
+
+#ifndef CONFIG_NUMA
+#define CONFIG_NUMA 0
+#endif
+#define __used __attribute__((__used__))
+
+/** ## Global state for the test framework and assertions */
+
+/** Set when assertions are expected to fail */
+static bool testcase_assert_expected_to_fail = false;
+/** Set when verbose assertions are enabled */
+static bool testcase_assert_verbose_assertions = true;
+/**
+ * The current function for verbose assertions, used to avoid repeating the
+ * function name in the logs for multiple assertions within the same function.
+ */
+static const char *testcase_assert_current_func = NULL;
+/** The current indentation level for verbose assertions */
+static int testcase_assert_verbose_indent_level = 0;
+/** Failed checks since the last call call to EXPECTED_TO_FAIL_BEGIN() */
+static int testcase_assert_expected_failures = 0;
+/** Failed checks within EXPECTED_TO_FAIL_BEGIN()/END() in this test case */
+static int testcase_assert_expected_failures_total = 0;
+/** Successful assertions in this test case */
+static int testcase_assert_successful_assert_total = 0;
+#define assert_failed_str "Assertion failed: "
+
+/** ## Assertion macros and helpers */
+
+/** Check a condition and log the result with context. */
+#define CHECK(condition, fmt, ...)                                    \
+        testcase_assert(condition, __FILE__, __LINE__, __func__, fmt, \
+                        ##__VA_ARGS__)
+
+/** If the condition is false, treat it as a test assertion failure */
+#define ASSERT(x) \
+        testcase_assert(x, __FILE__, __LINE__, __func__, assert_failed_str #x)
+
+/** If the condition is true, treat it as a bug, used in Xen hypervisor code */
+#define BUG_ON(x) \
+        testcase_assert(!(x), __FILE__, __LINE__, __func__, "BUG_ON: " #x)
+
+/** Assert that the code is unreachable */
+#define ASSERT_UNREACHABLE() assert(false)
+
+/** ## Helpers for expected assertion failures */
+
+/** Marks the beginning of a block where assertions are expected to fail */
+#define EXPECTED_TO_FAIL_BEGIN() (testcase_assert_expected_to_fail = true)
+/** Marks the end of a block where assertions are expected to fail */
+#define EXPECTED_TO_FAIL_END(c) testcase_assert_check_expected_failures(c)
+
+/** Checks the number of expected failures against the actual count */
+static void __used testcase_assert_check_expected_failures(int expected)
+{
+    if ( testcase_assert_expected_failures != expected )
+    {
+        fprintf(stderr, "Test assertion expected %d failures, but got %d\n",
+                expected, testcase_assert_expected_failures);
+        abort();
+    }
+    testcase_assert_expected_to_fail = false;
+    testcase_assert_expected_failures = 0;
+    testcase_assert_expected_failures_total += expected;
+}
+
+/** ## Test case management and reporting */
+
+/** Function pointer used for initializing a test case */
+static void (*testcase_init_func)(const char *, int);
+
+/** Set up the function pointer for initializing a test case */
+static void __used setup_testcase_init_func(void (*init_fn)(const char *, int))
+{
+    testcase_init_func = init_fn;
+}
+
+/**
+ * Assert a condition within a test case
+ *
+ * This function is the core of the assertion mechanism for test cases.
+ * It checks a given condition and handles both expected and unexpected
+ * assertion failures.
+ *
+ * If the assertion is expected to fail, it logs the failure and increments
+ * the expected failure count.  If the assertion is not expected to fail but
+ * does, it logs the failure and aborts the test.  If the assertion passes,
+ * it increments the successful assertion count and optionally logs the
+ * successful assertion if verbose assertions are enabled.
+ *
+ * Args:
+ *  condition (bool):    The condition to check.  If false, the assertion 
fails.
+ *  file (const char *): The file where the assertion is located, for logging.
+ *  line (int):          The source line of the assertion, for logging.
+ *  func (const char *): The function name where the assertion is located.
+ *  fmt (const char *):  A printf format string with context for the assertion.
+ *  ...:                 Additional arguments for the format string.
+ */
+static void testcase_assert(bool condition, const char *file, int line,
+                            const char *func, const char *fmt, ...)
+{
+    va_list ap;
+    const char *relpath = file;
+
+    while ( (file = strstr(relpath, "../")) )
+        relpath += 3;
+
+    va_start(ap, fmt);
+    if ( testcase_assert_expected_to_fail )
+    {
+        fprintf(stderr, "\n- Test assertion %s at %s:%d:\n  ",
+                condition ? "unexpectedly passed" : "expectedly failed",
+                relpath, line);
+        vfprintf(stderr, fmt, ap);
+        va_end(ap);
+        fprintf(stderr, "\n");
+
+        if ( condition )
+            abort(); /* Unexpected pass, treat as test failure */
+        else
+            testcase_assert_expected_failures++; /* update for the report */
+        return;
+    }
+    if ( !condition )
+    {
+        fprintf(stderr, "Test assertion failed at %s:%d: ", relpath, line);
+        vfprintf(stderr, fmt, ap);
+        fprintf(stderr, "\n");
+        abort();
+    }
+    testcase_assert_successful_assert_total++;
+    if ( testcase_assert_verbose_assertions )
+    {
+        /* As the assertion didn't actually fail, remove the prefix */
+        if ( strncmp(fmt, assert_failed_str, strlen(assert_failed_str)) == 0 )
+            fmt += strlen(assert_failed_str);
+
+        if ( strcmp(fmt, "ret == 0") == 0 )
+            return;
+
+        for ( int i = 0; i < testcase_assert_verbose_indent_level; i++ )
+            printf("  ");
+
+        printf("%s:%d: ", relpath, line);
+
+        /*
+         * Skip logging the passed function if it was already logged for the
+         * current function, or the source or the function starts with test.
+         */
+        if ( (testcase_assert_current_func == NULL ||
+              strcmp(testcase_assert_current_func, func)) &&
+             (strncmp(relpath, "test-", strlen("test-")) &&
+              strncmp(func, "test_", strlen("test_"))) )
+            printf("%s(): ", func);
+
+        if ( strncmp(fmt, "BUG_ON:", strlen("BUG_ON:")) )
+            printf("ASSERT(");
+
+        vprintf(fmt, ap);
+        va_end(ap);
+
+        if ( strncmp(fmt, "BUG_ON:", strlen("BUG_ON:")) )
+            printf(")");
+
+        printf("\n");
+    }
+}
+
+/** Structure to represent a test case and its results for reporting */
+struct testcase {
+    /** Human-readable name of the test case */
+    const char *name;
+    /** Test ID */
+    const char *tid;
+    /** Integer argument for the test case */
+    int         intarg;
+    /** Function pointer to the test case function */
+    void        (*func)(int);
+    /** Number of assertions passed */
+    int         passed_asserts;
+    /** Number of expected failures occurred */
+    int         expected_failures;
+} testcases[40];
+/** Pointer to the current test case being executed, for tracking results */
+struct testcase *current_testcase = testcases;
+
+static void print_testcase_report(struct testcase *tc)
+{
+    printf("- %-5s %-34s %2d: %3d assertions passed", tc->tid, tc->name,
+           tc->intarg, tc->passed_asserts);
+    if ( tc->expected_failures )
+        printf(" (%2d XFAIL)", tc->expected_failures);
+    printf("\n");
+}
+
+/**
+ * Execute the given test function and record the number of assertions
+ * passed and expected failures for the test report.  The test function
+ * is expected to use the CHECK, ASSERT, and BUG_ON macros for assertions,
+ * and can use EXPECTED_TO_FAIL_BEGIN and EXPECTED_TO_FAIL_END to mark
+ * assertions that are expected to fail for testing negative scenarios.
+ *
+ * The test function is also passed an integer argument that can be used
+ * to specify different scenarios or parameters for the test.
+ *
+ * The test report will include the name of the test case, the integer
+ * argument, the number of assertions passed, and the number of expected
+ * failures.  The test report is printed after the test function completes,
+ * and a summary report is printed after all test cases have been executed.
+ *
+ * The test function can also use the verbose assertion mode to print
+ * additional context for each assertion, which can be helpful for debugging
+ * test failures and understanding the test flow.
+ *
+ * Args:
+ *   case_func (void (*)(int)):
+ *                  The test function to execute, which takes an int argument.
+ *   int_arg (int): An argument to pass to the test function, which can be used
+ *                  to specify different scenarios or parameters for the test.
+ *   tid (const char *):
+ *                  A test id; string identifier for the test case.
+ *   case_name (const char *):
+ *                  A human-readable name for the test case, used for 
reporting.
+ */
+static void run_testcase(void (*case_func)(int), int int_arg, const char *tid,
+                         const char *case_name)
+{
+    printf("\nTest Case: %s...\n", case_name);
+    current_testcase->name = case_name;
+    current_testcase->func = case_func;
+    current_testcase->intarg = int_arg;
+    current_testcase->tid = tid;
+    current_testcase->passed_asserts = 0;
+    current_testcase->expected_failures = 0;
+
+    /*
+     * Call the testcase initialization function if it is set, which can be
+     * used to reset global state or set up specific scenarios for the test.
+     *
+     * For example, the page allocator tests use this to reset the state of the
+     * synthetic page structures and the heap before each test case.
+     */
+    if ( testcase_init_func && int_arg >= 0 )
+        testcase_init_func(case_name, int_arg);
+
+    case_func(int_arg);
+
+    current_testcase->passed_asserts = testcase_assert_successful_assert_total;
+    current_testcase->expected_failures =
+        testcase_assert_expected_failures_total;
+
+    testcase_assert_successful_assert_total = 0;
+    testcase_assert_expected_failures_total = 0;
+
+    printf("\nResults:\n");
+    print_testcase_report(current_testcase);
+    current_testcase++;
+}
+#define RUN_TESTCASE(tid, func, arg) run_testcase(func, arg, #tid, #func)
+
+/**
+ * Provide a report of all test cases executed and their results,
+ * including the total number of assertions passed and expected failures.
+ */
+static int testcase_print_summary(const char *argv0)
+{
+    struct utsname uts;
+    int total_asserts = 0, expected_failures = 0;
+
+    fprintf(stderr, "\nTest Report:\n");
+
+    current_testcase = testcases;
+    for ( size_t i = 0; i < ARRAY_SIZE(testcases) && current_testcase->func;
+          i++ )
+    {
+        print_testcase_report(current_testcase);
+        total_asserts += current_testcase->passed_asserts;
+        expected_failures += current_testcase->expected_failures;
+        current_testcase++;
+    }
+    current_testcase->tid = "Total";
+    current_testcase->name = "";
+    current_testcase->passed_asserts = total_asserts;
+    current_testcase->expected_failures = expected_failures;
+    current_testcase->intarg = current_testcase - testcases;
+    print_testcase_report(current_testcase);
+
+    uname(&uts);
+    printf("\nTest suite %s for %s completed.\n", argv0, uts.machine);
+    return 0;
+}
+
+static const char *parse_args(int argc, char *argv[], const char *topic)
+{
+    const char *program_name = argv[0];
+    struct utsname uts;
+
+    if ( argc != 1 )
+    {
+        fprintf(stderr, "Usage: %s\n", argv[0]);
+        return NULL;
+    }
+    program_name = strrchr(program_name, '/');
+    if ( program_name )
+        program_name++;
+    else
+        program_name = argv[0];
+
+    uname(&uts);
+    printf("Suite : %s\n", program_name);
+    printf("Topic : %s\n", topic);
+    printf("Config: CONFIG_NUMA %s\n",
+           config_enabled(CONFIG_NUMA) ? "enabled" : "disabled");
+#ifndef __clang__
+    printf("Target: gcc %s/%s\n", __VERSION__, uts.machine);
+#else
+    printf("Target: %s/%s\n", __VERSION__, uts.machine);
+#endif
+    return program_name;
+}
+
+#endif /* _CHECK_ASSERTS_H_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/harness.h b/tools/tests/alloc/harness.h
new file mode 100644
index 000000000000..946c796c5475
--- /dev/null
+++ b/tools/tests/alloc/harness.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common test harness for page allocation unit tests.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#ifndef _TEST_HARNESS_
+#define _TEST_HARNESS_
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+
+/* Enable debug mode to enable additional checks */
+#define CONFIG_DEBUG
+
+/* Define common macros which are compatible with the test context */
+#include "hypervisor-macros.h"
+
+/* Provide the common check_asserts library for test assertions */
+#include "check-asserts.h"
+
+/* Common Xen types for the test context */
+typedef uint8_t u8;
+typedef uint64_t paddr_t;
+typedef unsigned long cpumask_t;
+typedef long long s_time_t;
+typedef bool spinlock_t;
+
+/*
+ * The original implementation of reserve_offlined_page() causes the GCC
+ * and clang AddressSanitizer (ASAN) to report stack-buffer-overflow
+ * when the test_merge_tail_pair test case is run with ASAN enabled,
+ * when test verifies the state of the free lists in the heap.
+ *
+ * It finds several list pointer errors in the heap state and one of the
+ * appears to trigger ASAN's stack-buffer-overflow detection on x86_64.
+ *
+ * To temporarily work around this issue, we detect if ASAN is enabled
+ * and in order to be able skip the ASSERT_LIST_EQUAL verification step
+ * in the test case that triggers the ASAN error, while still allowing
+ * the rest of the test case to run and verify all execution with ASAN.
+ */
+/* clang-format off */
+#if defined(__has_feature)
+/* Clang uses __has_feature to detect AddressSanitizer */
+# if __has_feature(address_sanitizer)
+#  define ASAN_ENABLED 1
+# endif
+/* GCC uses __SANITIZE_ADDRESS__ to detect AddressSanitizer */
+#elif defined(__SANITIZE_ADDRESS__)
+# define ASAN_ENABLED 1
+#else
+# define ASAN_ENABLED 0
+#endif
+/* clang-format on */
+#endif /* _TEST_HARNESS_H_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/hypervisor-macros.h 
b/tools/tests/alloc/hypervisor-macros.h
new file mode 100644
index 000000000000..0d35bd9a806c
--- /dev/null
+++ b/tools/tests/alloc/hypervisor-macros.h
@@ -0,0 +1,101 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Common macros and definitions for building host-side unit tests
+ * the Xen hypervisor.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+
+#ifndef _TEST_ALLOC_XEN_MACROS_
+#define _TEST_ALLOC_XEN_MACROS_
+
+/*
+ * In Xen, STATIC_IF(x) and config_enabled(x) are defined in kconfig.h
+ * which we cannot include, so we need to define the necessary macros.
+ */
+#define STATIC_IF(option)        static_if(option)
+#define static_if(value)         _static_if(__ARG_PLACEHOLDER_##value)
+#define _static_if(arg1_or_junk) ___config_enabled(arg1_or_junk static, )
+#define __ARG_PLACEHOLDER_1      0,
+#define config_enabled(cfg)      _config_enabled(cfg)
+#define _config_enabled(value)   __config_enabled(__ARG_PLACEHOLDER_##value)
+
+#define __config_enabled(arg1_or_junk) ___config_enabled(arg1_or_junk 1, 0)
+
+#define ___config_enabled(__ignored, val, ...) val
+
+/*
+ * We include common-macros.h to reuse the Xen-tools macros, which are
+ * not necessarily the same as the Xen hypervisor macros, but are close
+ * enough for the test context.
+ */
+#include <xen-tools/common-macros.h>
+
+/*
+ * Define the header guards of the Xen headers that the Xen hypervisor
+ * variants of the definitions in common-macros.h and bitops.h to prevent
+ * conflicting definitions from those headers that prevent clean compilation.
+ */
+#define __XEN_CONST_H__
+#define __MACROS_H__
+
+/*
+ * We also define the Xen hypervisor macros that are used by page_alloc.c
+ * but not defined by common-macros.h, but needed to build hypervisor code
+ * in the test context, such as IS_ALIGNED() and the ffsl/flsl macros.
+ */
+#define IS_ALIGNED(x, a) (!((x) & ((a) - 1)))
+
+/*
+ * Inclde the Xen-tools bitops.h to reuse the bitops from the tools side.
+ * They are not necessarily the same as the Xen hypervisor bitops, but are
+ * close enough for the test context.
+ */
+#include <xen-tools/bitops.h>
+/*
+ * Afer including the Xen-tools bitops.h, we need to redefine the ffsl and flsl
+ * macros to match the behavior of the Xen hypervisor's ffsl and flsl, which
+ * return unsigned int and are relevant for signed/unsigned conversion checking
+ * and type hints in the test context.
+ * And we need to undefine conflicting macros defined by xen-tools headers.
+ */
+#undef BITS_PER_LONG
+#undef __LITTLE_ENDIAN
+#undef __BIG_ENDIAN
+/* Xen ffsl returns 1-based position of lowest set bit as unsigned int */
+#undef ffsl /* tools/include/xen-tools/bitops.h returns signed int */
+#define ffsl(x) ((unsigned int)__builtin_ffsl(x))
+/* Xen flsl returns 1-based position of highest set bit as unsigned int */
+#define flsl(x) ((unsigned int)((x) ? BITS_PER_LONG - __builtin_clzl(x) : 0))
+
+/* Common assertion and logging macros */
+#define BUG()                     assert(false)
+#define domain_crash(d)           ((void)(d))
+#define PRI_mfn                   "lu"
+#define PRI_stime                 "lld"
+#define printk                    printf
+#define dprintk(level, fmt, ...)  printk(fmt, ##__VA_ARGS__)
+#define gdprintk(level, fmt, ...) printk(fmt, ##__VA_ARGS__)
+#define gprintk(level, fmt, ...)  printk(fmt, ##__VA_ARGS__)
+#define panic(fmt, ...)                          \
+        do                                       \
+        {                                        \
+            fprintf(stderr, fmt, ##__VA_ARGS__); \
+            abort();                             \
+        } while ( 0 )
+
+/* Support including xen/sections.h and other function attributes */
+#define __initdata
+#define __init        __used
+#define __initcall(f) static int __used (*f##_ptr)(void) = (f)
+
+#endif /* _TEST_ALLOC_XEN_MACROS_ */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tools/tests/alloc/libtest-page_alloc.h 
b/tools/tests/alloc/libtest-page_alloc.h
new file mode 100644
index 000000000000..5654152cd48a
--- /dev/null
+++ b/tools/tests/alloc/libtest-page_alloc.h
@@ -0,0 +1,356 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Test framework for testing the memory-allocation functionality
+ * of xen/common/page_alloc.c, consisting of:
+ *
+ * 1. A header-only shim for page_alloc.c to provide the necessary
+ *    definitions and helpers to allow the test framework to include
+ *    the real page_alloc.c directly into its translation unit.
+ *
+ * 2. A set of mocks for the Xen types and functions used by page_alloc.c,
+ *    sufficient to support the test scenarios in tools/tests/alloc.
+ *
+ *    This includes mocks for NUMA topology, designed to allow the test
+ *    scenarios to manipulate the state of the allocator and verify its
+ *    behavior in a way that is consistent with how page_alloc.c acts when
+ *    used by the running Xen hypervisor, while being self-contained and
+ *    suitable for unit and integration testing.
+ *
+ * 3. A tiny wrapper which includes the real page_alloc.c for testing.
+ *
+ *    It disables a few of the -Wextra warnings enabled by the test
+ *    framework that are not yet fixed in page_alloc.c, such as some
+ *    sign-compare warnings and unused parameter warnings in its code.
+ *
+ * 4. A library for NUMA heap initialisation, and asserting the heap status.
+ *
+ *    This library provides functions to prepare the state of the memory
+ *    allocator for the test scenarios, such as:
+ *
+ *    a. Initializing the heap before each test case, creating NUMA nodes,
+ *       and adding pages to the heap in specific states, such as free,
+ *       allocated, marked to be offlined or already offlined.
+ *
+ *    b. Verifying the state of the heap and the page_info structures after
+ *       test actions, such as checking that pages are allocated or freed
+ *       as expected, that the state of the page_info structures is consistent
+ *       with the expected state.
+ *
+ * 5. Test case lifecycle management, such as initializing the test context
+ *    before each test case, printing the outcome of each test case,
+ *    tracking the number of assertions, logging assertions with file
+ *    and line information, and printing a summary report at the end.
+ *
+ * 6. A Makefile for discovering, compiling, running the test cases,
+ *    and reporting results which test cases were run.
+ *
+ *    The Makefile is designed to allow running individual test cases
+ *    or the entire test suite for all supported CPU architectures, if
+ *    so desired.
+ *
+ *    It is also responsible for compiling the tests with address sanitizer
+ *    (ASAN) enabled to catch memory errors in the page allocator code
+ *    and the test code, especially when manipulating the state of the
+ *    page_info structures inside the test scenarios.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#include <stdarg.h>
+#include <execinfo.h>
+#include <string.h>
+
+/* Enable -Wextra warnings as errors to catch e.g. sign-compare issues */
+#pragma GCC diagnostic error "-Wextra"
+
+/* Support for printing the status of pages for debugging */
+struct page_info;
+static void print_page_info(struct page_info *pos);
+static void print_page_count_info(unsigned long count_info);
+
+#define TEST_USES_PAGE_ALLOC_SHIM
+#include "page_alloc_shim.h"
+
+/* Include the real page_alloc.c for testing */
+
+#include "page_alloc-wrapper.h"
+
+static const unsigned int node = 0;
+static const unsigned int node0 = 0;
+static const unsigned int node1 = 1;
+static const unsigned int order0 = 0;
+static const unsigned int order1 = 1;
+static const unsigned int order2 = 2;
+static const unsigned int order3 = 3;
+
+/**
+ * Functions for setting up test scenarios with a clean allocator state,
+ * and for building synthetic buddy trees with the expected page_info state.
+ */
+
+/* Set up a bare minimum NUMA node topology. */
+static void init_numa_node_data(unsigned int start_mfn)
+{
+    (void)start_mfn;
+#ifdef CONFIG_NUMA
+    /*
+     * For simplicity, we assign each CPU to its own node, and set each
+     * node's cpumask to contain just that CPU.
+     *
+     * If needed, we could easily modify this setup to have multiple CPUs
+     * per node by adjusting the cpu_to_node assignments and node_to_cpumask
+     * values accordingly.
+     *
+     * The test scenarios in this suite do not currently require multiple
+     * CPUs per node, but we could extend them to do so if desired.
+     *
+     * This is just a default setup that the test scenarios can rely on,
+     * and they are free to modify the cpu_to_node and node_to_cpumask
+     * values as needed for their specific test cases.
+     */
+    for ( unsigned int i = 0; i < NR_CPUS; i++ )
+        cpu_to_node[i] = i;
+
+    /* Each node has a single CPU in its cpumask for simplicity */
+    for ( unsigned int i = 0; i < MAX_NUMNODES; i++ )
+        node_to_cpumask[i] = (1UL << i);
+
+    /* Initialize node data structures */
+    for ( unsigned int i = 0; i < MAX_NUMNODES; i++ )
+    {
+        /* Each node has 8 pages for testing for now */
+        node_data[i].node_start_pfn = start_mfn + (i * 8);
+        node_data[i].node_present_pages = 8UL;
+        node_data[i].node_spanned_pages = 8UL;
+    }
+
+    /*
+     * Set up memnodemap so that mfn_to_nid() correctly resolves MFN
+     * ranges to NUMA nodes: with memnode_shift=3 each memnodemap entry
+     * covers 8 MFNs (2^3), matching the 8-page-per-node layout above.
+     * Entry i maps MFNs [i*8 .. (i+1)*8 - 1] to node i.
+     */
+    memnode_shift = 3;
+    for ( unsigned int i = 0; i < 64; i++ )
+        memnodemap[i] = (nodeid_t)i;
+#endif /* CONFIG_NUMA */
+}
+
+static void init_dummy_domains(void);
+/**
+ * Reset all page_alloc translation-unit globals that these tests observe.
+ *
+ * The test program includes xen/common/page_alloc.c directly, so its
+ * file-scope variables become global variables of this translation unit.
+ *
+ * Each test must start from a clean page allocator state, with a clean heap,
+ * clean availability counters, and empty offlined and broken lists.
+ */
+static void reset_page_alloc_state(const char *caller_func, int start_mfn)
+{
+    unsigned int zone;
+    unsigned int order;
+
+    printf("\n%s: start_mfn = %u\n", caller_func, start_mfn);
+
+    /* Clear the test page table used for synthetic page_info objects. */
+    memset(frame_table, 0, sizeof(frame_table));
+
+    /* Clear the backing storage used by the imported allocator globals. */
+    memset(test_heap_storage, 0, sizeof(test_heap_storage));
+    memset(test_avail_storage, 0, sizeof(test_avail_storage));
+
+    /* Clear the shim-owned singleton objects used by helper macros. */
+    memset(&test_dummy_domain1, 0, sizeof(test_dummy_domain1));
+    memset(&test_dummy_domain2, 0, sizeof(test_dummy_domain2));
+    memset(&test_current_vcpu, 0, sizeof(test_current_vcpu));
+
+    /* Reinitialise the global page lists manipulated by the allocator. */
+    INIT_PAGE_LIST_HEAD(&page_offlined_list);
+    INIT_PAGE_LIST_HEAD(&page_broken_list);
+    INIT_PAGE_LIST_HEAD(&test_page_list);
+
+    init_numa_node_data(start_mfn); /* Only used for NUMA-enabled builds */
+
+    /* Reinitialise every per-zone, per-order free-list bucket. */
+    for ( nodeid_t node = 0; node < MAX_NUMNODES; node++ )
+    {
+        _heap[node] = &test_heap_storage[node];
+        avail[node] = test_avail_storage[node];
+        node_avail_pages[node] = 0;
+        for ( zone = 0; zone < NR_ZONES; zone++ )
+            for ( order = 0; order <= MAX_ORDER; order++ )
+                INIT_PAGE_LIST_HEAD(&heap(node, zone, order));
+    }
+
+    total_avail_pages = 0;
+    outstanding_claims = 0;
+    /*
+     * The valid MFN range for the test context is configured to cover only
+     * the test frame table, so that any attempts by page_alloc.c to prevent
+     * functions in page_alloc.c is only manipulating the intended test
+     * state and not accessing uninitialized memory or going out of bounds.
+     *
+     * Set up the initial range of valid pages for mfn_valid() used by
+     * free_heap_pages() as condition if there are successors/predecessors
+     * to merge pages with. Unless successors/predecessors are initialized
+     * to be free, it should forgoe merging and just add the provided page
+     * as-is to the heap, but to prevent it looking up uninitialised memory,
+     * we set the valid MFN range to cover the frame_table only.
+     */
+    first_valid_mfn = start_mfn;
+    max_page = sizeof(frame_table) / sizeof(frame_table[0]);
+    assert(first_valid_mfn < max_page);
+
+    init_dummy_domains();
+}
+
+static void init_dummy_domains(void)
+{
+    nodemask_t dom_node_affinity;
+    struct domain *dom;
+    int dom_id = 1;            /* Start domain IDs from 1 for clarity in logs 
*/
+
+    /* Provide a current vcpu/domain pair for code paths that inspect it. */
+    test_current_vcpu.domain = &test_dummy_domain1;
+
+    /* Provide the dummy domains for tests that need some domains */
+    domain_list = &test_dummy_domain1;
+    test_dummy_domain1.next_in_list = &test_dummy_domain2;
+
+    nodes_clear(dom_node_affinity);
+    node_set(node0, dom_node_affinity);
+    node_set(node1, dom_node_affinity);
+
+    for_each_domain ( dom )
+    {
+        dom->node_affinity = dom_node_affinity;
+        dom->max_pages = MAX_PAGES;
+        dom->domain_id = dom_id++;
+    }
+}
+
+/* Initialize the page allocator tests */
+static void __used init_page_alloc_tests(void)
+{
+    /* Define the function above as the testcase initialization function */
+    setup_testcase_init_func(reset_page_alloc_state);
+}
+
+/**
+ * Populate a page descriptor with the minimal state needed by
+ * reserve_offlined_page().
+ *
+ * Tests build synthetic buddy trees by placing a small set of
+ * page_info objects into allocator free lists. This helper
+ * keeps that setup consistent across scenarios.
+ *
+ * Args:
+ *  page (struct page_info *): Pointer to the page_info to be initialised.
+ *  order (unsigned int):      The order to set in the page_info's order field
+ *  state (unsigned long):     The state bits to set in the page's count_info
+ *                             field, e.g. PGC_state_inuse for pages to be
+ *                             added to the heap, or PGC_state_offlined for
+ *                             pages to be added to the offlined list.
+ */
+static void init_test_page(struct page_info *page, unsigned int order,
+                           unsigned long state)
+{
+    mfn_t mfn = page_to_mfn(page);
+
+    if ( mfn < first_valid_mfn && mfn > 0 && mfn < max_page )
+        first_valid_mfn = mfn;
+
+    if ( mfn >= max_page && mfn < ARRAY_SIZE(frame_table) )
+        max_page = mfn + 1;
+
+    CHECK(mfn_valid(mfn), "mfn %lu valid: %lu-%lu", mfn, first_valid_mfn,
+          max_page);
+
+    memset(page, 0, sizeof(*page));
+
+    /* Model the page as a free buddy head of the requested order. */
+    page->v.free.order = order;
+
+    /* Default to no tracked dirty subrange and no active scrubbing. */
+    page->u.free.first_dirty = INVALID_DIRTY_IDX;
+    page->u.free.scrub_state = BUDDY_NOT_SCRUBBING;
+
+    /* Install the requested allocator state bits for this synthetic page. */
+    page->count_info = state;
+}
+
+/**
+ * Initialize the given pages as a buddy of the requested order,
+ * with the first page as the buddy head and the rest as subpages
+ * of it, and add the intialised buddy to the heap.
+ *
+ * This helper is intended to be used by test scenarios to set up
+ * the heap with buddies of the expected order and state for testing
+ * operations that manipulate the heap, such as reserve_offlined_page()
+ * and free_heap_pages(), and to ensure that the heap state is consistent
+ * with the page_info state after those operations.
+ *
+ * The buddy is added to the heap using free_heap_pages() which
+ * models the expected usage of the heap and ensures that the
+ * heap structures are updated correctly according to the logic
+ * of the allocator, which may change over time.
+ *
+ * For example, if the logic for merging buddies or tracking claims changes,
+ * using free_heap_pages() ensures that the test setup will be correct even
+ * after such changes, and that the test scenarios will be testing the real
+ * behaviour of the allocator rather than an idealised version of it.
+ *
+ * Args:
+ *  pages (struct page_info *): Pointer to the first page_info in an array.
+ *  order (unsigned int):       The order of the buddy to be created.
+ *  caller (const char *):      The name of the calling function for context.
+ * Returns:
+ *  The zone of the added buddy, which can be useful for test scenarios that
+ *  need to know the zone of the buddy for further operations or assertions.
+ */
+static zone_t __used page_list_add_buddy(struct page_info *pages,
+                                         unsigned int order, const char 
*caller)
+{
+    size_t i, num_pages = 1U << order;
+    bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+    /* Avoid logging spinlocks and verbose assertions during initialization */
+    testcase_assert_verbose_assertions = false;
+
+    /*
+     * Initialize the first page as the head of the buddy with the given order.
+     * All pages are initialized as in-use as this is the API expected by
+     * free_heap_pages() when it adds a buddy to the heap(). This model is
+     * consistent with the way the boot allocator and online_page() handle
+     * page initialization as well as the normal way for used pages to be 
freed.
+     */
+    init_test_page(&pages[0], order, PGC_state_inuse);
+
+    /* Add the subpages of the buddy as order-0 buddies to the heap */
+    for ( i = 1; i < num_pages; i++ )
+        init_test_page(&pages[i], order0, PGC_state_inuse);
+
+    /*
+     * Add the created buddy to the heap. This uses the same code path as
+     * freeing used pages and is consistent with the way the boot allocator
+     * and online_page() handle page initialization. Using free_heap_pages()
+     * has the additional benefit of ensuring that the heap structures are
+     * consistent even if the internal logic of the heap management changes.
+     *
+     * For example, implementing NUMA claims adds new per-node claims counters
+     * and logic to free_heap_pages(), so using it here ensures that the test
+     * setup will be correct even after such changes.
+     */
+    printf("%s: Adding buddy of order %u at MFN %lu to the heap.\n", caller,
+           order, page_to_mfn(&pages[0]));
+
+    testcase_assert_verbose_assertions = false;
+
+    free_heap_pages(&pages[0], order, false);
+
+    testcase_assert_verbose_assertions = verbose_asserts_save;
+    return page_to_zone(&pages[0]);
+}
+
+#define test_page_list_add_buddy(pages, order) \
+        page_list_add_buddy(pages, order, __func__)
diff --git a/tools/tests/alloc/mock-page_list.h 
b/tools/tests/alloc/mock-page_list.h
new file mode 100644
index 000000000000..92a0f7c53042
--- /dev/null
+++ b/tools/tests/alloc/mock-page_list.h
@@ -0,0 +1,307 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Mock page list implementation for testing page allocator functions.
+ *
+ * This mock implementation provides a simplified version of the page list
+ * structures and functions used by the page allocator, allowing unit tests
+ * to be written without relying on the full Xen environment.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _MOCK_PAGE_LIST_H_
+#define _MOCK_PAGE_LIST_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "harness.h" /* Provides the generic Xen defintitions needed */
+
+/*
+ * Wrapper around xen/config.h for common Xen definitions for the test context
+ */
+#define __XEN_KCONFIG_H
+#undef __nonnull
+#undef offsetof
+#include <xen/config.h>
+/* Xen code adds cf_check as an attribute macro to functions we don't call */
+#undef cf_check
+#define cf_check __used
+
+#define MAX_ORDER 20
+
+/*
+ * The page_info structures in the frame table are manipulated directly
+ * by page_alloc.c, so they must be defined in a way that is consistent
+ * with how page_alloc.c uses them, while being self-contained and suitable
+ * for unit testing.
+ *
+ * The test scenarios can then manipulate the state of these page_info
+ * structures to set up test conditions and verify the behavior of the
+ * allocator in a way that is consistent with how it acts when used by
+ * the running hypervisor.
+ */
+struct page_info {
+    unsigned long count_info; /* PGC_state_inuse and other flags/counters */
+    union {
+        /* When the page is in use, u.inuse.type_info is used for status */
+        struct {
+            unsigned long type_info;
+        } inuse;
+        /*
+         * When the page is free, u.free is used for buddy management.
+         *
+         * Using BUILD_BUG_ON(sizeof(var->u) != sizeof(long)), page_alloc
+         * enforces that the u.free struct is exactly the size of a long.
+         * 32-bit architectures need to use bitfields to fit the fields
+         * in a long, while 64-bit architectures can use normal fields.
+         */
+        union {
+            struct {
+                unsigned int  first_dirty : MAX_ORDER + 1;
+#define INVALID_DIRTY_IDX ((1UL << (MAX_ORDER + 1)) - 1)
+                bool          need_tlbflush : 1;
+                unsigned long scrub_state : 2;
+#define BUDDY_NOT_SCRUBBING 0
+#define BUDDY_SCRUBBING     1
+#define BUDDY_SCRUB_ABORT   2
+            };
+            unsigned long val;
+        } free;
+    } u;
+    union {
+        struct {
+            unsigned int order;
+#define PFN_ORDER(pg) ((pg)->v.free.order)
+        } free;
+        unsigned long type_info;
+    } v;
+    uint32_t          tlbflush_timestamp;
+    struct domain    *owner;
+    struct page_info *list_next;
+    struct page_info *list_prev;
+};
+
+struct page_list_head {
+    struct page_info *head;
+    struct page_info *tail;
+    unsigned int      count;
+};
+#define PAGE_LIST_HEAD(name) struct page_list_head name = {NULL, NULL, 0}
+
+static inline void test_page_list_init(struct page_list_head *list)
+{
+    list->head = NULL;
+    list->tail = NULL;
+    list->count = 0;
+}
+#define INIT_PAGE_LIST_HEAD(l) test_page_list_init(l)
+
+/* Used by page_alloc.c */
+#define page_list_for_each_safe(pos, tmp, list)        \
+        for ( (pos) = page_list_first(list),           \
+              (tmp) = (pos) ? (pos)->list_next : NULL; \
+              (pos) != NULL;                           \
+              (pos) = (tmp), (tmp) = (pos) ? (pos)->list_next : NULL )
+
+typedef unsigned long mfn_t;
+static struct page_list_head test_page_list;
+
+#define page_to_list(d, pg)          (&test_page_list)
+#define page_list_add(pg, list)      test_page_list_add((pg), (list))
+#define page_list_add_tail(pg, list) test_page_list_add_tail((pg), (list))
+#define page_list_del(pg, list)      test_page_list_del((pg), (list))
+#define page_list_empty(list)        ((list)->head == NULL)
+#define page_list_first(list)        ((list)->head)
+#define page_list_last(list)         ((list)->tail)
+#define page_list_remove_head(list)  test_page_list_remove_head((list))
+
+static inline void test_page_list_add_common(struct page_info *pg,
+                                             struct page_list_head *list,
+                                             bool at_tail)
+{
+    pg->list_next = NULL;
+    pg->list_prev = NULL;
+
+    if ( list->head == NULL )
+    {
+        list->head = pg;
+        list->tail = pg;
+    }
+    else if ( at_tail )
+    {
+        pg->list_prev = list->tail;
+        list->tail->list_next = pg;
+        list->tail = pg;
+    }
+    else
+    {
+        pg->list_next = list->head;
+        list->head->list_prev = pg;
+        list->head = pg;
+    }
+
+    list->count++;
+}
+
+#define test_page_list_add(pg, list) test_page_list_add_common(pg, list, false)
+#define test_page_list_add_tail(pg, list) \
+        test_page_list_add_common(pg, list, true)
+
+static inline void test_page_list_del(struct page_info *pg,
+                                      struct page_list_head *list)
+{
+    if ( pg->list_prev )
+        pg->list_prev->list_next = pg->list_next;
+    else
+        list->head = pg->list_next;
+
+    if ( pg->list_next )
+        pg->list_next->list_prev = pg->list_prev;
+    else
+        list->tail = pg->list_prev;
+
+    pg->list_next = NULL;
+    pg->list_prev = NULL;
+
+    ASSERT(list->count > 0);
+    list->count--;
+}
+
+static inline struct page_info *
+test_page_list_remove_head(struct page_list_head *list)
+{
+    struct page_info *pg = list->head;
+
+    if ( pg )
+        test_page_list_del(pg, list);
+
+    return pg;
+}
+
+/*
+ * The frame table is the foundation for the buddy allocator algorithm
+ * implemented by page_alloc.c, and the test scenarios manipulate the
+ * state of the page_info structures in the frame table to set up test
+ * conditions and verify the behavior of the allocator.
+ *
+ * The frame table is indexed by MFN as required by the buddy allocator
+ * algorithm in page_alloc.c, so the translation functions between page_info
+ * pointers and MFNs are defined to allow page_alloc.c to manipulate the
+ * page_info structures in the test frame table using MFN-based translations.
+ */
+extern struct page_info frame_table[];
+#define page_to_mfn(pg)   ((mfn_t)((pg) - &frame_table[0]))
+#define mfn_to_page(mfn)  (&frame_table[(mfn)])
+#define mfn_valid(mfn)    (mfn >= first_valid_mfn && mfn < max_page)
+#define maddr_to_page(pa) (CHECK(false, "Not implemented"))
+
+/*
+ * Helper functions to print the state of the heap and offlined pages
+ * for reference while asserting consistency of the heap and offlined
+ * page state.
+ *
+ * These functions are called at various points in the test scenarios
+ * to validate that the internal state of the allocator is consistent
+ * with expectations.
+ */
+
+/* Architecture-specific page state defines */
+#define PG_shift(idx)         (BITS_PER_LONG - (idx))
+#define PG_mask(x, idx)       (x##UL << PG_shift(idx))
+#define PGT_count_width       PG_shift(2)
+#define PGT_count_mask        ((1UL << PGT_count_width) - 1)
+#define PGC_allocated         PG_mask(1, 1)
+#define PGC_xen_heap          PG_mask(1, 2)
+#define _PGC_need_scrub       PG_shift(4)
+#define PGC_need_scrub        PG_mask(1, 4)
+#define _PGC_broken           PG_shift(7)
+#define PGC_broken            PG_mask(1, 7)
+#define PGC_state             PG_mask(3, 9)
+#define PGC_state_inuse       PG_mask(0, 9)
+#define PGC_state_offlining   PG_mask(1, 9)
+#define PGC_state_offlined    PG_mask(2, 9)
+#define PGC_state_free        PG_mask(3, 9)
+#define page_state_is(pg, st) (((pg)->count_info & PGC_state) == 
PGC_state_##st)
+#define PGC_count_width       PG_shift(9)
+#define PGC_count_mask        ((1UL << PGC_count_width) - 1)
+#define _PGC_extra            PG_shift(10)
+#define PGC_extra             PG_mask(1, 10)
+
+struct PGC_flag_names {
+    unsigned long flag;
+    const char   *name;
+} PGC_flag_names[] = {
+    {.flag = PGC_need_scrub, "PGC_need_scrub"},
+    {.flag = PGC_extra,      "PGC_extra"     },
+    {.flag = PGC_broken,     "PGC_broken"    },
+    {.flag = PGC_xen_heap,   "PGC_xen_heap"  },
+};
+
+static const char *pgc_state_name(unsigned long count_info)
+{
+    switch ( count_info & PGC_state )
+    {
+    case PGC_state_inuse:
+        return "PGC_state_inuse";
+    case PGC_state_offlining:
+        return "PGC_state_offlining";
+    case PGC_state_offlined:
+        return "PGC_state_offlined";
+    case PGC_state_free:
+        return "PGC_state_free";
+    default:
+        assert("Invalid page state" && false);
+    }
+}
+
+/* Print the count_info flags of a page_info for reference */
+static void print_page_count_info(unsigned long count_info)
+{
+    printf("        flags: %s", pgc_state_name(count_info));
+    for ( size_t i = 0; i < ARRAY_SIZE(PGC_flag_names); i++ )
+        if ( count_info & PGC_flag_names[i].flag )
+            printf(" %s", PGC_flag_names[i].name);
+    puts("");
+}
+
+/* Print the state of a single page for reference */
+static void print_page_info(struct page_info *pos)
+{
+    printf("      mfn %lu: order %u, first_dirty %x\n", page_to_mfn(pos),
+           PFN_ORDER(pos), pos->u.free.first_dirty);
+    print_page_count_info(pos->count_info);
+}
+
+/* Print and assert the state of an offlined page.*/
+static void print_and_assert_offlined_page(struct page_info *pos)
+{
+    print_page_info(pos);
+    /*
+     * The order of offlined pages must always be 0 because pages are only
+     * offlined as standalone pages.  Higher-order pages on the offline lists
+     * are not supported by reserve_offlined_page() and online_page().
+     */
+    CHECK(PFN_ORDER(pos) == 0, "All offlined pages must have order 0");
+
+    /*
+     * Check the first_dirty index of offlined pages: Current code does
+     * not use first_dirty for offlined pages as it only points to the
+     * first dirty subpage within a buddy on the heap, and offlined pages
+     * are not on the heap. As it is not used, current code sets it to
+     * INVALID_DIRTY_IDX when offlining a page, so just confirm that.
+     *
+     * PS: Their scrubbing state is tracked by count_info & PG_need_scrub.
+     * In case an offlined page is onlined, the onlining code will be
+     * responsible to set first_dirty based on the scrubbing state.
+     */
+    if ( pos->u.free.first_dirty != INVALID_DIRTY_IDX )
+    {
+        printf("WARNING: offlined page at MFN %lu has first_dirty %x but "
+               "expected INVALID_DIRTY_IDX\n",
+               page_to_mfn(pos), pos->u.free.first_dirty);
+        ASSERT(pos->u.free.first_dirty == INVALID_DIRTY_IDX);
+    }
+}
+
+#endif /* _MOCK_PAGE_LIST_H_ */
\ No newline at end of file
diff --git a/tools/tests/alloc/page_alloc-wrapper.h 
b/tools/tests/alloc/page_alloc-wrapper.h
new file mode 100644
index 000000000000..ce1889e3aa37
--- /dev/null
+++ b/tools/tests/alloc/page_alloc-wrapper.h
@@ -0,0 +1,465 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Test framework for testing Xen's memory-allocation functionality.
+ *
+ * This file wraps xen/common/page_alloc.c for the test framework.
+ *
+ * Context:
+ *
+ * The test framework includes the real page_alloc.c directly into its
+ * translation unit, along with mocks for the Xen types and functions
+ * used by page_alloc.c, and a library for NUMA heap initialisation
+ * and asserting the heap status.
+ *
+ * This file serves as the wrapper around page_alloc.c, providing the
+ * necessary definitions and helpers to allow the test framework to
+ * include the real page_alloc.c directly into its translation unit.
+ *
+ * It also provides wrapper functions for key page_alloc.c functions like
+ * mark_page_offline() and offline_page() to allow the test scenarios to
+ * log the actions being taken and the outcomes observed when these functions
+ * are called, which is important for understanding the behavior of the
+ * allocator during the test scenarios and for debugging any issues that arise.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#include <stdarg.h>
+#include <string.h>
+
+#define TEST_USES_PAGE_ALLOC_SHIM
+#include "page_alloc_shim.h"
+
+/* Include the real page_alloc.c for testing */
+
+#pragma GCC diagnostic push
+/* TODO: We should fix the remaining sign-compare warnings in page_alloc.c */
+#pragma GCC diagnostic ignored "-Wsign-compare"
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#include "../../xen/common/page_alloc.c"
+#pragma GCC diagnostic pop
+
+/* Allows the logging spinlock/unlock mocks to identify the heap lock */
+static spinlock_t *heap_lock_ptr = &heap_lock;
+
+/*
+ * Global state for the test page allocator shim and helpers.
+ *
+ * This includes the heap storage and availability counters that the test
+ * scenarios manipulate, as well as the domain list and a bug counter for
+ * the test program to track any unexpected conditions encountered in the
+ * test helpers.
+ */
+#ifndef PAGES_PER_ZONE
+#define PAGES_PER_ZONE 8
+#endif
+
+#ifndef MAX_PAGES
+#define MAX_PAGES (MAX_NUMNODES * NR_ZONES * PAGES_PER_ZONE)
+#endif
+
+/*
+ * The test frame table serves as the backing storage for the page_info
+ * structures used in the test scenarios, and the page_info structures
+ * are indexed by MFN for easy translation between page_info pointers and
+ * MFNs in the test helpers and assertions.
+ *
+ * The frame table is the foundation for the buddy allocator algorithm
+ * implemented by page_alloc.c, and the test scenarios manipulate the
+ * state of the page_info structures in the frame table to set up test
+ * conditions and verify the behavior of the allocator.
+ */
+struct page_info frame_table[MAX_PAGES];
+
+/* Provide a test pages pointer for the test scenarios */
+static struct page_info *test_pages = frame_table;
+
+/*
+ * Global state for the test page allocator shim and helpers.
+ *
+ * This includes the heap storage and availability counters that the test
+ * scenarios manipulate, as well as the domain list and a bug counter for
+ * the test program to track any unexpected conditions encountered in the
+ * test helpers.
+ */
+static heap_by_zone_and_order_t test_heap_storage[MAX_NUMNODES];
+static unsigned long test_avail_storage[MAX_NUMNODES][NR_ZONES];
+struct domain *domain_list;
+typedef size_t zone_t;
+
+static int __used test_domain_install_claim_set(struct domain *d,
+                                                unsigned int nr_claims,
+                                                memory_claim_t *claim_set)
+{
+    bool save_verbose_asserts = testcase_assert_verbose_assertions;
+    char target_str[16];
+
+    /* Avoid logging verbose logging while marking a page offline */
+    testcase_assert_verbose_assertions = false;
+
+    printf("%s => Installing claim set for domain %u:\n", __func__,
+           d->domain_id);
+    for ( unsigned int i = 0; i < nr_claims; i++ )
+    {
+        switch ( claim_set[i].target )
+        {
+        case XEN_DOMCTL_CLAIM_MEMORY_GLOBAL:
+            snprintf(target_str, sizeof(target_str), "GLOBAL");
+            break;
+        case XEN_DOMCTL_CLAIM_MEMORY_LEGACY:
+            snprintf(target_str, sizeof(target_str), "LEGACY");
+            break;
+        default:
+            snprintf(target_str, sizeof(target_str), "NODE %u",
+                     claim_set[i].target);
+            break;
+        }
+        printf("  Claim %u: pages=%lu, target=%s\n", i, claim_set[i].pages,
+               target_str);
+    }
+
+    int ret = domain_install_claim_set(d, nr_claims, claim_set);
+    printf("%s <= domain_install_claim_set() returned %d\n", __func__, ret);
+    testcase_assert_verbose_assertions = save_verbose_asserts;
+    return ret;
+}
+#define domain_install_claim_set(d, nr_claims, claim_set) \
+        test_domain_install_claim_set(d, nr_claims, claim_set)
+
+static unsigned long __used test_mark_page_offline(struct page_info *page,
+                                                   int flag,
+                                                   const char *caller_func)
+{
+    bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+    /* Avoid logging verbose logging while marking a page offline */
+    testcase_assert_verbose_assertions = false;
+
+    printf("%s => Marking page at MFN %lu as %s.\n", caller_func,
+           page_to_mfn(page), flag ? "broken" : "offlined");
+
+    mark_page_offline(page, flag);
+
+    testcase_assert_verbose_assertions = save_verbose_asserts;
+    return 0;
+}
+#define mark_page_offline(pg, flag) test_mark_page_offline(pg, flag, __func__)
+
+static const char *offline_state_name(uint32_t offline_status)
+{
+    switch ( offline_status )
+    {
+    case PG_OFFLINE_FAILED:
+        return "PG_OFFLINE_FAILED";
+    case PG_OFFLINE_PENDING:
+        return "PG_OFFLINE_PENDING";
+    case PG_OFFLINE_OFFLINED:
+        return "PG_OFFLINE_OFFLINED";
+    case PG_OFFLINE_AGAIN:
+        return "PG_OFFLINE_AGAIN";
+    default:
+        return "PG_OFFLINE_UNKNOWN_STATUS";
+    }
+}
+
+static int __used test_offline_page(mfn_t mfn, int broken, uint32_t *status)
+{
+    bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+    testcase_assert_verbose_assertions = false;
+    printf("%s => Offlining page at MFN %lu with broken=%d\n", __func__, mfn,
+           broken);
+
+    int ret = offline_page(mfn, broken, status);
+
+    printf("%s <= offline_page() returned %d, status=0x%x (%s)\n", __func__,
+           ret, *status, offline_state_name(*status));
+    testcase_assert_verbose_assertions = save_verbose_asserts;
+    return 0;
+}
+#define offline_page(mfn, broken, status) test_offline_page(mfn, broken, 
status)
+
+static int __used test_set_outstanding_pages(struct domain *dom,
+                                             unsigned long pages,
+                                             const char *caller_func)
+{
+    bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+    /* Avoid logging verbose logging while setting outstanding claims */
+    testcase_assert_verbose_assertions = false;
+    printf("%s => domain_set_outstanding_pages(dom=%u, pages=%lu)\n",
+           caller_func, dom->domain_id, pages);
+
+    int ret = domain_set_outstanding_pages(dom, pages);
+
+    printf("%s <= domain_set_outstanding_pages() = %d\n", caller_func, ret);
+    testcase_assert_current_func = NULL;
+    testcase_assert_verbose_assertions = save_verbose_asserts;
+    return ret;
+}
+#define domain_set_outstanding_pages(dom, pages) \
+        test_set_outstanding_pages(dom, pages, __func__)
+
+static struct page_info *__used test_alloc_domheap(struct domain *dom,
+                                                   unsigned int order,
+                                                   unsigned int memflags,
+                                                   const char *caller_func)
+{
+    bool save_verbose_asserts = testcase_assert_verbose_assertions;
+
+    /* Avoid logging verbose logging while allocating domheap pages */
+    testcase_assert_verbose_assertions = false;
+    printf("%s => alloc_domheap_pages(dom=%u, order=%u, memflags=%x)\n",
+           caller_func, dom->domain_id, order, memflags);
+    testcase_assert_current_func = "alloc_domheap_pages";
+    testcase_assert_verbose_indent_level++;
+    struct page_info *pg = alloc_domheap_pages(dom, order, memflags);
+    testcase_assert_verbose_indent_level--;
+    testcase_assert_current_func = NULL;
+
+    testcase_assert_verbose_assertions = save_verbose_asserts;
+    return pg;
+}
+#define alloc_domheap_pages(dom, order, memflags) \
+        test_alloc_domheap(dom, order, memflags, __func__)
+
+#ifdef CONFIG_SYSCTL
+/* Helper for just getting the number of free pages for ASSERTs */
+static uint64_t __used free_pages(void)
+{
+    uint64_t free_pages, total_claims;
+    bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+    /* Avoid logging spinlock actions while getting the free page count */
+    testcase_assert_verbose_assertions = false;
+    get_outstanding_claims(&free_pages, &total_claims);
+    testcase_assert_verbose_assertions = verbose_asserts_save;
+    return free_pages;
+}
+#define FREE_PAGES free_pages()
+
+/* Helper for just getting the total number of claimed pages for ASSERTs */
+static uint64_t __used total_claims(void)
+{
+    uint64_t free_pages, total_claims;
+    bool verbose_asserts_save = testcase_assert_verbose_assertions;
+
+    /* Avoid logging spinlock actions while getting the total claims */
+    testcase_assert_verbose_assertions = false;
+    get_outstanding_claims(&free_pages, &total_claims);
+    testcase_assert_verbose_assertions = verbose_asserts_save;
+    return total_claims;
+}
+#define TOTAL_CLAIMS total_claims()
+
+#define DOM_GLOBAL_CLAIMS(d)  ((d)->global_claims)
+#define DOM_NODE_CLAIMS(d, n) ((d)->claims[n])
+
+#endif /* CONFIG_SYSCTL */
+
+static void print_order_list(nodeid_t node, zone_t zone, size_t order)
+{
+    struct page_info *pos = page_list_first(&heap(node, zone, order));
+
+    if ( pos )
+        printf("    Heap for zone %zu, order %zu:\n", zone, order);
+
+    while ( pos )
+    {
+        size_t page_order = PFN_ORDER(pos);
+
+        print_page_info(pos);
+        /* Print the subpages of the buddy head */
+        for ( size_t sub_pg = 1; sub_pg < (1U << page_order); sub_pg++ )
+        {
+            struct page_info *sub_pos = pos + sub_pg;
+
+            printf("  ");
+            print_page_info(sub_pos);
+            /* Assert the subpages of a buddy to have order-0. */
+            ASSERT(PFN_ORDER(sub_pos) == 0);
+        }
+        /* Assert that the page_order matches the heap order. */
+        if ( page_order != order )
+        {
+            printf("ERROR:mfn %lu has order %zu but expected %zu "
+                   "based on heap position\n",
+                   page_to_mfn(pos), page_order, order);
+            ASSERT(page_order == order);
+        }
+        pos = pos->list_next;
+    }
+}
+
+#define CHECK_BUDDY(pages, fmt, ...) \
+        check_buddy(pages, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
+
+/* Function to print the order and first_dirty of each page for debugging. */
+static void check_buddy(struct page_info *pages, const char *file, int line,
+                        const char *fmt, ...)
+{
+    size_t size = 1U << PFN_ORDER(pages);
+    bool verbose_asserts_save = testcase_assert_verbose_assertions;
+    va_list args;
+
+    if ( fmt ) /* Print the given message for context in the logs.*/
+    {
+        va_start(args, fmt);
+        printf("  %s:%d: ", file, line);
+        vprintf(fmt, args);
+        puts(":");
+        va_end(args);
+    }
+    else
+        printf("  %s:%d: %s():\n", file, line, __func__);
+
+    /* Avoid logging internal assertions while logging the free list status */
+    testcase_assert_verbose_assertions = false;
+
+    /*
+     * Inside pages, first_dirty must (if not INVALID_DIRTY_IDX) index the
+     * (first) page itself or a subpage within the page's range (<= 2^order).
+     */
+    for ( size_t i = 0; i < size; i++ )
+    {
+        unsigned long first_dirty = pages[i].u.free.first_dirty;
+        unsigned int tail_offset = (1U << PFN_ORDER(&pages[i])) - 1;
+
+        if ( first_dirty != INVALID_DIRTY_IDX && first_dirty > tail_offset )
+        {
+            printf("page at index %zu has first_dirty %lx but expected <= %u "
+                   "based on its order\n",
+                   i, first_dirty, tail_offset);
+            ASSERT(pages[i].u.free.first_dirty == tail_offset);
+        }
+    }
+
+    /* Traverse the offlined list, print and assert errors in it. */
+    struct page_info *pos = page_list_first(&page_offlined_list);
+    if ( pos )
+        puts("    Offlined list:");
+    while ( pos )
+    {
+        print_and_assert_offlined_page(pos);
+        pos = pos->list_next;
+    }
+
+    /* Traverse the broken list, print and assert errors in it. */
+    pos = page_list_first(&page_broken_list);
+    if ( pos )
+        puts("    Broken list:");
+    while ( pos )
+    {
+        print_and_assert_offlined_page(pos);
+        pos = pos->list_next;
+    }
+
+    /*
+     * Traverse the _heap[node] for each order and zone and print and assert
+     * the order and first_dirty of each page for each heap for debugging.
+     *
+     * This is to help verify that the heap structure is consistent with the
+     * page_info order fields after operations that manipulate both, such as
+     * reserve_offlined_page().
+     */
+    for ( nodeid_t node = 0; node < MAX_NUMNODES; node++ )
+        for ( size_t order = 0; order <= MAX_ORDER; order++ )
+            for ( size_t zone = 0; zone < NR_ZONES; zone++ )
+                print_order_list(node, zone, order);
+    testcase_assert_verbose_assertions = verbose_asserts_save;
+}
+
+/*
+ * Failure reporting helper that prints the provided message, the test
+ * caller context and a native backtrace before aborting.
+ */
+static void fail_with_ctx(const char *caller_file, const char *caller_func,
+                          int caller_line, const char *fmt, ...)
+{
+    va_list ap;
+
+    fprintf(stderr, "\n- %s: Assertion failed: ", caller_func);
+    va_start(ap, fmt);
+    testcase_assert(false, caller_file, caller_line, caller_func, fmt, ap);
+    vfprintf(stderr, fmt, ap);
+    va_end(ap);
+}
+
+/*
+ * Assert that a page_list matches the provided sequence of page pointers.
+ *
+ * The public helper below is a macro so call sites can provide a simple list
+ * of page pointers while the implementation works over an ordinary array.
+ */
+static void __used assert_list_eq_array(struct page_list_head *list,
+                                        struct page_info *const expected[],
+                                        unsigned int nr_expected,
+                                        const char *call_file,
+                                        const char *caller_func,
+                                        int caller_line)
+{
+    struct page_info *pos;
+    int fails_before = testcase_assert_expected_failures;
+    unsigned int index = 0;
+
+    if ( list->count != nr_expected )
+        fail_with_ctx(call_file, caller_func, caller_line,
+                      "list count mismatch: expected %u, got %u", nr_expected,
+                      list->count);
+
+    if ( nr_expected == 0 )
+    {
+        if ( page_list_first(list) != NULL )
+            fail_with_ctx(call_file, caller_func, caller_line,
+                          "expected empty list but head != NULL");
+        else
+            testcase_assert_successful_assert_total++;
+        return;
+    }
+
+    if ( page_list_first(list) != expected[0] )
+        fail_with_ctx(call_file, caller_func, caller_line,
+                      "list head mismatch: expected %p, got %p", expected[0],
+                      page_list_first(list));
+
+    for ( pos = page_list_first(list); pos; pos = pos->list_next, index++ )
+    {
+        if ( index >= nr_expected )
+            fail_with_ctx(call_file, caller_func, caller_line,
+                          "list contains more elements than expected");
+
+        if ( pos != expected[index] )
+            fail_with_ctx(call_file, caller_func, caller_line,
+                          "element %u mismatch: expected %p, got %p", index,
+                          expected[index], pos);
+
+        if ( pos->list_prev != (index ? expected[index - 1] : NULL) )
+            fail_with_ctx(call_file, caller_func, caller_line,
+                          "list_prev mismatch at index %u", index);
+
+        if ( pos->list_next !=
+             (index + 1 < nr_expected ? expected[index + 1] : NULL) )
+            fail_with_ctx(call_file, caller_func, caller_line,
+                          "list_next mismatch at index %u", index);
+    }
+
+    if ( index != nr_expected )
+        fail_with_ctx(
+            call_file, caller_func, caller_line,
+            "list element count consumed mismatch: expected %u, got %u",
+            nr_expected, index);
+
+    if ( testcase_assert_expected_failures == fails_before )
+        testcase_assert_successful_assert_total++;
+}
+
+/** Assert that a page_list matches the provided sequence of page pointers. */
+#define ASSERT_LIST_EQUAL(list, ...)                                     \
+        do                                                               \
+        {                                                                \
+            struct page_info *const expected[] = {__VA_ARGS__};          \
+            assert_list_eq_array((list), expected, ARRAY_SIZE(expected), \
+                                 __FILE__,                               \
+                                 __func__, __LINE__);                    \
+        } while ( 0 )
+/** Assert that a page_list is empty. */
+#define ASSERT_LIST_EMPTY(list) ASSERT(page_list_empty(list))
diff --git a/tools/tests/alloc/page_alloc_shim.h 
b/tools/tests/alloc/page_alloc_shim.h
new file mode 100644
index 000000000000..74459b6dda61
--- /dev/null
+++ b/tools/tests/alloc/page_alloc_shim.h
@@ -0,0 +1,433 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Header-only shim for unit testing functions in xen/common/page_alloc.c.
+ *
+ * This header provides the necessary definitions and helpers to allow:
+ *
+ * 1) the test program to include the real page_alloc.c directly into its
+ *    translation unit, and
+ *
+ * 2) the test scenarios to manipulate the allocator state and verify its
+ *    behavior in a way that is consistent with the real page_alloc.c, while
+ *    being self-contained and suitable for unit testing.
+ *
+ * The test page allocator shim provides the necessary definitions for the
+ * page_info structure and the translation functions between page_info
+ * pointers and MFNs, so that page_alloc.c can manipulate the page_info
+ * structures in the test frame table and verify its behavior in a way
+ * that is consistent with how page_alloc.c acts when used by the running
+ * hypervisor, while being self-contained and suitable for unit testing.
+ *
+ * Shim definitions for Xen types and functions used by page_alloc.c.
+ *
+ * This shim is intended to be included directly in the test program
+ * or header-only library for testing a specific scenario, included
+ * by the test program, for unit testing functions in common/page_alloc.c.
+ *
+ * It provides stubs (minimal definitions for Xen types and functions)
+ * used by page_alloc.c, sufficient to support the test scenarios in
+ * tools/tests/alloc.
+ *
+ * It is not intended to be complete or accurate for general use in
+ * other test contexts or as a general-purpose shim for page_alloc.c.
+ *
+ * Copyright (C) 2026 Cloud Software Group
+ */
+#ifndef _TEST_ALLOC_PAGE_ALLOC_SHIM_
+#define _TEST_ALLOC_PAGE_ALLOC_SHIM_
+
+/*.
+ * Guard against language servers and linters picking up this header in
+ * the wrong context.
+ *
+ * This header is only intended to be used in the test program for unit
+ * testing functions in xen/common/page_alloc.c, and test programs define
+ * TEST_USES_PAGE_ALLOC_SHIM to enable the definitions in this header.
+ */
+#ifndef TEST_USES_PAGE_ALLOC_SHIM
+#warning "This header is only for use in page_alloc.c unit tests."
+#else
+/*
+ * Inside the intended test context, provide mocks and stub definitions.
+ */
+
+/* Configure the included headers for the test context */
+#ifndef CONFIG_NR_CPUS
+#define CONFIG_NR_CPUS 64
+#endif
+
+#if defined(CONFIG_NUMA) && !defined(CONFIG_NR_NUMA_NODES)
+#define CONFIG_NR_NUMA_NODES 64
+#endif
+
+#define CONFIG_SCRUB_DEBUG
+
+/* Provide struct page_info and related Xen definitions */
+#include "mock-page_list.h"
+
+/* Include the common check_asserts library for test assertions */
+#include "check-asserts.h"
+
+/*
+ * We add the Xen headers to the include path so page_alloc.c can
+ * resolve its #include directives without having to replicate all
+ * headers as actual files in the test tree:
+ *
+ * We define the header guards of those files to prevent unwanted
+ * definitions from those headers that conflict with the test harness.
+ */
+#define XEN_SOFTIRQ_H
+#define XEN__XVMALLOC_H
+#define _LINUX_INIT_H
+#define _XEN_PARAM_H
+#define __LIB_H__ /* C runtime library, only for the hypervisor */
+#define __LINUX_NODEMASK_H
+#define __FLUSHTLB_H__
+#define __SCHED_H__
+#define __SPINLOCK_H__
+#define __TYPES_H__ /* Conflicts with the compiler-provided types */
+#define __VM_EVENT_H__
+#define __X86_PAGE_H__
+#define __XEN_CPUMASK_H
+#define __XEN_EVENT_H__
+#define __XEN_FRAME_NUM_H__
+#define __XEN_IRQ_H__
+#define __XEN_MM_H__
+#define __XEN_PDX_H__
+
+#include <xen/keyhandler.h>
+#include <xen/page-size.h>
+#include <public/xen.h>
+
+/* Include xen/numa.h with stubs and unused parameter warnings disabled */
+#define cpumask_clear_cpu(cpu, mask) ((void)(cpu), (void)(mask))
+#define mfn_to_pdx(mfn)              ((unsigned long)(mfn))
+#pragma GCC diagnostic push
+#ifndef CONFIG_NUMA
+#pragma GCC diagnostic ignored "-Wunused-parameter"
+#endif
+#include <xen/numa.h>
+#pragma GCC diagnostic pop
+
+/* Flexible definition to support 32- and 64-bit architectures */
+#undef PADDR_BITS
+#define PADDR_BITS              (BITS_PER_LONG - PAGE_SHIFT)
+#define pfn_to_paddr(pfn)       ((paddr_t)(pfn) << PAGE_SHIFT)
+#define paddr_to_pfn(pa)        ((unsigned long)((pa) >> PAGE_SHIFT))
+#define INVALID_MFN_INITIALIZER (~0UL)
+
+typedef unsigned long nodemask_t;
+
+struct domain {
+    spinlock_t     page_alloc_lock;
+    nodemask_t     node_affinity;
+    nodeid_t       last_alloc_node;
+    domid_t        domain_id;
+    unsigned int   tot_pages;
+    unsigned int   max_pages;
+    unsigned int   extra_pages;
+    unsigned int   global_claims;
+    unsigned int   node_claims;
+    unsigned int   claims[MAX_NUMNODES];
+    unsigned int   xenheap_pages;
+    bool           is_dying;
+    struct domain *next_in_list;
+};
+extern struct domain *domain_list;
+
+struct vcpu {
+    struct domain *domain;
+};
+
+/*
+ * Provide two domains for the test context, so that test helpers can
+ * call allocator functions that require domain context and verify behavior
+ * that depends on domain state, such as claims accounting and page allocation
+ * for specific domains.
+ */
+static struct domain test_dummy_domain1;
+static struct domain test_dummy_domain2;
+static struct domain __used *dom1 = &test_dummy_domain1;
+static struct domain __used *dom2 = &test_dummy_domain2;
+
+/* To provide a current vcpu/domain pair for code paths that inspect it. */
+static unsigned char test_dummy_storage[PAGE_SIZE];
+static struct vcpu test_current_vcpu;
+static struct vcpu *current = &test_current_vcpu;
+static cpumask_t cpu_online_map = ~0UL;
+
+#define for_each_domain(_d) \
+        for ( (_d) = domain_list; (_d) != NULL; (_d) = (_d)->next_in_list )
+#define for_each_online_node(i) for ( (i) = 0; (i) < MAX_NUMNODES; ++(i) )
+#define for_each_cpu(i, mask)   for ( (i) = 0; (i) < 1; ++(i) )
+
+/* dom_cow is a domain pointer used by the memory sharing code */
+#ifdef CONFIG_MEM_SHARING
+static struct domain *dom_cow;
+#else
+#define dom_cow NULL
+#endif
+
+/*
+ * Logging spinlock for the test context
+ */
+static spinlock_t *heap_lock_ptr;
+
+/* Helper function to track spinlock actions for additional context */
+static void print_spinlock(const char *action, spinlock_t *lock,
+                           const char *file, int line, const char *func)
+{
+    const char *relpath = file;
+
+    if ( !testcase_assert_verbose_assertions )
+        return;
+
+    while ( (file = strstr(relpath, "../")) )
+        relpath += 3;
+
+    for ( int i = 0; i < testcase_assert_verbose_indent_level; i++ )
+        printf("  ");
+
+    /* Print the path first:*/
+    if ( testcase_assert_current_func == NULL ||
+         strcmp(testcase_assert_current_func, func) != 0 )
+        printf("%s:%d: %s(): ", relpath, line, func);
+    else
+        printf("%s:%d: ", relpath, line);
+
+    if ( lock == heap_lock_ptr )
+        printf("heap_lock %s\n", action);
+    else if ( domain_list && lock == &test_dummy_domain1.page_alloc_lock )
+        printf("dom1->page_alloc_lock %s\n", action);
+    else if ( domain_list && lock == &test_dummy_domain2.page_alloc_lock )
+        printf("dom2->page_alloc_lock %s\n", action);
+    else
+        printf("unknown lock %p %s\n", (void *)lock, action);
+}
+
+/*
+ * If testcase_assert_verbose_assertions is enabled, the spinlock
+ * functions print the spinlock being acquired or released along with
+ * the file and line number of the assertion that triggered it.
+ * This can be helpful for debugging test failures and understanding
+ * the sequence of events leading up to the failure.
+ */
+#define spin_lock(l) \
+        (print_spinlock("acquired", l, __FILE__, __LINE__, __func__), 
(void)(l))
+#define spin_unlock(l) \
+        (print_spinlock("released", l, __FILE__, __LINE__, __func__), 
(void)(l))
+#define spin_lock_cb(l, cb, data) spin_lock(l)
+#define spin_lock_kick()          ((void)0)
+#define nrspin_lock(l)            spin_lock(l)
+#define nrspin_unlock(l)          spin_unlock(l)
+#define rspin_lock(l)             spin_lock(l)
+#define rspin_unlock(l)           spin_unlock(l)
+#define DEFINE_SPINLOCK(l)        spinlock_t l
+/*
+ * For the test context, we assume all locks are always held to avoid having
+ * to manage lock state in the test helpers.  This allows the test helpers
+ * to call allocator functions that require locks to be held without needing
+ * to acquire those locks, which simplifies the test code and focuses on
+ * exercising the allocator logic under test.
+ *
+ * Invariants that would normally be protected by locks must still be upheld
+ * by the test helpers, but the test helpers can assume they have exclusive
+ * access to the allocator state and do not need to worry about concurrency.
+ */
+#define spin_is_locked(l)         true
+#define rspin_is_locked(l)        true
+
+/* memflags: */
+#define _MEMF_no_refcount         0
+#define MEMF_no_refcount          (1U << _MEMF_no_refcount)
+#define _MEMF_populate_on_demand  1
+#define MEMF_populate_on_demand   (1U << _MEMF_populate_on_demand)
+#define _MEMF_keep_scrub          2
+#define MEMF_keep_scrub           (1U << _MEMF_keep_scrub)
+#define _MEMF_no_dma              3
+#define MEMF_no_dma               (1U << _MEMF_no_dma)
+#define _MEMF_exact_node          4
+#define MEMF_exact_node           (1U << _MEMF_exact_node)
+#define _MEMF_no_owner            5
+#define MEMF_no_owner             (1U << _MEMF_no_owner)
+#define _MEMF_no_tlbflush         6
+#define MEMF_no_tlbflush          (1U << _MEMF_no_tlbflush)
+#define _MEMF_no_icache_flush     7
+#define MEMF_no_icache_flush      (1U << _MEMF_no_icache_flush)
+#define _MEMF_no_scrub            8
+#define MEMF_no_scrub             (1U << _MEMF_no_scrub)
+#define _MEMF_node                16
+#define MEMF_node_mask            ((1U << (8 * sizeof(nodeid_t))) - 1)
+#define MEMF_node(n)              ((((n) + 1)&MEMF_node_mask) << _MEMF_node)
+#define MEMF_get_node(f)          ((((f) >> _MEMF_node) - 1)&MEMF_node_mask)
+#define _MEMF_bits                24
+#define MEMF_bits(n)              ((n) << _MEMF_bits)
+
+#define string_param(name, var)
+#define custom_param(name, fn)
+#define size_param(name, var)
+#define boolean_param(name, func)
+#define integer_param(name, var)
+#define ACCESS_ONCE(x) (x)
+#define cmpxchg(ptr, oldv, newv) \
+        ({                       \
+             *(ptr) = (newv);    \
+             (oldv);             \
+         })
+
+#define is_xen_heap_page(pg)          false
+#define page_to_virt(pg)              ((void *)(pg))
+#define virt_to_page(v)               ((struct page_info *)(v))
+#define mfn_to_virt(mfn)              ((void *)&test_dummy_storage)
+#define __mfn_to_virt(mfn)            mfn_to_virt(mfn)
+#define _mfn(x)                       ((mfn_t)(x))
+#define mfn_x(x)                      ((unsigned long)(x))
+#define mfn_add(mfn, nr)              ((mfn) + (nr))
+#define mfn_min(a, b)                 ((a) < (b) ? (a) : (b))
+
+/*
+ * NUMA stubs for unit testing NUMA-aware page allocator logic.
+ *
+ * nodemask_test() and node_set() implement real bit operations so that
+ * domain_install_claim_set() can correctly detect duplicate node entries
+ * in a claim set. mfn_to_pdx() is defined before xen/numa.h is included.
+ */
+
+static nodemask_t node_online_map = ~0UL;
+#define num_online_nodes()            MAX_NUMNODES
+#define node_online(node)             ((node) < MAX_NUMNODES)
+#define nodes_intersects(a, b)        ((a) & (b))
+#define nodes_and(dst, a, b)          ((dst) = (a) & (b))
+#define nodes_andnot(dst, a, b)       ((dst) = (a) & ~(b))
+#define nodes_clear(dst)              ((dst) = 0)
+#define nodemask_test(node, mask)     ((*(mask) >> (node)) & 1UL)
+#define node_set(node, mask)          ((mask) |= (1UL << (node)))
+#define node_clear(node, mask)        ((void)(mask))
+#define node_test_and_set(node, mask) false
+#define first_node(mask)              0U
+#define next_node(node, mask)         MAX_NUMNODES
+#define cycle_node(node, mask)        0U
+
+#ifdef CONFIG_NUMA
+#define __node_distance(a, b) 0
+nodeid_t cpu_to_node[NR_CPUS];
+cpumask_t node_to_cpumask[MAX_NUMNODES];
+struct node_data node_data[MAX_NUMNODES];
+unsigned int memnode_shift;
+
+static typeof(*memnodemap) _memnodemap[64];
+nodeid_t *memnodemap = _memnodemap;
+unsigned long memnodemapsize = sizeof(_memnodemap);
+#endif /* CONFIG_NUMA */
+
+/*
+ * Stub definitions for Xen functions and macros used by page_alloc.c,
+ * sufficient to support the test scenarios in tools/tests/alloc.
+ *
+ * These are not intended to be complete or accurate for general use
+ * in other test contexts or as a general-purpose shim for page_alloc.c.
+ */
+#define rcu_lock_domain(id)               (&test_dummy_domain1)
+#define rcu_lock_domain_by_any_id(id)     (&test_dummy_domain1)
+#define NOW()                             0LL
+#define SYS_STATE_active                  1
+#define system_state                      0
+#define cpu_online(cpu)                   ((cpu) == 0)
+#define smp_processor_id()                0U
+#define smp_wmb()                         ((void)0)
+#define cpumask_empty(mask)               true
+#define cpumask_clear(mask)               ((void)(mask))
+#define cpumask_and(dst, a, b)            ((void)(dst), (void)(a), (void)(b))
+#define cpumask_or(dst, a, b)             ((void)(dst), (void)(a), (void)(b))
+#define cpumask_copy(dst, src)            ((void)(dst), (void)(src))
+#define cpumask_first(mask)               0U
+#define cpumask_intersects(a, b)          false
+#define cpumask_weight(mask)              1
+#define __cpumask_set_cpu(cpu, mask)      ((void)(cpu), (void)(mask))
+#define page_get_owner(pg)                ((pg)->owner)
+#define page_set_owner(pg, d)             ((pg)->owner = (d))
+#define page_get_owner_and_reference(pg)  ((pg)->owner)
+#define page_set_tlbflush_timestamp(pg)   ((pg)->tlbflush_timestamp = 0)
+#define set_gpfn_from_mfn(mfn, gpfn)      ((void)0)
+#define page_is_offlinable(mfn)           true
+#define is_xen_fixed_mfn(mfn)             false
+#define filtered_flush_tlb_mask(ts)       ((void)(ts))
+#define accumulate_tlbflush(need, pg, ts) ((void)(need), (void)(pg), 
(void)(ts))
+#define flush_page_to_ram(mfn, icache)    ((void)(mfn), (void)(icache))
+#define scrub_page_hot(ptr)               clear_page_hot(ptr)
+#define scrub_page_cold(ptr)              clear_page_cold(ptr)
+#define send_global_virq(virq)            ((void)(virq))
+#define softirq_pending(cpu)              false
+#define process_pending_softirqs()        ((void)0)
+#define on_selected_cpus(msk, f, data, w) ((void)0)
+#define cpu_relax()                       ((void)0)
+#define xmalloc(type)                     calloc(1, sizeof(type))
+#define xmalloc_array(type, nr)           calloc((nr), sizeof(type))
+#define xvzalloc_array(type, nr)          calloc((nr), sizeof(type))
+#define xvmalloc_array(type, nr)          calloc((nr), sizeof(type))
+#define get_order_from_pages(nr)          0U
+#define get_order_from_bytes(bytes)       0U
+#define arch_mfns_in_directmap(mfn, nr)   true
+#define maddr_to_mfn(pa)                  ((mfn_t)paddr_to_pfn(pa))
+
+#define ASSERT_ALLOC_CONTEXT()              ((void)0)
+#define arch_free_heap_page(d, pg)          ((void)(d), (void)(pg))
+#define get_knownalive_domain(d)            ((void)(d))
+#define domain_clamp_alloc_bitsize(d, bits) (bits)
+#define mem_paging_enabled(d)               false
+#define put_domain(d)                       ((void)(d))
+#define clear_page_hot(ptr)                 memset((ptr), 0, PAGE_SIZE)
+#define clear_page_cold(ptr)                memset((ptr), 0, PAGE_SIZE)
+#define unmap_domain_page(ptr)              ((void)(ptr))
+#define put_page(pg)                        ((void)(pg))
+
+void *alloc_xenheap_pages(unsigned int order, unsigned int memflags);
+void  init_domheap_pages(paddr_t ps, paddr_t pe);
+struct page_info *alloc_domheap_pages(struct domain *d, unsigned int order,
+                                      unsigned int memflags);
+
+/* Additional stubs for test support */
+
+unsigned int arch_get_dma_bitsize(void)
+{
+    return 32U;
+}
+
+/* Return number of pages currently posessed by the domain */
+static inline unsigned int domain_tot_pages(const struct domain *d)
+{
+    assert(d->extra_pages <= d->tot_pages);
+    return d->tot_pages - d->extra_pages;
+}
+
+/* LLC (Last Level Cache) coloring support stubs */
+#define llc_coloring_enabled                false
+unsigned int get_max_nr_llc_colors(void)
+{
+    return 1U;
+}
+unsigned int page_to_llc_color(const struct page_info *pg)
+{
+    (void)pg;
+    return 0U;
+}
+
+#define parse_bool(s, e) (-1) /* Not parsed, use the default */
+
+void __init register_keyhandler(unsigned char key, keyhandler_fn_t *fn,
+                                const char *desc, bool diagnostic)
+{
+    (void)key;
+    (void)fn;
+    (void)desc;
+    (void)diagnostic;
+}
+
+unsigned long simple_strtoul(const char *cp, const char **endp,
+                             unsigned int base)
+{
+    return strtoul(cp, (char **)endp, base);
+}
+
+#endif /* TEST_USES_PAGE_ALLOC_SHIM */
+#endif /* _TEST_ALLOC_PAGE_ALLOC_SHIM_ */
-- 
2.39.5




 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.