// Integration test for the SECS-I TCP transport. Two TcpTransport // instances are wired back-to-back over a loopback TCP pair; the test // sends a SECS-II message in one direction and asserts the other side // reassembles and delivers it. Verifies that the FSM, the framer, and // the asio I/O loop play together end-to-end. #include #include #include #include #include #include #include #include "secsgem/secs2/item.hpp" #include "secsgem/secs2/message.hpp" #include "secsgem/secsi/tcp_transport.hpp" using namespace secsgem::secsi; namespace s2 = secsgem::secs2; namespace { struct SocketPair { asio::io_context io; asio::ip::tcp::socket a{io}; asio::ip::tcp::socket b{io}; SocketPair() { asio::ip::tcp::acceptor acc(io, asio::ip::tcp::endpoint( asio::ip::address_v4::loopback(), 0)); const auto port = acc.local_endpoint().port(); bool accepted = false, connected = false; std::error_code ec_a, ec_b; acc.async_accept(a, [&](std::error_code ec) { ec_a = ec; accepted = true; }); b.async_connect({asio::ip::address_v4::loopback(), port}, [&](std::error_code ec) { ec_b = ec; connected = true; }); while (!(accepted && connected)) { if (io.stopped()) io.restart(); if (io.poll() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } REQUIRE_FALSE(ec_a); REQUIRE_FALSE(ec_b); } }; template void pump_until(asio::io_context& io, Pred pred, std::chrono::milliseconds budget = std::chrono::seconds(5)) { const auto deadline = std::chrono::steady_clock::now() + budget; while (!pred()) { if (std::chrono::steady_clock::now() > deadline) FAIL("pump_until budget exceeded"); if (io.stopped()) io.restart(); if (io.poll() == 0) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } // namespace TEST_CASE("SECS-I TCP transport: single-block round-trip") { SocketPair sp; auto master = std::make_shared(std::move(sp.a), Role::Master, 1); auto slave = std::make_shared(std::move(sp.b), Role::Slave, 1); std::vector received; slave->set_message_handler([&](s2::Message m) { received.push_back(std::move(m)); }); std::vector errors; master->set_error_handler([&](const std::string& e) { errors.push_back(e); }); slave->set_error_handler([&](const std::string& e) { errors.push_back(e); }); master->start(); slave->start(); master->send(s2::Message(1, 13, true, s2::Item::ascii("LOT-99"))); pump_until(sp.io, [&] { return !received.empty() || !errors.empty(); }); REQUIRE(errors.empty()); REQUIRE(received.size() == 1); CHECK(received[0].stream == 1); CHECK(received[0].function == 13); CHECK(received[0].reply_expected); master->close(); slave->close(); pump_until(sp.io, [&] { return master->state() == Protocol::State::Idle; }, std::chrono::seconds(1)); } TEST_CASE("SECS-I TCP transport: multi-block message reassembles correctly") { SocketPair sp; auto master = std::make_shared(std::move(sp.a), Role::Master, 1); auto slave = std::make_shared(std::move(sp.b), Role::Slave, 1); std::vector received; slave->set_message_handler([&](s2::Message m) { received.push_back(std::move(m)); }); std::vector errors; auto err = [&](const std::string& e) { errors.push_back(e); }; master->set_error_handler(err); slave->set_error_handler(err); master->start(); slave->start(); // Build a body that comfortably exceeds kMaxBlockBody. std::string big(700, 'X'); master->send(s2::Message(7, 3, true, s2::Item::ascii(big))); pump_until(sp.io, [&] { return !received.empty() || !errors.empty(); }); REQUIRE(errors.empty()); REQUIRE(received.size() == 1); CHECK(received[0].stream == 7); REQUIRE(received[0].body.has_value()); CHECK(received[0].body->as_ascii() == big); master->close(); slave->close(); } TEST_CASE("SECS-I TCP transport: bidirectional exchange") { SocketPair sp; auto master = std::make_shared(std::move(sp.a), Role::Master, 1); auto slave = std::make_shared(std::move(sp.b), Role::Slave, 1); std::vector at_master, at_slave; master->set_message_handler([&](s2::Message m) { at_master.push_back(std::move(m)); }); slave->set_message_handler([&](s2::Message m) { at_slave.push_back(std::move(m)); }); std::vector errors; auto err = [&](const std::string& e) { errors.push_back(e); }; master->set_error_handler(err); slave->set_error_handler(err); master->start(); slave->start(); master->send(s2::Message(1, 1, true, s2::Item::ascii("ping"))); pump_until(sp.io, [&] { return !at_slave.empty() || !errors.empty(); }); REQUIRE(errors.empty()); REQUIRE(at_slave.size() == 1); slave->send(s2::Message(1, 2, false, s2::Item::ascii("pong"))); pump_until(sp.io, [&] { return !at_master.empty() || !errors.empty(); }); REQUIRE(errors.empty()); REQUIRE(at_master.size() == 1); CHECK(at_master[0].function == 2); master->close(); slave->close(); }