@samitouri / QOSamiQemu / commits / c2237aaf6d

tests: add unit tests for RamDiscardManager multi-source aggregation

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>

Marc-André Lureau committed Jun 4, 2026 at 17:43 UTC c2237aaf6dcdef9a9886baee512ca2c43f3fed08
4 files changed +1292 -1
MAINTAINERS
+2
@@ -3363,6 +3363,8 @@ F: system/memory-internal.h
3363 F: system/ram-block-attributes.c
3364 F: system/ram-discard-manager.c
3365 F: scripts/coccinelle/memory-region-housekeeping.cocci
3366 +F: tests/unit/test-ram-discard-manager.c
3367 +F: tests/unit/test-ram-discard-manager-stubs.c
3368
3369 Memory devices
3370 M: David Hildenbrand <david@kernel.org>
tests/unit/meson.build
+7 -1
@@ -138,7 +138,13 @@ if have_system
138 'test-bufferiszero': [],
139 'test-smp-parse': [qom, meson.project_source_root() / 'hw/core/machine-smp.c'],
140 'test-vmstate': [migration, io],
141 - 'test-yank': ['socket-helpers.c', qom, io, chardev]
141 + 'test-yank': ['socket-helpers.c', qom, io, chardev],
142 + 'test-ram-discard-manager': [
143 + 'test-ram-discard-manager.c',
144 + 'test-ram-discard-manager-stubs.c',
145 + meson.project_source_root() / 'system/ram-discard-manager.c',
146 + genh, qemuutil, qom
147 + ],
148 }
149 if config_host_data.get('CONFIG_INOTIFY1')
150 tests += {'test-util-filemonitor': []}
tests/unit/test-ram-discard-manager-stubs.c new
+48
@@ -0,0 +1,48 @@
1 +/* SPDX-License-Identifier: GPL-2.0-or-later */
2 +#include "qemu/osdep.h"
3 +#include "qom/object.h"
4 +#include "glib.h"
5 +#include "system/memory.h"
6 +
7 +RamDiscardManager *memory_region_get_ram_discard_manager(MemoryRegion *mr)
8 +{
9 + return mr->rdm;
10 +}
11 +
12 +int memory_region_add_ram_discard_source(MemoryRegion *mr,
13 + RamDiscardSource *source)
14 +{
15 + if (!mr->rdm) {
16 + mr->rdm = ram_discard_manager_new(mr);
17 + }
18 + return ram_discard_manager_add_source(mr->rdm, source);
19 +}
20 +
21 +int memory_region_del_ram_discard_source(MemoryRegion *mr,
22 + RamDiscardSource *source)
23 +{
24 + RamDiscardManager *rdm = mr->rdm;
25 +
26 + if (!rdm) {
27 + return 0;
28 + }
29 +
30 + return ram_discard_manager_del_source(rdm, source);
31 +}
32 +
33 +uint64_t memory_region_size(const MemoryRegion *mr)
34 +{
35 + return int128_get64(mr->size);
36 +}
37 +
38 +MemoryRegionSection *memory_region_section_new_copy(MemoryRegionSection *s)
39 +{
40 + MemoryRegionSection *copy = g_new(MemoryRegionSection, 1);
41 + *copy = *s;
42 + return copy;
43 +}
44 +
45 +void memory_region_section_free_copy(MemoryRegionSection *s)
46 +{
47 + g_free(s);
48 +}
tests/unit/test-ram-discard-manager.c new
+1235
@@ -0,0 +1,1235 @@
1 +/* SPDX-License-Identifier: GPL-2.0-or-later */
2 +#include "qemu/osdep.h"
3 +#include "qemu/bitmap.h"
4 +#include "qemu/module.h"
5 +#include "qemu/main-loop.h"
6 +#include "qapi/error.h"
7 +#include "qom/object.h"
8 +#include "qom/qom-qobject.h"
9 +#include "glib.h"
10 +#include "system/memory.h"
11 +
12 +#define TYPE_TEST_RAM_DISCARD_SOURCE "test-ram-discard-source"
13 +
14 +OBJECT_DECLARE_SIMPLE_TYPE(TestRamDiscardSource, TEST_RAM_DISCARD_SOURCE)
15 +
16 +struct TestRamDiscardSource {
17 + Object parent;
18 +
19 + MemoryRegion *mr;
20 + uint64_t granularity;
21 + unsigned long *bitmap;
22 + uint64_t bitmap_size;
23 +};
24 +
25 +static uint64_t test_rds_get_min_granularity(const RamDiscardSource *rds,
26 + const MemoryRegion *mr)
27 +{
28 + TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
29 +
30 + g_assert(mr == src->mr);
31 + return src->granularity;
32 +}
33 +
34 +static bool test_rds_is_populated(const RamDiscardSource *rds,
35 + const MemoryRegionSection *section)
36 +{
37 + TestRamDiscardSource *src = TEST_RAM_DISCARD_SOURCE(rds);
38 + uint64_t offset = section->offset_within_region;
39 + uint64_t size = int128_get64(section->size);
40 + uint64_t first_bit = offset / src->granularity;
41 + uint64_t last_bit = (offset + size - 1) / src->granularity;
42 + unsigned long found;
43 +
44 + g_assert(section->mr == src->mr);
45 +
46 + /* Check if any bit in range is zero (discarded) */
47 + found = find_next_zero_bit(src->bitmap, last_bit + 1, first_bit);
48 + return found > last_bit;
49 +}
50 +
51 +static void test_rds_class_init(ObjectClass *klass, const void *data)
52 +{
53 + RamDiscardSourceClass *rdsc = RAM_DISCARD_SOURCE_CLASS(klass);
54 +
55 + rdsc->get_min_granularity = test_rds_get_min_granularity;
56 + rdsc->is_populated = test_rds_is_populated;
57 +}
58 +
59 +static const TypeInfo test_rds_info = {
60 + .name = TYPE_TEST_RAM_DISCARD_SOURCE,
61 + .parent = TYPE_OBJECT,
62 + .instance_size = sizeof(TestRamDiscardSource),
63 + .class_init = test_rds_class_init,
64 + .interfaces = (const InterfaceInfo[]) {
65 + { TYPE_RAM_DISCARD_SOURCE },
66 + { }
67 + },
68 +};
69 +
70 +static TestRamDiscardSource *test_source_new(MemoryRegion *mr,
71 + uint64_t granularity)
72 +{
73 + TestRamDiscardSource *src;
74 + uint64_t region_size = memory_region_size(mr);
75 +
76 + src = TEST_RAM_DISCARD_SOURCE(object_new(TYPE_TEST_RAM_DISCARD_SOURCE));
77 + src->mr = mr;
78 + src->granularity = granularity;
79 + src->bitmap_size = DIV_ROUND_UP(region_size, granularity);
80 + src->bitmap = bitmap_new(src->bitmap_size);
81 +
82 + return src;
83 +}
84 +
85 +static void test_source_free(TestRamDiscardSource *src)
86 +{
87 + g_free(src->bitmap);
88 + object_unref(OBJECT(src));
89 +}
90 +
91 +static void test_source_populate(TestRamDiscardSource *src,
92 + uint64_t offset, uint64_t size)
93 +{
94 + uint64_t first_bit = offset / src->granularity;
95 + uint64_t nbits = size / src->granularity;
96 +
97 + bitmap_set(src->bitmap, first_bit, nbits);
98 +}
99 +
100 +static void test_source_discard(TestRamDiscardSource *src,
101 + uint64_t offset, uint64_t size)
102 +{
103 + uint64_t first_bit = offset / src->granularity;
104 + uint64_t nbits = size / src->granularity;
105 +
106 + bitmap_clear(src->bitmap, first_bit, nbits);
107 +}
108 +
109 +typedef struct TestListener {
110 + RamDiscardListener rdl;
111 + int populate_count;
112 + int discard_count;
113 + uint64_t last_populate_offset;
114 + uint64_t last_populate_size;
115 + uint64_t last_discard_offset;
116 + uint64_t last_discard_size;
117 + int fail_on_populate; /* Return error on Nth populate */
118 + int populate_call_num;
119 +} TestListener;
120 +
121 +static int test_listener_populate(RamDiscardListener *rdl,
122 + const MemoryRegionSection *section)
123 +{
124 + TestListener *tl = container_of(rdl, TestListener, rdl);
125 +
126 + tl->populate_call_num++;
127 + if (tl->fail_on_populate > 0 &&
128 + tl->populate_call_num >= tl->fail_on_populate) {
129 + return -ENOMEM;
130 + }
131 +
132 + tl->populate_count++;
133 + tl->last_populate_offset = section->offset_within_region;
134 + tl->last_populate_size = int128_get64(section->size);
135 + return 0;
136 +}
137 +
138 +static void test_listener_discard(RamDiscardListener *rdl,
139 + const MemoryRegionSection *section)
140 +{
141 + TestListener *tl = container_of(rdl, TestListener, rdl);
142 +
143 + tl->discard_count++;
144 + tl->last_discard_offset = section->offset_within_region;
145 + tl->last_discard_size = int128_get64(section->size);
146 +}
147 +
148 +static void test_listener_init(TestListener *tl)
149 +{
150 + ram_discard_listener_init(&tl->rdl,
151 + test_listener_populate,
152 + test_listener_discard);
153 +}
154 +
155 +#define TEST_REGION_SIZE (16 * 1024 * 1024) /* 16 MB */
156 +#define GRANULARITY_4K (4 * 1024)
157 +#define GRANULARITY_2M (2 * 1024 * 1024)
158 +
159 +static MemoryRegion *test_mr;
160 +
161 +static void test_setup(void)
162 +{
163 + test_mr = g_new0(MemoryRegion, 1);
164 + test_mr->size = int128_make64(TEST_REGION_SIZE);
165 + test_mr->ram = true;
166 +}
167 +
168 +static void test_teardown(void)
169 +{
170 + g_clear_pointer(&test_mr->rdm, object_unref);
171 + object_unparent(OBJECT(test_mr));
172 + g_free(test_mr);
173 + test_mr = NULL;
174 +}
175 +
176 +static void test_single_source_basic(void)
177 +{
178 + TestRamDiscardSource *src;
179 + RamDiscardManager *rdm;
180 + MemoryRegionSection section;
181 + int ret;
182 +
183 + test_setup();
184 +
185 + src = test_source_new(test_mr, GRANULARITY_4K);
186 + rdm = memory_region_get_ram_discard_manager(test_mr);
187 + g_assert_null(rdm);
188 +
189 + /* Add source */
190 + ret = memory_region_add_ram_discard_source(test_mr,
191 + RAM_DISCARD_SOURCE(src));
192 + g_assert_cmpint(ret, ==, 0);
193 +
194 + rdm = memory_region_get_ram_discard_manager(test_mr);
195 + g_assert_nonnull(rdm);
196 +
197 + g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
198 + ==, GRANULARITY_4K);
199 +
200 + /* Initially all discarded */
201 + section.mr = test_mr;
202 + section.offset_within_region = 0;
203 + section.size = int128_make64(GRANULARITY_4K);
204 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
205 +
206 + /* Populate a range in source */
207 + test_source_populate(src, 0, GRANULARITY_4K * 4);
208 +
209 + /* Now should be populated */
210 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
211 +
212 + /* Check larger section */
213 + section.size = int128_make64(GRANULARITY_4K * 4);
214 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
215 +
216 + /* Check section that spans populated and discarded */
217 + section.size = int128_make64(GRANULARITY_4K * 8);
218 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
219 +
220 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
221 +
222 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
223 +
224 + test_source_free(src);
225 + test_teardown();
226 +}
227 +
228 +static void test_single_source_listener(void)
229 +{
230 + TestRamDiscardSource *src;
231 + RamDiscardManager *rdm;
232 + MemoryRegionSection section;
233 + TestListener tl = { 0, };
234 + int ret;
235 +
236 + test_setup();
237 +
238 + src = test_source_new(test_mr, GRANULARITY_4K);
239 +
240 + /* Populate some ranges before adding listener */
241 + test_source_populate(src, 0, GRANULARITY_4K * 4);
242 + test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
243 +
244 + ret = memory_region_add_ram_discard_source(test_mr,
245 + RAM_DISCARD_SOURCE(src));
246 + g_assert_cmpint(ret, ==, 0);
247 + rdm = memory_region_get_ram_discard_manager(test_mr);
248 + g_assert_nonnull(rdm);
249 +
250 + /* Register listener */
251 + test_listener_init(&tl);
252 + section.mr = test_mr;
253 + section.offset_within_region = 0;
254 + section.size = int128_make64(TEST_REGION_SIZE);
255 +
256 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
257 +
258 + /* Should have been notified about populated regions */
259 + g_assert_cmpint(tl.populate_count, ==, 2);
260 +
261 + /* Notify populate for new range */
262 + tl.populate_count = 0;
263 + test_source_populate(src, GRANULARITY_4K * 16, GRANULARITY_4K * 2);
264 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
265 + GRANULARITY_4K * 16,
266 + GRANULARITY_4K * 2);
267 + g_assert_cmpint(ret, ==, 0);
268 + g_assert_cmpint(tl.populate_count, ==, 1);
269 + g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 16);
270 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 2);
271 +
272 + /* Notify discard */
273 + tl.discard_count = 0;
274 + test_source_discard(src, 0, GRANULARITY_4K * 4);
275 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
276 + 0, GRANULARITY_4K * 4);
277 + g_assert_cmpint(tl.discard_count, ==, 1);
278 + g_assert_cmpuint(tl.last_discard_offset, ==, 0);
279 + g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
280 +
281 + /* Unregister listener */
282 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
283 +
284 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
285 + test_source_free(src);
286 + test_teardown();
287 +}
288 +
289 +static void test_two_sources_same_granularity(void)
290 +{
291 + TestRamDiscardSource *src1, *src2;
292 + RamDiscardManager *rdm;
293 + MemoryRegionSection section;
294 + int ret;
295 +
296 + test_setup();
297 +
298 + src1 = test_source_new(test_mr, GRANULARITY_4K);
299 + src2 = test_source_new(test_mr, GRANULARITY_4K);
300 +
301 + /* Add first source */
302 + ret = memory_region_add_ram_discard_source(test_mr,
303 + RAM_DISCARD_SOURCE(src1));
304 + g_assert_cmpint(ret, ==, 0);
305 +
306 + /* Add second source */
307 + ret = memory_region_add_ram_discard_source(test_mr,
308 + RAM_DISCARD_SOURCE(src2));
309 + g_assert_cmpint(ret, ==, 0);
310 +
311 + rdm = memory_region_get_ram_discard_manager(test_mr);
312 + g_assert_nonnull(rdm);
313 +
314 + /* Check granularity */
315 + g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
316 + ==, GRANULARITY_4K);
317 +
318 + section.mr = test_mr;
319 + section.offset_within_region = 0;
320 + section.size = int128_make64(GRANULARITY_4K);
321 +
322 + /* Both discarded -> aggregated discarded */
323 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
324 +
325 + /* Populate in src1 only */
326 + test_source_populate(src1, 0, GRANULARITY_4K);
327 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
328 +
329 + /* Populate in src2 only */
330 + test_source_discard(src1, 0, GRANULARITY_4K);
331 + test_source_populate(src2, 0, GRANULARITY_4K);
332 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
333 +
334 + /* Populate in both -> aggregated populated */
335 + test_source_populate(src1, 0, GRANULARITY_4K);
336 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
337 +
338 + /* Remove sources */
339 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
340 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
341 +
342 + test_source_free(src2);
343 + test_source_free(src1);
344 + test_teardown();
345 +}
346 +
347 +/*
348 + * Test: Two sources with different granularities (4K and 2M).
349 + * The aggregated granularity should be GCD(4K, 2M) = 4K.
350 + */
351 +static void test_two_sources_different_granularity(void)
352 +{
353 + TestRamDiscardSource *src_4k, *src_2m;
354 + RamDiscardManager *rdm;
355 + MemoryRegionSection section;
356 + int ret;
357 +
358 + test_setup();
359 +
360 + src_4k = test_source_new(test_mr, GRANULARITY_4K);
361 + src_2m = test_source_new(test_mr, GRANULARITY_2M);
362 +
363 + ret = memory_region_add_ram_discard_source(test_mr,
364 + RAM_DISCARD_SOURCE(src_4k));
365 + g_assert_cmpint(ret, ==, 0);
366 +
367 + ret = memory_region_add_ram_discard_source(test_mr,
368 + RAM_DISCARD_SOURCE(src_2m));
369 + g_assert_cmpint(ret, ==, 0);
370 +
371 + rdm = memory_region_get_ram_discard_manager(test_mr);
372 +
373 + g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr),
374 + ==, GRANULARITY_4K);
375 +
376 + section.mr = test_mr;
377 + section.offset_within_region = 0;
378 + section.size = int128_make64(GRANULARITY_4K);
379 +
380 + /* Both discarded */
381 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
382 +
383 + /* Populate 4K in src_4k, but src_2m still discarded the whole 2M block */
384 + test_source_populate(src_4k, 0, GRANULARITY_4K);
385 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
386 +
387 + /* Populate 2M in src_2m (which includes the 4K block) */
388 + test_source_populate(src_2m, 0, GRANULARITY_2M);
389 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
390 +
391 + /* Check a 4K block at offset 4K (populated in src_2m but not in src_4k) */
392 + section.offset_within_region = GRANULARITY_4K;
393 + g_assert_false(ram_discard_manager_is_populated(rdm, &section));
394 +
395 + /* Populate it in src_4k */
396 + test_source_populate(src_4k, GRANULARITY_4K, GRANULARITY_4K);
397 + g_assert_true(ram_discard_manager_is_populated(rdm, &section));
398 +
399 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_2m));
400 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4k));
401 +
402 + test_source_free(src_2m);
403 + test_source_free(src_4k);
404 + test_teardown();
405 +}
406 +
407 +/*
408 + * Test: Notification with two sources.
409 + * Populate notification should only fire when all sources are populated.
410 + */
411 +static void test_two_sources_notification(void)
412 +{
413 + TestRamDiscardSource *src1, *src2;
414 + RamDiscardManager *rdm;
415 + MemoryRegionSection section;
416 + TestListener tl = { 0, };
417 + int ret;
418 +
419 + test_setup();
420 +
421 + src1 = test_source_new(test_mr, GRANULARITY_4K);
422 + src2 = test_source_new(test_mr, GRANULARITY_4K);
423 +
424 + ret = memory_region_add_ram_discard_source(test_mr,
425 + RAM_DISCARD_SOURCE(src1));
426 + g_assert_cmpint(ret, ==, 0);
427 + ret = memory_region_add_ram_discard_source(test_mr,
428 + RAM_DISCARD_SOURCE(src2));
429 + g_assert_cmpint(ret, ==, 0);
430 +
431 + rdm = memory_region_get_ram_discard_manager(test_mr);
432 +
433 + /* Register listener */
434 + test_listener_init(&tl);
435 + section.mr = test_mr;
436 + section.offset_within_region = 0;
437 + section.size = int128_make64(TEST_REGION_SIZE);
438 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
439 +
440 + /* No populate notifications yet (all discarded) */
441 + g_assert_cmpint(tl.populate_count, ==, 0);
442 +
443 + /* Populate in src1 only - no notification (src2 still discarded) */
444 + test_source_populate(src1, 0, GRANULARITY_4K * 4);
445 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
446 + 0, GRANULARITY_4K * 4);
447 + g_assert_cmpint(ret, ==, 0);
448 + g_assert_cmpint(tl.populate_count, ==, 0);
449 +
450 + /* Populate same range in src2 - now should notify */
451 + test_source_populate(src2, 0, GRANULARITY_4K * 4);
452 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src2),
453 + 0, GRANULARITY_4K * 4);
454 + g_assert_cmpint(ret, ==, 0);
455 + g_assert_cmpint(tl.populate_count, ==, 1);
456 +
457 + /* Discard from src1 - should notify discard immediately */
458 + tl.discard_count = 0;
459 + test_source_discard(src1, 0, GRANULARITY_4K * 2);
460 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1),
461 + 0, GRANULARITY_4K * 2);
462 + g_assert_cmpint(tl.discard_count, ==, 1);
463 +
464 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
465 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
466 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
467 +
468 + test_source_free(src2);
469 + test_source_free(src1);
470 + test_teardown();
471 +}
472 +
473 +/*
474 + * Test: Adding source with existing listener.
475 + * When a new source is added, listeners should be notified about
476 + * regions that become discarded.
477 + */
478 +static void test_add_source_with_listener(void)
479 +{
480 + TestRamDiscardSource *src1, *src2;
481 + RamDiscardManager *rdm;
482 + MemoryRegionSection section;
483 + TestListener tl = { 0, };
484 + int ret;
485 +
486 + test_setup();
487 +
488 + src1 = test_source_new(test_mr, GRANULARITY_4K);
489 + src2 = test_source_new(test_mr, GRANULARITY_4K);
490 +
491 + /* Populate some range in src1 */
492 + test_source_populate(src1, 0, GRANULARITY_4K * 8);
493 +
494 + ret = memory_region_add_ram_discard_source(test_mr,
495 + RAM_DISCARD_SOURCE(src1));
496 + g_assert_cmpint(ret, ==, 0);
497 + rdm = memory_region_get_ram_discard_manager(test_mr);
498 +
499 + /* Register listener */
500 + test_listener_init(&tl);
501 + section.mr = test_mr;
502 + section.offset_within_region = 0;
503 + section.size = int128_make64(TEST_REGION_SIZE);
504 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
505 +
506 + /* Should have been notified about populated region */
507 + g_assert_cmpint(tl.populate_count, ==, 1);
508 + g_assert_cmpint(tl.last_populate_offset, ==, 0);
509 + g_assert_cmpint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
510 +
511 + /* src2 has part of the region populated, part discarded */
512 + /* src2 has 0-4 populated, 4-8 discarded */
513 + test_source_populate(src2, 0, GRANULARITY_4K * 4);
514 +
515 + /* Add src2 - listener should be notified about newly discarded regions */
516 + tl.discard_count = 0;
517 + ret = memory_region_add_ram_discard_source(test_mr,
518 + RAM_DISCARD_SOURCE(src2));
519 + g_assert_cmpint(ret, ==, 0);
520 +
521 + /*
522 + * The range 4K*4 to 4K*8 was populated in src1 but discarded in src2,
523 + * so it becomes aggregated-discarded. Listener should be notified.
524 + * Only this range should trigger a discard notification - regions beyond
525 + * 4K*8 were already discarded in src1, so adding src2 doesn't change them.
526 + */
527 + g_assert_cmpint(tl.discard_count, ==, 1);
528 + g_assert_cmpint(tl.last_discard_offset, ==, GRANULARITY_4K * 4);
529 + g_assert_cmpint(tl.last_discard_size, ==, GRANULARITY_4K * 4);
530 +
531 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
532 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
533 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
534 +
535 + test_source_free(src2);
536 + test_source_free(src1);
537 + test_teardown();
538 +}
539 +
540 +/*
541 + * Test: Removing source with existing listener.
542 + * When a source is removed, listeners should be notified about
543 + * regions that become populated.
544 + */
545 +static void test_remove_source_with_listener(void)
546 +{
547 + TestRamDiscardSource *src1, *src2;
548 + RamDiscardManager *rdm;
549 + MemoryRegionSection section;
550 + TestListener tl = { 0, };
551 + int ret;
552 +
553 + test_setup();
554 +
555 + src1 = test_source_new(test_mr, GRANULARITY_4K);
556 + src2 = test_source_new(test_mr, GRANULARITY_4K);
557 +
558 + /* src1: all of first 8 blocks populated */
559 + test_source_populate(src1, 0, GRANULARITY_4K * 8);
560 + /* src2: only first 4 blocks populated */
561 + test_source_populate(src2, 0, GRANULARITY_4K * 4);
562 +
563 + ret = memory_region_add_ram_discard_source(test_mr,
564 + RAM_DISCARD_SOURCE(src1));
565 + g_assert_cmpint(ret, ==, 0);
566 + ret = memory_region_add_ram_discard_source(test_mr,
567 + RAM_DISCARD_SOURCE(src2));
568 + g_assert_cmpint(ret, ==, 0);
569 +
570 + rdm = memory_region_get_ram_discard_manager(test_mr);
571 +
572 + /* Register listener */
573 + test_listener_init(&tl);
574 + section.mr = test_mr;
575 + section.offset_within_region = 0;
576 + section.size = int128_make64(TEST_REGION_SIZE);
577 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
578 +
579 + /* Only first 4 blocks are aggregated-populated */
580 + g_assert_cmpint(tl.populate_count, ==, 1);
581 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
582 +
583 + /* Remove src2 - blocks 4-8 should become populated */
584 + tl.populate_count = 0;
585 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
586 +
587 + /* Listener should be notified about newly populated region (4K*4 to 4K*8) */
588 + g_assert_cmpint(tl.populate_count, >=, 1);
589 +
590 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
591 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
592 +
593 + test_source_free(src2);
594 + test_source_free(src1);
595 + test_teardown();
596 +}
597 +
598 +/*
599 + * Test: Add a source, register a listener, remove the source, then add it back.
600 + * This checks the transition from 0 sources (all populated) to 1 source
601 + * (partially discarded) with an active listener.
602 + */
603 +static void test_readd_source_with_listener(void)
604 +{
605 + TestRamDiscardSource *src;
606 + RamDiscardManager *rdm;
607 + MemoryRegionSection section;
608 + TestListener tl = { 0, };
609 + int ret;
610 +
611 + test_setup();
612 +
613 + src = test_source_new(test_mr, GRANULARITY_4K);
614 +
615 + /* Populate some range in src */
616 + test_source_populate(src, 0, GRANULARITY_4K * 8);
617 +
618 + /* 1. Add source */
619 + ret = memory_region_add_ram_discard_source(test_mr,
620 + RAM_DISCARD_SOURCE(src));
621 + g_assert_cmpint(ret, ==, 0);
622 + rdm = memory_region_get_ram_discard_manager(test_mr);
623 +
624 + /* 2. Register listener */
625 + test_listener_init(&tl);
626 + section.mr = test_mr;
627 + section.offset_within_region = 0;
628 + section.size = int128_make64(TEST_REGION_SIZE);
629 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
630 +
631 + /* Listener notified about populated region (0 - 32K) */
632 + g_assert_cmpint(tl.populate_count, ==, 1);
633 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 8);
634 +
635 + /* 3. Remove source */
636 + tl.populate_count = 0;
637 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
638 +
639 + /*
640 + * With 0 sources, everything is populated.
641 + * The range that was discarded in src (from 32K to end) becomes populated.
642 + */
643 + g_assert_cmpint(tl.populate_count, ==, 1);
644 + g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 8);
645 + g_assert_cmpuint(tl.last_populate_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
646 +
647 + /* 4. Add source back */
648 + tl.discard_count = 0;
649 + ret = memory_region_add_ram_discard_source(test_mr,
650 + RAM_DISCARD_SOURCE(src));
651 + g_assert_cmpint(ret, ==, 0);
652 +
653 + /*
654 + * Now we have 1 source again. The range (32K to end) is discarded again.
655 + * Listener should be notified about this discard.
656 + */
657 + g_assert_cmpint(tl.discard_count, ==, 1);
658 + g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 8);
659 + g_assert_cmpuint(tl.last_discard_size, ==, TEST_REGION_SIZE - GRANULARITY_4K * 8);
660 +
661 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
662 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
663 + test_source_free(src);
664 + test_teardown();
665 +}
666 +
667 +/*
668 + * Test: Duplicate source registration should fail.
669 + */
670 +static void test_duplicate_source(void)
671 +{
672 + TestRamDiscardSource *src;
673 + int ret;
674 +
675 + test_setup();
676 +
677 + src = test_source_new(test_mr, GRANULARITY_4K);
678 +
679 + ret = memory_region_add_ram_discard_source(test_mr,
680 + RAM_DISCARD_SOURCE(src));
681 + g_assert_cmpint(ret, ==, 0);
682 +
683 + /* Adding same source again should fail */
684 + ret = memory_region_add_ram_discard_source(test_mr,
685 + RAM_DISCARD_SOURCE(src));
686 + g_assert_cmpint(ret, ==, -EBUSY);
687 +
688 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
689 + test_source_free(src);
690 + test_teardown();
691 +}
692 +
693 +/*
694 + * Test: Populate notification rollback on listener error.
695 + */
696 +static void test_populate_rollback(void)
697 +{
698 + TestRamDiscardSource *src;
699 + RamDiscardManager *rdm;
700 + MemoryRegionSection section;
701 + TestListener tl1 = { 0, }, tl2 = { 0, };
702 + int ret;
703 +
704 + test_setup();
705 +
706 + src = test_source_new(test_mr, GRANULARITY_4K);
707 +
708 + ret = memory_region_add_ram_discard_source(test_mr,
709 + RAM_DISCARD_SOURCE(src));
710 + g_assert_cmpint(ret, ==, 0);
711 + rdm = memory_region_get_ram_discard_manager(test_mr);
712 +
713 + /* Register two listeners */
714 + test_listener_init(&tl1);
715 + test_listener_init(&tl2);
716 + tl2.fail_on_populate = 1; /* Second listener fails on first populate */
717 +
718 + section.mr = test_mr;
719 + section.offset_within_region = 0;
720 + section.size = int128_make64(TEST_REGION_SIZE);
721 +
722 + /*
723 + * Register tl2 first so it's visited second (QLIST_INSERT_HEAD reverses
724 + * registration order). This ensures tl1 receives populate before tl2
725 + * fails.
726 + */
727 + ram_discard_manager_register_listener(rdm, &tl2.rdl, &section);
728 + ram_discard_manager_register_listener(rdm, &tl1.rdl, &section);
729 +
730 + /* Try to populate - should fail and roll back */
731 + test_source_populate(src, 0, GRANULARITY_4K);
732 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
733 + 0, GRANULARITY_4K);
734 + g_assert_cmpint(ret, ==, -ENOMEM);
735 +
736 + /* First listener should have received populate then discard (rollback) */
737 + g_assert_cmpint(tl1.populate_count, ==, 1);
738 + g_assert_cmpint(tl1.discard_count, ==, 1);
739 +
740 + ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
741 + ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
742 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
743 + test_source_free(src);
744 + test_teardown();
745 +}
746 +
747 +/*
748 + * Test: Replay populated with two sources (intersection).
749 + */
750 +static void test_replay_populated_intersection(void)
751 +{
752 + TestRamDiscardSource *src1, *src2;
753 + RamDiscardManager *rdm;
754 + MemoryRegionSection section;
755 + TestListener tl = { 0, };
756 + int ret;
757 +
758 + test_setup();
759 +
760 + src1 = test_source_new(test_mr, GRANULARITY_4K);
761 + src2 = test_source_new(test_mr, GRANULARITY_4K);
762 +
763 + /*
764 + * src1: blocks 0-7 populated
765 + * src2: blocks 4-11 populated
766 + * Intersection: blocks 4-7
767 + */
768 + test_source_populate(src1, 0, GRANULARITY_4K * 8);
769 + test_source_populate(src2, GRANULARITY_4K * 4, GRANULARITY_4K * 8);
770 +
771 + ret = memory_region_add_ram_discard_source(test_mr,
772 + RAM_DISCARD_SOURCE(src1));
773 + g_assert_cmpint(ret, ==, 0);
774 + ret = memory_region_add_ram_discard_source(test_mr,
775 + RAM_DISCARD_SOURCE(src2));
776 + g_assert_cmpint(ret, ==, 0);
777 +
778 + rdm = memory_region_get_ram_discard_manager(test_mr);
779 +
780 + /* Register listener - should only get notified about intersection */
781 + test_listener_init(&tl);
782 + section.mr = test_mr;
783 + section.offset_within_region = 0;
784 + section.size = int128_make64(TEST_REGION_SIZE);
785 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
786 +
787 + /* Should have been notified about blocks 4-7 (intersection) */
788 + g_assert_cmpint(tl.populate_count, ==, 1);
789 + g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 4);
790 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
791 +
792 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
793 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
794 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
795 +
796 + test_source_free(src2);
797 + test_source_free(src1);
798 + test_teardown();
799 +}
800 +
801 +/*
802 + * Test: Empty region (no sources).
803 + */
804 +static void test_no_sources(void)
805 +{
806 + test_setup();
807 +
808 + /* No sources - should have no manager */
809 + g_assert_null(memory_region_get_ram_discard_manager(test_mr));
810 + g_assert_false(memory_region_has_ram_discard_manager(test_mr));
811 +
812 + test_teardown();
813 +}
814 +
815 +static void test_redundant_discard(void)
816 +{
817 + TestRamDiscardSource *src1, *src2;
818 + RamDiscardManager *rdm;
819 + MemoryRegionSection section;
820 + TestListener tl = { 0, };
821 + int ret;
822 +
823 + test_setup();
824 +
825 + src1 = test_source_new(test_mr, GRANULARITY_4K);
826 + src2 = test_source_new(test_mr, GRANULARITY_4K);
827 +
828 + /* Add sources */
829 + ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
830 + g_assert_cmpint(ret, ==, 0);
831 + ret = memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
832 + g_assert_cmpint(ret, ==, 0);
833 +
834 + rdm = memory_region_get_ram_discard_manager(test_mr);
835 +
836 + /* Register listener */
837 + test_listener_init(&tl);
838 + section.mr = test_mr;
839 + section.offset_within_region = 0;
840 + section.size = int128_make64(TEST_REGION_SIZE);
841 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
842 +
843 + /* Populate intersection (0-4K) in both sources */
844 + test_source_populate(src1, 0, GRANULARITY_4K);
845 + test_source_populate(src2, 0, GRANULARITY_4K);
846 +
847 + /* Notify populate src1 - should trigger listener populate */
848 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src1),
849 + 0, GRANULARITY_4K);
850 + g_assert_cmpint(ret, ==, 0);
851 + g_assert_cmpint(tl.populate_count, ==, 1);
852 +
853 + /* Now Discard src1 -> Aggregate Discarded */
854 + tl.discard_count = 0;
855 + test_source_discard(src1, 0, GRANULARITY_4K);
856 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, GRANULARITY_4K);
857 + g_assert_cmpint(tl.discard_count, ==, 1);
858 +
859 + /* Now Discard src2 -> Aggregate Discarded (Already Discarded!) */
860 + /* Listener should NOT receive another discard notification for the same range. */
861 + test_source_discard(src2, 0, GRANULARITY_4K);
862 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src2), 0, GRANULARITY_4K);
863 +
864 + g_assert_cmpint(tl.discard_count, ==, 1);
865 +
866 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
867 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
868 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
869 +
870 + test_source_free(src2);
871 + test_source_free(src1);
872 + test_teardown();
873 +}
874 +
875 +/*
876 + * Test: Listener with partial section coverage.
877 + * Listener should only receive notifications for its registered range.
878 + */
879 +static void test_partial_listener_section(void)
880 +{
881 + TestRamDiscardSource *src;
882 + RamDiscardManager *rdm;
883 + MemoryRegionSection section;
884 + TestListener tl = { 0, };
885 + int ret;
886 +
887 + test_setup();
888 +
889 + src = test_source_new(test_mr, GRANULARITY_4K);
890 +
891 + /* Populate blocks 0-7 */
892 + test_source_populate(src, 0, GRANULARITY_4K * 8);
893 +
894 + ret = memory_region_add_ram_discard_source(test_mr,
895 + RAM_DISCARD_SOURCE(src));
896 + g_assert_cmpint(ret, ==, 0);
897 + rdm = memory_region_get_ram_discard_manager(test_mr);
898 +
899 + /* Register listener for only blocks 2-5 (not the full region) */
900 + test_listener_init(&tl);
901 + section.mr = test_mr;
902 + section.offset_within_region = GRANULARITY_4K * 2;
903 + section.size = int128_make64(GRANULARITY_4K * 4);
904 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
905 +
906 + /* Should be notified only about blocks 2-5 (intersection) */
907 + g_assert_cmpint(tl.populate_count, ==, 1);
908 + g_assert_cmpuint(tl.last_populate_offset, ==, GRANULARITY_4K * 2);
909 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
910 +
911 + /* Discard block 0 - outside listener's section, no notification */
912 + tl.discard_count = 0;
913 + test_source_discard(src, 0, GRANULARITY_4K);
914 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
915 + 0, GRANULARITY_4K);
916 + g_assert_cmpint(tl.discard_count, ==, 0);
917 +
918 + /* Discard block 3 - inside listener's section */
919 + test_source_discard(src, GRANULARITY_4K * 3, GRANULARITY_4K);
920 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
921 + GRANULARITY_4K * 3, GRANULARITY_4K);
922 + g_assert_cmpint(tl.discard_count, ==, 1);
923 + g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 3);
924 +
925 + /* Discard spanning boundary (blocks 5-6) - only block 5 in section */
926 + tl.discard_count = 0;
927 + test_source_discard(src, GRANULARITY_4K * 5, GRANULARITY_4K * 2);
928 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
929 + GRANULARITY_4K * 5, GRANULARITY_4K * 2);
930 + g_assert_cmpint(tl.discard_count, ==, 1);
931 + g_assert_cmpuint(tl.last_discard_offset, ==, GRANULARITY_4K * 5);
932 + g_assert_cmpuint(tl.last_discard_size, ==, GRANULARITY_4K);
933 +
934 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
935 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
936 + test_source_free(src);
937 + test_teardown();
938 +}
939 +
940 +/*
941 + * Test: Multiple listeners with different (non-overlapping) sections.
942 + */
943 +static void test_multiple_listeners_different_sections(void)
944 +{
945 + TestRamDiscardSource *src;
946 + RamDiscardManager *rdm;
947 + MemoryRegionSection section1, section2;
948 + TestListener tl1 = { 0, }, tl2 = { 0, };
949 + int ret;
950 +
951 + test_setup();
952 +
953 + src = test_source_new(test_mr, GRANULARITY_4K);
954 +
955 + ret = memory_region_add_ram_discard_source(test_mr,
956 + RAM_DISCARD_SOURCE(src));
957 + g_assert_cmpint(ret, ==, 0);
958 + rdm = memory_region_get_ram_discard_manager(test_mr);
959 +
960 + /* Listener 1: blocks 0-3 */
961 + test_listener_init(&tl1);
962 + section1.mr = test_mr;
963 + section1.offset_within_region = 0;
964 + section1.size = int128_make64(GRANULARITY_4K * 4);
965 + ram_discard_manager_register_listener(rdm, &tl1.rdl, &section1);
966 +
967 + /* Listener 2: blocks 8-11 */
968 + test_listener_init(&tl2);
969 + section2.mr = test_mr;
970 + section2.offset_within_region = GRANULARITY_4K * 8;
971 + section2.size = int128_make64(GRANULARITY_4K * 4);
972 + ram_discard_manager_register_listener(rdm, &tl2.rdl, &section2);
973 +
974 + /* Initially all discarded - no populate notifications */
975 + g_assert_cmpint(tl1.populate_count, ==, 0);
976 + g_assert_cmpint(tl2.populate_count, ==, 0);
977 +
978 + /* Populate blocks 0-3 - only tl1 should be notified */
979 + test_source_populate(src, 0, GRANULARITY_4K * 4);
980 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
981 + 0, GRANULARITY_4K * 4);
982 + g_assert_cmpint(ret, ==, 0);
983 + g_assert_cmpint(tl1.populate_count, ==, 1);
984 + g_assert_cmpint(tl2.populate_count, ==, 0);
985 +
986 + /* Populate blocks 8-11 - only tl2 should be notified */
987 + test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
988 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
989 + GRANULARITY_4K * 8,
990 + GRANULARITY_4K * 4);
991 + g_assert_cmpint(ret, ==, 0);
992 + g_assert_cmpint(tl1.populate_count, ==, 1);
993 + g_assert_cmpint(tl2.populate_count, ==, 1);
994 +
995 + /* Populate blocks 4-7 (gap) - neither listener should be notified */
996 + test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
997 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
998 + GRANULARITY_4K * 4,
999 + GRANULARITY_4K * 4);
1000 + g_assert_cmpint(ret, ==, 0);
1001 + g_assert_cmpint(tl1.populate_count, ==, 1);
1002 + g_assert_cmpint(tl2.populate_count, ==, 1);
1003 +
1004 + ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
1005 + ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
1006 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1007 + test_source_free(src);
1008 + test_teardown();
1009 +}
1010 +
1011 +/*
1012 + * Test: Multiple listeners with overlapping sections.
1013 + */
1014 +static void test_overlapping_listener_sections(void)
1015 +{
1016 + TestRamDiscardSource *src;
1017 + RamDiscardManager *rdm;
1018 + MemoryRegionSection section1, section2;
1019 + TestListener tl1 = { 0, }, tl2 = { 0, };
1020 + int ret;
1021 +
1022 + test_setup();
1023 +
1024 + src = test_source_new(test_mr, GRANULARITY_4K);
1025 +
1026 + ret = memory_region_add_ram_discard_source(test_mr,
1027 + RAM_DISCARD_SOURCE(src));
1028 + g_assert_cmpint(ret, ==, 0);
1029 + rdm = memory_region_get_ram_discard_manager(test_mr);
1030 +
1031 + /* Listener 1: blocks 0-7 */
1032 + test_listener_init(&tl1);
1033 + section1.mr = test_mr;
1034 + section1.offset_within_region = 0;
1035 + section1.size = int128_make64(GRANULARITY_4K * 8);
1036 + ram_discard_manager_register_listener(rdm, &tl1.rdl, &section1);
1037 +
1038 + /* Listener 2: blocks 4-11 (overlaps with tl1 on blocks 4-7) */
1039 + test_listener_init(&tl2);
1040 + section2.mr = test_mr;
1041 + section2.offset_within_region = GRANULARITY_4K * 4;
1042 + section2.size = int128_make64(GRANULARITY_4K * 8);
1043 + ram_discard_manager_register_listener(rdm, &tl2.rdl, &section2);
1044 +
1045 + /* Populate blocks 4-7 (overlap region) - both should be notified */
1046 + test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4);
1047 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1048 + GRANULARITY_4K * 4,
1049 + GRANULARITY_4K * 4);
1050 + g_assert_cmpint(ret, ==, 0);
1051 + g_assert_cmpint(tl1.populate_count, ==, 1);
1052 + g_assert_cmpint(tl2.populate_count, ==, 1);
1053 +
1054 + /* Populate blocks 0-3 - only tl1 */
1055 + test_source_populate(src, 0, GRANULARITY_4K * 4);
1056 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1057 + 0, GRANULARITY_4K * 4);
1058 + g_assert_cmpint(ret, ==, 0);
1059 + g_assert_cmpint(tl1.populate_count, ==, 2);
1060 + g_assert_cmpint(tl2.populate_count, ==, 1);
1061 +
1062 + /* Populate blocks 8-11 - only tl2 */
1063 + test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4);
1064 + ret = ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(src),
1065 + GRANULARITY_4K * 8,
1066 + GRANULARITY_4K * 4);
1067 + g_assert_cmpint(ret, ==, 0);
1068 + g_assert_cmpint(tl1.populate_count, ==, 2);
1069 + g_assert_cmpint(tl2.populate_count, ==, 2);
1070 +
1071 + ram_discard_manager_unregister_listener(rdm, &tl2.rdl);
1072 + ram_discard_manager_unregister_listener(rdm, &tl1.rdl);
1073 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1074 + test_source_free(src);
1075 + test_teardown();
1076 +}
1077 +
1078 +/*
1079 + * Test: Listener at exact memory region boundaries.
1080 + */
1081 +static void test_boundary_section(void)
1082 +{
1083 + TestRamDiscardSource *src;
1084 + RamDiscardManager *rdm;
1085 + MemoryRegionSection section;
1086 + TestListener tl = { 0, };
1087 + uint64_t last_offset;
1088 + int ret;
1089 +
1090 + test_setup();
1091 +
1092 + src = test_source_new(test_mr, GRANULARITY_4K);
1093 +
1094 + /* Populate last 4 blocks of the region */
1095 + last_offset = TEST_REGION_SIZE - GRANULARITY_4K * 4;
1096 + test_source_populate(src, last_offset, GRANULARITY_4K * 4);
1097 +
1098 + ret = memory_region_add_ram_discard_source(test_mr,
1099 + RAM_DISCARD_SOURCE(src));
1100 + g_assert_cmpint(ret, ==, 0);
1101 + rdm = memory_region_get_ram_discard_manager(test_mr);
1102 +
1103 + /* Register listener for exactly the last 4 blocks */
1104 + test_listener_init(&tl);
1105 + section.mr = test_mr;
1106 + section.offset_within_region = last_offset;
1107 + section.size = int128_make64(GRANULARITY_4K * 4);
1108 + ram_discard_manager_register_listener(rdm, &tl.rdl, &section);
1109 +
1110 + /* Should receive notification for the populated range */
1111 + g_assert_cmpint(tl.populate_count, ==, 1);
1112 + g_assert_cmpuint(tl.last_populate_offset, ==, last_offset);
1113 + g_assert_cmpuint(tl.last_populate_size, ==, GRANULARITY_4K * 4);
1114 +
1115 + /* Discard exactly at boundary */
1116 + tl.discard_count = 0;
1117 + test_source_discard(src, last_offset, GRANULARITY_4K * 4);
1118 + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src),
1119 + last_offset, GRANULARITY_4K * 4);
1120 + g_assert_cmpint(tl.discard_count, ==, 1);
1121 +
1122 + ram_discard_manager_unregister_listener(rdm, &tl.rdl);
1123 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src));
1124 + test_source_free(src);
1125 + test_teardown();
1126 +}
1127 +
1128 +static int count_discarded_blocks(const MemoryRegionSection *section,
1129 + void *opaque)
1130 +{
1131 + int *count = opaque;
1132 + *count += int128_get64(section->size) / GRANULARITY_4K;
1133 + return 0;
1134 +}
1135 +
1136 +/*
1137 + * Test: replay_discarded with two sources (union semantics).
1138 + */
1139 +static void test_replay_discarded(void)
1140 +{
1141 + TestRamDiscardSource *src1, *src2;
1142 + RamDiscardManager *rdm;
1143 + MemoryRegionSection section;
1144 + int count = 0;
1145 + int ret;
1146 +
1147 + test_setup();
1148 +
1149 + src1 = test_source_new(test_mr, GRANULARITY_4K);
1150 + src2 = test_source_new(test_mr, GRANULARITY_4K);
1151 +
1152 + /*
1153 + * src1: blocks 0-3 populated, rest discarded
1154 + * src2: blocks 2-5 populated, rest discarded
1155 + * Aggregated populated: blocks 2-3 (intersection)
1156 + * Aggregated discarded: blocks 0-1, 4-5, 6+ (union of discarded)
1157 + */
1158 + test_source_populate(src1, 0, GRANULARITY_4K * 4);
1159 + test_source_populate(src2, GRANULARITY_4K * 2, GRANULARITY_4K * 4);
1160 +
1161 + ret = memory_region_add_ram_discard_source(test_mr,
1162 + RAM_DISCARD_SOURCE(src1));
1163 + g_assert_cmpint(ret, ==, 0);
1164 + ret = memory_region_add_ram_discard_source(test_mr,
1165 + RAM_DISCARD_SOURCE(src2));
1166 + g_assert_cmpint(ret, ==, 0);
1167 +
1168 + rdm = memory_region_get_ram_discard_manager(test_mr);
1169 +
1170 + section.mr = test_mr;
1171 + section.offset_within_region = 0;
1172 + section.size = int128_make64(GRANULARITY_4K * 8);
1173 +
1174 + /* Count discarded blocks */
1175 + ret = ram_discard_manager_replay_discarded(rdm, &section,
1176 + count_discarded_blocks, &count);
1177 +
1178 + g_assert_cmpint(ret, ==, 0);
1179 + /* Discarded: blocks 0-1 (2), blocks 4-5 (2), blocks 6-7 (2) = 6 blocks */
1180 + g_assert_cmpint(count, ==, 6);
1181 +
1182 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2));
1183 + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src1));
1184 +
1185 + test_source_free(src2);
1186 + test_source_free(src1);
1187 + test_teardown();
1188 +}
1189 +
1190 +int main(int argc, char **argv)
1191 +{
1192 + g_test_init(&argc, &argv, NULL);
1193 +
1194 + module_call_init(MODULE_INIT_QOM);
1195 + type_register_static(&test_rds_info);
1196 +
1197 + g_test_add_func("/ram-discard-manager/single-source/basic",
1198 + test_single_source_basic);
1199 + g_test_add_func("/ram-discard-manager/single-source/listener",
1200 + test_single_source_listener);
1201 + g_test_add_func("/ram-discard-manager/two-sources/same-granularity",
1202 + test_two_sources_same_granularity);
1203 + g_test_add_func("/ram-discard-manager/two-sources/different-granularity",
1204 + test_two_sources_different_granularity);
1205 + g_test_add_func("/ram-discard-manager/two-sources/notification",
1206 + test_two_sources_notification);
1207 + g_test_add_func("/ram-discard-manager/dynamic/add-source-with-listener",
1208 + test_add_source_with_listener);
1209 + g_test_add_func("/ram-discard-manager/dynamic/remove-source-with-listener",
1210 + test_remove_source_with_listener);
1211 + g_test_add_func("/ram-discard-manager/dynamic/readd-source-with-listener",
1212 + test_readd_source_with_listener);
1213 + g_test_add_func("/ram-discard-manager/edge/duplicate-source",
1214 + test_duplicate_source);
1215 + g_test_add_func("/ram-discard-manager/edge/populate-rollback",
1216 + test_populate_rollback);
1217 + g_test_add_func("/ram-discard-manager/edge/replay-intersection",
1218 + test_replay_populated_intersection);
1219 + g_test_add_func("/ram-discard-manager/edge/no-sources",
1220 + test_no_sources);
1221 + g_test_add_func("/ram-discard-manager/multi-source/redundant-discard",
1222 + test_redundant_discard);
1223 + g_test_add_func("/ram-discard-manager/listener/partial-section",
1224 + test_partial_listener_section);
1225 + g_test_add_func("/ram-discard-manager/listener/multiple-different",
1226 + test_multiple_listeners_different_sections);
1227 + g_test_add_func("/ram-discard-manager/listener/overlapping",
1228 + test_overlapping_listener_sections);
1229 + g_test_add_func("/ram-discard-manager/edge/boundary-section",
1230 + test_boundary_section);
1231 + g_test_add_func("/ram-discard-manager/multi-source/replay-discarded",
1232 + test_replay_discarded);
1233 +
1234 + return g_test_run();
1235 +}