From nobody Mon Mar 2 11:03:51 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1772114519; cv=none; d=zohomail.com; s=zohoarc; b=YpD8ciRBN03/PhmLSk5YUF2ZTFMo5LtWgMxrGUH+nv0BU0jnh11YwpBCEM3ClHY6X/bK2f2gPevvwd4oK4dzkdWpa+SaDpEG6/hNZbnclsIOnwH7xHejRbBSa4pYxm9LoMFmh3xNc4QuCJE6xd1gJgY5N57EkgSwYcsnlRAnGQg= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1772114519; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=v+Wfy79tDlnvrYFZGxYmwhOvwpG3r+hIgVzlFKhZ7Nc=; b=gE3Qu4nmUFhA38HgdsiS9Enjs7bEIwQ0VkQUp6MXSt0iuY5CsB/ApEprU0WZn33W7QdE1mBqZJGAHTgKvwAU/6/f2qHj/VtkFTJxdK7+qZjgvXGHPaAmU1p1MTDYGJ2O0yzkR9X2NL1GfXv/kq6AgR/Gg4Lo42nCtusfXi3mHiM= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1772114519048540.1360625946443; Thu, 26 Feb 2026 06:01:59 -0800 (PST) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1vvbvQ-00039J-RY; Thu, 26 Feb 2026 09:01:08 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vvbvO-000330-Cs for qemu-devel@nongnu.org; Thu, 26 Feb 2026 09:01:06 -0500 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vvbvK-0006NB-6g for qemu-devel@nongnu.org; Thu, 26 Feb 2026 09:01:06 -0500 Received: from mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-244-b6nZpNh4M2Cpa3UJZQz58Q-1; Thu, 26 Feb 2026 09:00:55 -0500 Received: from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-08.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 700C818004AD; Thu, 26 Feb 2026 14:00:53 +0000 (UTC) Received: from localhost (unknown [10.45.242.29]) by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 4C29E30001BF; Thu, 26 Feb 2026 14:00:52 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1772114461; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=v+Wfy79tDlnvrYFZGxYmwhOvwpG3r+hIgVzlFKhZ7Nc=; b=h1Ec5z4Oy0q3PkmoQBecbjSAVBefeOGm8Y7pv1KkDiZNPGePARMts6ojJ/+6Esv7L0pF1I 7ieIeSdVKmKIcdVllbCE5MRWMdBTYClUmcJjY8bB04M+FS8pQ3knomC4QGMP2L8yTJr2oe r5mnZ3h7G4JG96ncCaGLN0TLDcyu9J8= X-MC-Unique: b6nZpNh4M2Cpa3UJZQz58Q-1 X-Mimecast-MFC-AGG-ID: b6nZpNh4M2Cpa3UJZQz58Q_1772114453 From: marcandre.lureau@redhat.com To: qemu-devel@nongnu.org Cc: Ben Chaney , "Michael S. Tsirkin" , =?UTF-8?q?C=C3=A9dric=20Le=20Goater?= , Paolo Bonzini , Alex Williamson , Fabiano Rosas , David Hildenbrand , =?UTF-8?q?Philippe=20Mathieu-Daud=C3=A9?= , Peter Xu , kvm@vger.kernel.org, Mark Kanda , =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Subject: [PATCH v3 15/15] tests: add unit tests for RamDiscardManager multi-source aggregation Date: Thu, 26 Feb 2026 15:00:00 +0100 Message-ID: <20260226140001.3622334-16-marcandre.lureau@redhat.com> In-Reply-To: <20260226140001.3622334-1-marcandre.lureau@redhat.com> References: <20260226140001.3622334-1-marcandre.lureau@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.4 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.133.124; envelope-from=marcandre.lureau@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -10 X-Spam_score: -1.1 X-Spam_bar: - X-Spam_report: (-1.1 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H5=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.306, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.668, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=no autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1772114520586158500 From: Marc-Andr=C3=A9 Lureau 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=C3=A9 Lureau --- tests/unit/test-ram-discard-manager-stubs.c | 48 + tests/unit/test-ram-discard-manager.c | 1234 +++++++++++++++++++ tests/unit/meson.build | 8 +- 3 files changed, 1289 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/tests/unit/test-ram-discard-manager-stubs.c b/tests/unit/test-= ram-discard-manager-stubs.c new file mode 100644 index 00000000000..5daef09e49e --- /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 =3D ram_discard_manager_new(mr); + } + return ram_discard_manager_add_source(mr->rdm, source); +} + +void memory_region_del_ram_discard_source(MemoryRegion *mr, + RamDiscardSource *source) +{ + RamDiscardManager *rdm =3D mr->rdm; + + if (!rdm) { + return; + } + + ram_discard_manager_del_source(rdm, source); +} + +uint64_t memory_region_size(MemoryRegion *mr) +{ + return int128_get64(mr->size); +} + +MemoryRegionSection *memory_region_section_new_copy(MemoryRegionSection *s) +{ + MemoryRegionSection *copy =3D g_new(MemoryRegionSection, 1); + *copy =3D *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-di= scard-manager.c new file mode 100644 index 00000000000..9bd418d389a --- /dev/null +++ b/tests/unit/test-ram-discard-manager.c @@ -0,0 +1,1234 @@ +/* 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 =3D TEST_RAM_DISCARD_SOURCE(rds); + + g_assert(mr =3D=3D src->mr); + return src->granularity; +} + +static bool test_rds_is_populated(const RamDiscardSource *rds, + const MemoryRegionSection *section) +{ + TestRamDiscardSource *src =3D TEST_RAM_DISCARD_SOURCE(rds); + uint64_t offset =3D section->offset_within_region; + uint64_t size =3D int128_get64(section->size); + uint64_t first_bit =3D offset / src->granularity; + uint64_t last_bit =3D (offset + size - 1) / src->granularity; + unsigned long found; + + g_assert(section->mr =3D=3D src->mr); + + /* Check if any bit in range is zero (discarded) */ + found =3D 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 =3D RAM_DISCARD_SOURCE_CLASS(klass); + + rdsc->get_min_granularity =3D test_rds_get_min_granularity; + rdsc->is_populated =3D test_rds_is_populated; +} + +static const TypeInfo test_rds_info =3D { + .name =3D TYPE_TEST_RAM_DISCARD_SOURCE, + .parent =3D TYPE_OBJECT, + .instance_size =3D sizeof(TestRamDiscardSource), + .class_init =3D test_rds_class_init, + .interfaces =3D (const InterfaceInfo[]) { + { TYPE_RAM_DISCARD_SOURCE }, + { } + }, +}; + +static TestRamDiscardSource *test_source_new(MemoryRegion *mr, + uint64_t granularity) +{ + TestRamDiscardSource *src; + uint64_t region_size =3D memory_region_size(mr); + + src =3D TEST_RAM_DISCARD_SOURCE(object_new(TYPE_TEST_RAM_DISCARD_SOURC= E)); + src->mr =3D mr; + src->granularity =3D granularity; + src->bitmap_size =3D DIV_ROUND_UP(region_size, granularity); + src->bitmap =3D 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 =3D offset / src->granularity; + uint64_t nbits =3D 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 =3D offset / src->granularity; + uint64_t nbits =3D 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 =3D container_of(rdl, TestListener, rdl); + + tl->populate_call_num++; + if (tl->fail_on_populate > 0 && + tl->populate_call_num >=3D tl->fail_on_populate) { + return -ENOMEM; + } + + tl->populate_count++; + tl->last_populate_offset =3D section->offset_within_region; + tl->last_populate_size =3D int128_get64(section->size); + return 0; +} + +static void test_listener_discard(RamDiscardListener *rdl, + const MemoryRegionSection *section) +{ + TestListener *tl =3D container_of(rdl, TestListener, rdl); + + tl->discard_count++; + tl->last_discard_offset =3D section->offset_within_region; + tl->last_discard_size =3D 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 =3D g_new0(MemoryRegion, 1); + test_mr->size =3D int128_make64(TEST_REGION_SIZE); + test_mr->ram =3D 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 =3D NULL; +} + +static void test_single_source_basic(void) +{ + TestRamDiscardSource *src; + RamDiscardManager *rdm; + MemoryRegionSection section; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + g_assert_null(rdm); + + /* Add source */ + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + g_assert_nonnull(rdm); + + g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr), + =3D=3D, GRANULARITY_4K); + + /* Initially all discarded */ + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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 =3D int128_make64(GRANULARITY_4K * 4); + g_assert_true(ram_discard_manager_is_populated(rdm, §ion)); + + /* Check section that spans populated and discarded */ + section.size =3D 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 =3D { 0, }; + int ret; + + test_setup(); + + src =3D 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 =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + g_assert_nonnull(rdm); + + /* Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 2); + + /* Notify populate for new range */ + tl.populate_count =3D 0; + test_source_populate(src, GRANULARITY_4K * 16, GRANULARITY_4K * 2); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + GRANULARITY_4K * 16, + GRANULARITY_4K * 2); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl.populate_count, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_offset, =3D=3D, GRANULARITY_4K * 16); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, GRANULARITY_4K * 2); + + /* Notify discard */ + tl.discard_count =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_discard_offset, =3D=3D, 0); + g_assert_cmpuint(tl.last_discard_size, =3D=3D, 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 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Add first source */ + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + + /* Add second source */ + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D 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), + =3D=3D, GRANULARITY_4K); + + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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) =3D 4K. + */ +static void test_two_sources_different_granularity(void) +{ + TestRamDiscardSource *src_4k, *src_2m; + RamDiscardManager *rdm; + MemoryRegionSection section; + int ret; + + test_setup(); + + src_4k =3D test_source_new(test_mr, GRANULARITY_4K); + src_2m =3D test_source_new(test_mr, GRANULARITY_2M); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src_4k)); + g_assert_cmpint(ret, =3D=3D, 0); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src_2m)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + g_assert_cmpuint(ram_discard_manager_get_min_granularity(rdm, test_mr), + =3D=3D, GRANULARITY_4K); + + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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 bloc= k */ + 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_4= k) */ + section.offset_within_region =3D 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_2= m)); + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src_4= k)); + + 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 =3D { 0, }; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D test_source_new(test_mr, GRANULARITY_4K); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 0); + + /* Populate in src1 only - no notification (src2 still discarded) */ + test_source_populate(src1, 0, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c1), + 0, GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl.populate_count, =3D=3D, 0); + + /* Populate same range in src2 - now should notify */ + test_source_populate(src2, 0, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c2), + 0, GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl.populate_count, =3D=3D, 1); + + /* Discard from src1 - should notify discard immediately */ + tl.discard_count =3D 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, =3D=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Populate some range in src1 */ + test_source_populate(src1, 0, GRANULARITY_4K * 8); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpint(tl.last_populate_offset, =3D=3D, 0); + g_assert_cmpint(tl.last_populate_size, =3D=3D, 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 region= s */ + tl.discard_count =3D 0; + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 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 bey= ond + * 4K*8 were already discarded in src1, so adding src2 doesn't change = them. + */ + g_assert_cmpint(tl.discard_count, =3D=3D, 1); + g_assert_cmpint(tl.last_discard_offset, =3D=3D, GRANULARITY_4K * 4); + g_assert_cmpint(tl.last_discard_size, =3D=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D 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 =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, GRANULARITY_4K * 4); + + /* Remove src2 - blocks 4-8 should become populated */ + tl.populate_count =3D 0; + memory_region_del_ram_discard_source(test_mr, RAM_DISCARD_SOURCE(src2)= ); + + /* Listener should be notified about newly populated region (4K*4 to 4= K*8) */ + g_assert_cmpint(tl.populate_count, >=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Populate some range in src */ + test_source_populate(src, 0, GRANULARITY_4K * 8); + + /* 1. Add source */ + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* 2. Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, GRANULARITY_4K * 8); + + /* 3. Remove source */ + tl.populate_count =3D 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 popul= ated. + */ + g_assert_cmpint(tl.populate_count, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_offset, =3D=3D, GRANULARITY_4K * 8); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, TEST_REGION_SIZE - GRA= NULARITY_4K * 8); + + /* 4. Add source back */ + tl.discard_count =3D 0; + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + + /* + * Now we have 1 source again. The range (32K to end) is discarded aga= in. + * Listener should be notified about this discard. + */ + g_assert_cmpint(tl.discard_count, =3D=3D, 1); + g_assert_cmpuint(tl.last_discard_offset, =3D=3D, GRANULARITY_4K * 8); + g_assert_cmpuint(tl.last_discard_size, =3D=3D, TEST_REGION_SIZE - GRAN= ULARITY_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 =3D test_source_new(test_mr, GRANULARITY_4K); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + + /* Adding same source again should fail */ + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, -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 =3D { 0, }, tl2 =3D { 0, }; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register two listeners */ + test_listener_init(&tl1); + test_listener_init(&tl2); + tl2.fail_on_populate =3D 1; /* Second listener fails on first populat= e */ + + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D int128_make64(TEST_REGION_SIZE); + + /* + * Register tl2 first so it's visited second (QLIST_INSERT_HEAD revers= es + * 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 =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + 0, GRANULARITY_4K); + g_assert_cmpint(ret, =3D=3D, -ENOMEM); + + /* First listener should have received populate then discard (rollback= ) */ + g_assert_cmpint(tl1.populate_count, =3D=3D, 1); + g_assert_cmpint(tl1.discard_count, =3D=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D 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 =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener - should only get notified about intersection */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_offset, =3D=3D, GRANULARITY_4K * 4); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Add sources */ + ret =3D memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOUR= CE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + ret =3D memory_region_add_ram_discard_source(test_mr, RAM_DISCARD_SOUR= CE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D 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 (as src2 is= also populated) */ + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c1), 0, GRANULARITY_4K); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl.populate_count, =3D=3D, 1); + + /* Now Discard src1 -> Aggregate Discarded */ + tl.discard_count =3D 0; + test_source_discard(src1, 0, GRANULARITY_4K); + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src1), 0, G= RANULARITY_4K); + g_assert_cmpint(tl.discard_count, =3D=3D, 1); + + /* Now Discard src2 -> Aggregate Discarded (Already Discarded!) */ + /* Listener should NOT receive another discard notification for the sa= me range. */ + test_source_discard(src2, 0, GRANULARITY_4K); + ram_discard_manager_notify_discard(rdm, RAM_DISCARD_SOURCE(src2), 0, G= RANULARITY_4K); + + g_assert_cmpint(tl.discard_count, =3D=3D, 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 =3D { 0, }; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Populate blocks 0-7 */ + test_source_populate(src, 0, GRANULARITY_4K * 8); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D 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 =3D test_mr; + section.offset_within_region =3D GRANULARITY_4K * 2; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_offset, =3D=3D, GRANULARITY_4K * 2); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, GRANULARITY_4K * 4); + + /* Discard block 0 - outside listener's section, no notification */ + tl.discard_count =3D 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, =3D=3D, 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_discard_offset, =3D=3D, GRANULARITY_4K * 3); + + /* Discard spanning boundary (blocks 5-6) - only block 5 in section */ + tl.discard_count =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_discard_offset, =3D=3D, GRANULARITY_4K * 5); + g_assert_cmpuint(tl.last_discard_size, =3D=3D, 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 =3D { 0, }, tl2 =3D { 0, }; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Listener 1: blocks 0-3 */ + test_listener_init(&tl1); + section1.mr =3D test_mr; + section1.offset_within_region =3D 0; + section1.size =3D 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 =3D test_mr; + section2.offset_within_region =3D GRANULARITY_4K * 8; + section2.size =3D 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, =3D=3D, 0); + g_assert_cmpint(tl2.populate_count, =3D=3D, 0); + + /* Populate blocks 0-3 - only tl1 should be notified */ + test_source_populate(src, 0, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + 0, GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 1); + g_assert_cmpint(tl2.populate_count, =3D=3D, 0); + + /* Populate blocks 8-11 - only tl2 should be notified */ + test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + GRANULARITY_4K * 8, + GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 1); + g_assert_cmpint(tl2.populate_count, =3D=3D, 1); + + /* Populate blocks 4-7 (gap) - neither listener should be notified */ + test_source_populate(src, GRANULARITY_4K * 4, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + GRANULARITY_4K * 4, + GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 1); + g_assert_cmpint(tl2.populate_count, =3D=3D, 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 =3D { 0, }, tl2 =3D { 0, }; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Listener 1: blocks 0-7 */ + test_listener_init(&tl1); + section1.mr =3D test_mr; + section1.offset_within_region =3D 0; + section1.size =3D 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 =3D test_mr; + section2.offset_within_region =3D GRANULARITY_4K * 4; + section2.size =3D 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 =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + GRANULARITY_4K * 4, + GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 1); + g_assert_cmpint(tl2.populate_count, =3D=3D, 1); + + /* Populate blocks 0-3 - only tl1 */ + test_source_populate(src, 0, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + 0, GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 2); + g_assert_cmpint(tl2.populate_count, =3D=3D, 1); + + /* Populate blocks 8-11 - only tl2 */ + test_source_populate(src, GRANULARITY_4K * 8, GRANULARITY_4K * 4); + ret =3D ram_discard_manager_notify_populate(rdm, RAM_DISCARD_SOURCE(sr= c), + GRANULARITY_4K * 8, + GRANULARITY_4K * 4); + g_assert_cmpint(ret, =3D=3D, 0); + g_assert_cmpint(tl1.populate_count, =3D=3D, 2); + g_assert_cmpint(tl2.populate_count, =3D=3D, 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 =3D { 0, }; + uint64_t last_offset; + int ret; + + test_setup(); + + src =3D test_source_new(test_mr, GRANULARITY_4K); + + /* Populate last 4 blocks of the region */ + last_offset =3D TEST_REGION_SIZE - GRANULARITY_4K * 4; + test_source_populate(src, last_offset, GRANULARITY_4K * 4); + + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src)); + g_assert_cmpint(ret, =3D=3D, 0); + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + /* Register listener for exactly the last 4 blocks */ + test_listener_init(&tl); + section.mr =3D test_mr; + section.offset_within_region =3D last_offset; + section.size =3D 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, =3D=3D, 1); + g_assert_cmpuint(tl.last_populate_offset, =3D=3D, last_offset); + g_assert_cmpuint(tl.last_populate_size, =3D=3D, GRANULARITY_4K * 4); + + /* Discard exactly at boundary */ + tl.discard_count =3D 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, =3D=3D, 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 =3D opaque; + *count +=3D 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 =3D 0; + int ret; + + test_setup(); + + src1 =3D test_source_new(test_mr, GRANULARITY_4K); + src2 =3D 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 =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src1)); + g_assert_cmpint(ret, =3D=3D, 0); + ret =3D memory_region_add_ram_discard_source(test_mr, + RAM_DISCARD_SOURCE(src2)); + g_assert_cmpint(ret, =3D=3D, 0); + + rdm =3D memory_region_get_ram_discard_manager(test_mr); + + section.mr =3D test_mr; + section.offset_within_region =3D 0; + section.size =3D int128_make64(GRANULARITY_4K * 8); + + /* Count discarded blocks */ + ret =3D ram_discard_manager_replay_discarded(rdm, §ion, + count_discarded_blocks, &co= unt); + + g_assert_cmpint(ret, =3D=3D, 0); + /* Discarded: blocks 0-1 (2), blocks 4-5 (2), blocks 6-7 (2) =3D 6 blo= cks */ + g_assert_cmpint(count, =3D=3D, 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-granularit= y", + 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-liste= ner", + test_remove_source_with_listener); + g_test_add_func("/ram-discard-manager/dynamic/readd-source-with-listen= er", + 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 41e8b06c339..7a569ef7abd 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -136,7 +136,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 +=3D {'test-util-filemonitor': []} --=20 2.53.0