[v3,21/37] test: Add ValueNode unit test
diff mbox series

Message ID 20260423230059.3180987-22-laurent.pinchart@ideasonboard.com
State New
Headers show
Series
  • libcamera: Global configuration file improvements
Related show

Commit Message

Laurent Pinchart April 23, 2026, 11 p.m. UTC
Add a unit test for the ValueNode class. The tests focus on the class
itself, without considering that is currently only used when parsing
YAML files. This duplicates some of the tests of the YamlParser class,
which will be dropped from the corresponding unit test in a subsequent
change.

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Isaac Scott <isaac.scott@ideasonboard.com>
---
Changes since v2:

- Add commit message
- Drop 'overloaded' definition
---
 test/meson.build    |   1 +
 test/value-node.cpp | 558 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 559 insertions(+)
 create mode 100644 test/value-node.cpp

Patch
diff mbox series

diff --git a/test/meson.build b/test/meson.build
index 52f04364e4fc..e4450625ee4c 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -74,6 +74,7 @@  internal_tests = [
     {'name': 'timer-thread', 'sources': ['timer-thread.cpp']},
     {'name': 'unique-fd', 'sources': ['unique-fd.cpp']},
     {'name': 'utils', 'sources': ['utils.cpp']},
+    {'name': 'value-node', 'sources': ['value-node.cpp']},
     {'name': 'vector', 'sources': ['vector.cpp']},
     {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']},
 ]
diff --git a/test/value-node.cpp b/test/value-node.cpp
new file mode 100644
index 000000000000..3b6466e75b13
--- /dev/null
+++ b/test/value-node.cpp
@@ -0,0 +1,558 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026, Ideas on Board
+ *
+ * ValueNode tests
+ */
+
+#include <array>
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <set>
+#include <string>
+#include <string_view>
+#include <variant>
+
+#include <libcamera/base/utils.h>
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/value_node.h"
+
+#include "test.h"
+
+using namespace libcamera;
+using namespace std;
+
+class ValueNodeTest : public Test
+{
+protected:
+	enum class NodeType {
+		Empty,
+		Value,
+		List,
+		Dictionary,
+	};
+
+	enum class ValueType {
+		Int8,
+		UInt8,
+		Int16,
+		UInt16,
+		Int32,
+		UInt32,
+		Float,
+		Double,
+		String,
+		Size,
+	};
+
+	int testNodeValueType(const ValueNode &node, std::string_view name, ValueType type)
+	{
+		bool isInteger8 = type == ValueType::Int8 || type == ValueType::UInt8;
+		bool isInteger16 = type == ValueType::Int16 || type == ValueType::UInt16;
+		bool isInteger32 = type == ValueType::Int32 || type == ValueType::UInt32;
+		bool isIntegerUpTo16 = isInteger8 || isInteger16;
+		bool isIntegerUpTo32 = isIntegerUpTo16 || isInteger32;
+		bool isSigned = type == ValueType::Int8 || type == ValueType::Int16 ||
+				type == ValueType::Int32;
+
+		if (!isInteger8 && node.get<int8_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "int8_t" << std::endl;
+			return TestFail;
+		}
+
+		if ((!isInteger8 || isSigned) && node.get<uint8_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "uint8_t" << std::endl;
+			return TestFail;
+		}
+
+		if (!isIntegerUpTo16 && node.get<int16_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "int16_t" << std::endl;
+			return TestFail;
+		}
+
+		if ((!isIntegerUpTo16 || isSigned) && node.get<uint16_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "uint16_t" << std::endl;
+			return TestFail;
+		}
+
+		if (!isIntegerUpTo32 && node.get<int32_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "int32_t" << std::endl;
+			return TestFail;
+		}
+
+		if ((!isIntegerUpTo32 || isSigned) && node.get<uint32_t>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "uint32_t" << std::endl;
+			return TestFail;
+		}
+
+		if (!isIntegerUpTo32 && type != ValueType::Float &&
+		    type != ValueType::Double && node.get<double>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "double" << std::endl;
+			return TestFail;
+		}
+
+		if (type != ValueType::Size && node.get<Size>()) {
+			std::cerr
+				<< "Node " << name << " didn't fail to parse as "
+				<< "Size" << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	int testIntegerValue(const ValueNode &node, std::string_view name,
+			     ValueType type, int64_t value)
+	{
+		uint64_t unsignedValue = static_cast<uint64_t>(value);
+		std::string strValue = std::to_string(value);
+		bool isSigned = type == ValueType::Int8 || type == ValueType::Int16 ||
+				type == ValueType::Int32;
+		bool isInteger8 = type == ValueType::Int8 || type == ValueType::UInt8;
+		bool isInteger16 = type == ValueType::Int16 || type == ValueType::UInt16;
+
+		/* All integers can be accessed as strings and double. */
+
+		if (node.get<string>().value_or("") != strValue ||
+		    node.get<string>("") != strValue) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "string" << std::endl;
+			return TestFail;
+		}
+
+		if (node.get<double>().value_or(0.0) != value ||
+		    node.get<double>(0.0) != value) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "double" << std::endl;
+			return TestFail;
+		}
+
+		if (isInteger8) {
+			if (node.get<int8_t>().value_or(0) != value ||
+			    node.get<int8_t>(0) != value) {
+				std::cerr
+					<< "Node " << name << " failed to parse as "
+					<< "int8_t" << std::endl;
+				return TestFail;
+			}
+		}
+
+		if (isInteger8 && !isSigned) {
+			if (node.get<uint8_t>().value_or(0) != unsignedValue ||
+			    node.get<uint8_t>(0) != unsignedValue) {
+				std::cerr
+					<< "Node " << name << " failed to parse as "
+					<< "uint8_t" << std::endl;
+				return TestFail;
+			}
+		}
+
+		if (isInteger8 || isInteger16) {
+			if (node.get<int16_t>().value_or(0) != value ||
+			    node.get<int16_t>(0) != value) {
+				std::cerr
+					<< "Node " << name << " failed to parse as "
+					<< "int16_t" << std::endl;
+				return TestFail;
+			}
+		}
+
+		if ((isInteger8 || isInteger16) && !isSigned) {
+			if (node.get<uint16_t>().value_or(0) != unsignedValue ||
+			    node.get<uint16_t>(0) != unsignedValue) {
+				std::cerr
+					<< "Node " << name << " failed to parse as "
+					<< "uint16_t" << std::endl;
+				return TestFail;
+			}
+		}
+
+		if (node.get<int32_t>().value_or(0) != value ||
+		    node.get<int32_t>(0) != value) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "int32_t" << std::endl;
+			return TestFail;
+		}
+
+		if (!isSigned) {
+			if (node.get<uint32_t>().value_or(0) != unsignedValue ||
+			    node.get<uint32_t>(0) != unsignedValue) {
+				std::cerr
+					<< "Node " << name << " failed to parse as "
+					<< "uint32_t" << std::endl;
+				return TestFail;
+			}
+		}
+
+		return TestPass;
+	}
+
+	template<typename T>
+	bool equal(const ValueNode &node, T value)
+	{
+		constexpr T eps = std::numeric_limits<T>::epsilon();
+
+		if (std::abs(node.get<T>().value_or(0.0) - value) >= eps)
+			return false;
+		if (std::abs(node.get<T>(0.0) - value) >= eps)
+			return false;
+		return true;
+	}
+
+	int testFloatValue(const ValueNode &node, std::string_view name, double value)
+	{
+		std::string strValue = std::to_string(value);
+
+		if (node.get<string>().value_or("") != strValue ||
+		    node.get<string>("") != strValue) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "string" << std::endl;
+			return TestFail;
+		}
+
+		if (!equal<float>(node, value)) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "float" << std::endl;
+			return TestFail;
+		}
+
+		if (!equal<double>(node, value)) {
+			std::cerr
+				<< "Node " << name << " failed to parse as "
+				<< "double" << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	bool testNodeType(const ValueNode &node, NodeType nodeType)
+	{
+		using NodeFunc = bool (ValueNode::*)() const;
+		using NodeDesc = std::tuple<NodeType, std::string_view, NodeFunc>;
+
+		static constexpr std::array<NodeDesc, 4> nodeTypes = { {
+			NodeDesc{ NodeType::Empty, "empty", &ValueNode::isEmpty },
+			NodeDesc{ NodeType::Value, "value", &ValueNode::isValue },
+			NodeDesc{ NodeType::List, "list", &ValueNode::isList },
+			NodeDesc{ NodeType::Dictionary, "dictionary", &ValueNode::isDictionary },
+		} };
+
+		for (const auto &[type, name, func] : nodeTypes) {
+			bool value = type == nodeType;
+			if ((node.*func)() != value) {
+				std::cerr
+					<< "Empty ValueNode should "
+					<< (value ? "" : "not ") << "be a "
+					<< name << std::endl;
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	int run()
+	{
+		/* Tests on empty nodes. */
+		ValueNode emptyNode;
+
+		if (!testNodeType(emptyNode, NodeType::Empty)) {
+			std::cerr
+				<< "Empty node should have empty type"
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (static_cast<bool>(emptyNode)) {
+			std::cerr
+				<< "Empty node should cast to false"
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (emptyNode.size()) {
+			std::cerr
+				<< "Empty node should have zero size"
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (emptyNode.get<std::string>()) {
+			std::cerr
+				<< "Empty node should have no value"
+				<< std::endl;
+			return TestFail;
+		}
+
+		/* Tests on list nodes. */
+		ValueNode listNode;
+
+		static constexpr std::array<std::string_view, 3> listElemNames = {
+			"libcamera", "linux", "isp"
+		};
+
+		for (const auto &name : listElemNames)
+			listNode.add(std::make_unique<ValueNode>(std::string{ name }));
+
+		if (!testNodeType(listNode, NodeType::List))
+			return TestFail;
+
+		if (!static_cast<bool>(listNode)) {
+			std::cerr
+				<< "List node should cast to true"
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (listNode.size() != 3) {
+			std::cerr << "Invalid list node size" << std::endl;
+			return TestFail;
+		}
+
+		listNode.set("value"s);
+		if (listNode.get<std::string>()) {
+			std::cerr
+				<< "Setting a value on a list node should fail"
+				<< std::endl;
+			return TestFail;
+		}
+
+		std::set<std::string_view> names{
+			listElemNames.begin(), listElemNames.end()
+		};
+
+		for (const auto &child : listNode.asList()) {
+			const std::string childName = child.get<std::string>("");
+
+			if (!names.erase(childName)) {
+				std::cerr
+					<< "Invalid list child '" << childName
+					<< "'" << std::endl;
+				return TestFail;
+			}
+		}
+
+		if (!names.empty()) {
+			std::cerr
+				<< "Missing elements in list: "
+				<< utils::join(names, ", ") << std::endl;
+			return TestFail;
+		}
+
+		/* Tests on dictionary nodes. */
+		ValueNode dictNode;
+
+		static const std::array<std::pair<std::string, int>, 3> dictElemKeyValues = { {
+			{ "a", 1 },
+			{ "b", 2 },
+			{ "c", 3 },
+		} };
+
+		for (const auto &[key, value] : dictElemKeyValues)
+			dictNode.add(key, std::make_unique<ValueNode>(value));
+
+		if (!testNodeType(dictNode, NodeType::Dictionary))
+			return TestFail;
+
+		if (!static_cast<bool>(dictNode)) {
+			std::cerr
+				<< "Dictionary node should cast to true"
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (dictNode.size() != 3) {
+			std::cerr << "Invalid dictionary node size" << std::endl;
+			return TestFail;
+		}
+
+		dictNode.set("value"s);
+		if (dictNode.get<std::string>()) {
+			std::cerr
+				<< "Setting a value on a dict node should fail"
+				<< std::endl;
+			return TestFail;
+		}
+
+		std::map<std::string, int> keyValues{
+			dictElemKeyValues.begin(), dictElemKeyValues.end()
+		};
+
+		for (const auto &[key, child] : dictNode.asDict()) {
+			auto iter = keyValues.find(key);
+			if (iter == keyValues.end()) {
+				std::cerr
+					<< "Invalid dictionary key '" << key
+					<< "'" << std::endl;
+				return TestFail;
+			}
+
+			const int value = child.get<int>(0);
+			if (value != iter->second) {
+				std::cerr
+					<< "Invalid dictionary value " << value
+					<< " for key '" << key << "'" << std::endl;
+				return TestFail;
+			}
+
+			if (dictNode[key].get<int>(0) != value) {
+				std::cerr
+					<< "Dictionary lookup failed for key '"
+					<< key << "'" << std::endl;
+				return TestFail;
+			}
+
+			keyValues.erase(iter);
+		}
+
+		if (!keyValues.empty()) {
+			std::cerr
+				<< "Missing elements in dictionary: "
+				<< utils::join(utils::map_keys(keyValues), ", ")
+				<< std::endl;
+			return TestFail;
+		}
+
+		if (!dictNode["nonexistent"].isEmpty()) {
+			std::cerr
+				<< "Accessing nonexistent dictionary element returns non-empty node"
+				<< std::endl;
+			return TestFail;
+		}
+
+		/* Make sure utils::map_keys() works on the adapter. */
+		(void)utils::map_keys(dictNode.asDict());
+
+		/* Tests on value nodes. */
+		ValueNode values;
+
+		values.add("int8_t", std::make_unique<ValueNode>(static_cast<int8_t>(-100)));
+		values.add("uint8_t", std::make_unique<ValueNode>(static_cast<uint8_t>(100)));
+		values.add("int16_t", std::make_unique<ValueNode>(static_cast<int16_t>(-1000)));
+		values.add("uint16_t", std::make_unique<ValueNode>(static_cast<uint16_t>(1000)));
+		values.add("int32_t", std::make_unique<ValueNode>(static_cast<int32_t>(-100000)));
+		values.add("uint32_t", std::make_unique<ValueNode>(static_cast<uint32_t>(100000)));
+		values.add("float", std::make_unique<ValueNode>(3.14159f));
+		values.add("double", std::make_unique<ValueNode>(3.14159));
+		values.add("string", std::make_unique<ValueNode>("libcamera"s));
+
+		std::unique_ptr<ValueNode> sizeNode = std::make_unique<ValueNode>();
+		sizeNode->add(std::make_unique<ValueNode>(640));
+		sizeNode->add(std::make_unique<ValueNode>(480));
+
+		values.add("size", std::move(sizeNode));
+
+		using ValueVariant = std::variant<int64_t, double, Size, std::string>;
+
+		static const
+		std::array<std::tuple<std::string_view, ValueType, ValueVariant>, 10> nodesValues{ {
+			{ "int8_t", ValueType::Int8, static_cast<int64_t>(-100) },
+			{ "uint8_t", ValueType::UInt8, static_cast<int64_t>(100) },
+			{ "int16_t", ValueType::Int16, static_cast<int64_t>(-1000) },
+			{ "uint16_t", ValueType::UInt16, static_cast<int64_t>(1000) },
+			{ "int32_t", ValueType::Int32, static_cast<int64_t>(-100000) },
+			{ "uint32_t", ValueType::UInt32, static_cast<int64_t>(100000) },
+			{ "float", ValueType::Float, 3.14159 },
+			{ "double", ValueType::Double, 3.14159 },
+			{ "string", ValueType::String, "libcamera" },
+			{ "size", ValueType::Size, Size{ 640, 480 } },
+		} };
+
+		for (const auto &nodeValue : nodesValues) {
+			/*
+			 * P0588R1 (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html)
+			 * explicitly forbids a lambda from capturing structured
+			 * bindings. This was fixed in a later release of the
+			 * C++ specification, but some compilers (including
+			 * clang-14 used in CI) choke on it. We can't use
+			 * structured bindings in the for loop, unpack the tuple
+			 * manually instead.
+			 */
+			const auto &name = std::get<0>(nodeValue);
+			const auto &type = std::get<1>(nodeValue);
+			const auto &value = std::get<2>(nodeValue);
+
+			const ValueNode &node = values[name];
+
+			if (testNodeValueType(node, name, type) != TestPass)
+				return TestFail;
+
+			int ret = std::visit(utils::overloaded{
+				[&](int64_t arg) -> int {
+					return testIntegerValue(node, name, type, arg);
+				},
+
+				[&](double arg) -> int {
+					return testFloatValue(node, name, arg);
+				},
+
+				[&](const Size &arg) -> int {
+					if (node.get<Size>().value_or(Size{}) != arg ||
+					    node.get<Size>(Size{}) != arg) {
+						std::cerr
+							<< "Invalid node size value"
+							<< std::endl;
+						return TestFail;
+					}
+
+					return TestPass;
+				},
+
+				[&](const std::string &arg) -> int {
+					if (node.get<std::string>().value_or(std::string{}) != arg ||
+					    node.get<std::string>(std::string{}) != arg) {
+						std::cerr
+							<< "Invalid node string value"
+							<< std::endl;
+						return TestFail;
+					}
+
+					return TestPass;
+				},
+			}, value);
+
+			if (ret != TestPass)
+				return ret;
+		}
+
+		/* Test erasure. */
+		values.erase("float");
+		if (values.contains("float")) {
+			std::cerr << "Failed to erase child node" << std::endl;
+			return TestFail;
+		}
+
+		values.add({ "a", "b", "c" }, std::make_unique<ValueNode>(0));
+		values.erase({ "a", "b" });
+		if (values["a"].contains("b")) {
+			std::cerr << "Failed to erase descendant node" << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(ValueNodeTest)