diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index ed6adf38..4485488a 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -22,8 +22,6 @@ env: on: # # run on push or pull request events for the master branch push: - paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"] - branches: [ "main", "main-dev" ] tags: - "v*.*.*" diff --git a/CMakeLists.txt b/CMakeLists.txt index 776e044a..5212d8c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ set(AGENT_VERSION_MAJOR 2) set(AGENT_VERSION_MINOR 2) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 10) +set(AGENT_VERSION_BUILD 11) set(AGENT_VERSION_RC "") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent diff --git a/README.md b/README.md index 5f669eeb..61d49d23 100644 --- a/README.md +++ b/README.md @@ -656,6 +656,11 @@ These can be overridden on a per-adapter basis *Default*: false +* `Heartbeat` – Overrides the heartbeat interval sent back from the adapter in the + `* PONG `. The heartbeat will always be this value in milliseconds. + + *Default*: _None_ + * `IgnoreTimestamps` - Overwrite timestamps with the agent time. This will correct clock drift but will not give as accurate relative time since it will not take into consideration network latencies. This can be overridden on a per adapter basis. diff --git a/src/mtconnect/configuration/agent_config.cpp b/src/mtconnect/configuration/agent_config.cpp index f23e9153..097f25c7 100644 --- a/src/mtconnect/configuration/agent_config.cpp +++ b/src/mtconnect/configuration/agent_config.cpp @@ -936,6 +936,7 @@ namespace mtconnect::configuration { {{configuration::Url, string()}, {configuration::Device, string()}, {configuration::UUID, string()}, + {configuration::Heartbeat, std::chrono::milliseconds()}, {configuration::Uuid, string()}}); if (HasOption(adapterOptions, configuration::Uuid) && diff --git a/src/mtconnect/source/adapter/shdr/connector.cpp b/src/mtconnect/source/adapter/shdr/connector.cpp index 1adb8a1a..216d4c89 100644 --- a/src/mtconnect/source/adapter/shdr/connector.cpp +++ b/src/mtconnect/source/adapter/shdr/connector.cpp @@ -43,7 +43,8 @@ namespace sys = boost::system; namespace mtconnect::source::adapter::shdr { // Connector public methods Connector::Connector(asio::io_context::strand &strand, string server, unsigned int port, - seconds legacyTimeout, seconds reconnectInterval) + seconds legacyTimeout, seconds reconnectInterval, + std::optional heartbeat) : m_server(std::move(server)), m_strand(strand), m_socket(strand.context()), @@ -57,6 +58,7 @@ namespace mtconnect::source::adapter::shdr { m_connected(false), m_disconnecting(false), m_realTime(false), + m_heartbeatOverride(heartbeat), m_legacyTimeout(duration_cast(legacyTimeout)), m_reconnectInterval(duration_cast(reconnectInterval)), m_receiveTimeLimit(m_legacyTimeout) @@ -399,10 +401,14 @@ namespace mtconnect::source::adapter::shdr { NAMED_SCOPE("Connector::startHeartbeats"); size_t pos; - if (arg.length() > 7 && arg[6] == ' ' && - (pos = arg.find_first_of("0123456789", 7)) != string::npos) + if (m_heartbeatOverride || (arg.length() > 7 && arg[6] == ' ' && + (pos = arg.find_first_of("0123456789", 7)) != string::npos)) { - auto freq = milliseconds {atoi(arg.substr(pos).c_str())}; + std::chrono::milliseconds freq; + if (m_heartbeatOverride) + freq = *m_heartbeatOverride; + else + freq = milliseconds {atoi(arg.substr(pos).c_str())}; constexpr minutes maxTimeOut = minutes {30}; // Make the maximum timeout 30 minutes. if (freq > 0ms && freq < maxTimeOut) diff --git a/src/mtconnect/source/adapter/shdr/connector.hpp b/src/mtconnect/source/adapter/shdr/connector.hpp index 7ed948bf..2f298226 100644 --- a/src/mtconnect/source/adapter/shdr/connector.hpp +++ b/src/mtconnect/source/adapter/shdr/connector.hpp @@ -42,7 +42,8 @@ namespace mtconnect::source::adapter::shdr { /// @param reconnectInterval time between reconnection attempts (defaults to 10 seconds) Connector(boost::asio::io_context::strand &strand, std::string server, unsigned int port, std::chrono::seconds legacyTimout = std::chrono::seconds {600}, - std::chrono::seconds reconnectInterval = std::chrono::seconds {10}); + std::chrono::seconds reconnectInterval = std::chrono::seconds {10}, + std::optional heartbeat = std::nullopt); virtual ~Connector(); @@ -94,6 +95,8 @@ namespace mtconnect::source::adapter::shdr { } void setRealTime(bool realTime = true) { m_realTime = realTime; } + + const auto &getHeartbeatOverride() const { return m_heartbeatOverride; } protected: void close(); @@ -146,7 +149,8 @@ namespace mtconnect::source::adapter::shdr { // Heartbeats bool m_heartbeats = false; - std::chrono::milliseconds m_heartbeatFrequency = std::chrono::milliseconds {HEARTBEAT_FREQ}; + std::chrono::milliseconds m_heartbeatFrequency {HEARTBEAT_FREQ}; + std::optional m_heartbeatOverride; std::chrono::milliseconds m_legacyTimeout; std::chrono::milliseconds m_reconnectInterval; std::chrono::milliseconds m_receiveTimeLimit; diff --git a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp index fcdb1f18..34f8a1f8 100644 --- a/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp +++ b/src/mtconnect/source/adapter/shdr/shdr_adapter.cpp @@ -48,15 +48,15 @@ namespace mtconnect::source::adapter::shdr { { GetOptions(block, m_options, options); AddOptions(block, m_options, - { - {configuration::UUID, string()}, - {configuration::Manufacturer, string()}, - {configuration::Station, string()}, - {configuration::Url, string()}, - }); + {{configuration::Heartbeat, Milliseconds {0}}, + {configuration::UUID, string()}, + {configuration::Manufacturer, string()}, + {configuration::Station, string()}, + {configuration::Url, string()}}); m_options.erase(configuration::Host); m_options.erase(configuration::Port); + m_heartbeatOverride = GetOption(m_options, configuration::Heartbeat); AddDefaultedOptions(block, m_options, {{configuration::Host, "localhost"s}, diff --git a/test_package/adapter_test.cpp b/test_package/adapter_test.cpp index 1ac8c8e8..e87ce78d 100644 --- a/test_package/adapter_test.cpp +++ b/test_package/adapter_test.cpp @@ -15,6 +15,9 @@ // limitations under the License. // +/// @file +/// SHDR Adapter tests + // Ensure that gtest is the first header otherwise Windows raises an error #include // Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) @@ -45,7 +48,8 @@ int main(int argc, char *argv[]) return RUN_ALL_TESTS(); } -TEST(AdapterTest, MultilineData) +/// @test check if multiline data is getting treated as a single chunk +TEST(AdapterTest, should_handle_multiline_data) { asio::io_context ioc; asio::io_context::strand strand(ioc); @@ -76,6 +80,7 @@ Another Line... EXPECT_EQ(exp, data); } +/// @test verify that commands can also be multiline TEST(AdapterTest, should_forward_multiline_command) { asio::io_context ioc; @@ -108,6 +113,7 @@ TEST(AdapterTest, should_forward_multiline_command) EXPECT_EQ(exp, value); } +/// @test Check that the options are reset when given as a command TEST(AdapterTest, should_set_options_from_commands) { asio::io_context ioc; @@ -122,3 +128,23 @@ TEST(AdapterTest, should_set_options_from_commands) auto v = GetOption(adapter->getOptions(), "ShdrVersion"); ASSERT_EQ(int64_t(3), *v); } + +/// @test validate heartbeat override is set properly +TEST(AdapterTest, should_set_heartbeat_override_from_configuration) +{ + namespace pt = boost::property_tree; + asio::io_context ioc; + asio::io_context::strand strand(ioc); + ConfigOptions options; + pt::ptree tree; + tree.push_back(make_pair(configuration::Host, pt::ptree("locahost"s))); + tree.push_back(make_pair(configuration::Port, pt::ptree("7878"s))); + tree.push_back(make_pair(configuration::Heartbeat, pt::ptree("123"s))); + pipeline::PipelineContextPtr context = make_shared(); + auto adapter = make_unique(ioc, context, options, tree); + + const auto &over = adapter->getHeartbeatOverride(); + ASSERT_TRUE(over); + ASSERT_EQ(123ms, *over); +} + diff --git a/test_package/connector_test.cpp b/test_package/connector_test.cpp index 092cb202..48017903 100644 --- a/test_package/connector_test.cpp +++ b/test_package/connector_test.cpp @@ -15,6 +15,10 @@ // limitations under the License. // +/// @file +/// Test the SHDR connector behavior. The connector handles the lower level communications for the +/// adapter. + // Ensure that gtest is the first header otherwise Windows raises an error #include // Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) @@ -44,6 +48,8 @@ using tcp = boost::asio::ip::tcp; namespace ip = boost::asio::ip; namespace sys = boost::system; +using namespace chrono_literals; + // main int main(int argc, char *argv[]) { @@ -51,12 +57,17 @@ int main(int argc, char *argv[]) return RUN_ALL_TESTS(); } +/// @brief Mock adapter for this connector. class TestConnector : public Connector { public: TestConnector(boost::asio::io_context::strand &strand, const std::string &server, - unsigned int port, std::chrono::seconds legacyTimeout = std::chrono::seconds {5}) - : Connector(strand, server, port, legacyTimeout), m_disconnected(false), m_strand(strand) + unsigned int port, std::chrono::seconds legacyTimeout = std::chrono::seconds {5}, + std::chrono::seconds reconnectInterval = std::chrono::seconds {10}, + std::optional heartbeat = std::nullopt) + : Connector(strand, server, port, legacyTimeout, reconnectInterval, heartbeat), + m_disconnected(false), + m_strand(strand) {} bool start(unsigned short port) @@ -97,6 +108,7 @@ class TestConnector : public Connector boost::asio::io_context::strand &m_strand; }; +/// @brief Connector test runner class ConnectorTest : public testing::Test { protected: @@ -198,22 +210,27 @@ class ConnectorTest : public testing::Test string m_line; }; -TEST_F(ConnectorTest, Connection) +/// @test check if the connector is able to connect to a socket. Make sure it sends a ping. +TEST_F(ConnectorTest, should_be_able_to_connect) { ASSERT_TRUE(m_connector->m_disconnected); startServer(); + /// - Start the connector m_connector->start(m_port); + /// - Wait for 5 seconds for a connection runUntil(5s, [this]() -> bool { return m_connected && m_connector->isConnected(); }); + /// - Check for a ping EXPECT_FALSE(m_connector->m_disconnected); auto line = read(1s); ASSERT_EQ("* PING", line); } -TEST_F(ConnectorTest, DataCapture) +/// @test check if the connector can receive data from the connector +TEST_F(ConnectorTest, should_be_able_to_read_data_from_a_socket) { // Start the accept thread ASSERT_TRUE(m_connector->m_disconnected); @@ -230,7 +247,8 @@ TEST_F(ConnectorTest, DataCapture) ASSERT_EQ("Hello Connector", m_connector->m_data); } -TEST_F(ConnectorTest, Disconnect) +/// @test check if the connector disconnects +TEST_F(ConnectorTest, should_disconnect_when_server_closes_the_connection) { // Start the accept thread startServer(); @@ -247,7 +265,8 @@ TEST_F(ConnectorTest, Disconnect) ASSERT_TRUE(m_connector->m_disconnected); } -TEST_F(ConnectorTest, ProtocolCommand) +/// @test check if the connector correctly handles a protocol command starting with a `*` +TEST_F(ConnectorTest, should_process_a_protocol_command) { // Start the accept thread startServer(); @@ -479,7 +498,7 @@ TEST_F(ConnectorTest, IPV6Connection) #endif } -TEST_F(ConnectorTest, StartHeartbeats) +TEST_F(ConnectorTest, should_start_heartbeats_when_a_valid_pong_is_received) { ASSERT_TRUE(!m_connector->heartbeats()); @@ -543,3 +562,34 @@ TEST_F(ConnectorTest, test_trimming_trailing_white_space) ASSERT_EQ((string) "fourth", m_connector->m_list[3]); ASSERT_EQ((string) "r", m_connector->m_list[4]); } + +/// @test validate the heartbeat override works for the connector. +TEST_F(ConnectorTest, should_override_adapter_heartbeat_in_pong) +{ + boost::asio::io_context::strand strand(m_context); + m_connector = std::make_unique(strand, "127.0.0.1", m_port, 600s, 10s, 123ms); + m_connector->m_disconnected = true; + m_connected = false; + + ASSERT_TRUE(!m_connector->heartbeats()); + + string line = "* PONG 888"; + m_connector->startHeartbeats(line); + + ASSERT_TRUE(m_connector->heartbeats()); + ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency()); + + m_connector->resetHeartbeats(); + + line = "* PONG 456 "; + m_connector->startHeartbeats(line); + + ASSERT_TRUE(m_connector->heartbeats()); + ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency()); + + line = "* PONG 323"; + m_connector->startHeartbeats(line); + + ASSERT_TRUE(m_connector->heartbeats()); + ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency()); +}