From cfb650ebbcd2bfaa774b2f4f4f86a9f553b3bb90 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Tue, 20 Oct 2026 04:20:52 +0200 Subject: [PATCH 1/5] packaging: omx-contract 2.18.0 is the floor, the first with the vocabulary header (#115) The probe output's shared words (latency method, voice basis, core and clock bands, thread scheduling) come from omx-contract's omx_vocabulary.h, which 2.18.0 is the first release to install. --- debian/control | 2 +- packaging/plugin-hostd.spec | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 2ab66c0..89fce33 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: Pau Aliagas Build-Depends: debhelper-compat (= 13), libmod-host-protocol-dev, - libomx-contract-dev (>= 2.16.0~), + libomx-contract-dev (>= 2.18.0~), omx-clap-host-tests (>= 0.9.0~) , pkgconf, python3 diff --git a/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index 8b0b6a7..27f05df 100644 --- a/packaging/plugin-hostd.spec +++ b/packaging/plugin-hostd.spec @@ -14,8 +14,9 @@ BuildRequires: make BuildRequires: diffutils BuildRequires: python3 BuildRequires: pkgconfig(mod-host-protocol) -# the probe runner's default rates and the protocol's parameter scales are read from omx-contract's C header; a floor only -BuildRequires: omx-contract-devel >= 2.16.0 +# the probe runner's default rates, the protocol's parameter scales and the probe output's shared words are read from +# omx-contract's C headers (2.18.0 is the first with omx_vocabulary.h); a floor only +BuildRequires: omx-contract-devel >= 2.18.0 # a command of exactly the socket's buffer size, or several commands in one read, needs mod-audio/mod-host#113: # FreeMixer's build carrying it (11.1.fmx) or audinux's once it carries it (12); a floor only Requires: mod-host-protocol%{?_isa} >= 0.10.6.f14a230-11.1 From 4b9a58caad7c9618602794300631d04751094b36 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Tue, 20 Oct 2026 04:21:07 +0200 Subject: [PATCH 2/5] probe-output.schema: the words shared with omx-contract come from its vocabulary header (#115) The latency method, the voice basis, the core and clock bands and the realtime scheduling were lists typed in the template. Each is now an @OMX_@ token the generator replaces with the item's _INIT words from omx_vocabulary.h, in the contract's order, with a static check that the list holds as many words as the item's count. The generated schema keeps the same values, note-onset among the methods. The probe.* outcomes, steps, limits, reasons and profile codes stay this protocol's own. --- BUILDING.md | 17 ++++++++++------ protocol/probe-output.schema.in | 33 +++++-------------------------- protocol/probe-output.schema.json | 33 +++++-------------------------- tests/sabotage.py | 2 +- tools/protocol-gen.c | 29 +++++++++++++++++++++++---- 5 files changed, 47 insertions(+), 67 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index b003a84..f8c9bea 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -8,12 +8,17 @@ mod-host-protocol` when it is installed (mod-host's `make install-lib`), otherwise from `MOD_HOST_DIR`, a mod-host tree where the library is built if missing. The daemon needs libc and that library and nothing else. -The probe runner and the protocol generator also read omx-contract's C header, -``, from `pkg-config omx-contract` -(`omx-contract-devel`, `libomx-contract-dev`; `CONTRACT_CFLAGS=-I` for one -that is not installed). The runner's default rates are the contract's -`ORACLE_FLOOR_RATES`, and the generator refuses to build or run when the -protocol's parameter scales are not the contract's `PARAM_SCALES`. `make +The probe runner and the protocol generator also read omx-contract's C headers, +`` and ``, from +`pkg-config omx-contract` (`omx-contract-devel`, `libomx-contract-dev`, 2.18.0 or +later; `CONTRACT_CFLAGS=-I` for one that is not installed). The runner's +default rates are the contract's `ORACLE_FLOOR_RATES`, and the generator refuses +to build or run when the protocol's parameter scales are not the contract's +`PARAM_SCALES`. The probe output schema takes the words it shares with the +contract (`MEASUREMENT_LATENCY_METHODS`, `MEASUREMENT_VOICE_BASES`, +`MACHINE_CORE_BANDS`, `MACHINE_CLOCK_BANDS`, `RESOURCE_THREAD_SCHEDULING`) from +the vocabulary header: `protocol/probe-output.schema.in` names each as an +`@OMX_@` token and holds no list of its own. `make test-contract` checks both, against the installed contract and against a copy of its header with the values changed. Nothing is linked: the header holds values only. diff --git a/protocol/probe-output.schema.in b/protocol/probe-output.schema.in index 04e24b1..cda0fdb 100644 --- a/protocol/probe-output.schema.in +++ b/protocol/probe-output.schema.in @@ -233,23 +233,10 @@ "type": "string" }, "coreBand": { - "enum": [ - "1-2", - "3-4", - "5-8", - "9-16", - "17-32", - "33+" - ] + "enum": [@OMX_MACHINE_CORE_BANDS@] }, "clockBand": { - "enum": [ - "<1.5GHz", - "1.5-2.5GHz", - "2.5-3.5GHz", - "3.5-4.5GHz", - "4.5GHz+" - ] + "enum": [@OMX_MACHINE_CLOCK_BANDS@] }, "preemptRt": { "type": "boolean" @@ -399,10 +386,7 @@ "type": "object" }, "realtime": { - "enum": [ - "none", - "fifo" - ] + "enum": [@OMX_RESOURCE_THREAD_SCHEDULING@] } } }, @@ -743,11 +727,7 @@ "minimum": 0 }, "method": { - "enum": [ - "impulse-onset", - "declared-port", - "note-onset" - ] + "enum": [@OMX_MEASUREMENT_LATENCY_METHODS@] }, "heard": { "description": "A note-onset latency's: whether the note sounded, so an onset that never came is not read as one.", @@ -840,10 +820,7 @@ "minimum": 1 }, "basis": { - "enum": [ - "declared", - "probe" - ] + "enum": [@OMX_MEASUREMENT_VOICE_BASES@] }, "capacity": { "type": "integer", diff --git a/protocol/probe-output.schema.json b/protocol/probe-output.schema.json index 641710e..044d35d 100644 --- a/protocol/probe-output.schema.json +++ b/protocol/probe-output.schema.json @@ -233,23 +233,10 @@ "type": "string" }, "coreBand": { - "enum": [ - "1-2", - "3-4", - "5-8", - "9-16", - "17-32", - "33+" - ] + "enum": ["1-2", "3-4", "5-8", "9-16", "17-32", "33+"] }, "clockBand": { - "enum": [ - "<1.5GHz", - "1.5-2.5GHz", - "2.5-3.5GHz", - "3.5-4.5GHz", - "4.5GHz+" - ] + "enum": ["<1.5GHz", "1.5-2.5GHz", "2.5-3.5GHz", "3.5-4.5GHz", "4.5GHz+"] }, "preemptRt": { "type": "boolean" @@ -399,10 +386,7 @@ "type": "object" }, "realtime": { - "enum": [ - "none", - "fifo" - ] + "enum": ["none", "fifo"] } } }, @@ -743,11 +727,7 @@ "minimum": 0 }, "method": { - "enum": [ - "impulse-onset", - "declared-port", - "note-onset" - ] + "enum": ["impulse-onset", "declared-port", "note-onset"] }, "heard": { "description": "A note-onset latency's: whether the note sounded, so an onset that never came is not read as one.", @@ -840,10 +820,7 @@ "minimum": 1 }, "basis": { - "enum": [ - "declared", - "probe" - ] + "enum": ["declared", "probe"] }, "capacity": { "type": "integer", diff --git a/tests/sabotage.py b/tests/sabotage.py index 3741ad5..5e3ea99 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -501,7 +501,7 @@ "a_latency_taken_from_a_note_says_so_and_whether_the_note_was_heard", "probe"), ("the probe schema has no notes held in a cost", "protocol/probe-output.schema.json", ' "voices": {\n', ' "voices-": {\n', "instrument-notes validates", "probeschema"), - ("the probe schema has no note-onset latency", "protocol/probe-output.schema.json", ' "note-onset"\n', ' "note-guess"\n', + ("the probe schema has no note-onset latency", "protocol/probe-output.schema.json", '"declared-port", "note-onset"]', '"declared-port", "note-guess"]', "instrument-notes validates", "probeschema"), ("the probe schema accepts a verdict", "protocol/probe-output.schema.json", ' "verdict"\n', ' "verdict-"\n', "success-msi validates", "probeschema"), diff --git a/tools/protocol-gen.c b/tools/protocol-gen.c index 5a49e74..bdb8f8b 100644 --- a/tools/protocol-gen.c +++ b/tools/protocol-gen.c @@ -22,7 +22,8 @@ * * protocol-gen json protocol/plugin-hostd.json on stdout * protocol-gen regions the name of every region, one per line - * protocol-gen probe-schema TEMPLATE TEMPLATE on stdout with each @PHD_PROBE_...@ token replaced by that list of the header + * protocol-gen probe-schema TEMPLATE TEMPLATE on stdout with each @PHD_PROBE_...@ token replaced by that list of the header, + * and each @OMX_@ token by the words of omx-contract's (omx_vocabulary.h) * protocol-gen splice FILE FILE on stdout with every generated region replaced; a region is the lines between * "BEGIN GENERATED protocol:" and "END GENERATED protocol:" */ @@ -32,6 +33,7 @@ #include #include +#include #include /* The parameter scales are omx-contract's PARAM_SCALES, whose words the contract declares for this protocol: the same @@ -595,10 +597,24 @@ static const char *const PROBE_STEPS[] = { #undef X }; +/* the words of the omx-contract items the probe output shares with the other readers of a measurement: the contract + * declares them once, and the schema takes them from it, in its order */ +#define VOCABULARY(item) \ + static const char *const VOCAB_##item[] = OMX_##item##_INIT; \ + _Static_assert(sizeof(VOCAB_##item) / sizeof(*VOCAB_##item) == OMX_##item##_COUNT, "omx-contract's " #item ": not as many words as its count"); +VOCABULARY(MEASUREMENT_LATENCY_METHODS) +VOCABULARY(MEASUREMENT_VOICE_BASES) +VOCABULARY(MACHINE_CORE_BANDS) +VOCABULARY(MACHINE_CLOCK_BANDS) +VOCABULARY(RESOURCE_THREAD_SCHEDULING) +#undef VOCABULARY + #define PROBE_LIST(token, list) { "@" token "@", list, sizeof(list) / sizeof(*list) } +#define VOCAB_LIST(item) PROBE_LIST("OMX_" #item, VOCAB_##item) -/* TEMPLATE on stdout with each @PHD_PROBE_...@ token replaced by the quoted codes of that list of the header, so the - * schema's enumerations are the header's own; a token the template holds and the generator does not know is an error */ +/* TEMPLATE on stdout with each @PHD_PROBE_...@ token replaced by the quoted codes of that list of the header and each + * @OMX_@ token by the quoted words of that omx-contract item, so the schema's enumerations are the headers' own; + * a token the template holds and the generator does not know is an error */ static int probe_schema(const char *path) { static const struct @@ -614,6 +630,11 @@ static int probe_schema(const char *path) PROBE_LIST("PHD_PROBE_PROFILE_CODES", PROBE_PROFILE_CODES), PROBE_LIST("PHD_PROBE_STEPS", PROBE_STEPS), PROBE_LIST("PHD_PROBE_REASONS", PROBE_REASONS), + VOCAB_LIST(MEASUREMENT_LATENCY_METHODS), + VOCAB_LIST(MEASUREMENT_VOICE_BASES), + VOCAB_LIST(MACHINE_CORE_BANDS), + VOCAB_LIST(MACHINE_CLOCK_BANDS), + VOCAB_LIST(RESOURCE_THREAD_SCHEDULING), }; FILE *f = fopen(path, "r"); char line[512]; @@ -650,7 +671,7 @@ static int probe_schema(const char *path) } if (n == sizeof(LISTS) / sizeof(*LISTS)) { - fprintf(stderr, "%s: no list of the header is called %s", path, at); + fprintf(stderr, "%s: no list of the headers is called %s", path, at); fclose(f); return 1; } From e65d189a947a9f38200bd2f465d0abba9c1d13d3 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Tue, 20 Oct 2026 04:22:35 +0200 Subject: [PATCH 3/5] test-vocabulary: the probe schema's shared enums are the contract's, word for word (#115) tests/vocabulary_derived.py reads the installed omx-vocabulary.schema.json and holds every enum the template takes by an @OMX_@ token to $defs..enum, same words, same order; requires the five items of #115 to be taken that way; refuses a template list that is a whole contract item typed by hand; and checks note-onset is a latency method. A positive control tells a contract with a word more and one reordered apart. make sabotage gains five breaks, each seen red: a method dropped from the generated schema, the clock bands reordered, a scheduling class added to a copy of the contract, the methods typed by hand in the template, and the voice basis taken from nowhere. --- BUILDING.md | 4 +- Makefile | 7 ++- tests/sabotage.py | 30 ++++++++- tests/vocabulary_derived.py | 122 ++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 tests/vocabulary_derived.py diff --git a/BUILDING.md b/BUILDING.md index f8c9bea..b07c4b2 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -18,7 +18,9 @@ to build or run when the protocol's parameter scales are not the contract's contract (`MEASUREMENT_LATENCY_METHODS`, `MEASUREMENT_VOICE_BASES`, `MACHINE_CORE_BANDS`, `MACHINE_CLOCK_BANDS`, `RESOURCE_THREAD_SCHEDULING`) from the vocabulary header: `protocol/probe-output.schema.in` names each as an -`@OMX_@` token and holds no list of its own. `make +`@OMX_@` token and holds no list of its own. `make test-vocabulary` holds +each of those enums of the generated schema to the installed contract's +`omx-vocabulary.schema.json`, word for word and in order. `make test-contract` checks both, against the installed contract and against a copy of its header with the values changed. Nothing is linked: the header holds values only. diff --git a/Makefile b/Makefile index 3b73daf..5d10cd6 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ clean: # the daemon against workers that are not plugin hosts at all (tests/fake-host speaks the same protocol and can be # made to die on cue): placement, forwarding, ledger, replay, attribution, the storm bound; no jack, no plugin -test: test-pin test-relay test-alloc test-format test-daemon check-generated check-schema test-probe-schema test-probe test-contract test-consumer test-perturbation check-readme check-own-host-page check-no-pins check-ci-guards test-mod-host-json test-socket +test: test-pin test-relay test-alloc test-format test-daemon check-generated check-schema test-probe-schema test-vocabulary test-probe test-contract test-consumer test-perturbation check-readme check-own-host-page check-no-pins check-ci-guards test-mod-host-json test-socket MOD_HOST_DIR=$(MOD_HOST_DIR) python3 tests/verbs_contract.py # the same without the verb table, which is read from a mod-host checkout's README @@ -292,6 +292,11 @@ test-mod-host-json: check-schema: python3 tests/protocol_schema.py protocol/plugin-hostd.json protocol/plugin-hostd.schema.json +# the probe output's words shared with omx-contract are the contract's: each enum the template takes from it matches the +# installed omx-vocabulary.schema.json, and the template types none of the contract's lists by hand +test-vocabulary: + python3 tests/vocabulary_derived.py + # the probe runner over the stress plugin: crash, hang and a process left behind each give the named outcome in a document # that validates test-probe: $(PROBE) tests/probe-child tests/stress.clap tests/lv2-probe-child tests/rt_interpose.so $(LV2_FIXTURE) diff --git a/tests/sabotage.py b/tests/sabotage.py index 5e3ea99..bed9d30 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -13,7 +13,9 @@ # an LV2 bundle (MOD_HOST, LV2_DIR, LV2_URI, LV2_BUNDLE, LV2_PARAM) and is skipped without them; "pin" breaks # include/plugin-hostd/pin.h and runs tests/pin_test.c; "readme" breaks README.md or BUILDING.md and runs # tests/readme_build.py, with no daemon to build; "probeschema" breaks protocol/probe-output.schema.json and runs -# tests/probe_schema.py; "relay" breaks src/relay.c and runs tests/relay_test.c; "alloc" breaks src/relay.c or src/lv2.c and runs make test-alloc; "contract" breaks the probe runner or the generator and runs tests/contract_derived.py; "consumer" breaks the header and compiles tests/consumer.c against it; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py; "ciguards" breaks a workflow and runs tests/ci_guards.py +# tests/probe_schema.py; "relay" breaks src/relay.c and runs tests/relay_test.c; "alloc" breaks src/relay.c or src/lv2.c and runs make test-alloc; "contract" breaks the probe runner or the generator and runs tests/contract_derived.py; "consumer" breaks the header and compiles tests/consumer.c against it; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py; "ciguards" breaks a workflow and runs tests/ci_guards.py; +# "vocabulary" breaks the probe schema, its template or a copy of omx-contract's omx-vocabulary.schema.json and runs +# tests/vocabulary_derived.py SABOTAGE = [ ("a runner that refuses unshare passes build-and-test with a warning", ".github/workflows/ci.yml", ' echo "::error::test-jack did not run: this runner refuses a private user and pid namespace with its own /proc"\n exit 1\n', @@ -514,6 +516,21 @@ "success-msi validates", "probeschema"), ("the probe schema takes a percentile of 50", "protocol/probe-output.schema.json", '"exclusiveMinimum": 50', '"exclusiveMinimum": 0', "success-msi validates", "probeschema"), + ("the generated schema lost a latency method the contract has", "protocol/probe-output.schema.json", + '"declared-port", "note-onset"]', '"declared-port"]', + "MEASUREMENT_LATENCY_METHODS at /$defs/point/properties/latency/properties/method is the contract's", "vocabulary"), + ("the generated schema has the clock bands in an order of its own", "protocol/probe-output.schema.json", + '"3.5-4.5GHz", "4.5GHz+"]', '"4.5GHz+", "3.5-4.5GHz"]', + "MACHINE_CLOCK_BANDS at /properties/machine/properties/class/properties/clockBand is the contract's", "vocabulary"), + ("the contract gives its hosts a scheduling class the schema does not have", "omx-vocabulary.schema.json", + ' "none",\n "fifo"\n', ' "none",\n "fifo",\n "rr"\n', + "RESOURCE_THREAD_SCHEDULING at /properties/offer/properties/realtime is the contract's", "vocabulary"), + ("the template types the latency methods by hand again", "protocol/probe-output.schema.in", + '"enum": [@OMX_MEASUREMENT_LATENCY_METHODS@]', '"enum": ["impulse-onset", "declared-port", "note-onset"]', + "the template types no list of the contract's by hand", "vocabulary"), + ("the template takes the voice basis from nowhere", "protocol/probe-output.schema.in", + '"enum": [@OMX_MEASUREMENT_VOICE_BASES@]', '"type": "string"', + "the template takes MEASUREMENT_VOICE_BASES from the contract", "vocabulary"), ] @@ -608,6 +625,10 @@ def run_test(tree, name, jack=False): p = subprocess.run([sys.executable, os.path.join(ROOT, "tests", "probe_schema.py"), tree], capture_output=True, text=True, timeout=60) return p.returncode, p.stdout + if jack == "vocabulary": + p = subprocess.run([sys.executable, os.path.join(ROOT, "tests", "vocabulary_derived.py"), tree], capture_output=True, + text=True, timeout=60, env=dict(os.environ, OMX_VOCABULARY=os.path.join(tree, "omx-vocabulary.schema.json"))) + return p.returncode, p.stdout if jack in ("nopins", "ciguards"): script = {"nopins": "no_pins.py", "ciguards": "ci_guards.py"}[jack] p = subprocess.run([sys.executable, os.path.join(ROOT, "tests", script), tree], capture_output=True, @@ -658,6 +679,11 @@ def run_test(tree, name, jack=False): elif jack == "probeschema": os.makedirs(os.path.join(tree, "protocol")) shutil.copy(os.path.join(ROOT, "protocol", "probe-output.schema.json"), os.path.join(tree, "protocol")) + elif jack == "vocabulary": + shutil.copytree(os.path.join(ROOT, "protocol"), os.path.join(tree, "protocol")) + datadir = subprocess.run(["pkg-config", "--variable=datadir", "omx-contract"], capture_output=True, text=True, + check=True).stdout.strip() + shutil.copy(os.path.join(datadir, "omx-vocabulary.schema.json"), tree) elif jack == "contract": for d in ("src", "include", "tools", "protocol"): shutil.copytree(os.path.join(ROOT, d), os.path.join(tree, d)) @@ -693,7 +719,7 @@ def run_test(tree, name, jack=False): open(os.path.join(tree, path), "w").write(src.replace(text, repl)) if jack == "probe": build_probe(tree) - elif jack not in ("readme", "nopins", "ciguards", "format", "relay", "contract", "consumer", "probeschema", "alloc"): + elif jack not in ("readme", "nopins", "ciguards", "format", "relay", "contract", "consumer", "probeschema", "vocabulary", "alloc"): for f in os.listdir(os.path.join(tree, "src")): if f.endswith(".o"): os.unlink(os.path.join(tree, "src", f)) diff --git a/tests/vocabulary_derived.py b/tests/vocabulary_derived.py new file mode 100644 index 0000000..a03b9fb --- /dev/null +++ b/tests/vocabulary_derived.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# The words the probe output shares with omx-contract are declared once, in the contract: protocol/probe-output.schema.in +# names each as an @OMX_@ token, the generator writes the item's words from omx_vocabulary.h, and every enum the +# generated schema holds at a token's place must be the contract's omx-vocabulary.schema.json $defs..enum, the +# same words in the same order. A list of the template that is the whole of a contract item, typed by hand, is refused +# even when its words are right today: it is the second copy that drifts. +# vocabulary_derived.py [tree] tree holds protocol/ (default: this checkout); the contract's schema is +# $OMX_VOCABULARY, or omx-vocabulary.schema.json in pkg-config's datadir of omx-contract +import copy, json, os, re, subprocess, sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TREE = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else ROOT +PKG_CONFIG = os.environ.get("PKG_CONFIG", "pkg-config") +TOKEN = re.compile(r"@((?:OMX|PHD)_[A-Z0-9_]+)@") +# the items the probe output must take from the contract (#115); any other @OMX_@ the template names is held to +# the contract all the same +REQUIRED = ("MEASUREMENT_LATENCY_METHODS", "MEASUREMENT_VOICE_BASES", "MACHINE_CORE_BANDS", "MACHINE_CLOCK_BANDS", + "RESOURCE_THREAD_SCHEDULING") +failed = 0 + + +def check(cond, what): + global failed + print(("ok " if cond else "FAIL ") + what) + if not cond: + failed += 1 + + +def vocabulary_path(): + if os.environ.get("OMX_VOCABULARY"): + return os.environ["OMX_VOCABULARY"] + datadir = subprocess.run([PKG_CONFIG, "--variable=datadir", "omx-contract"], capture_output=True, text=True, + check=True).stdout.strip() + path = os.path.join(datadir, "omx-vocabulary.schema.json") + if not os.path.exists(path): + sys.exit("omx-contract's %s is not installed (omx-contract-devel, libomx-contract-dev >= 2.18.0)" % path) + return path + + +def enums(node, pointer=""): + """(JSON pointer, enum list) of every enum under node""" + if isinstance(node, dict): + if isinstance(node.get("enum"), list): + yield pointer, node["enum"] + for k, v in node.items(): + yield from enums(v, pointer + "/" + k.replace("~", "~0").replace("/", "~1")) + elif isinstance(node, list): + for i, v in enumerate(node): + yield from enums(v, "%s/%d" % (pointer, i)) + + +def at(document, pointer): + for part in pointer.split("/")[1:]: + part = part.replace("~1", "/").replace("~0", "~") + document = document[int(part)] if isinstance(document, list) else document[part] + return document + + +def template(path): + """the template as JSON, each token a string of its own""" + return json.loads(TOKEN.sub(lambda m: '"@%s@"' % m.group(1), open(path).read())) + + +def derived(tmpl): + """{pointer: item} of every enum of the template that is one @OMX_@ token""" + out = {} + for pointer, values in enums(tmpl): + m = TOKEN.fullmatch(values[0]) if len(values) == 1 and isinstance(values[0], str) else None + if m and m.group(1).startswith("OMX_"): + out[pointer] = m.group(1)[len("OMX_"):] + return out + + +def differences(generated, places, core): + """what the generated schema holds at each token's place that is not the contract's item""" + out = [] + for pointer, item in sorted(places.items()): + want = core.get(item, {}).get("enum") + try: + have = at(generated, pointer)["enum"] + except (KeyError, IndexError, TypeError): + have = None + if want is None or have != want: + out.append((pointer, item, have, want)) + return out + + +def main(): + core = json.load(open(vocabulary_path()))["$defs"] + tmpl = template(os.path.join(TREE, "protocol", "probe-output.schema.in")) + generated = json.load(open(os.path.join(TREE, "protocol", "probe-output.schema.json"))) + places = derived(tmpl) + + for item in REQUIRED: + check(item in places.values(), "the template takes %s from the contract" % item) + + hand = [(p, k) for p, values in enums(tmpl) if all(isinstance(v, str) and not TOKEN.fullmatch(v) for v in values) + for k, d in core.items() if isinstance(d.get("enum"), list) and sorted(set(values)) == sorted(set(d["enum"]))] + check(not hand, "the template types no list of the contract's by hand%s" % + "".join("; %s is %s" % h for h in hand)) + + found = {(p, i): (h, w) for p, i, h, w in differences(generated, places, core)} + for pointer, item in sorted(places.items()): + check((pointer, item) not in found, "%s at %s is the contract's%s" % (item, pointer, "" if (pointer, item) not in found + else ": the schema has %s, the contract %s" % found[(pointer, item)])) + + methods = [at(generated, p)["enum"] for p, i in places.items() if i == "MEASUREMENT_LATENCY_METHODS"] + check(any("note-onset" in m for m in methods), "the latency methods include note-onset: %s" % methods) + + # positive control: a contract whose item has one word more, or its words in another order, is told apart + moved = copy.deepcopy(core) + moved["MEASUREMENT_LATENCY_METHODS"]["enum"] = moved["MEASUREMENT_LATENCY_METHODS"]["enum"] + ["x-onset"] + moved["MACHINE_CLOCK_BANDS"]["enum"] = list(reversed(moved["MACHINE_CLOCK_BANDS"]["enum"])) + told = {i for _, i, _, _ in differences(generated, places, moved)} + check(told == {"MEASUREMENT_LATENCY_METHODS", "MACHINE_CLOCK_BANDS"}, + "positive control: a contract with a word more and one in another order differs there only: %s" % sorted(told)) + + print("vocabulary: %s" % ("FAILED (%d)" % failed if failed else "ok")) + return 1 if failed else 0 + + +sys.exit(main()) From 6e41e748c9439f804070cedfcc5b45019a5b84f0 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Tue, 20 Oct 2026 04:35:23 +0200 Subject: [PATCH 4/5] probe: write the bands, the latency method and the scheduling class by omx-contract's names (#115) The runner typed the core and clock bands, impulse-onset and note-onset, and "none" as literals of its own; it now writes the words of omx_vocabulary.h (MACHINE_CORE_BANDS, MACHINE_CLOCK_BANDS, MEASUREMENT_LATENCY_METHODS, RESOURCE_THREAD_SCHEDULING). The documents it writes are unchanged. --- src/probe.c | 15 +++++++++++---- tests/sabotage.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/probe.c b/src/probe.c index ab7e6ed..e267ad1 100644 --- a/src/probe.c +++ b/src/probe.c @@ -67,6 +67,7 @@ #include #include +#include #include #include @@ -731,8 +732,13 @@ static void put_machine(sb_t *b, const cpu_set_t *pinned, int pinned_by_option) khz = atol(text); if (read_text("/sys/kernel/realtime", text, sizeof(text)) == 0) preempt_rt = atoi(text) == 1; - core_band = ncpu <= 2 ? "1-2" : ncpu <= 4 ? "3-4" : ncpu <= 8 ? "5-8" : ncpu <= 16 ? "9-16" : ncpu <= 32 ? "17-32" : "33+"; - clock_band = khz < 1500000 ? "<1.5GHz" : khz < 2500000 ? "1.5-2.5GHz" : khz < 3500000 ? "2.5-3.5GHz" : khz < 4500000 ? "3.5-4.5GHz" : "4.5GHz+"; + /* the bands are omx-contract's MACHINE_CORE_BANDS and MACHINE_CLOCK_BANDS */ + core_band = ncpu <= 2 ? OMX_MACHINE_CORE_BANDS_CORES1_TO2 : ncpu <= 4 ? OMX_MACHINE_CORE_BANDS_CORES3_TO4 : + ncpu <= 8 ? OMX_MACHINE_CORE_BANDS_CORES5_TO8 : ncpu <= 16 ? OMX_MACHINE_CORE_BANDS_CORES9_TO16 : + ncpu <= 32 ? OMX_MACHINE_CORE_BANDS_CORES17_TO32 : OMX_MACHINE_CORE_BANDS_CORES33_UP; + clock_band = khz < 1500000 ? OMX_MACHINE_CLOCK_BANDS_UNDER1500_MHZ : khz < 2500000 ? OMX_MACHINE_CLOCK_BANDS_FROM1500_TO2500_MHZ : + khz < 3500000 ? OMX_MACHINE_CLOCK_BANDS_FROM2500_TO3500_MHZ : + khz < 4500000 ? OMX_MACHINE_CLOCK_BANDS_FROM3500_TO4500_MHZ : OMX_MACHINE_CLOCK_BANDS_FROM4500_MHZ; for (i = 0; i < g_nidle; i++) { if (g_idle_after[i].latency_us > DEEP_IDLE_US && !g_idle_after[i].disabled) @@ -2071,7 +2077,7 @@ int main(int argc, char **argv) } sb_printf(&doc, "]"); } - sb_printf(&doc, ", \"realtime\": \"none\"},\n \"limits\": {\"set\": "); + sb_printf(&doc, ", \"realtime\": \"" OMX_RESOURCE_THREAD_SCHEDULING_NONE "\"},\n \"limits\": {\"set\": "); sb_str(&doc, o.set); sb_printf(&doc, ",\n \"applied\": ["); for (i = 0; i < g_napplied; i++) @@ -2115,7 +2121,8 @@ int main(int argc, char **argv) from_note = jstr(latency->kid[k], "from") && !strcmp(jstr(latency->kid[k], "from"), "note"); sb_printf(&doc, "%s\n {\"rate\": %ld, \"quantum\": %u, \"outcome\": \"%s\", \"latency\": {\"declaredFrames\": %.0f, " "\"measuredFrames\": %.0f, \"method\": \"%s\"", - k ? "," : "", atol(latency->key[k]), o.quantum, OUT_OK, decl, meas, from_note ? "note-onset" : "impulse-onset"); + k ? "," : "", atol(latency->key[k]), o.quantum, OUT_OK, decl, meas, + from_note ? OMX_MEASUREMENT_LATENCY_METHODS_NOTE_ONSET : OMX_MEASUREMENT_LATENCY_METHODS_IMPULSE_ONSET); if (from_note) sb_printf(&doc, ", \"heard\": %s", jbool(latency->kid[k], "heard", 1) ? "true" : "false"); sb_printf(&doc, "}"); diff --git a/tests/sabotage.py b/tests/sabotage.py index bed9d30..71aa6bd 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -497,7 +497,8 @@ "an_lv2_point_carries_the_note_sweep_and_what_run_did_while_the_notes_played", "probe"), ("a note sweep leaves out its allocations", "src/probe.c", ' put_count(b, ns, "allocationsInRun", "allocations");\n', "", "a_clap_point_carries_the_note_sweep_counts", "probe"), - ("a latency from a note is called an impulse's", "src/probe.c", 'from_note ? "note-onset" : "impulse-onset"', '"impulse-onset"', + ("a latency from a note is called an impulse's", "src/probe.c", 'from_note ? OMX_MEASUREMENT_LATENCY_METHODS_NOTE_ONSET : OMX_MEASUREMENT_LATENCY_METHODS_IMPULSE_ONSET', + 'OMX_MEASUREMENT_LATENCY_METHODS_IMPULSE_ONSET', "a_latency_taken_from_a_note_says_so_and_whether_the_note_was_heard", "probe"), ("a note that did not sound is heard", "src/probe.c", 'jbool(latency->kid[k], "heard", 1) ? "true" : "false"', '"true"', "a_latency_taken_from_a_note_says_so_and_whether_the_note_was_heard", "probe"), From 91aeb1892357c950d44e80df54f009841d1be57b Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Tue, 20 Oct 2026 04:45:00 +0200 Subject: [PATCH 5/5] test-contract: the generator follows a scratch vocabulary header (#115) A copy of omx_vocabulary.h with a latency method more moves the probe schema the generator writes, and the same word list without its count does not build. Two sabotages, each seen red: the generator types the methods itself, and it drops the check of a list against its count. --- Makefile | 4 ++-- tests/contract_derived.py | 43 ++++++++++++++++++++++++++++++++++----- tests/sabotage.py | 6 ++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 5d10cd6..92a0386 100644 --- a/Makefile +++ b/Makefile @@ -312,8 +312,8 @@ tests/rt_interpose.so: tests/rt_interpose_stub.c tests/probe-child: tests/probe_child.c $(CC) $(CLAP_CFLAGS) $(CFLAGS) -Wall -Wextra -Werror -o $@ $< -ldl -# what is omx-contract's is read from it: the runner's default rates and the protocol's parameter scales follow a -# scratch copy of its header with other values +# what is omx-contract's is read from it: the runner's default rates, the protocol's parameter scales and the probe +# schema's latency methods follow a scratch copy of its headers with other values test-contract: $(PROBE) PROBE=./$(PROBE) python3 tests/contract_derived.py diff --git a/tests/contract_derived.py b/tests/contract_derived.py index 5bdc550..4c59b73 100644 --- a/tests/contract_derived.py +++ b/tests/contract_derived.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # What plugin-hostd shares with omx-contract is read from omx-contract, never typed again here: the probe runner's -# default rates are ORACLE_FLOOR_RATES, and the parameter scales of the protocol are PARAM_SCALES, the same words in -# the same order. Checked twice: against the installed contract (its data and its C header), and against a scratch +# default rates are ORACLE_FLOOR_RATES, the parameter scales of the protocol are PARAM_SCALES, the same words in +# the same order, and the probe output's shared enums are the words of omx_vocabulary.h. Checked twice: against the installed contract (its data and its C header), and against a scratch # copy of the header with the values changed, where the runner must follow and the generator must refuse to build. # Usage: contract_derived.py (run from the tree; CC, PKG_CONFIG and PROBE from the environment, or their defaults). import json, os, re, shutil, subprocess, sys, tempfile @@ -37,13 +37,13 @@ def contract_data(name): raise SystemExit("omx-contract declares no %s" % name) -def header(): +def header(name="omx_contract_limits.h"): for flag in pkg("--cflags").split() + ["-I/usr/include"]: if flag.startswith("-I"): - path = os.path.join(flag[2:], "omxcontract", "omx_contract_limits.h") + path = os.path.join(flag[2:], "omxcontract", name) if os.path.exists(path): return path - raise SystemExit("omx-contract's C header is not installed") + raise SystemExit("omx-contract's C header %s is not installed" % name) def child_rates(probe, tmp): @@ -84,6 +84,15 @@ def gen_json(gen): return p.returncode, p.stdout, p.stderr +def gen_methods(gen): + """the latency methods of the probe schema the generator writes from this tree's template""" + p = subprocess.run([gen, "probe-schema", os.path.join(ROOT, "protocol", "probe-output.schema.in")], capture_output=True, + text=True, timeout=30) + if p.returncode != 0: + return None + return json.loads(p.stdout)["$defs"]["point"]["properties"]["latency"]["properties"]["method"]["enum"] + + def main(): floor = ",".join(str(r) for r in contract_data("ORACLE_FLOOR_RATES")["values"]) scales = contract_data("PARAM_SCALES")["ids"] @@ -142,6 +151,30 @@ def main(): built = build_gen(scratch, os.path.join(tmp, "gen-swapped")) check(built.returncode != 0 and "PARAM_SCALES" in built.stderr, "the generator refuses scales the contract orders otherwise: rc %d %s" % (built.returncode, built.stderr[-300:])) + + # a scratch vocabulary whose latency methods have a word more: the probe schema the generator writes has it + # too, and a word list longer than its count does not build + vocab = os.path.join(tmp, "vocab") + os.makedirs(os.path.join(vocab, "omxcontract")) + shutil.copy(header(), os.path.join(vocab, "omxcontract")) + vtext = open(header("omx_vocabulary.h")).read() + grown = re.sub(r'(#define OMX_MEASUREMENT_LATENCY_METHODS_INIT \{[^}]*)\s\}', r'\1, "x-onset" }', vtext) + check(grown != vtext, "positive control: the scratch vocabulary's latency methods were changed") + counted = re.sub(r"(#define OMX_MEASUREMENT_LATENCY_METHODS_COUNT )([0-9]+)u", + lambda m: "%s%du" % (m.group(1), int(m.group(2)) + 1), grown) + with open(os.path.join(vocab, "omxcontract", "omx_vocabulary.h"), "w") as f: + f.write(counted) + built = build_gen(vocab, os.path.join(tmp, "gen-vocab")) + check(built.returncode == 0, "the generator builds against the scratch vocabulary: %s" % built.stderr[-400:]) + if built.returncode == 0: + methods = gen_methods(os.path.join(tmp, "gen-vocab")) + check(methods is not None and methods[-1:] == ["x-onset"], + "a word the contract adds to the latency methods is in the probe schema: %s" % methods) + with open(os.path.join(vocab, "omxcontract", "omx_vocabulary.h"), "w") as f: + f.write(grown) + built = build_gen(vocab, os.path.join(tmp, "gen-vocab-uncounted")) + check(built.returncode != 0 and "MEASUREMENT_LATENCY_METHODS" in built.stderr, + "the generator refuses a word list that is not its count: rc %d %s" % (built.returncode, built.stderr[-300:])) finally: shutil.rmtree(tmp, ignore_errors=True) print("contract: %s" % ("FAILED (%d)" % failures if failures else "ok")) diff --git a/tests/sabotage.py b/tests/sabotage.py index 71aa6bd..75cd966 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -297,6 +297,12 @@ "PHD_PARAM_SCALES(X)\n#undef X\n_Static_assert(", "#undef X\n_Static_assert(", "contract", "contract"), ("the generator takes a scale word that is not the contract's", "tools/protocol-gen.c", " if (!scales_are_the_contracts())\n return 1;\n", "", "contract", "contract"), + ("the generator types the latency methods by hand", "tools/protocol-gen.c", "VOCABULARY(MEASUREMENT_LATENCY_METHODS)\n", + 'static const char *const VOCAB_MEASUREMENT_LATENCY_METHODS[] = { "impulse-onset", "declared-port", "note-onset" };\n', + "contract", "contract"), + ("the generator does not hold a word list to its count", "tools/protocol-gen.c", + ' _Static_assert(sizeof(VOCAB_##item) / sizeof(*VOCAB_##item) == OMX_##item##_COUNT, "omx-contract\'s " #item ": not as many words as its count");\n', + "\n", "contract", "contract"), ("two errors share a code", "include/plugin-hostd/protocol.h", "#define PHD_ERR_NOTE_QUEUE_FULL (-517)", "#define PHD_ERR_NOTE_QUEUE_FULL (-516)", "consumer", "consumer"), ("a gesture the relay has no memory to hold takes the table with it", "src/relay.c",