From: Peter Xu <peterx@redhat.com>
To: qemu-devel@nongnu.org
Cc: "Peter Xu" <peterx@redhat.com>, "Fabiano Rosas" <farosas@suse.de>,
"Paolo Bonzini" <pbonzini@redhat.com>,
"Marc-André Lureau" <marcandre.lureau@redhat.com>
Subject: [PULL 17/18] tests: add unit tests for RamDiscardManager multi-source aggregation
Date: Tue, 23 Jun 2026 08:47:58 -0400 [thread overview]
Message-ID: <20260623124759.125399-18-peterx@redhat.com> (raw)
In-Reply-To: <20260623124759.125399-1-peterx@redhat.com>
From: Marc-André Lureau <marcandre.lureau@redhat.com>
Add various unit tests for the RamDiscardManager multi-source
aggregation functionality.
The test uses a TestRamDiscardSource QOM object that tracks populated
state via a bitmap, similar to RamBlockAttributes implementation.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Link: https://lore.kernel.org/r/20260604-rdm5-v5-10-5768e6a0943d@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
MAINTAINERS | 2 +
tests/unit/test-ram-discard-manager-stubs.c | 48 +
tests/unit/test-ram-discard-manager.c | 1235 +++++++++++++++++++
tests/unit/meson.build | 8 +-
4 files changed, 1292 insertions(+), 1 deletion(-)
create mode 100644 tests/unit/test-ram-discard-manager-stubs.c
create mode 100644 tests/unit/test-ram-discard-manager.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 18b659a44c..fc6d035d4c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3363,6 +3363,8 @@ F: system/memory-internal.h
F: system/ram-block-attributes.c
F: system/ram-discard-manager.c
F: scripts/coccinelle/memory-region-housekeeping.cocci
+F: tests/unit/test-ram-discard-manager.c
+F: tests/unit/test-ram-discard-manager-stubs.c
Memory devices
M: David Hildenbrand <david@kernel.org>
diff --git a/tests/unit/test-ram-discard-manager-stubs.c b/tests/unit/test-ram-discard-manager-stubs.c
new file mode 100644
index 0000000000..74b126fd3f
--- /dev/null
+++ b/tests/unit/test-ram-discard-manager-stubs.c
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "qemu/osdep.h"
+#include "qom/object.h"
+#include "glib.h"
+#include "system/memory.h"
+
+RamDiscardManager *memory_region_get_ram_discard_manager(MemoryRegion *mr)
+{
+ return mr->rdm;
+}
+
+int memory_region_add_ram_discard_source(MemoryRegion *mr,
+ RamDiscardSource *source)
+{
+ if (!mr->rdm) {
+ mr->rdm = ram_discard_manager_new(mr);
+ }
+ return ram_discard_manager_add_source(mr->rdm, source);
+}
+
+int memory_region_del_ram_discard_source(MemoryRegion *mr,
+ RamDiscardSource *source)
+{
+ RamDiscardManager *rdm = mr->rdm;
+
+ if (!rdm) {
+ return 0;
+ }
+
+ return ram_discard_manager_del_source(rdm, source);
+}
+
+uint64_t memory_region_size(const MemoryRegion *mr)
+{
+ return int128_get64(mr->size);
+}
+
+MemoryRegionSection *memory_region_section_new_copy(MemoryRegionSection *s)
+{
+ MemoryRegionSection *copy = g_new(MemoryRegionSection, 1);
+ *copy = *s;
+ return copy;
+}
+
+void memory_region_section_free_copy(MemoryRegionSection *s)
+{
+ g_free(s);
+}
diff --git a/tests/unit/test-ram-discard-manager.c b/tests/unit/test-ram-discard-manager.c
new file mode 100644
index 0000000000..3d39a1e94b
--- /dev/null
+++ b/tests/unit/test-ram-discard-manager.c
@@ -0,0 +1,1235 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#include "qemu/osdep.h"
+#include "qemu/bitmap.h"
+#include "qemu/module.h"
+#include "qemu/main-loop.h"
+#include "qapi/error.h"
+#include "qom/object.h"
+#include "qom/qom-qobject.h"
+#include "glib.h"
+#include "system/memory.h"
+
+#define TYPE_TEST_RAM_DISCARD_SOURCE "test-ram-discard-source"
+
+OBJECT_DECLARE_SIMPLE_TYPE(TestRamDiscardSource, TEST_RAM_DISCARD_SOURCE)
+
+struct TestRamDiscardSource {
+ Object parent;
+
+ MemoryRegion *mr;
+ uint64_t granularity;
+ unsigned long *bitmap;
+ uint64_t bitmap_size;
+};
+
+static uint64_t test_rds_get_min_granularity(const RamDiscardSource *rds,
+ const MemoryRegion *mr)
+{
+ TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
+
+ g_assert(mr == src->mr);
+ return src->granularity;
+}
+
+static bool test_rds_is_populated(const RamDiscardSource *rds,
+ const MemoryRegionSection *section)
+{
+ TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
+ uint64_t offset = section->offset_within_region;
+ uint64_t size = int128_get64(section->size);
+ uint64_t first_bit = offset / src->granularity;
+ uint64_t last_bit = (offset + size - 1) / src->granularity;
+ unsigned long found;
+
+ g_assert(section->mr == src->mr);
+
+ /* Check if any bit in range is zero (discarded) */
+ found = find_next_zero_bit(src->bitmap, last_bit + 1, first_bit);
+ return found > last_bit;
+}
+
+static void test_rds_class_init(ObjectClass *klass, const void *data)
+{
+ RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_CLASS(klass);
+
+ rdsc->get_min_granularity = test_rds_get_min_granularity;
+ rdsc->is_populated = test_rds_is_populated;
+}
+
+static const TypeInfo test_rds_info = {
+ .name = TYPE_TEST_RAM_DISCARD_SOURCE,
+ .parent = TYPE_OBJECT,
+ .instance_size = sizeof(TestRamDiscardSource),
+ .class_init = test_rds_class_init,
+ .interfaces = (const InterfaceInfo[]) {
+ { TYPE_RAM_DISCARD_SOURCE },
+ { }
+ },
+};
+
+static TestRamDiscardSource *test_source_new(MemoryRegion *mr,
+ uint64_t granularity)
+{
+ TestRamDiscardSource *src;
+ uint64_t region_size = memory_region_size(mr);
+
+ src = TEST_RAM_DISCARD_SOURCE(object_new(TYPE_TEST_RAM_DISCARD_SOURCE));
+ src->mr = mr;
+ src->granularity = granularity;
+ src->bitmap_size = DIV_ROUND_UP(region_size, granularity);
+ src->bitmap = bitmap_new(src->bitmap_size);
+
+ return src;
+}
+
+static void test_source_free(TestRamDiscardSource *src)
+{
+ g_free(src->bitmap);
+ object_unref(OBJECT(src));
+}
+
+static void test_source_populate(TestRamDiscardSource *src,
+ uint64_t offset, uint64_t size)
+{
+ uint64_t first_bit = offset / src->granularity;
+ uint64_t nbits = size / src->granularity;
+
+ bitmap_set(src->bitmap, first_bit, nbits);
+}
+
+static void test_source_discard(TestRamDiscardSource *src,
+ uint64_t offset, uint64_t size)
+{
+ uint64_t first_bit = offset / src->granularity;
+ uint64_t nbits = size / src->granularity;
+
+ bitmap_clear(src->bitmap, first_bit, nbits);
+}
+
+typedef struct TestListener {
+ RamDiscardListener rdl;
+ int populate_count;
+ int discard_count;
+ uint64_t last_populate_offset;
+ uint64_t last_populate_size;
+ uint64_t last_discard_offset;
+ uint64_t last_discard_size;
+ int fail_on_populate; /* Return error on Nth populate */
+ int populate_call_num;
+} TestListener;
+
+static int test_listener_populate(RamDiscardListener *rdl,
+ const MemoryRegionSection *section)
+{
+ TestListener *tl = container_of(rdl, TestListener, rdl);
+
+ tl->populate_call_num++;
+ if (tl->fail_on_populate > 0 &&
+ tl->populate_call_num >= tl->fail_on_populate) {
+ return -ENOMEM;
+ }
+
+ tl->populate_count++;
+ tl->last_populate_offset = section->offset_within_region;
+ tl->last_populate_size = int128_get64(section->size);
+ return 0;
+}
+
+static void test_listener_discard(RamDiscardListener *rdl,
+ const MemoryRegionSection *section)
+{
+ TestListener *tl = container_of(rdl, TestListener, rdl);
+
+ tl->discard_count++;
+ tl->last_discard_offset = section->offset_within_region;
+ tl->last_discard_size = int128_get64(section->size);
+}
+
+static void test_listener_init(TestListener *tl)
+{
+ ram_discard_listener_init(&tl->rdl,
+ test_listener_populate,
+ test_listener_discard);
+}
+
+#define TEST_REGION_SIZE (16 * 1024 * 1024) /* 16 MB */
+#define GRANULARITY_4K (4 * 1024)
+#define GRANULARITY_2M (2 * 1024 * 1024)
+
+static MemoryRegion *test_mr;
+
+static void test_setup(void)
+{
+ test_mr = g_new0(MemoryRegion, 1);
+ test_mr->size = int128_make64(TEST_REGION_SIZE);
+ test_mr->ram = true;
+}
+
+static void test_teardown(void)
+{
+ g_clear_pointer(&test_mr->rdm, object_unref);
+ object_unparent(OBJECT(test_mr));
+ g_free(test_mr);
+ test_mr = NULL;
+}
+
+static void test_single_source_basic(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+ g_assert_null(rdm);
+
+ /* Add source */
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+ g_assert_nonnull(rdm);
+
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
+ ==, GRANULARITY_4K);
+
+ /* Initially all discarded */
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(GRANULARITY_4K);
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate a range in source */
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
+
+ /* Now should be populated */
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Check larger section */
+ section.size = int128_make64(GRANULARITY_4K * 4);
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Check section that spans populated and discarded */
+ section.size = int128_make64(GRANULARITY_4K * 8);
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ test_source_free(src);
+ test_teardown();
+}
+
+static void test_single_source_listener(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Populate some ranges before adding listener */
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+ g_assert_nonnull(rdm);
+
+ /* Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Should have been notified about populated regions */
+ g_assert_cmpint(tl.populate_count, ==, 2);
+
+ /* Notify populate for new range */
+ tl.populate_count = 0;
+ test_source_populate(src, GRANULARITY_4K * 16, GRANULARITY_4K * 2);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 16,
+ GRANULARITY_4K * 2);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 16);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 2);
+
+ /* Notify discard */
+ tl.discard_count = 0;
+ test_source_discard(src, 0, GRANULARITY_4K * 4);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
+ 0, GRANULARITY_4K * 4);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+ g_assert_cmpuint(tl.last_discard_offset, ==, 0);
+ g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
+
+ /* Unregister listener */
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+static void test_two_sources_same_granularity(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Add first source */
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+
+ /* Add second source */
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+ g_assert_nonnull(rdm);
+
+ /* Check granularity */
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
+ ==, GRANULARITY_4K);
+
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(GRANULARITY_4K);
+
+ /* Both discarded -> aggregated discarded */
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate in src1 only */
+ test_source_populate(src1, 0, GRANULARITY_4K);
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate in src2 only */
+ test_source_discard(src1, 0, GRANULARITY_4K);
+ test_source_populate(src2, 0, GRANULARITY_4K);
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate in both -> aggregated populated */
+ test_source_populate(src1, 0, GRANULARITY_4K);
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Remove sources */
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Two sources with different granularities (4K and 2M).
+ * The aggregated granularity should be GCD(4K, 2M) = 4K.
+ */
+static void test_two_sources_different_granularity(void)
+{
+ TestRamDiscardSource *src_4k, *src_2m;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ int ret;
+
+ test_setup();
+
+ src_4k = test_source_new(test_mr, GRANULARITY_4K);
+ src_2m = test_source_new(test_mr, GRANULARITY_2M);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src_4k));
+ g_assert_cmpint(ret, ==, 0);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src_2m));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
+ ==, GRANULARITY_4K);
+
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(GRANULARITY_4K);
+
+ /* Both discarded */
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate 4K in src_4k, but src_2m still discarded the whole 2M block */
+ test_source_populate(src_4k, 0, GRANULARITY_4K);
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate 2M in src_2m (which includes the 4K block) */
+ test_source_populate(src_2m, 0, GRANULARITY_2M);
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Check a 4K block at offset 4K (populated in src_2m but not in src_4k) */
+ section.offset_within_region = GRANULARITY_4K;
+ g_assert_false(ram_discard_manager_is_populated(rdm, §ion));
+
+ /* Populate it in src_4k */
+ test_source_populate(src_4k, GRANULARITY_4K, GRANULARITY_4K);
+ g_assert_true(ram_discard_manager_is_populated(rdm, §ion));
+
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_2m));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4k));
+
+ test_source_free(src_2m);
+ test_source_free(src_4k);
+ test_teardown();
+}
+
+/*
+ * Test: Notification with two sources.
+ * Populate notification should only fire when all sources are populated.
+ */
+static void test_two_sources_notification(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* No populate notifications yet (all discarded) */
+ g_assert_cmpint(tl.populate_count, ==, 0);
+
+ /* Populate in src1 only - no notification (src2 still discarded) */
+ test_source_populate(src1, 0, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
+ 0, GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl.populate_count, ==, 0);
+
+ /* Populate same range in src2 - now should notify */
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src2),
+ 0, GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl.populate_count, ==, 1);
+
+ /* Discard from src1 - should notify discard immediately */
+ tl.discard_count = 0;
+ test_source_discard(src1, 0, GRANULARITY_4K * 2);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1),
+ 0, GRANULARITY_4K * 2);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Adding source with existing listener.
+ * When a new source is added, listeners should be notified about
+ * regions that become discarded.
+ */
+static void test_add_source_with_listener(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Populate some range in src1 */
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Should have been notified about populated region */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpint(tl.last_populate_offset, ==, 0);
+ g_assert_cmpint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
+
+ /* src2 has part of the region populated, part discarded */
+ /* src2 has 0-4 populated, 4-8 discarded */
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
+
+ /* Add src2 - listener should be notified about newly discarded regions */
+ tl.discard_count = 0;
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ /*
+ * The range 4K*4 to 4K*8 was populated in src1 but discarded in src2,
+ * so it becomes aggregated-discarded. Listener should be notified.
+ * Only this range should trigger a discard notification - regions beyond
+ * 4K*8 were already discarded in src1, so adding src2 doesn't change them.
+ */
+ g_assert_cmpint(tl.discard_count, ==, 1);
+ g_assert_cmpint(tl.last_discard_offset, ==, GRANULARITY_4K * 4);
+ g_assert_cmpint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Removing source with existing listener.
+ * When a source is removed, listeners should be notified about
+ * regions that become populated.
+ */
+static void test_remove_source_with_listener(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* src1: all of first 8 blocks populated */
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
+ /* src2: only first 4 blocks populated */
+ test_source_populate(src2, 0, GRANULARITY_4K * 4);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Only first 4 blocks are aggregated-populated */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
+
+ /* Remove src2 - blocks 4-8 should become populated */
+ tl.populate_count = 0;
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+
+ /* Listener should be notified about newly populated region (4K*4 to 4K*8) */
+ g_assert_cmpint(tl.populate_count, >=, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Add a source, register a listener, remove the source, then add it back.
+ * This checks the transition from 0 sources (all populated) to 1 source
+ * (partially discarded) with an active listener.
+ */
+static void test_readd_source_with_listener(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Populate some range in src */
+ test_source_populate(src, 0, GRANULARITY_4K * 8);
+
+ /* 1. Add source */
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* 2. Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Listener notified about populated region (0 - 32K) */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
+
+ /* 3. Remove source */
+ tl.populate_count = 0;
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+
+ /*
+ * With 0 sources, everything is populated.
+ * The range that was discarded in src (from 32K to end) becomes populated.
+ */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 8);
+ g_assert_cmpuint(tl.last_populate_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
+
+ /* 4. Add source back */
+ tl.discard_count = 0;
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+
+ /*
+ * Now we have 1 source again. The range (32K to end) is discarded again.
+ * Listener should be notified about this discard.
+ */
+ g_assert_cmpint(tl.discard_count, ==, 1);
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 8);
+ g_assert_cmpuint(tl.last_discard_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Duplicate source registration should fail.
+ */
+static void test_duplicate_source(void)
+{
+ TestRamDiscardSource *src;
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+
+ /* Adding same source again should fail */
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, -EBUSY);
+
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Populate notification rollback on listener error.
+ */
+static void test_populate_rollback(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl1 = { 0, }, tl2 = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register two listeners */
+ test_listener_init(&tl1);
+ test_listener_init(&tl2);
+ tl2.fail_on_populate = 1; /* Second listener fails on first populate */
+
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+
+ /*
+ * Register tl2 first so it's visited second (QLIST_INSERT_HEAD reverses
+ * registration order). This ensures tl1 receives populate before tl2
+ * fails.
+ */
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion);
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion);
+
+ /* Try to populate - should fail and roll back */
+ test_source_populate(src, 0, GRANULARITY_4K);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ 0, GRANULARITY_4K);
+ g_assert_cmpint(ret, ==, -ENOMEM);
+
+ /* First listener should have received populate then discard (rollback) */
+ g_assert_cmpint(tl1.populate_count, ==, 1);
+ g_assert_cmpint(tl1.discard_count, ==, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Replay populated with two sources (intersection).
+ */
+static void test_replay_populated_intersection(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /*
+ * src1: blocks 0-7 populated
+ * src2: blocks 4-11 populated
+ * Intersection: blocks 4-7
+ */
+ test_source_populate(src1, 0, GRANULARITY_4K * 8);
+ test_source_populate(src2, GRANULARITY_4K * 4, GRANULARITY_4K * 8);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener - should only get notified about intersection */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Should have been notified about blocks 4-7 (intersection) */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 4);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Empty region (no sources).
+ */
+static void test_no_sources(void)
+{
+ test_setup();
+
+ /* No sources - should have no manager */
+ g_assert_null(memory_region_get_ram_discard_manager(test_mr));
+ g_assert_false(memory_region_has_ram_discard_manager(test_mr));
+
+ test_teardown();
+}
+
+static void test_redundant_discard(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Add sources */
+ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(TEST_REGION_SIZE);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Populate intersection (0-4K) in both sources */
+ test_source_populate(src1, 0, GRANULARITY_4K);
+ test_source_populate(src2, 0, GRANULARITY_4K);
+
+ /* Notify populate src1 - should trigger listener populate */
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
+ 0, GRANULARITY_4K);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl.populate_count, ==, 1);
+
+ /* Now Discard src1 -> Aggregate Discarded */
+ tl.discard_count = 0;
+ test_source_discard(src1, 0, GRANULARITY_4K);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+
+ /* Now Discard src2 -> Aggregate Discarded (Already Discarded!) */
+ /* Listener should NOT receive another discard notification for the same range. */
+ test_source_discard(src2, 0, GRANULARITY_4K);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src2), 0, GRANULARITY_4K);
+
+ g_assert_cmpint(tl.discard_count, ==, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+/*
+ * Test: Listener with partial section coverage.
+ * Listener should only receive notifications for its registered range.
+ */
+static void test_partial_listener_section(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Populate blocks 0-7 */
+ test_source_populate(src, 0, GRANULARITY_4K * 8);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener for only blocks 2-5 (not the full region) */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = GRANULARITY_4K * 2;
+ section.size = int128_make64(GRANULARITY_4K * 4);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Should be notified only about blocks 2-5 (intersection) */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 2);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
+
+ /* Discard block 0 - outside listener's section, no notification */
+ tl.discard_count = 0;
+ test_source_discard(src, 0, GRANULARITY_4K);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
+ 0, GRANULARITY_4K);
+ g_assert_cmpint(tl.discard_count, ==, 0);
+
+ /* Discard block 3 - inside listener's section */
+ test_source_discard(src, GRANULARITY_4K * 3, GRANULARITY_4K);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 3, GRANULARITY_4K);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 3);
+
+ /* Discard spanning boundary (blocks 5-6) - only block 5 in section */
+ tl.discard_count = 0;
+ test_source_discard(src, GRANULARITY_4K * 5, GRANULARITY_4K * 2);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 5, GRANULARITY_4K * 2);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+ g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 5);
+ g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Multiple listeners with different (non-overlapping) sections.
+ */
+static void test_multiple_listeners_different_sections(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section1, section2;
+ TestListener tl1 = { 0, }, tl2 = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Listener 1: blocks 0-3 */
+ test_listener_init(&tl1);
+ section1.mr = test_mr;
+ section1.offset_within_region = 0;
+ section1.size = int128_make64(GRANULARITY_4K * 4);
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1);
+
+ /* Listener 2: blocks 8-11 */
+ test_listener_init(&tl2);
+ section2.mr = test_mr;
+ section2.offset_within_region = GRANULARITY_4K * 8;
+ section2.size = int128_make64(GRANULARITY_4K * 4);
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2);
+
+ /* Initially all discarded - no populate notifications */
+ g_assert_cmpint(tl1.populate_count, ==, 0);
+ g_assert_cmpint(tl2.populate_count, ==, 0);
+
+ /* Populate blocks 0-3 - only tl1 should be notified */
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ 0, GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 1);
+ g_assert_cmpint(tl2.populate_count, ==, 0);
+
+ /* Populate blocks 8-11 - only tl2 should be notified */
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 8,
+ GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 1);
+ g_assert_cmpint(tl2.populate_count, ==, 1);
+
+ /* Populate blocks 4-7 (gap) - neither listener should be notified */
+ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 4,
+ GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 1);
+ g_assert_cmpint(tl2.populate_count, ==, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Multiple listeners with overlapping sections.
+ */
+static void test_overlapping_listener_sections(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section1, section2;
+ TestListener tl1 = { 0, }, tl2 = { 0, };
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Listener 1: blocks 0-7 */
+ test_listener_init(&tl1);
+ section1.mr = test_mr;
+ section1.offset_within_region = 0;
+ section1.size = int128_make64(GRANULARITY_4K * 8);
+ ram_discard_manager_register_listener(rdm, &tl1.rdl, §ion1);
+
+ /* Listener 2: blocks 4-11 (overlaps with tl1 on blocks 4-7) */
+ test_listener_init(&tl2);
+ section2.mr = test_mr;
+ section2.offset_within_region = GRANULARITY_4K * 4;
+ section2.size = int128_make64(GRANULARITY_4K * 8);
+ ram_discard_manager_register_listener(rdm, &tl2.rdl, §ion2);
+
+ /* Populate blocks 4-7 (overlap region) - both should be notified */
+ test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 4,
+ GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 1);
+ g_assert_cmpint(tl2.populate_count, ==, 1);
+
+ /* Populate blocks 0-3 - only tl1 */
+ test_source_populate(src, 0, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ 0, GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 2);
+ g_assert_cmpint(tl2.populate_count, ==, 1);
+
+ /* Populate blocks 8-11 - only tl2 */
+ test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
+ ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
+ GRANULARITY_4K * 8,
+ GRANULARITY_4K * 4);
+ g_assert_cmpint(ret, ==, 0);
+ g_assert_cmpint(tl1.populate_count, ==, 2);
+ g_assert_cmpint(tl2.populate_count, ==, 2);
+
+ ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
+ ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+/*
+ * Test: Listener at exact memory region boundaries.
+ */
+static void test_boundary_section(void)
+{
+ TestRamDiscardSource *src;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ TestListener tl = { 0, };
+ uint64_t last_offset;
+ int ret;
+
+ test_setup();
+
+ src = test_source_new(test_mr, GRANULARITY_4K);
+
+ /* Populate last 4 blocks of the region */
+ last_offset = TEST_REGION_SIZE - GRANULARITY_4K * 4;
+ test_source_populate(src, last_offset, GRANULARITY_4K * 4);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src));
+ g_assert_cmpint(ret, ==, 0);
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ /* Register listener for exactly the last 4 blocks */
+ test_listener_init(&tl);
+ section.mr = test_mr;
+ section.offset_within_region = last_offset;
+ section.size = int128_make64(GRANULARITY_4K * 4);
+ ram_discard_manager_register_listener(rdm, &tl.rdl, §ion);
+
+ /* Should receive notification for the populated range */
+ g_assert_cmpint(tl.populate_count, ==, 1);
+ g_assert_cmpuint(tl.last_populate_offset, ==, last_offset);
+ g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
+
+ /* Discard exactly at boundary */
+ tl.discard_count = 0;
+ test_source_discard(src, last_offset, GRANULARITY_4K * 4);
+ ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
+ last_offset, GRANULARITY_4K * 4);
+ g_assert_cmpint(tl.discard_count, ==, 1);
+
+ ram_discard_manager_unregister_listener(rdm, &tl.rdl);
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
+ test_source_free(src);
+ test_teardown();
+}
+
+static int count_discarded_blocks(const MemoryRegionSection *section,
+ void *opaque)
+{
+ int *count = opaque;
+ *count += int128_get64(section->size) / GRANULARITY_4K;
+ return 0;
+}
+
+/*
+ * Test: replay_discarded with two sources (union semantics).
+ */
+static void test_replay_discarded(void)
+{
+ TestRamDiscardSource *src1, *src2;
+ RamDiscardManager *rdm;
+ MemoryRegionSection section;
+ int count = 0;
+ int ret;
+
+ test_setup();
+
+ src1 = test_source_new(test_mr, GRANULARITY_4K);
+ src2 = test_source_new(test_mr, GRANULARITY_4K);
+
+ /*
+ * src1: blocks 0-3 populated, rest discarded
+ * src2: blocks 2-5 populated, rest discarded
+ * Aggregated populated: blocks 2-3 (intersection)
+ * Aggregated discarded: blocks 0-1, 4-5, 6+ (union of discarded)
+ */
+ test_source_populate(src1, 0, GRANULARITY_4K * 4);
+ test_source_populate(src2, GRANULARITY_4K * 2, GRANULARITY_4K * 4);
+
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src1));
+ g_assert_cmpint(ret, ==, 0);
+ ret = memory_region_add_ram_discard_source(test_mr,
+ RAM_DISCARD_SOURCE(src2));
+ g_assert_cmpint(ret, ==, 0);
+
+ rdm = memory_region_get_ram_discard_manager(test_mr);
+
+ section.mr = test_mr;
+ section.offset_within_region = 0;
+ section.size = int128_make64(GRANULARITY_4K * 8);
+
+ /* Count discarded blocks */
+ ret = ram_discard_manager_replay_discarded(rdm, §ion,
+ count_discarded_blocks, &count);
+
+ g_assert_cmpint(ret, ==, 0);
+ /* Discarded: blocks 0-1 (2), blocks 4-5 (2), blocks 6-7 (2) = 6 blocks */
+ g_assert_cmpint(count, ==, 6);
+
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
+ memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
+
+ test_source_free(src2);
+ test_source_free(src1);
+ test_teardown();
+}
+
+int main(int argc, char **argv)
+{
+ g_test_init(&argc, &argv, NULL);
+
+ module_call_init(MODULE_INIT_QOM);
+ type_register_static(&test_rds_info);
+
+ g_test_add_func("/ram-discard-manager/single-source/basic",
+ test_single_source_basic);
+ g_test_add_func("/ram-discard-manager/single-source/listener",
+ test_single_source_listener);
+ g_test_add_func("/ram-discard-manager/two-sources/same-granularity",
+ test_two_sources_same_granularity);
+ g_test_add_func("/ram-discard-manager/two-sources/different-granularity",
+ test_two_sources_different_granularity);
+ g_test_add_func("/ram-discard-manager/two-sources/notification",
+ test_two_sources_notification);
+ g_test_add_func("/ram-discard-manager/dynamic/add-source-with-listener",
+ test_add_source_with_listener);
+ g_test_add_func("/ram-discard-manager/dynamic/remove-source-with-listener",
+ test_remove_source_with_listener);
+ g_test_add_func("/ram-discard-manager/dynamic/readd-source-with-listener",
+ test_readd_source_with_listener);
+ g_test_add_func("/ram-discard-manager/edge/duplicate-source",
+ test_duplicate_source);
+ g_test_add_func("/ram-discard-manager/edge/populate-rollback",
+ test_populate_rollback);
+ g_test_add_func("/ram-discard-manager/edge/replay-intersection",
+ test_replay_populated_intersection);
+ g_test_add_func("/ram-discard-manager/edge/no-sources",
+ test_no_sources);
+ g_test_add_func("/ram-discard-manager/multi-source/redundant-discard",
+ test_redundant_discard);
+ g_test_add_func("/ram-discard-manager/listener/partial-section",
+ test_partial_listener_section);
+ g_test_add_func("/ram-discard-manager/listener/multiple-different",
+ test_multiple_listeners_different_sections);
+ g_test_add_func("/ram-discard-manager/listener/overlapping",
+ test_overlapping_listener_sections);
+ g_test_add_func("/ram-discard-manager/edge/boundary-section",
+ test_boundary_section);
+ g_test_add_func("/ram-discard-manager/multi-source/replay-discarded",
+ test_replay_discarded);
+
+ return g_test_run();
+}
diff --git a/tests/unit/meson.build b/tests/unit/meson.build
index 01cc540a45..5ba6b1a230 100644
--- a/tests/unit/meson.build
+++ b/tests/unit/meson.build
@@ -138,7 +138,13 @@ if have_system
'test-bufferiszero': [],
'test-smp-parse': [qom, meson.project_source_root() / 'hw/core/machine-smp.c'],
'test-vmstate': [migration, io],
- 'test-yank': ['socket-helpers.c', qom, io, chardev]
+ 'test-yank': ['socket-helpers.c', qom, io, chardev],
+ 'test-ram-discard-manager': [
+ 'test-ram-discard-manager.c',
+ 'test-ram-discard-manager-stubs.c',
+ meson.project_source_root() / 'system/ram-discard-manager.c',
+ genh, qemuutil, qom
+ ],
}
if config_host_data.get('CONFIG_INOTIFY1')
tests += {'test-util-filemonitor': []}
--
2.54.0
next prev parent reply other threads:[~2026-06-23 12:50 UTC|newest]
Thread overview: 20+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-23 12:47 [PULL 00/18] Next patches Peter Xu
2026-06-23 12:47 ` [PULL 01/18] thread-pool: Allow at least 1 thread in thread_pool_adjust_max_threads_to_work() Peter Xu
2026-06-23 12:47 ` [PULL 02/18] qapi/migration: Remove @cpr-exec-command doc in MigrationParameter Peter Xu
2026-06-23 12:47 ` [PULL 03/18] system/physmem: Synchronize ram_list accesses Peter Xu
2026-06-23 12:47 ` [PULL 04/18] system/memory: Remove MAX_PHYS_ADDR Peter Xu
2026-06-23 12:47 ` [PULL 05/18] migration: Use OBJECT_DECLARE_SIMPLE_TYPE Peter Xu
2026-06-23 12:47 ` [PULL 06/18] tests/qtest/migration: Add migration test on loongarch Peter Xu
2026-06-23 12:47 ` [PULL 07/18] migration/tests: Update a-b-boot images for all archs Peter Xu
2026-06-23 12:47 ` [PULL 08/18] system/memory: split RamDiscardManager into source and manager Peter Xu
2026-06-23 12:47 ` [PULL 09/18] system/memory: move RamDiscardManager to separate compilation unit Peter Xu
2026-06-23 12:47 ` [PULL 10/18] system/memory: constify section arguments Peter Xu
2026-06-23 12:47 ` [PULL 11/18] system/ram-discard-manager: implement replay via is_populated iteration Peter Xu
2026-06-23 12:47 ` [PULL 12/18] virtio-mem: remove replay_populated/replay_discarded implementation Peter Xu
2026-06-23 12:47 ` [PULL 13/18] system/ram-discard-manager: drop replay from source interface Peter Xu
2026-06-23 12:47 ` [PULL 14/18] system/memory: implement RamDiscardManager multi-source aggregation Peter Xu
2026-06-23 12:47 ` [PULL 15/18] system/physmem: destroy ram block attributes before RCU-deferred reclaim Peter Xu
2026-06-23 12:47 ` [PULL 16/18] system/memory: add RamDiscardManager reference counting and cleanup Peter Xu
2026-06-23 12:47 ` Peter Xu [this message]
2026-06-23 12:47 ` [PULL 18/18] system/physmem: make ram_block_discard_range() handle guest_memfd Peter Xu
2026-06-25 20:26 ` [PULL 00/18] Next patches Stefan Hajnoczi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260623124759.125399-18-peterx@redhat.com \
--to=peterx@redhat.com \
--cc=farosas@suse.de \
--cc=marcandre.lureau@redhat.com \
--cc=pbonzini@redhat.com \
--cc=qemu-devel@nongnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.