[v2,2/2] test: pipeline: Add a duplicate camera construction test
diff mbox series

Message ID 20261028-kbingham-duplicate-id-v2-2-e82448998d88@ideasonboard.com
State New
Headers show
Series
  • libcamera: camera_manager: Non-fatal duplicate id detection
Related show

Commit Message

Kieran Bingham Oct. 28, 2026, 12:02 p.m. UTC
Prepare a test pipeline handler which will allow us to construct
custom cameras.

Use this pipeline handler to specifically prepare an environment to be
able to construct multiple cameras with the same ID to ensure we do not
have any system regressions from this in the future.

Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
---
 test/meson.build                      |   1 +
 test/pipeline/duplicate_camera_id.cpp | 202 ++++++++++++++++++++++++++++++++++
 test/pipeline/meson.build             |  14 +++
 3 files changed, 217 insertions(+)

Patch
diff mbox series

diff --git a/test/meson.build b/test/meson.build
index 363a2829d895..62f8c7da8b30 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -32,6 +32,7 @@  subdir('ipa')
 subdir('ipc')
 subdir('log')
 subdir('media_device')
+subdir('pipeline')
 subdir('process')
 subdir('py')
 subdir('serialization')
diff --git a/test/pipeline/duplicate_camera_id.cpp b/test/pipeline/duplicate_camera_id.cpp
new file mode 100644
index 000000000000..d91cb02f53e6
--- /dev/null
+++ b/test/pipeline/duplicate_camera_id.cpp
@@ -0,0 +1,202 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026 Ideas on Board Oy.
+ */
+
+#include <iostream>
+#include <stdlib.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/property_ids.h>
+
+#include "libcamera/internal/camera.h"
+#include "libcamera/internal/pipeline_handler.h"
+
+#include "test.h"
+
+using namespace libcamera;
+
+namespace {
+
+/*
+ * There's no easy way to communicate to our custom test pipeline
+ * handler. Use a global here to state and track how many cameras should
+ * be constructed.
+ */
+struct MatchState {
+	unsigned int requested;
+	unsigned int created;
+} matchState;
+
+std::string cameraId = "NotVeryUniqueCameraId";
+
+} /* namespace */
+
+namespace libcamera {
+
+class TestCameraData : public Camera::Private
+{
+public:
+	TestCameraData(PipelineHandler *pipe)
+		: Camera::Private(pipe)
+	{
+	}
+};
+
+class PipelineHandlerTest : public PipelineHandler
+{
+public:
+	PipelineHandlerTest(CameraManager *manager)
+		: PipelineHandler(manager)
+	{
+		(void)manager;
+	}
+
+	std::unique_ptr<CameraConfiguration>
+	generateConfiguration(Camera *camera,
+			      std::span<const StreamRole> roles) override
+	{
+		(void)camera;
+		(void)roles;
+		return {};
+	}
+
+	int configure(Camera *camera, CameraConfiguration *config) override
+	{
+		(void)camera;
+		(void)config;
+		return true;
+	}
+
+	int exportFrameBuffers(Camera *camera, Stream *stream,
+			       std::vector<std::unique_ptr<FrameBuffer>> *buffers) override
+	{
+		(void)camera;
+		(void)stream;
+		(void)buffers;
+		return true;
+	}
+
+	int start(Camera *camera, const ControlList *controls) override
+	{
+		(void)camera;
+		(void)controls;
+		return false;
+	}
+
+	void stopDevice(Camera *camera) override
+	{
+		(void)camera;
+	}
+
+	int queueRequestDevice(Camera *camera, Request *request) override
+	{
+		(void)camera;
+		(void)request;
+		return false;
+	}
+
+	bool match(DeviceEnumerator *enumerator) override
+	{
+		(void)enumerator;
+
+		/* Only construct requested number of pipelines */
+		if (matchState.created >= matchState.requested)
+			return false;
+
+		std::unique_ptr<TestCameraData> data = std::make_unique<TestCameraData>(this);
+
+		std::set<Stream *> streams; /* No streams yet on this camera... */
+		std::shared_ptr<Camera> camera = Camera::create(std::move(data), cameraId, streams);
+
+		registerCamera(std::move(camera));
+
+		matchState.created++;
+
+		return true;
+	}
+};
+
+REGISTER_PIPELINE_HANDLER(PipelineHandlerTest, "test")
+
+} /* namespace libcamera */
+
+namespace {
+
+class CameraIdTest : public Test
+{
+protected:
+	int init() override
+	{
+		/* Make sure CameraManager only uses our custom test pipeline */
+		if (setenv("LIBCAMERA_PIPELINES_MATCH_LIST", "test", 1) != 0) {
+			std::cerr << "Failed to set pipeline match list" << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	int cameraConstructionTest()
+	{
+		libcamera::CameraManager cm;
+
+		cm.start();
+
+		/*
+		 * Even if we try to create multiple cameras, we always expect
+		 * only one to succeed.
+		 */
+		const std::vector<std::shared_ptr<Camera>> &cameras = cm.cameras();
+		if (cameras.size() != 1) {
+			std::cerr
+				<< "Expected 1 camera, got "
+				<< cameras.size() << std::endl;
+			return TestFail;
+		}
+
+		/*
+		 * I can't see how it could ever be anything but this here, but
+		 * let's check anyway as the intent of the test is that we get
+		 * only a single camera with the expected ID.
+		 */
+		const std::string &id = cameras[0]->id();
+		if (id != cameraId) {
+			std::cerr
+				<< "Expected '" << cameraId
+				<< "', got '" << id << "'" << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	int run() override
+	{
+		int ret;
+
+		/* Validate a single camera construction */
+		matchState.created = 0;
+		matchState.requested = 1;
+
+		ret = cameraConstructionTest();
+		if (ret != TestPass) {
+			return ret;
+		}
+
+		/* Validate multiple camera construction */
+		matchState.created = 0;
+		matchState.requested = 3;
+
+		ret = cameraConstructionTest();
+		if (ret != TestPass) {
+			return ret;
+		}
+
+		return TestPass;
+	}
+};
+
+} /* namespace */
+
+TEST_REGISTER(CameraIdTest)
diff --git a/test/pipeline/meson.build b/test/pipeline/meson.build
new file mode 100644
index 000000000000..c1f4ee969202
--- /dev/null
+++ b/test/pipeline/meson.build
@@ -0,0 +1,14 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+pipeline_tests = [
+    {'name': 'duplicate_camera_id', 'sources': ['duplicate_camera_id.cpp']},
+]
+
+foreach test : pipeline_tests
+    exe = executable(test['name'], test['sources'],
+                     dependencies : libcamera_private,
+                     link_with : test_libraries,
+                     include_directories : test_includes_internal)
+
+    test(test['name'], exe, suite : 'pipeline', is_parallel : true)
+endforeach