Files
sdbus-cpp/tests/unittests/Message_test.cpp
Stanislav Angelovič 02ca7212d1 feat: introduce support for struct-as-dict serialization (#459)
This extends the functionality of SDBUSCPP_REGISTER_STRUCT macro.

It now provides functionality for serializing a user-defined struct as an a{sv} dictionary, and for deserializing an a{sv} dictionary into a user-defined struct. The former one is achieved by decorating the struct with sdbus::as_dictionary(struct), the latter one is an automatic behavior -- when sdbus-c++ is asked to deserialize into a struct but the data in the message is of type a{sv}, then the dict-to-struct deserialization is performed automatically.

There are some aspects of behavior in the serialization/deserialization functionality that can be customized by the client. Newly introduced SDBUSCPP_ENABLE_NESTED_STRUCT2DICT_SERIALIZATION and SDBUSCPP_ENABLE_RELAXED_DICT2STRUCT_DESERIALIZATION macros serve the purpose.
2024-11-20 23:25:13 +01:00

683 lines
18 KiB
C++

/**
* (C) 2016 - 2021 KISTLER INSTRUMENTE AG, Winterthur, Switzerland
* (C) 2016 - 2024 Stanislav Angelovic <stanislav.angelovic@protonmail.com>
*
* @file Message_test.cpp
*
* Created on: Dec 3, 2016
* Project: sdbus-c++
* Description: High-level D-Bus IPC C++ library based on sd-bus
*
* This file is part of sdbus-c++.
*
* sdbus-c++ is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* sdbus-c++ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with sdbus-c++. If not, see <http://www.gnu.org/licenses/>.
*/
#include <sdbus-c++/Types.h>
#include "MessageUtils.h"
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <cstdint>
#include <list>
using ::testing::Eq;
using ::testing::StrEq;
using ::testing::Gt;
using ::testing::DoubleEq;
using ::testing::IsNull;
using ::testing::SizeIs;
using ::testing::ElementsAre;
using namespace std::string_literals;
namespace
{
std::string deserializeString(sdbus::PlainMessage& msg)
{
std::string str;
msg >> str;
return str;
}
}
namespace sdbus {
template <typename _ElementType>
sdbus::Message& operator<<(sdbus::Message& msg, const std::list<_ElementType>& items)
{
// TODO: This can also be simplified on the basis of a callback (see dictionary...)
msg.openContainer<_ElementType>();
for (const auto& item : items)
msg << item;
msg.closeContainer();
return msg;
}
template <typename _ElementType>
sdbus::Message& operator>>(sdbus::Message& msg, std::list<_ElementType>& items)
{
if(!msg.enterContainer<_ElementType>())
return msg;
while (true)
{
_ElementType elem;
if (msg >> elem)
items.emplace_back(std::move(elem));
else
break;
}
msg.clearFlags();
msg.exitContainer();
return msg;
}
}
template <typename _Element, typename _Allocator>
struct sdbus::signature_of<std::list<_Element, _Allocator>>
: sdbus::signature_of<std::vector<_Element, _Allocator>>
{};
namespace my {
enum class Enum
{
Value1,
Value2,
Value3
};
struct Struct
{
int i;
std::string s;
std::list<double> l;
Enum e;
friend bool operator==(const Struct& lhs, const Struct& rhs) = default;
};
struct RelaxedStruct
{
int i;
std::string s;
std::list<double> l;
Enum e;
friend bool operator==(const RelaxedStruct& lhs, const RelaxedStruct& rhs) = default;
};
struct NestedStruct
{
int i;
std::string s;
Enum e;
Struct x;
friend bool operator==(const NestedStruct& lhs, const NestedStruct& rhs) = default;
};
}
SDBUSCPP_REGISTER_STRUCT(my::Struct, i, s, l, e);
SDBUSCPP_ENABLE_RELAXED_DICT2STRUCT_DESERIALIZATION(my::RelaxedStruct);
SDBUSCPP_REGISTER_STRUCT(my::RelaxedStruct, i, s, l, e);
SDBUSCPP_ENABLE_NESTED_STRUCT2DICT_SERIALIZATION(my::NestedStruct);
SDBUSCPP_REGISTER_STRUCT(my::NestedStruct, i, s, e, x);
/*-------------------------------------*/
/* -- TEST CASES -- */
/*-------------------------------------*/
TEST(AMessage, CanBeDefaultConstructed)
{
ASSERT_NO_THROW(sdbus::PlainMessage());
}
TEST(AMessage, IsInvalidAfterDefaultConstructed)
{
sdbus::PlainMessage msg;
ASSERT_FALSE(msg.isValid());
}
TEST(AMessage, IsValidWhenConstructedAsRealMessage)
{
auto msg = sdbus::createPlainMessage();
ASSERT_TRUE(msg.isValid());
}
TEST(AMessage, CreatesShallowCopyWhenCopyConstructed)
{
auto msg = sdbus::createPlainMessage();
msg << "I am a string"s;
msg.seal();
sdbus::PlainMessage msgCopy = msg;
std::string str;
msgCopy >> str;
ASSERT_THAT(str, Eq("I am a string"));
ASSERT_THROW(msgCopy >> str, sdbus::Error);
}
TEST(AMessage, CreatesDeepCopyWhenEplicitlyCopied)
{
auto msg = sdbus::createPlainMessage();
msg << "I am a string"s;
msg.seal();
auto msgCopy = sdbus::createPlainMessage();
msg.copyTo(msgCopy, true);
msgCopy.seal(); // Seal to be able to read from it subsequently
msg.rewind(true); // Rewind to the beginning after copying
ASSERT_THAT(deserializeString(msg), Eq("I am a string"));
ASSERT_THAT(deserializeString(msgCopy), Eq("I am a string"));
}
TEST(AMessage, IsEmptyWhenContainsNoValue)
{
auto msg = sdbus::createPlainMessage();
ASSERT_TRUE(msg.isEmpty());
}
TEST(AMessage, IsNotEmptyWhenContainsAValue)
{
auto msg = sdbus::createPlainMessage();
msg << "I am a string"s;
ASSERT_FALSE(msg.isEmpty());
}
TEST(AMessage, CanCarryASimpleInteger)
{
auto msg = sdbus::createPlainMessage();
const int dataWritten = 5;
msg << dataWritten;
msg.seal();
int dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryAStringAsAStringView)
{
auto msg = sdbus::createPlainMessage();
const std::string_view dataWritten = "Hello";
msg << dataWritten;
msg.seal();
std::string dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryAUnixFd)
{
auto msg = sdbus::createPlainMessage();
const sdbus::UnixFd dataWritten{0};
msg << dataWritten;
msg.seal();
sdbus::UnixFd dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead.get(), Gt(dataWritten.get()));
}
TEST(AMessage, CanCarryAVariant)
{
auto msg = sdbus::createPlainMessage();
const auto dataWritten = sdbus::Variant((double)3.14);
msg << dataWritten;
msg.seal();
sdbus::Variant dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead.get<double>(), Eq(dataWritten.get<double>()));
}
TEST(AMessage, CanCarryACollectionOfEmbeddedVariants)
{
auto msg = sdbus::createPlainMessage();
std::vector<sdbus::Variant> value{sdbus::Variant{"hello"s}, sdbus::Variant{(double)3.14}};
const auto dataWritten = sdbus::Variant{value};
msg << dataWritten;
msg.seal();
sdbus::Variant dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead.get<decltype(value)>()[0].get<std::string>(), Eq(value[0].get<std::string>()));
ASSERT_THAT(dataRead.get<decltype(value)>()[1].get<double>(), Eq(value[1].get<double>()));
}
TEST(AMessage, CanCarryDBusArrayOfTrivialTypesGivenAsStdVector)
{
auto msg = sdbus::createPlainMessage();
const std::vector<int64_t> dataWritten{3545342, 43643532, 324325};
msg << dataWritten;
msg.seal();
std::vector<int64_t> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryDBusArrayOfNontrivialTypesGivenAsStdVector)
{
auto msg = sdbus::createPlainMessage();
const std::vector dataWritten{sdbus::Signature{"s"}, sdbus::Signature{"u"}, sdbus::Signature{"b"}};
msg << dataWritten;
msg.seal();
std::vector<sdbus::Signature> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryDBusArrayOfTrivialTypesGivenAsStdArray)
{
auto msg = sdbus::createPlainMessage();
const std::array<int, 3> dataWritten{3545342, 43643532, 324325};
msg << dataWritten;
msg.seal();
std::array<int, 3> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryDBusArrayOfNontrivialTypesGivenAsStdArray)
{
auto msg = sdbus::createPlainMessage();
const std::array dataWritten{sdbus::Signature{"s"}, sdbus::Signature{"u"}, sdbus::Signature{"b"}};
msg << dataWritten;
msg.seal();
std::array<sdbus::Signature, 3> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
#ifdef __cpp_lib_span
TEST(AMessage, CanCarryDBusArrayOfTrivialTypesGivenAsStdSpan)
{
auto msg = sdbus::createPlainMessage();
const std::array<int, 3> sourceArray{3545342, 43643532, 324325};
const std::span dataWritten{sourceArray};
msg << dataWritten;
msg.seal();
std::array<int, 3> destinationArray;
std::span dataRead{destinationArray};
msg >> dataRead;
ASSERT_THAT(std::vector(dataRead.begin(), dataRead.end()), Eq(std::vector(dataWritten.begin(), dataWritten.end())));
}
TEST(AMessage, CanCarryDBusArrayOfNontrivialTypesGivenAsStdSpan)
{
auto msg = sdbus::createPlainMessage();
const std::array sourceArray{sdbus::Signature{"s"}, sdbus::Signature{"u"}, sdbus::Signature{"b"}};
const std::span dataWritten{sourceArray};
msg << dataWritten;
msg.seal();
std::array<sdbus::Signature, 3> destinationArray;
std::span dataRead{destinationArray};
msg >> dataRead;
ASSERT_THAT(std::vector(dataRead.begin(), dataRead.end()), Eq(std::vector(dataWritten.begin(), dataWritten.end())));
}
#endif
TEST(AMessage, CanCarryAnEnumValue)
{
auto msg = sdbus::createPlainMessage();
enum class EnumA : int16_t {X = 5} aWritten{EnumA::X};
enum EnumB {Y = 11} bWritten{EnumB::Y};
msg << aWritten << bWritten;
msg.seal();
EnumA aRead{};
EnumB bRead{};
msg >> aRead >> bRead;
ASSERT_THAT(aRead, Eq(aWritten));
ASSERT_THAT(bRead, Eq(bWritten));
}
TEST(AMessage, ThrowsWhenDestinationStdArrayIsTooSmallDuringDeserialization)
{
auto msg = sdbus::createPlainMessage();
const std::vector<int> dataWritten{3545342, 43643532, 324325, 89789, 15343};
msg << dataWritten;
msg.seal();
std::array<int, 3> dataRead;
ASSERT_THROW(msg >> dataRead, sdbus::Error);
}
#ifdef __cpp_lib_span
TEST(AMessage, ThrowsWhenDestinationStdSpanIsTooSmallDuringDeserialization)
{
auto msg = sdbus::createPlainMessage();
const std::array<int, 3> dataWritten{3545342, 43643532, 324325};
msg << dataWritten;
msg.seal();
std::array<int, 2> destinationArray;
std::span dataRead{destinationArray};
ASSERT_THROW(msg >> dataRead, sdbus::Error);
}
#endif
TEST(AMessage, CanCarryADictionary)
{
auto msg = sdbus::createPlainMessage();
std::map<int, std::string> dataWritten{{1, "one"}, {2, "two"}};
msg << dataWritten;
msg.seal();
std::map<int, std::string> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryAComplexType)
{
auto msg = sdbus::createPlainMessage();
using ComplexType = std::map<
uint64_t,
sdbus::Struct<
std::map<
uint8_t,
std::vector<
sdbus::Struct<
sdbus::ObjectPath,
bool,
int16_t,
/*sdbus::Variant,*/
std::map<int, std::string>
>
>
>,
sdbus::Signature,
double
>
>;
ComplexType dataWritten = { {1, {{{5, {{sdbus::ObjectPath{"/some/object"}, true, 45, {{6, "hello"}, {7, "world"}}}}}}, sdbus::Signature{"av"}, 3.14}}};
msg << dataWritten;
msg.seal();
ComplexType dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanPeekASimpleType)
{
auto msg = sdbus::createPlainMessage();
msg << 123;
msg.seal();
auto [type, contents] = msg.peekType();
ASSERT_THAT(type, Eq('i'));
ASSERT_THAT(contents, IsNull());
}
TEST(AMessage, CanPeekContainerContents)
{
auto msg = sdbus::createPlainMessage();
msg << std::map<int, std::string>{{1, "one"}, {2, "two"}};
msg.seal();
auto [type, contents] = msg.peekType();
ASSERT_THAT(type, Eq('a'));
ASSERT_THAT(contents, StrEq("{is}"));
}
TEST(AMessage, CanCarryDBusArrayGivenAsCustomType)
{
auto msg = sdbus::createPlainMessage();
const std::list<int64_t> dataWritten{3545342, 43643532, 324325};
//custom::MyType t;
msg << dataWritten;
// msg << t;
msg.seal();
std::list<int64_t> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
const my::Struct dataWritten{3545342, "hello"s, {3.14, 2.4568546}, my::Enum::Value2};
msg << dataWritten;
msg.seal();
my::Struct dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryNestedUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
const my::NestedStruct dataWritten{3545342, "hello"s, my::Enum::Value2, {12, "world"s, {3.14, 2.4568546}, my::Enum::Value3}};
msg << dataWritten;
msg.seal();
my::NestedStruct dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanSerializeUserDefinedStructAsDictionaryOfStringsToVariants)
{
auto msg = sdbus::createPlainMessage();
const my::Struct dataWritten{3545342, "hello"s, {3.14, 2.4568546}, my::Enum::Value2};
msg << sdbus::as_dictionary{dataWritten};
msg.seal();
std::map<std::string, sdbus::Variant> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, SizeIs(4));
ASSERT_THAT(dataRead.at("i").get<int>(), Eq(3545342));
ASSERT_THAT(dataRead.at("s").get<std::string>(), Eq("hello"));
ASSERT_THAT(dataRead.at("l").get<std::list<double>>(), ElementsAre(3.14, 2.4568546));
ASSERT_THAT(dataRead.at("e").get<my::Enum>(), Eq(my::Enum::Value2));
}
TEST(AMessage, CanRecursivelySerializeUserDefinedStructAsDictionaryOfStringsToVariants)
{
auto msg = sdbus::createPlainMessage();
const my::NestedStruct dataWritten{3545342, "hello"s, my::Enum::Value2, {12, "world"s, {3.14, 2.4568546}, my::Enum::Value3}};
msg << sdbus::as_dictionary{dataWritten};
msg.seal();
std::map<std::string, sdbus::Variant> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, SizeIs(4));
ASSERT_THAT(dataRead.at("i").get<int>(), Eq(3545342));
ASSERT_THAT(dataRead.at("s").get<std::string>(), Eq("hello"));
ASSERT_THAT(dataRead.at("e").get<my::Enum>(), Eq(my::Enum::Value2));
auto nestedStructRead = dataRead.at("x").get<std::map<std::string, sdbus::Variant>>(); // Nested struct serialized as dict
ASSERT_THAT(nestedStructRead.at("i").get<int>(), Eq(12));
ASSERT_THAT(nestedStructRead.at("s").get<std::string>(), Eq("world"));
ASSERT_THAT(nestedStructRead.at("l").get<std::list<double>>(), ElementsAre(3.14, 2.4568546));
ASSERT_THAT(nestedStructRead.at("e").get<my::Enum>(), Eq(my::Enum::Value3));
}
TEST(AMessage, CanDeserializeDictionaryOfStringsToVariantsIntoUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"i", sdbus::Variant{3545342}}
, {"s", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::Struct dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(my::Struct{3545342, "hello"s, {3.14, 2.4568546}, my::Enum::Value2}));
}
TEST(AMessage, FailsDeserializingDictionaryIntoUserDefinedStructIfStructMemberIsNotFound)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"i", sdbus::Variant{3545342}}
, {"nonexistent", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::Struct dataRead;
ASSERT_THROW(msg >> dataRead, sdbus::Error);
}
TEST(AMessage, DeserializesDictionaryIntoStructWithMissingMembersSuccessfullyIfRelaxedOptionIsSet)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"some_nonexistent_struct_member", sdbus::Variant{3545342}}
, {"another_nonexistent_struct_member", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::RelaxedStruct dataRead{};
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(my::RelaxedStruct{{}, {}, {3.14, 2.4568546}, my::Enum::Value2}));
}
class AMessage : public ::testing::TestWithParam<std::variant<int32_t, std::string, my::Struct>>
{
};
TEST_P(AMessage, CanCarryDBusVariantGivenAsStdVariant)
{
auto msg = sdbus::createPlainMessage();
const std::variant<int32_t, std::string, my::Struct> dataWritten{GetParam()};
msg << dataWritten;
msg.seal();
std::variant<int32_t, std::string, my::Struct> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST_P(AMessage, ThrowsWhenDestinationStdVariantHasWrongTypeDuringDeserialization)
{
auto msg = sdbus::createPlainMessage();
const std::variant<int32_t, std::string, my::Struct> dataWritten{GetParam()};
msg << dataWritten;
msg.seal();
std::variant<std::vector<bool>> dataRead;
ASSERT_THROW(msg >> dataRead, sdbus::Error);
}
INSTANTIATE_TEST_SUITE_P( StringIntStruct
, AMessage
, ::testing::Values("hello"s, 1, my::Struct{}));