From patchwork Tue Apr 7 15:34:12 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Laurent Pinchart X-Patchwork-Id: 26467 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 6C02FC3316 for ; Tue, 7 Apr 2026 15:35:11 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id BA8DC62E09; Tue, 7 Apr 2026 17:35:10 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="PdMV9S7U"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id A23AB62E0F for ; Tue, 7 Apr 2026 17:35:05 +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 UTF8SMTPSA id 0E85778E for ; Tue, 7 Apr 2026 17:33:38 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1775576018; bh=HOt119qKSFJu/3Px9eFQewp9OFlZyFGA9ULH743CQQ0=; h=From:To:Subject:Date:In-Reply-To:References:From; b=PdMV9S7UzK2ElSf02CtiVk915juilZutdPdIcxEuw6gAkmRQf4Vjs8NxziPVQPR15 XZQV19eLDfpJAJNeFYjJfg7/uDRiPJIXPvy34B3YaZxMZp66yJRdcNSgf8ObC8kFDc PkUv7j0WN5yloVX9lPNKeHi/ADrC9e2X7kwq1Zpo= From: Laurent Pinchart To: libcamera-devel@lists.libcamera.org Subject: [PATCH v2 27/42] test: Add ValueNode unit test Date: Tue, 7 Apr 2026 18:34:12 +0300 Message-ID: <20260407153427.1825999-28-laurent.pinchart@ideasonboard.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20260407153427.1825999-1-laurent.pinchart@ideasonboard.com> References: <20260407153427.1825999-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" Signed-off-by: Laurent Pinchart --- test/meson.build | 1 + test/value-node.cpp | 565 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 566 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..e9cb7ae11b9f --- /dev/null +++ b/test/value-node.cpp @@ -0,0 +1,565 @@ +/* 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; + +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + +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(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)