/* 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)