mirror of
https://github.com/logos-co/logos-logoscore-py.git
synced 2026-08-27 19:21:11 +00:00
* feat(conformance): the py driver for the LIDL type matrix
The driver half of the conformance matrix; the case table and the xfail
registry live in logos-test-modules/conformance/, with the providers they
describe. The driver lives here because it uses this package's client — and
logoscore-py already depends on logos-test-modules, so the reverse would be a
cycle.
`conformance/run_matrix.py` replays every case against BOTH providers and
reports one JSONL line per cell, keyed by coordinate. `checks.conformance-matrix`
runs it; it fails on a red cell, on an `xpass` (a registered known-broken cell
that started passing — the registry has to be updated), or on a (type, position)
the contract declares that no case covers.
Three things it took a run to get right, all recorded in the code:
* `same()` is type-strict — a matrix that compares with `==` cannot see an
integer degrading to a float, which is most of what this exists to catch,
and `1 == True` would pass too.
* a case may set `"raw": true` to opt out of tagged-bytes materialization.
Without it the adversarial `_bytes`-collision cases are inexpressible: the
driver would convert BOTH the argument and the expectation to bytes and the
cell would compare bytes to bytes and pass no matter what the system did —
the exact fake-green the matrix is meant to remove.
* a dispatch rejection arrives as the RESULT ({"code": "dispatch_failed"}),
not as a raised error, so a driver that only watches for exceptions records
a rejection as a successful call returning a dict.
Each provider gets one daemon PER PHASE. Sharing a daemon between the ~80-call
method phase and the event phase wedged it partway through the events under the
nix sandbox — the last five failed contiguously with RPC_FAILED while every one
of them passes on a fresh daemon. A phase that can poison the next one makes a
red cell mean "something earlier used up a resource", which is the kind of
unreliable signal this exercise exists to remove.
Verified end to end: green (139 pass / 13 registered xfail / 68 differential),
and RED on 19 cells when pointed at the pre-fix logoscore CLI — so it demonstrably
catches the class of bug it is for, rather than merely claiming to.
Also adds a unit test asserting the inline `_fullapi_module_cases.py` table and
the shared `cases.json` agree. It does not merge them — rewriting a passing
integration suite to prove a point is a bad trade — but it makes the drift
between two hand-maintained copies a red test instead of a silent surprise.
(Confirmed it fails on an injected divergence, not just that it passes.)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(conformance): N-ary events and positional cells in the driver
Follows the arity surface added to the contract. Three changes:
* an event case may carry `values` (a list) instead of `value`; the driver
fires with *values and compares the ORDERED argument list, so slot order is
part of the assertion rather than something the payload shape hides;
* a multi-parameter event arrives as {arg0, arg1, ...} — the driver rebuilds
the ordered list, and a single-argument event still reduces to its one
value, so no existing case changes;
* coverage now distinguishes `method_arg` from `method_arg@k` (and the event
equivalent). A sole argument cannot catch a generator that mixes up
positional slots, so they are genuinely different cells, and a case that
covers several declares the (type, position) PAIRS explicitly instead of
the cross-product of its type and position lists — the cross-product would
claim cells the case never exercises.
79 cases x 2 providers, green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(conformance): data-driven provider set + the ext matrix check
`--modules NAME=DIR` (repeatable) generalises the two hardcoded provider flags,
so a table with a different provider set runs through the SAME driver instead of
a second one. The full_api_ext table has one provider — the C++ cdylib backend
cannot express records or [bstr] yet — so the differential simply has nothing to
compare there; it is not silently skipped, there is just no pair.
The table names the providers it describes and the driver runs the intersection,
failing loudly if a declared provider has no module dir rather than quietly
reporting a smaller matrix.
Adds `checks.conformance-matrix-ext`: 20 cases, full contract coverage, green
with 9 registered xfails (E1 bytes-at-depth mangling, E2 empty-bytes-at-depth
dropped to null, E3 M1 through a record field).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(conformance): per-case provider precision + the C++ ext provider
The ext check now runs both ext providers, so that table has a differential like
full_api does.
`known.json` entries may carry `per_case_providers`, narrowing an individual
case to the providers that actually fail it. `providers` alone is an entry-wide
union, and a defect that only one provider surfaces then gets registered against
both — so the provider that PASSES is reported as `xpass` and the registry
manufactures a failure it then demands you fix. 4 of E1's 9 cases are
single-provider, which is how this surfaced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(conformance): state what this driver does not cover
The module docstring claimed "Other consumers (the C++, Rust and QML proxies)
replay the same cases.json". No such driver exists — `--consumer` is a label
written into the report, not a driver selector. Grepping the workspace finds
only this one.
That overclaim mattered. The event bridge failed to decode canonical
{"_bytes": ...} into a QByteArray, and this driver could not see it: the
undecoded map round-trips to JSON and the python client decodes the tag itself.
The cells stayed green while a Qt/C++ or QML subscriber got a map. A matrix with
one consumer cannot see a defect that its own consumer happens to undo, and a
docstring promising four consumers hides that.
Also states the transport limit: the daemon is built with no `transports=`, so
every cell is measured over LocalSocket/QtRO and the plain wire's separate uint64
defect is out of reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(fullapi): 64-bit boundaries, and re-pin onto the merged chain
The integration matrix — replayed by the local, tcp and tcp_ssl checks and by
the docker codec matrix — topped out at 2^53-1 for `int` and 2^32-1 for `uint`.
So the 64-bit band that the whole LIDL type contract is about was untested on
every transport, and it was broken on two of them.
Against the pinned protocol over tcp the new cases fail exactly as the code
predicts:
echoUint(2^63) -> -9223372036854775808 (RpcValue has no unsigned alt)
echoUint(2^64-1) -> -1 (same)
uintEvent(2^64-1) -> 1.8446744073709552e+19 (the event bridge, M6)
Two different defects, on the same transport, that no existing case could see.
Both are fixed in logos-protocol; all 68 pass on local, tcp and tcp_ssl.
2^53+1 is in the table on purpose: it is the smallest integer a double cannot
represent, so it separates "degraded through a float" from "wrapped as an
integer" — the two failure modes look alike in a report and have different
causes.
Also re-pins onto the merged chain now that logos-test-modules#28 landed:
logos-test-modules -> d4c0d04 (the conformance matrix)
logos-logoscore-cli -> a143727 (64-bit call args)
That re-pin matters beyond housekeeping: the lock previously resolved
logos-protocol to a pre-#29 revision, so the matrix's own registry claimed fixes
that the code under measurement did not contain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: re-pin onto the merged uint64 chain
logos-logoscore-cli -> 94f54b3 (#75 — carries logos-protocol 8b8a358)
logos-test-modules -> 1157a46 (#29 — M6 retired from the registry)
Both in one commit because each is the other's precondition. 1157a46 removes M6
from the xfail registry, so pinning it against a daemon without the fix turns
that cell into a hard failure; pinning the fixed daemon without it turns the same
cell into an xpass, which also fails the run. Either alone is red — which is the
registry's forcing function working in both directions.
This is also the commit that makes the matrix describe the code it measures. The
lock previously resolved logos-protocol to a pre-#29 revision, so known.json
claimed fixes that the daemon under measurement did not contain.
verified with NO overrides, on merged revisions only:
conformance-matrix 158 pass / 6 xfail, differential 73
conformance-matrix-ext 40 pass, differential 19
integration local/tcp/tcp_ssl 68 passed each
unit 51 passed, 2 skipped
The 64-bit boundary cases added earlier in this branch are the ones that were
red before the protocol fix landed — echoUint(2^64-1) as -1 on the plain wire,
uintEvent(2^64-1) as 1.8446744073709552e+19 on every transport.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
129 lines
6.5 KiB
Python
129 lines
6.5 KiB
Python
"""Shared expectations for the `test_fullapi_cpp` full type surface.
|
|
|
|
Extracted so the same matrix can be replayed against different daemons
|
|
(local socket, TCP, TCP+SSL, in a docker container, over JSON or CBOR).
|
|
The full-api module declares one echo method per supported type **and one
|
|
typed event per event-legal type** — so these tables are the single place
|
|
that pins the whole parameter/return/event surface, and the docker smoke
|
|
suite replays them over both wire codecs.
|
|
|
|
Structure:
|
|
FULLAPI_METHOD_CASES — `(method, args, expected)`; `expected=None` means
|
|
"must dispatch cleanly, value unchecked".
|
|
FULLAPI_EVENT_CASES — `(event, fire_method, value)`; firing
|
|
`fire<X>Event(value)` emits `<X>Event` whose `data.arg0 == value`.
|
|
|
|
Kept parallel to the inline assertions in
|
|
`tests/integration/test_fullapi_module_cpp.py` so the two drift together;
|
|
`FULLAPI_EVENT_CASES` is imported directly by that file (single source).
|
|
|
|
`bytes` args/returns cross the wire as the canonical `{"_bytes":"<b64url>"}`
|
|
tag (see `LogoscoreClient._arg_to_str` / `_proc.decode_bytes_tags`), so
|
|
every byte value round-trips. Float cases use exactly-representable values
|
|
so a plain `==` holds across both codecs (no approximate compare needed).
|
|
|
|
See: repos/logos-test-modules/test-fullapi-module-cpp/src/test_fullapi_cpp_impl.h
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
# ── Method call matrix (param / return, every supported type) ───────────────
|
|
FULLAPI_METHOD_CASES: list[tuple[str, tuple, object]] = [
|
|
# identity
|
|
("whoAmI", (), "test_fullapi_cpp"),
|
|
|
|
# tstr
|
|
("echoString", ("round-trip",), "round-trip"),
|
|
|
|
# int / uint
|
|
("echoInt", (42,), 42),
|
|
("echoInt", (-7,), -7),
|
|
("echoUint", (7,), 7),
|
|
|
|
# 64-bit boundaries. These matter far more than the small values above:
|
|
# this table is what the tcp / tcp_ssl / json / cbor matrices replay, and it
|
|
# had NO value outside int32 range, so the plain wire's uint64 handling was
|
|
# entirely untested. A uint64 above int64max used to arrive as -1 there
|
|
# (RpcValue had no unsigned alternative), and int64::min/max exercise the
|
|
# signed edges the double-degradation bugs kept landing on.
|
|
("echoUint", (2**64 - 1,), 2**64 - 1),
|
|
("echoUint", (2**63,), 2**63),
|
|
("echoInt", (2**63 - 1,), 2**63 - 1),
|
|
("echoInt", (-(2**63),), -(2**63)),
|
|
# 2^53+1 is the smallest integer a double cannot represent — it separates
|
|
# "degraded through a float" from "wrapped as an integer".
|
|
("echoInt", (2**53 + 1,), 2**53 + 1),
|
|
|
|
# float64 — values MUST stay exactly-representable (dyadic) so a plain
|
|
# `==` holds across json + cbor; the docker matrix compares without approx.
|
|
("echoDouble", (2.5,), 2.5),
|
|
("echoDouble", (-0.5,), -0.5),
|
|
|
|
# bool
|
|
("echoBool", (True,), True),
|
|
("echoBool", (False,), False),
|
|
|
|
# bstr — canonical {"_bytes"} tag, NUL/high-byte safe
|
|
("echoBytes", (b"hello",), b"hello"),
|
|
("echoBytes", (b"\x01\x02\x03\xff",), b"\x01\x02\x03\xff"),
|
|
|
|
# any — scalars, container, and map all pass through unchanged
|
|
("echoAny", ("hi",), "hi"),
|
|
("echoAny", (42,), 42),
|
|
("echoAny", (True,), True),
|
|
("echoAny", ([1, 2, 3],), [1, 2, 3]),
|
|
("echoAny", ({"k": "v"},), {"k": "v"}),
|
|
|
|
# typed arrays
|
|
("echoStringList", (["a", "b", "c"],), ["a", "b", "c"]),
|
|
("echoIntList", ([1, 2, 3],), [1, 2, 3]),
|
|
("echoIntList", ([-1, 0, 5],), [-1, 0, 5]),
|
|
("echoIntList", ([],), []),
|
|
("echoUintList", ([4, 5, 6],), [4, 5, 6]),
|
|
("echoDoubleList", ([1.5, 2.5, -0.5],), [1.5, 2.5, -0.5]),
|
|
("echoBoolList", ([True, False, True],), [True, False, True]),
|
|
|
|
# [any] (LogosList) — heterogeneous incl. a nested float (dyadic),
|
|
# nested map, and nested list (exercises float-inside-[any] over cbor)
|
|
("echoList", ([1, "two", 3.5, {"k": 1}, [9]],), [1, "two", 3.5, {"k": 1}, [9]]),
|
|
|
|
# {tstr:any} (LogosMap) — mixed value types incl. a nested float + nesting
|
|
("echoMap", ({"s": "v", "n": 42, "f": 1.5, "b": True, "nested": {"x": [1, 2]}},),
|
|
{"s": "v", "n": 42, "f": 1.5, "b": True, "nested": {"x": [1, 2]}}),
|
|
|
|
# result — {"success", "value", "error"} (absent side is null)
|
|
("makeResult", (True,),
|
|
{"success": True, "value": {"ok": True, "provider": "test_fullapi_cpp"}, "error": None}),
|
|
("makeResult", (False,),
|
|
{"success": False, "value": None, "error": "deliberate error for testing"}),
|
|
|
|
# void — CLI reports `true` as the success sentinel
|
|
("doVoid", (), True),
|
|
]
|
|
|
|
|
|
# ── Typed-event matrix (one event per event-legal type) ─────────────────────
|
|
# Each event is fired by its bool-returning `fire<X>Event(v)` shim; the
|
|
# provider emits `<X>Event` carrying `v` as its single `data.arg0` payload.
|
|
FULLAPI_EVENT_CASES: list[tuple[str, str, object]] = [
|
|
("stringEvent", "fireStringEvent", "hello"),
|
|
("bytesEvent", "fireBytesEvent", b"\x01\x02\x03\xff"),
|
|
("intEvent", "fireIntEvent", -42),
|
|
("uintEvent", "fireUintEvent", 7),
|
|
# The event path's own 64-bit boundaries — the method cases above do not
|
|
# cover them, and the two paths have diverged before (see M6: exact as a
|
|
# method return, degraded to a double as an event).
|
|
("uintEvent", "fireUintEvent", 2**64 - 1),
|
|
("intEvent", "fireIntEvent", -(2**63)),
|
|
("intEvent", "fireIntEvent", 2**53 + 1),
|
|
("doubleEvent", "fireDoubleEvent", 2.5),
|
|
("boolEvent", "fireBoolEvent", True),
|
|
("anyEvent", "fireAnyEvent", {"k": "v", "n": 1}),
|
|
("stringListEvent", "fireStringListEvent", ["p", "q"]),
|
|
("intListEvent", "fireIntListEvent", [1, 2, 3]),
|
|
("uintListEvent", "fireUintListEvent", [4, 5]),
|
|
("doubleListEvent", "fireDoubleListEvent", [1.5, 2.5]),
|
|
("boolListEvent", "fireBoolListEvent", [True, False]),
|
|
("listEvent", "fireListEvent", [1, "two", {"k": 1}]),
|
|
("mapEvent", "fireMapEvent", {"mk": "mv"}),
|
|
]
|