From patchwork Thu Apr 23 23:00:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Laurent Pinchart X-Patchwork-Id: 26543 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id AE8ADBDCB5 for ; Thu, 23 Apr 2026 23:01:46 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 1D05E62F75; Fri, 24 Apr 2026 01:01:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="GzkPvzSB"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 8C37D62F7B for ; Fri, 24 Apr 2026 01:01:30 +0200 (CEST) Received: from killaraus.ideasonboard.com (2001-14ba-703d-e500--2a1.rev.dnainternet.fi [IPv6:2001:14ba:703d:e500::2a1]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 09D48802 for ; Fri, 24 Apr 2026 00:59:50 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1776985191; bh=hvL4+04xk3K2ZACY0uey9xClIel8AEr7z8Z9nj0qExk=; h=From:To:Subject:Date:In-Reply-To:References:From; b=GzkPvzSBaRMygMTT/5D4zYAlGVxyR4XqgScrvwuM5SvR3ELIYO5Yen/zTLnW5/VEx r7/bhB2ZIb4ZWnNLUdhvavTyZlRw+0nAT8yN9El6aQEbIyUacDxZfyxBmB4nG0+C2x G1nkqbIwbl2odeuBJCGFpcxCPV7S4doqz4IMy3LY= From: Laurent Pinchart To: libcamera-devel@lists.libcamera.org Subject: [PATCH v3 21/37] test: Add ValueNode unit test Date: Fri, 24 Apr 2026 02:00:43 +0300 Message-ID: <20260423230059.3180987-22-laurent.pinchart@ideasonboard.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260423230059.3180987-1-laurent.pinchart@ideasonboard.com> References: <20260423230059.3180987-1-laurent.pinchart@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" 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 Reviewed-by: Isaac Scott --- 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 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "int8_t" << std::endl; + return TestFail; + } + + if ((!isInteger8 || isSigned) && node.get()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "uint8_t" << std::endl; + return TestFail; + } + + if (!isIntegerUpTo16 && node.get()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "int16_t" << std::endl; + return TestFail; + } + + if ((!isIntegerUpTo16 || isSigned) && node.get()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "uint16_t" << std::endl; + return TestFail; + } + + if (!isIntegerUpTo32 && node.get()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "int32_t" << std::endl; + return TestFail; + } + + if ((!isIntegerUpTo32 || isSigned) && node.get()) { + 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()) { + std::cerr + << "Node " << name << " didn't fail to parse as " + << "double" << std::endl; + return TestFail; + } + + if (type != ValueType::Size && node.get()) { + 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(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().value_or("") != strValue || + node.get("") != strValue) { + std::cerr + << "Node " << name << " failed to parse as " + << "string" << std::endl; + return TestFail; + } + + if (node.get().value_or(0.0) != value || + node.get(0.0) != value) { + std::cerr + << "Node " << name << " failed to parse as " + << "double" << std::endl; + return TestFail; + } + + if (isInteger8) { + if (node.get().value_or(0) != value || + node.get(0) != value) { + std::cerr + << "Node " << name << " failed to parse as " + << "int8_t" << std::endl; + return TestFail; + } + } + + if (isInteger8 && !isSigned) { + if (node.get().value_or(0) != unsignedValue || + node.get(0) != unsignedValue) { + std::cerr + << "Node " << name << " failed to parse as " + << "uint8_t" << std::endl; + return TestFail; + } + } + + if (isInteger8 || isInteger16) { + if (node.get().value_or(0) != value || + node.get(0) != value) { + std::cerr + << "Node " << name << " failed to parse as " + << "int16_t" << std::endl; + return TestFail; + } + } + + if ((isInteger8 || isInteger16) && !isSigned) { + if (node.get().value_or(0) != unsignedValue || + node.get(0) != unsignedValue) { + std::cerr + << "Node " << name << " failed to parse as " + << "uint16_t" << std::endl; + return TestFail; + } + } + + if (node.get().value_or(0) != value || + node.get(0) != value) { + std::cerr + << "Node " << name << " failed to parse as " + << "int32_t" << std::endl; + return TestFail; + } + + if (!isSigned) { + if (node.get().value_or(0) != unsignedValue || + node.get(0) != unsignedValue) { + std::cerr + << "Node " << name << " failed to parse as " + << "uint32_t" << std::endl; + return TestFail; + } + } + + return TestPass; + } + + template + bool equal(const ValueNode &node, T value) + { + constexpr T eps = std::numeric_limits::epsilon(); + + if (std::abs(node.get().value_or(0.0) - value) >= eps) + return false; + if (std::abs(node.get(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().value_or("") != strValue || + node.get("") != strValue) { + std::cerr + << "Node " << name << " failed to parse as " + << "string" << std::endl; + return TestFail; + } + + if (!equal(node, value)) { + std::cerr + << "Node " << name << " failed to parse as " + << "float" << std::endl; + return TestFail; + } + + if (!equal(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; + + static constexpr std::array 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(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::cerr + << "Empty node should have no value" + << std::endl; + return TestFail; + } + + /* Tests on list nodes. */ + ValueNode listNode; + + static constexpr std::array listElemNames = { + "libcamera", "linux", "isp" + }; + + for (const auto &name : listElemNames) + listNode.add(std::make_unique(std::string{ name })); + + if (!testNodeType(listNode, NodeType::List)) + return TestFail; + + if (!static_cast(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::cerr + << "Setting a value on a list node should fail" + << std::endl; + return TestFail; + } + + std::set names{ + listElemNames.begin(), listElemNames.end() + }; + + for (const auto &child : listNode.asList()) { + const std::string childName = child.get(""); + + 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, 3> dictElemKeyValues = { { + { "a", 1 }, + { "b", 2 }, + { "c", 3 }, + } }; + + for (const auto &[key, value] : dictElemKeyValues) + dictNode.add(key, std::make_unique(value)); + + if (!testNodeType(dictNode, NodeType::Dictionary)) + return TestFail; + + if (!static_cast(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::cerr + << "Setting a value on a dict node should fail" + << std::endl; + return TestFail; + } + + std::map 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(0); + if (value != iter->second) { + std::cerr + << "Invalid dictionary value " << value + << " for key '" << key << "'" << std::endl; + return TestFail; + } + + if (dictNode[key].get(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(static_cast(-100))); + values.add("uint8_t", std::make_unique(static_cast(100))); + values.add("int16_t", std::make_unique(static_cast(-1000))); + values.add("uint16_t", std::make_unique(static_cast(1000))); + values.add("int32_t", std::make_unique(static_cast(-100000))); + values.add("uint32_t", std::make_unique(static_cast(100000))); + values.add("float", std::make_unique(3.14159f)); + values.add("double", std::make_unique(3.14159)); + values.add("string", std::make_unique("libcamera"s)); + + std::unique_ptr sizeNode = std::make_unique(); + sizeNode->add(std::make_unique(640)); + sizeNode->add(std::make_unique(480)); + + values.add("size", std::move(sizeNode)); + + using ValueVariant = std::variant; + + static const + std::array, 10> nodesValues{ { + { "int8_t", ValueType::Int8, static_cast(-100) }, + { "uint8_t", ValueType::UInt8, static_cast(100) }, + { "int16_t", ValueType::Int16, static_cast(-1000) }, + { "uint16_t", ValueType::UInt16, static_cast(1000) }, + { "int32_t", ValueType::Int32, static_cast(-100000) }, + { "uint32_t", ValueType::UInt32, static_cast(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().value_or(Size{}) != arg || + node.get(Size{}) != arg) { + std::cerr + << "Invalid node size value" + << std::endl; + return TestFail; + } + + return TestPass; + }, + + [&](const std::string &arg) -> int { + if (node.get().value_or(std::string{}) != arg || + node.get(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(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)