Patrick Schurig 308444f39d test: traces réelles du moteur jugées contre le contrat (refoptimizer)
Les 16 lignes de testTracesReplayWithZeroDivergence : chaque requête, et
chaque réponse de l'optimiseur rejugée sur sa requête (--trace --semantic).

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

115 lines
4.5 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`).
*/
/*! Lance le validateur avec \p arguments (après `--schema`). */
inline Verdict lancer(const QString &schema, const QStringList &arguments)
{
Verdict v;
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} + 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;
}
/*!
* 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 = {})
{
QTemporaryDir dir;
QFile f(dir.filePath(QStringLiteral("session.ndjson")));
if (!dir.isValid() || !f.open(QIODevice::WriteOnly)) {
Verdict v;
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();
return lancer(schema, QStringList{QStringLiteral("--ndjson"), f.fileName()} + arguments);
}
/*! Juge un fichier de traces de décision (annexe D) contre \p schema. */
inline Verdict jugerTraces(const QString &trace, const QString &schema, const QStringList &arguments = {})
{
return lancer(schema, QStringList{QStringLiteral("--trace"), trace} + arguments);
}
/*! É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;
}
}