Files
logos-cpp-sdk/nix/tests-generator-cli.nix
T
Dario Gabriel LipicarandClaude Opus 5 620f2e184c feat(generator): a Qt-typed umbrella that needs no LogosAPI
Splits a consumer's TYPE SURFACE from its TRANSPORT. Until now the qt umbrella
was `explicit LogosModules(LogosAPI* api)` while the lp one was default-
constructible, so "Qt types" implicitly meant "has a LogosAPI" — and a cdylib
module, whose provider surface is the std logos_module_impl.h C ABI and which
holds no LogosAPI anywhere, could not have Qt-typed dependency wrappers at all.
Its generated glue emits `new LogosModules()` unconditionally
(lidl_gen_cdylib.cpp:693), so the combination did not merely misbehave, it did
not compile.

That was a codegen choice, not a law: the wrapper bodies already run over lp_*.

`--binding api|origin` selects it, defaulting to `api`. A second enum rather
than a third ApiStyle value, deliberately: ApiStyle names the type surface and
is switched on by six emitters (makeHeader/makeSource/returnTypeFor/
paramTypeFor/toWireFor/fromWireFor); a "Qt types, explicit origin" member would
force all six to answer a transport question whose honest answer is "same as
Qt" every time. ApiStyle::Lp ignores the new axis — lp is origin-bound by
construction — and that is asserted rather than assumed.

The emitted umbrella bakes metadata.json#name as the origin literal:

    LogosModules() : test_fullapi_cpp(QStringLiteral("test_fullapi_qtproxy")) {}
    FullApi bind_full_api(const QString& moduleName) {
        return FullApi(QStringLiteral("test_fullapi_qtproxy"), moduleName); }

Origin is the CONSUMER's own name and target is the dep — origin first in both
bind_ overloads. This is the load-bearing property: LpBridge::forTarget derives
origin from `api->moduleName()`, and reusing it silently gives a consumer the
caller's identity, which has already preserved a privilege escalation once in
this tree. An empty metadata name is refused at the CLI (exit 6, naming the
file) and emits `#error` in the header: a module that cannot state its identity
must not compile, and must never be handed a blank or borrowed one.

Verified additive on 172 real metadata.json x 2 api-styles = 344 runs, all
producing output, byte-identical old binary vs new. Mutation control: swapping
bind_<iface>'s (origin, moduleName) to (moduleName, origin) fails the suite at
MakeUmbrellaTest.QtExplicitOriginStatesTheConsumersOwnNameEverywhere. 281 -> 286
tests.

Framing worth keeping: the origin is SELF-ASSERTED from the module's own
metadata and is not attested by the transport. That is not a regression —
`api->moduleName()` is equally process-stated — but "explicit origin" means the
module names itself, not that the host vouches for the name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:14:58 -03:00

154 lines
7.9 KiB
Nix

# CLI-level assertions about logos-cpp-generator's ARGUMENT SURFACE.
#
# The gtest suite in nix/tests.nix links the generator's internals; it never
# runs the binary, so a flag that was removed from the CLI cannot be asserted
# there. This check runs the real `logos-cpp-generator` and looks at EXIT CODES.
#
# Why exit codes and not output diffing: `--module-dir` had no caller left in
# any nix build, so removing it changes no build output anywhere — a
# store-path diff of every module in the tree would be empty either way and
# would prove nothing. The only observable difference is what the binary does
# when handed the flag, and that is exactly what this asserts.
{ pkgs, common, generator }:
pkgs.runCommand "${common.pname}-generator-cli-tests"
{
nativeBuildInputs = [ generator ];
meta = common.meta;
}
''
set -u
fail() { echo "FAIL: $*" >&2; exit 1; }
mkdir -p work/modules && cd work
cat > metadata.json <<'EOF'
{
"name": "cli_probe_module",
"version": "1.0.0",
"type": "core",
"dependencies": ["dep_one", "dep_two"]
}
EOF
# ── Positive control ──────────────────────────────────────────────────
# Same binary, same metadata, no `--module-dir`: must exit 0 and list the
# dependencies. Without this, a non-zero exit below could just as well mean
# "the binary is broken" or "the metadata is unreadable".
# NB: never assign to `out` here — that is the derivation's output path.
set +e
listing=$(logos-cpp-generator --metadata ./metadata.json 2>err.txt)
control_status=$?
set -e
if [ "$control_status" -ne 0 ]; then
echo "--- stderr ---" >&2; cat err.txt >&2
fail "control: the generator refused a plain --metadata run (exit $control_status)"
fi
echo "$listing" | grep -qx 'dep_one' || fail "control: dep_one missing from the dependency listing"
echo "$listing" | grep -qx 'dep_two' || fail "control: dep_two missing from the dependency listing"
echo "OK: control — --metadata alone exits 0 and lists dependencies"
# ── The assertion ─────────────────────────────────────────────────────
# `--module-dir <dir>` was the multi-dependency plugin-introspection mode.
# It must now be REFUSED, not ignored: a silent fall-through to the listing
# above would exit 0 having generated nothing.
set +e
logos-cpp-generator --metadata ./metadata.json --module-dir ./modules \
>moddir.out 2>moddir.err
status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "--- stdout ---" >&2; cat moddir.out >&2
fail "--module-dir exited 0; the removed flag is still accepted"
fi
echo "OK: --module-dir exits non-zero (status=$status)"
grep -q -- '--module-dir was removed' moddir.err \
|| { echo "--- stderr ---" >&2; cat moddir.err >&2
fail "--module-dir failed, but not with the removal diagnostic"; }
echo "OK: --module-dir fails with the removal diagnostic"
# An existing directory must not change the answer — the old code only
# errored when the directory was MISSING (exit 2 from the QDir::exists
# check), so a passing test against a nonexistent path would prove nothing.
if [ ! -d ./modules ]; then fail "fixture: ./modules should exist"; fi
# `--general-only` is the supported replacement and must still work, so the
# refusal above is a removal of one mode rather than of the metadata path.
logos-cpp-generator --metadata ./metadata.json --general-only \
--output-dir ./gen >/dev/null 2>generalonly.err \
|| { cat generalonly.err >&2; fail "--general-only regressed"; }
[ -s ./gen/logos_sdk.h ] || fail "--general-only emitted no logos_sdk.h"
echo "OK: --general-only still emits the umbrella"
# ── `--binding origin`: the umbrella a module with no LogosAPI needs ──
#
# Emitter-level assertions live in the gtest suite; these are the ones only
# the BINARY can answer — that the flag is wired to the mode at all, that an
# unrecognised value is refused rather than defaulted, and that a module
# with no name of its own is refused rather than given a blank identity.
cat > origin_metadata.json <<'EOF'
{
"name": "cli_origin_module",
"version": "1.0.0",
"type": "core",
"dependencies": ["dep_one", "dep_two"]
}
EOF
logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --binding origin --output-dir ./gen-origin >/dev/null 2>origin.err || { cat origin.err >&2; fail "--binding origin was refused"; }
[ -s ./gen-origin/logos_sdk.h ] || fail "--binding origin emitted no logos_sdk.h"
# Default-constructible, so the cdylib glue's `new LogosModules()` compiles.
grep -q 'LogosModules() : dep_one(QStringLiteral("cli_origin_module"))' ./gen-origin/logos_sdk.h || { cat ./gen-origin/logos_sdk.h >&2
fail "the origin-bound umbrella is not default-constructible"; }
# THE property: the origin is this module's OWN name, never an api object's.
# `forTarget` derives an origin from `api->moduleName()`, and a wrapper
# built on a borrowed api calls out under the lender's identity — so the
# umbrella must hand every wrapper a stated name and hold no LogosAPI at all.
if grep -q 'LogosAPI' ./gen-origin/logos_sdk.h; then
cat ./gen-origin/logos_sdk.h >&2
fail "the origin-bound umbrella still mentions LogosAPI"
fi
grep -q 'dep_two(QStringLiteral("cli_origin_module"))' ./gen-origin/logos_sdk.h || fail "a dependency was not handed the consuming module's own name"
echo "OK: --binding origin emits a default-constructible, LogosAPI-free umbrella"
# The default is unchanged — same metadata, no flag, the historical shape.
logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --output-dir ./gen-api >/dev/null 2>&1 || fail "the default (LogosAPI) umbrella regressed"
grep -q 'explicit LogosModules(LogosAPI\* api)' ./gen-api/logos_sdk.h || { cat ./gen-api/logos_sdk.h >&2
fail "the default umbrella is no longer the LogosAPI-taking one"; }
echo "OK: the default binding still emits the LogosAPI umbrella"
# A misspelt value is refused. Defaulting it back to the LogosAPI form would
# emit `LogosModules(LogosAPI*)` into a module that has none, and the
# diagnostic would land as a constructor mismatch in generated code.
set +e
logos-cpp-generator --metadata ./origin_metadata.json --general-only --api-style qt --binding orgin --output-dir ./gen-bad >badbinding.out 2>badbinding.err
status=$?
set -e
[ "$status" -ne 0 ] || fail "--binding orgin (misspelt) exited 0"
grep -q -- 'Unknown --binding value' badbinding.err || { cat badbinding.err >&2; fail "a bad --binding failed without saying why"; }
echo "OK: an unrecognised --binding is refused"
# A module with no name cannot state an origin, and must not be given a
# blank one. Refused at the CLI, where the metadata file can be named.
cat > anonymous_metadata.json <<'EOF'
{
"version": "1.0.0",
"type": "core",
"dependencies": ["dep_one"]
}
EOF
set +e
logos-cpp-generator --metadata ./anonymous_metadata.json --general-only --api-style qt --binding origin --output-dir ./gen-anon >anon.out 2>anon.err
status=$?
set -e
[ "$status" -ne 0 ] || fail "--binding origin accepted metadata with no name"
grep -q "asserted" anon.err || { cat anon.err >&2; fail "the anonymous-origin refusal does not explain itself"; }
echo "OK: --binding origin refuses a module that cannot name itself"
mkdir -p "$out"
echo "logos-cpp-generator CLI argument-surface tests passed" > "$out/result.txt"
''