diff --git a/ntcore/src/main/native/cpp/LocalStorage.cpp b/ntcore/src/main/native/cpp/LocalStorage.cpp index 04b9398c1e..dd5d4e4a93 100644 --- a/ntcore/src/main/native/cpp/LocalStorage.cpp +++ b/ntcore/src/main/native/cpp/LocalStorage.cpp @@ -20,6 +20,7 @@ #include "Log.h" #include "PubSubOptions.h" #include "Types_internal.h" +#include "Value_internal.h" #include "networktables/NetworkTableValue.h" #include "ntcore_c.h" @@ -271,7 +272,7 @@ struct LSImpl { unsigned int eventFlags, bool sendNetwork, bool updateFlags = true); - void RefreshPubSubActive(TopicData* topic); + void RefreshPubSubActive(TopicData* topic, bool warnOnSubMismatch); void NetworkAnnounce(TopicData* topic, std::string_view typeStr, const wpi::json& properties, NT_Publisher pubHandle); @@ -394,17 +395,9 @@ void PublisherData::UpdateActive() { void SubscriberData::UpdateActive() { // for subscribers, unassigned is a wildcard // also allow numerically compatible subscribers - active = - config.type == NT_UNASSIGNED || - (config.type == topic->type && config.typeStr == topic->typeStr) || - ((config.type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) != 0 && - (config.type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) == - (topic->type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE))) || - ((config.type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) != - 0 && - (config.type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) == - (topic->type & - (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY))); + active = config.type == NT_UNASSIGNED || + (config.type == topic->type && config.typeStr == topic->typeStr) || + IsNumericCompatible(config.type, topic->type); } void LSImpl::NotifyTopic(TopicData* topic, unsigned int eventFlags) { @@ -620,12 +613,19 @@ void LSImpl::PropertiesUpdated(TopicData* topic, const wpi::json& update, } } -void LSImpl::RefreshPubSubActive(TopicData* topic) { +void LSImpl::RefreshPubSubActive(TopicData* topic, bool warnOnSubMismatch) { for (auto&& publisher : topic->localPublishers) { publisher->UpdateActive(); } for (auto&& subscriber : topic->localSubscribers) { subscriber->UpdateActive(); + if (warnOnSubMismatch && topic->Exists() && !subscriber->active) { + // warn on type mismatch + INFO( + "local subscribe to '{}' disabled due to type mismatch (wanted '{}', " + "published as '{}')", + topic->name, subscriber->config.typeStr, topic->typeStr); + } } } @@ -653,7 +653,7 @@ void LSImpl::NetworkAnnounce(TopicData* topic, std::string_view typeStr, } topic->type = type; topic->typeStr = typeStr; - RefreshPubSubActive(topic); + RefreshPubSubActive(topic, true); } if (!didExist) { event |= NT_EVENT_PUBLISH; @@ -702,7 +702,7 @@ void LSImpl::RemoveNetworkPublisher(TopicData* topic) { nextPub->config.typeStr != topic->typeStr) { topic->type = nextPub->config.type; topic->typeStr = nextPub->config.typeStr; - RefreshPubSubActive(topic); + RefreshPubSubActive(topic, false); // this may result in a duplicate publish warning on the server side, // but send one anyway in this case just to be sure if (nextPub->active && m_network) { @@ -730,19 +730,20 @@ PublisherData* LSImpl::AddLocalPublisher(TopicData* topic, topic->localPublishers.Add(publisher); if (!didExist) { + DEBUG4("AddLocalPublisher: setting {} type {} typestr {}", topic->name, + static_cast(config.type), config.typeStr); // set the type to the published type topic->type = config.type; topic->typeStr = config.typeStr; - RefreshPubSubActive(topic); + RefreshPubSubActive(topic, true); if (properties.is_null()) { topic->properties = wpi::json::object(); } else if (properties.is_object()) { topic->properties = properties; } else { - WPI_WARNING(m_logger, - "ignoring non-object properties when publishing '{}'", - topic->name); + WARNING("ignoring non-object properties when publishing '{}'", + topic->name); topic->properties = wpi::json::object(); } @@ -794,7 +795,7 @@ std::unique_ptr LSImpl::RemoveLocalPublisher( nextPub->config.typeStr != topic->typeStr) { topic->type = nextPub->config.type; topic->typeStr = nextPub->config.typeStr; - RefreshPubSubActive(topic); + RefreshPubSubActive(topic, false); if (nextPub->active && m_network) { m_network->Publish(nextPub->handle, topic->handle, topic->name, topic->typeStr, topic->properties, @@ -817,7 +818,7 @@ SubscriberData* LSImpl::AddLocalSubscriber(TopicData* topic, // warn on type mismatch INFO( "local subscribe to '{}' disabled due to type mismatch (wanted '{}', " - "currently '{}')", + "published as '{}')", topic->name, config.typeStr, topic->typeStr); } if (m_network) { @@ -1176,8 +1177,12 @@ PublisherData* LSImpl::PublishEntry(EntryData* entry, NT_Type type) { entry->subscriber->config.typeStr = typeStr; } else if (entry->subscriber->config.type != type || entry->subscriber->config.typeStr != typeStr) { - // don't allow dynamically changing the type of an entry - return nullptr; + if (!IsNumericCompatible(type, entry->subscriber->config.type)) { + // don't allow dynamically changing the type of an entry + ERROR("cannot publish entry {} as type {}, previously subscribed as {}", + entry->topic->name, typeStr, entry->subscriber->config.typeStr); + return nullptr; + } } // create publisher entry->publisher = AddLocalPublisher(entry->topic, wpi::json::object(), @@ -1199,6 +1204,10 @@ bool LSImpl::PublishLocalValue(PublisherData* publisher, const Value& value) { } if (publisher->topic->type != NT_UNASSIGNED && publisher->topic->type != value.type()) { + if (IsNumericCompatible(publisher->topic->type, value.type())) { + return PublishLocalValue( + publisher, ConvertNumericValue(value, publisher->topic->type)); + } return false; } if (publisher->active) { @@ -1229,27 +1238,39 @@ bool LSImpl::SetEntryValue(NT_Handle pubentryHandle, const Value& value) { bool LSImpl::SetDefaultEntryValue(NT_Handle pubsubentryHandle, const Value& value) { + DEBUG4("SetDefaultEntryValue({}, {})", pubsubentryHandle, + static_cast(value.type())); if (!value) { return false; } if (auto topic = GetTopic(pubsubentryHandle)) { - if (topic->type == NT_UNASSIGNED || topic->type == value.type()) { - // force value timestamps to 0 - topic->type = value.type(); - topic->lastValue = value; - topic->lastValue.SetTime(0); - topic->lastValue.SetServerTime(0); - + if (!topic->lastValue && + (topic->type == NT_UNASSIGNED || topic->type == value.type() || + IsNumericCompatible(topic->type, value.type()))) { + // publish if we haven't yet auto publisher = m_publishers.Get(pubsubentryHandle); if (!publisher) { if (auto entry = m_entries.Get(pubsubentryHandle)) { publisher = PublishEntry(entry, value.type()); } - if (!publisher) { - return true; - } } - PublishLocalValue(publisher, topic->lastValue); + + // force value timestamps to 0 + if (topic->type == NT_UNASSIGNED) { + topic->type = value.type(); + } + if (topic->type == value.type()) { + topic->lastValue = value; + } else if (IsNumericCompatible(topic->type, value.type())) { + topic->lastValue = ConvertNumericValue(value, topic->type); + } else { + return true; + } + topic->lastValue.SetTime(0); + topic->lastValue.SetServerTime(0); + if (publisher) { + PublishLocalValue(publisher, topic->lastValue); + } return true; } } @@ -2045,10 +2066,17 @@ READ_QUEUE_NUMBER(Double) Value LocalStorage::GetEntryValue(NT_Handle subentryHandle) { std::scoped_lock lock{m_mutex}; if (auto subscriber = m_impl->GetSubEntry(subentryHandle)) { - return subscriber->topic->lastValue; - } else { - return {}; + if (subscriber->config.type == NT_UNASSIGNED || + !subscriber->topic->lastValue || + subscriber->config.type == subscriber->topic->lastValue.type()) { + return subscriber->topic->lastValue; + } else if (IsNumericCompatible(subscriber->config.type, + subscriber->topic->lastValue.type())) { + return ConvertNumericValue(subscriber->topic->lastValue, + subscriber->config.type); + } } + return {}; } void LocalStorage::SetEntryFlags(NT_Entry entryHandle, unsigned int flags) { diff --git a/ntcore/src/main/native/cpp/Types_internal.h b/ntcore/src/main/native/cpp/Types_internal.h index 80c7f8e159..09196417ef 100644 --- a/ntcore/src/main/native/cpp/Types_internal.h +++ b/ntcore/src/main/native/cpp/Types_internal.h @@ -14,4 +14,17 @@ std::string_view TypeToString(NT_Type type); NT_Type StringToType(std::string_view typeStr); NT_Type StringToType3(std::string_view typeStr); +constexpr bool IsNumeric(NT_Type type) { + return (type & (NT_INTEGER | NT_FLOAT | NT_DOUBLE)) != 0; +} + +constexpr bool IsNumericArray(NT_Type type) { + return (type & (NT_INTEGER_ARRAY | NT_FLOAT_ARRAY | NT_DOUBLE_ARRAY)) != 0; +} + +constexpr bool IsNumericCompatible(NT_Type a, NT_Type b) { + return (IsNumeric(a) && IsNumeric(b)) || + (IsNumericArray(a) && IsNumericArray(b)); +} + } // namespace nt diff --git a/ntcore/src/main/native/cpp/Value.cpp b/ntcore/src/main/native/cpp/Value.cpp index d02fb3d39a..240447f2ea 100644 --- a/ntcore/src/main/native/cpp/Value.cpp +++ b/ntcore/src/main/native/cpp/Value.cpp @@ -89,6 +89,15 @@ Value Value::MakeBooleanArray(std::span value, int64_t time) { return val; } +Value Value::MakeBooleanArray(std::vector&& value, int64_t time) { + Value val{NT_BOOLEAN_ARRAY, time, private_init{}}; + auto data = std::make_shared>(std::move(value)); + val.m_val.data.arr_boolean.arr = data->data(); + val.m_val.data.arr_boolean.size = data->size(); + val.m_storage = std::move(data); + return val; +} + Value Value::MakeIntegerArray(std::span value, int64_t time) { Value val{NT_INTEGER_ARRAY, time, private_init{}}; auto data = @@ -99,6 +108,15 @@ Value Value::MakeIntegerArray(std::span value, int64_t time) { return val; } +Value Value::MakeIntegerArray(std::vector&& value, int64_t time) { + Value val{NT_INTEGER_ARRAY, time, private_init{}}; + auto data = std::make_shared>(std::move(value)); + val.m_val.data.arr_int.arr = data->data(); + val.m_val.data.arr_int.size = data->size(); + val.m_storage = std::move(data); + return val; +} + Value Value::MakeFloatArray(std::span value, int64_t time) { Value val{NT_FLOAT_ARRAY, time, private_init{}}; auto data = std::make_shared>(value.begin(), value.end()); @@ -108,6 +126,15 @@ Value Value::MakeFloatArray(std::span value, int64_t time) { return val; } +Value Value::MakeFloatArray(std::vector&& value, int64_t time) { + Value val{NT_FLOAT_ARRAY, time, private_init{}}; + auto data = std::make_shared>(std::move(value)); + val.m_val.data.arr_float.arr = data->data(); + val.m_val.data.arr_float.size = data->size(); + val.m_storage = std::move(data); + return val; +} + Value Value::MakeDoubleArray(std::span value, int64_t time) { Value val{NT_DOUBLE_ARRAY, time, private_init{}}; auto data = std::make_shared>(value.begin(), value.end()); @@ -117,6 +144,15 @@ Value Value::MakeDoubleArray(std::span value, int64_t time) { return val; } +Value Value::MakeDoubleArray(std::vector&& value, int64_t time) { + Value val{NT_DOUBLE_ARRAY, time, private_init{}}; + auto data = std::make_shared>(std::move(value)); + val.m_val.data.arr_double.arr = data->data(); + val.m_val.data.arr_double.size = data->size(); + val.m_storage = std::move(data); + return val; +} + Value Value::MakeStringArray(std::span value, int64_t time) { Value val{NT_STRING_ARRAY, time, private_init{}}; auto data = std::make_shared(value); diff --git a/ntcore/src/main/native/cpp/Value_internal.cpp b/ntcore/src/main/native/cpp/Value_internal.cpp new file mode 100644 index 0000000000..2003d31f57 --- /dev/null +++ b/ntcore/src/main/native/cpp/Value_internal.cpp @@ -0,0 +1,49 @@ +// Copyright (c) FIRST and other WPILib contributors. +// Open Source Software; you can modify and/or share it under the terms of +// the WPILib BSD license file in the root directory of this project. + +#include "Value_internal.h" + +using namespace nt; + +Value nt::ConvertNumericValue(const Value& value, NT_Type type) { + switch (type) { + case NT_INTEGER: { + Value newval = + Value::MakeInteger(GetNumericAs(value), value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + case NT_FLOAT: { + Value newval = Value::MakeFloat(GetNumericAs(value), value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + case NT_DOUBLE: { + Value newval = + Value::MakeDouble(GetNumericAs(value), value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + case NT_INTEGER_ARRAY: { + Value newval = Value::MakeIntegerArray(GetNumericArrayAs(value), + value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + case NT_FLOAT_ARRAY: { + Value newval = + Value::MakeFloatArray(GetNumericArrayAs(value), value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + case NT_DOUBLE_ARRAY: { + Value newval = Value::MakeDoubleArray(GetNumericArrayAs(value), + value.time()); + newval.SetServerTime(value.server_time()); + return newval; + } + default: + return {}; + } +} diff --git a/ntcore/src/main/native/cpp/Value_internal.h b/ntcore/src/main/native/cpp/Value_internal.h index 3b56768764..03532ac61f 100644 --- a/ntcore/src/main/native/cpp/Value_internal.h +++ b/ntcore/src/main/native/cpp/Value_internal.h @@ -12,6 +12,7 @@ #include +#include "networktables/NetworkTableValue.h" #include "ntcore_c.h" namespace nt { @@ -56,4 +57,35 @@ O* ConvertToC(const std::basic_string& in, size_t* out_len) { return out; } +template +T GetNumericAs(const Value& value) { + if (value.IsInteger()) { + return static_cast(value.GetInteger()); + } else if (value.IsFloat()) { + return static_cast(value.GetFloat()); + } else if (value.IsDouble()) { + return static_cast(value.GetDouble()); + } else { + return {}; + } +} + +template +std::vector GetNumericArrayAs(const Value& value) { + if (value.IsIntegerArray()) { + auto arr = value.GetIntegerArray(); + return {arr.begin(), arr.end()}; + } else if (value.IsFloatArray()) { + auto arr = value.GetFloatArray(); + return {arr.begin(), arr.end()}; + } else if (value.IsDoubleArray()) { + auto arr = value.GetDoubleArray(); + return {arr.begin(), arr.end()}; + } else { + return {}; + } +} + +Value ConvertNumericValue(const Value& value, NT_Type type); + } // namespace nt diff --git a/ntcore/src/main/native/include/networktables/NetworkTableValue.h b/ntcore/src/main/native/include/networktables/NetworkTableValue.h index f4454c2924..52e1430fd1 100644 --- a/ntcore/src/main/native/include/networktables/NetworkTableValue.h +++ b/ntcore/src/main/native/include/networktables/NetworkTableValue.h @@ -471,6 +471,18 @@ class Value final { return MakeBooleanArray(std::span(value.begin(), value.end()), time); } + /** + * Creates a boolean array entry value. + * + * @param value the value + * @param time if nonzero, the creation time to use (instead of the current + * time) + * @return The entry value + * + * @note This function moves the values out of the vector. + */ + static Value MakeBooleanArray(std::vector&& value, int64_t time = 0); + /** * Creates an integer array entry value. * @@ -495,6 +507,18 @@ class Value final { return MakeIntegerArray(std::span(value.begin(), value.end()), time); } + /** + * Creates an integer array entry value. + * + * @param value the value + * @param time if nonzero, the creation time to use (instead of the current + * time) + * @return The entry value + * + * @note This function moves the values out of the vector. + */ + static Value MakeIntegerArray(std::vector&& value, int64_t time = 0); + /** * Creates a float array entry value. * @@ -518,6 +542,18 @@ class Value final { return MakeFloatArray(std::span(value.begin(), value.end()), time); } + /** + * Creates a float array entry value. + * + * @param value the value + * @param time if nonzero, the creation time to use (instead of the current + * time) + * @return The entry value + * + * @note This function moves the values out of the vector. + */ + static Value MakeFloatArray(std::vector&& value, int64_t time = 0); + /** * Creates a double array entry value. * @@ -541,6 +577,18 @@ class Value final { return MakeDoubleArray(std::span(value.begin(), value.end()), time); } + /** + * Creates a double array entry value. + * + * @param value the value + * @param time if nonzero, the creation time to use (instead of the current + * time) + * @return The entry value + * + * @note This function moves the values out of the vector. + */ + static Value MakeDoubleArray(std::vector&& value, int64_t time = 0); + /** * Creates a string array entry value. * diff --git a/ntcore/src/test/native/cpp/LocalStorageTest.cpp b/ntcore/src/test/native/cpp/LocalStorageTest.cpp index 83386465df..2598a88346 100644 --- a/ntcore/src/test/native/cpp/LocalStorageTest.cpp +++ b/ntcore/src/test/native/cpp/LocalStorageTest.cpp @@ -15,19 +15,23 @@ #include "ntcore_c.h" using ::testing::_; +using ::testing::AllOf; +using ::testing::ElementsAre; +using ::testing::Field; +using ::testing::IsEmpty; +using ::testing::Property; using ::testing::Return; namespace nt { ::testing::Matcher IsPubSubOptions( const PubSubOptions& good) { - return ::testing::AllOf( - ::testing::Field("periodic", &PubSubOptions::periodic, good.periodic), - ::testing::Field("pollStorageSize", &PubSubOptions::pollStorageSize, - good.pollStorageSize), - ::testing::Field("logging", &PubSubOptions::sendAll, good.sendAll), - ::testing::Field("keepDuplicates", &PubSubOptions::keepDuplicates, - good.keepDuplicates)); + return AllOf(Field("periodic", &PubSubOptions::periodic, good.periodic), + Field("pollStorageSize", &PubSubOptions::pollStorageSize, + good.pollStorageSize), + Field("logging", &PubSubOptions::sendAll, good.sendAll), + Field("keepDuplicates", &PubSubOptions::keepDuplicates, + good.keepDuplicates)); } class LocalStorageTest : public ::testing::Test { @@ -247,6 +251,9 @@ TEST_F(LocalStorageTest, PubUnpubPub) { EXPECT_CALL(network, Publish(_, fooTopic, std::string_view{"foo"}, std::string_view{"boolean"}, wpi::json::object(), IsPubSubOptions({}))); + EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _, + "local subscribe to 'foo' disabled due to type " + "mismatch (wanted 'int', published as 'boolean')")); auto pub = storage.Publish(fooTopic, NT_BOOLEAN, "boolean", {}, {}); auto val = Value::MakeBoolean(true, 5); @@ -329,7 +336,7 @@ TEST_F(LocalStorageTest, LocalSubConflict) { IsPubSubOptions({}))); EXPECT_CALL(logger, Call(NT_LOG_INFO, _, _, "local subscribe to 'foo' disabled due to type " - "mismatch (wanted 'int', currently 'boolean')")); + "mismatch (wanted 'int', published as 'boolean')")); storage.Subscribe(fooTopic, NT_INTEGER, "int", {}); } @@ -466,6 +473,12 @@ TEST_F(LocalStorageTest, SetValueEmptyUntypedEntry) { } TEST_F(LocalStorageTest, PublishUntyped) { + EXPECT_CALL( + logger, + Call( + NT_LOG_ERROR, _, _, + "cannot publish 'foo' with an unassigned type or empty type string")); + EXPECT_EQ(storage.Publish(fooTopic, NT_UNASSIGNED, "", {}, {}), 0u); } @@ -473,4 +486,205 @@ TEST_F(LocalStorageTest, SetValueInvalidHandle) { EXPECT_FALSE(storage.SetEntryValue(0u, {})); } +class LocalStorageNumberVariantsTest : public LocalStorageTest { + public: + void CreateSubscriber(NT_Handle* handle, std::string_view name, NT_Type type, + std::string_view typeStr); + void CreateSubscribers(); + void CreateSubscribersArray(); + + NT_Subscriber sub1, sub2, sub3, sub4; + NT_Entry entry; + + struct SubEntry { + SubEntry(NT_Handle subentry, NT_Type type, std::string_view name) + : subentry{subentry}, type{type}, name{name} {} + NT_Handle subentry; + NT_Type type; + std::string name; + }; + std::vector subentries; +}; + +void LocalStorageNumberVariantsTest::CreateSubscriber( + NT_Handle* handle, std::string_view name, NT_Type type, + std::string_view typeStr) { + *handle = storage.Subscribe(fooTopic, type, typeStr, {}); + subentries.emplace_back(*handle, type, name); +} + +void LocalStorageNumberVariantsTest::CreateSubscribers() { + EXPECT_CALL(logger, + Call(NT_LOG_INFO, _, _, + "local subscribe to 'foo' disabled due to type " + "mismatch (wanted 'boolean', published as 'double')")); + CreateSubscriber(&sub1, "subDouble", NT_DOUBLE, "double"); + CreateSubscriber(&sub2, "subInteger", NT_INTEGER, "int"); + CreateSubscriber(&sub3, "subFloat", NT_FLOAT, "float"); + CreateSubscriber(&sub4, "subBoolean", NT_BOOLEAN, "boolean"); + entry = storage.GetEntry("foo"); + subentries.emplace_back(entry, NT_UNASSIGNED, "entry"); +} + +void LocalStorageNumberVariantsTest::CreateSubscribersArray() { + EXPECT_CALL(logger, + Call(NT_LOG_INFO, _, _, + "local subscribe to 'foo' disabled due to type " + "mismatch (wanted 'boolean[]', published as 'double[]')")); + CreateSubscriber(&sub1, "subDouble", NT_DOUBLE_ARRAY, "double[]"); + CreateSubscriber(&sub2, "subInteger", NT_INTEGER_ARRAY, "int[]"); + CreateSubscriber(&sub3, "subFloat", NT_FLOAT_ARRAY, "float[]"); + CreateSubscriber(&sub4, "subBoolean", NT_BOOLEAN_ARRAY, "boolean[]"); + entry = storage.GetEntry("foo"); + subentries.emplace_back(entry, NT_UNASSIGNED, "entry"); +} + +TEST_F(LocalStorageNumberVariantsTest, GetEntryPubAfter) { + EXPECT_CALL(network, Subscribe(_, _, _)).Times(5); + EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(network, SetValue(_, _)).Times(1); + CreateSubscribers(); + auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {}); + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + // all subscribers get the actual type and time + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + EXPECT_EQ(storage.GetEntryType(subentry.subentry), NT_DOUBLE); + EXPECT_EQ(storage.GetEntryLastChange(subentry.subentry), 50); + } + // for subscribers, they get a converted value or nothing on mismatch + EXPECT_EQ(storage.GetEntryValue(sub1), Value::MakeDouble(1.0, 50)); + EXPECT_EQ(storage.GetEntryValue(sub2), Value::MakeInteger(1, 50)); + EXPECT_EQ(storage.GetEntryValue(sub3), Value::MakeFloat(1.0, 50)); + EXPECT_EQ(storage.GetEntryValue(sub4), Value{}); + // entrys just get whatever the value is + EXPECT_EQ(storage.GetEntryValue(entry), Value::MakeDouble(1.0, 50)); +} + +TEST_F(LocalStorageNumberVariantsTest, GetEntryPubBefore) { + EXPECT_CALL(network, Subscribe(_, _, _)).Times(5); + EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(network, SetValue(_, _)).Times(1); + auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {}); + CreateSubscribers(); + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + // all subscribers get the actual type and time + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + EXPECT_EQ(storage.GetEntryType(subentry.subentry), NT_DOUBLE); + EXPECT_EQ(storage.GetEntryLastChange(subentry.subentry), 50); + } + // for subscribers, they get a converted value or nothing on mismatch + EXPECT_EQ(storage.GetEntryValue(sub1), Value::MakeDouble(1.0, 50)); + EXPECT_EQ(storage.GetEntryValue(sub2), Value::MakeInteger(1, 50)); + EXPECT_EQ(storage.GetEntryValue(sub3), Value::MakeFloat(1.0, 50)); + EXPECT_EQ(storage.GetEntryValue(sub4), Value{}); + // entrys just get whatever the value is + EXPECT_EQ(storage.GetEntryValue(entry), Value::MakeDouble(1.0, 50)); +} + +template +::testing::Matcher TSEq(auto value, int64_t time) { + return AllOf(Field("value", &T::value, value), Field("time", &T::time, time)); +} + +TEST_F(LocalStorageNumberVariantsTest, GetAtomic) { + EXPECT_CALL(network, Subscribe(_, _, _)).Times(5); + EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(network, SetValue(_, _)).Times(1); + auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {}); + CreateSubscribers(); + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + EXPECT_THAT(storage.GetAtomicDouble(subentry.subentry, 0), + TSEq(1.0, 50)); + EXPECT_THAT(storage.GetAtomicInteger(subentry.subentry, 0), + TSEq(1, 50)); + EXPECT_THAT(storage.GetAtomicFloat(subentry.subentry, 0), + TSEq(1.0, 50)); + EXPECT_THAT(storage.GetAtomicBoolean(subentry.subentry, false), + TSEq(false, 0)); + } +} + +template +::testing::Matcher TSSpanEq(std::span value, int64_t time) { + return AllOf( + Field("value", &T::value, wpi::SpanEq(std::span(value))), + Field("time", &T::time, time)); +} + +TEST_F(LocalStorageNumberVariantsTest, GetAtomicArray) { + EXPECT_CALL(network, Subscribe(_, _, _)).Times(5); + EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(network, SetValue(_, _)).Times(1); + auto pub = storage.Publish(fooTopic, NT_DOUBLE_ARRAY, "double[]", {}, {}); + CreateSubscribersArray(); + storage.SetEntryValue(pub, Value::MakeDoubleArray({1.0}, 50)); + + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + double doubleVal = 1.0; + EXPECT_THAT(storage.GetAtomicDoubleArray(subentry.subentry, {}), + TSSpanEq(std::span{&doubleVal, 1}, 50)); + int64_t intVal = 1; + EXPECT_THAT(storage.GetAtomicIntegerArray(subentry.subentry, {}), + TSSpanEq(std::span{&intVal, 1}, 50)); + float floatVal = 1.0; + EXPECT_THAT(storage.GetAtomicFloatArray(subentry.subentry, {}), + TSSpanEq(std::span{&floatVal, 1}, 50)); + EXPECT_THAT(storage.GetAtomicBooleanArray(subentry.subentry, {}), + TSSpanEq(std::span{}, 0)); + } +} + +TEST_F(LocalStorageNumberVariantsTest, ReadQueue) { + EXPECT_CALL(network, Subscribe(_, _, _)).Times(5); + EXPECT_CALL(network, Publish(_, _, _, _, _, _)).Times(1); + EXPECT_CALL(network, SetValue(_, _)).Times(4); + auto pub = storage.Publish(fooTopic, NT_DOUBLE, "double", {}, {}); + CreateSubscribers(); + + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + if (subentry.type == NT_BOOLEAN) { + EXPECT_THAT(storage.ReadQueueDouble(subentry.subentry), IsEmpty()); + } else { + EXPECT_THAT(storage.ReadQueueDouble(subentry.subentry), + ElementsAre(TSEq(1.0, 50))); + } + } + + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + if (subentry.type == NT_BOOLEAN) { + EXPECT_THAT(storage.ReadQueueInteger(subentry.subentry), IsEmpty()); + } else { + EXPECT_THAT(storage.ReadQueueInteger(subentry.subentry), + ElementsAre(TSEq(1, 50))); + } + } + + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + if (subentry.type == NT_BOOLEAN) { + EXPECT_THAT(storage.ReadQueueFloat(subentry.subentry), IsEmpty()); + } else { + EXPECT_THAT(storage.ReadQueueFloat(subentry.subentry), + ElementsAre(TSEq(1.0, 50))); + } + } + + storage.SetEntryValue(pub, Value::MakeDouble(1.0, 50)); + for (auto&& subentry : subentries) { + SCOPED_TRACE(subentry.name); + EXPECT_THAT(storage.ReadQueueBoolean(subentry.subentry), IsEmpty()); + } +} + } // namespace nt diff --git a/ntcore/src/test/native/cpp/TestPrinters.cpp b/ntcore/src/test/native/cpp/TestPrinters.cpp index 34afe4cd45..2d9eb91664 100644 --- a/ntcore/src/test/native/cpp/TestPrinters.cpp +++ b/ntcore/src/test/native/cpp/TestPrinters.cpp @@ -121,37 +121,37 @@ void PrintTo(const Value& value, std::ostream* os) { case NT_UNASSIGNED: break; case NT_BOOLEAN: - *os << (value.GetBoolean() ? "true" : "false"); + *os << "boolean, " << (value.GetBoolean() ? "true" : "false"); break; case NT_DOUBLE: - *os << value.GetDouble(); + *os << "double, " << value.GetDouble(); break; case NT_FLOAT: - *os << value.GetFloat(); + *os << "float, " << value.GetFloat(); break; case NT_INTEGER: - *os << value.GetInteger(); + *os << "int, " << value.GetInteger(); break; case NT_STRING: - *os << '"' << value.GetString() << '"'; + *os << "string, \"" << value.GetString() << '"'; break; case NT_RAW: - *os << ::testing::PrintToString(value.GetRaw()); + *os << "raw, " << ::testing::PrintToString(value.GetRaw()); break; case NT_BOOLEAN_ARRAY: - *os << ::testing::PrintToString(value.GetBooleanArray()); + *os << "boolean[], " << ::testing::PrintToString(value.GetBooleanArray()); break; case NT_DOUBLE_ARRAY: - *os << ::testing::PrintToString(value.GetDoubleArray()); + *os << "double[], " << ::testing::PrintToString(value.GetDoubleArray()); break; case NT_FLOAT_ARRAY: - *os << ::testing::PrintToString(value.GetFloatArray()); + *os << "float[], " << ::testing::PrintToString(value.GetFloatArray()); break; case NT_INTEGER_ARRAY: - *os << ::testing::PrintToString(value.GetIntegerArray()); + *os << "int[], " << ::testing::PrintToString(value.GetIntegerArray()); break; case NT_STRING_ARRAY: - *os << ::testing::PrintToString(value.GetStringArray()); + *os << "string[], " << ::testing::PrintToString(value.GetStringArray()); break; default: *os << "UNKNOWN TYPE " << value.type();