mirror of
https://github.com/logos-co/logos-logoscore-cli.git
synced 2026-08-31 04:41:06 +00:00
replace QJson* with nlohmann/json
This commit is contained in:
+80
-119
@@ -10,12 +10,11 @@
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RpcClient implementation — delegates all calls to daemon's core_service
|
||||
@@ -28,12 +27,10 @@ struct RpcClient::Impl {
|
||||
std::string token;
|
||||
ClientState clientState;
|
||||
|
||||
// Helper: invoke a core_service method and return a QVariant result.
|
||||
// String args must be passed as QString (QVariant has no std::string ctor).
|
||||
template<typename... Args>
|
||||
QVariant invoke(const char* method, Args&&... args) {
|
||||
return coreService->invokeRemoteMethod(
|
||||
QString("core_service"), QString(method), std::forward<Args>(args)...);
|
||||
// Helper: invoke a core_service method via the nlohmann::json overload.
|
||||
nlohmann::json invoke(const std::string& method,
|
||||
const nlohmann::json& args = nlohmann::json::array()) {
|
||||
return coreService->invokeRemoteMethod("core_service", method, args);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -138,138 +135,96 @@ std::string RpcClient::lastError() const
|
||||
// Module lifecycle — delegate to core_service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject RpcClient::loadModule(const std::string& name)
|
||||
LogosMap RpcClient::loadModule(const std::string& name)
|
||||
{
|
||||
QVariant ret = d->invoke("loadModule", QString::fromStdString(name));
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = QString::fromStdString(
|
||||
fmt::format("loadModule('{}') RPC call failed.", name));
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("loadModule", nlohmann::json::array({name}));
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message", fmt::format("loadModule('{}') RPC call failed.", name)}};
|
||||
}
|
||||
|
||||
QJsonObject RpcClient::unloadModule(const std::string& name)
|
||||
LogosMap RpcClient::unloadModule(const std::string& name)
|
||||
{
|
||||
QVariant ret = d->invoke("unloadModule", QString::fromStdString(name));
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = QString::fromStdString(
|
||||
fmt::format("unloadModule('{}') RPC call failed.", name));
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("unloadModule", nlohmann::json::array({name}));
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message", fmt::format("unloadModule('{}') RPC call failed.", name)}};
|
||||
}
|
||||
|
||||
QJsonObject RpcClient::reloadModule(const std::string& name)
|
||||
LogosMap RpcClient::reloadModule(const std::string& name)
|
||||
{
|
||||
QVariant ret = d->invoke("reloadModule", QString::fromStdString(name));
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = QString::fromStdString(
|
||||
fmt::format("reloadModule('{}') RPC call failed.", name));
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("reloadModule", nlohmann::json::array({name}));
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message", fmt::format("reloadModule('{}') RPC call failed.", name)}};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries — delegate to core_service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QJsonArray RpcClient::listModules(const std::string& filter)
|
||||
LogosList RpcClient::listModules(const std::string& filter)
|
||||
{
|
||||
QVariant ret = d->invoke("listModules", QString::fromStdString(filter));
|
||||
if (ret.canConvert<QJsonArray>())
|
||||
return qvariant_cast<QJsonArray>(ret);
|
||||
return {};
|
||||
nlohmann::json ret = d->invoke("listModules", nlohmann::json::array({filter}));
|
||||
if (ret.is_array()) return ret;
|
||||
return LogosList::array();
|
||||
}
|
||||
|
||||
QJsonObject RpcClient::getStatus()
|
||||
LogosMap RpcClient::getStatus()
|
||||
{
|
||||
QVariant ret = d->invoke("getStatus");
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
nlohmann::json ret = d->invoke("getStatus");
|
||||
if (ret.is_object()) return ret;
|
||||
|
||||
QJsonObject status;
|
||||
QJsonObject daemon;
|
||||
daemon["status"] = "not_running";
|
||||
std::string version = QCoreApplication::applicationVersion().toStdString();
|
||||
LogosMap daemon{{"status","not_running"},{"version", version}};
|
||||
if (!d->instanceId.empty())
|
||||
daemon["instance_id"] = QString::fromStdString(d->instanceId);
|
||||
daemon["version"] = QCoreApplication::applicationVersion();
|
||||
status["daemon"] = daemon;
|
||||
status["modules"] = QJsonArray();
|
||||
status["rpc_error"] = "core_service not reachable";
|
||||
return status;
|
||||
daemon["instance_id"] = d->instanceId;
|
||||
return LogosMap{{"daemon", daemon},
|
||||
{"modules", LogosList::array()},
|
||||
{"rpc_error", "core_service not reachable"}};
|
||||
}
|
||||
|
||||
QJsonObject RpcClient::getModuleInfo(const std::string& name)
|
||||
LogosMap RpcClient::getModuleInfo(const std::string& name)
|
||||
{
|
||||
QVariant ret = d->invoke("getModuleInfo", QString::fromStdString(name));
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = QString::fromStdString(
|
||||
fmt::format("getModuleInfo('{}') RPC call failed.", name));
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("getModuleInfo", nlohmann::json::array({name}));
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message", fmt::format("getModuleInfo('{}') RPC call failed.", name)}};
|
||||
}
|
||||
|
||||
QJsonArray RpcClient::getModuleStats()
|
||||
LogosList RpcClient::getModuleStats()
|
||||
{
|
||||
QVariant ret = d->invoke("getModuleStats");
|
||||
if (ret.canConvert<QJsonArray>())
|
||||
return qvariant_cast<QJsonArray>(ret);
|
||||
return {};
|
||||
nlohmann::json ret = d->invoke("getModuleStats");
|
||||
if (ret.is_array()) return ret;
|
||||
return LogosList::array();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proxied call — delegate to core_service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject RpcClient::callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const QVariantList& args)
|
||||
LogosMap RpcClient::callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const LogosList& args)
|
||||
{
|
||||
QVariant ret = d->invoke("callModuleMethod",
|
||||
QString::fromStdString(module),
|
||||
QString::fromStdString(method),
|
||||
args);
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = QString::fromStdString(
|
||||
fmt::format("callModuleMethod('{}', '{}') RPC call failed.", module, method));
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("callModuleMethod",
|
||||
nlohmann::json::array({module, method, args}));
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message", fmt::format("callModuleMethod('{}','{}') RPC call failed.",
|
||||
module, method)}};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Daemon lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QJsonObject RpcClient::shutdown()
|
||||
LogosMap RpcClient::shutdown()
|
||||
{
|
||||
QVariant ret = d->invoke("shutdown");
|
||||
if (ret.canConvert<QJsonObject>())
|
||||
return ret.toJsonObject();
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "error";
|
||||
result["code"] = "RPC_FAILED";
|
||||
result["message"] = "shutdown RPC call failed.";
|
||||
return result;
|
||||
nlohmann::json ret = d->invoke("shutdown");
|
||||
if (ret.is_object()) return ret;
|
||||
return LogosMap{{"status","error"},{"code","RPC_FAILED"},
|
||||
{"message","shutdown RPC call failed."}};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -278,37 +233,43 @@ QJsonObject RpcClient::shutdown()
|
||||
|
||||
bool RpcClient::watchModuleEvents(const std::string& module,
|
||||
const std::string& eventName,
|
||||
std::function<void(const QJsonObject&)> callback)
|
||||
std::function<void(const LogosMap&)> callback)
|
||||
{
|
||||
if (!m_connected)
|
||||
return false;
|
||||
|
||||
QVariant subscribed = d->invoke("watchModuleEvents",
|
||||
QString::fromStdString(module),
|
||||
QString::fromStdString(eventName));
|
||||
if (!subscribed.toBool())
|
||||
nlohmann::json subscribed = d->invoke("watchModuleEvents",
|
||||
nlohmann::json::array({module, eventName}));
|
||||
if (!subscribed.is_boolean() || !subscribed.get<bool>())
|
||||
return false;
|
||||
|
||||
LogosObject* obj = d->coreService->requestObject("core_service");
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
d->coreService->onEvent(obj, "module_event",
|
||||
[module, callback](const QString& event, const QVariantList& data) {
|
||||
Q_UNUSED(event);
|
||||
if (data.size() < 2)
|
||||
d->coreService->onEvent(obj, std::string("module_event"),
|
||||
[module, callback](const std::string& /*event*/, const nlohmann::json& data) {
|
||||
if (!data.is_array() || data.size() < 2)
|
||||
return;
|
||||
if (data.at(0).toString().toStdString() != module)
|
||||
if (!data[0].is_string() || data[0].get<std::string>() != module)
|
||||
return;
|
||||
|
||||
QJsonObject eventObj;
|
||||
eventObj["timestamp"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
|
||||
eventObj["module"] = data.at(0).toString();
|
||||
eventObj["event"] = data.at(1).toString();
|
||||
// Build ISO timestamp without Qt date helpers to avoid Qt includes
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t tt = std::chrono::system_clock::to_time_t(now);
|
||||
struct tm utc{};
|
||||
gmtime_r(&tt, &utc);
|
||||
char tsBuf[32];
|
||||
std::strftime(tsBuf, sizeof(tsBuf), "%Y-%m-%dT%H:%M:%SZ", &utc);
|
||||
|
||||
QJsonObject eventData;
|
||||
for (int i = 2; i < data.size(); ++i)
|
||||
eventData[QString("arg%1").arg(i - 2)] = QJsonValue::fromVariant(data.at(i));
|
||||
LogosMap eventObj;
|
||||
eventObj["timestamp"] = std::string(tsBuf);
|
||||
eventObj["module"] = data[0];
|
||||
eventObj["event"] = data[1];
|
||||
|
||||
LogosMap eventData;
|
||||
for (size_t i = 2; i < data.size(); ++i)
|
||||
eventData["arg" + std::to_string(i - 2)] = data[i];
|
||||
eventObj["data"] = eventData;
|
||||
callback(eventObj);
|
||||
});
|
||||
|
||||
+27
-29
@@ -1,12 +1,10 @@
|
||||
#ifndef CLIENT_H
|
||||
#define CLIENT_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <logos_json.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Abstract client interface for communicating with daemon's core_service.
|
||||
// The real implementation (RpcClient) uses LogosAPIClient from logos-cpp-sdk.
|
||||
@@ -20,28 +18,28 @@ public:
|
||||
virtual std::string lastError() const = 0;
|
||||
|
||||
// Module lifecycle
|
||||
virtual QJsonObject loadModule(const std::string& name) = 0;
|
||||
virtual QJsonObject unloadModule(const std::string& name) = 0;
|
||||
virtual QJsonObject reloadModule(const std::string& name) = 0;
|
||||
virtual LogosMap loadModule(const std::string& name) = 0;
|
||||
virtual LogosMap unloadModule(const std::string& name) = 0;
|
||||
virtual LogosMap reloadModule(const std::string& name) = 0;
|
||||
|
||||
// Queries
|
||||
virtual QJsonArray listModules(const std::string& filter) = 0;
|
||||
virtual QJsonObject getStatus() = 0;
|
||||
virtual QJsonObject getModuleInfo(const std::string& name) = 0;
|
||||
virtual QJsonArray getModuleStats() = 0;
|
||||
virtual LogosList listModules(const std::string& filter) = 0;
|
||||
virtual LogosMap getStatus() = 0;
|
||||
virtual LogosMap getModuleInfo(const std::string& name) = 0;
|
||||
virtual LogosList getModuleStats() = 0;
|
||||
|
||||
// Proxied call
|
||||
virtual QJsonObject callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const QVariantList& args) = 0;
|
||||
// Proxied call — args is a json array of scalar/object arguments
|
||||
virtual LogosMap callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const LogosList& args) = 0;
|
||||
|
||||
// Daemon lifecycle
|
||||
virtual QJsonObject shutdown() = 0;
|
||||
virtual LogosMap shutdown() = 0;
|
||||
|
||||
// Event watching
|
||||
virtual bool watchModuleEvents(const std::string& module,
|
||||
const std::string& eventName,
|
||||
std::function<void(const QJsonObject&)> callback) = 0;
|
||||
std::function<void(const LogosMap&)> callback) = 0;
|
||||
};
|
||||
|
||||
// Real RPC client implementation that connects to the daemon.
|
||||
@@ -55,20 +53,20 @@ public:
|
||||
bool isConnected() const override;
|
||||
std::string lastError() const override;
|
||||
|
||||
QJsonObject loadModule(const std::string& name) override;
|
||||
QJsonObject unloadModule(const std::string& name) override;
|
||||
QJsonObject reloadModule(const std::string& name) override;
|
||||
QJsonArray listModules(const std::string& filter) override;
|
||||
QJsonObject getStatus() override;
|
||||
QJsonObject getModuleInfo(const std::string& name) override;
|
||||
QJsonArray getModuleStats() override;
|
||||
QJsonObject callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const QVariantList& args) override;
|
||||
QJsonObject shutdown() override;
|
||||
LogosMap loadModule(const std::string& name) override;
|
||||
LogosMap unloadModule(const std::string& name) override;
|
||||
LogosMap reloadModule(const std::string& name) override;
|
||||
LogosList listModules(const std::string& filter) override;
|
||||
LogosMap getStatus() override;
|
||||
LogosMap getModuleInfo(const std::string& name) override;
|
||||
LogosList getModuleStats() override;
|
||||
LogosMap callModuleMethod(const std::string& module,
|
||||
const std::string& method,
|
||||
const LogosList& args) override;
|
||||
LogosMap shutdown() override;
|
||||
bool watchModuleEvents(const std::string& module,
|
||||
const std::string& eventName,
|
||||
std::function<void(const QJsonObject&)> callback) override;
|
||||
std::function<void(const LogosMap&)> callback) override;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#include "call_command.h"
|
||||
#include "../../string_utils.h"
|
||||
#include <QJsonDocument>
|
||||
#include <fmt/format.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
std::string CallCommand::resolveFileParam(const std::string& param)
|
||||
{
|
||||
@@ -33,7 +31,6 @@ int CallCommand::execute(const std::vector<std::string>& args)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check for verbose "module <name> method <method>" syntax
|
||||
if (args.size() >= 3 && args[1] == "method") {
|
||||
moduleName = args[0];
|
||||
methodName = args[2];
|
||||
@@ -61,8 +58,8 @@ int CallCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
// Resolve @file parameters and coerce types
|
||||
QVariantList resolvedArgs;
|
||||
// Resolve @file parameters and coerce types to native JSON values
|
||||
LogosList resolvedArgs = LogosList::array();
|
||||
for (const std::string& arg : methodArgs) {
|
||||
std::string resolved = resolveFileParam(arg);
|
||||
if (strutil::starts_with(arg, '@') && resolved.empty()) {
|
||||
@@ -71,11 +68,10 @@ int CallCommand::execute(const std::vector<std::string>& args)
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Coerce to native types so the RPC target can match signatures
|
||||
if (resolved == "true") {
|
||||
resolvedArgs.append(true);
|
||||
resolvedArgs.push_back(true);
|
||||
} else if (resolved == "false") {
|
||||
resolvedArgs.append(false);
|
||||
resolvedArgs.push_back(false);
|
||||
} else {
|
||||
bool isInt = false;
|
||||
int intVal = 0;
|
||||
@@ -83,7 +79,7 @@ int CallCommand::execute(const std::vector<std::string>& args)
|
||||
catch (...) {}
|
||||
|
||||
if (isInt) {
|
||||
resolvedArgs.append(intVal);
|
||||
resolvedArgs.push_back(intVal);
|
||||
} else {
|
||||
bool isDouble = false;
|
||||
double dblVal = 0.0;
|
||||
@@ -91,48 +87,46 @@ int CallCommand::execute(const std::vector<std::string>& args)
|
||||
catch (...) {}
|
||||
|
||||
if (isDouble) {
|
||||
resolvedArgs.append(dblVal);
|
||||
resolvedArgs.push_back(dblVal);
|
||||
} else {
|
||||
resolvedArgs.append(QString::fromStdString(resolved));
|
||||
resolvedArgs.push_back(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject result = client().callModuleMethod(moduleName, methodName, resolvedArgs);
|
||||
LogosMap result = client().callModuleMethod(moduleName, methodName, resolvedArgs);
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
std::string status = result.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
std::string code = result.value("code").toString().toStdString();
|
||||
std::string code = result.value("code", std::string{});
|
||||
int exitCode = 4;
|
||||
if (code == "MODULE_NOT_LOADED" || code == "MODULE_NOT_FOUND")
|
||||
exitCode = 3;
|
||||
output().printError(code, result.value("message").toString().toStdString(), result);
|
||||
output().printError(code, result.value("message", std::string{}), result);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
if (output().isJsonMode()) {
|
||||
output().printSuccess(result);
|
||||
} else {
|
||||
QJsonValue resultValue = result.value("result");
|
||||
if (resultValue.isString()) {
|
||||
output().printRaw(resultValue.toString().toStdString());
|
||||
} else if (resultValue.isDouble()) {
|
||||
double d = resultValue.toDouble();
|
||||
if (d == static_cast<int>(d))
|
||||
output().printRaw(fmt::format("{}", static_cast<int>(d)));
|
||||
const auto& resultValue = result["result"];
|
||||
if (resultValue.is_string()) {
|
||||
output().printRaw(resultValue.get<std::string>());
|
||||
} else if (resultValue.is_number_float()) {
|
||||
double d = resultValue.get<double>();
|
||||
if (d == static_cast<double>(static_cast<int64_t>(d)))
|
||||
output().printRaw(fmt::format("{}", static_cast<int64_t>(d)));
|
||||
else
|
||||
output().printRaw(fmt::format("{}", d));
|
||||
} else if (resultValue.isBool()) {
|
||||
output().printRaw(resultValue.toBool() ? "true" : "false");
|
||||
} else if (resultValue.isNull() || resultValue.isUndefined()) {
|
||||
} else if (resultValue.is_number_integer()) {
|
||||
output().printRaw(fmt::format("{}", resultValue.get<int64_t>()));
|
||||
} else if (resultValue.is_boolean()) {
|
||||
output().printRaw(resultValue.get<bool>() ? "true" : "false");
|
||||
} else if (resultValue.is_null()) {
|
||||
// Nothing useful to print
|
||||
} else if (resultValue.isArray()) {
|
||||
QJsonDocument doc(resultValue.toArray());
|
||||
output().printRaw(doc.toJson(QJsonDocument::Indented).toStdString());
|
||||
} else if (resultValue.isObject()) {
|
||||
QJsonDocument doc(resultValue.toObject());
|
||||
output().printRaw(doc.toJson(QJsonDocument::Indented).toStdString());
|
||||
} else {
|
||||
output().printRaw(resultValue.dump(2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
@@ -134,14 +132,14 @@ int IssueTokenCommand::execute(const std::vector<std::string>& args)
|
||||
|
||||
std::string rawPath = store.rawTokenFilePath(name);
|
||||
|
||||
QJsonObject result;
|
||||
LogosMap result;
|
||||
result["status"] = "ok";
|
||||
result["name"] = QString::fromStdString(name);
|
||||
result["token"] = QString::fromStdString(outcome.token);
|
||||
result["file"] = QString::fromStdString(rawPath);
|
||||
result["name"] = name;
|
||||
result["token"] = outcome.token;
|
||||
result["file"] = rawPath;
|
||||
result["local_only"] = localOnly;
|
||||
if (!resolvedExpiry->empty())
|
||||
result["expires_at"] = QString::fromStdString(*resolvedExpiry);
|
||||
result["expires_at"] = *resolvedExpiry;
|
||||
|
||||
if (output().isJsonMode()) {
|
||||
output().printSuccess(result);
|
||||
|
||||
@@ -5,8 +5,8 @@ int ListModulesCommand::execute(const std::vector<std::string>& args)
|
||||
{
|
||||
CLI::App cli{"list-modules"};
|
||||
cli.set_help_flag();
|
||||
bool loaded = false;
|
||||
cli.add_flag("--loaded", loaded, "Show only loaded modules");
|
||||
bool loadedOnly = false;
|
||||
cli.add_flag("--loaded", loadedOnly, "Show only loaded modules");
|
||||
try {
|
||||
auto argsCopy = args;
|
||||
cli.parse(argsCopy);
|
||||
@@ -19,9 +19,8 @@ int ListModulesCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
std::string filter = loaded ? "loaded" : "all";
|
||||
QJsonArray modules = client().listModules(filter);
|
||||
|
||||
std::string filter = loadedOnly ? "loaded" : "all";
|
||||
LogosList modules = client().listModules(filter);
|
||||
output().printModuleList(modules);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
#include "../../config.h"
|
||||
#include "../../daemon/token_store.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <fmt/format.h>
|
||||
|
||||
int ListTokensCommand::execute(const std::vector<std::string>& /*args*/)
|
||||
@@ -13,21 +10,19 @@ int ListTokensCommand::execute(const std::vector<std::string>& /*args*/)
|
||||
TokenStore store;
|
||||
const auto issued = store.listTokens();
|
||||
|
||||
QJsonArray arr;
|
||||
LogosList arr = LogosList::array();
|
||||
for (const auto& t : issued) {
|
||||
QJsonObject o;
|
||||
o["name"] = QString::fromStdString(t.name);
|
||||
o["issued_at"] = QString::fromStdString(t.issuedAt);
|
||||
o["expires_at"] = t.expiresAt.empty()
|
||||
? QJsonValue(QJsonValue::Null)
|
||||
: QJsonValue(QString::fromStdString(t.expiresAt));
|
||||
LogosMap o;
|
||||
o["name"] = t.name;
|
||||
o["issued_at"] = t.issuedAt;
|
||||
o["expires_at"] = t.expiresAt.empty() ? nullptr : nlohmann::json(t.expiresAt);
|
||||
o["local_only"] = t.localOnly;
|
||||
o["raw_file_present"] = t.rawFilePresent;
|
||||
arr.append(o);
|
||||
arr.push_back(o);
|
||||
}
|
||||
|
||||
if (output().isJsonMode()) {
|
||||
output().printRaw(QJsonDocument(arr).toJson(QJsonDocument::Compact).toStdString());
|
||||
output().printSuccess(arr);
|
||||
} else if (issued.empty()) {
|
||||
output().printRaw("No tokens issued.");
|
||||
} else {
|
||||
|
||||
@@ -21,26 +21,26 @@ int LoadModuleCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
QJsonObject result = client().loadModule(name);
|
||||
LogosMap result = client().loadModule(name);
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
std::string status = result.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
output().printError(result.value("code").toString().toStdString(),
|
||||
result.value("message").toString().toStdString(), result);
|
||||
output().printError(result.value("code", std::string{}),
|
||||
result.value("message", std::string{}), result);
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (output().isJsonMode()) {
|
||||
output().printSuccess(result);
|
||||
} else {
|
||||
std::string version = result.value("version").toString().toStdString();
|
||||
QJsonArray deps = result.value("dependencies_loaded").toArray();
|
||||
std::string version = result.value("version", std::string{});
|
||||
LogosList deps = result.value("dependencies_loaded", LogosList::array());
|
||||
|
||||
output().printRaw(fmt::format("Loaded module: {} (v{})", name, version));
|
||||
if (!deps.isEmpty()) {
|
||||
if (!deps.empty()) {
|
||||
std::vector<std::string> depNames;
|
||||
for (const QJsonValue& v : deps)
|
||||
depNames.push_back(v.toString().toStdString());
|
||||
for (const auto& v : deps)
|
||||
depNames.push_back(v.get<std::string>());
|
||||
output().printRaw(fmt::format(" Dependencies loaded: {}",
|
||||
strutil::join(depNames, ", ")));
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ int ModuleInfoCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
QJsonObject info = client().getModuleInfo(name);
|
||||
LogosMap info = client().getModuleInfo(name);
|
||||
|
||||
std::string status = info.value("status").toString().toStdString();
|
||||
std::string status = info.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
output().printError(info.value("code").toString().toStdString(),
|
||||
info.value("message").toString().toStdString(), info);
|
||||
output().printError(info.value("code", std::string{}),
|
||||
info.value("message", std::string{}), info);
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ int ReloadModuleCommand::execute(const std::vector<std::string>& args)
|
||||
if (!output().isJsonMode())
|
||||
fprintf(stderr, "Reloading %s...\n", name.c_str());
|
||||
|
||||
QJsonObject result = client().reloadModule(name);
|
||||
LogosMap result = client().reloadModule(name);
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
std::string status = result.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
output().printError(result.value("code").toString().toStdString(),
|
||||
result.value("message").toString().toStdString(), result);
|
||||
output().printError(result.value("code", std::string{}),
|
||||
result.value("message", std::string{}), result);
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "../../config.h"
|
||||
#include "../../daemon/token_store.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <fmt/format.h>
|
||||
|
||||
int RevokeTokenCommand::execute(const std::vector<std::string>& args)
|
||||
@@ -35,9 +34,7 @@ int RevokeTokenCommand::execute(const std::vector<std::string>& args)
|
||||
return 1;
|
||||
}
|
||||
|
||||
QJsonObject result;
|
||||
result["status"] = "ok";
|
||||
result["name"] = QString::fromStdString(name);
|
||||
LogosMap result{{"status","ok"},{"name", name}};
|
||||
if (output().isJsonMode()) {
|
||||
output().printSuccess(result);
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
#include "stats_command.h"
|
||||
#include <CLI/CLI.hpp>
|
||||
|
||||
int StatsCommand::execute(const std::vector<std::string>& args)
|
||||
{
|
||||
(void)args;
|
||||
CLI::App cli{"stats"};
|
||||
cli.set_help_flag();
|
||||
try {
|
||||
auto argsCopy = args;
|
||||
cli.parse(argsCopy);
|
||||
} catch (const CLI::ParseError&) {
|
||||
output().printError("INVALID_ARGS", "Usage: logoscore stats");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int err = ensureConnected();
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
QJsonArray stats = client().getModuleStats();
|
||||
|
||||
LogosList stats = client().getModuleStats();
|
||||
output().printStats(stats);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
#include "../client_state.h"
|
||||
#include "../../daemon/daemon_state.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <signal.h>
|
||||
#include <errno.h>
|
||||
|
||||
@@ -12,8 +10,7 @@ int StatusCommand::execute(const std::vector<std::string>& args)
|
||||
(void)args;
|
||||
|
||||
if (!ClientStateFile::read().fileOk) {
|
||||
QJsonObject result;
|
||||
result["daemon"] = QJsonObject{{"status", "not_configured"}};
|
||||
LogosMap result{{"daemon", LogosMap{{"status","not_configured"}}}};
|
||||
output().printStatus(result);
|
||||
return 1;
|
||||
}
|
||||
@@ -22,12 +19,11 @@ int StatusCommand::execute(const std::vector<std::string>& args)
|
||||
const DaemonRuntimeState rs = DaemonRuntimeStateFile::read();
|
||||
if (rs.fileOk && rs.pid > 0 && ::kill(static_cast<pid_t>(rs.pid), 0) != 0
|
||||
&& errno == ESRCH) {
|
||||
QJsonObject result;
|
||||
result["daemon"] = QJsonObject{
|
||||
LogosMap result{{"daemon", LogosMap{
|
||||
{"status", "not_running"},
|
||||
{"reason", "stale state file (daemon crashed; pid no longer alive)"},
|
||||
{"pid", qlonglong(rs.pid)},
|
||||
};
|
||||
{"pid", rs.pid},
|
||||
}}};
|
||||
output().printStatus(result);
|
||||
return 1;
|
||||
}
|
||||
@@ -35,27 +31,24 @@ int StatusCommand::execute(const std::vector<std::string>& args)
|
||||
|
||||
int err = ensureConnected();
|
||||
if (err != 0) {
|
||||
QJsonObject result;
|
||||
result["daemon"] = QJsonObject{
|
||||
LogosMap result{{"daemon", LogosMap{
|
||||
{"status", "not_running"},
|
||||
{"reason", QString::fromStdString(client().lastError())},
|
||||
};
|
||||
{"reason", client().lastError()},
|
||||
}}};
|
||||
output().printStatus(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
QJsonObject status = client().getStatus();
|
||||
LogosMap status = client().getStatus();
|
||||
|
||||
if (status.contains("status") &&
|
||||
status.value("status").toString().toStdString() == "error") {
|
||||
output().printError(status.value("code").toString().toStdString(),
|
||||
status.value("message").toString().toStdString());
|
||||
if (status.value("status", std::string{}) == "error") {
|
||||
output().printError(status.value("code", std::string{}),
|
||||
status.value("message", std::string{}));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!status.contains("daemon")) {
|
||||
QJsonObject result;
|
||||
result["daemon"] = QJsonObject{{"status", "not_running"}};
|
||||
LogosMap result{{"daemon", LogosMap{{"status","not_running"}}}};
|
||||
output().printStatus(result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "stop_command.h"
|
||||
#include <fmt/format.h>
|
||||
|
||||
int StopCommand::execute(const std::vector<std::string>& args)
|
||||
{
|
||||
@@ -8,33 +9,21 @@ int StopCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
QJsonObject result = client().shutdown();
|
||||
LogosMap result = client().shutdown();
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
|
||||
// If the daemon shut down before the RPC response arrived, the call
|
||||
// returns an RPC_FAILED error. That's expected — treat it as success.
|
||||
std::string status = result.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
std::string code = result.value("code").toString().toStdString();
|
||||
if (code == "RPC_FAILED") {
|
||||
if (output().isJsonMode()) {
|
||||
QJsonObject ok;
|
||||
ok["status"] = "ok";
|
||||
ok["message"] = "Daemon stopped.";
|
||||
output().printSuccess(ok);
|
||||
} else {
|
||||
output().printRaw("Daemon stopped.");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
output().printError(code, result.value("message").toString().toStdString());
|
||||
return 1;
|
||||
output().printError(result.value("code", std::string{}),
|
||||
result.value("message", std::string{}), result);
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (output().isJsonMode()) {
|
||||
output().printSuccess(result);
|
||||
} else {
|
||||
output().printRaw("Daemon stopped.");
|
||||
output().printRaw(fmt::format("Daemon shutdown initiated: {}",
|
||||
result.value("message", std::string{"ok"})));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ int UnloadModuleCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
QJsonObject result = client().unloadModule(name);
|
||||
LogosMap result = client().unloadModule(name);
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
std::string status = result.value("status", std::string{});
|
||||
if (status == "error") {
|
||||
output().printError(result.value("code").toString().toStdString(),
|
||||
result.value("message").toString().toStdString(), result);
|
||||
output().printError(result.value("code", std::string{}),
|
||||
result.value("message", std::string{}), result);
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
#include "watch_command.h"
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <QCoreApplication>
|
||||
#include <fmt/format.h>
|
||||
#include <csignal>
|
||||
|
||||
static volatile sig_atomic_t g_watchInterrupted = 0;
|
||||
|
||||
static void watchSignalHandler(int)
|
||||
{
|
||||
g_watchInterrupted = 1;
|
||||
}
|
||||
#include <QCoreApplication>
|
||||
#include <iostream>
|
||||
|
||||
int WatchCommand::execute(const std::vector<std::string>& args)
|
||||
{
|
||||
CLI::App cli{"watch"};
|
||||
cli.set_help_flag();
|
||||
std::string module;
|
||||
std::string event;
|
||||
std::string eventName;
|
||||
cli.add_option("module", module, "Module name")->required();
|
||||
cli.add_option("--event", event, "Event name to filter");
|
||||
cli.add_option("--event", eventName, "Event name filter (optional)")->default_val("");
|
||||
try {
|
||||
std::vector<std::string> argsCopy(args.rbegin(), args.rend());
|
||||
auto argsCopy = args;
|
||||
cli.parse(argsCopy);
|
||||
} catch (const CLI::ParseError&) {
|
||||
output().printError("INVALID_ARGS",
|
||||
"Usage: logoscore watch <module> [--event <name>]");
|
||||
"Usage: logoscore watch <module> [--event <event>]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -32,29 +25,20 @@ int WatchCommand::execute(const std::vector<std::string>& args)
|
||||
if (err != 0)
|
||||
return err;
|
||||
|
||||
g_watchInterrupted = 0;
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = watchSignalHandler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
sigaction(SIGINT, &sa, nullptr);
|
||||
sigaction(SIGTERM, &sa, nullptr);
|
||||
|
||||
bool watching = client().watchModuleEvents(module, event,
|
||||
[this](const QJsonObject& ev) {
|
||||
output().printEvent(ev);
|
||||
bool ok = client().watchModuleEvents(module, eventName,
|
||||
[this](const LogosMap& event) {
|
||||
output().printEvent(event);
|
||||
});
|
||||
|
||||
if (!watching) {
|
||||
output().printError("MODULE_NOT_LOADED",
|
||||
fmt::format("Module '{}' is not loaded. "
|
||||
"Load it with: logoscore load-module {}",
|
||||
module, module));
|
||||
if (!ok) {
|
||||
output().printError("WATCH_FAILED",
|
||||
fmt::format("Failed to watch events for module '{}'.", module));
|
||||
return 3;
|
||||
}
|
||||
|
||||
while (!g_watchInterrupted)
|
||||
QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, 100);
|
||||
if (!output().isJsonMode())
|
||||
std::cerr << fmt::format("Watching events from '{}'... (Ctrl+C to stop)\n", module);
|
||||
|
||||
QCoreApplication::exec();
|
||||
return 0;
|
||||
}
|
||||
|
||||
+95
-124
@@ -1,6 +1,5 @@
|
||||
#include "output.h"
|
||||
#include "../string_utils.h"
|
||||
#include <QJsonDocument>
|
||||
#include <fmt/format.h>
|
||||
#include <iostream>
|
||||
#include <cstdio>
|
||||
@@ -44,7 +43,7 @@ void Output::setJsonMode(bool json)
|
||||
m_forceJson = json;
|
||||
}
|
||||
|
||||
std::string Output::formatUptime(qint64 seconds) const
|
||||
std::string Output::formatUptime(int64_t seconds) const
|
||||
{
|
||||
if (seconds < 0)
|
||||
return "-";
|
||||
@@ -53,8 +52,8 @@ std::string Output::formatUptime(qint64 seconds) const
|
||||
if (seconds < 3600)
|
||||
return fmt::format("{}m", seconds / 60);
|
||||
|
||||
qint64 hours = seconds / 3600;
|
||||
qint64 mins = (seconds % 3600) / 60;
|
||||
int64_t hours = seconds / 3600;
|
||||
int64_t mins = (seconds % 3600) / 60;
|
||||
return fmt::format("{}h {}m", hours, mins);
|
||||
}
|
||||
|
||||
@@ -63,72 +62,57 @@ std::string Output::padRight(const std::string& str, int width) const
|
||||
return fmt::format("{:<{}}", str, width);
|
||||
}
|
||||
|
||||
void Output::printSuccess(const QJsonObject& data)
|
||||
void Output::printSuccess(const LogosMap& data)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(data).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << data.dump() << std::endl;
|
||||
} else {
|
||||
for (auto it = data.begin(); it != data.end(); ++it) {
|
||||
std::string value;
|
||||
if (it.value().isString())
|
||||
value = it.value().toString().toStdString();
|
||||
else if (it.value().isDouble())
|
||||
value = fmt::format("{}", it.value().toDouble());
|
||||
else if (it.value().isBool())
|
||||
value = it.value().toBool() ? "true" : "false";
|
||||
else
|
||||
value = QJsonDocument(QJsonObject{{it.key(), it.value()}})
|
||||
.toJson(QJsonDocument::Compact)
|
||||
.toStdString();
|
||||
std::cout << it.key().toStdString() << ": " << value << std::endl;
|
||||
const auto& val = it.value();
|
||||
if (val.is_string()) value = val.get<std::string>();
|
||||
else if (val.is_number()) value = fmt::format("{}", val.get<double>());
|
||||
else if (val.is_boolean()) value = val.get<bool>() ? "true" : "false";
|
||||
else value = val.dump();
|
||||
std::cout << it.key() << ": " << value << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printSuccess(const QJsonArray& data)
|
||||
void Output::printSuccess(const LogosList& data)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(data).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << data.dump() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printSuccess(const std::string& message)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
QJsonObject obj;
|
||||
obj["status"] = "ok";
|
||||
obj["message"] = QString::fromStdString(message);
|
||||
std::cout << QJsonDocument(obj).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
LogosMap obj{{"status","ok"},{"message", message}};
|
||||
std::cout << obj.dump() << std::endl;
|
||||
} else {
|
||||
std::cout << message << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printError(const std::string& code, const std::string& message,
|
||||
const QJsonObject& extra)
|
||||
const LogosMap& extra)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
QJsonObject obj;
|
||||
obj["status"] = "error";
|
||||
obj["code"] = QString::fromStdString(code);
|
||||
obj["message"] = QString::fromStdString(message);
|
||||
LogosMap obj{{"status","error"},{"code", code},{"message", message}};
|
||||
for (auto it = extra.begin(); it != extra.end(); ++it)
|
||||
obj[it.key()] = it.value();
|
||||
std::cout << QJsonDocument(obj).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << obj.dump() << std::endl;
|
||||
} else {
|
||||
std::cerr << "Error: " << message << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printModuleList(const QJsonArray& modules)
|
||||
void Output::printModuleList(const LogosList& modules)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(modules).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << modules.dump() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,15 +121,14 @@ void Output::printModuleList(const QJsonArray& modules)
|
||||
<< padRight("STATUS", 12)
|
||||
<< "UPTIME" << std::endl;
|
||||
|
||||
for (const QJsonValue& v : modules) {
|
||||
QJsonObject m = v.toObject();
|
||||
std::string name = m.value("name").toString().toStdString();
|
||||
std::string version = "v" + m.value("version").toString().toStdString();
|
||||
std::string status = m.value("status").toString().toStdString();
|
||||
for (const auto& v : modules) {
|
||||
std::string name = v.value("name", std::string{});
|
||||
std::string version = "v" + v.value("version", std::string{});
|
||||
std::string status = v.value("status", std::string{});
|
||||
std::string uptime = "-";
|
||||
|
||||
if (m.contains("uptime_seconds"))
|
||||
uptime = formatUptime(static_cast<qint64>(m.value("uptime_seconds").toDouble()));
|
||||
if (v.contains("uptime_seconds"))
|
||||
uptime = formatUptime(static_cast<int64_t>(v["uptime_seconds"].get<double>()));
|
||||
|
||||
std::cout << padRight(name, 12)
|
||||
<< padRight(version, 10)
|
||||
@@ -154,11 +137,10 @@ void Output::printModuleList(const QJsonArray& modules)
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printStats(const QJsonArray& stats)
|
||||
void Output::printStats(const LogosList& stats)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(stats).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << stats.dump() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -167,26 +149,24 @@ void Output::printStats(const QJsonArray& stats)
|
||||
<< padRight("CPU%", 8)
|
||||
<< "MEMORY" << std::endl;
|
||||
|
||||
for (const QJsonValue& v : stats) {
|
||||
QJsonObject s = v.toObject();
|
||||
std::cout << padRight(s.value("name").toString().toStdString(), 12)
|
||||
<< padRight(fmt::format("{}", static_cast<qint64>(s.value("pid").toDouble())), 8)
|
||||
<< padRight(fmt::format("{:.1f}%", s.value("cpu_percent").toDouble()), 8)
|
||||
<< fmt::format("{:.1f} MB", s.value("memory_mb").toDouble())
|
||||
for (const auto& s : stats) {
|
||||
std::cout << padRight(s.value("name", std::string{}), 12)
|
||||
<< padRight(fmt::format("{}", static_cast<int64_t>(s.value("pid", 0.0))), 8)
|
||||
<< padRight(fmt::format("{:.1f}%", s.value("cpu_percent", 0.0)), 8)
|
||||
<< fmt::format("{:.1f} MB", s.value("memory_mb", 0.0))
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printStatus(const QJsonObject& status)
|
||||
void Output::printStatus(const LogosMap& status)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(status).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << status.dump() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject daemon = status.value("daemon").toObject();
|
||||
std::string daemonStatus = daemon.value("status").toString().toStdString();
|
||||
LogosMap daemon = status.value("daemon", LogosMap::object());
|
||||
std::string daemonStatus = daemon.value("status", std::string{});
|
||||
|
||||
std::cout << "Logoscore Daemon" << std::endl;
|
||||
std::cout << " Status: " << daemonStatus << std::endl;
|
||||
@@ -198,11 +178,11 @@ void Output::printStatus(const QJsonObject& status)
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 pid = static_cast<qint64>(daemon.value("pid").toDouble());
|
||||
qint64 uptime = static_cast<qint64>(daemon.value("uptime_seconds").toDouble());
|
||||
std::string version = daemon.value("version").toString().toStdString();
|
||||
std::string instanceId = daemon.value("instance_id").toString().toStdString();
|
||||
std::string socket = daemon.value("socket").toString().toStdString();
|
||||
int64_t pid = static_cast<int64_t>(daemon.value("pid", 0.0));
|
||||
int64_t uptime = static_cast<int64_t>(daemon.value("uptime_seconds", 0.0));
|
||||
std::string version = daemon.value("version", std::string{});
|
||||
std::string instanceId = daemon.value("instance_id", std::string{});
|
||||
std::string socket = daemon.value("socket", std::string{});
|
||||
|
||||
std::cout << " PID: " << pid << std::endl;
|
||||
std::cout << " Uptime: " << formatUptime(uptime) << std::endl;
|
||||
@@ -213,25 +193,24 @@ void Output::printStatus(const QJsonObject& status)
|
||||
if (!socket.empty())
|
||||
std::cout << " Socket: " << socket << std::endl;
|
||||
|
||||
QJsonObject summary = status.value("modules_summary").toObject();
|
||||
int loaded = summary.value("loaded").toInt();
|
||||
int crashed = summary.value("crashed").toInt();
|
||||
int notLoaded = summary.value("not_loaded").toInt();
|
||||
LogosMap summary = status.value("modules_summary", LogosMap::object());
|
||||
int loaded = summary.value("loaded", 0);
|
||||
int crashed = summary.value("crashed", 0);
|
||||
int notLoaded = summary.value("not_loaded", 0);
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << "Modules: " << loaded << " loaded, "
|
||||
<< crashed << " crashed, "
|
||||
<< notLoaded << " not loaded" << std::endl;
|
||||
|
||||
QJsonArray modules = status.value("modules").toArray();
|
||||
for (const QJsonValue& v : modules) {
|
||||
QJsonObject m = v.toObject();
|
||||
std::string name = m.value("name").toString().toStdString();
|
||||
std::string ver = "v" + m.value("version").toString().toStdString();
|
||||
std::string st = m.value("status").toString().toStdString();
|
||||
LogosList modules = status.value("modules", LogosList::array());
|
||||
for (const auto& m : modules) {
|
||||
std::string name = m.value("name", std::string{});
|
||||
std::string ver = "v" + m.value("version", std::string{});
|
||||
std::string st = m.value("status", std::string{});
|
||||
std::string up = "-";
|
||||
if (m.contains("uptime_seconds"))
|
||||
up = formatUptime(static_cast<qint64>(m.value("uptime_seconds").toDouble()));
|
||||
up = formatUptime(static_cast<int64_t>(m["uptime_seconds"].get<double>()));
|
||||
|
||||
std::cout << " " << padRight(name, 12)
|
||||
<< padRight(ver, 10)
|
||||
@@ -240,32 +219,31 @@ void Output::printStatus(const QJsonObject& status)
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printModuleInfo(const QJsonObject& info)
|
||||
void Output::printModuleInfo(const LogosMap& info)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(info).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << info.dump() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name = info.value("name").toString().toStdString();
|
||||
std::string version = info.value("version").toString().toStdString();
|
||||
std::string status = info.value("status").toString().toStdString();
|
||||
std::string name = info.value("name", std::string{});
|
||||
std::string version = info.value("version", std::string{});
|
||||
std::string status = info.value("status", std::string{});
|
||||
|
||||
std::cout << "Name: " << name << std::endl;
|
||||
std::cout << "Version: v" << version << std::endl;
|
||||
std::cout << "Status: " << status << std::endl;
|
||||
|
||||
if (status == "loaded") {
|
||||
qint64 pid = static_cast<qint64>(info.value("pid").toDouble());
|
||||
qint64 uptime = static_cast<qint64>(info.value("uptime_seconds").toDouble());
|
||||
int64_t pid = static_cast<int64_t>(info.value("pid", 0.0));
|
||||
int64_t uptime = static_cast<int64_t>(info.value("uptime_seconds", 0.0));
|
||||
|
||||
std::cout << "PID: " << pid << std::endl;
|
||||
std::cout << "Uptime: " << formatUptime(uptime) << std::endl;
|
||||
} else if (status == "crashed") {
|
||||
if (info.contains("exit_code")) {
|
||||
int exitCode = info.value("exit_code").toInt();
|
||||
std::string signal = info.value("crash_signal").toString().toStdString();
|
||||
int exitCode = info.value("exit_code", 0);
|
||||
std::string signal = info.value("crash_signal", std::string{});
|
||||
std::string display = fmt::format("{}", exitCode);
|
||||
if (!signal.empty())
|
||||
display += " (" + signal + ")";
|
||||
@@ -273,41 +251,39 @@ void Output::printModuleInfo(const QJsonObject& info)
|
||||
}
|
||||
if (info.contains("crashed_at"))
|
||||
std::cout << "Crashed At: "
|
||||
<< info.value("crashed_at").toString().toStdString() << std::endl;
|
||||
<< info.value("crashed_at", std::string{}) << std::endl;
|
||||
if (info.contains("restart_count"))
|
||||
std::cout << "Restart Count: " << info.value("restart_count").toInt() << std::endl;
|
||||
std::cout << "Restart Count: " << info.value("restart_count", 0) << std::endl;
|
||||
if (info.contains("last_log_line"))
|
||||
std::cout << "Last Log: \""
|
||||
<< info.value("last_log_line").toString().toStdString()
|
||||
<< info.value("last_log_line", std::string{})
|
||||
<< "\"" << std::endl;
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
QJsonArray deps = info.value("dependencies").toArray();
|
||||
if (!deps.isEmpty()) {
|
||||
LogosList deps = info.value("dependencies", LogosList::array());
|
||||
if (!deps.empty()) {
|
||||
std::vector<std::string> depList;
|
||||
for (const QJsonValue& v : deps)
|
||||
depList.push_back(v.toString().toStdString());
|
||||
for (const auto& v : deps)
|
||||
depList.push_back(v.get<std::string>());
|
||||
std::cout << "Dependencies: " << strutil::join(depList, ", ") << std::endl;
|
||||
}
|
||||
|
||||
// Methods
|
||||
QJsonArray methods = info.value("methods").toArray();
|
||||
if (!methods.isEmpty()) {
|
||||
LogosList methods = info.value("methods", LogosList::array());
|
||||
if (!methods.empty()) {
|
||||
std::cout << std::endl;
|
||||
std::cout << "Methods:" << std::endl;
|
||||
for (const QJsonValue& v : methods) {
|
||||
QJsonObject method = v.toObject();
|
||||
std::string methodName = method.value("name").toString().toStdString();
|
||||
std::string returnType = method.value("return_type").toString().toStdString();
|
||||
for (const auto& v : methods) {
|
||||
std::string methodName = v.value("name", std::string{});
|
||||
std::string returnType = v.value("return_type", std::string{});
|
||||
|
||||
QJsonArray params = method.value("params").toArray();
|
||||
LogosList params = v.value("params", LogosList::array());
|
||||
std::vector<std::string> paramStrs;
|
||||
for (const QJsonValue& p : params) {
|
||||
QJsonObject param = p.toObject();
|
||||
for (const auto& p : params) {
|
||||
paramStrs.push_back(
|
||||
param.value("name").toString().toStdString() + ": " +
|
||||
param.value("type").toString().toStdString());
|
||||
p.value("name", std::string{}) + ": " +
|
||||
p.value("type", std::string{}));
|
||||
}
|
||||
|
||||
std::cout << " " << methodName
|
||||
@@ -317,19 +293,18 @@ void Output::printModuleInfo(const QJsonObject& info)
|
||||
}
|
||||
}
|
||||
|
||||
void Output::printEvent(const QJsonObject& event)
|
||||
void Output::printEvent(const LogosMap& event)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(event).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << event.dump() << std::endl;
|
||||
std::cout.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string timestamp = event.value("timestamp").toString().toStdString();
|
||||
std::string module = event.value("module").toString().toStdString();
|
||||
std::string eventName = event.value("event").toString().toStdString();
|
||||
QJsonObject data = event.value("data").toObject();
|
||||
std::string timestamp = event.value("timestamp", std::string{});
|
||||
std::string module = event.value("module", std::string{});
|
||||
std::string eventName = event.value("event", std::string{});
|
||||
LogosMap data = event.value("data", LogosMap::object());
|
||||
|
||||
// Extract time portion from ISO timestamp
|
||||
auto tPos = timestamp.find('T');
|
||||
@@ -346,41 +321,37 @@ void Output::printEvent(const QJsonObject& event)
|
||||
|
||||
for (auto it = data.begin(); it != data.end(); ++it) {
|
||||
std::string value;
|
||||
if (it.value().isString())
|
||||
value = it.value().toString().toStdString();
|
||||
else
|
||||
value = QJsonDocument(QJsonObject{{it.key(), it.value()}})
|
||||
.toJson(QJsonDocument::Compact)
|
||||
.toStdString();
|
||||
std::cout << " " << it.key().toStdString() << ": " << value << std::endl;
|
||||
const auto& val = it.value();
|
||||
if (val.is_string()) value = val.get<std::string>();
|
||||
else value = val.dump();
|
||||
std::cout << " " << it.key() << ": " << value << std::endl;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
void Output::printReload(const QJsonObject& result)
|
||||
void Output::printReload(const LogosMap& result)
|
||||
{
|
||||
if (isJsonMode()) {
|
||||
std::cout << QJsonDocument(result).toJson(QJsonDocument::Compact).constData()
|
||||
<< std::endl;
|
||||
std::cout << result.dump() << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string status = result.value("status").toString().toStdString();
|
||||
std::string module = result.value("module").toString().toStdString();
|
||||
std::string status = result.value("status", std::string{});
|
||||
std::string module = result.value("module", std::string{});
|
||||
|
||||
if (status == "loaded" || status == "ok") {
|
||||
std::string version = result.value("version").toString().toStdString();
|
||||
qint64 pid = static_cast<qint64>(result.value("pid").toDouble());
|
||||
std::string version = result.value("version", std::string{});
|
||||
int64_t pid = static_cast<int64_t>(result.value("pid", 0.0));
|
||||
std::cout << "Module \"" << module
|
||||
<< "\" reloaded successfully (v" << version
|
||||
<< ", pid " << pid << ")" << std::endl;
|
||||
} else if (status == "error") {
|
||||
std::cerr << "Error: "
|
||||
<< result.value("error").toString().toStdString() << std::endl;
|
||||
<< result.value("error", std::string{}) << std::endl;
|
||||
if (result.contains("last_log_line"))
|
||||
std::cerr << " Last log: \""
|
||||
<< result.value("last_log_line").toString().toStdString()
|
||||
<< result.value("last_log_line", std::string{})
|
||||
<< "\"" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-13
@@ -1,9 +1,7 @@
|
||||
#ifndef OUTPUT_H
|
||||
#define OUTPUT_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
#include <logos_json.h>
|
||||
#include <string>
|
||||
|
||||
class Output {
|
||||
@@ -15,25 +13,25 @@ public:
|
||||
void setJsonMode(bool json);
|
||||
|
||||
// Success output
|
||||
void printSuccess(const QJsonObject& data);
|
||||
void printSuccess(const QJsonArray& data);
|
||||
void printSuccess(const LogosMap& data);
|
||||
void printSuccess(const LogosList& data);
|
||||
void printSuccess(const std::string& message);
|
||||
|
||||
// Error output
|
||||
void printError(const std::string& code, const std::string& message,
|
||||
const QJsonObject& extra = {});
|
||||
const LogosMap& extra = {});
|
||||
|
||||
// Table output (list-modules, stats)
|
||||
void printModuleList(const QJsonArray& modules);
|
||||
void printStats(const QJsonArray& stats);
|
||||
void printStatus(const QJsonObject& status);
|
||||
void printModuleInfo(const QJsonObject& info);
|
||||
void printModuleList(const LogosList& modules);
|
||||
void printStats(const LogosList& stats);
|
||||
void printStatus(const LogosMap& status);
|
||||
void printModuleInfo(const LogosMap& info);
|
||||
|
||||
// Event output (watch)
|
||||
void printEvent(const QJsonObject& event);
|
||||
void printEvent(const LogosMap& event);
|
||||
|
||||
// Reload output
|
||||
void printReload(const QJsonObject& result);
|
||||
void printReload(const LogosMap& result);
|
||||
|
||||
// Generic key-value display
|
||||
void printKeyValue(const std::string& key, const std::string& value);
|
||||
@@ -47,7 +45,7 @@ private:
|
||||
bool m_isTTY = false;
|
||||
|
||||
void checkTTY();
|
||||
std::string formatUptime(qint64 seconds) const;
|
||||
std::string formatUptime(int64_t seconds) const;
|
||||
std::string padRight(const std::string& str, int width) const;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,16 +2,9 @@
|
||||
#include "logos_core.h"
|
||||
#include <logos_api.h>
|
||||
#include <logos_api_client.h>
|
||||
#include <logos_types.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QMetaType>
|
||||
#include <QTimer>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
@@ -208,16 +201,13 @@ LogosMap CoreServiceImpl::getModuleInfo(const std::string& name)
|
||||
info["status"] = "loaded";
|
||||
|
||||
if (m_api) {
|
||||
QString qName = QString::fromStdString(name);
|
||||
LogosAPIClient* moduleClient = m_api->getClient(qName);
|
||||
// Use the nlohmann::json overload — no QJson types needed here
|
||||
LogosAPIClient* moduleClient = m_api->getClient(QString::fromStdString(name));
|
||||
if (moduleClient) {
|
||||
QVariant ret = moduleClient->invokeRemoteMethod(qName, "getPluginMethods");
|
||||
if (ret.canConvert<QJsonArray>()) {
|
||||
QJsonArray methods = qvariant_cast<QJsonArray>(ret);
|
||||
QJsonDocument doc(methods);
|
||||
info["methods"] = nlohmann::json::parse(
|
||||
doc.toJson(QJsonDocument::Compact).toStdString());
|
||||
}
|
||||
nlohmann::json methods = moduleClient->invokeRemoteMethod(
|
||||
name, "getPluginMethods", nlohmann::json::array());
|
||||
if (methods.is_array())
|
||||
info["methods"] = methods;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -241,7 +231,7 @@ LogosList CoreServiceImpl::getModuleStats()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proxied call
|
||||
// Proxied call — uses the nlohmann::json SDK overload; no QJson needed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
StdLogosResult CoreServiceImpl::callModuleMethod(const std::string& module,
|
||||
@@ -257,10 +247,7 @@ StdLogosResult CoreServiceImpl::callModuleMethod(const std::string& module,
|
||||
return {false, result, "core_service not initialized."};
|
||||
}
|
||||
|
||||
QString qModule = QString::fromStdString(module);
|
||||
QString qMethod = QString::fromStdString(method);
|
||||
|
||||
LogosAPIClient* moduleClient = m_api->getClient(qModule);
|
||||
LogosAPIClient* moduleClient = m_api->getClient(QString::fromStdString(module));
|
||||
if (!moduleClient) {
|
||||
result["status"] = "error";
|
||||
result["code"] = "MODULE_NOT_LOADED";
|
||||
@@ -268,33 +255,10 @@ StdLogosResult CoreServiceImpl::callModuleMethod(const std::string& module,
|
||||
return {false, result, "Module '" + module + "' is not loaded."};
|
||||
}
|
||||
|
||||
// Convert LogosList args to QVariantList for the Qt SDK call
|
||||
QVariantList qArgs;
|
||||
for (const auto& arg : args) {
|
||||
if (arg.is_string())
|
||||
qArgs.append(QString::fromStdString(arg.get<std::string>()));
|
||||
else if (arg.is_number_integer())
|
||||
qArgs.append(static_cast<qlonglong>(arg.get<int64_t>()));
|
||||
else if (arg.is_number_float())
|
||||
qArgs.append(arg.get<double>());
|
||||
else if (arg.is_boolean())
|
||||
qArgs.append(arg.get<bool>());
|
||||
else if (arg.is_array()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(
|
||||
QByteArray::fromStdString(arg.dump()));
|
||||
qArgs.append(QVariant::fromValue(doc.array()));
|
||||
} else if (arg.is_object()) {
|
||||
QJsonDocument doc = QJsonDocument::fromJson(
|
||||
QByteArray::fromStdString(arg.dump()));
|
||||
qArgs.append(QVariant::fromValue(doc.object()));
|
||||
} else {
|
||||
qArgs.append(QVariant());
|
||||
}
|
||||
}
|
||||
// The nlohmann::json overload handles QVariant<->json conversion internally.
|
||||
nlohmann::json ret = moduleClient->invokeRemoteMethod(module, method, args);
|
||||
|
||||
QVariant ret = moduleClient->invokeRemoteMethod(qModule, qMethod, qArgs);
|
||||
|
||||
if (!ret.isValid()) {
|
||||
if (ret.is_null()) {
|
||||
result["status"] = "error";
|
||||
result["code"] = "METHOD_FAILED";
|
||||
result["message"] = "Call to " + module + "." + method + " failed.";
|
||||
@@ -304,58 +268,13 @@ StdLogosResult CoreServiceImpl::callModuleMethod(const std::string& module,
|
||||
result["status"] = "ok";
|
||||
result["module"] = module;
|
||||
result["method"] = method;
|
||||
|
||||
const int logosResultId = QMetaType::fromName("LogosResult").id();
|
||||
if (logosResultId != QMetaType::UnknownType
|
||||
&& ret.userType() == logosResultId) {
|
||||
const LogosResult lr = ret.value<LogosResult>();
|
||||
LogosMap obj;
|
||||
obj["success"] = lr.success;
|
||||
QJsonValue valJson = QJsonValue::fromVariant(lr.value);
|
||||
QJsonValue errJson = QJsonValue::fromVariant(lr.error);
|
||||
QJsonDocument valDoc;
|
||||
if (valJson.isObject()) valDoc = QJsonDocument(valJson.toObject());
|
||||
else if (valJson.isArray()) valDoc = QJsonDocument(valJson.toArray());
|
||||
if (!valDoc.isNull())
|
||||
obj["value"] = nlohmann::json::parse(valDoc.toJson(QJsonDocument::Compact).toStdString());
|
||||
else if (valJson.isString())
|
||||
obj["value"] = valJson.toString().toStdString();
|
||||
else if (valJson.isBool())
|
||||
obj["value"] = valJson.toBool();
|
||||
else if (valJson.isDouble())
|
||||
obj["value"] = valJson.toDouble();
|
||||
else
|
||||
obj["value"] = nullptr;
|
||||
if (errJson.isString())
|
||||
obj["error"] = errJson.toString().toStdString();
|
||||
else
|
||||
obj["error"] = nullptr;
|
||||
result["result"] = obj;
|
||||
} else if (ret.canConvert<QJsonObject>()) {
|
||||
QJsonDocument doc(ret.toJsonObject());
|
||||
result["result"] = nlohmann::json::parse(
|
||||
doc.toJson(QJsonDocument::Compact).toStdString());
|
||||
} else if (ret.canConvert<QJsonArray>()) {
|
||||
QJsonDocument doc(ret.toJsonArray());
|
||||
result["result"] = nlohmann::json::parse(
|
||||
doc.toJson(QJsonDocument::Compact).toStdString());
|
||||
} else {
|
||||
QJsonValue jv = QJsonValue::fromVariant(ret);
|
||||
if (jv.isString())
|
||||
result["result"] = jv.toString().toStdString();
|
||||
else if (jv.isBool())
|
||||
result["result"] = jv.toBool();
|
||||
else if (jv.isDouble())
|
||||
result["result"] = jv.toDouble();
|
||||
else
|
||||
result["result"] = nullptr;
|
||||
}
|
||||
result["result"] = ret;
|
||||
|
||||
return {true, result};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event forwarding
|
||||
// Event forwarding — uses the nlohmann::json onEvent overload
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool CoreServiceImpl::watchModuleEvents(const std::string& module,
|
||||
@@ -364,39 +283,22 @@ bool CoreServiceImpl::watchModuleEvents(const std::string& module,
|
||||
if (!m_api)
|
||||
return false;
|
||||
|
||||
QString qModule = QString::fromStdString(module);
|
||||
QString qEventName = QString::fromStdString(eventName);
|
||||
|
||||
LogosAPIClient* moduleClient = m_api->getClient(qModule);
|
||||
LogosAPIClient* moduleClient = m_api->getClient(QString::fromStdString(module));
|
||||
if (!moduleClient)
|
||||
return false;
|
||||
|
||||
LogosObject* obj = moduleClient->requestObject(qModule);
|
||||
LogosObject* obj = moduleClient->requestObject(QString::fromStdString(module));
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
moduleClient->onEvent(obj, qEventName,
|
||||
[this, module](const QString& event, const QVariantList& data) {
|
||||
moduleClient->onEvent(obj, eventName,
|
||||
[this, module](const std::string& event, const nlohmann::json& data) {
|
||||
nlohmann::json forwardData = nlohmann::json::array();
|
||||
forwardData.push_back(module);
|
||||
forwardData.push_back(event.toStdString());
|
||||
for (const QVariant& d : data) {
|
||||
QJsonValue jv = QJsonValue::fromVariant(d);
|
||||
if (jv.isString())
|
||||
forwardData.push_back(jv.toString().toStdString());
|
||||
else if (jv.isBool())
|
||||
forwardData.push_back(jv.toBool());
|
||||
else if (jv.isDouble())
|
||||
forwardData.push_back(jv.toDouble());
|
||||
else if (jv.isObject() || jv.isArray()) {
|
||||
QJsonDocument doc;
|
||||
if (jv.isObject()) doc = QJsonDocument(jv.toObject());
|
||||
else doc = QJsonDocument(jv.toArray());
|
||||
forwardData.push_back(nlohmann::json::parse(
|
||||
doc.toJson(QJsonDocument::Compact).toStdString()));
|
||||
} else {
|
||||
forwardData.push_back(nullptr);
|
||||
}
|
||||
forwardData.push_back(event);
|
||||
if (data.is_array()) {
|
||||
for (const auto& item : data)
|
||||
forwardData.push_back(item);
|
||||
}
|
||||
if (emitEvent)
|
||||
emitEvent("module_event", forwardData.dump());
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
#include "call_executor.h"
|
||||
#include "logos_sdk_c.h"
|
||||
#include "../string_utils.h"
|
||||
#include <logos_json.h>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <fmt/format.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
struct CallResult {
|
||||
bool completed = false;
|
||||
@@ -41,11 +38,11 @@ std::string CallExecutor::resolveParam(const std::string& param) {
|
||||
}
|
||||
|
||||
std::string CallExecutor::buildParamsJson(const std::vector<std::string>& params) {
|
||||
QJsonArray paramsArray;
|
||||
nlohmann::json paramsArray = nlohmann::json::array();
|
||||
|
||||
for (size_t i = 0; i < params.size(); ++i) {
|
||||
const std::string& rawParam = params[i];
|
||||
const std::string resolvedParam = resolveParam(rawParam);
|
||||
const std::string& rawParam = params[i];
|
||||
const std::string resolvedParam = resolveParam(rawParam);
|
||||
|
||||
if (strutil::starts_with(rawParam, '@') && resolvedParam.empty()) {
|
||||
fprintf(stderr, "Warning: failed to resolve file parameter: %s\n",
|
||||
@@ -53,10 +50,9 @@ std::string CallExecutor::buildParamsJson(const std::vector<std::string>& params
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonObject paramObj;
|
||||
paramObj["name"] = QString::fromStdString(fmt::format("arg{}", i));
|
||||
nlohmann::json paramObj;
|
||||
paramObj["name"] = fmt::format("arg{}", i);
|
||||
|
||||
// Determine type and coerce value
|
||||
std::string type;
|
||||
std::string lower = strutil::to_lower(resolvedParam);
|
||||
if (lower == "true" || lower == "false") {
|
||||
@@ -73,13 +69,12 @@ std::string CallExecutor::buildParamsJson(const std::vector<std::string>& params
|
||||
}
|
||||
}
|
||||
|
||||
paramObj["type"] = QString::fromStdString(type);
|
||||
paramObj["value"] = QString::fromStdString(resolvedParam);
|
||||
paramsArray.append(paramObj);
|
||||
paramObj["type"] = type;
|
||||
paramObj["value"] = resolvedParam;
|
||||
paramsArray.push_back(paramObj);
|
||||
}
|
||||
|
||||
QJsonDocument doc(paramsArray);
|
||||
return doc.toJson(QJsonDocument::Compact).toStdString();
|
||||
return paramsArray.dump();
|
||||
}
|
||||
|
||||
bool CallExecutor::executeCall(const ModuleCall& call) {
|
||||
|
||||
+77
-75
@@ -1,7 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <logos_json.h>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -16,15 +14,15 @@ public:
|
||||
// Control mock behavior
|
||||
bool shouldConnect = true;
|
||||
std::string connectError = "No running logoscore daemon. Start one with: logoscore -D";
|
||||
QJsonObject loadModuleResult;
|
||||
QJsonObject unloadModuleResult;
|
||||
QJsonObject reloadModuleResult;
|
||||
QJsonArray listModulesResult;
|
||||
QJsonObject statusResult;
|
||||
QJsonObject moduleInfoResult;
|
||||
QJsonArray moduleStatsResult;
|
||||
QJsonObject callMethodResult;
|
||||
QJsonObject shutdownResult;
|
||||
LogosMap loadModuleResult;
|
||||
LogosMap unloadModuleResult;
|
||||
LogosMap reloadModuleResult;
|
||||
LogosList listModulesResult;
|
||||
LogosMap statusResult;
|
||||
LogosMap moduleInfoResult;
|
||||
LogosList moduleStatsResult;
|
||||
LogosMap callMethodResult;
|
||||
LogosMap shutdownResult;
|
||||
|
||||
// Track calls
|
||||
bool shutdownCalled = false;
|
||||
@@ -34,7 +32,7 @@ public:
|
||||
std::string lastInfoModule;
|
||||
std::string lastCallModule;
|
||||
std::string lastCallMethod;
|
||||
QVariantList lastCallArgs;
|
||||
LogosList lastCallArgs;
|
||||
std::string lastListFilter;
|
||||
std::string lastWatchModule;
|
||||
std::string lastWatchEventName;
|
||||
@@ -49,53 +47,53 @@ public:
|
||||
bool isConnected() const override { return m_connected; }
|
||||
std::string lastError() const override { return m_lastError; }
|
||||
|
||||
QJsonObject loadModule(const std::string& name) override {
|
||||
LogosMap loadModule(const std::string& name) override {
|
||||
lastLoadedModule = name;
|
||||
return loadModuleResult;
|
||||
}
|
||||
|
||||
QJsonObject unloadModule(const std::string& name) override {
|
||||
LogosMap unloadModule(const std::string& name) override {
|
||||
lastUnloadedModule = name;
|
||||
return unloadModuleResult;
|
||||
}
|
||||
|
||||
QJsonObject reloadModule(const std::string& name) override {
|
||||
LogosMap reloadModule(const std::string& name) override {
|
||||
lastReloadedModule = name;
|
||||
return reloadModuleResult;
|
||||
}
|
||||
|
||||
QJsonArray listModules(const std::string& filter) override {
|
||||
LogosList listModules(const std::string& filter) override {
|
||||
lastListFilter = filter;
|
||||
return listModulesResult;
|
||||
}
|
||||
|
||||
QJsonObject getStatus() override { return statusResult; }
|
||||
LogosMap getStatus() override { return statusResult; }
|
||||
|
||||
QJsonObject getModuleInfo(const std::string& name) override {
|
||||
LogosMap getModuleInfo(const std::string& name) override {
|
||||
lastInfoModule = name;
|
||||
return moduleInfoResult;
|
||||
}
|
||||
|
||||
QJsonArray getModuleStats() override { return moduleStatsResult; }
|
||||
LogosList getModuleStats() override { return moduleStatsResult; }
|
||||
|
||||
QJsonObject callModuleMethod(const std::string& module, const std::string& method,
|
||||
const QVariantList& args) override {
|
||||
LogosMap callModuleMethod(const std::string& module, const std::string& method,
|
||||
const LogosList& args) override {
|
||||
lastCallModule = module;
|
||||
lastCallMethod = method;
|
||||
lastCallArgs = args;
|
||||
lastCallArgs = args;
|
||||
return callMethodResult;
|
||||
}
|
||||
|
||||
QJsonObject shutdown() override {
|
||||
LogosMap shutdown() override {
|
||||
shutdownCalled = true;
|
||||
return shutdownResult;
|
||||
}
|
||||
|
||||
bool watchModuleEvents(const std::string& module, const std::string& eventName,
|
||||
std::function<void(const QJsonObject&)> callback) override {
|
||||
std::function<void(const LogosMap&)> callback) override {
|
||||
(void)callback;
|
||||
lastWatchModule = module;
|
||||
lastWatchEventName = eventName;
|
||||
lastWatchModule = module;
|
||||
lastWatchEventName = eventName;
|
||||
return m_connected && watchShouldSucceed;
|
||||
}
|
||||
|
||||
@@ -120,6 +118,10 @@ protected:
|
||||
std::cout.rdbuf(oldBuf);
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
nlohmann::json parseJson(const std::string& s) {
|
||||
return nlohmann::json::parse(s);
|
||||
}
|
||||
};
|
||||
|
||||
// ── createCommand ────────────────────────────────────────────────────────────
|
||||
@@ -185,17 +187,17 @@ TEST_F(CommandTest, LoadModule_NoDaemon_ReturnsExit2)
|
||||
EXPECT_EQ(exitCode, 2);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "NO_DAEMON");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "NO_DAEMON");
|
||||
}
|
||||
|
||||
// ── load-module ──────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_F(CommandTest, LoadModule_Success)
|
||||
{
|
||||
mockClient.loadModuleResult = QJsonObject{
|
||||
mockClient.loadModuleResult = LogosMap{
|
||||
{"status", "ok"}, {"module", "waku"}, {"version", "0.1.0"},
|
||||
{"dependencies_loaded", QJsonArray{"store"}}
|
||||
{"dependencies_loaded", nlohmann::json::array({"store"})}
|
||||
};
|
||||
|
||||
auto cmd = createCommand("load-module", mockClient, output);
|
||||
@@ -206,14 +208,14 @@ TEST_F(CommandTest, LoadModule_Success)
|
||||
|
||||
EXPECT_EQ(mockClient.lastLoadedModule, "waku");
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("status").toString(), "ok");
|
||||
EXPECT_EQ(doc.object().value("module").toString(), "waku");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
||||
EXPECT_EQ(doc["module"].get<std::string>(), "waku");
|
||||
}
|
||||
|
||||
TEST_F(CommandTest, LoadModule_NotFound)
|
||||
{
|
||||
mockClient.loadModuleResult = QJsonObject{
|
||||
mockClient.loadModuleResult = LogosMap{
|
||||
{"status", "error"}, {"code", "MODULE_NOT_FOUND"},
|
||||
{"message", "Module 'nonexistent' not found."}
|
||||
};
|
||||
@@ -224,8 +226,8 @@ TEST_F(CommandTest, LoadModule_NotFound)
|
||||
EXPECT_EQ(exitCode, 3);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "MODULE_NOT_FOUND");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "MODULE_NOT_FOUND");
|
||||
}
|
||||
|
||||
TEST_F(CommandTest, LoadModule_MissingArg)
|
||||
@@ -236,15 +238,15 @@ TEST_F(CommandTest, LoadModule_MissingArg)
|
||||
EXPECT_EQ(exitCode, 1);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "INVALID_ARGS");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "INVALID_ARGS");
|
||||
}
|
||||
|
||||
// ── unload-module ────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_F(CommandTest, UnloadModule_Success)
|
||||
{
|
||||
mockClient.unloadModuleResult = QJsonObject{
|
||||
mockClient.unloadModuleResult = LogosMap{
|
||||
{"status", "ok"}, {"module", "waku"}
|
||||
};
|
||||
|
||||
@@ -261,7 +263,7 @@ TEST_F(CommandTest, UnloadModule_Success)
|
||||
|
||||
TEST_F(CommandTest, ReloadModule_Success)
|
||||
{
|
||||
mockClient.reloadModuleResult = QJsonObject{
|
||||
mockClient.reloadModuleResult = LogosMap{
|
||||
{"action", "reload"}, {"module", "chat"}, {"version", "0.2.0"},
|
||||
{"status", "loaded"}, {"pid", 51203}
|
||||
};
|
||||
@@ -277,7 +279,7 @@ TEST_F(CommandTest, ReloadModule_Success)
|
||||
|
||||
TEST_F(CommandTest, ReloadModule_Error)
|
||||
{
|
||||
mockClient.reloadModuleResult = QJsonObject{
|
||||
mockClient.reloadModuleResult = LogosMap{
|
||||
{"status", "error"}, {"code", "MODULE_LOAD_FAILED"},
|
||||
{"message", "Module failed to start."}
|
||||
};
|
||||
@@ -293,10 +295,10 @@ TEST_F(CommandTest, ReloadModule_Error)
|
||||
|
||||
TEST_F(CommandTest, ListModules_All)
|
||||
{
|
||||
mockClient.listModulesResult = QJsonArray{
|
||||
QJsonObject{{"name", "waku"}, {"status", "loaded"}},
|
||||
QJsonObject{{"name", "chat"}, {"status", "not_loaded"}}
|
||||
};
|
||||
mockClient.listModulesResult = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"status", "loaded"}},
|
||||
LogosMap{{"name", "chat"}, {"status", "not_loaded"}}
|
||||
});
|
||||
|
||||
auto cmd = createCommand("list-modules", mockClient, output);
|
||||
std::string out = captureOutput([&]() {
|
||||
@@ -309,9 +311,9 @@ TEST_F(CommandTest, ListModules_All)
|
||||
|
||||
TEST_F(CommandTest, ListModules_LoadedFilter)
|
||||
{
|
||||
mockClient.listModulesResult = QJsonArray{
|
||||
QJsonObject{{"name", "waku"}, {"status", "loaded"}}
|
||||
};
|
||||
mockClient.listModulesResult = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"status", "loaded"}}
|
||||
});
|
||||
|
||||
auto cmd = createCommand("list-modules", mockClient, output);
|
||||
std::string out = captureOutput([&]() {
|
||||
@@ -326,7 +328,7 @@ TEST_F(CommandTest, ListModules_LoadedFilter)
|
||||
|
||||
TEST_F(CommandTest, ModuleInfo_Success)
|
||||
{
|
||||
mockClient.moduleInfoResult = QJsonObject{
|
||||
mockClient.moduleInfoResult = LogosMap{
|
||||
{"name", "chat"}, {"version", "0.2.0"}, {"status", "loaded"},
|
||||
{"pid", 23457}, {"uptime_seconds", 8040}
|
||||
};
|
||||
@@ -342,7 +344,7 @@ TEST_F(CommandTest, ModuleInfo_Success)
|
||||
|
||||
TEST_F(CommandTest, InfoAlias_SameAsModuleInfo)
|
||||
{
|
||||
mockClient.moduleInfoResult = QJsonObject{
|
||||
mockClient.moduleInfoResult = LogosMap{
|
||||
{"name", "chat"}, {"version", "0.2.0"}, {"status", "loaded"}
|
||||
};
|
||||
|
||||
@@ -357,7 +359,7 @@ TEST_F(CommandTest, InfoAlias_SameAsModuleInfo)
|
||||
|
||||
TEST_F(CommandTest, ModuleInfo_NotFound)
|
||||
{
|
||||
mockClient.moduleInfoResult = QJsonObject{
|
||||
mockClient.moduleInfoResult = LogosMap{
|
||||
{"status", "error"}, {"code", "MODULE_NOT_FOUND"},
|
||||
{"message", "Module 'nonexistent' not found."}
|
||||
};
|
||||
@@ -373,7 +375,7 @@ TEST_F(CommandTest, ModuleInfo_NotFound)
|
||||
|
||||
TEST_F(CommandTest, Call_Success)
|
||||
{
|
||||
mockClient.callMethodResult = QJsonObject{
|
||||
mockClient.callMethodResult = LogosMap{
|
||||
{"status", "ok"}, {"module", "chat"}, {"method", "send_message"},
|
||||
{"result", "message sent (id: msg_123)"}
|
||||
};
|
||||
@@ -386,12 +388,12 @@ TEST_F(CommandTest, Call_Success)
|
||||
|
||||
EXPECT_EQ(mockClient.lastCallModule, "chat");
|
||||
EXPECT_EQ(mockClient.lastCallMethod, "send_message");
|
||||
EXPECT_EQ(mockClient.lastCallArgs.size(), 1);
|
||||
EXPECT_EQ(mockClient.lastCallArgs.size(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(CommandTest, Call_VerboseSyntax)
|
||||
{
|
||||
mockClient.callMethodResult = QJsonObject{
|
||||
mockClient.callMethodResult = LogosMap{
|
||||
{"status", "ok"}, {"module", "chat"}, {"method", "send_message"}
|
||||
};
|
||||
|
||||
@@ -407,7 +409,7 @@ TEST_F(CommandTest, Call_VerboseSyntax)
|
||||
|
||||
TEST_F(CommandTest, Call_MethodNotFound)
|
||||
{
|
||||
mockClient.callMethodResult = QJsonObject{
|
||||
mockClient.callMethodResult = LogosMap{
|
||||
{"status", "error"}, {"code", "METHOD_NOT_FOUND"},
|
||||
{"message", "Method 'bad' not found on module 'chat'."}
|
||||
};
|
||||
@@ -421,7 +423,7 @@ TEST_F(CommandTest, Call_MethodNotFound)
|
||||
|
||||
TEST_F(CommandTest, Call_ModuleNotLoaded)
|
||||
{
|
||||
mockClient.callMethodResult = QJsonObject{
|
||||
mockClient.callMethodResult = LogosMap{
|
||||
{"status", "error"}, {"code", "MODULE_NOT_LOADED"},
|
||||
{"message", "Module 'delivery' is not loaded."}
|
||||
};
|
||||
@@ -446,10 +448,10 @@ TEST_F(CommandTest, Call_MissingArgs)
|
||||
|
||||
TEST_F(CommandTest, Stats_Success)
|
||||
{
|
||||
mockClient.moduleStatsResult = QJsonArray{
|
||||
QJsonObject{{"name", "waku"}, {"pid", 23456}, {"cpu_percent", 2.1}, {"memory_mb", 48.3}},
|
||||
QJsonObject{{"name", "chat"}, {"pid", 23457}, {"cpu_percent", 0.4}, {"memory_mb", 22.1}}
|
||||
};
|
||||
mockClient.moduleStatsResult = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"pid", 23456}, {"cpu_percent", 2.1}, {"memory_mb", 48.3}},
|
||||
LogosMap{{"name", "chat"}, {"pid", 23457}, {"cpu_percent", 0.4}, {"memory_mb", 22.1}}
|
||||
});
|
||||
|
||||
auto cmd = createCommand("stats", mockClient, output);
|
||||
std::string out = captureOutput([&]() {
|
||||
@@ -457,9 +459,9 @@ TEST_F(CommandTest, Stats_Success)
|
||||
EXPECT_EQ(exitCode, 0);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isArray());
|
||||
EXPECT_EQ(doc.array().size(), 2);
|
||||
nlohmann::json doc = parseJson(out);
|
||||
ASSERT_TRUE(doc.is_array());
|
||||
EXPECT_EQ(doc.size(), 2u);
|
||||
}
|
||||
|
||||
// ── watch ────────────────────────────────────────────────────────────────────
|
||||
@@ -478,7 +480,7 @@ TEST_F(CommandTest, Watch_ParsesModuleAndEventName)
|
||||
auto cmd = createCommand("watch", mockClient, output);
|
||||
captureOutput([&]() {
|
||||
int exitCode = cmd->execute({"chat", "--event", "message"});
|
||||
EXPECT_EQ(exitCode, 3); // watchShouldSucceed=false => MODULE_NOT_LOADED
|
||||
EXPECT_EQ(exitCode, 3); // watchShouldSucceed=false => WATCH_FAILED
|
||||
});
|
||||
|
||||
EXPECT_EQ(mockClient.lastWatchModule, "chat");
|
||||
@@ -504,9 +506,9 @@ TEST_F(CommandTest, Watch_ModuleNotLoaded_ReturnsExit3)
|
||||
EXPECT_EQ(exitCode, 3);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "MODULE_NOT_LOADED");
|
||||
EXPECT_TRUE(doc.object().value("message").toString().contains("'missing'"));
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "WATCH_FAILED");
|
||||
EXPECT_NE(doc["message"].get<std::string>().find("'missing'"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST_F(CommandTest, Watch_NoDaemon_ReturnsExit2)
|
||||
@@ -519,15 +521,15 @@ TEST_F(CommandTest, Watch_NoDaemon_ReturnsExit2)
|
||||
EXPECT_EQ(exitCode, 2);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "NO_DAEMON");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "NO_DAEMON");
|
||||
}
|
||||
|
||||
// ── stop ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
TEST_F(CommandTest, Stop_Success)
|
||||
{
|
||||
mockClient.shutdownResult = QJsonObject{
|
||||
mockClient.shutdownResult = LogosMap{
|
||||
{"status", "ok"}, {"message", "Daemon shutting down."}
|
||||
};
|
||||
|
||||
@@ -539,8 +541,8 @@ TEST_F(CommandTest, Stop_Success)
|
||||
|
||||
EXPECT_TRUE(mockClient.shutdownCalled);
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("status").toString(), "ok");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
||||
}
|
||||
|
||||
TEST_F(CommandTest, Stop_NoDaemon)
|
||||
@@ -553,6 +555,6 @@ TEST_F(CommandTest, Stop_NoDaemon)
|
||||
EXPECT_EQ(exitCode, 2);
|
||||
});
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "NO_DAEMON");
|
||||
nlohmann::json doc = parseJson(out);
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "NO_DAEMON");
|
||||
}
|
||||
|
||||
+119
-121
@@ -32,11 +32,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <logos_json.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
@@ -69,7 +65,7 @@ std::string slurp(const fs::path& p)
|
||||
// `call --json` prints one compact envelope line on stdout, but a stray
|
||||
// qWarning on stderr (captured via 2>&1) could precede it — scan from
|
||||
// the end for the first line that parses as a JSON object.
|
||||
QJsonObject lastJsonObject(const std::string& out)
|
||||
nlohmann::json lastJsonObject(const std::string& out)
|
||||
{
|
||||
std::vector<std::string> lines;
|
||||
std::string line;
|
||||
@@ -79,13 +75,13 @@ QJsonObject lastJsonObject(const std::string& out)
|
||||
}
|
||||
if (!line.empty()) lines.push_back(line);
|
||||
for (auto it = lines.rbegin(); it != lines.rend(); ++it) {
|
||||
QJsonParseError pe;
|
||||
QJsonDocument d =
|
||||
QJsonDocument::fromJson(QByteArray::fromStdString(*it), &pe);
|
||||
if (pe.error == QJsonParseError::NoError && d.isObject())
|
||||
return d.object();
|
||||
if (it->empty()) continue;
|
||||
try {
|
||||
nlohmann::json d = nlohmann::json::parse(*it);
|
||||
if (d.is_object()) return d;
|
||||
} catch (...) {}
|
||||
}
|
||||
return {};
|
||||
return nlohmann::json::object();
|
||||
}
|
||||
|
||||
// A real logoscore daemon in an isolated config/HOME, plus helpers to
|
||||
@@ -311,13 +307,13 @@ TEST_F(ErrorPathTest, CrashedModuleDoesNotKillDaemon) {
|
||||
// host crash). Pulls the methods list from module-info --json.
|
||||
ASSERT_EQ(d.run("module-info test_basic_module", &out), 0)
|
||||
<< "module-info should succeed for a loaded module.\n" << out;
|
||||
QJsonObject pre = lastJsonObject(out);
|
||||
ASSERT_EQ(pre.value("status").toString().toStdString(), "loaded")
|
||||
nlohmann::json pre = lastJsonObject(out);
|
||||
ASSERT_EQ(pre.value("status", std::string{}), "loaded")
|
||||
<< "module should be loaded before the crash.\n" << out;
|
||||
QJsonArray methods = pre.value("methods").toArray();
|
||||
nlohmann::json methods = pre.value("methods", nlohmann::json::array());
|
||||
bool hasCrash = false;
|
||||
for (const QJsonValue& v : methods) {
|
||||
if (v.toObject().value("name").toString() == "crashOnDemand") {
|
||||
for (const auto& v : methods) {
|
||||
if (v.value("name", std::string{}) == "crashOnDemand") {
|
||||
hasCrash = true;
|
||||
break;
|
||||
}
|
||||
@@ -345,8 +341,7 @@ TEST_F(ErrorPathTest, CrashedModuleDoesNotKillDaemon) {
|
||||
bool unloaded = false;
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
if (d.run("module-info test_basic_module", &out, /*timeoutSecs=*/5) == 0) {
|
||||
lastStatus = lastJsonObject(out)
|
||||
.value("status").toString().toStdString();
|
||||
lastStatus = lastJsonObject(out).value("status", std::string{});
|
||||
if (lastStatus != "loaded") { unloaded = true; break; }
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
@@ -417,15 +412,15 @@ protected:
|
||||
|
||||
// `call test_basic_module <method> [args]` → `result` of the success
|
||||
// envelope {"status":"ok","module":...,"result":<v>}.
|
||||
QJsonValue call(const std::string& method, const std::string& args = "") {
|
||||
nlohmann::json call(const std::string& method, const std::string& args = "") {
|
||||
std::string out;
|
||||
const std::string cmd =
|
||||
"call test_basic_module " + method + (args.empty() ? "" : " " + args);
|
||||
EXPECT_EQ(s_d->run(cmd, &out), 0) << cmd << "\n" << out;
|
||||
QJsonObject env = lastJsonObject(out);
|
||||
EXPECT_EQ(env.value("status").toString().toStdString(), "ok")
|
||||
nlohmann::json env = lastJsonObject(out);
|
||||
EXPECT_EQ(env.value("status", std::string{}), "ok")
|
||||
<< cmd << "\n" << out;
|
||||
return env.value("result");
|
||||
return env.value("result", nlohmann::json{});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -436,111 +431,110 @@ std::string LoadedModuleTest::s_skipWhy;
|
||||
// ── void / bool / int (void surfaces as `true` — call_executor.cpp) ──────────
|
||||
|
||||
TEST_F(LoadedModuleTest, VoidAndBoolReturns) {
|
||||
EXPECT_TRUE(call("doNothing").toBool());
|
||||
EXPECT_TRUE(call("doNothingWithArgs", "hello 7").toBool());
|
||||
EXPECT_TRUE(call("returnTrue").toBool());
|
||||
EXPECT_FALSE(call("returnFalse").toBool());
|
||||
EXPECT_TRUE(call("isPositive", "5").toBool());
|
||||
EXPECT_FALSE(call("isPositive", "-3").toBool());
|
||||
EXPECT_FALSE(call("isPositive", "0").toBool());
|
||||
EXPECT_TRUE(call("doNothing").get<bool>());
|
||||
EXPECT_TRUE(call("doNothingWithArgs", "hello 7").get<bool>());
|
||||
EXPECT_TRUE(call("returnTrue").get<bool>());
|
||||
EXPECT_FALSE(call("returnFalse").get<bool>());
|
||||
EXPECT_TRUE(call("isPositive", "5").get<bool>());
|
||||
EXPECT_FALSE(call("isPositive", "-3").get<bool>());
|
||||
EXPECT_FALSE(call("isPositive", "0").get<bool>());
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, IntReturns) {
|
||||
EXPECT_EQ(call("returnInt").toInt(), 42);
|
||||
EXPECT_EQ(call("addInts", "2 3").toInt(), 5);
|
||||
EXPECT_EQ(call("stringLength", "abcdef").toInt(), 6);
|
||||
EXPECT_EQ(call("echoInt", "123").toInt(), 123);
|
||||
EXPECT_EQ(call("byteArraySize", "abcde").toInt(), 5);
|
||||
EXPECT_EQ(call("returnInt").get<int>(), 42);
|
||||
EXPECT_EQ(call("addInts", "2 3").get<int>(), 5);
|
||||
EXPECT_EQ(call("stringLength", "abcdef").get<int>(), 6);
|
||||
EXPECT_EQ(call("echoInt", "123").get<int>(), 123);
|
||||
EXPECT_EQ(call("byteArraySize", "abcde").get<int>(), 5);
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, StringReturns) {
|
||||
EXPECT_EQ(call("returnString").toString().toStdString(), "test_basic_module");
|
||||
EXPECT_EQ(call("echo", "roundtrip").toString().toStdString(), "roundtrip");
|
||||
EXPECT_EQ(call("concat", "foo bar").toString().toStdString(), "foobar");
|
||||
EXPECT_EQ(call("urlToString", "https://example.com/p")
|
||||
.toString().toStdString(), "https://example.com/p");
|
||||
EXPECT_EQ(call("returnString").get<std::string>(), "test_basic_module");
|
||||
EXPECT_EQ(call("echo", "roundtrip").get<std::string>(), "roundtrip");
|
||||
EXPECT_EQ(call("concat", "foo bar").get<std::string>(), "foobar");
|
||||
EXPECT_EQ(call("urlToString", "https://example.com/p").get<std::string>(),
|
||||
"https://example.com/p");
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, LogosResultShapes) {
|
||||
QJsonObject ok = call("successResult").toObject();
|
||||
EXPECT_TRUE(ok.value("success").toBool());
|
||||
EXPECT_EQ(ok.value("value").toString().toStdString(), "operation succeeded");
|
||||
EXPECT_TRUE(ok.value("error").isNull());
|
||||
nlohmann::json ok = call("successResult");
|
||||
EXPECT_TRUE(ok["success"].get<bool>());
|
||||
EXPECT_EQ(ok["value"].get<std::string>(), "operation succeeded");
|
||||
EXPECT_TRUE(ok["error"].is_null());
|
||||
|
||||
QJsonObject err = call("errorResult").toObject();
|
||||
EXPECT_FALSE(err.value("success").toBool());
|
||||
EXPECT_TRUE(err.value("value").isNull());
|
||||
EXPECT_EQ(err.value("error").toString().toStdString(),
|
||||
"deliberate error for testing");
|
||||
nlohmann::json err = call("errorResult");
|
||||
EXPECT_FALSE(err["success"].get<bool>());
|
||||
EXPECT_TRUE(err["value"].is_null());
|
||||
EXPECT_EQ(err["error"].get<std::string>(), "deliberate error for testing");
|
||||
|
||||
QJsonObject m = call("resultWithMap").toObject().value("value").toObject();
|
||||
EXPECT_EQ(m.value("name").toString().toStdString(), "test");
|
||||
EXPECT_EQ(m.value("count").toInt(), 42);
|
||||
EXPECT_TRUE(m.value("active").toBool());
|
||||
nlohmann::json m = call("resultWithMap")["value"];
|
||||
EXPECT_EQ(m["name"].get<std::string>(), "test");
|
||||
EXPECT_EQ(m["count"].get<int>(), 42);
|
||||
EXPECT_TRUE(m["active"].get<bool>());
|
||||
|
||||
QJsonArray lst = call("resultWithList").toObject().value("value").toArray();
|
||||
ASSERT_EQ(lst.size(), 2);
|
||||
EXPECT_EQ(lst[0].toObject().value("label").toString().toStdString(), "first");
|
||||
EXPECT_EQ(lst[1].toObject().value("id").toInt(), 2);
|
||||
nlohmann::json lst = call("resultWithList")["value"];
|
||||
ASSERT_EQ(lst.size(), 2u);
|
||||
EXPECT_EQ(lst[0]["label"].get<std::string>(), "first");
|
||||
EXPECT_EQ(lst[1]["id"].get<int>(), 2);
|
||||
|
||||
QJsonObject vOk = call("validateInput", "hello").toObject();
|
||||
EXPECT_TRUE(vOk.value("success").toBool());
|
||||
EXPECT_EQ(vOk.value("value").toObject().value("length").toInt(), 5);
|
||||
nlohmann::json vOk = call("validateInput", "hello");
|
||||
EXPECT_TRUE(vOk["success"].get<bool>());
|
||||
EXPECT_EQ(vOk["value"]["length"].get<int>(), 5);
|
||||
|
||||
QJsonObject vErr = call("validateInput", "''").toObject();
|
||||
EXPECT_FALSE(vErr.value("success").toBool());
|
||||
EXPECT_EQ(vErr.value("error").toString().toStdString(), "input cannot be empty");
|
||||
nlohmann::json vErr = call("validateInput", "''");
|
||||
EXPECT_FALSE(vErr["success"].get<bool>());
|
||||
EXPECT_EQ(vErr["error"].get<std::string>(), "input cannot be empty");
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, VariantAndCollectionReturns) {
|
||||
EXPECT_EQ(call("returnVariantInt").toInt(), 99);
|
||||
EXPECT_EQ(call("returnVariantString").toString().toStdString(), "variant_string");
|
||||
EXPECT_EQ(call("returnVariantInt").get<int>(), 99);
|
||||
EXPECT_EQ(call("returnVariantString").get<std::string>(), "variant_string");
|
||||
|
||||
QJsonObject vm = call("returnVariantMap").toObject();
|
||||
EXPECT_EQ(vm.value("key").toString().toStdString(), "value");
|
||||
EXPECT_EQ(vm.value("number").toInt(), 7);
|
||||
nlohmann::json vm = call("returnVariantMap");
|
||||
EXPECT_EQ(vm["key"].get<std::string>(), "value");
|
||||
EXPECT_EQ(vm["number"].get<int>(), 7);
|
||||
|
||||
QJsonArray vl = call("returnVariantList").toArray();
|
||||
ASSERT_EQ(vl.size(), 3);
|
||||
EXPECT_EQ(vl[0].toString().toStdString(), "alpha");
|
||||
EXPECT_EQ(vl[2].toString().toStdString(), "gamma");
|
||||
nlohmann::json vl = call("returnVariantList");
|
||||
ASSERT_EQ(vl.size(), 3u);
|
||||
EXPECT_EQ(vl[0].get<std::string>(), "alpha");
|
||||
EXPECT_EQ(vl[2].get<std::string>(), "gamma");
|
||||
|
||||
QJsonArray ja = call("returnJsonArray").toArray();
|
||||
ASSERT_EQ(ja.size(), 3);
|
||||
EXPECT_EQ(ja[0].toInt(), 1);
|
||||
EXPECT_EQ(ja[2].toInt(), 3);
|
||||
nlohmann::json ja = call("returnJsonArray");
|
||||
ASSERT_EQ(ja.size(), 3u);
|
||||
EXPECT_EQ(ja[0].get<int>(), 1);
|
||||
EXPECT_EQ(ja[2].get<int>(), 3);
|
||||
|
||||
QJsonArray mk = call("makeJsonArray", "x y").toArray();
|
||||
ASSERT_EQ(mk.size(), 2);
|
||||
EXPECT_EQ(mk[1].toString().toStdString(), "y");
|
||||
nlohmann::json mk = call("makeJsonArray", "x y");
|
||||
ASSERT_EQ(mk.size(), 2u);
|
||||
EXPECT_EQ(mk[1].get<std::string>(), "y");
|
||||
|
||||
QJsonArray sl = call("returnStringList").toArray();
|
||||
ASSERT_EQ(sl.size(), 3);
|
||||
EXPECT_EQ(sl[1].toString().toStdString(), "two");
|
||||
nlohmann::json sl = call("returnStringList");
|
||||
ASSERT_EQ(sl.size(), 3u);
|
||||
EXPECT_EQ(sl[1].get<std::string>(), "two");
|
||||
|
||||
QJsonArray sp = call("splitString", "a,b,c").toArray();
|
||||
ASSERT_EQ(sp.size(), 3);
|
||||
EXPECT_EQ(sp[0].toString().toStdString(), "a");
|
||||
EXPECT_EQ(sp[2].toString().toStdString(), "c");
|
||||
nlohmann::json sp = call("splitString", "a,b,c");
|
||||
ASSERT_EQ(sp.size(), 3u);
|
||||
EXPECT_EQ(sp[0].get<std::string>(), "a");
|
||||
EXPECT_EQ(sp[2].get<std::string>(), "c");
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, ArgCountFanOut) {
|
||||
EXPECT_EQ(call("noArgs").toString().toStdString(), "noArgs()");
|
||||
EXPECT_EQ(call("oneArg", "x").toString().toStdString(), "oneArg(x)");
|
||||
EXPECT_EQ(call("twoArgs", "x 7").toString().toStdString(), "twoArgs(x, 7)");
|
||||
EXPECT_EQ(call("threeArgs", "x 7 true").toString().toStdString(),
|
||||
EXPECT_EQ(call("noArgs").get<std::string>(), "noArgs()");
|
||||
EXPECT_EQ(call("oneArg", "x").get<std::string>(), "oneArg(x)");
|
||||
EXPECT_EQ(call("twoArgs", "x 7").get<std::string>(), "twoArgs(x, 7)");
|
||||
EXPECT_EQ(call("threeArgs", "x 7 true").get<std::string>(),
|
||||
"threeArgs(x, 7, true)");
|
||||
EXPECT_EQ(call("fourArgs", "x 7 false y").toString().toStdString(),
|
||||
EXPECT_EQ(call("fourArgs", "x 7 false y").get<std::string>(),
|
||||
"fourArgs(x, 7, false, y)");
|
||||
EXPECT_EQ(call("fiveArgs", "x 7 true y 9").toString().toStdString(),
|
||||
EXPECT_EQ(call("fiveArgs", "x 7 true y 9").get<std::string>(),
|
||||
"fiveArgs(x, 7, true, y, 9)");
|
||||
EXPECT_TRUE(call("echoBool", "true").toBool());
|
||||
EXPECT_FALSE(call("echoBool", "false").toBool());
|
||||
EXPECT_TRUE(call("echoBool", "true").get<bool>());
|
||||
EXPECT_FALSE(call("echoBool", "false").get<bool>());
|
||||
}
|
||||
|
||||
TEST_F(LoadedModuleTest, AsyncEchoWithDelay) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
EXPECT_EQ(call("echoWithDelay", "pong 200").toString().toStdString(), "pong");
|
||||
EXPECT_EQ(call("echoWithDelay", "pong 200").get<std::string>(), "pong");
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
EXPECT_GE(ms, 200) << "echoWithDelay returned too fast (" << ms << "ms)";
|
||||
@@ -612,8 +606,10 @@ TEST_F(LoadedModuleTest, ConcurrentEchoFromManyClients) {
|
||||
const std::string tok = "tok" + std::to_string(i);
|
||||
std::string out;
|
||||
int rc = s_d->run("call test_basic_module echo " + tok, &out);
|
||||
const std::string got =
|
||||
lastJsonObject(out).value("result").toString().toStdString();
|
||||
std::string got;
|
||||
try {
|
||||
got = lastJsonObject(out)["result"].get<std::string>();
|
||||
} catch (...) {}
|
||||
ok[i] = (rc == 0 && got == tok) ? 1 : 0;
|
||||
if (!ok[i]) detail[i] = "rc=" + std::to_string(rc) +
|
||||
" got='" + got + "' want='" + tok + "'\n" + out;
|
||||
@@ -636,30 +632,32 @@ TEST_F(LoadedModuleTest, ConcurrentMixedMethodsFromManyClients) {
|
||||
std::string out;
|
||||
int rc = -1;
|
||||
bool good = false;
|
||||
switch (i % 4) {
|
||||
case 0: { // addInts(i, i) == 2*i
|
||||
rc = s_d->run("call test_basic_module addInts " +
|
||||
std::to_string(i) + " " + std::to_string(i), &out);
|
||||
good = lastJsonObject(out).value("result").toInt() == 2 * i;
|
||||
break;
|
||||
}
|
||||
case 1: { // echoInt(i) == i
|
||||
rc = s_d->run("call test_basic_module echoInt " + std::to_string(i), &out);
|
||||
good = lastJsonObject(out).value("result").toInt() == i;
|
||||
break;
|
||||
}
|
||||
case 2: { // returnString() == "test_basic_module"
|
||||
rc = s_d->run("call test_basic_module returnString", &out);
|
||||
good = lastJsonObject(out).value("result").toString() == "test_basic_module";
|
||||
break;
|
||||
}
|
||||
default: { // stringLength("xxxx..i..") == i
|
||||
rc = s_d->run("call test_basic_module stringLength " +
|
||||
std::string(static_cast<size_t>(i), 'x'), &out);
|
||||
good = lastJsonObject(out).value("result").toInt() == i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
switch (i % 4) {
|
||||
case 0: { // addInts(i, i) == 2*i
|
||||
rc = s_d->run("call test_basic_module addInts " +
|
||||
std::to_string(i) + " " + std::to_string(i), &out);
|
||||
good = lastJsonObject(out)["result"].get<int>() == 2 * i;
|
||||
break;
|
||||
}
|
||||
case 1: { // echoInt(i) == i
|
||||
rc = s_d->run("call test_basic_module echoInt " + std::to_string(i), &out);
|
||||
good = lastJsonObject(out)["result"].get<int>() == i;
|
||||
break;
|
||||
}
|
||||
case 2: { // returnString() == "test_basic_module"
|
||||
rc = s_d->run("call test_basic_module returnString", &out);
|
||||
good = lastJsonObject(out)["result"].get<std::string>() == "test_basic_module";
|
||||
break;
|
||||
}
|
||||
default: { // stringLength("xxxx..i..") == i
|
||||
rc = s_d->run("call test_basic_module stringLength " +
|
||||
std::string(static_cast<size_t>(i), 'x'), &out);
|
||||
good = lastJsonObject(out)["result"].get<int>() == i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (...) {}
|
||||
ok[i] = (rc == 0 && good) ? 1 : 0;
|
||||
if (!ok[i]) detail[i] = "case=" + std::to_string(i % 4) +
|
||||
" rc=" + std::to_string(rc) + "\n" + out;
|
||||
|
||||
+77
-86
@@ -1,7 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <logos_json.h>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
#include "client/output.h"
|
||||
@@ -54,26 +52,29 @@ TEST_F(OutputTest, IsJsonMode_TrueWhenForced)
|
||||
TEST_F(OutputTest, PrintSuccess_JsonObject)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject data{{"status", "ok"}, {"module", "waku"}};
|
||||
LogosMap data{{"status", "ok"}, {"module", "waku"}};
|
||||
jsonOutput.printSuccess(data);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("status").toString(), "ok");
|
||||
EXPECT_EQ(doc.object().value("module").toString(), "waku");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
||||
EXPECT_EQ(doc["module"].get<std::string>(), "waku");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintSuccess_JsonArray)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonArray data{QJsonObject{{"name", "waku"}}, QJsonObject{{"name", "chat"}}};
|
||||
LogosList data = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}},
|
||||
LogosMap{{"name", "chat"}}
|
||||
});
|
||||
jsonOutput.printSuccess(data);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isArray());
|
||||
EXPECT_EQ(doc.array().size(), 2);
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_array());
|
||||
EXPECT_EQ(doc.size(), 2u);
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintSuccess_String)
|
||||
@@ -82,155 +83,147 @@ TEST_F(OutputTest, PrintSuccess_String)
|
||||
jsonOutput.printSuccess(std::string("All good"));
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("status").toString(), "ok");
|
||||
EXPECT_EQ(doc.object().value("message").toString(), "All good");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["status"].get<std::string>(), "ok");
|
||||
EXPECT_EQ(doc["message"].get<std::string>(), "All good");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintError_Json)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
jsonOutput.printError("MODULE_NOT_FOUND", "Module 'foo' not found.",
|
||||
QJsonObject{{"known_modules", QJsonArray{"waku", "chat"}}});
|
||||
LogosMap{{"known_modules", LogosList::array({"waku", "chat"})}});
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("status").toString(), "error");
|
||||
EXPECT_EQ(doc.object().value("code").toString(), "MODULE_NOT_FOUND");
|
||||
EXPECT_EQ(doc.object().value("message").toString(), "Module 'foo' not found.");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["status"].get<std::string>(), "error");
|
||||
EXPECT_EQ(doc["code"].get<std::string>(), "MODULE_NOT_FOUND");
|
||||
EXPECT_EQ(doc["message"].get<std::string>(), "Module 'foo' not found.");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintModuleList_Json)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonArray modules = {
|
||||
QJsonObject{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}},
|
||||
QJsonObject{{"name", "chat"}, {"version", "0.2.0"}, {"status", "crashed"}}
|
||||
};
|
||||
LogosList modules = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}},
|
||||
LogosMap{{"name", "chat"}, {"version", "0.2.0"}, {"status", "crashed"}}
|
||||
});
|
||||
jsonOutput.printModuleList(modules);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isArray());
|
||||
EXPECT_EQ(doc.array().size(), 2);
|
||||
EXPECT_EQ(doc.array().at(0).toObject().value("name").toString(), "waku");
|
||||
EXPECT_EQ(doc.array().at(1).toObject().value("status").toString(), "crashed");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_array());
|
||||
EXPECT_EQ(doc.size(), 2u);
|
||||
EXPECT_EQ(doc[0]["name"].get<std::string>(), "waku");
|
||||
EXPECT_EQ(doc[1]["status"].get<std::string>(), "crashed");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintStatus_Json_Running)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject status;
|
||||
status["daemon"] = QJsonObject{
|
||||
{"status", "running"}, {"pid", 12345}, {"uptime_seconds", 3600}, {"version", "1.0"}
|
||||
};
|
||||
status["modules_summary"] = QJsonObject{{"loaded", 2}, {"crashed", 0}, {"not_loaded", 1}};
|
||||
status["modules"] = QJsonArray{
|
||||
QJsonObject{{"name", "waku"}, {"status", "loaded"}},
|
||||
QJsonObject{{"name", "chat"}, {"status", "loaded"}},
|
||||
QJsonObject{{"name", "delivery"}, {"status", "not_loaded"}}
|
||||
LogosMap status{
|
||||
{"daemon", LogosMap{
|
||||
{"status", "running"}, {"pid", 12345}, {"uptime_seconds", 3600}, {"version", "1.0"}
|
||||
}},
|
||||
{"modules_summary", LogosMap{{"loaded", 2}, {"crashed", 0}, {"not_loaded", 1}}},
|
||||
{"modules", nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"status", "loaded"}},
|
||||
LogosMap{{"name", "chat"}, {"status", "loaded"}},
|
||||
LogosMap{{"name", "delivery"}, {"status", "not_loaded"}}
|
||||
})}
|
||||
};
|
||||
jsonOutput.printStatus(status);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("daemon").toObject().value("status").toString(), "running");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["daemon"]["status"].get<std::string>(), "running");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintStatus_Json_NotRunning)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject status;
|
||||
status["daemon"] = QJsonObject{{"status", "not_running"}};
|
||||
LogosMap status{{"daemon", LogosMap{{"status", "not_running"}}}};
|
||||
jsonOutput.printStatus(status);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("daemon").toObject().value("status").toString(), "not_running");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["daemon"]["status"].get<std::string>(), "not_running");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintModuleInfo_Json)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject info{
|
||||
LogosMap info{
|
||||
{"name", "chat"},
|
||||
{"version", "0.2.0"},
|
||||
{"status", "loaded"},
|
||||
{"pid", 23457},
|
||||
{"uptime_seconds", 8040},
|
||||
{"dependencies", QJsonArray{"waku", "store"}},
|
||||
{"methods", QJsonArray{
|
||||
QJsonObject{
|
||||
{"dependencies", nlohmann::json::array({"waku", "store"})},
|
||||
{"methods", nlohmann::json::array({
|
||||
LogosMap{
|
||||
{"name", "send_message"},
|
||||
{"params", QJsonArray{QJsonObject{{"name", "text"}, {"type", "QString"}}}},
|
||||
{"params", nlohmann::json::array({LogosMap{{"name", "text"}, {"type", "QString"}}})},
|
||||
{"return_type", "QString"}
|
||||
}
|
||||
}}
|
||||
})}
|
||||
};
|
||||
jsonOutput.printModuleInfo(info);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("name").toString(), "chat");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["name"].get<std::string>(), "chat");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintEvent_Ndjson)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject event{
|
||||
LogosMap event{
|
||||
{"timestamp", "2026-03-23T14:30:01Z"},
|
||||
{"module", "chat"},
|
||||
{"event", "chat-message"},
|
||||
{"data", QJsonObject{{"from", "alice"}, {"text", "hello"}}}
|
||||
{"data", LogosMap{{"from", "alice"}, {"text", "hello"}}}
|
||||
};
|
||||
jsonOutput.printEvent(event);
|
||||
|
||||
std::string out = cap.str();
|
||||
// NDJSON: should be a single line of JSON
|
||||
EXPECT_NE(out.find("{"), std::string::npos);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("event").toString(), "chat-message");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["event"].get<std::string>(), "chat-message");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintStats_Json)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonArray stats = {
|
||||
QJsonObject{{"name", "waku"}, {"pid", 23456}, {"cpu_percent", 2.1}, {"memory_mb", 48.3}},
|
||||
QJsonObject{{"name", "chat"}, {"pid", 23457}, {"cpu_percent", 0.4}, {"memory_mb", 22.1}}
|
||||
};
|
||||
LogosList stats = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"pid", 23456}, {"cpu_percent", 2.1}, {"memory_mb", 48.3}},
|
||||
LogosMap{{"name", "chat"}, {"pid", 23457}, {"cpu_percent", 0.4}, {"memory_mb", 22.1}}
|
||||
});
|
||||
jsonOutput.printStats(stats);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isArray());
|
||||
EXPECT_EQ(doc.array().size(), 2);
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_array());
|
||||
EXPECT_EQ(doc.size(), 2u);
|
||||
}
|
||||
|
||||
// ── Human Mode Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
TEST_F(OutputTest, PrintModuleList_Human)
|
||||
{
|
||||
// Force human mode by setting JSON false and calling directly
|
||||
Output out(false);
|
||||
out.setJsonMode(false);
|
||||
|
||||
// We can't easily test human output since it goes to stdout and
|
||||
// isTTY detection may not work in tests. But we verify the method doesn't crash.
|
||||
CaptureStdout cap;
|
||||
|
||||
// Force the output object to think it's NOT json
|
||||
// In test environments stdout is often not a TTY so it defaults to JSON.
|
||||
// We test the JSON path above. Here we at least verify no crashes.
|
||||
QJsonArray modules = {
|
||||
QJsonObject{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}}
|
||||
};
|
||||
LogosList modules = nlohmann::json::array({
|
||||
LogosMap{{"name", "waku"}, {"version", "0.1.0"}, {"status", "loaded"}, {"uptime_seconds", 8040}}
|
||||
});
|
||||
out.printModuleList(modules);
|
||||
std::string output = cap.str();
|
||||
EXPECT_FALSE(output.empty());
|
||||
@@ -239,24 +232,22 @@ TEST_F(OutputTest, PrintModuleList_Human)
|
||||
TEST_F(OutputTest, PrintReload_Json_Success)
|
||||
{
|
||||
CaptureStdout cap;
|
||||
QJsonObject result{
|
||||
LogosMap result{
|
||||
{"action", "reload"}, {"module", "chat"}, {"version", "0.2.0"},
|
||||
{"status", "loaded"}, {"pid", 51203}, {"previous_status", "crashed"}
|
||||
};
|
||||
jsonOutput.printReload(result);
|
||||
|
||||
std::string out = cap.str();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(QByteArray::fromStdString(out));
|
||||
ASSERT_TRUE(doc.isObject());
|
||||
EXPECT_EQ(doc.object().value("module").toString(), "chat");
|
||||
nlohmann::json doc = nlohmann::json::parse(out);
|
||||
ASSERT_TRUE(doc.is_object());
|
||||
EXPECT_EQ(doc["module"].get<std::string>(), "chat");
|
||||
}
|
||||
|
||||
TEST_F(OutputTest, PrintError_Human)
|
||||
{
|
||||
CaptureStderr cap;
|
||||
Output out(false);
|
||||
// In non-TTY environment, this may still go through JSON path,
|
||||
// but we ensure no crash
|
||||
out.printError("NO_DAEMON", "No running logoscore daemon.");
|
||||
// Just verify it doesn't crash; actual output depends on TTY detection
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user