summaryrefslogtreecommitdiff
path: root/test/test_driver_model.cc
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_driver_model.cc')
-rw-r--r--test/test_driver_model.cc209
1 files changed, 209 insertions, 0 deletions
diff --git a/test/test_driver_model.cc b/test/test_driver_model.cc
new file mode 100644
index 0000000..8f105b3
--- /dev/null
+++ b/test/test_driver_model.cc
@@ -0,0 +1,209 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_driver_model — exercises kref, kobject, and the bus/device/driver
+ * core. Uses a dummy bus + dummy driver + dummy device so the test is
+ * self-contained and doesn't depend on FDT/platform_device.
+ */
+
+#include <lume/selftest.h>
+#include <lume/kref.h>
+#include <lume/kobject.h>
+#include <lume/device.h>
+
+/* ------------------------------------------------------------------ */
+/* Test 1: kref get/put + release fires exactly once */
+/* ------------------------------------------------------------------ */
+
+namespace {
+
+struct krefable {
+ kref ref;
+ int release_count;
+};
+
+void krefable_release(kref* r)
+{
+ auto* k = container_of(r, krefable, ref);
+ k->release_count++;
+}
+
+void test_kref()
+{
+ st_begin("kref: init/get/put");
+
+ krefable k = {};
+ kref_init(&k.ref);
+
+ ST_ASSERT_EQ(kref_read(&k.ref), 1);
+
+ kref_get(&k.ref);
+ kref_get(&k.ref);
+ ST_ASSERT_EQ(kref_read(&k.ref), 3);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(k.release_count, 0);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 1);
+ ST_ASSERT_EQ(k.release_count, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 2: kobject parent/child + release on final put */
+/* ------------------------------------------------------------------ */
+
+int g_child_released = 0;
+int g_parent_released = 0;
+
+void child_release(kobject*) { g_child_released++; }
+void parent_release(kobject*) { g_parent_released++; }
+
+const kobj_type child_ktype = { .release = child_release };
+const kobj_type parent_ktype = { .release = parent_release };
+
+void test_kobject_hierarchy()
+{
+ st_begin("kobject: parent/child + ref counting");
+
+ g_child_released = 0;
+ g_parent_released = 0;
+
+ kobject parent;
+ kobject_init(&parent, &parent_ktype);
+
+ kobject child;
+ kobject_init(&child, &child_ktype);
+ kobject_add(&child, &parent, "child");
+
+ /* Adding child took a ref on parent. */
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 2);
+ ST_ASSERT(child.parent == &parent);
+
+ /* Drop child's "added" reference — release should NOT fire because
+ * we (the test) still hold the original kobject_init ref. */
+ kobject_del(&child);
+ ST_ASSERT(child.parent == nullptr);
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 1);
+ ST_ASSERT_EQ(g_child_released, 0);
+ ST_ASSERT_EQ(g_parent_released, 0);
+
+ /* Drop final refs. */
+ kobject_put(&child);
+ ST_ASSERT_EQ(g_child_released, 1);
+
+ kobject_put(&parent);
+ ST_ASSERT_EQ(g_parent_released, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 3: bus / device / driver bind via match + probe */
+/* ------------------------------------------------------------------ */
+
+struct dummy_dev { device dev; int probed; };
+struct dummy_drv { device_driver drv; const char* want; };
+
+int g_probes = 0;
+
+int dummy_match(device* dev, device_driver* drv)
+{
+ auto* dd = container_of(drv, dummy_drv, drv);
+ return (dev->init_name != nullptr &&
+ dd->want != nullptr &&
+ __builtin_strcmp(dev->init_name, dd->want) == 0) ? 1 : 0;
+}
+
+int dummy_probe(device* dev)
+{
+ auto* dd = container_of(dev, dummy_dev, dev);
+ dd->probed++;
+ g_probes++;
+ return 0;
+}
+
+bus_type dummy_bus = {
+ .name = "test_dummy",
+ .match = dummy_match,
+ .probe = nullptr, /* fall through to drv->probe */
+ .devices = {},
+ .drivers = {},
+ .lock = {},
+ .registered = false,
+};
+
+void test_bus_bind()
+{
+ st_begin("driver model: bus / driver / device bind");
+
+ g_probes = 0;
+ ST_ASSERT_EQ(bus_register(&dummy_bus), 0);
+
+ /* Register driver before any matching device exists. */
+ dummy_drv drv_a = {};
+ drv_a.drv.name = "drv_a";
+ drv_a.drv.bus = &dummy_bus;
+ drv_a.drv.probe = dummy_probe;
+ drv_a.want = "compat_a";
+ ST_ASSERT_EQ(driver_register(&drv_a.drv), 0);
+
+ /* Register a device that should bind to drv_a immediately. */
+ dummy_dev dev_a = {};
+ dev_a.dev.bus = &dummy_bus;
+ dev_a.dev.init_name = "compat_a";
+ ST_ASSERT_EQ(device_register(&dev_a.dev), 0);
+ ST_ASSERT_EQ(dev_a.probed, 1);
+ ST_ASSERT(dev_a.dev.driver == &drv_a.drv);
+
+ /* Register a device first, then a driver — should also bind. */
+ dummy_dev dev_b = {};
+ dev_b.dev.bus = &dummy_bus;
+ dev_b.dev.init_name = "compat_b";
+ ST_ASSERT_EQ(device_register(&dev_b.dev), 0);
+ ST_ASSERT_EQ(dev_b.probed, 0);
+
+ dummy_drv drv_b = {};
+ drv_b.drv.name = "drv_b";
+ drv_b.drv.bus = &dummy_bus;
+ drv_b.drv.probe = dummy_probe;
+ drv_b.want = "compat_b";
+ ST_ASSERT_EQ(driver_register(&drv_b.drv), 0);
+ ST_ASSERT_EQ(dev_b.probed, 1);
+ ST_ASSERT(dev_b.dev.driver == &drv_b.drv);
+
+ /* Non-matching device sits unbound. */
+ dummy_dev dev_c = {};
+ dev_c.dev.bus = &dummy_bus;
+ dev_c.dev.init_name = "compat_unknown";
+ ST_ASSERT_EQ(device_register(&dev_c.dev), 0);
+ ST_ASSERT_EQ(dev_c.probed, 0);
+ ST_ASSERT(dev_c.dev.driver == nullptr);
+
+ ST_ASSERT_EQ(g_probes, 2);
+
+ /* Cleanup. */
+ device_unregister(&dev_a.dev);
+ device_unregister(&dev_b.dev);
+ device_unregister(&dev_c.dev);
+ driver_unregister(&drv_a.drv);
+ driver_unregister(&drv_b.drv);
+
+ st_pass();
+}
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* Entry */
+/* ------------------------------------------------------------------ */
+
+void selftest_driver_model()
+{
+ test_kref();
+ test_kobject_hierarchy();
+ test_bus_bind();
+}