mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-31 20:21:09 +00:00
381 lines
13 KiB
C++
381 lines
13 KiB
C++
#include "test_basic_module_impl.h"
|
|||
|
|
|
||
|
|
#include <chrono>
|
||
|
|
#include <sstream>
|
||
|
|
#include <thread>
|
||
|
|
|
||
|
|
// Every body below is a straight transcription of the Qt plugin's, with the
|
||
|
|
// Qt containers replaced by their std / nlohmann counterparts. The RESULTS are
|
||
|
|
// deliberately byte-identical strings — the integration suite asserts on them
|
||
|
|
// literally ("Result: fiveArgs(a, 1, true, b, 2)"), so any drift in the
|
||
|
|
// formatting would be a real regression, not cosmetics.
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
// QStringLiteral("twoArgs(%1, %2)").arg(a).arg(b) has no std equivalent, and
|
||
|
|
// std::ostringstream turns bools into "1"/"0" rather than "true"/"false".
|
||
|
|
// One helper keeps the bool spelling in a single place.
|
||
|
|
inline const char* boolText(bool v) { return v ? "true" : "false"; }
|
||
|
|
|
||
|
|
// ── UTF-8 ────────────────────────────────────────────────────────────────────
|
||
|
|
// Every std::string crossing this module's contract is UTF-8, and "length"
|
||
|
|
// means CHARACTERS (Unicode code points). In UTF-8 a character is one lead
|
||
|
|
// byte plus zero or more continuation bytes (top bits 10), so counting the
|
||
|
|
// bytes that are NOT continuations counts the characters — for any character,
|
||
|
|
// of any width, including the 4-byte ones above the BMP.
|
||
|
|
inline bool isContinuationByte(char c)
|
||
|
|
{
|
||
|
|
return (static_cast<unsigned char>(c) & 0xC0) == 0x80;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t characterCount(const std::string& s)
|
||
|
|
{
|
||
|
|
int64_t n = 0;
|
||
|
|
for (char c : s) {
|
||
|
|
if (!isContinuationByte(c)) ++n;
|
||
|
|
}
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── URL ──────────────────────────────────────────────────────────────────────
|
||
|
|
// Deterministic ASCII lowercase. std::tolower is locale-dependent and would
|
||
|
|
// also touch bytes >= 0x80, which in UTF-8 are fragments of characters.
|
||
|
|
inline char asciiLower(char c)
|
||
|
|
{
|
||
|
|
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
|
||
|
|
}
|
||
|
|
|
||
|
|
inline bool isSchemeStart(char c)
|
||
|
|
{
|
||
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
||
|
|
}
|
||
|
|
|
||
|
|
inline bool isSchemeChar(char c)
|
||
|
|
{
|
||
|
|
return isSchemeStart(c) || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.';
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
// ── Return type: void ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
void TestBasicModuleImpl::doNothing()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
void TestBasicModuleImpl::doNothingWithArgs(const std::string& a, int64_t b)
|
||
|
|
{
|
||
|
|
(void)a;
|
||
|
|
(void)b;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: bool ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
bool TestBasicModuleImpl::returnTrue()
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool TestBasicModuleImpl::returnFalse()
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool TestBasicModuleImpl::isPositive(int64_t value)
|
||
|
|
{
|
||
|
|
return value > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: int ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
int64_t TestBasicModuleImpl::returnInt()
|
||
|
|
{
|
||
|
|
return 42;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t TestBasicModuleImpl::addInts(int64_t a, int64_t b)
|
||
|
|
{
|
||
|
|
return a + b;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t TestBasicModuleImpl::stringLength(const std::string& s)
|
||
|
|
{
|
||
|
|
// Characters, not bytes: "héllo" is 5 even though it is 6 bytes of UTF-8.
|
||
|
|
// An emoji outside the BMP is 1 — the byte count says 4 and Qt's UTF-16
|
||
|
|
// QString said 2, and neither of those is a number of characters.
|
||
|
|
return characterCount(s);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: string ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::returnString()
|
||
|
|
{
|
||
|
|
return "test_basic_module";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::echo(const std::string& input)
|
||
|
|
{
|
||
|
|
return input;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::concat(const std::string& a, const std::string& b)
|
||
|
|
{
|
||
|
|
return a + b;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: result ──────────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// StdLogosResult is {bool success; nlohmann::json value; std::string error;}.
|
||
|
|
// The glue converts it to the same Qt LogosResult {success, value, error}
|
||
|
|
// shape the Qt plugin returned, so the wire payload is unchanged.
|
||
|
|
|
||
|
|
StdLogosResult TestBasicModuleImpl::successResult()
|
||
|
|
{
|
||
|
|
return {true, "operation succeeded", ""};
|
||
|
|
}
|
||
|
|
|
||
|
|
StdLogosResult TestBasicModuleImpl::errorResult()
|
||
|
|
{
|
||
|
|
return {false, nlohmann::json(), "deliberate error for testing"};
|
||
|
|
}
|
||
|
|
|
||
|
|
StdLogosResult TestBasicModuleImpl::resultWithMap()
|
||
|
|
{
|
||
|
|
nlohmann::json map;
|
||
|
|
map["name"] = "test";
|
||
|
|
map["count"] = 42;
|
||
|
|
map["active"] = true;
|
||
|
|
return {true, map, ""};
|
||
|
|
}
|
||
|
|
|
||
|
|
StdLogosResult TestBasicModuleImpl::resultWithList()
|
||
|
|
{
|
||
|
|
nlohmann::json item1;
|
||
|
|
item1["id"] = 1;
|
||
|
|
item1["label"] = "first";
|
||
|
|
nlohmann::json item2;
|
||
|
|
item2["id"] = 2;
|
||
|
|
item2["label"] = "second";
|
||
|
|
return {true, nlohmann::json::array({item1, item2}), ""};
|
||
|
|
}
|
||
|
|
|
||
|
|
StdLogosResult TestBasicModuleImpl::validateInput(const std::string& input)
|
||
|
|
{
|
||
|
|
if (input.empty()) {
|
||
|
|
return {false, nlohmann::json(), "input cannot be empty"};
|
||
|
|
}
|
||
|
|
nlohmann::json data;
|
||
|
|
data["input"] = input;
|
||
|
|
// Same definition of length as stringLength: characters, not bytes.
|
||
|
|
data["length"] = characterCount(input);
|
||
|
|
return {true, data, ""};
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: any (QVariant) ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
nlohmann::json TestBasicModuleImpl::returnVariantInt()
|
||
|
|
{
|
||
|
|
return 99;
|
||
|
|
}
|
||
|
|
|
||
|
|
nlohmann::json TestBasicModuleImpl::returnVariantString()
|
||
|
|
{
|
||
|
|
return "variant_string";
|
||
|
|
}
|
||
|
|
|
||
|
|
nlohmann::json TestBasicModuleImpl::returnVariantMap()
|
||
|
|
{
|
||
|
|
nlohmann::json map;
|
||
|
|
map["key"] = "value";
|
||
|
|
map["number"] = 7;
|
||
|
|
return map;
|
||
|
|
}
|
||
|
|
|
||
|
|
nlohmann::json TestBasicModuleImpl::returnVariantList()
|
||
|
|
{
|
||
|
|
return nlohmann::json::array({"alpha", "beta", "gamma"});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: JSON array ──────────────────────────────────────────────────
|
||
|
|
|
||
|
|
LogosList TestBasicModuleImpl::returnJsonArray()
|
||
|
|
{
|
||
|
|
return nlohmann::json::array({1, 2, 3});
|
||
|
|
}
|
||
|
|
|
||
|
|
LogosList TestBasicModuleImpl::makeJsonArray(const std::string& a, const std::string& b)
|
||
|
|
{
|
||
|
|
return nlohmann::json::array({a, b});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Return type: string list ─────────────────────────────────────────────────
|
||
|
|
|
||
|
|
std::vector<std::string> TestBasicModuleImpl::returnStringList()
|
||
|
|
{
|
||
|
|
return {"one", "two", "three"};
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> TestBasicModuleImpl::splitString(const std::string& input)
|
||
|
|
{
|
||
|
|
// QString::split(",", Qt::SkipEmptyParts) — empty fields are dropped, and
|
||
|
|
// an input with no comma yields the whole string as the single element.
|
||
|
|
std::vector<std::string> out;
|
||
|
|
std::string field;
|
||
|
|
std::istringstream stream(input);
|
||
|
|
while (std::getline(stream, field, ',')) {
|
||
|
|
if (!field.empty()) out.push_back(field);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Parameter types ──────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
int64_t TestBasicModuleImpl::echoInt(int64_t n)
|
||
|
|
{
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool TestBasicModuleImpl::echoBool(bool b)
|
||
|
|
{
|
||
|
|
return b;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::joinStrings(const std::vector<std::string>& list)
|
||
|
|
{
|
||
|
|
std::string out;
|
||
|
|
for (size_t i = 0; i < list.size(); ++i) {
|
||
|
|
if (i) out += ", ";
|
||
|
|
out += list[i];
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
int64_t TestBasicModuleImpl::byteArraySize(const std::vector<uint8_t>& data)
|
||
|
|
{
|
||
|
|
return static_cast<int64_t>(data.size());
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::urlToString(const std::string& url)
|
||
|
|
{
|
||
|
|
// The Qt version took a QUrl and returned url.toString(); QUrl has no
|
||
|
|
// universal spelling, so the parameter is the string form the caller was
|
||
|
|
// serialising to anyway. What the method OWES its caller is unchanged:
|
||
|
|
// the URL back, with the parts that are defined to be case-insensitive
|
||
|
|
// normalised to lower case. That is the scheme and the host, and nothing
|
||
|
|
// else — the path and the query are case-sensitive, so "/a/../b?x=1" is
|
||
|
|
// returned exactly as given (collapsing the "/.." would change which
|
||
|
|
// resource the URL names, and no caller asked for that).
|
||
|
|
std::string out = url;
|
||
|
|
|
||
|
|
// ── scheme ──────────────────────────────────────────────────────────────
|
||
|
|
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) followed by ':'.
|
||
|
|
size_t colon = std::string::npos;
|
||
|
|
if (!out.empty() && isSchemeStart(out[0])) {
|
||
|
|
size_t i = 1;
|
||
|
|
while (i < out.size() && isSchemeChar(out[i])) ++i;
|
||
|
|
if (i < out.size() && out[i] == ':') colon = i;
|
||
|
|
}
|
||
|
|
if (colon == std::string::npos) {
|
||
|
|
// No scheme: nothing here is case-insensitive, so nothing is touched.
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
for (size_t i = 0; i < colon; ++i) out[i] = asciiLower(out[i]);
|
||
|
|
|
||
|
|
// ── host ────────────────────────────────────────────────────────────────
|
||
|
|
// Only an authority-based URL ("scheme://…") has a host. It runs to the
|
||
|
|
// first '/', '?' or '#'.
|
||
|
|
if (out.compare(colon + 1, 2, "//") != 0) return out;
|
||
|
|
const size_t authority = colon + 3;
|
||
|
|
size_t authorityEnd = authority;
|
||
|
|
while (authorityEnd < out.size() && out[authorityEnd] != '/' && out[authorityEnd] != '?'
|
||
|
|
&& out[authorityEnd] != '#') {
|
||
|
|
++authorityEnd;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Userinfo ("user:password@") is case-SENSITIVE and stays as it is; the
|
||
|
|
// host (and any ":port", which is digits) is what gets lowercased.
|
||
|
|
size_t host = authority;
|
||
|
|
for (size_t i = authority; i < authorityEnd; ++i) {
|
||
|
|
if (out[i] == '@') host = i + 1;
|
||
|
|
}
|
||
|
|
for (size_t i = host; i < authorityEnd; ++i) out[i] = asciiLower(out[i]);
|
||
|
|
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Argument counts 0–5 ──────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::noArgs()
|
||
|
|
{
|
||
|
|
return "noArgs()";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::oneArg(const std::string& a)
|
||
|
|
{
|
||
|
|
return "oneArg(" + a + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::twoArgs(const std::string& a, int64_t b)
|
||
|
|
{
|
||
|
|
return "twoArgs(" + a + ", " + std::to_string(b) + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::threeArgs(const std::string& a, int64_t b, bool c)
|
||
|
|
{
|
||
|
|
return "threeArgs(" + a + ", " + std::to_string(b) + ", " + boolText(c) + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::fourArgs(const std::string& a, int64_t b, bool c,
|
||
|
|
const std::string& d)
|
||
|
|
{
|
||
|
|
return "fourArgs(" + a + ", " + std::to_string(b) + ", " + boolText(c) + ", " + d + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::fiveArgs(const std::string& a, int64_t b, bool c,
|
||
|
|
const std::string& d, int64_t e)
|
||
|
|
{
|
||
|
|
return "fiveArgs(" + a + ", " + std::to_string(b) + ", " + boolText(c) + ", " + d + ", "
|
||
|
|
+ std::to_string(e) + ")";
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Events ───────────────────────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// The Qt plugin emitted these dynamically:
|
||
|
|
// emit eventResponse("testEvent", QVariantList() << data);
|
||
|
|
// The typed emitters below carry the SAME event names and the same payload
|
||
|
|
// arity; their bodies are generated into test_basic_module_events.cpp.
|
||
|
|
|
||
|
|
void TestBasicModuleImpl::emitTestEvent(const std::string& data)
|
||
|
|
{
|
||
|
|
testEvent(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
void TestBasicModuleImpl::emitMultiArgEvent(const std::string& name, int64_t count)
|
||
|
|
{
|
||
|
|
multiArgEvent(name, count);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Async helpers ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
std::string TestBasicModuleImpl::echoWithDelay(const std::string& value, int64_t delayMs)
|
||
|
|
{
|
||
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Crash testing ────────────────────────────────────────────────────────────
|
||
|
|
//
|
||
|
|
// Used by the crash-isolation integration test in logos-logoscore-cli to prove
|
||
|
|
// that a faulty module brings down only its host subprocess, not the logoscore
|
||
|
|
// daemon. A null deref produces a real SIGSEGV under any sanitizer or debugger,
|
||
|
|
// which is exactly what we want to defend against.
|
||
|
|
|
||
|
|
void TestBasicModuleImpl::crashOnDemand()
|
||
|
|
{
|
||
|
|
// Force the optimizer to actually emit the deref. `volatile` defeats
|
||
|
|
// -O2 dead-store elimination; reading back guarantees the load fires.
|
||
|
|
volatile int* p = nullptr;
|
||
|
|
*p = 0xDEAD;
|
||
|
|
(void)*p;
|
||
|
|
}
|