Patrick Schurig d95a65cb71 test: suite etm-powersync-protocolschema (contrat == docs == code)
Contrat embarqué identique à docs/ ; listes x-etm-generated égales au code
(ancre côté test, dump pour régénérer) ; chaque code de verdict, d'erreur et
de repli lu dans les SOURCES figure dans les listes ; règles sémantiques ==
verdicts du validateur, même ordre ; sérialiseur GetPlan jugé conforme sur
toutes ses branches par le validateur publié.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:30:44 +02:00

103 lines
4.0 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
//
// Juger des messages du protocole optimiseur contre son contrat OpenRPC (lot « contrat OpenRPC »),
// par le validateur du dépôt : tools/etm-powersync-schema-check, lancé en sous-processus Python.
//
// Un seul validateur, pas deux : un second, écrit en C++ pour les tests, divergerait du premier, et
// c'est la conformité au validateur PUBLIÉ (celui qu'utilise un auteur d'optimiseur) qui compte.
#pragma once
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QList>
#include <QProcess>
#include <QTemporaryDir>
namespace Contrat {
/*! Racine des sources : surchargée par $ETM_POWERSYNC_SOURCE_DIR (arbre mutant, arbre archivé). */
inline QString racineSources()
{
const QByteArray env = qgetenv("ETM_POWERSYNC_SOURCE_DIR");
return env.isEmpty() ? QStringLiteral(ETM_POWERSYNC_SOURCE_DIR) : QString::fromLocal8Bit(env);
}
/*! Le contrat de docs/ — à défaut du document EMBARQUÉ (suites qui n'ont pas le sérialiseur). */
inline QString schemaDuDepot()
{
return racineSources() + QStringLiteral("/docs/etm-powersync-optimizer-protocol.openrpc.json");
}
/*! Verdict du validateur. */
struct Verdict {
int code = -1; //!< 0 conforme, 1 écart, 2 erreur d'usage ou de schéma, -1 pas lancé.
QJsonObject resume; //!< La ligne RÉSUMÉ (comptes, écarts, verdicts).
QString sortie; //!< Tout ce qu'il a écrit — à citer dans le message d'échec.
int compte(const QString &partie) const { return resume.value(QStringLiteral("comptes")).toObject().value(partie).toInt(); }
};
/*!
* Juge une session (un message JSON-RPC par élément, dans l'ordre) contre \p schema.
* \p arguments : options en plus (`--requests-only`, `--semantic`).
*/
inline Verdict juger(const QList<QJsonObject> &messages, const QString &schema, const QStringList &arguments = {})
{
Verdict v;
QTemporaryDir dir;
if (!dir.isValid()) {
v.sortie = QStringLiteral("répertoire temporaire impossible");
return v;
}
QFile f(dir.filePath(QStringLiteral("session.ndjson")));
if (!f.open(QIODevice::WriteOnly)) {
v.sortie = QStringLiteral("écriture de la session impossible");
return v;
}
for (const QJsonObject &m : messages)
f.write(QJsonDocument(m).toJson(QJsonDocument::Compact) + '\n');
f.close();
QProcess p;
p.setProcessChannelMode(QProcess::MergedChannels);
p.start(QStringLiteral("python3"),
QStringList{QStringLiteral("-I"), QStringLiteral("-B"),
racineSources() + QStringLiteral("/tools/etm-powersync-schema-check"),
QStringLiteral("--schema"), schema, QStringLiteral("--ndjson"), f.fileName()}
+ arguments);
if (!p.waitForFinished(60000)) {
p.kill();
v.sortie = QStringLiteral("validateur sans réponse en 60 s");
return v;
}
v.code = p.exitStatus() == QProcess::NormalExit ? p.exitCode() : -1;
v.sortie = QString::fromUtf8(p.readAll());
for (const QString &ligne : v.sortie.split(QLatin1Char('\n')))
if (ligne.startsWith(QStringLiteral("RÉSUMÉ ")))
v.resume = QJsonDocument::fromJson(ligne.mid(7).toUtf8()).object();
return v;
}
/*! Écrit \p octets dans \p dir et rend le chemin : le contrat EMBARQUÉ, passé au validateur. */
inline QString ecrireSchema(const QTemporaryDir &dir, const QByteArray &octets)
{
const QString chemin = dir.filePath(QStringLiteral("contrat-embarque.json"));
QFile f(chemin);
if (f.open(QIODevice::WriteOnly))
f.write(octets);
return chemin;
}
/*! Les messages de \p messages dont la méthode est \p methode. */
inline QList<QJsonObject> deMethode(const QList<QJsonObject> &messages, const QString &methode)
{
QList<QJsonObject> out;
for (const QJsonObject &m : messages)
if (m.value(QStringLiteral("method")).toString() == methode)
out << m;
return out;
}
}