about summary refs log tree commit diff
path: root/src/core/platform/tests/test-nmp-object.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
committerMichael Biebl <biebl@debian.org>2021-02-11 18:11:46 +0100
commit80ec1decc49c72efec2a8b87c06245c92c0ab807 (patch)
treee3b229aa94e8dcf0590f2317664176e7b8f7607b /src/core/platform/tests/test-nmp-object.c
parent65f86e8f56267192d42f2b629fc6b0c99fb9cd0c (diff)
New upstream version 1.29.90 upstream/1.29.90
Diffstat (limited to 'src/core/platform/tests/test-nmp-object.c')
-rw-r--r--src/core/platform/tests/test-nmp-object.c617
1 files changed, 617 insertions, 0 deletions
diff --git a/src/core/platform/tests/test-nmp-object.c b/src/core/platform/tests/test-nmp-object.c
new file mode 100644
index 00000000..310611ee
--- /dev/null
+++ b/src/core/platform/tests/test-nmp-object.c
@@ -0,0 +1,617 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2015 - 2017 Red Hat, Inc.
+ */
+
+#include "src/core/nm-default-daemon.h"
+
+#include <libudev.h>
+#include <linux/pkt_sched.h>
+
+#include "platform/nmp-object.h"
+#include "nm-udev-aux/nm-udev-utils.h"
+
+#include "nm-test-utils-core.h"
+
+struct {
+    GList *udev_devices;
+} global;
+
+/*****************************************************************************/
+
+static void
+test_obj_base(void)
+{
+    static const union {
+        GObject   g;
+        NMPObject k;
+    } x = {};
+    static const union {
+        GTypeClass k;
+        NMPClass   c;
+    } l                        = {};
+    static const GObject *   g = &x.g;
+    static const GTypeClass *k = &l.k;
+    static const NMPObject * o = &x.k;
+    static const NMPClass *  c = &l.c;
+
+    NMObjBaseInst * obj;
+    gs_unref_object GCancellable *obj_cancellable = g_cancellable_new();
+    nm_auto_nmpobj NMPObject *obj_link            = nmp_object_new_link(10);
+
+    g_assert(&g->g_type_instance == (void *) &o->_class);
+    g_assert(&g->g_type_instance.g_class == (void *) &o->_class);
+
+    g_assert(sizeof(o->parent.parent) == sizeof(GTypeInstance));
+
+    g_assert(&c->parent == (void *) c);
+    g_assert(&c->parent.parent.g_type_class == (void *) c);
+    g_assert(&c->parent.parent.g_type == (void *) c);
+    g_assert(&c->parent.parent.g_type == &k->g_type);
+
+    g_assert(sizeof(c->parent.parent) == sizeof(GTypeClass));
+
+    g_assert(&o->parent == (void *) o);
+    g_assert(&o->parent.klass == (void *) &o->_class);
+
+    obj = (NMObjBaseInst *) obj_cancellable;
+    g_assert(!NMP_CLASS_IS_VALID((NMPClass *) obj->klass));
+    g_assert(G_TYPE_CHECK_INSTANCE_TYPE(obj, G_TYPE_CANCELLABLE));
+
+    obj = (NMObjBaseInst *) obj_link;
+    g_assert(NMP_CLASS_IS_VALID((NMPClass *) obj->klass));
+    g_assert(!G_TYPE_CHECK_INSTANCE_TYPE(obj, G_TYPE_CANCELLABLE));
+}
+
+/*****************************************************************************/
+
+static gboolean
+_nmp_object_id_equal(const NMPObject *a, const NMPObject *b)
+{
+    gboolean a_b = nmp_object_id_equal(a, b);
+
+    g_assert(NM_IN_SET(a_b, FALSE, TRUE) && a_b == nmp_object_id_equal(b, a));
+    return a_b;
+}
+#define nmp_object_id_equal _nmp_object_id_equal
+
+static gboolean
+_nmp_object_equal(const NMPObject *a, const NMPObject *b)
+{
+    gboolean a_b = nmp_object_equal(a, b);
+
+    g_assert(NM_IN_SET(a_b, FALSE, TRUE) && a_b == nmp_object_equal(b, a));
+    return a_b;
+}
+#define nmp_object_equal _nmp_object_equal
+
+/*****************************************************************************/
+
+static void
+_assert_cache_multi_lookup_contains(const NMPCache *             cache,
+                                    const NMDedupMultiHeadEntry *head_entry,
+                                    const NMPObject *            obj,
+                                    gboolean                     visible_only,
+                                    gboolean                     contains)
+{
+    NMDedupMultiIter iter;
+    gboolean         found;
+    guint            i, len;
+    const NMPObject *o;
+
+    g_assert(NMP_OBJECT_IS_VALID(obj));
+
+    g_assert(nmp_cache_lookup_obj(cache, obj) == obj);
+    g_assert(!head_entry
+             || (head_entry->len > 0
+                 && c_list_length(&head_entry->lst_entries_head) == head_entry->len));
+
+    len = head_entry ? head_entry->len : 0;
+
+    found = FALSE;
+    i     = 0;
+    nmp_cache_iter_for_each (&iter, head_entry, &o) {
+        g_assert(NMP_OBJECT_IS_VALID(o));
+        if (obj == o) {
+            if (!visible_only || nmp_object_is_visible(o)) {
+                g_assert(!found);
+                found = TRUE;
+            }
+        }
+        i++;
+    }
+
+    g_assert(len == i);
+    g_assert(!!contains == found);
+}
+
+static void
+_assert_cache_multi_lookup_contains_link(const NMPCache * cache,
+                                         gboolean         visible_only,
+                                         const NMPObject *obj,
+                                         gboolean         contains)
+{
+    const NMDedupMultiHeadEntry *head_entry;
+    NMPLookup                    lookup;
+
+    g_assert(cache);
+
+    nmp_lookup_init_obj_type(&lookup, NMP_OBJECT_TYPE_LINK);
+    head_entry = nmp_cache_lookup(cache, &lookup);
+    _assert_cache_multi_lookup_contains(cache, head_entry, obj, visible_only, contains);
+}
+
+/*****************************************************************************/
+
+static void
+ops_post_check(NMPCache *       cache,
+               NMPCacheOpsType  ops_type,
+               const NMPObject *obj_old,
+               const NMPObject *obj_new,
+               const NMPObject *obj_new_expected,
+               NMPCacheOpsType  expected_ops_type)
+{
+    g_assert(cache);
+
+    g_assert_cmpint(expected_ops_type, ==, ops_type);
+
+    switch (ops_type) {
+    case NMP_CACHE_OPS_ADDED:
+        g_assert(!obj_old);
+        g_assert(NMP_OBJECT_IS_VALID(obj_new));
+        g_assert(nmp_object_is_alive(obj_new));
+        g_assert(nmp_object_id_equal(obj_new_expected, obj_new));
+        g_assert(nmp_object_equal(obj_new_expected, obj_new));
+        break;
+    case NMP_CACHE_OPS_UPDATED:
+        g_assert(obj_old != obj_new);
+        g_assert(NMP_OBJECT_IS_VALID(obj_old));
+        g_assert(NMP_OBJECT_IS_VALID(obj_new));
+        g_assert(nmp_object_is_alive(obj_old));
+        g_assert(nmp_object_is_alive(obj_new));
+        g_assert(nmp_object_id_equal(obj_new_expected, obj_new));
+        g_assert(nmp_object_id_equal(obj_new_expected, obj_old));
+        g_assert(nmp_object_id_equal(obj_old, obj_new));
+        g_assert(nmp_object_equal(obj_new_expected, obj_new));
+        g_assert(!nmp_object_equal(obj_new_expected, obj_old));
+        g_assert(!nmp_object_equal(obj_old, obj_new));
+        break;
+    case NMP_CACHE_OPS_REMOVED:
+        g_assert(!obj_new);
+        g_assert(NMP_OBJECT_IS_VALID(obj_old));
+        g_assert(nmp_object_is_alive(obj_old));
+        if (obj_new_expected)
+            g_assert(nmp_object_id_equal(obj_new_expected, obj_old));
+        break;
+    case NMP_CACHE_OPS_UNCHANGED:
+        g_assert(obj_old == obj_new);
+        if (obj_old) {
+            g_assert(NMP_OBJECT_IS_VALID(obj_old));
+            g_assert(nmp_object_is_alive(obj_old));
+            g_assert(nmp_object_equal(obj_old, obj_new));
+            g_assert(nmp_object_id_equal(obj_new_expected, obj_new));
+        } else
+            g_assert(!obj_new_expected);
+        break;
+    default:
+        g_assert_not_reached();
+    }
+}
+
+static void
+_nmp_cache_update_netlink(NMPCache *        cache,
+                          NMPObject *       obj,
+                          const NMPObject **out_obj_old,
+                          const NMPObject **out_obj_new,
+                          NMPCacheOpsType   expected_ops_type)
+{
+    NMPCacheOpsType  ops_type;
+    const NMPObject *obj_prev;
+    const NMPObject *obj_old;
+    const NMPObject *obj_new;
+    nm_auto_nmpobj NMPObject *obj_new_expected = NULL;
+
+    g_assert(cache);
+    g_assert(NMP_OBJECT_IS_VALID(obj));
+
+    obj_prev         = nmp_cache_lookup_link(cache, NMP_OBJECT_CAST_LINK(obj)->ifindex);
+    obj_new_expected = nmp_object_clone(obj, FALSE);
+    if (obj_prev && obj_prev->_link.udev.device)
+        obj_new_expected->_link.udev.device = udev_device_ref(obj_prev->_link.udev.device);
+    _nmp_object_fixup_link_udev_fields(&obj_new_expected, NULL, nmp_cache_use_udev_get(cache));
+
+    ops_type = nmp_cache_update_netlink(cache, obj, FALSE, &obj_old, &obj_new);
+    ops_post_check(cache,
+                   ops_type,
+                   obj_old,
+                   obj_new,
+                   nmp_object_is_alive(obj_new_expected) ? obj_new_expected : NULL,
+                   expected_ops_type);
+
+    if (out_obj_new)
+        *out_obj_new = obj_new;
+    else
+        nmp_object_unref(obj_new);
+    if (out_obj_old)
+        *out_obj_old = obj_old;
+    else
+        nmp_object_unref(obj_old);
+}
+
+static const NMPlatformLink pl_link_2 = {
+    .ifindex = 2,
+    .name    = "eth0",
+    .type    = NM_LINK_TYPE_ETHERNET,
+};
+
+static const NMPlatformLink pl_link_3 = {
+    .ifindex = 3,
+    .name    = "wlan0",
+    .type    = NM_LINK_TYPE_WIFI,
+};
+
+static void
+test_cache_link(void)
+{
+    NMPCache *                      cache;
+    NMPObject *                     objm1;
+    const NMPObject *               obj_old, *obj_new;
+    NMPObject                       objs1;
+    struct udev_device *            udev_device_2 = g_list_nth_data(global.udev_devices, 0);
+    struct udev_device *            udev_device_3 = g_list_nth_data(global.udev_devices, 0);
+    NMPCacheOpsType                 ops_type;
+    nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = NULL;
+    gboolean                                           use_udev  = nmtst_get_rand_uint32() % 2;
+
+    multi_idx = nm_dedup_multi_index_new();
+
+    cache = nmp_cache_new(multi_idx, use_udev);
+
+    /* if we have a link, and don't set is_in_netlink, adding it has no effect. */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    g_assert(NMP_OBJECT_UP_CAST(&objm1->object) == objm1);
+    g_assert(!nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UNCHANGED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    g_assert(!obj_old);
+    g_assert(!obj_new);
+    g_assert(!nmp_cache_lookup_obj(cache, objm1));
+    g_assert(!nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex)));
+    nmp_object_unref(objm1);
+
+    /* Only when setting @is_in_netlink the link is added. */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    objm1->_link.netlink.is_in_netlink = TRUE;
+    g_assert(nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_ADDED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    g_assert(!obj_old);
+    g_assert(obj_new);
+    g_assert(objm1 == obj_new);
+    g_assert(nmp_object_equal(objm1, obj_new));
+    g_assert(nmp_cache_lookup_obj(cache, objm1) == obj_new);
+    g_assert(nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+             == obj_new);
+    g_assert(nmp_object_is_visible(obj_new));
+    _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+    _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, TRUE);
+    nmp_object_unref(objm1);
+    nmp_object_unref(obj_new);
+
+    /* updating the same link with identical value, has no effect. */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    objm1->_link.netlink.is_in_netlink = TRUE;
+    g_assert(nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UNCHANGED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    g_assert(obj_old);
+    g_assert(obj_new);
+    g_assert(obj_new != objm1);
+    g_assert(nmp_object_equal(objm1, obj_new));
+    g_assert(nmp_cache_lookup_obj(cache, objm1) == obj_new);
+    g_assert(nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+             == obj_new);
+    nmp_object_unref(objm1);
+    nmp_object_unref(obj_new);
+    nmp_object_unref(obj_new);
+
+    /* remove the link from netlink */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    g_assert(!nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_REMOVED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    g_assert(obj_old);
+    g_assert(!obj_new);
+    g_assert(!nmp_cache_lookup_obj(cache, objm1));
+    g_assert(!nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex)));
+    nmp_object_unref(objm1);
+    nmp_object_unref(obj_old);
+    nmp_object_unref(obj_new);
+
+    if (udev_device_2) {
+        /* now add the link only with aspect UDEV. */
+        ops_type =
+            nmp_cache_update_link_udev(cache, pl_link_2.ifindex, udev_device_2, &obj_old, &obj_new);
+        nmtst_assert_nmp_cache_is_consistent(cache);
+        g_assert_cmpint(ops_type, ==, NMP_CACHE_OPS_ADDED);
+        g_assert(!obj_old);
+        g_assert(obj_new);
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+            == obj_new);
+        g_assert(!nmp_object_is_visible(obj_new));
+        _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, FALSE);
+        _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+        nmp_object_unref(obj_new);
+    }
+
+    /* add it in netlink too. */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    objm1->_link.netlink.is_in_netlink = TRUE;
+    g_assert(nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache,
+                              objm1,
+                              &obj_old,
+                              &obj_new,
+                              udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_ADDED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    if (udev_device_2) {
+        g_assert(obj_old);
+        g_assert(!nmp_object_is_visible(obj_old));
+    } else
+        g_assert(!obj_old);
+    g_assert(nmp_object_equal(objm1, obj_new));
+    g_assert(nmp_cache_lookup_obj(cache, objm1) == obj_new);
+    g_assert(nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+             == obj_new);
+    g_assert(nmp_object_is_visible(obj_new));
+    _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, TRUE);
+    _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+    nmp_object_unref(objm1);
+    nmp_object_unref(obj_old);
+    nmp_object_unref(obj_new);
+
+    /* remove again from netlink. */
+    objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_2);
+    objm1->_link.netlink.is_in_netlink = FALSE;
+    g_assert(!nmp_object_is_alive(objm1));
+    _nmp_cache_update_netlink(cache,
+                              objm1,
+                              &obj_old,
+                              &obj_new,
+                              udev_device_2 ? NMP_CACHE_OPS_UPDATED : NMP_CACHE_OPS_REMOVED);
+    nmtst_assert_nmp_cache_is_consistent(cache);
+    if (udev_device_2)
+        g_assert(obj_new == objm1);
+    else
+        g_assert(!obj_new);
+    g_assert(obj_old);
+    g_assert(nmp_object_is_alive(obj_old));
+    if (udev_device_2) {
+        g_assert(nmp_cache_lookup_obj(cache, objm1) == obj_new);
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+            == obj_new);
+        g_assert(!nmp_object_is_visible(obj_new));
+        _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, FALSE);
+        _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+    } else {
+        g_assert(nmp_cache_lookup_obj(cache, objm1) == NULL);
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_2.ifindex))
+            == NULL);
+        g_assert(!nmp_object_is_alive(obj_new));
+        g_assert(!nmp_object_is_visible(obj_new));
+    }
+    nmp_object_unref(objm1);
+    nmp_object_unref(obj_old);
+    nmp_object_unref(obj_new);
+
+    /* now another link only with aspect UDEV. */
+    if (udev_device_3) {
+        /* now add the link only with aspect UDEV. */
+        ops_type =
+            nmp_cache_update_link_udev(cache, pl_link_3.ifindex, udev_device_3, &obj_old, &obj_new);
+        g_assert_cmpint(ops_type, ==, NMP_CACHE_OPS_ADDED);
+        nmtst_assert_nmp_cache_is_consistent(cache);
+        g_assert(NMP_OBJECT_IS_VALID(obj_new));
+        g_assert(!obj_old);
+        g_assert(!nmp_object_is_visible(obj_new));
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_3.ifindex))
+            == obj_new);
+        _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, FALSE);
+        _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+        g_assert_cmpint(obj_new->_link.netlink.is_in_netlink, ==, FALSE);
+        g_assert_cmpint(obj_new->link.initialized, ==, FALSE);
+        nmp_object_unref(obj_new);
+
+        /* add it in netlink too. */
+        objm1 = nmp_object_new(NMP_OBJECT_TYPE_LINK, (NMPlatformObject *) &pl_link_3);
+        objm1->_link.netlink.is_in_netlink = TRUE;
+        g_assert(nmp_object_is_alive(objm1));
+        _nmp_cache_update_netlink(cache, objm1, &obj_old, &obj_new, NMP_CACHE_OPS_UPDATED);
+        nmtst_assert_nmp_cache_is_consistent(cache);
+        g_assert(obj_old);
+        g_assert(obj_new == objm1);
+        g_assert(nmp_object_equal(objm1, obj_new));
+        g_assert(!obj_old || !nmp_object_is_visible(obj_old));
+        g_assert(nmp_cache_lookup_obj(cache, objm1) == obj_new);
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_3.ifindex))
+            == obj_new);
+        g_assert(nmp_object_is_visible(obj_new));
+        _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, TRUE);
+        _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+        g_assert_cmpint(obj_new->_link.netlink.is_in_netlink, ==, TRUE);
+        g_assert_cmpint(obj_new->link.initialized, ==, TRUE);
+        nmp_object_unref(objm1);
+        nmp_object_unref(obj_old);
+        nmp_object_unref(obj_new);
+
+        /* remove UDEV. */
+        ops_type = nmp_cache_update_link_udev(cache, pl_link_3.ifindex, NULL, &obj_old, &obj_new);
+        g_assert_cmpint(ops_type, ==, NMP_CACHE_OPS_UPDATED);
+        nmtst_assert_nmp_cache_is_consistent(cache);
+        g_assert(obj_old && nmp_object_is_visible(obj_old));
+        g_assert(
+            nmp_cache_lookup_obj(cache, nmp_object_stackinit_id_link(&objs1, pl_link_3.ifindex))
+            == obj_new);
+        g_assert(nmp_object_is_visible(obj_new));
+        _assert_cache_multi_lookup_contains_link(cache, TRUE, obj_new, TRUE);
+        _assert_cache_multi_lookup_contains_link(cache, FALSE, obj_new, TRUE);
+        g_assert_cmpint(obj_new->_link.netlink.is_in_netlink, ==, TRUE);
+        g_assert_cmpint(obj_new->link.initialized, ==, !nmp_cache_use_udev_get(cache));
+        nmp_object_unref(obj_new);
+        nmp_object_unref(obj_old);
+    }
+
+    nmp_cache_free(cache);
+}
+
+const char noqueue[]  = "noqueue";
+const char fq_codel[] = "fq_codel";
+const char ingress[]  = "ingress";
+
+static const NMPlatformQdisc pl_qdisc_1a = {
+    .ifindex     = 1,
+    .kind        = noqueue,
+    .addr_family = AF_UNSPEC,
+    .handle      = 0,
+    .parent      = TC_H_ROOT,
+    .info        = 0,
+};
+
+static const NMPlatformQdisc pl_qdisc_1b = {
+    .ifindex     = 1,
+    .kind        = fq_codel,
+    .addr_family = AF_UNSPEC,
+    .handle      = 0,
+    .parent      = TC_H_ROOT,
+    .info        = 0,
+};
+
+static const NMPlatformQdisc pl_qdisc_1c = {
+    .ifindex     = 1,
+    .kind        = ingress,
+    .addr_family = AF_UNSPEC,
+    .handle      = TC_H_MAKE(TC_H_INGRESS, 0),
+    .parent      = TC_H_INGRESS,
+    .info        = 0,
+};
+
+static const NMPlatformQdisc pl_qdisc_2 = {
+    .ifindex     = 2,
+    .kind        = fq_codel,
+    .addr_family = AF_UNSPEC,
+    .handle      = 0,
+    .parent      = TC_H_ROOT,
+    .info        = 0,
+};
+
+static void
+test_cache_qdisc(void)
+{
+    NMPCache *                      cache;
+    nm_auto_unref_dedup_multi_index NMDedupMultiIndex *multi_idx = NULL;
+    NMPLookup                                          lookup;
+    const NMDedupMultiHeadEntry *                      head_entry;
+    nm_auto_nmpobj NMPObject *obj1a =
+        nmp_object_new(NMP_OBJECT_TYPE_QDISC, (NMPlatformObject *) &pl_qdisc_1a);
+    nm_auto_nmpobj NMPObject *obj1b =
+        nmp_object_new(NMP_OBJECT_TYPE_QDISC, (NMPlatformObject *) &pl_qdisc_1b);
+    nm_auto_nmpobj NMPObject *obj1c =
+        nmp_object_new(NMP_OBJECT_TYPE_QDISC, (NMPlatformObject *) &pl_qdisc_1c);
+    nm_auto_nmpobj NMPObject *obj2 =
+        nmp_object_new(NMP_OBJECT_TYPE_QDISC, (NMPlatformObject *) &pl_qdisc_2);
+
+    multi_idx = nm_dedup_multi_index_new();
+    cache     = nmp_cache_new(multi_idx, nmtst_get_rand_uint32() % 2);
+
+    g_assert(nmp_cache_lookup_obj(cache, obj1a) == NULL);
+
+    g_assert(nmp_cache_update_netlink(cache, obj1a, FALSE, NULL, NULL) == NMP_CACHE_OPS_ADDED);
+    g_assert(nmp_cache_lookup_obj(cache, obj1a) == obj1a);
+    g_assert(nmp_cache_lookup_obj(cache, obj1b) == obj1a);
+    g_assert(nmp_cache_lookup_obj(cache, obj2) == NULL);
+
+    g_assert(nmp_cache_update_netlink(cache, obj1b, FALSE, NULL, NULL) == NMP_CACHE_OPS_UPDATED);
+    g_assert(nmp_cache_lookup_obj(cache, obj1a) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj1b) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj2) == NULL);
+
+    g_assert(nmp_cache_update_netlink(cache, obj1c, FALSE, NULL, NULL) == NMP_CACHE_OPS_ADDED);
+    g_assert(nmp_cache_lookup_obj(cache, obj1a) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj1b) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj1c) == obj1c);
+    g_assert(nmp_cache_lookup_obj(cache, obj2) == NULL);
+
+    g_assert(nmp_cache_update_netlink(cache, obj2, FALSE, NULL, NULL) == NMP_CACHE_OPS_ADDED);
+    g_assert(nmp_cache_lookup_obj(cache, obj1a) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj1b) == obj1b);
+    g_assert(nmp_cache_lookup_obj(cache, obj2) == obj2);
+
+    head_entry = nmp_cache_lookup(cache, nmp_lookup_init_object(&lookup, NMP_OBJECT_TYPE_QDISC, 1));
+    g_assert(head_entry->len == 2);
+
+    nmp_cache_free(cache);
+}
+
+/*****************************************************************************/
+
+NMTST_DEFINE();
+
+int
+main(int argc, char **argv)
+{
+    int           result;
+    NMUdevClient *udev_client;
+
+    nmtst_init_assert_logging(&argc, &argv, "INFO", "DEFAULT");
+
+    udev_client = nm_udev_client_new(NM_MAKE_STRV("net"), NULL, NULL);
+    {
+        struct udev_enumerate * enumerator;
+        struct udev_list_entry *devices, *l;
+
+        enumerator = nm_udev_client_enumerate_new(udev_client);
+
+        /* Demand that the device is initialized (udev rules ran,
+         * device has a stable name now) in case udev is running
+         * (not in a container). */
+        if (access("/sys", W_OK) == 0)
+            udev_enumerate_add_match_is_initialized(enumerator);
+
+        udev_enumerate_scan_devices(enumerator);
+
+        devices = udev_enumerate_get_list_entry(enumerator);
+        for (l = devices; l != NULL; l = udev_list_entry_get_next(l)) {
+            struct udev_device *udevice;
+
+            udevice = udev_device_new_from_syspath(udev_enumerate_get_udev(enumerator),
+                                                   udev_list_entry_get_name(l));
+            if (udevice == NULL)
+                continue;
+
+            global.udev_devices = g_list_prepend(global.udev_devices, udevice);
+        }
+        global.udev_devices = g_list_reverse(global.udev_devices);
+
+        udev_enumerate_unref(enumerator);
+    }
+
+    g_test_add_func("/nmp-object/obj-base", test_obj_base);
+    g_test_add_func("/nmp-object/cache_link", test_cache_link);
+    g_test_add_func("/nmp-object/cache_qdisc", test_cache_qdisc);
+
+    result = g_test_run();
+
+    while (global.udev_devices) {
+        udev_device_unref(global.udev_devices->data);
+        global.udev_devices = g_list_delete_link(global.udev_devices, global.udev_devices);
+    }
+
+    nm_udev_client_destroy(udev_client);
+
+    return result;
+}