From 94a2c801fdb8f38ec45e8bf34ac02f264310bd08 Mon Sep 17 00:00:00 2001 From: Jens Geudens Date: Sun, 5 Apr 2026 15:01:12 +0200 Subject: [PATCH] Add full integration test --- tests/integration/tst_dummyadapter.cpp | 117 +++++++++++++++++++++++++ tests/integration/tst_dummyadapter.h | 3 + 2 files changed, 120 insertions(+) diff --git a/tests/integration/tst_dummyadapter.cpp b/tests/integration/tst_dummyadapter.cpp index 8fe9f5bb..30b7d0b2 100644 --- a/tests/integration/tst_dummyadapter.cpp +++ b/tests/integration/tst_dummyadapter.cpp @@ -2,6 +2,7 @@ #include "ProtocolAdapter/adapterclient.h" #include "ProtocolAdapter/adapterprocess.h" +#include "util/result.h" #include #include @@ -13,6 +14,11 @@ namespace { constexpr int cSessionTimeoutMs = 10000; constexpr int cReadTimeoutMs = 5000; +// Dummy adapter built-in server settings +constexpr const char* DUMMY_IP = "127.0.0.1"; +constexpr quint16 DUMMY_PORT = 5020; +constexpr quint8 DUMMY_SLAVE_ID = 1; + //! Minimal adapter configuration with no real connections or devices. //! The dummymodbusadapter accepts this without attempting any real I/O. QJsonObject minimalConfig() @@ -25,6 +31,30 @@ QJsonObject minimalConfig() return config; } +//! Full configuration pointing at the dummymodbusadapter's built-in TCP server. +QJsonObject realConfig() +{ + QJsonObject connection; + connection["id"] = 0; + connection["type"] = QStringLiteral("tcp"); + connection["ip"] = DUMMY_IP; + connection["port"] = static_cast(DUMMY_PORT); + connection["persistent"] = true; + connection["timeout"] = 1000; + + QJsonObject device; + device["id"] = 1; + device["connectionId"] = 0; + device["slaveId"] = static_cast(DUMMY_SLAVE_ID); + + QJsonObject config; + config["version"] = 1; + config["general"] = QJsonObject(); + config["connections"] = QJsonArray({ connection }); + config["devices"] = QJsonArray({ device }); + return config; +} + } // namespace void TestDummyAdapter::describeReturnsRequiredFields() @@ -97,4 +127,91 @@ void TestDummyAdapter::fullLifecycleSessionStarts() client.stopSession(); } +void TestDummyAdapter::readRegistersReturnsValidData() +{ + auto* process = new AdapterProcess(); + AdapterClient client(process); + + const QStringList registers = { "${40001}", "${40002}", "${40010}" }; + + connect(&client, &AdapterClient::describeResult, &client, + [&client, ®isters]() { client.provideConfig(realConfig(), registers); }); + + QSignalSpy spyStarted(&client, &AdapterClient::sessionStarted); + QSignalSpy spyError(&client, &AdapterClient::sessionError); + + client.prepareAdapter(QString::fromUtf8(DUMMY_ADAPTER_EXECUTABLE)); + + QVERIFY2(spyStarted.wait(cSessionTimeoutMs), "sessionStarted not emitted"); + QCOMPARE(spyError.count(), 0); + + QSignalSpy spyData(&client, &AdapterClient::readDataResult); + client.requestReadData(); + QVERIFY2(spyData.wait(cReadTimeoutMs), "readDataResult not emitted"); + + const auto results = spyData.at(0).at(0).value(); + QCOMPARE(results.size(), registers.size()); + // The dummymodbusadapter stores the 0-based register offset as the register value: + // register 40001 → offset 0, register 40002 → offset 1, register 40010 → offset 9. + QVERIFY2(results[0].isValid(), "Expected SUCCESS for register 40001"); + QCOMPARE(results[0].value(), 0.0); + QVERIFY2(results[1].isValid(), "Expected SUCCESS for register 40002"); + QCOMPARE(results[1].value(), 1.0); + QVERIFY2(results[2].isValid(), "Expected SUCCESS for register 40010"); + QCOMPARE(results[2].value(), 9.0); + + client.stopSession(); +} + +void TestDummyAdapter::multipleReadCyclesAllSucceed() +{ + auto* process = new AdapterProcess(); + AdapterClient client(process); + + const QStringList registers = { "${40001}", "${40005}" }; + + connect(&client, &AdapterClient::describeResult, &client, + [&client, ®isters]() { client.provideConfig(realConfig(), registers); }); + + QSignalSpy spyStarted(&client, &AdapterClient::sessionStarted); + client.prepareAdapter(QString::fromUtf8(DUMMY_ADAPTER_EXECUTABLE)); + QVERIFY2(spyStarted.wait(cSessionTimeoutMs), "sessionStarted not emitted"); + + QSignalSpy spyData(&client, &AdapterClient::readDataResult); + + constexpr int cReadCycles = 3; + for (int cycle = 0; cycle < cReadCycles; ++cycle) + { + client.requestReadData(); + QVERIFY2(spyData.wait(cReadTimeoutMs), "readDataResult not emitted"); + + const auto results = spyData.takeLast().at(0).value(); + QCOMPARE(results.size(), registers.size()); + // The dummymodbusadapter stores the 0-based register offset as the value. + QVERIFY2(results[0].isValid(), "Expected SUCCESS for register 40001 on each cycle"); + QCOMPARE(results[0].value(), 0.0); + QVERIFY2(results[1].isValid(), "Expected SUCCESS for register 40005 on each cycle"); + QCOMPARE(results[1].value(), 4.0); + } + + client.stopSession(); +} + +void TestDummyAdapter::stopSessionEmitsSessionStopped() +{ + auto* process = new AdapterProcess(); + AdapterClient client(process); + + connect(&client, &AdapterClient::describeResult, &client, + [&client]() { client.provideConfig(realConfig(), QStringList()); }); + + QSignalSpy spyStarted(&client, &AdapterClient::sessionStarted); + client.prepareAdapter(QString::fromUtf8(DUMMY_ADAPTER_EXECUTABLE)); + QVERIFY2(spyStarted.wait(cSessionTimeoutMs), "sessionStarted not emitted"); + + QSignalSpy spyStopped(&client, &AdapterClient::sessionStopped); + client.stopSession(); + QVERIFY2(spyStopped.wait(cSessionTimeoutMs), "sessionStopped not emitted after stopSession"); +} + QTEST_GUILESS_MAIN(TestDummyAdapter) diff --git a/tests/integration/tst_dummyadapter.h b/tests/integration/tst_dummyadapter.h index 264c8a9e..6c7388be 100644 --- a/tests/integration/tst_dummyadapter.h +++ b/tests/integration/tst_dummyadapter.h @@ -16,6 +16,9 @@ private slots: void describeReturnsRequiredFields(); void describeNameIsModbusAdapter(); void fullLifecycleSessionStarts(); + void readRegistersReturnsValidData(); + void multipleReadCyclesAllSucceed(); + void stopSessionEmitsSessionStopped(); }; #endif // TST_DUMMYADAPTER_H