feat(generator): async callers can see the error, sync callers can set a deadline (#132)

The two consumer surfaces had complementary holes:

  sync :  T    foo(params…, logos::CallError* err = nullptr)   error yes, timeout NO
  async:  void fooAsync(params…, cb, Timeout = Timeout())      timeout yes, error NO

so an async caller could not tell a failed remote call from a provider that
legitimately returned 0 / "" / false — the exact ambiguity the sync path's
CallError* was added to resolve — and a sync caller could not say how long it
was willing to wait, even though the transport overload the generator already
calls takes both.

Both fixes are additive:

  T    foo(params…, logos::CallError* err = nullptr, Timeout timeout = Timeout());
  void fooAsync(params…, std::function<void(T)> cb, Timeout timeout = Timeout());   // unchanged
  void fooAsyncResult(params…, std::function<void(logos::AsyncResult<T>)> cb,
                      Timeout timeout = Timeout());                                  // new

logos::AsyncResult<T> (new, Qt-free, cpp/logos_async_result.h) is {value, error}
plus ok(); AsyncResult<void> carries only the error so every fooAsyncResult has
the same callback shape. The name is distinct rather than an overload because
std::function<void(AsyncResult<T>)> next to std::function<void(T)> is ambiguous
for a generic lambda.

Applied to both emitters that produce this surface — legacy/generator_lib.cpp
(the module-builder path) and experimental/lidl_gen_client.cpp (`--lidl
--module-only`, from a published contract) — since a consumer can reach either
for the same contract.

The Qt-free (ApiStyle::Lp) surface gets the sync timeout (spelled `int
timeout_ms`; `Timeout` lives behind a Qt header) but NOT fooAsyncResult:
logos-protocol's lp_invoke_async hard-codes `cb(1, …)`, so an AsyncResult there
would report ok() on a failed call. Measured, not assumed. See the note in
makeHeaderLp.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Lipicar
2026-08-03 10:19:08 -03:00
committed by GitHub
co-authored by Claude Opus 5
parent d972fe207a
commit f3369faca4
14 changed files with 716 additions and 134 deletions
+47
View File
@@ -203,6 +203,53 @@ logos-cpp-generator --metadata metadata.json --general-only --output-dir ./gener
This approach gives you fine-grained control over which modules to include and allows rebuilding just the umbrella headers without regenerating all module wrappers.
#### The three call surfaces on a generated wrapper
Every LIDL `method foo(...) -> T` produces three entry points:
```cpp
// 1. sync — optional error out-channel, optional deadline. Both trailing and
// defaulted, so `dep.foo(a, b)` and `dep.foo(a, b, &err)` are unchanged.
T foo(params, logos::CallError* err = nullptr, Timeout timeout = Timeout());
// 2. async, value only — the historical form, unchanged.
void fooAsync(params, std::function<void(T)> cb, Timeout timeout = Timeout());
// 3. async, value + error.
void fooAsyncResult(params, std::function<void(logos::AsyncResult<T>)> cb,
Timeout timeout = Timeout());
```
Use (3) whenever a default-constructed `T` is also a legal success value — which
is almost always. `fooAsync` hands the callback a bare `T`, so a failed call and
a provider that genuinely returned `0` / `""` / `false` are indistinguishable;
that is exactly the ambiguity the sync form's `CallError*` exists to resolve.
```cpp
dep.balanceAsyncResult(account, [](logos::AsyncResult<qlonglong> r) {
if (!r.ok()) { // r.error is {code, message, origin}
qWarning() << "balance failed:" << r.error.code.c_str();
return;
}
use(r.value); // now known to be a real answer
});
```
`logos::AsyncResult<T>` (`logos_async_result.h`) is `{ T value; CallError error; }`
plus `ok()`; `AsyncResult<void>` carries only the error, so a `void`-returning
method has the same callback shape as every other one.
The name is deliberately distinct rather than an overload of `fooAsync`: two
overloads differing only in `std::function<void(T)>` vs
`std::function<void(AsyncResult<T>)>` are ambiguous for a generic lambda
(`[](auto v){…}`), which would break existing call sites.
**Qt-free (`--api-style lp`) wrappers** spell the deadline `int timeout_ms = 0`
(`<= 0` selects the protocol default) because `Timeout` lives in a Qt header,
and they do **not** yet get `fooAsyncResult` — logos-protocol's
`lp_invoke_async` does not report the call error to its callback, so an
`AsyncResult` there would report success on a failed call.
### Universal modules: LogosModuleContext
Universal (codegen-driven) modules — those built from a `package_xxx_impl.h` header rather than a handcrafted `QObject` plugin — don't see the raw `LogosAPI` at all. Instead, the codegen-generated provider populates a narrow `LogosModuleContext` base class with everything an impl typically needs:
+18 -12
View File
@@ -329,16 +329,22 @@ Fixture files in `tests/experimental/fixtures/`:
single-`_bytes`-field record is refused under both spellings. It used to read `f.type`,
which refused `? _bytes: tstr` and let `_bytes: ?tstr` through — the same declaration,
two answers. `?bstr` is unaffected either way: the tag lives in the value, not the slot.
- **A provider REJECTION reaches an async consumer callback only as a log line.** A
provider that refuses a call answers the canonical
`{"code":"dispatch_failed", "message":…, "origin":…}` object as its RESULT, not as a
transport error, and the Qt return table would convert it like any other value —
- **A provider REJECTION reaches `…Async`'s callback only as a log line** (but
`…AsyncResult`'s callback gets it properly). A provider that refuses a call answers the
canonical `{"code":"dispatch_failed", "message":…, "origin":…}` object as its RESULT, not
as a transport error, and the Qt return table would convert it like any other value —
erasing it (`_result.toList()` on that map is `[]`). The Qt consumer emitter therefore
detects it and folds it into the `logos::CallError` out-parameter the sync wrapper
already carries, so `mod.echoUintList(v, &err)` can tell a rejection from an empty
return. The generated `…Async` overload has no such channel — its callback is
`std::function<void(T)>`, and adding an error parameter would change the generated
public surface (which logos-qt-sdk's `qt-generator --backend consumer` veneer mirrors
1:1) — so an async rejection is reported with `qWarning` and the callback still
receives the default-converted value. Giving async an error channel is an API change,
not a code-generation fix.
detects it (`logosDispatchRejection`, emitted once per wrapper) and folds it into the
error channel of every surface that HAS one:
- **sync** — the `logos::CallError*` out-parameter, so `mod.echoUintList(v, &err)` can
tell a rejection from an empty return;
- **`…AsyncResult`** — `logos::AsyncResult<T>::error`, so `r.ok()` is false and
`r.error.code == "dispatch_failed"` exactly as on the sync path.
The historical **`…Async`** overload is the one exception: its callback is
`std::function<void(T)>`, and adding an error parameter would change a generated public
surface (which logos-qt-sdk's `qt-generator --backend consumer` veneer mirrors 1:1). It
is left untouched, so there an async rejection is reported with `qWarning` and the
callback still receives the default-converted value. `…AsyncResult` exists precisely
because giving async an error channel was an API addition rather than a code-generation
fix — a caller that needs to SEE the rejection uses it.
+85 -31
View File
@@ -264,6 +264,7 @@ QString lidlMakeHeader(const ModuleDecl& module, BindMode bindMode)
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_api_client.h\"\n";
s << "#include \"logos_call_error.h\"\n";
s << "#include \"logos_async_result.h\"\n";
s << "#include \"logos_object.h\"\n\n";
emitRecords(s, module);
@@ -288,19 +289,38 @@ QString lidlMakeHeader(const ModuleDecl& module, BindMode bindMode)
if (i + 1 < md.params.size()) s << ", ";
}
// Optional error out-channel: pass a logos::CallError* to distinguish
// a failed remote call from a legitimately default-valued result.
// a failed remote call from a legitimately default-valued result
// followed by an optional Timeout. Both trailing and defaulted, so
// existing call sites (including ones passing `&err` positionally)
// compile unchanged. Mirrors the legacy emitter in
// legacy/generator_lib.cpp; the two must agree, since a consumer can
// reach either (this one from a published `.lidl`, that one through the
// module builder) for the same contract.
if (!md.params.empty()) s << ", ";
s << "logos::CallError* err = nullptr);\n";
s << "logos::CallError* err = nullptr, Timeout timeout = Timeout());\n";
auto emitAsyncParams = [&]() {
for (int i = 0; i < md.params.size(); ++i) {
emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
if (!md.params.empty()) s << ", ";
};
QString asyncCb = (ret == "void")
? QString("std::function<void()>")
: QString("std::function<void(") + ret + ")>";
s << " void " << md.name << "Async(";
for (int i = 0; i < md.params.size(); ++i) {
emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name);
if (i + 1 < md.params.size()) s << ", ";
}
if (!md.params.empty()) s << ", ";
emitAsyncParams();
s << asyncCb << " callback, Timeout timeout = Timeout());\n";
// Result-carrying async entry point. Distinct name, not an overload:
// std::function<void(AsyncResult<T>)> alongside std::function<void(T)>
// is ambiguous for a generic lambda.
s << " void " << md.name << "AsyncResult(";
emitAsyncParams();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback"
<< ", Timeout timeout = Timeout());\n";
}
s << "\nprivate:\n";
@@ -384,7 +404,7 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode)
if (i + 1 < nParams) s << ", ";
}
if (nParams > 0) s << ", ";
s << "logos::CallError* err) {\n";
s << "logos::CallError* err, Timeout timeout) {\n";
// Call through the err-out overload: with a logos::CallError* the
// caller can distinguish a failed remote call from a legitimately
@@ -404,7 +424,9 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode)
s << qtArgExpr(md.params[i].type, qs(md.params[i].name));
if (i + 1 < nParams) s << ", ";
}
s << "), Timeout(), &_err);\n";
// The caller's deadline, not a hard-coded default: this is the overload
// that carries BOTH the deadline and the error out-channel.
s << "), timeout, &_err);\n";
s << " if (err) *err = _err;\n";
s << " else if (!_err.ok()) qWarning() << \"" << className << "::" << md.name
<< ": remote call failed:\" << QString::fromStdString(_err.message);\n";
@@ -413,31 +435,63 @@ QString lidlMakeSource(const ModuleDecl& module, BindMode bindMode)
s << " " << returnConversionFor(md.returnType, ret) << "\n";
s << "}\n\n";
s << "void " << className << "::" << md.name << "Async(";
for (int i = 0; i < nParams; ++i) {
emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name);
if (i + 1 < nParams) s << ", ";
}
if (nParams > 0) s << ", ";
s << "std::function<void(" << (ret == "void" ? "void" : ret) << ")> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
// Shared between the two async entry points so they cannot drift in how
// they marshal args or decode the reply.
auto emitAsyncParams = [&]() {
for (int i = 0; i < nParams; ++i) {
emitParam(s, lidlTypeToQt(md.params[i].type), md.params[i].name);
if (i + 1 < nParams) s << ", ";
}
if (nParams > 0) s << ", ";
};
// Same one-element-per-arg packing as the sync path (see above): a
// QVariantList-typed arg must not be spread across the args list.
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", packVariantList(";
for (int i = 0; i < nParams; ++i) {
s << qtArgExpr(md.params[i].type, qs(md.params[i].name));
if (i + 1 < nParams) s << ", ";
}
s << ")";
auto emitAsyncArgs = [&]() {
s << "packVariantList(";
for (int i = 0; i < nParams; ++i) {
s << qtArgExpr(md.params[i].type, qs(md.params[i].name));
if (i + 1 < nParams) s << ", ";
}
s << ")";
};
// The QVariant -> typed-return expression, given the QVariant's name.
auto asyncDecodeExpr = [&](const QString& var) -> QString {
if (ret == "void") return QString();
if (ret == "QVariant") return var;
return var + ".isValid() ? " + asyncReturnConversionFor(md.returnType, ret)
+ " : " + asyncDefaultVal(ret);
};
s << "void " << className << "::" << md.name << "Async(";
emitAsyncParams();
s << "std::function<void(" << (ret == "void" ? "void" : ret) << ")> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", ";
emitAsyncArgs();
// ONE-argument lambda -> LogosAPIClient::AsyncResultCallback, i.e. the
// historical value-only transport overload.
s << ", [callback](QVariant v) {\n";
if (ret == "void") {
s << " callback();\n";
} else if (ret == "QVariant") {
s << " callback(v);\n";
} else {
s << " callback(v.isValid() ? " << asyncReturnConversionFor(md.returnType, ret)
<< " : " << asyncDefaultVal(ret) << ");\n";
}
if (ret == "void") s << " callback();\n";
else s << " callback(" << asyncDecodeExpr("v") << ");\n";
s << " }, timeout);\n";
s << "}\n\n";
// Result-carrying async: a TWO-argument lambda, so it binds to the
// transport's CallError-aware AsyncResultErrorCallback overload. The
// value on failure is exactly what `<name>Async` would have delivered;
// what changes is that the callback can now tell.
s << "void " << className << "::" << md.name << "AsyncResult(";
emitAsyncParams();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << md.name << "\", ";
emitAsyncArgs();
s << ", [callback](QVariant v, const logos::CallError& _err) {\n";
s << " logos::AsyncResult<" << ret << "> _r;\n";
s << " _r.error = _err;\n";
if (ret == "void") s << " (void)v;\n";
else s << " _r.value = " << asyncDecodeExpr("v") << ";\n";
s << " callback(_r);\n";
s << " }, timeout);\n";
s << "}\n\n";
}
+185 -79
View File
@@ -589,6 +589,7 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_api_client.h\"\n";
s << "#include \"logos_call_error.h\"\n";
s << "#include \"logos_async_result.h\"\n";
s << "#include \"logos_object.h\"\n\n";
s << "class " << className << " {\n";
s << "public:\n";
@@ -662,25 +663,56 @@ QString makeHeader(const QString& moduleName, const QString& className, const QJ
// Optional error out-channel: pass a logos::CallError* to distinguish
// a failed remote call from a legitimately default-valued result.
// Existing call sites compile unchanged.
//
// ...and an optional Timeout AFTER it, so the sync surface can say how
// long it is willing to wait. Appending (rather than inserting next to
// the value args, where the async overload carries it) keeps every
// existing call site source-compatible, including the ones that already
// pass `&err` positionally. The transport has taken both since it grew
// the error channel — logos_api_client.h's
// `invokeRemoteMethod(obj, method, args, Timeout, CallError*)` — and the
// generated body simply hard-coded `Timeout()` there.
if (!params.isEmpty()) s << ", ";
s << "logos::CallError* err = nullptr);\n";
s << "logos::CallError* err = nullptr, Timeout timeout = Timeout());\n";
// Param list shared by both async entry points.
auto emitAsyncParams = [&]() {
for (int i = 0; i < params.size(); ++i) {
QJsonObject p = params.at(i).toObject();
QString qtPt = p.value("type").toString();
QString pt = paramTypeFor(qtPt, apiStyle, rs);
QString pn = p.value("name").toString();
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
if (byRef) s << "const " << pt << "& " << pn;
else s << pt << " " << pn;
if (i + 1 < params.size()) s << ", ";
}
if (params.size() > 0) s << ", ";
};
// Async overload: same params + callback + optional Timeout
QString asyncCallbackType = (ret == "void")
? QString("std::function<void()>")
: QString("std::function<void(") + ret + ")>";
s << " void " << name << "Async(";
for (int i = 0; i < params.size(); ++i) {
QJsonObject p = params.at(i).toObject();
QString qtPt = p.value("type").toString();
QString pt = paramTypeFor(qtPt, apiStyle, rs);
QString pn = p.value("name").toString();
bool byRef = byRefFor(qtPt, pt, apiStyle, rs);
if (byRef) s << "const " << pt << "& " << pn;
else s << pt << " " << pn;
if (i + 1 < params.size()) s << ", ";
}
if (params.size() > 0) s << ", ";
emitAsyncParams();
s << asyncCallbackType << " callback, Timeout timeout = Timeout());\n";
// Result-carrying async entry point. The plain `<name>Async` above
// hands the callback a bare value, so a failed call is
// INDISTINGUISHABLE from a provider that legitimately returned
// 0 / "" / false — the exact ambiguity the sync `CallError*` exists to
// resolve. This one delivers logos::AsyncResult<T> {value, error}.
//
// A DISTINCT NAME, not an overload of `<name>Async`: two overloads
// differing only in std::function<void(T)> vs
// std::function<void(AsyncResult<T>)> are ambiguous for a generic
// lambda (`[](auto v){...}` is invocable with either), which would
// break existing call sites. A distinct name has zero resolution risk.
s << " void " << name << "AsyncResult(";
emitAsyncParams();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback"
<< ", Timeout timeout = Timeout());\n";
}
s << "\nprivate:\n";
// ensureReplica() is needed whenever the wrapper subscribes to events,
@@ -928,7 +960,7 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
if (i + 1 < params.size()) s << ", ";
}
if (!params.isEmpty()) s << ", ";
s << "logos::CallError* err) {\n";
s << "logos::CallError* err, Timeout timeout) {\n";
// Body: perform call through the err-out overload. When the caller
// passes a logos::CallError* it can distinguish a failed remote call
@@ -953,7 +985,11 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
s << "QVariant::fromValue(" << wireArg(params.at(i).toObject()) << ")";
if (i + 1 < params.size()) s << ", ";
}
s << "}, Timeout(), &_err);\n";
// `timeout` — the caller's, defaulted to Timeout() at the declaration —
// not a hard-coded Timeout(). This is the overload that carries BOTH
// the deadline and the error out-channel; the generator used to call it
// with the error and drop the deadline on the floor.
s << "}, timeout, &_err);\n";
// A provider REJECTION arrives as the result, not as a transport error.
// Fold it into the same error channel BEFORE the return table converts
// it, or the conversion erases it (a rejected `[uint]` call answered []
@@ -1003,44 +1039,41 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
}
s << "}\n\n";
// Async implementation
s << "void " << className << "::" << name << "Async(";
for (int i = 0; i < params.size(); ++i) {
bool byRef;
emitParam(params.at(i).toObject(), byRef);
if (i + 1 < params.size()) s << ", ";
}
if (params.size() > 0) s << ", ";
s << "std::function<void(" << (ret == "void" ? "void" : ret) << ")> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << name << "\", ";
if (params.size() == 0) {
s << "QVariantList()";
} else {
// Same one-element-per-arg wrapping as the sync path (see above): a
// QVariantList-typed arg must not be spread across the args list.
s << "QVariantList{";
// Shared pieces of the two async entry points, so `<name>Async` and
// `<name>AsyncResult` cannot drift apart in how they marshal args or
// decode the reply.
auto emitAsyncParams = [&]() {
for (int i = 0; i < params.size(); ++i) {
s << "QVariant::fromValue(" << wireArg(params.at(i).toObject()) << ")";
bool byRef;
emitParam(params.at(i).toObject(), byRef);
if (i + 1 < params.size()) s << ", ";
}
s << "}";
}
s << ", [callback](QVariant v) {\n";
// The async callback carries the value only — there is no CallError
// parameter to fill, and adding one would change the generated public
// surface. A rejection is at least made visible in the module log
// instead of vanishing into the return conversion below.
s << " { logos::CallError _rej; if (logosDispatchRejection(v, _rej))\n";
s << " qWarning() << \"" << className << "::" << name
<< "Async: remote call failed:\" << QString::fromStdString(_rej.message); }\n";
if (ret == "void") {
s << " (void)v; callback();\n";
} else if (retIsRecord) {
// A record decodes field by field; an invalid QVariant yields a
// default-constructed struct, matching the scalar paths.
s << " callback(" << fromWireFor(qtRet, apiStyle, rs, "v", className + "::") << ");\n";
} else {
if (params.size() > 0) s << ", ";
};
auto emitAsyncArgs = [&]() {
if (params.size() == 0) {
s << "QVariantList()";
} else {
// Same one-element-per-arg wrapping as the sync path (see above): a
// QVariantList-typed arg must not be spread across the args list.
s << "QVariantList{";
for (int i = 0; i < params.size(); ++i) {
s << "QVariant::fromValue(" << wireArg(params.at(i).toObject()) << ")";
if (i + 1 < params.size()) s << ", ";
}
s << "}";
}
};
// The QVariant -> typed-return expression, given the QVariant's name.
// Empty for a void return.
auto asyncDecodeExpr = [&](const QString& var) -> QString {
if (ret == "void") return QString();
if (retIsRecord) {
// A record decodes field by field; an invalid QVariant yields a
// default-constructed struct, matching the scalar paths.
return fromWireFor(qtRet, apiStyle, rs, var, className + "::");
}
if (ret == "QVariant") return var;
QString defaultVal;
if (ret == "bool") defaultVal = "false";
else if (ret == "int" || ret == "qlonglong" || ret == "qulonglong"
@@ -1051,12 +1084,61 @@ QString makeSource(const QString& moduleName, const QString& className, const QS
else if (ret == "QVariantList") defaultVal = "QVariantList()";
else if (ret == "QVariantMap") defaultVal = "QVariantMap()";
else defaultVal = ret + "{}";
if (ret == "QVariant") {
s << " callback(v);\n";
} else {
s << " callback(v.isValid() ? qvariant_cast<" << ret << ">(v) : " << defaultVal << ");\n";
}
}
return var + ".isValid() ? qvariant_cast<" + ret + ">(" + var + ") : " + defaultVal;
};
// Async implementation
s << "void " << className << "::" << name << "Async(";
emitAsyncParams();
s << "std::function<void(" << (ret == "void" ? "void" : ret) << ")> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << name << "\", ";
emitAsyncArgs();
// A ONE-argument lambda: it is invocable only as
// LogosAPIClient::AsyncResultCallback, so this keeps binding to the
// historical value-only overload even though a CallError-aware one
// exists next to it.
s << ", [callback](QVariant v) {\n";
// The value-only async callback has nowhere to put an error — there is
// no CallError parameter to fill, and adding one would change the
// historical public surface. A rejection is at least made visible in
// the module log instead of vanishing into the return conversion below.
// `<name>AsyncResult` is the surface that can actually REPORT it.
s << " { logos::CallError _rej; if (logosDispatchRejection(v, _rej))\n";
s << " qWarning() << \"" << className << "::" << name
<< "Async: remote call failed:\" << QString::fromStdString(_rej.message); }\n";
if (ret == "void") s << " (void)v; callback();\n";
else s << " callback(" << asyncDecodeExpr("v") << ");\n";
s << " }, timeout);\n";
s << "}\n\n";
// Result-carrying async implementation. Routes to the transport's
// CallError-aware async overload (AsyncResultErrorCallback) — a TWO
// argument lambda, which is invocable only as that overload, so the
// pair above and below resolve unambiguously.
//
// On failure the value stays default-constructed exactly as
// `<name>Async` would have delivered it; what changes is that the
// callback can now TELL, via r.error / r.ok().
//
// That includes a provider REJECTION, which arrives as the RESULT and
// not as a transport error: it is folded into `_r.error` exactly as the
// sync path folds it into the caller's CallError. `<name>Async` can only
// warn about one because its callback has no error slot; this one has,
// so a rejected call must NOT report ok() here.
s << "void " << className << "::" << name << "AsyncResult(";
emitAsyncParams();
s << "std::function<void(logos::AsyncResult<" << ret << ">)> callback, Timeout timeout) {\n";
s << " if (!callback) return;\n";
s << " m_client->invokeRemoteMethodAsync(" << targetExpr << ", \"" << name << "\", ";
emitAsyncArgs();
s << ", [callback](QVariant v, const logos::CallError& _err) {\n";
s << " logos::AsyncResult<" << ret << "> _r;\n";
s << " _r.error = _err;\n";
s << " if (_r.error.ok()) logosDispatchRejection(v, _r.error);\n";
if (ret == "void") s << " (void)v;\n";
else s << " _r.value = " << asyncDecodeExpr("v") << ";\n";
s << " callback(_r);\n";
s << " }, timeout);\n";
s << "}\n\n";
}
@@ -1305,7 +1387,30 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
}
if (!events.isEmpty()) s << "\n";
// Methods: sync (with optional CallError out-param) + async overload.
// Methods: sync (with optional CallError out-param + timeout) + async
// overload.
//
// TIMEOUTS ARE SPELLED `int timeout_ms`, NOT `Timeout`, on this surface.
// `Timeout` lives in logos-protocol's logos_mode.h, which includes <QDebug>
// — naming it here would drag Qt into a translation unit whose whole reason
// for existing is not to have any. logos::LpClient already spells its
// deadlines `int timeout_ms` with the C ABI's rule (`<= 0` selects the
// protocol default), and this matches it.
//
// NO `<name>AsyncResult` HERE — deliberately, for now. The Qt surface gets
// one because its transport reports the error
// (LogosAPIClient::AsyncResultErrorCallback). This surface's transport does
// NOT: logos-protocol's lp_invoke_async (cpp/logos_protocol.cpp) subscribes
// with the VALUE-ONLY invokeRemoteMethodAsync overload and unconditionally
// calls back `cb(1, json, ...)` — ok is hard-coded to 1 — even though
// lp_result_cb is documented as "ok == 0 → `json` is the canonical error
// object", and even though its own sync twin lp_invoke does return
// LP_ERR_UNAVAILABLE + makeErrorJson. So an AsyncResult emitted here would
// report ok() on a failed call to a module that is not loaded: an error
// channel that lies is worse than no error channel. (Measured, not assumed:
// a wrapper wired to it fires its callback with the default value and an
// EMPTY error code.) Once lp_invoke_async reports the error, emitting the
// AsyncResult twin here is the same few lines as above.
for (const QJsonValue& v : methods) {
const QJsonObject o = v.toObject();
if (!o.value("isInvokable").toBool()) continue;
@@ -1313,31 +1418,29 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const
const QString ret = returnTypeFor(o.value("returnType").toString(), ApiStyle::Lp, rs);
const QJsonArray params = o.value("parameters").toArray();
auto emitDeclParams = [&]() {
for (int i = 0; i < params.size(); ++i) {
const QJsonObject p = params.at(i).toObject();
const QString qtPt = p.value("type").toString();
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
else s << pt << " " << p.value("name").toString();
if (i + 1 < params.size()) s << ", ";
}
if (!params.isEmpty()) s << ", ";
};
s << " " << ret << " " << name << "(";
for (int i = 0; i < params.size(); ++i) {
const QJsonObject p = params.at(i).toObject();
const QString qtPt = p.value("type").toString();
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
else s << pt << " " << p.value("name").toString();
if (i + 1 < params.size()) s << ", ";
}
if (!params.isEmpty()) s << ", ";
s << "logos::CallError* err = nullptr);\n";
emitDeclParams();
// Trailing, defaulted, and in that order — existing call sites,
// including ones already passing `&err` positionally, are unaffected.
s << "logos::CallError* err = nullptr, int timeout_ms = 0);\n";
const QString asyncCb = (ret == "void")
? QString("std::function<void()>")
: QString("std::function<void(") + ret + ")>";
s << " void " << name << "Async(";
for (int i = 0; i < params.size(); ++i) {
const QJsonObject p = params.at(i).toObject();
const QString qtPt = p.value("type").toString();
const QString pt = paramTypeFor(qtPt, ApiStyle::Lp, rs);
if (byRefFor(qtPt, pt, ApiStyle::Lp, rs)) s << "const " << pt << "& " << p.value("name").toString();
else s << pt << " " << p.value("name").toString();
if (i + 1 < params.size()) s << ", ";
}
if (!params.isEmpty()) s << ", ";
emitDeclParams();
s << asyncCb << " callback);\n";
}
@@ -1430,16 +1533,18 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
}
};
// Sync
// Sync — routes the caller's deadline to LpClient::invoke's
// `timeout_ms` parameter, which the generated body used to leave at its
// default (i.e. silently drop).
s << retQual << " " << className << "::" << name << "(";
emitParams();
if (!params.isEmpty()) s << ", ";
s << "logos::CallError* err) {\n";
s << "logos::CallError* err, int timeout_ms) {\n";
emitArgsArray();
if (ret == "void") {
s << " " << clientExpr << ".invoke(\"" << name << "\", _args, err);\n";
s << " " << clientExpr << ".invoke(\"" << name << "\", _args, err, timeout_ms);\n";
} else {
s << " nlohmann::json _r = " << clientExpr << ".invoke(\"" << name << "\", _args, err);\n";
s << " nlohmann::json _r = " << clientExpr << ".invoke(\"" << name << "\", _args, err, timeout_ms);\n";
s << " return " << fromWireFor(qtRet, ApiStyle::Lp, rs, "_r", className + "::") << ";\n";
}
s << "}\n\n";
@@ -1462,6 +1567,7 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const
}
s << " });\n";
s << "}\n\n";
// (No <name>AsyncResult on this surface yet — see makeHeaderLp.)
}
return c;
}
+1
View File
@@ -65,5 +65,6 @@ install(FILES
logos_json.h
logos_result.h
logos_lp_client.h
logos_async_result.h
DESTINATION include
)
+78
View File
@@ -0,0 +1,78 @@
#ifndef LOGOS_ASYNC_RESULT_H
#define LOGOS_ASYNC_RESULT_H
// The async counterpart of logos::CallError's sync out-parameter.
//
// The sync generated wrapper takes an optional `logos::CallError*` precisely
// "to distinguish a failed remote call from a legitimately default-valued
// result". The async wrapper had no such channel: `fooAsync` hands the callback
// a bare T, and a failed call is indistinguishable from a provider that
// legitimately returned 0 / "" / false. AsyncResult<T> is that missing channel
// — the value and the error travel together, so `fooAsyncResult`'s callback can
// check `.ok()` before trusting `.value`.
//
// This mirrors logos-rust-sdk, where BOTH surfaces already carry the error
// (lidl-gen/src/rustgen.rs: `-> Result<T, LogosError>` on the sync wrapper and
// `FnOnce(Result<T, LogosError>)` on the async one). C++ was the outlier.
//
// Deliberately Qt-FREE, exactly like logos_call_error.h next to which it
// conceptually lives: it is named in the signatures of BOTH generated surfaces
// — the Qt-typed one (ApiStyle::Qt) and the Qt-free lp one (ApiStyle::Lp, used
// by cdylib/universal modules whose translation units must not see Qt). Only
// std types and logos::CallError may appear here.
//
// (It lives in logos-cpp-sdk rather than in logos-protocol's
// logos_call_error.h only because that is where the generator that emits it
// lives; ${LOGOS_CPP_SDK_ROOT}/include is on the include path of every module
// build — see logos-plugin-qt/cmake/LogosModule.cmake and its module-builder
// twin, which add it unconditionally.)
#include "logos_call_error.h"
namespace logos {
/**
* @brief An async call's outcome: the decoded value plus the call error.
*
* `error.ok()` (surfaced as `ok()`) is the ONLY reliable success test — a
* failed call leaves `value` default-constructed, which for most return types
* is also a perfectly legal success value.
*
* dep.echoIntAsyncResult(7, [](logos::AsyncResult<qlonglong> r) {
* if (!r.ok()) { qWarning() << r.error.code.c_str(); return; }
* use(r.value);
* });
*
* Aggregate — `AsyncResult<T>{v, err}` and designated-ish brace init both work.
*/
template <typename T>
struct AsyncResult {
T value{};
CallError error;
bool ok() const { return error.ok(); }
explicit operator bool() const { return error.ok(); }
};
/**
* @brief The void specialization: an error channel with no value.
*
* A `void`-returning method could equally have been given a plain
* `std::function<void(logos::CallError)>` callback. It is spelled
* AsyncResult<void> instead so that EVERY generated `fooAsyncResult` takes
* `std::function<void(logos::AsyncResult<R>)>` for its own return type R with
* no special case — forwarding/proxy code (and the generator itself) can write
* the callback type from the return type mechanically, and every call site
* reads `if (!r.ok())` regardless of what the method returns.
*/
template <>
struct AsyncResult<void> {
CallError error;
bool ok() const { return error.ok(); }
explicit operator bool() const { return error.ok(); }
};
} // namespace logos
#endif // LOGOS_ASYNC_RESULT_H
+24 -2
View File
@@ -269,8 +269,12 @@ sections:
text: |
The consumer side. From the same contract, `--module-only` emits the typed
wrapper a *consumer* compiles against to call `sensor_module`. Each LIDL
`method` becomes a synchronous caller plus an `…Async` variant, and each
`event` an `on(...)` subscription. The type mapping is the Qt caller style:
`method` becomes three entry points — a synchronous caller, an `…Async`
variant that hands the callback a bare value, and an `…AsyncResult`
variant that hands it a `logos::AsyncResult<T>` (`{value, error}`) so a
failed call is distinguishable from a legitimately default-valued one —
and each `event` an `on(...)` subscription. The type mapping is the Qt
caller style:
`float64`→`double`, `tstr`→`QString`, `int`→`qlonglong`, `uint`→
`qulonglong`, `bstr`→`QByteArray`, `[tstr]`→`QStringList`, other
arrays→`QVariantList`, and `result`→`LogosResult`.
@@ -300,6 +304,24 @@ sections:
- "QByteArray firmware(const QByteArray& image"
- "QStringList labels(const QVariantList& ids"
- "LogosResult reset(const QString& id"
- title: "The three call surfaces per method"
text: |
One method, three entry points. The sync form takes an optional
`logos::CallError*` **and** an optional `Timeout` — both trailing and
defaulted, so `temperature()` and `temperature(&err)` still compile.
`temperatureAsync` delivers the value alone; `temperatureAsyncResult`
delivers `logos::AsyncResult<double>`, whose `.error` tells a failed
call apart from a provider that legitimately returned `0.0`. The
result-carrying entry point has its own name rather than being an
overload: a `std::function<void(AsyncResult<T>)>` overload alongside
`std::function<void(T)>` is ambiguous for a generic `[](auto v){…}`.
run: "grep -E 'temperature' consumer/sensor_module_api.h"
code_block: |
grep -E 'temperature' consumer/sensor_module_api.h
expect_contains:
- "double temperature(logos::CallError* err = nullptr, Timeout timeout = Timeout());"
- "void temperatureAsync(std::function<void(double)> callback, Timeout timeout = Timeout());"
- "void temperatureAsyncResult(std::function<void(logos::AsyncResult<double>)> callback, Timeout timeout = Timeout());"
- title: "The full type system: composite types"
step: true
+1 -1
View File
@@ -28,7 +28,7 @@ pkgs.stdenv.mkDerivation {
# include/cpp/, a single TU would pull logos_result.h through two
# distinct realpaths and #pragma once could not dedup them
# (redefinition of StdLogosResult). Ship every std header in BOTH roots.
for file in logos_module_context.h logos_json.h logos_result.h logos_lp_client.h; do
for file in logos_module_context.h logos_json.h logos_result.h logos_lp_client.h logos_async_result.h; do
cp cpp/$file $out/include/cpp/
cp cpp/$file $out/include/
done
@@ -483,3 +483,69 @@ TEST(LidlGenClient, BytesTagCollisionIsRefusedThroughAnOptional)
EXPECT_TRUE(error.contains("Sneaky")) << error.toStdString();
}
}
// ---------------------------------------------------------------------------
// Sync timeout + result-carrying async
//
// This emitter and legacy/generator_lib.cpp produce the SAME consumer surface
// for the same contract — one is reached from a published `.lidl`, the other
// through the module builder — so the two must agree. tests/generator/
// test_async_result.cpp holds the legacy twin of these assertions.
// ---------------------------------------------------------------------------
TEST(LidlGenClient, SyncTakesBothErrorAndTimeout)
{
auto m = makeTestModule();
QString h = lidlMakeHeader(m);
// Trailing and defaulted, err first — `createAccount(p)` and
// `createAccount(p, &err)` are both unaffected.
EXPECT_TRUE(h.contains("QString createAccount(const QString& passphrase, "
"logos::CallError* err = nullptr, Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("QStringList listAccounts(logos::CallError* err = nullptr, "
"Timeout timeout = Timeout());"));
}
TEST(LidlGenClient, SyncBodyForwardsTheCallersTimeout)
{
auto m = makeTestModule();
QString s = lidlMakeSource(m);
EXPECT_TRUE(s.contains("WalletModule::createAccount(const QString& passphrase, "
"logos::CallError* err, Timeout timeout)"));
EXPECT_TRUE(s.contains("), timeout, &_err);"));
EXPECT_FALSE(s.contains("), Timeout(), &_err);"));
}
TEST(LidlGenClient, HeaderDeclaresTheResultCarryingAsyncEntryPoint)
{
auto m = makeTestModule();
QString h = lidlMakeHeader(m);
EXPECT_TRUE(h.contains("#include \"logos_async_result.h\""));
EXPECT_TRUE(h.contains("void createAccountAsyncResult(const QString& passphrase, "
"std::function<void(logos::AsyncResult<QString>)> callback, "
"Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("void listAccountsAsyncResult("
"std::function<void(logos::AsyncResult<QStringList>)> callback, "
"Timeout timeout = Timeout());"));
}
TEST(LidlGenClient, ResultCarryingAsyncRoutesToTheCallErrorAwareOverload)
{
auto m = makeTestModule();
QString s = lidlMakeSource(m);
// Two-argument lambda: only AsyncResultErrorCallback is invocable with it.
EXPECT_TRUE(s.contains("[callback](QVariant v, const logos::CallError& _err) {"));
EXPECT_TRUE(s.contains("logos::AsyncResult<QString> _r;"));
EXPECT_TRUE(s.contains("_r.error = _err;"));
EXPECT_TRUE(s.contains("callback(_r);"));
}
TEST(LidlGenClient, ThePlainAsyncEntryPointIsUnchanged)
{
auto m = makeTestModule();
QString h = lidlMakeHeader(m);
EXPECT_TRUE(h.contains("void createAccountAsync(const QString& passphrase, "
"std::function<void(QString)> callback, Timeout timeout = Timeout());"));
QString s = lidlMakeSource(m);
// Still a ONE-argument lambda -> still the value-only transport overload.
EXPECT_TRUE(s.contains("[callback](QVariant v) {"));
}
+1
View File
@@ -18,6 +18,7 @@ add_executable(generator_tests
test_make_umbrella.cpp
test_parse_provider_header.cpp
test_records.cpp
test_async_result.cpp
test_optional_spellings.cpp
)
+201
View File
@@ -0,0 +1,201 @@
// The two complementary gaps this suite pins down:
//
// sync had an error channel (logos::CallError*) but NO timeout;
// async had a timeout but NO error channel.
//
// After the change the sync wrapper takes both, and a distinctly-named
// `<name>AsyncResult` delivers logos::AsyncResult<T> {value, error}. Every
// assertion here fails on the pre-change generator.
#include <gtest/gtest.h>
#include <QJsonArray>
#include <QJsonObject>
#include "generator_lib.h"
namespace {
QJsonObject method(const QString& name, const QString& ret, const QStringList& paramTypes = {})
{
QJsonObject m;
m["name"] = name;
m["returnType"] = ret;
m["isInvokable"] = true;
QJsonArray params;
for (int i = 0; i < paramTypes.size(); ++i) {
QJsonObject p;
p["type"] = paramTypes.at(i);
p["name"] = QString("p%1").arg(i);
params.append(p);
}
m["parameters"] = params;
return m;
}
QJsonArray sampleMethods()
{
QJsonArray a;
a.append(method("add", "int", {"int", "int"}));
a.append(method("reset", "void"));
a.append(method("name", "QString"));
return a;
}
QString qtHeader() { return makeHeader("mod", "Mod", sampleMethods(), ApiStyle::Qt); }
QString qtSource() { return makeSource("mod", "Mod", "mod.h", sampleMethods(), ApiStyle::Qt); }
QString lpHeader() { return makeHeader("mod", "Mod", sampleMethods(), ApiStyle::Lp); }
QString lpSource() { return makeSource("mod", "Mod", "mod.h", sampleMethods(), ApiStyle::Lp); }
} // namespace
// ─── 1. Sync gains a timeout, appended AFTER the error out-param ────────────
TEST(SyncTimeout, QtDeclarationTakesBothErrorAndTimeout)
{
const QString h = qtHeader();
// Order matters: err first, timeout second, both defaulted — so a call site
// that already passes `&err` positionally is unaffected.
EXPECT_TRUE(h.contains("int add(int p0, int p1, logos::CallError* err = nullptr, Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("void reset(logos::CallError* err = nullptr, Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("QString name(logos::CallError* err = nullptr, Timeout timeout = Timeout());"));
}
TEST(SyncTimeout, QtBodyForwardsTheCallersTimeout)
{
const QString src = qtSource();
EXPECT_TRUE(src.contains("Mod::add(int p0, int p1, logos::CallError* err, Timeout timeout)"));
// Routes to the transport overload that takes BOTH — logos_api_client.h's
// invokeRemoteMethod(obj, method, args, Timeout, logos::CallError*).
EXPECT_TRUE(src.contains("}, timeout, &_err);"));
// ...and no longer hard-codes a fresh default.
EXPECT_FALSE(src.contains("}, Timeout(), &_err);"));
}
TEST(SyncTimeout, LpDeclarationTakesBothErrorAndTimeoutMs)
{
// `Timeout` lives in logos_mode.h, which includes <QDebug>; the Qt-free
// surface spells its deadline the way LpClient/the C ABI do.
const QString h = lpHeader();
EXPECT_TRUE(h.contains("int64_t add(int64_t p0, int64_t p1, logos::CallError* err = nullptr, int timeout_ms = 0);"));
EXPECT_TRUE(h.contains("void reset(logos::CallError* err = nullptr, int timeout_ms = 0);"));
EXPECT_FALSE(h.contains("Timeout timeout"));
}
TEST(SyncTimeout, LpBodyForwardsTimeoutMs)
{
const QString src = lpSource();
EXPECT_TRUE(src.contains("Mod::add(int64_t p0, int64_t p1, logos::CallError* err, int timeout_ms)"));
EXPECT_TRUE(src.contains("m_client.invoke(\"add\", _args, err, timeout_ms);"));
EXPECT_TRUE(src.contains("m_client.invoke(\"reset\", _args, err, timeout_ms);"));
EXPECT_FALSE(src.contains("_args, err);"));
}
// ─── 2. Async gains a result-carrying entry point ───────────────────────────
TEST(AsyncResult, QtHeaderDeclaresTheDistinctlyNamedEntryPoint)
{
const QString h = qtHeader();
EXPECT_TRUE(h.contains("void addAsyncResult(int p0, int p1, "
"std::function<void(logos::AsyncResult<int>)> callback, "
"Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("void nameAsyncResult(std::function<void(logos::AsyncResult<QString>)> callback, "
"Timeout timeout = Timeout());"));
}
TEST(AsyncResult, VoidReturnUsesTheVoidSpecializationNotABespokeCallback)
{
// Uniform shape: std::function<void(AsyncResult<R>)> for every R, including
// void, so generated forwarding code never special-cases the return type.
const QString h = qtHeader();
EXPECT_TRUE(h.contains("void resetAsyncResult(std::function<void(logos::AsyncResult<void>)> callback, "
"Timeout timeout = Timeout());"));
}
TEST(AsyncResult, QtHeaderIncludesTheAsyncResultHeader)
{
EXPECT_TRUE(qtHeader().contains("#include \"logos_async_result.h\""));
}
TEST(AsyncResult, QtBodyRoutesToTheCallErrorAwareTransportOverload)
{
const QString src = qtSource();
// A TWO-argument lambda: only LogosAPIClient::AsyncResultErrorCallback is
// invocable with it, so this cannot silently bind to the value-only one.
EXPECT_TRUE(src.contains("[callback](QVariant v, const logos::CallError& _err) {"));
EXPECT_TRUE(src.contains("logos::AsyncResult<int> _r;"));
EXPECT_TRUE(src.contains("_r.error = _err;"));
EXPECT_TRUE(src.contains("_r.value = v.isValid() ? qvariant_cast<int>(v) : 0;"));
EXPECT_TRUE(src.contains("callback(_r);"));
// The void form carries the error and nothing else.
EXPECT_TRUE(src.contains("logos::AsyncResult<void> _r;"));
}
TEST(AsyncResult, TheValueDecodeIsSharedWithThePlainAsyncEntryPoint)
{
// Same decode expression in both, so a failed call delivers exactly the
// value `<name>Async` would have delivered — plus the error.
const QString src = qtSource();
EXPECT_TRUE(src.contains("callback(v.isValid() ? qvariant_cast<int>(v) : 0);"));
EXPECT_TRUE(src.contains("_r.value = v.isValid() ? qvariant_cast<int>(v) : 0;"));
}
// ─── 3. The existing async surface is untouched ─────────────────────────────
TEST(AsyncResult, ThePlainAsyncEntryPointIsUnchanged)
{
const QString h = qtHeader();
EXPECT_TRUE(h.contains("void addAsync(int p0, int p1, std::function<void(int)> callback, "
"Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("void resetAsync(std::function<void()> callback, Timeout timeout = Timeout());"));
const QString src = qtSource();
EXPECT_TRUE(src.contains("void Mod::addAsync(int p0, int p1, std::function<void(int)> callback, Timeout timeout)"));
// Still a ONE-argument lambda -> still the value-only transport overload.
EXPECT_TRUE(src.contains("[callback](QVariant v) {"));
}
// ─── 4. The Qt-free surface deliberately has no AsyncResult yet ─────────────
TEST(AsyncResult, LpSurfaceDoesNotEmitAsyncResultWhileTheCAbiCannotReportOne)
{
// logos-protocol's lp_invoke_async hard-codes `cb(1, ...)`, so an
// AsyncResult here would report ok() on a failed call. See makeHeaderLp.
EXPECT_FALSE(lpHeader().contains("AsyncResult"));
EXPECT_FALSE(lpSource().contains("AsyncResult"));
// ...and the Lp async entry point keeps its exact shape.
EXPECT_TRUE(lpHeader().contains("void addAsync(int64_t p0, int64_t p1, std::function<void(int64_t)> callback);"));
}
// ─── 5. A REJECTION reaches the surface that can report it ──────────────────
//
// A provider that refuses a call answers the canonical
// {"code":"dispatch_failed", …} object as its RESULT, not as a transport error.
// The sync path folds it into the caller's CallError. `<name>Async` can only
// warn — its callback takes the value alone. `<name>AsyncResult` is the first
// async surface with an error slot, so it must fold it too; reporting ok() for
// a rejected call there would reintroduce, on the new surface, exactly the
// defect the sync fold removed.
TEST(AsyncResult, RejectionIsFoldedIntoTheAsyncResultError)
{
const QString src = qtSource();
EXPECT_TRUE(src.contains("if (_r.error.ok()) logosDispatchRejection(v, _r.error);"));
// Folded BEFORE the value is decoded and before the callback runs, so the
// callback never sees an ok() AsyncResult for a rejected call.
const int fold = src.indexOf("if (_r.error.ok()) logosDispatchRejection(v, _r.error);");
const int decode = src.indexOf("_r.value = v.isValid() ? qvariant_cast<int>(v) : 0;");
const int deliver = src.indexOf("callback(_r);");
EXPECT_NE(fold, -1);
EXPECT_NE(decode, -1);
EXPECT_NE(deliver, -1);
EXPECT_LT(fold, decode);
EXPECT_LT(decode, deliver);
}
TEST(AsyncResult, ThePlainAsyncEntryPointStillOnlyWarns)
{
// Unchanged public surface -> nowhere to put an error -> the log is all
// there is. The warning names `<name>Async`, never `<name>AsyncResult`.
const QString src = qtSource();
EXPECT_TRUE(src.contains("{ logos::CallError _rej; if (logosDispatchRejection(v, _rej))"));
EXPECT_TRUE(src.contains("Mod::addAsync: remote call failed:"));
EXPECT_FALSE(src.contains("Mod::addAsyncResult: remote call failed:"));
}
+4 -4
View File
@@ -94,8 +94,8 @@ TEST(MakeHeaderTest, ContainsMethodDeclarations)
QJsonArray methods = makeTestMethods();
QString h = makeHeader("test_mod", "TestMod", methods);
EXPECT_TRUE(h.contains("int add(int a, int b, logos::CallError* err = nullptr)"));
EXPECT_TRUE(h.contains("void reset(logos::CallError* err = nullptr)"));
EXPECT_TRUE(h.contains("int add(int a, int b, logos::CallError* err = nullptr, Timeout timeout = Timeout())"));
EXPECT_TRUE(h.contains("void reset(logos::CallError* err = nullptr, Timeout timeout = Timeout())"));
// Non-invokable should not appear
EXPECT_FALSE(h.contains("internal"));
}
@@ -197,7 +197,7 @@ TEST(MakeHeaderTest, QVariantListAsyncOverload)
}
QString h = makeHeader("mod", "Mod", methods);
EXPECT_TRUE(h.contains("QVariantList getItems(logos::CallError* err = nullptr)"));
EXPECT_TRUE(h.contains("QVariantList getItems(logos::CallError* err = nullptr, Timeout timeout = Timeout())"));
EXPECT_TRUE(h.contains("getItemsAsync("));
EXPECT_TRUE(h.contains("std::function<void(QVariantList)> callback"));
}
@@ -215,7 +215,7 @@ TEST(MakeHeaderTest, QVariantMapAsyncOverload)
}
QString h = makeHeader("mod", "Mod", methods);
EXPECT_TRUE(h.contains("QVariantMap getData(logos::CallError* err = nullptr)"));
EXPECT_TRUE(h.contains("QVariantMap getData(logos::CallError* err = nullptr, Timeout timeout = Timeout())"));
EXPECT_TRUE(h.contains("getDataAsync("));
EXPECT_TRUE(h.contains("std::function<void(QVariantMap)> callback"));
}
+4 -4
View File
@@ -44,7 +44,7 @@ TEST(MakeSourceTest, ZeroParams)
QJsonArray methods;
methods.append(makeMethod("doStuff", "int", 0));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"doStuff\", QVariantList{}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"doStuff\", QVariantList{}, timeout, &_err)"));
EXPECT_TRUE(src.contains("return _result.toInt()"));
}
@@ -53,7 +53,7 @@ TEST(MakeSourceTest, OneParam)
QJsonArray methods;
methods.append(makeMethod("fn", "bool", 1));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{QVariant::fromValue(p0)}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{QVariant::fromValue(p0)}, timeout, &_err)"));
EXPECT_TRUE(src.contains("return _result.toBool()"));
}
@@ -62,7 +62,7 @@ TEST(MakeSourceTest, TwoParams)
QJsonArray methods;
methods.append(makeMethod("fn", "void", 2));
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{QVariant::fromValue(p0), QVariant::fromValue(p1)}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", QVariantList{QVariant::fromValue(p0), QVariant::fromValue(p1)}, timeout, &_err)"));
}
TEST(MakeSourceTest, ThreeParams)
@@ -121,7 +121,7 @@ TEST(MakeSourceTest, ListArgWrappedAsOneElement)
methods.append(m);
QString src = makeSource("mod", "Mod", "mod.h", methods);
EXPECT_TRUE(src.contains("invokeRemoteMethod(\"mod\", \"echoList\", QVariantList{QVariant::fromValue(v)}, Timeout(), &_err)"));
EXPECT_TRUE(src.contains("invokeRemoteMethod(\"mod\", \"echoList\", QVariantList{QVariant::fromValue(v)}, timeout, &_err)"));
EXPECT_TRUE(src.contains("invokeRemoteMethodAsync(\"mod\", \"echoList\", QVariantList{QVariant::fromValue(v)}"));
// The bare (spreading) form must not appear.
EXPECT_FALSE(src.contains("QVariantList{v}"));
+1 -1
View File
@@ -88,7 +88,7 @@ TEST(Records, QtWrapperExposesTheStruct)
EXPECT_TRUE(h.contains(" QList<Status> items{};"));
EXPECT_TRUE(h.contains(" QMap<QString, Status> tags{};"));
EXPECT_TRUE(h.contains("Status getStatus(logos::CallError* err = nullptr);"));
EXPECT_TRUE(h.contains("Status getStatus(logos::CallError* err = nullptr, Timeout timeout = Timeout());"));
EXPECT_TRUE(h.contains("QString describeStatus(const Status& s,"));
EXPECT_TRUE(h.contains("QList<Status> listStatuses("));