mirror of
https://git.libcamera.org/libcamera/libcamera.git
synced 2025-07-12 23:09:45 +03:00
When accessing a nonexistent key on a dict the YamlObject returns an empty element. This element can happily be cast to a string which is unexpected. For example the following statement: yamlDict["nonexistent"].get<string>("default") is expected to return "default" but actually returns "". Fix this by introducing an empty type to distinguish between an empty YamlObject and a YamlObject of type value containing an empty string. For completeness add an isEmpty() function and an explicit cast to bool to be able to test for that type. Extend the tests accordingly. Signed-off-by: Stefan Klug <stefan.klug@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com>
620 lines
16 KiB
C++
620 lines
16 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
/*
|
|
* Copyright (C) 2022, Google Inc.
|
|
*
|
|
* YAML parser operations tests
|
|
*/
|
|
|
|
#include <array>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
|
|
#include <libcamera/base/file.h>
|
|
#include <libcamera/base/utils.h>
|
|
|
|
#include "libcamera/internal/yaml_parser.h"
|
|
|
|
#include "test.h"
|
|
|
|
using namespace libcamera;
|
|
using namespace std;
|
|
|
|
static const string testYaml =
|
|
"string: libcamera\n"
|
|
"double: 3.14159\n"
|
|
"int8_t: -100\n"
|
|
"uint8_t: 100\n"
|
|
"int16_t: -1000\n"
|
|
"uint16_t: 1000\n"
|
|
"int32_t: -100000\n"
|
|
"uint32_t: 100000\n"
|
|
"size: [1920, 1080]\n"
|
|
"list:\n"
|
|
" - James\n"
|
|
" - Mary\n"
|
|
" - \n"
|
|
"dictionary:\n"
|
|
" a: 1\n"
|
|
" c: 3\n"
|
|
" b: 2\n"
|
|
" empty:\n"
|
|
"level1:\n"
|
|
" level2:\n"
|
|
" - [1, 2]\n"
|
|
" - {one: 1, two: 2}\n";
|
|
|
|
static const string invalidYaml =
|
|
"Invalid : - YAML : - Content";
|
|
|
|
class YamlParserTest : public Test
|
|
{
|
|
protected:
|
|
bool createFile(const string &content, string &filename)
|
|
{
|
|
filename = "/tmp/libcamera.test.XXXXXX";
|
|
int fd = mkstemp(&filename.front());
|
|
if (fd == -1)
|
|
return false;
|
|
|
|
int ret = write(fd, content.c_str(), content.size());
|
|
close(fd);
|
|
|
|
if (ret != static_cast<int>(content.size()))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
int init()
|
|
{
|
|
if (!createFile(testYaml, testYamlFile_))
|
|
return TestFail;
|
|
|
|
if (!createFile(invalidYaml, invalidYamlFile_))
|
|
return TestFail;
|
|
|
|
return TestPass;
|
|
}
|
|
|
|
enum class Type {
|
|
String,
|
|
Int8,
|
|
UInt8,
|
|
Int16,
|
|
UInt16,
|
|
Int32,
|
|
UInt32,
|
|
Double,
|
|
Size,
|
|
List,
|
|
Dictionary,
|
|
};
|
|
|
|
int testObjectType(const YamlObject &obj, const char *name, Type type)
|
|
{
|
|
bool isList = type == Type::List || type == Type::Size;
|
|
bool isScalar = !isList && type != Type::Dictionary;
|
|
bool isInteger8 = type == Type::Int8 || type == Type::UInt8;
|
|
bool isInteger16 = type == Type::Int16 || type == Type::UInt16;
|
|
bool isInteger32 = type == Type::Int32 || type == Type::UInt32;
|
|
bool isIntegerUpTo16 = isInteger8 || isInteger16;
|
|
bool isIntegerUpTo32 = isIntegerUpTo16 || isInteger32;
|
|
bool isSigned = type == Type::Int8 || type == Type::Int16 ||
|
|
type == Type::Int32;
|
|
|
|
if ((isScalar && !obj.isValue()) || (!isScalar && obj.isValue())) {
|
|
std::cerr
|
|
<< "Object " << name << " type mismatch when compared to "
|
|
<< "value" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if ((isList && !obj.isList()) || (!isList && obj.isList())) {
|
|
std::cerr
|
|
<< "Object " << name << " type mismatch when compared to "
|
|
<< "list" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if ((type == Type::Dictionary && !obj.isDictionary()) ||
|
|
(type != Type::Dictionary && obj.isDictionary())) {
|
|
std::cerr
|
|
<< "Object " << name << " type mismatch when compared to "
|
|
<< "dictionary" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isScalar && obj.get<std::string>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "string" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isInteger8 && obj.get<int8_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "int8_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if ((!isInteger8 || isSigned) && obj.get<uint8_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "uint8_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isIntegerUpTo16 && obj.get<int16_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "int16_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if ((!isIntegerUpTo16 || isSigned) && obj.get<uint16_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "uint16_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isIntegerUpTo32 && obj.get<int32_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "int32_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if ((!isIntegerUpTo32 || isSigned) && obj.get<uint32_t>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "uint32_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isIntegerUpTo32 && type != Type::Double && obj.get<double>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "double" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (type != Type::Size && obj.get<Size>()) {
|
|
std::cerr
|
|
<< "Object " << name << " didn't fail to parse as "
|
|
<< "Size" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
return TestPass;
|
|
}
|
|
|
|
int testIntegerObject(const YamlObject &obj, const char *name, Type type,
|
|
int64_t value)
|
|
{
|
|
uint64_t unsignedValue = static_cast<uint64_t>(value);
|
|
std::string strValue = std::to_string(value);
|
|
bool isInteger8 = type == Type::Int8 || type == Type::UInt8;
|
|
bool isInteger16 = type == Type::Int16 || type == Type::UInt16;
|
|
bool isSigned = type == Type::Int8 || type == Type::Int16 ||
|
|
type == Type::Int32;
|
|
|
|
/* All integers can be parsed as strings or double. */
|
|
|
|
if (obj.get<string>().value_or("") != strValue ||
|
|
obj.get<string>("") != strValue) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "string" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (obj.get<double>().value_or(0.0) != value ||
|
|
obj.get<double>(0.0) != value) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "double" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (isInteger8) {
|
|
if (obj.get<int8_t>().value_or(0) != value ||
|
|
obj.get<int8_t>(0) != value) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "int8_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
if (isInteger8 && !isSigned) {
|
|
if (obj.get<uint8_t>().value_or(0) != unsignedValue ||
|
|
obj.get<uint8_t>(0) != unsignedValue) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "uint8_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
if (isInteger8 || isInteger16) {
|
|
if (obj.get<int16_t>().value_or(0) != value ||
|
|
obj.get<int16_t>(0) != value) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "int16_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
if ((isInteger8 || isInteger16) && !isSigned) {
|
|
if (obj.get<uint16_t>().value_or(0) != unsignedValue ||
|
|
obj.get<uint16_t>(0) != unsignedValue) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "uint16_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
if (obj.get<int32_t>().value_or(0) != value ||
|
|
obj.get<int32_t>(0) != value) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "int32_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!isSigned) {
|
|
if (obj.get<uint32_t>().value_or(0) != unsignedValue ||
|
|
obj.get<uint32_t>(0) != unsignedValue) {
|
|
std::cerr
|
|
<< "Object " << name << " failed to parse as "
|
|
<< "uint32_t" << std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
return TestPass;
|
|
}
|
|
|
|
int run()
|
|
{
|
|
/* Test invalid YAML file */
|
|
File file{ invalidYamlFile_ };
|
|
if (!file.open(File::OpenModeFlag::ReadOnly)) {
|
|
cerr << "Fail to open invalid YAML file" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
std::unique_ptr<YamlObject> root = YamlParser::parse(file);
|
|
if (root) {
|
|
cerr << "Invalid YAML file parse successfully" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test YAML file */
|
|
file.close();
|
|
file.setFileName(testYamlFile_);
|
|
if (!file.open(File::OpenModeFlag::ReadOnly)) {
|
|
cerr << "Fail to open test YAML file" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
root = YamlParser::parse(file);
|
|
|
|
if (!root) {
|
|
cerr << "Fail to parse test YAML file: " << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (!root->isDictionary()) {
|
|
cerr << "YAML root is not dictionary" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
std::vector<const char *> rootElemNames = {
|
|
"string", "double", "int8_t", "uint8_t", "int16_t",
|
|
"uint16_t", "int32_t", "uint32_t", "size", "list",
|
|
"dictionary", "level1",
|
|
};
|
|
|
|
for (const char *name : rootElemNames) {
|
|
if (!root->contains(name)) {
|
|
cerr << "Missing " << name << " object in YAML root"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
}
|
|
|
|
/* Test string object */
|
|
auto &strObj = (*root)["string"];
|
|
|
|
if (testObjectType(strObj, "string", Type::String) != TestPass)
|
|
return TestFail;
|
|
|
|
if (strObj.get<string>().value_or("") != "libcamera" ||
|
|
strObj.get<string>("") != "libcamera") {
|
|
cerr << "String object parse as wrong content" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test int8_t object */
|
|
auto &int8Obj = (*root)["int8_t"];
|
|
|
|
if (testObjectType(int8Obj, "int8_t", Type::Int8) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(int8Obj, "int8_t", Type::Int8, -100) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test uint8_t object */
|
|
auto &uint8Obj = (*root)["uint8_t"];
|
|
|
|
if (testObjectType(uint8Obj, "uint8_t", Type::UInt8) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(uint8Obj, "uint8_t", Type::UInt8, 100) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test int16_t object */
|
|
auto &int16Obj = (*root)["int16_t"];
|
|
|
|
if (testObjectType(int16Obj, "int16_t", Type::Int16) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(int16Obj, "int16_t", Type::Int16, -1000) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test uint16_t object */
|
|
auto &uint16Obj = (*root)["uint16_t"];
|
|
|
|
if (testObjectType(uint16Obj, "uint16_t", Type::UInt16) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(uint16Obj, "uint16_t", Type::UInt16, 1000) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test int32_t object */
|
|
auto &int32Obj = (*root)["int32_t"];
|
|
|
|
if (testObjectType(int32Obj, "int32_t", Type::Int32) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(int32Obj, "int32_t", Type::Int32, -100000) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test uint32_t object */
|
|
auto &uint32Obj = (*root)["uint32_t"];
|
|
|
|
if (testObjectType(uint32Obj, "uint32_t", Type::UInt32) != TestPass)
|
|
return TestFail;
|
|
|
|
if (testIntegerObject(uint32Obj, "uint32_t", Type::UInt32, 100000) != TestPass)
|
|
return TestFail;
|
|
|
|
/* Test double value */
|
|
auto &doubleObj = (*root)["double"];
|
|
|
|
if (testObjectType(doubleObj, "double", Type::Double) != TestPass)
|
|
return TestFail;
|
|
|
|
if (doubleObj.get<string>().value_or("") != "3.14159" ||
|
|
doubleObj.get<string>("") != "3.14159") {
|
|
cerr << "Double object fail to parse as string" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (doubleObj.get<double>().value_or(0.0) != 3.14159 ||
|
|
doubleObj.get<double>(0.0) != 3.14159) {
|
|
cerr << "Double object parse as wrong value" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test Size value */
|
|
auto &sizeObj = (*root)["size"];
|
|
|
|
if (testObjectType(sizeObj, "size", Type::Size) != TestPass)
|
|
return TestFail;
|
|
|
|
if (sizeObj.get<Size>().value_or(Size(0, 0)) != Size(1920, 1080) ||
|
|
sizeObj.get<Size>(Size(0, 0)) != Size(1920, 1080)) {
|
|
cerr << "Size object parse as wrong value" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test list object */
|
|
auto &listObj = (*root)["list"];
|
|
|
|
if (testObjectType(listObj, "list", Type::List) != TestPass)
|
|
return TestFail;
|
|
|
|
static constexpr std::array<const char *, 3> listValues{
|
|
"James",
|
|
"Mary",
|
|
"",
|
|
};
|
|
|
|
if (listObj.size() != listValues.size()) {
|
|
cerr << "List object parse with wrong size" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
unsigned int i = 0;
|
|
for (auto &elem : listObj.asList()) {
|
|
if (i >= listValues.size()) {
|
|
std::cerr << "Too many elements in list during iteration"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
std::string value = listValues[i];
|
|
|
|
if (&elem != &listObj[i]) {
|
|
std::cerr << "List element " << i << " has wrong address"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (elem.get<std::string>("") != value) {
|
|
std::cerr << "List element " << i << " has wrong value"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
/* Ensure that empty objects get parsed as empty strings. */
|
|
if (!listObj[2].isValue()) {
|
|
cerr << "Empty object is not a value" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test dictionary object */
|
|
auto &dictObj = (*root)["dictionary"];
|
|
|
|
if (testObjectType(dictObj, "dictionary", Type::Dictionary) != TestPass)
|
|
return TestFail;
|
|
|
|
static constexpr std::array<std::pair<const char *, int>, 4> dictValues{ {
|
|
{ "a", 1 },
|
|
{ "c", 3 },
|
|
{ "b", 2 },
|
|
{ "empty", -100 },
|
|
} };
|
|
|
|
size_t dictSize = dictValues.size();
|
|
|
|
if (dictObj.size() != dictSize) {
|
|
cerr << "Dictionary object has wrong size" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
i = 0;
|
|
for (const auto &[key, elem] : dictObj.asDict()) {
|
|
if (i >= dictSize) {
|
|
std::cerr << "Too many elements in dictionary during iteration"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
const auto &item = dictValues[i];
|
|
if (item.first != key) {
|
|
std::cerr << "Dictionary key " << i << " has wrong value"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (&elem != &dictObj[key]) {
|
|
std::cerr << "Dictionary element " << i << " has wrong address"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
if (elem.get<int32_t>(-100) != item.second) {
|
|
std::cerr << "Dictionary element " << i << " has wrong value"
|
|
<< std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
/* Ensure that empty objects get parsed as empty strings. */
|
|
if (!dictObj["empty"].isValue()) {
|
|
cerr << "Empty object is not of type value" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Ensure that keys without values are added to a dict. */
|
|
if (!dictObj.contains("empty")) {
|
|
cerr << "Empty element is missing in dict" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test access to nonexistent member. */
|
|
if (dictObj["nonexistent"].get<std::string>("default") != "default") {
|
|
cerr << "Accessing nonexistent dict entry fails to return default" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test nonexistent object has value type empty. */
|
|
if (!dictObj["nonexistent"].isEmpty()) {
|
|
cerr << "Accessing nonexistent object returns non-empty object" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test explicit cast to bool on an empty object returns true. */
|
|
if (!!dictObj["empty"] != true) {
|
|
cerr << "Casting empty entry to bool returns false" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Test explicit cast to bool on nonexistent object returns false. */
|
|
if (!!dictObj["nonexistent"] != false) {
|
|
cerr << "Casting nonexistent dict entry to bool returns true" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
/* Make sure utils::map_keys() works on the adapter. */
|
|
(void)utils::map_keys(dictObj.asDict());
|
|
|
|
/* Test leveled objects */
|
|
auto &level1Obj = (*root)["level1"];
|
|
|
|
if (!level1Obj.isDictionary()) {
|
|
cerr << "level1 object fail to parse as Dictionary" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
auto &level2Obj = level1Obj["level2"];
|
|
|
|
if (!level2Obj.isList() || level2Obj.size() != 2) {
|
|
cerr << "level2 object should be 2 element list" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
auto &firstElement = level2Obj[0];
|
|
if (!firstElement.isList() ||
|
|
firstElement.size() != 2 ||
|
|
firstElement[0].get<int32_t>(0) != 1 ||
|
|
firstElement[1].get<int32_t>(0) != 2) {
|
|
cerr << "The first element of level2 object fail to parse as integer list" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
const auto &values = firstElement.getList<uint16_t>();
|
|
if (!values || values->size() != 2 || (*values)[0] != 1 || (*values)[1] != 2) {
|
|
cerr << "getList() failed to return correct vector" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
auto &secondElement = level2Obj[1];
|
|
if (!secondElement.isDictionary() ||
|
|
!secondElement.contains("one") ||
|
|
!secondElement.contains("two") ||
|
|
secondElement["one"].get<int32_t>(0) != 1 ||
|
|
secondElement["two"].get<int32_t>(0) != 2) {
|
|
cerr << "The second element of level2 object fail to parse as dictionary" << std::endl;
|
|
return TestFail;
|
|
}
|
|
|
|
return TestPass;
|
|
}
|
|
|
|
void cleanup()
|
|
{
|
|
unlink(testYamlFile_.c_str());
|
|
unlink(invalidYamlFile_.c_str());
|
|
}
|
|
|
|
private:
|
|
std::string testYamlFile_;
|
|
std::string invalidYamlFile_;
|
|
};
|
|
|
|
TEST_REGISTER(YamlParserTest)
|