From 7597c87e58895819c5671a9899a2657a5f89d395 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Fri, 16 Oct 2026 22:06:17 +0200 Subject: [PATCH] =?UTF-8?q?test(optimiseur):=20faux=20optimiseur=20de=20T2?= =?UTF-8?q?=20extrait=20dans=20un=20en-t=C3=AAte=20partag=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le FakeOptimizer (QLocalServer/QTcpServer du processus), waitFor(), settle() et handshakeResult() quittent testoptimizerclient.cpp pour tests/auto/common/etm-powersync-fakeoptimizer.h, sans changement de comportement, afin que la suite socketscheduler (brief 4 T3) les réutilise. Seul ajout : un répondeur optionnel (`responder`) qui consomme les messages reçus au lieu de les filer — vide par défaut, la suite optimizerclient ne l'utilise pas. optimizerclient : 16 fonctions, 67 passés, 0 échec (inchangé). Co-Authored-By: Claude Opus 5.5 --- .../auto/common/etm-powersync-fakeoptimizer.h | 164 ++++++++++++++++++ .../auto/optimizerclient/optimizerclient.pro | 3 +- .../optimizerclient/testoptimizerclient.cpp | 139 +-------------- 3 files changed, 167 insertions(+), 139 deletions(-) create mode 100644 tests/auto/common/etm-powersync-fakeoptimizer.h diff --git a/tests/auto/common/etm-powersync-fakeoptimizer.h b/tests/auto/common/etm-powersync-fakeoptimizer.h new file mode 100644 index 0000000..9fd925f --- /dev/null +++ b/tests/auto/common/etm-powersync-fakeoptimizer.h @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +// +// Faux optimiseur dans le processus de test (QLocalServer / QTcpServer) — extrait de la suite +// `optimizerclient` (brief 4 T2) pour être partagé avec `socketscheduler` (brief 4 T3). +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Attente d'une condition d'E/S. L'horloge murale ne borne QUE l'attente du test, jamais un +// délai du client. +template +bool waitFor(F condition, int timeoutMs = 5000) +{ + QElapsedTimer t; + t.start(); + while (!condition()) { + if (t.elapsed() > timeoutMs) + return false; + QCoreApplication::processEvents(QEventLoop::AllEvents, 10); + } + return true; +} + +// Laisse passer les événements en attente sans rien attendre de précis : sert à constater +// qu'il NE se passe rien. +inline void settle(int ms = 50) +{ + QElapsedTimer t; + t.start(); + while (t.elapsed() < ms) + QCoreApplication::processEvents(QEventLoop::AllEvents, 5); +} + + +/*! Faux optimiseur : accepte une connexion à la fois, découpe ce qu'il reçoit en lignes JSON, + * et répond ce que le test lui dicte. */ +class FakeOptimizer : public QObject +{ + Q_OBJECT +public: + enum Transport { Unix, Tcp }; + + FakeOptimizer(Transport transport, const QString &dir) : m_transport(transport), m_dir(dir) {} + + bool listen() + { + if (m_transport == Unix) { + m_local = new QLocalServer(this); + QLocalServer::removeServer(socketPath()); + connect(m_local, &QLocalServer::newConnection, this, [this] { + while (QLocalSocket *s = m_local->nextPendingConnection()) + adopt(s); + }); + return m_local->listen(socketPath()); + } + m_tcp = new QTcpServer(this); + connect(m_tcp, &QTcpServer::newConnection, this, [this] { + while (QTcpSocket *s = m_tcp->nextPendingConnection()) + adopt(s); + }); + // Port déjà choisi lors d'une écoute précédente : on le reprend (reconnexion). + return m_tcp->listen(QHostAddress::LocalHost, m_port) && (m_port = m_tcp->serverPort()) != 0; + } + + QString socketPath() const { return m_dir + QStringLiteral("/optimizer.sock"); } + + QString endpoint() const + { + if (m_transport == Unix) + return QStringLiteral("unix://") + socketPath(); + return QStringLiteral("tcp://127.0.0.1:%1").arg(m_port); + } + + bool waitMessage(QJsonObject *out, int timeoutMs = 5000) + { + if (!waitFor([this] { return !m_messages.isEmpty(); }, timeoutMs)) + return false; + *out = m_messages.takeFirst(); + return true; + } + + void sendRaw(const QByteArray &bytes) + { + QVERIFY(m_client); + m_client->write(bytes); + if (auto *tcp = qobject_cast(m_client.data())) + tcp->flush(); + else if (auto *local = qobject_cast(m_client.data())) + local->flush(); + } + + void sendJson(const QJsonObject &o) { sendRaw(QJsonDocument(o).toJson(QJsonDocument::Compact) + '\n'); } + + void replyResult(qint64 id, const QJsonObject &result) + { + sendJson(QJsonObject{{"jsonrpc", "2.0"}, {"id", id}, {"result", result}}); + } + + void abortClient() + { + if (auto *tcp = qobject_cast(m_client.data())) + tcp->abort(); + else if (auto *local = qobject_cast(m_client.data())) + local->abort(); + } + + int connections = 0; + int disconnections = 0; + //! Répondeur automatique (brief 4 T3) : appelé sur chaque message reçu ; s'il rend vrai, le + //! message est consommé et n'entre pas dans la file de waitMessage(). Vide = tout est filé. + std::function responder; + +private: + void adopt(QIODevice *s) + { + ++connections; + m_client = s; + m_buffer.clear(); + connect(s, &QIODevice::readyRead, this, [this, s] { + m_buffer.append(s->readAll()); + int nl; + while ((nl = m_buffer.indexOf('\n')) >= 0) { + const QJsonObject message = QJsonDocument::fromJson(m_buffer.left(nl)).object(); + m_buffer.remove(0, nl + 1); + if (!(responder && responder(message))) + m_messages.append(message); + } + }); + if (auto *tcp = qobject_cast(s)) + connect(tcp, &QTcpSocket::disconnected, this, [this] { ++disconnections; }); + else if (auto *local = qobject_cast(s)) + connect(local, &QLocalSocket::disconnected, this, [this] { ++disconnections; }); + } + + Transport m_transport; + QString m_dir; + quint16 m_port = 0; + QLocalServer *m_local = nullptr; + QTcpServer *m_tcp = nullptr; + QPointer m_client; + QByteArray m_buffer; + QList m_messages; +}; + +inline QJsonObject handshakeResult(const QString &version = QStringLiteral("1.1"), bool withCapabilities = true) +{ + QJsonObject r{{"protocolVersion", version}, {"name", "faux-optimiseur"}, {"planning", true}}; + if (withCapabilities) + r.insert("capabilities", QJsonObject{{"maxHorizonH", 48}, {"kinds", QJsonArray{"setpoint", "state"}}}); + return r; +} diff --git a/tests/auto/optimizerclient/optimizerclient.pro b/tests/auto/optimizerclient/optimizerclient.pro index be1bd49..0598f27 100644 --- a/tests/auto/optimizerclient/optimizerclient.pro +++ b/tests/auto/optimizerclient/optimizerclient.pro @@ -13,7 +13,8 @@ TARGET = etm-powersync-optimizerclient INCLUDEPATH += $$PWD/../../../energyplugin -HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h +HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h \ + $$PWD/../common/etm-powersync-fakeoptimizer.h SOURCES += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.cpp \ testoptimizerclient.cpp diff --git a/tests/auto/optimizerclient/testoptimizerclient.cpp b/tests/auto/optimizerclient/testoptimizerclient.cpp index 3a15223..6512df8 100644 --- a/tests/auto/optimizerclient/testoptimizerclient.cpp +++ b/tests/auto/optimizerclient/testoptimizerclient.cpp @@ -6,6 +6,7 @@ // des sockets, jamais à laisser s'écouler un délai du client. #include "etm/scheduler/optimizerclient.h" +#include "../common/etm-powersync-fakeoptimizer.h" #include #include @@ -50,31 +51,6 @@ int countLog(const QString &needle) return n; } -// Attente d'une condition d'E/S. L'horloge murale ne borne QUE l'attente du test, jamais un -// délai du client. -template -bool waitFor(F condition, int timeoutMs = 5000) -{ - QElapsedTimer t; - t.start(); - while (!condition()) { - if (t.elapsed() > timeoutMs) - return false; - QCoreApplication::processEvents(QEventLoop::AllEvents, 10); - } - return true; -} - -// Laisse passer les événements en attente sans rien attendre de précis : sert à constater -// qu'il NE se passe rien. -void settle(int ms = 50) -{ - QElapsedTimer t; - t.start(); - while (t.elapsed() < ms) - QCoreApplication::processEvents(QEventLoop::AllEvents, 5); -} - } // namespace /*! Horloge simulée : le temps n'avance que par advance(), et les réveils échus sonnent dans @@ -106,119 +82,6 @@ private: qint64 m_deadline = -1; }; -/*! Faux optimiseur : accepte une connexion à la fois, découpe ce qu'il reçoit en lignes JSON, - * et répond ce que le test lui dicte. */ -class FakeOptimizer : public QObject -{ - Q_OBJECT -public: - enum Transport { Unix, Tcp }; - - FakeOptimizer(Transport transport, const QString &dir) : m_transport(transport), m_dir(dir) {} - - bool listen() - { - if (m_transport == Unix) { - m_local = new QLocalServer(this); - QLocalServer::removeServer(socketPath()); - connect(m_local, &QLocalServer::newConnection, this, [this] { - while (QLocalSocket *s = m_local->nextPendingConnection()) - adopt(s); - }); - return m_local->listen(socketPath()); - } - m_tcp = new QTcpServer(this); - connect(m_tcp, &QTcpServer::newConnection, this, [this] { - while (QTcpSocket *s = m_tcp->nextPendingConnection()) - adopt(s); - }); - // Port déjà choisi lors d'une écoute précédente : on le reprend (reconnexion). - return m_tcp->listen(QHostAddress::LocalHost, m_port) && (m_port = m_tcp->serverPort()) != 0; - } - - QString socketPath() const { return m_dir + QStringLiteral("/optimizer.sock"); } - - QString endpoint() const - { - if (m_transport == Unix) - return QStringLiteral("unix://") + socketPath(); - return QStringLiteral("tcp://127.0.0.1:%1").arg(m_port); - } - - bool waitMessage(QJsonObject *out, int timeoutMs = 5000) - { - if (!waitFor([this] { return !m_messages.isEmpty(); }, timeoutMs)) - return false; - *out = m_messages.takeFirst(); - return true; - } - - void sendRaw(const QByteArray &bytes) - { - QVERIFY(m_client); - m_client->write(bytes); - if (auto *tcp = qobject_cast(m_client.data())) - tcp->flush(); - else if (auto *local = qobject_cast(m_client.data())) - local->flush(); - } - - void sendJson(const QJsonObject &o) { sendRaw(QJsonDocument(o).toJson(QJsonDocument::Compact) + '\n'); } - - void replyResult(qint64 id, const QJsonObject &result) - { - sendJson(QJsonObject{{"jsonrpc", "2.0"}, {"id", id}, {"result", result}}); - } - - void abortClient() - { - if (auto *tcp = qobject_cast(m_client.data())) - tcp->abort(); - else if (auto *local = qobject_cast(m_client.data())) - local->abort(); - } - - int connections = 0; - int disconnections = 0; - -private: - void adopt(QIODevice *s) - { - ++connections; - m_client = s; - m_buffer.clear(); - connect(s, &QIODevice::readyRead, this, [this, s] { - m_buffer.append(s->readAll()); - int nl; - while ((nl = m_buffer.indexOf('\n')) >= 0) { - m_messages.append(QJsonDocument::fromJson(m_buffer.left(nl)).object()); - m_buffer.remove(0, nl + 1); - } - }); - if (auto *tcp = qobject_cast(s)) - connect(tcp, &QTcpSocket::disconnected, this, [this] { ++disconnections; }); - else if (auto *local = qobject_cast(s)) - connect(local, &QLocalSocket::disconnected, this, [this] { ++disconnections; }); - } - - Transport m_transport; - QString m_dir; - quint16 m_port = 0; - QLocalServer *m_local = nullptr; - QTcpServer *m_tcp = nullptr; - QPointer m_client; - QByteArray m_buffer; - QList m_messages; -}; - -static QJsonObject handshakeResult(const QString &version = QStringLiteral("1.1"), bool withCapabilities = true) -{ - QJsonObject r{{"protocolVersion", version}, {"name", "faux-optimiseur"}, {"planning", true}}; - if (withCapabilities) - r.insert("capabilities", QJsonObject{{"maxHorizonH", 48}, {"kinds", QJsonArray{"setpoint", "state"}}}); - return r; -} - class TestOptimizerClient : public QObject { Q_OBJECT