Patrick Schurig 955016cc47 sunspec: fix a dangling capture in the device suite and widen it
The suite captured a stack local of initTestCase() by reference in two
lambdas that outlive it. Once the function returned they wrote into a
stack slot reused by later frames, corrupting the running test: a
QSignalSpy reported a phantom emission, reading its recorded arguments
segfaulted, and the count changed when unrelated locals were added. Both
observed failures came from that, not from the plugin. The two flags are
members now.

Covers the two remaining claims the offline suites cannot reach: a meter
slave ID of 0 opens no second connection against a real device, and
losing the meter connection leaves the parent connected and the inverter
data flowing, marking only the meter child disconnected.

That last test drives one explicit block read first. This bench replaces
the plugin timer with a null object, on purpose, so nothing would
otherwise ever mark a child connected and the cut would prove nothing.

Run against the bench simulator: 8 tests, 8 green. Each of the two
corrections was confirmed able to fail by reintroducing its pre-F3 form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 09:51:10 +02:00

585 lines
24 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2026, ETM-PowerSync
*
* This file is part of nymea.
*
* nymea is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* nymea 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* F3 face a un vrai appareil.
*
* Les deux defauts les plus certains du multi unit ID sont invisibles pour les
* suites hors ligne, parce qu'ils n'apparaissent qu'une fois la decouverte
* reellement passee :
*
* - le balayage des orphelins, qui sur une seule des deux connexions ferait
* disparaitre les enfants de l'autre a chaque decouverte ;
* - l'agregation de l'etat connected entre les deux connexions.
*
* Cette suite n'est pas lancee par "make check" : elle exige un appareil. Voir
* integration.pro pour les variables d'environnement. Sans SUNSPEC_TEST_HOST,
* tout se met en QSKIP.
*
* Aucune adresse par defaut n'est ecrite ici. L'adresse d'une installation
* reelle est une donnee client (AGENTS.md, "Miroir public").
*/
#include <QtTest>
#include <QCoreApplication>
#include <QTemporaryDir>
#include <nymeasettings.h>
#include <hardwaremanager.h>
#include <plugintimer.h>
#include <logging/logengine.h>
#include <logging/logger.h>
#include <network/networkdevicediscovery.h>
#include <network/networkdevicemonitor.h>
#include <integrations/thingmanagerimplementation.h>
#include <integrations/thingsetupinfo.h>
#include <sunspecconnection.h>
#include "integrationpluginsunspec.h"
/* --------------------------------------------------------------------------
* Doublures
* -------------------------------------------------------------------------- */
/* Contrairement aux suites hors ligne, le moniteur porte ici une vraie adresse
* et se declare joignable : c'est ce qui autorise le plugin a se connecter. */
class DeviceMonitor : public NetworkDeviceMonitor
{
public:
DeviceMonitor(const QHostAddress &address, QObject *parent = nullptr)
: NetworkDeviceMonitor(parent), m_address(address)
{
m_info = NetworkDeviceInfo(address);
m_info.setMonitorMode(NetworkDeviceInfo::MonitorModeIp);
}
MacAddress macAddress() const override { return MacAddress(); }
QString hostName() const override { return QString(); }
QHostAddress address() const override { return m_address; }
NetworkDeviceInfo::MonitorMode monitorMode() const override { return NetworkDeviceInfo::MonitorModeIp; }
NetworkDeviceInfo networkDeviceInfo() const override { return m_info; }
bool reachable() const override { return true; }
QDateTime lastSeen() const override { return QDateTime::currentDateTime(); }
uint pingRetries() const override { return 0; }
void setPingRetries(uint) override {}
private:
QHostAddress m_address;
NetworkDeviceInfo m_info;
};
class DeviceDiscovery : public NetworkDeviceDiscovery
{
public:
DeviceDiscovery(const QHostAddress &address, QObject *parent = nullptr)
: NetworkDeviceDiscovery(parent), m_address(address) {}
NetworkDeviceDiscoveryReply *discover() override { return nullptr; }
bool running() const override { return false; }
NetworkDeviceMonitor *registerMonitor(Thing *) override { return new DeviceMonitor(m_address, this); }
void unregisterMonitor(NetworkDeviceMonitor *monitor) override { delete monitor; }
PingReply *ping(const QHostAddress &, uint) override { return nullptr; }
PingReply *ping(const QString &, uint) override { return nullptr; }
MacAddressDatabaseReply *lookupMacAddress(const QString &) override { return nullptr; }
MacAddressDatabaseReply *lookupMacAddress(const MacAddress &) override { return nullptr; }
bool sendArpRequest(const QHostAddress &) override { return false; }
NetworkDeviceInfos cache() const override { return NetworkDeviceInfos(); }
bool available() const override { return true; }
bool enabled() const override { return true; }
protected:
void setEnabled(bool) override {}
private:
QHostAddress m_address;
};
class NullPluginTimer : public PluginTimer
{
public:
explicit NullPluginTimer(int seconds, QObject *parent = nullptr)
: PluginTimer(parent), m_interval(seconds) {}
int interval() const override { return m_interval; }
int currentTick() const override { return 0; }
bool running() const override { return m_running; }
void reset() override {}
void start() override { m_running = true; }
void stop() override { m_running = false; }
void pause() override {}
void resume() override {}
private:
int m_interval = 0;
bool m_running = false;
};
/* Le minuteur n'est jamais declenche par cette suite. Interroger en boucle un
* onduleur client est precisement ce que le plancher de cadence cherche a
* eviter ; ici on ne veut que la decouverte. */
class NullPluginTimerManager : public PluginTimerManager
{
public:
explicit NullPluginTimerManager(QObject *parent = nullptr) : PluginTimerManager(parent) {}
PluginTimer *registerTimer(int seconds) override { return new NullPluginTimer(seconds, this); }
void unregisterTimer(PluginTimer *timer) override { delete timer; }
bool available() const override { return true; }
bool enabled() const override { return true; }
protected:
void setEnabled(bool) override {}
};
/* Les things enfants sont crees par le plugin, pas ecrits d'avance dans les
* reglages : on ne peut donc pas leur retirer leurs states journalises comme le
* font les suites hors ligne. Il faut un LogEngine, meme creux, sans quoi le
* ThingManager en dereference un absent des la creation du premier enfant. */
class NullLogEngine : public LogEngine
{
public:
explicit NullLogEngine(QObject *parent = nullptr) : LogEngine(parent) {}
Logger *registerLogSource(const QString &name, const QStringList &tags,
Types::LoggingType loggingType, const QString &) override
{
Logger *logger = createLogger(name, tags, loggingType);
m_loggers.insert(name, logger);
return logger;
}
void unregisterLogSource(const QString &name) override { m_loggers.remove(name); }
LogFetchJob *fetchLogEntries(const QStringList &, const QStringList &, const QDateTime &,
const QDateTime &, const QVariantMap &, Types::SampleRate,
Qt::SortOrder, int, int) override
{
LogFetchJob *job = new LogFetchJob(this);
finishFetchJob(job, LogEntries());
return job;
}
bool jobsRunning() const override { return false; }
void clear(const QString &) override {}
void enable() override {}
void disable() override {}
private:
void logEvent(Logger *, const QStringList &, const QVariantMap &) override {}
QHash<QString, Logger *> m_loggers;
};
class DeviceHardwareManager : public HardwareManager
{
public:
DeviceHardwareManager(const QHostAddress &address, QObject *parent = nullptr)
: HardwareManager(parent)
, m_pluginTimerManager(new NullPluginTimerManager(this))
, m_networkDeviceDiscovery(new DeviceDiscovery(address, this))
{}
Radio433 *radio433() override { return nullptr; }
PluginTimerManager *pluginTimerManager() override { return m_pluginTimerManager; }
NetworkAccessManager *networkManager() override { return nullptr; }
UpnpDiscovery *upnpDiscovery() override { return nullptr; }
PlatformZeroConfController *zeroConfController() override { return nullptr; }
BluetoothLowEnergyManager *bluetoothLowEnergyManager() override { return nullptr; }
MqttProvider *mqttProvider() override { return nullptr; }
I2CManager *i2cManager() override { return nullptr; }
ZigbeeHardwareResource *zigbeeResource() override { return nullptr; }
ZWaveHardwareResource *zwaveResource() override { return nullptr; }
ModbusRtuHardwareResource *modbusRtuResource() override { return nullptr; }
NetworkDeviceDiscovery *networkDeviceDiscovery() override { return m_networkDeviceDiscovery; }
private:
NullPluginTimerManager *m_pluginTimerManager = nullptr;
DeviceDiscovery *m_networkDeviceDiscovery = nullptr;
};
class TestableSunSpecPlugin : public IntegrationPluginSunSpec
{
public:
using IntegrationPlugin::setMetaData;
};
/* -------------------------------------------------------------------------- */
class SunSpecDeviceTest : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void test_F3_both_connections_reach_the_device();
void test_F3_meter_connects_only_after_the_primary_discovery();
void test_F3_inverter_and_meter_children_share_one_parent();
void test_F3_no_child_disappears_across_repeated_discoveries();
void test_F3_meter_slave_id_zero_opens_no_second_connection();
void test_F3_losing_the_meter_leaves_the_inverter_alone();
private:
QTemporaryDir m_pluginSearchDir;
QHostAddress m_address;
int m_port = 502;
int m_slaveId = 1;
int m_meterSlaveId = 200;
DeviceHardwareManager *m_hardwareManager = nullptr;
ThingManagerImplementation *m_thingManager = nullptr;
NullLogEngine *m_logEngine = nullptr;
TestableSunSpecPlugin *m_plugin = nullptr;
bool m_configured = false;
/* Ces deux temoins sont ecrits par des lambdas connectees dans
* initTestCase() et relues par un test ulterieur : ce sont donc des membres,
* jamais des locales capturees par reference. Une locale de initTestCase()
* capturee par reference serait relachee au retour de la fonction, et les
* lambdas ecriraient ensuite dans une case de pile reutilisee par les frames
* suivantes - corruption silencieuse du test lui-meme. */
bool m_primaryDiscovered = false;
/* Vrai des que la connexion compteur signale connected, pour verifier que
* cela n'arrive jamais avant la fin de la decouverte primaire. */
bool m_meterConnectedBeforePrimaryDiscovery = false;
bool skipUnlessConfigured();
QList<Thing *> childrenOfKind(const ThingClassId &thingClassId) const;
void storeConnection(const ThingId &thingId, const QString &name,
const PluginId &pluginId, int meterSlaveId);
SunSpecModel *modelOn(SunSpecConnection *connection, quint16 modelId) const;
};
static const ThingId DeviceThingId = ThingId("5d3f8a26-41bc-4e79-a05f-9b7c2e6d1483");
static const ThingId DeviceNoMeterThingId = ThingId("6a4e1b07-52d8-4c93-8e7b-1f39c04a5d62");
void SunSpecDeviceTest::storeConnection(const ThingId &thingId, const QString &name,
const PluginId &pluginId, int meterSlaveId)
{
NymeaSettings settings(NymeaSettings::SettingsRoleThings);
settings.beginGroup("ThingConfig");
settings.beginGroup(thingId.toString());
settings.setValue("thingName", name);
settings.setValue("pluginid", pluginId.toString());
settings.setValue("thingClassId", froniusConnectionThingClassId.toString());
settings.beginGroup("Params");
settings.setValue(froniusConnectionThingAddressParamTypeId.toString(), m_address.toString());
settings.setValue(froniusConnectionThingPortParamTypeId.toString(), m_port);
settings.setValue(froniusConnectionThingSlaveIdParamTypeId.toString(), m_slaveId);
settings.setValue(froniusConnectionThingMeterSlaveIdParamTypeId.toString(), meterSlaveId);
settings.endGroup();
settings.endGroup();
settings.endGroup();
settings.sync();
}
bool SunSpecDeviceTest::skipUnlessConfigured()
{
if (m_configured)
return false;
/* QSKIP ne peut pas etre appele depuis une fonction auxiliaire, donc le
* message est laisse a l'appelant. */
return true;
}
QList<Thing *> SunSpecDeviceTest::childrenOfKind(const ThingClassId &thingClassId) const
{
QList<Thing *> found;
foreach (Thing *thing, m_thingManager->configuredThings()) {
if (thing->thingClassId() == thingClassId && thing->parentId() == DeviceThingId)
found.append(thing);
}
return found;
}
void SunSpecDeviceTest::initTestCase()
{
const QByteArray host = qgetenv("SUNSPEC_TEST_HOST");
if (host.isEmpty()) {
qInfo() << "SUNSPEC_TEST_HOST is not set: every test will be skipped.";
return;
}
m_address = QHostAddress(QString::fromUtf8(host));
QVERIFY2(!m_address.isNull(), "SUNSPEC_TEST_HOST is not a valid address");
if (!qgetenv("SUNSPEC_TEST_PORT").isEmpty())
m_port = qgetenv("SUNSPEC_TEST_PORT").toInt();
if (!qgetenv("SUNSPEC_TEST_SLAVE_ID").isEmpty())
m_slaveId = qgetenv("SUNSPEC_TEST_SLAVE_ID").toInt();
if (!qgetenv("SUNSPEC_TEST_METER_SLAVE_ID").isEmpty())
m_meterSlaveId = qgetenv("SUNSPEC_TEST_METER_SLAVE_ID").toInt();
QCoreApplication::setOrganizationName("nymea-test-sunspec-device");
NymeaSettings thingSettings(NymeaSettings::SettingsRoleThings);
thingSettings.clear();
NymeaSettings pluginSettings(NymeaSettings::SettingsRolePlugins);
pluginSettings.clear();
QVERIFY(m_pluginSearchDir.isValid());
qputenv("NYMEA_PLUGINS_PATH", m_pluginSearchDir.path().toUtf8());
QFile jsonFile(PLUGIN_JSON_PATH);
QVERIFY2(jsonFile.open(QIODevice::ReadOnly), qPrintable(QString("Cannot open %1").arg(PLUGIN_JSON_PATH)));
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(jsonFile.readAll(), &error);
QVERIFY2(error.error == QJsonParseError::NoError, qPrintable(error.errorString()));
const PluginMetadata metadata(document.object());
QVERIFY2(metadata.isValid(), "metadonnee du plugin invalide");
storeConnection(DeviceThingId, "Fronius", metadata.pluginId(), m_meterSlaveId);
/* Le cas degenere documente au contrat : 0 = pas de compteur derriere cet
* onduleur. Meme appareil, meme unit ID onduleur, mais aucune seconde
* connexion ne doit etre ouverte. */
storeConnection(DeviceNoMeterThingId, "Fronius sans compteur", metadata.pluginId(), 0);
m_hardwareManager = new DeviceHardwareManager(m_address, this);
m_logEngine = new NullLogEngine(this);
m_thingManager = new ThingManagerImplementation(m_hardwareManager, m_logEngine, QLocale(), this);
m_plugin = new TestableSunSpecPlugin();
m_plugin->setMetaData(metadata);
m_thingManager->registerStaticPlugin(m_plugin);
Thing *thing = nullptr;
QTRY_VERIFY_WITH_TIMEOUT((thing = m_thingManager->findConfiguredThing(DeviceThingId)) != nullptr, 5000);
QTRY_COMPARE_WITH_TIMEOUT(thing->setupStatus(), Thing::ThingSetupStatusComplete, 30000);
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
QVERIFY(primary);
QVERIFY2(meter, "aucune connexion compteur creee : verifier SUNSPEC_TEST_METER_SLAVE_ID");
/* Arme avant toute attente, pour que l'ordre des deux evenements soit
* observe et pas reconstitue apres coup. */
connect(primary, &SunSpecConnection::discoveryFinished, this, [this](bool success) {
if (success)
m_primaryDiscovered = true;
});
connect(meter, &SunSpecConnection::connectedChanged, this, [this](bool connected) {
if (connected && !m_primaryDiscovered)
m_meterConnectedBeforePrimaryDiscovery = true;
});
m_configured = true;
}
void SunSpecDeviceTest::cleanupTestCase()
{
delete m_thingManager;
m_thingManager = nullptr;
}
void SunSpecDeviceTest::test_F3_both_connections_reach_the_device()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
QVERIFY(primary && meter);
QTRY_VERIFY_WITH_TIMEOUT(primary->connected(), 30000);
QTRY_VERIFY_WITH_TIMEOUT(meter->connected(), 60000);
QCOMPARE(static_cast<int>(primary->slaveId()), m_slaveId);
QCOMPARE(static_cast<int>(meter->slaveId()), m_meterSlaveId);
// Les deux ont trouve un registre de base SunSpec, chacune sur son unit ID.
QTRY_VERIFY_WITH_TIMEOUT(!primary->models().isEmpty(), 60000);
QTRY_VERIFY_WITH_TIMEOUT(!meter->models().isEmpty(), 60000);
}
/* Q4. Deux decouvertes SunSpec simultanees seraient la charge la plus lourde
* que ce plugin fasse jamais subir a un onduleur. */
void SunSpecDeviceTest::test_F3_meter_connects_only_after_the_primary_discovery()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
QVERIFY2(!m_meterConnectedBeforePrimaryDiscovery,
"la connexion compteur est montee avant la fin de la decouverte primaire");
}
/* Un seul appareil a appairer, tous les enfants sous le meme parent. */
void SunSpecDeviceTest::test_F3_inverter_and_meter_children_share_one_parent()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
QTRY_VERIFY_WITH_TIMEOUT(!childrenOfKind(sunspecThreePhaseInverterThingClassId).isEmpty()
|| !childrenOfKind(sunspecSplitPhaseInverterThingClassId).isEmpty()
|| !childrenOfKind(sunspecSinglePhaseInverterThingClassId).isEmpty(), 60000);
QTRY_VERIFY_WITH_TIMEOUT(!childrenOfKind(sunspecThreePhaseMeterThingClassId).isEmpty()
|| !childrenOfKind(sunspecSplitPhaseMeterThingClassId).isEmpty()
|| !childrenOfKind(sunspecSinglePhaseMeterThingClassId).isEmpty(), 60000);
}
/* Le defaut le plus certain du multi unit ID : chaque connexion ne voit que la
* moitie du tableau, donc un balayage des orphelins fait sur cette moitie ferait
* disparaitre les enfants de l'autre. Une redecouverte suffit a le declencher. */
void SunSpecDeviceTest::test_F3_no_child_disappears_across_repeated_discoveries()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
const int childrenBefore = m_thingManager->configuredThings().filterByParentId(DeviceThingId).count();
QVERIFY2(childrenBefore > 0, "aucun enfant a surveiller");
QSignalSpy disappeared(m_plugin, &IntegrationPlugin::autoThingDisappeared);
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
for (int round = 0; round < 2; round++) {
QSignalSpy primaryDone(primary, &SunSpecConnection::discoveryFinished);
primary->startDiscovery();
QVERIFY2(primaryDone.wait(60000), "la decouverte primaire n'a pas rendu la main");
QSignalSpy meterDone(meter, &SunSpecConnection::discoveryFinished);
meter->startDiscovery();
QVERIFY2(meterDone.wait(60000), "la decouverte compteur n'a pas rendu la main");
}
QCOMPARE(disappeared.count(), 0);
QCOMPARE(m_thingManager->configuredThings().filterByParentId(DeviceThingId).count(), childrenBefore);
}
SunSpecModel *SunSpecDeviceTest::modelOn(SunSpecConnection *connection, quint16 modelId) const
{
foreach (SunSpecModel *model, connection->models()) {
if (model->modelId() == modelId)
return model;
}
return nullptr;
}
/* Le cas degenere : meterSlaveId a 0 veut dire "pas de compteur derriere cet
* onduleur". Meme appareil, meme unit ID onduleur, mais une seule connexion.
*
* On n'affirme rien sur les enfants de ce second thing : le premier a deja cree
* un enfant onduleur portant le numero de serie du simulateur, et la
* deduplication par numero de serie de autocreateSunSpecModelThing() est
* globale, pas par parent. C'est la connexion qui est mesuree ici. */
void SunSpecDeviceTest::test_F3_meter_slave_id_zero_opens_no_second_connection()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
Thing *thing = nullptr;
QTRY_VERIFY_WITH_TIMEOUT((thing = m_thingManager->findConfiguredThing(DeviceNoMeterThingId)) != nullptr, 5000);
QTRY_COMPARE_WITH_TIMEOUT(thing->setupStatus(), Thing::ThingSetupStatusComplete, 30000);
QVERIFY2(m_plugin->testPrimaryConnection(DeviceNoMeterThingId),
"la connexion onduleur n'a pas ete creee");
QVERIFY2(!m_plugin->testMeterConnection(DeviceNoMeterThingId),
"meterSlaveId a 0 ne doit ouvrir aucune connexion compteur");
}
/* Le second defaut invisible hors ligne : l'etat connected du parent suit la
* connexion primaire seule. Un Smart Meter qui tombe ne doit ni marquer
* l'onduleur injoignable, ni effacer les donnees qu'il continue de livrer.
*
* Place en dernier, parce qu'il coupe une connexion pour de bon. */
void SunSpecDeviceTest::test_F3_losing_the_meter_leaves_the_inverter_alone()
{
if (skipUnlessConfigured())
QSKIP("SUNSPEC_TEST_HOST is not set");
Thing *parent = m_thingManager->findConfiguredThing(DeviceThingId);
QVERIFY(parent);
QVERIFY2(parent->stateValue("connected").toBool(), "le parent n'etait pas connecte au depart");
QList<Thing *> inverters = childrenOfKind(sunspecThreePhaseInverterThingClassId);
QList<Thing *> meters = childrenOfKind(sunspecThreePhaseMeterThingClassId);
QVERIFY2(!inverters.isEmpty(), "pas d'enfant onduleur a surveiller");
QVERIFY2(!meters.isEmpty(), "pas d'enfant compteur a couper");
Thing *inverterThing = inverters.first();
Thing *meterThing = meters.first();
SunSpecConnection *primary = m_plugin->testPrimaryConnection(DeviceThingId);
SunSpecConnection *meter = m_plugin->testMeterConnection(DeviceThingId);
QVERIFY(primary && meter);
SunSpecModel *inverterModel = modelOn(primary, 113);
SunSpecModel *meterModel = modelOn(meter, 213);
QVERIFY2(inverterModel, "modele onduleur 113 introuvable sur la connexion primaire");
QVERIFY2(meterModel, "modele compteur 213 introuvable sur la connexion compteur");
/* Un tour d'interrogation explicite. Ce banc remplace le minuteur du plugin
* par un objet creux - on ne veut pas interroger un appareil en boucle - or
* c'est la mise a jour de bloc qui met un enfant a connected. Sans ce tour,
* les enfants n'auraient jamais ete connectes et la coupure ci-dessous ne
* prouverait rien. */
QSignalSpy inverterFirst(inverterModel, &SunSpecModel::blockUpdated);
QSignalSpy meterFirst(meterModel, &SunSpecModel::blockUpdated);
inverterModel->readBlockData();
meterModel->readBlockData();
QVERIFY2(inverterFirst.wait(30000), "pas de donnee onduleur avant la coupure");
if (meterFirst.isEmpty())
QVERIFY2(meterFirst.wait(30000), "pas de donnee compteur avant la coupure");
QVERIFY2(inverterThing->stateValue("connected").toBool(),
"l'enfant onduleur n'etait pas connecte avant la coupure");
QVERIFY2(meterThing->stateValue("connected").toBool(),
"l'enfant compteur n'etait pas connecte avant la coupure");
// Coupure franche de la seule connexion compteur.
meter->disconnectDevice();
QTRY_VERIFY_WITH_TIMEOUT(!meter->connected(), 15000);
// Le parent reste connecte : c'est la primaire qui fait foi, jamais un ET logique.
QVERIFY2(parent->stateValue("connected").toBool(),
"le parent est passe deconnecte alors que seule la connexion compteur est tombee");
QVERIFY(primary->connected());
// L'enfant onduleur n'est pas affecte...
QVERIFY2(inverterThing->stateValue("connected").toBool(),
"l'enfant onduleur a ete marque deconnecte par la chute du compteur");
QVERIFY2(inverterThing->stateValue("currentPower").isValid(),
"les donnees onduleur ont ete effacees");
// ...et ses donnees continuent d'arriver.
QSignalSpy inverterUpdated(inverterModel, &SunSpecModel::blockUpdated);
inverterModel->readBlockData();
QVERIFY2(inverterUpdated.wait(30000),
"plus aucune donnee onduleur apres la chute du compteur");
// Seul l'enfant compteur est marque deconnecte.
QVERIFY2(!meterThing->stateValue("connected").toBool(),
"l'enfant compteur aurait du etre marque deconnecte");
}
QTEST_MAIN(SunSpecDeviceTest)
#include "tst_sunspecdevice.moc"