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>
103 lines
4.0 KiB
C++
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;
|
|
}
|
|
|
|
}
|