Files
Dario LipicarandClaude Opus 5 318286de7a conformance: the consumer axis, two relocks, and the two races they exposed (#22)
* build: relock onto test-modules and logoscore-cli master

test-modules ff5dbbd brings the ext qtproxy fixture this branch consumes, and
the conformance registries with the arity entries retired.

logoscore-cli 1ec3683 carries ed19258 (#99) and b3f1a403 (#101), which is what
retires the pre-99 cells: the daemon reads the error channel instead of
inferring failure from a null RESULT, and asks the module for
getPluginMethods on a null return so an unknown name becomes METHOD_NOT_FOUND.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* conformance(ext): run the ext table through the Qt consumer, twice

The ext gate ran one consumer point and computed no consumer differential. It
now runs the same three-consumer shape the full_api gate has — py,
extqtproxy-sync, extqtproxy-async — against test_fullapi_ext_qtproxy, the
Qt-typed consumer of full_api_ext (a SEPARATE module from test_fullapi_qtproxy,
because a Qt consumer wrapper is generated per contract).

THE PROBE IS THE ONLY DRIVER CHANGE, and it is a real one. A consumer point with
a call mode proves the selected wrapper table actually ran by making one
known-good call and reading `lastCallStatus()` back — `useCallMode("async")`
returning true only says the flag is set. That probe was hard-coded to
`echoInt(1)`, which is a full_api method. full_api_ext has none like it: no
method takes a bare scalar, and probing its one zero-parameter method with a
spare `1` would work only because of a REGISTERED defect (known-ext.json
B-arity-overflow drops extra arguments) and would break the day that is fixed.

So `--proxy-consumer` grew a fifth field: `METHOD[:JSON_ARGS]`, defaulting to
`echoInt:[1]` so no existing invocation moves. Arguments go through the table's
own `materialize`, so bytes are spelled `{"_bytes": ...}` in the probe exactly
as they are in a case. A malformed spec is REFUSED rather than defaulted —
falling back to `echoInt` would silently stop proving the call mode and the run
would stay green. tests/unit/test_matrix_proxy_consumer.py pins that, the
default, and the four-field spelling.

Measured on framework.lan with logos-test-modules at its matching branch:
264 cells, 217 pass / 47 xfail / 0 fail / 0 xpass / 0 uncovered.

* conformance: an error expectation may name a set of acceptable codes

`matches` tested one code as a substring of the reported error. A cell whose
answer is decided by which of two deadlines fires first cannot be spelled that
way: asserting either token asserts the timing rather than the claim.

A LIST means any-of. Deliberately not a general escape hatch -- widening a
single-code expectation weakens it, so a case that does this owes an
explanation in `why` of what the codes have in common. The one case that uses
it (failure/A/module-not-loaded) gives it: both are reported on the error
channel and name a transport reason, and a VALUE or a provider-side code still
fails.

The single-string form is untouched, and the new test pins both -- including
that a successful call is still a failure for an error expectation, which is
the property the widening must not cost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* conformance: retry the SUBSCRIBE half of the event race, not just the fire half

`capture_event` already knew the watcher and the fire race each other, and
re-fired on a cadence to close it. Subscribing races too, and that half was
attempted exactly once: `on_event` asked too early is REFUSED, the daemon
answers WATCH_FAILED, and nothing tried again -- so an event cell reported a
transport error instead of a value.

MEASURED, across three otherwise identical runs of the same table at the same
revision: event/tstr on test_fullapi_rust failed, passed, then failed. In both
failing runs BOTH proxy consumers received "hello" from the same provider,
which is what rules out the module: it was emitting, and the `py`
subscription was losing the race. It is not a regression from the SDK bump
either -- the bump is what made module startup slow enough to lose more often.

Retried against the same deadline as the fire loop rather than a fixed count,
so a slow start costs time instead of a false negative. A subscription that
never succeeds still raises and still fails the cell, so this closes a race
without hiding a refusal.

Not registered as an xfail: the cell PASSES most of the time, and a registered
cell that passes is an xpass, which reddens the gate. A flake has to be fixed
or left visible; it cannot be parked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* conformance: an any-of expectation declares the DIFFERENTIAL too, not just the cell

Widening failure/A/module-not-loaded to accept either transport code fixed the
CELL and left three differential rows failing: qtproxy-sync answered
RPC_FAILED while qtproxy-async answered object_unavailable, and the
differential compares actual values independently of `expect`. I had assumed
the delta rows follow the cell's status; they do not, and the run said so.

That independence is deliberate and worth keeping -- it is what caught the
`void` divergence nobody predicted -- so this does not become "both sides
matched the expectation, therefore they agree".

The narrow rule instead: an `expect` naming a LIST of codes has already said
more than one answer satisfies the claim, which is the same kind of statement
`expect_by_provider` makes for the provider axis. It gets the same treatment --
status `declared`, printed so the divergence stays visible, not counted as a
failure. It applies only when the expectation names several codes AND BOTH
sides satisfy it; a coordinate answering something outside the set, or a value
instead of an error, still fails.

One implementation, at module scope, called by the differential and exercised
directly by the test -- the first draft had the rule written twice, once in a
closure and once for the test to reach, which is a pair that drifts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: move the driver tests to the file that loads the driver

CI red on both arches, 2 failed / 140 passed:

  AttributeError: module 'matrix_report' has no attribute 'matches'
  AttributeError: module 'matrix_report' has no attribute 'Result'

I put two tests for run_matrix.py's `matches` and `declared_multi_divergence`
into tests/unit/test_matrix_report.py, whose `R` is the matrix_report module.
The driver is a different file and is loaded by importlib, in
tests/unit/test_matrix_proxy_consumer.py. The tests move there, unchanged in
substance.

Why this reached CI at all: I could not run pytest locally and shipped the
tests unexecuted. That was wrong twice over — `python3 -m pytest` failed only
because the homebrew interpreter has no pytest, and pyenv had one the whole
time. "The tool is missing" was a conclusion I drew from one command instead of
looking.

Now run: 138 passed, 4 skipped locally (the 4 skips are environmental — the
shared conformance table is not resolvable outside the nix build; CI resolves
it and runs them). Total matches CI's 142.

Both conformance matrices were already green in CI on this branch; this touches
nothing they exercise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:42:34 -03:00

491 lines
28 KiB
Nix

{
description = "Python wrappers for the logoscore and logosctl CLIs — launch daemons, load modules, call methods, subscribe to events";
inputs = {
logos-nix.url = "github:logos-co/logos-nix";
nixpkgs.follows = "logos-nix/nixpkgs";
logos-logoscore-cli.url = "github:logos-co/logos-logoscore-cli";
logos-test-modules.url = "github:logos-co/logos-test-modules";
};
outputs = { self, nixpkgs, logos-logoscore-cli, logos-test-modules, ... }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
inherit system;
pkgs = import nixpkgs { inherit system; };
});
in
{
# ── Packages ──────────────────────────────────────────────────────────
# `nix build` produces a Python wheel. The `logoscore` CLI is propagated
# so anyone using this package also has the binary on PATH.
#
# `dockerBundle` / `dockerBundlePortable` (Linux only) prepare an
# `out/bundle` directory consumed by `tests/docker_smoke/Dockerfile`
# — the smoke image's stage-1 nix-build copies it into the
# ubuntu-based runtime stage. The actual docker image is built
# via `tests/docker_smoke/build_smoke_image.sh`, not directly
# from these flake outputs.
packages = forAllSystems ({ pkgs, system }:
let
logoscoreBin = logos-logoscore-cli.packages.${system}.default;
pythonPkg = pkgs.python3Packages.buildPythonPackage {
pname = "logoscore";
version = "0.1.0";
format = "pyproject";
src = ./.;
nativeBuildInputs = [ pkgs.python3Packages.hatchling ];
# Only `logoscore` is propagated, even though the wheel now also
# ships the `logosctl` client. Propagating `ctl` would put both
# binaries on the critical path of `nix build` — the one output
# every consumer of this flake pulls. Anyone who wants logosctl
# takes it from `logos-logoscore-cli.packages.*.ctl` explicitly
# (the dev shell and the logosctl checks below do exactly that).
propagatedBuildInputs = [ logoscoreBin ];
doCheck = false;
pythonImportsCheck = [ "logoscore" "logosctl" ];
};
# ── Docker bundles ─────────────────────────────────────────────
# The bundle is **just the logoscore CLI** plus whatever modules
# it ships with (currently capability_module, package_manager_module).
# No test modules — those get bind-mounted at runtime via
# `-v $modules_dir:/user-modules` and `-m /user-modules`. That
# makes the image reusable for anyone who wants to test their
# own module: pull the image, `docker run -v ./my-modules:/user-modules
# logoscore:smoke-dev daemon -m /user-modules …`.
#
# Two flavors:
#
# * `dockerBundle` (dev) — uses `.#cli` (the default logoscore
# package, which links against Qt/Boost/OpenSSL from the nix
# store via rpath). Smaller bundle (~60 MB payload) but the
# runtime image MUST ship the nix store so those rpaths
# resolve, and the CLI's built-in modules are found via
# LOGOS_BUNDLED_MODULES_DIR (set by `wrapQtAppsNoGuiHook`
# when the CLI was built). This is the flavor the
# `logoscore-py` dev shell matches.
#
# * `dockerBundlePortable` — uses `.#cli-bundle-dir` (a
# self-contained `bin/ + lib/ + modules/` tree with every
# Qt dep + the CLI's built-in modules copied in). Larger
# (~400 MB) but runs standalone — no nix store needed. The
# CLI's built-in modules live at `/opt/logoscore/modules`
# and are discovered by explicitly passing `-m
# /opt/logoscore/modules` (no wrapper env var here).
#
# The Dockerfile picks one via `--build-arg FLAVOR=dev|portable`.
# The pytest suite parametrises over both flavors so regressions
# in either path surface in the smoke matrix.
logoscorePortable = logos-logoscore-cli.packages.${system}.cli-bundle-dir;
dockerBundle = pkgs.runCommand "logoscore-bundle-dev" { } ''
# Dev flavor: just the binary. rpath points into /nix/store
# (copied wholesale in Dockerfile stage 2), and the
# wrapped binary carries `LOGOS_BUNDLED_MODULES_DIR` baked
# in — pointing at the CLI's own modules dir in the store —
# so capability_module etc. resolve without extra `-m` flags.
mkdir -p $out/bin
cp ${logoscoreBin}/bin/logoscore $out/bin/
'';
dockerBundlePortable = pkgs.runCommand "logoscore-bundle-portable" { } ''
# Portable flavor: cli-bundle-dir is already a self-contained
# bin/ + lib/ + modules/ tree — the CLI's own built-in
# modules live under its modules/ subdir. Copy it as-is.
mkdir -p $out
cp -r ${logoscorePortable}/* $out/
chmod -R u+w $out
'';
in {
default = pythonPkg;
logoscore-py = pythonPkg;
dockerBundle = dockerBundle;
dockerBundlePortable = dockerBundlePortable;
}
);
# ── Dev shell ─────────────────────────────────────────────────────────
# `nix develop` drops you into a shell with python + pytest + logoscore
# + a pre-built test_fullapi_cpp install tree, so `pytest` just works
# without any extra environment setup. The nix `integration` check
# sets the same two env vars, so the dev shell matches CI behaviour.
devShells = forAllSystems ({ pkgs, system }:
let
logoscoreBin = logos-logoscore-cli.packages.${system}.default;
# Both binaries are on PATH here so a plain `pytest` covers both
# suites — tests/logosctl skips silently when LOGOSCTL_BIN is unset,
# which would make the new suite look green while never running.
logosctlBin = logos-logoscore-cli.packages.${system}.ctl;
# `test_fullapi_cpp` (universal C++) is the single test module the
# suite loads — its methods + typed events span the whole
# parameter/return/event surface. `.install` lays out
# modules/<name>/… ready for the daemon's `-m` flag.
testModulesInstall = logos-test-modules.modules.${system}.test_fullapi_cpp.install;
testModulesInstallPortable = logos-test-modules.modules.${system}.test_fullapi_cpp.install-portable;
in {
default = pkgs.mkShell {
packages = [
(pkgs.python3.withPackages (ps: [ ps.pytest ]))
logoscoreBin
logosctlBin
];
# Integration tests skip when these are unset (by design, so
# `pytest` on a plain Python env doesn't try to spawn daemons).
# Exporting them here means the dev shell exercises the full
# suite out of the box.
#
# Two module-dir vars because the docker smoke flavors differ:
# the `dev` image has /nix/store so `.install` modules (which
# rpath into the store) work; the `portable` image is standalone
# so we need `.install-portable` (self-contained). The docker
# smoke fixture picks the right one per flavor.
LOGOSCORE_BIN = "${logoscoreBin}/bin/logoscore";
LOGOSCORE_TEST_MODULES_DIR = "${testModulesInstall}/modules";
LOGOSCORE_TEST_MODULES_DIR_PORTABLE = "${testModulesInstallPortable}/modules";
# The logosctl suite reads its own pair of variables (its conftest
# rebinds `test_modules_dir` to LOGOSCTL_TEST_MODULES_DIR) so a
# machine can point the two suites at different builds. Here they
# are the same modules — the module ABI is shared, only the CLI differs.
LOGOSCTL_BIN = "${logosctlBin}/bin/logosctl";
LOGOSCTL_TEST_MODULES_DIR = "${testModulesInstall}/modules";
shellHook = ''
echo "logos-logoscore-py dev shell"
echo " python: $(python --version)"
echo " logoscore: $(logoscore --version 2>/dev/null || echo 'not on PATH')"
echo " logosctl: $(logosctl --version 2>/dev/null || echo 'not on PATH')"
echo " LOGOSCORE_BIN: $LOGOSCORE_BIN"
echo " LOGOSCORE_TEST_MODULES_DIR (dev): $LOGOSCORE_TEST_MODULES_DIR"
echo " LOGOSCORE_TEST_MODULES_DIR_PORTABLE: $LOGOSCORE_TEST_MODULES_DIR_PORTABLE"
echo " LOGOSCTL_BIN: $LOGOSCTL_BIN"
echo " LOGOSCTL_TEST_MODULES_DIR: $LOGOSCTL_TEST_MODULES_DIR"
export PYTHONPATH="$PWD/src:$PYTHONPATH"
'';
};
});
# ── Checks ────────────────────────────────────────────────────────────
# `nix flake check` runs the unit tests (no daemon required) and the
# integration test suite against a real CLI + test modules.
#
# Both suites exist twice, once per client: `unit` / `integration-*`
# drive `logoscore`, `unit-logosctl` / `integration-logosctl-*` drive
# `logosctl`. Separate derivations throughout, never one derivation
# looping over both — see the `unit-logosctl` comment.
#
# The integration suite is replicated across three transports so a
# regression in tcp framing or tcp_ssl handshaking surfaces at the
# same layer the test names already cover. Three separate flake
# outputs (rather than one derivation that loops) so:
# - CI can fan them out across runners in parallel,
# - a tcp_ssl failure doesn't block the local/tcp signal,
# - the build log of any single transport stays focused.
checks = forAllSystems ({ pkgs, system }:
let
python = pkgs.python3.withPackages (ps: [ ps.pytest ]);
logoscoreBin = logos-logoscore-cli.packages.${system}.default;
# Same repo, sibling output: `ctl` ships alongside `default`/
# `cli` since logos-logoscore-cli#76. Referenced only from the
# `*-logosctl` checks below so a logosctl hiccup cannot redden a
# logoscore check's evaluation path either.
logosctlBin = logos-logoscore-cli.packages.${system}.ctl;
# `.install` lays out modules/<name>/<name>_plugin.{so,dylib} +
# manifest.json — the layout logoscore's `-m` flag expects.
# `test_fullapi_cpp` (universal C++) is the single test module the
# integration suite loads; its methods + typed events span the
# whole parameter/return/event surface.
testModulesInstall = logos-test-modules.modules.${system}.test_fullapi_cpp.install;
# The conformance matrix replays every case against BOTH providers —
# a divergence between them is a finding in its own right, and one
# provider must never be able to satisfy an assertion for the other.
testModulesRustInstall = logos-test-modules.modules.${system}.test_fullapi_rust.install;
# The ext contract (records, bytes at depth, typed maps, nested
# composites). The C++ cdylib backend could not express these types
# when the table was split out, so it ran single-provider and without a
# differential; logos-cpp-sdk#125 lifted that and both providers are
# wired below, so this table carries a provider differential like
# full_api. Its consumer axis is `testModulesExtQtProxyInstall` below.
testModulesExtInstall = logos-test-modules.modules.${system}.test_fullapi_ext_rust.install;
testModulesExtCppInstall = logos-test-modules.modules.${system}.test_fullapi_ext_cpp.install;
# The QT-TYPED consumer. `type: core` with no `interface` key selects
# apiStyle=qt, so this module's generated wrappers are the Qt ones —
# the surface the two existing proxies cannot reach (universal forces
# apiStyle=lp, cdylib forces the Rust client). It forwards the whole
# contract, so the case table replays through it unchanged, twice:
# once per generated wrapper table (sync / async).
testModulesQtProxyInstall =
logos-test-modules.modules.${system}.test_fullapi_qtproxy.install;
# The ext table's Qt-typed consumer, and the reason it is a SECOND
# module rather than a second binding of the first: a Qt consumer
# wrapper is generated per CONTRACT, and full_api_ext is a different
# contract. It is also where the widened Qt mapping actually lives —
# records, QList<Blob>, QMap<QString, QList<QByteArray>>,
# QList<QList<qlonglong>>, std::optional<QString> — none of which
# full_api can spell, so none of which qtproxy-sync/async execute.
testModulesExtQtProxyInstall =
logos-test-modules.modules.${system}.test_fullapi_ext_qtproxy.install;
# Helper: run the integration suite once with the given
# `--transport` value. Same env wiring as the unit check
# plus openssl (needed by the `self_signed_cert` fixture
# for `tcp_ssl`; harmless for `local` / `tcp`).
mkIntegration = transport: pkgs.runCommand
"logoscore-py-integration-tests-${transport}" {
nativeBuildInputs = [ python logoscoreBin pkgs.openssl ]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.qt6.qtbase ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export QT_QPA_PLATFORM=offscreen
export QT_FORCE_STDERR_LOGGING=1
${pkgs.lib.optionalString pkgs.stdenv.isLinux ''
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.qtPluginPrefix}"
''}
export PYTHONPATH=$PWD/src
export LOGOSCORE_BIN=${logoscoreBin}/bin/logoscore
export LOGOSCORE_TEST_MODULES_DIR=${testModulesInstall}/modules
# Run from a writable HOME so any stray ~/.logoscore writes are isolated.
export HOME=$PWD/home
mkdir -p $HOME
${python}/bin/pytest tests/integration -v --transport=${transport}
touch $out
'';
# Helper: the same thing for the logosctl suite. A sibling rather
# than a `binary:`/`suite:` parameter on `mkIntegration`, for the
# reason the two test trees are duplicated in the first place — the
# two CLIs configure a daemon through different mechanisms, and
# retiring logoscore should be a delete, not an untangle. The two
# helpers drifting apart is expected, not a smell.
mkIntegrationLogosctl = transport: pkgs.runCommand
"logosctl-py-integration-tests-${transport}" {
nativeBuildInputs = [ python logosctlBin pkgs.openssl ]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.qt6.qtbase ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export QT_QPA_PLATFORM=offscreen
export QT_FORCE_STDERR_LOGGING=1
${pkgs.lib.optionalString pkgs.stdenv.isLinux ''
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.qtPluginPrefix}"
''}
export PYTHONPATH=$PWD/src
export LOGOSCTL_BIN=${logosctlBin}/bin/logosctl
export LOGOSCTL_TEST_MODULES_DIR=${testModulesInstall}/modules
# tests/logosctl/conftest.py SKIPS when either of those is unset,
# so a rename here turns the whole suite green-by-omission.
#
# Writable HOME: logosctl defaults to ~/.logosctl and refuses to
# start a second daemon in a config dir that already holds a live
# one. Every test drives an isolated --config-dir, but a stray
# default-session write must still land somewhere sandbox-local.
export HOME=$PWD/home
mkdir -p $HOME
${python}/bin/pytest tests/logosctl/integration -v --transport=${transport}
touch $out
'';
in
# `rec` so `conformance-matrix-merged` can name the two runs it is built
# from. It depends on them; it does not re-measure anything.
rec {
unit = pkgs.runCommand "logoscore-py-unit-tests" {
nativeBuildInputs = [ python ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export PYTHONPATH=$PWD/src
# The inline-vs-shared table guard resolves the shared table by
# sibling checkout, which does not exist in the sandbox — so without
# this it SKIPPED here and ran only on a developer's workspace. A
# drift guard that is green because it never ran is worse than no
# guard: pointed at the table it found two boundaries pinned inline
# and absent from cases.json.
export LOGOS_CONFORMANCE_DIR=${logos-test-modules}/conformance
${python}/bin/pytest tests/unit -v
touch $out
'';
# ── logosctl ────────────────────────────────────────────────────
# A parallel suite for the parallel client, in its own derivations
# so nix builds it concurrently with the logoscore ones and a red
# logosctl cannot mask a logoscore regression. Dropping logosctl
# later is deleting these four attributes, `mkIntegrationLogosctl`,
# and `logosctlBin`.
#
# Deliberately NOT duplicated: the conformance matrix. It measures
# the LIDL type contract, which lives in the runtime both binaries
# embed — replaying the whole matrix through a second CLI would
# double the longest job in the flake for no signal the first run
# does not already carry.
unit-logosctl = pkgs.runCommand "logosctl-py-unit-tests" {
nativeBuildInputs = [ python ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export PYTHONPATH=$PWD/src
# No LOGOSCTL_BIN: these tests drive a fake binary and assert on
# argv, env and the generated config documents. Nothing here
# spawns a daemon, which is why they run without the CLI closure.
${python}/bin/pytest tests/logosctl/unit -v
touch $out
'';
# The LIDL conformance matrix: every (type x position) in the
# `full_api` contract, replayed against BOTH providers and every
# CONSUMER surface, reported as per-cell coordinates. The case table
# and the xfail registry live in logos-test-modules/conformance/ (with
# the providers they describe); this repo owns the driver because it
# owns the client it uses.
#
# Three consumers, one run — they have to share a process for the
# consumer differential to exist at all:
# py this package's client, talking to the provider
# qtproxy-sync Qt-typed wrappers, sync table (_result.toT())
# qtproxy-async Qt-typed wrappers, async table (qvariant_cast<T>)
#
# Fails on: a red cell, an `xpass` (a registered known-broken cell
# that started passing — the registry has to be updated), a `skip`
# entry whose cell turns out to work, or a (type, position) the
# contract declares and no case covers.
#
# Three artifacts land in $out, and none of them is the verdict — the
# exit status is: matrix.txt (the terminal report, with the TYPE x
# POSITION grid and the differential), matrix.jsonl (one object per
# cell, unchanged), and matrix.html — a self-contained page with no
# external requests, publishable to Pages the way the doctest
# harness's report already is.
conformance-matrix = pkgs.runCommand "logoscore-py-conformance-matrix" {
nativeBuildInputs = [ python logoscoreBin pkgs.openssl ]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.qt6.qtbase ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export QT_QPA_PLATFORM=offscreen
export QT_FORCE_STDERR_LOGGING=1
${pkgs.lib.optionalString pkgs.stdenv.isLinux ''
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.qtPluginPrefix}"
''}
export PYTHONPATH=$PWD/src
export HOME=$PWD/home
mkdir -p $HOME $out
${python}/bin/python conformance/run_matrix.py \
--logoscore ${logoscoreBin}/bin/logoscore \
--cases ${logos-test-modules}/conformance/cases.json \
--known ${logos-test-modules}/conformance/known.json \
--contract ${logos-test-modules}/test-fullapi-proxy-module-rust/full_api.lidl \
--cpp-modules ${testModulesInstall}/modules \
--rust-modules ${testModulesRustInstall}/modules \
--proxy-consumer qtproxy-sync=test_fullapi_qtproxy=${testModulesQtProxyInstall}/modules=sync \
--proxy-consumer qtproxy-async=test_fullapi_qtproxy=${testModulesQtProxyInstall}/modules=async \
--jsonl $out/matrix.jsonl \
--report $out/matrix.html \
--md $out/known-broken.md \
--no-color \
2>&1 | tee $out/matrix.txt
'';
# The ext contract, with the SAME three-consumer shape the full_api
# gate has:
# py this package's client, talking to the provider
# extqtproxy-sync Qt-typed wrappers, sync table
# extqtproxy-async Qt-typed wrappers, async table
#
# The proxy is a DIFFERENT module from the full_api one because a Qt
# consumer wrapper is generated per contract. The probe method is
# given explicitly (fifth field of --proxy-consumer): the driver's
# default is `echoInt`, which full_api_ext does not have.
conformance-matrix-ext = pkgs.runCommand "logoscore-py-conformance-matrix-ext" {
nativeBuildInputs = [ python logoscoreBin pkgs.openssl ]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.qt6.qtbase ];
} ''
cp -r ${./.}/. .
chmod -R +w .
export QT_QPA_PLATFORM=offscreen
export QT_FORCE_STDERR_LOGGING=1
${pkgs.lib.optionalString pkgs.stdenv.isLinux ''
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/${pkgs.qt6.qtbase.qtPluginPrefix}"
''}
export PYTHONPATH=$PWD/src
export HOME=$PWD/home
mkdir -p $HOME $out
${python}/bin/python conformance/run_matrix.py \
--logoscore ${logoscoreBin}/bin/logoscore \
--cases ${logos-test-modules}/conformance/ext-cases.json \
--known ${logos-test-modules}/conformance/known-ext.json \
--contract ${logos-test-modules}/test-fullapi-ext-module-rust/rust-lib/test_fullapi_ext_rust.lidl \
--modules test_fullapi_ext_rust=${testModulesExtInstall}/modules \
--modules test_fullapi_ext_cpp=${testModulesExtCppInstall}/modules \
--proxy-consumer 'extqtproxy-sync=test_fullapi_ext_qtproxy=${testModulesExtQtProxyInstall}/modules=sync=echoStringMap:[{"k":"v"}]' \
--proxy-consumer 'extqtproxy-async=test_fullapi_ext_qtproxy=${testModulesExtQtProxyInstall}/modules=async=echoStringMap:[{"k":"v"}]' \
--jsonl $out/matrix-ext.jsonl \
--report $out/matrix-ext.html \
--no-color \
2>&1 | tee $out/matrix-ext.txt
'';
# ── the merged report ───────────────────────────────────────────
# ONE page, BOTH contracts, and nothing computed across them. A
# view-layer merge: it re-runs no measurement, it reads what the two
# runs above already wrote (each page carries its payload in
# `<script id="report-data">`) and lays them out as two labelled
# contracts on one page.
#
# A third DERIVATION rather than a third run, and the two gates stay
# separate on purpose: they measure different contracts against
# different providers, and one going red must not suppress the
# other's report. The consequence is that this derivation cannot
# build while either gate is red — which is right for a `check`, and
# exactly why CI does NOT get its landing page from here: CI merges
# the uploaded ARTIFACTS with the same script (see
# .github/workflows/conformance.yml), so the merged page still exists
# on the run where it is most worth reading.
#
# $out is laid out as the published site, so `nix build` produces
# something serveable as-is: index.html is the merged page, and
# full/ + ext/ are the standalone reports it links to.
conformance-matrix-merged =
pkgs.runCommand "logoscore-py-conformance-matrix-merged" {
nativeBuildInputs = [ python ];
} ''
mkdir -p $out/full $out/ext
cp ${conformance-matrix}/matrix.html $out/full/index.html
cp ${conformance-matrix}/matrix.jsonl $out/full/matrix.jsonl
cp ${conformance-matrix}/matrix.txt $out/full/matrix.txt
cp ${conformance-matrix}/known-broken.md $out/full/known-broken.md
cp ${conformance-matrix-ext}/matrix-ext.html $out/ext/index.html
cp ${conformance-matrix-ext}/matrix-ext.jsonl $out/ext/matrix.jsonl
cp ${conformance-matrix-ext}/matrix-ext.txt $out/ext/matrix.txt
chmod -R u+w $out
${python}/bin/python ${./conformance/matrix_report.py} \
--merge $out/full/index.html $out/ext/index.html \
--href ./full/ ./ext/ \
-o $out/index.html
'';
# One check per transport. CI's matrix fans them out; a local
# `nix flake check` runs all three sequentially.
integration-local = mkIntegration "local";
integration-tcp = mkIntegration "tcp";
integration-tcp_ssl = mkIntegration "tcp_ssl";
# Same three transports, driven through logosctl. Split the same way
# and for the same reasons.
integration-logosctl-local = mkIntegrationLogosctl "local";
integration-logosctl-tcp = mkIntegrationLogosctl "tcp";
integration-logosctl-tcp_ssl = mkIntegrationLogosctl "tcp_ssl";
# Back-compat alias — equivalent to `integration-local`. Kept
# so anyone with `nix build .#checks.<system>.integration` in
# muscle memory still gets a green path.
integration = mkIntegration "local";
}
);
};
}