From 2d11975c93093126de357b2694bac94cc92ec684 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Sun, 18 Oct 2026 12:01:41 +0200 Subject: [PATCH] Read ORACLE_FLOOR_RATES and PARAM_SCALES from omx-contract The probe runner's default rates, "44100,48000,96000,192000", were typed by hand in two places (the option default and the usage text). They are omx-contract's ORACLE_FLOOR_RATES. The runner now builds both from OMX_ORACLE_FLOOR_RATES_INIT in , and the man page names the contract list instead of repeating its values. The protocol's parameter scales (linear, log, stepped) are omx-contract's PARAM_SCALES, whose data says the words are for this protocol. They stay in protocol.h, because an installed header a consumer compiles against must not need omx-contract. protocol-gen, which writes them into the JSON and the README, now refuses to build when an id is not where the contract's enum has it or when the count differs. It also refuses to run when a word is not the contract's id in lower case. omx-contract-devel / libomx-contract-dev >= 2.16.0 is a build dependency (a floor only). Nothing is linked: the header holds values only. tests/contract_derived.py (make test-contract, part of make test) checks the scales against the installed contract's data. It checks the runner's default rates by what its child is given and by its usage text. It also builds against a copy of the header with other rates (the runner follows) and with the scales swapped (the generator does not build), and against a protocol.h with a renamed word (the generator writes nothing). Each of the three guards has a sabotage arm. Closes #75 --- BUILDING.md | 10 +++ Makefile | 15 +++- debian/control | 1 + doc/plugin-hostd-probe.1 | 3 +- packaging/plugin-hostd.spec | 2 + src/probe.c | 20 ++++- tests/contract_derived.py | 151 ++++++++++++++++++++++++++++++++++++ tests/sabotage.py | 28 ++++++- tools/protocol-gen.c | 37 +++++++++ 9 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 tests/contract_derived.py diff --git a/BUILDING.md b/BUILDING.md index 383f314..83746f0 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -8,6 +8,16 @@ 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 +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. + Building without omx-clap-core ------------------------------ diff --git a/Makefile b/Makefile index 4e4b67a..fb7b518 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,10 @@ endif # protocol's flags (mod-host-protocol.pc names /mod-host), or MOD_HOST_INCLUDE= on the command line MOD_HOST_INCLUDE ?= $(patsubst -I%,%,$(firstword $(filter -I%,$(PROTOCOL_CFLAGS)))) +# omx-contract's C header (omx-contract-devel, libomx-contract-dev): the probe runner's default rates and the protocol's +# parameter scales are read from it; CONTRACT_CFLAGS=-I for a header that is not installed +CONTRACT_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags omx-contract 2>/dev/null) + # CLAP headers for the test plugin: pkg-config when clap-devel is installed, CLAP_CFLAGS=-I otherwise CLAP_CFLAGS ?= $(shell $(PKG_CONFIG) --cflags clap 2>/dev/null) @@ -60,7 +64,7 @@ $(PROG): $(OBJ) $(PROTOCOL_LIB) # the probe runner: it loads no plugin and links nothing of the daemon or of mod-host $(PROBE): src/probe.c include/plugin-hostd/protocol.h include/plugin-hostd/pin.h - $(CC) -Iinclude $(CFLAGS) -D_GNU_SOURCE -DPROBE_VERSION='"$(VERSION)"' -o $@ src/probe.c + $(CC) -Iinclude $(CONTRACT_CFLAGS) $(CFLAGS) -D_GNU_SOURCE -DPROBE_VERSION='"$(VERSION)"' -o $@ src/probe.c ifneq ($(PROTOCOL_LIB),) $(PROTOCOL_LIB): @@ -126,7 +130,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-format test-daemon check-generated check-schema test-probe-schema test-probe test-consumer test-perturbation check-readme check-own-host-page check-no-pins test-mod-host-json test-socket +test: test-pin test-relay 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 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 @@ -227,7 +231,7 @@ GEN = tools/protocol-gen GENERATED = README.md doc/plugin-hostd.1 protocol/plugin-hostd.json protocol/mod-host.json protocol/probe-output.schema.json $(GEN): tools/protocol-gen.c include/plugin-hostd/protocol.h - $(CC) -Wall -Wextra -Werror -std=gnu99 -Iinclude -o $@ tools/protocol-gen.c + $(CC) -Wall -Wextra -Werror -std=gnu99 -Iinclude $(CONTRACT_CFLAGS) -o $@ tools/protocol-gen.c gen: $(GEN) $(GEN) json > protocol/plugin-hostd.json.new && mv protocol/plugin-hostd.json.new protocol/plugin-hostd.json @@ -283,6 +287,11 @@ 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 +test-contract: $(PROBE) + PROBE=./$(PROBE) python3 tests/contract_derived.py + # the probe output: its three fixtures validate and five broken copies of the first are refused test-probe-schema: python3 tests/probe_schema.py diff --git a/debian/control b/debian/control index fe9e0ea..3cd6bcc 100644 --- a/debian/control +++ b/debian/control @@ -4,6 +4,7 @@ Priority: optional Maintainer: Pau Aliagas Build-Depends: debhelper-compat (= 13), libmod-host-protocol-dev, + libomx-contract-dev (>= 2.16.0~), omx-clap-host-tests (>= 0.9.0~) , pkgconf, python3 diff --git a/doc/plugin-hostd-probe.1 b/doc/plugin-hostd-probe.1 index 32184eb..81ed2bf 100644 --- a/doc/plugin-hostd-probe.1 +++ b/doc/plugin-hostd-probe.1 @@ -29,7 +29,8 @@ Nothing a plugin prints can enter it. The program that loads the plugin. Default clap\-qualify, and /usr/libexec/plugin\-qualify/lv2_measure for an LV2. .TP .BI \-\-rates " R,R,..." -Sample rates to run at. Default 44100,48000,96000,192000. +Sample rates to run at. Default: omx\-contract's ORACLE_FLOOR_RATES, the rates every kernel oracle runs at, as the +runner's usage prints them. .TP .BI \-\-quantum " n" Block size in frames. Default 512. diff --git a/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index 08a51a0..d76ac08 100644 --- a/packaging/plugin-hostd.spec +++ b/packaging/plugin-hostd.spec @@ -14,6 +14,8 @@ 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 # 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 diff --git a/src/probe.c b/src/probe.c index 8045a09..3109884 100644 --- a/src/probe.c +++ b/src/probe.c @@ -66,6 +66,7 @@ #include #include +#include #include #include @@ -1516,6 +1517,19 @@ static void put_lv2_measure(sb_t *b, const jnode_t *m, long rate, unsigned quant sb_printf(b, "]}}"); } +/* the rates a run measures at when it is given none: omx-contract's ORACLE_FLOOR_RATES, every kernel oracle's floor */ +static const unsigned g_floor_rates[] = OMX_ORACLE_FLOOR_RATES_INIT; + +static const char *floor_rates(void) +{ + static char text[16 * sizeof(g_floor_rates) / sizeof(g_floor_rates[0])]; + size_t n, used = 0; + + for (n = 0; n < sizeof(g_floor_rates) / sizeof(g_floor_rates[0]) && used < sizeof(text); n++) + used += snprintf(text + used, sizeof(text) - used, "%s%u", n ? "," : "", g_floor_rates[n]); + return text; +} + static const char *base_name(const char *path) { const char *s = strrchr(path, '/'); @@ -1525,7 +1539,7 @@ static const char *base_name(const char *path) int main(int argc, char **argv) { - options_t o = { .child = "clap-qualify", .rates = "44100,48000,96000,192000", .quantum = 512, .reason = "on-request", + options_t o = { .child = "clap-qualify", .rates = floor_rates(), .quantum = 512, .reason = "on-request", .set = "default", .wall_s = 600, .cpu_s = 540, .files = 256, .data_bytes = 4LL << 30, .file_bytes = 64LL << 20, .grace_ms = 300 }; int a = 1, pin_option = 0, exit_code = 0, i, rc; @@ -1551,7 +1565,7 @@ int main(int argc, char **argv) fprintf(stderr, "usage: " RUNNER_NAME " run [options] []\n" " " RUNNER_NAME " run [options] lv2:\n" " --child PROGRAM the program that loads the plugin (default clap-qualify; " PROBE_LV2_CHILD " for lv2:)\n" - " --rates R,R,... sample rates (default 44100,48000,96000,192000)\n" + " --rates R,R,... sample rates (default %s)\n" " --quantum N block size in frames (default 512) --blocks N blocks per rate\n" " --offer EXT,... host extensions to offer on top of the child's own\n" " --out FILE the document, written atomically (default stdout)\n" @@ -1559,7 +1573,7 @@ int main(int argc, char **argv) " --reason WHY add, rescan, binary-changed, on-request, auto-measure, reference, ci\n" " --wall-s N --cpu-s N --data-bytes N --files N --file-bytes N --grace-ms N\n" " --cpus LIST pin the child to these cpus (\"0-3,8\")\n" - " --require LIMIT refuse to run unless the limit is applied (repeatable)\n"); + " --require LIMIT refuse to run unless the limit is applied (repeatable)\n", floor_rates()); return 2; } for (a = 2; a < argc; a++) diff --git a/tests/contract_derived.py b/tests/contract_derived.py new file mode 100644 index 0000000..5bdc550 --- /dev/null +++ b/tests/contract_derived.py @@ -0,0 +1,151 @@ +# 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 +# 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 + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +CC = os.environ.get("CC", "cc") +PKG_CONFIG = os.environ.get("PKG_CONFIG", "pkg-config") +PROBE = os.path.abspath(os.environ.get("PROBE", os.path.join(ROOT, "plugin-hostd-probe"))) +failures = 0 + + +def check(cond, what): + global failures + if not cond: + failures += 1 + print("FAIL contract: " + what) + + +def pkg(*args): + return subprocess.run([PKG_CONFIG] + list(args) + ["omx-contract"], capture_output=True, text=True, check=True).stdout.strip() + + +def contract_data(name): + """an item of omx-contract's own data, as the installed package lays it out (contract.d//omx-contract)""" + base = pkg("--variable=contractdir") + for major in sorted(os.listdir(base), reverse=True): + for f in sorted(os.listdir(os.path.join(base, major, "omx-contract"))): + if f.endswith(".json") and f != "MANIFEST.json": + with open(os.path.join(base, major, "omx-contract", f)) as fh: + data = json.load(fh) + if name in data: + return data[name] + raise SystemExit("omx-contract declares no %s" % name) + + +def header(): + for flag in pkg("--cflags").split() + ["-I/usr/include"]: + if flag.startswith("-I"): + path = os.path.join(flag[2:], "omxcontract", "omx_contract_limits.h") + if os.path.exists(path): + return path + raise SystemExit("omx-contract's C header is not installed") + + +def child_rates(probe, tmp): + """the --rates the runner hands its child when it is given none""" + seen = os.path.join(tmp, "argv") + child = os.path.join(tmp, "child.sh") + with open(child, "w") as f: + f.write('#!/bin/sh\nprintf "%%s\\n" "$@" > %s\n' % seen) + os.chmod(child, 0o755) + plugin = os.path.join(tmp, "x.clap") + open(plugin, "w").close() + subprocess.run([probe, "run", "--child", child, "--wall-s", "5", "--out", os.path.join(tmp, "doc.json"), plugin], + capture_output=True, text=True, timeout=60) + args = open(seen).read().split("\n") if os.path.exists(seen) else [] + return args[args.index("--rates") + 1] if "--rates" in args else None + + +def help_rates(probe): + p = subprocess.run([probe], capture_output=True, text=True, timeout=10) + m = re.search(r"--rates R,R,\.\.\.\s+sample rates \(default ([0-9,]+)\)", p.stderr) + return m.group(1) if m else None + + +def build_probe(include_first, out): + cmd = [CC, "-I" + include_first, "-I" + os.path.join(ROOT, "include")] + pkg("--cflags").split() + \ + ["-O2", "-std=gnu99", "-D_GNU_SOURCE", "-DPROBE_VERSION=\"0\"", "-o", out, os.path.join(ROOT, "src", "probe.c")] + return subprocess.run(cmd, capture_output=True, text=True) + + +def build_gen(include_first, out): + cmd = [CC, "-I" + include_first, "-I" + os.path.join(ROOT, "include")] + pkg("--cflags").split() + \ + ["-std=gnu99", "-o", out, os.path.join(ROOT, "tools", "protocol-gen.c")] + return subprocess.run(cmd, capture_output=True, text=True) + + +def gen_json(gen): + p = subprocess.run([gen, "json"], capture_output=True, text=True, timeout=30) + return p.returncode, p.stdout, p.stderr + + +def main(): + floor = ",".join(str(r) for r in contract_data("ORACLE_FLOOR_RATES")["values"]) + scales = contract_data("PARAM_SCALES")["ids"] + with open(os.path.join(ROOT, "protocol", "plugin-hostd.json")) as f: + declared = [s["name"] for s in json.load(f)["param_scales"]] + check(declared == scales, "the protocol's parameter scales %s are omx-contract's PARAM_SCALES %s" % (declared, scales)) + + tmp = tempfile.mkdtemp(prefix="plugin-hostd-contract.") + try: + check(child_rates(PROBE, tmp) == floor, "the runner's child gets omx-contract's ORACLE_FLOOR_RATES %s by default, " + "not %s" % (floor, child_rates(PROBE, tmp))) + check(help_rates(PROBE) == floor, "the runner's usage names the same default: %s" % help_rates(PROBE)) + + # a scratch contract whose floor is other rates: the runner built against it follows, with no edit of its own + scratch = os.path.join(tmp, "include") + os.makedirs(os.path.join(scratch, "omxcontract")) + text = open(header()).read() + moved = re.sub(r"(#define OMX_ORACLE_FLOOR_RATES_INIT )\{[^}]*\}", r"\1{ 32000, 88200 }", text) + check(moved != text, "positive control: the scratch header's floor was changed") + with open(os.path.join(scratch, "omxcontract", "omx_contract_limits.h"), "w") as f: + f.write(moved.replace("#define OMX_ORACLE_FLOOR_RATES_COUNT 4u", "#define OMX_ORACLE_FLOOR_RATES_COUNT 2u")) + built = build_probe(scratch, os.path.join(tmp, "probe")) + check(built.returncode == 0, "the runner builds against the scratch contract: %s" % built.stderr[-400:]) + if built.returncode == 0: + check(child_rates(os.path.join(tmp, "probe"), tmp) == "32000,88200", + "a moved floor moves the runner's default: %s" % child_rates(os.path.join(tmp, "probe"), tmp)) + + # the generator builds against the contract as it is and writes its scales; it refuses a word of the + # protocol's own that is not the contract's, and a contract whose scales are in another order + built = build_gen(os.path.dirname(os.path.dirname(header())), os.path.join(tmp, "gen")) + check(built.returncode == 0, "positive control: the generator builds against the contract: %s" % built.stderr[-400:]) + if built.returncode == 0: + rc, out, err = gen_json(os.path.join(tmp, "gen")) + check(rc == 0 and [x["name"] for x in json.loads(out)["param_scales"]] == scales, + "positive control: the generator writes the contract's scales: rc %d %s" % (rc, err[-200:])) + own = os.path.join(tmp, "own") + shutil.copytree(os.path.join(ROOT, "include"), own) + proto = os.path.join(own, "plugin-hostd", "protocol.h") + ptext = open(proto).read() + renamed = ptext.replace('X(LOG, "log", ', 'X(LOG, "logarithmic", ') + check(renamed != ptext, "positive control: the scratch protocol.h's word was changed") + with open(proto, "w") as f: + f.write(renamed) + built = build_gen(own, os.path.join(tmp, "gen-renamed")) + if built.returncode == 0: + rc, out, err = gen_json(os.path.join(tmp, "gen-renamed")) + check(rc != 0 and "PARAM_SCALES" in err and out == "", + "the generator writes nothing for a scale word that is not the contract's: rc %d %r" % (rc, err[-200:])) + else: + check(0, "the generator builds with a renamed word, to refuse it when it runs: %s" % built.stderr[-300:]) + swapped = text.replace("OMX_PARAM_SCALES_LOG = 1", "OMX_PARAM_SCALES_LOG = 2").replace( + "OMX_PARAM_SCALES_STEPPED = 2", "OMX_PARAM_SCALES_STEPPED = 1") + check(swapped != text, "positive control: the scratch header's scales were swapped") + with open(os.path.join(scratch, "omxcontract", "omx_contract_limits.h"), "w") as f: + f.write(swapped) + 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:])) + finally: + shutil.rmtree(tmp, ignore_errors=True) + print("contract: %s" % ("FAILED (%d)" % failures if failures else "ok")) + return 1 if failures else 0 + + +sys.exit(main()) diff --git a/tests/sabotage.py b/tests/sabotage.py index c8aaf88..ae40674 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -13,7 +13,7 @@ # 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; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py +# tests/probe_schema.py; "relay" breaks src/relay.c and runs tests/relay_test.c; "contract" breaks the probe runner or the generator and runs tests/contract_derived.py; "probe" breaks src/probe.c and runs tests/probe_test.py; "nopins" breaks the packaging or a workflow and runs tests/no_pins.py SABOTAGE = [ ("the verb tail is not replayed", "src/supervisor.c", "if (replay_line(w, i->tail[m].line) != RPC_OK)", "if (0 && replay_line(w, i->tail[m].line) != RPC_OK)", @@ -276,6 +276,12 @@ ("a pin whose verdict is refused admits the add", "src/pins.c", " if (rc == SUCCESS && !strcmp(p->verdict, PHD_PIN_VERDICT_REFUSED))\n", " if (0)\n", "a_pin_whose_verdict_is_refused_refuses_the_add_with_its_own_code"), + ("the runner's default rates are typed by hand again", "src/probe.c", ".rates = floor_rates(),", + ".rates = \"44100,48000,96000,192000\",", "contract", "contract"), + ("the generator does not hold the scales to the contract's order", "tools/protocol-gen.c", + "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 build steps are back on the README", "README.md", "Building from source: see [BUILDING.md](BUILDING.md).", "Building from source:\n\n make\n\nsee [BUILDING.md](BUILDING.md).", "README.md runs no build tool", "readme"), ("BUILDING.md lost its build command", "BUILDING.md", " make [MOD_HOST_DIR=]\n", "", @@ -435,6 +441,13 @@ def run_relay_test(tree): stdout=subprocess.DEVNULL) p = subprocess.run([os.path.join(tree, "tests", "relay-test")], capture_output=True, text=True, timeout=60) return p.returncode, p.stdout + ("ok relay_test\n" if p.returncode == 0 else "") +def run_contract_test(tree): + """tests/contract_derived.py against the tree's probe runner and generator; "ok contract" when it passes""" + subprocess.run(["make", "-s", "-B", "MOD_HOST_DIR=" + MOD_HOST_DIR, "plugin-hostd-probe"], cwd=tree, check=True, + stdout=subprocess.DEVNULL) + p = subprocess.run([sys.executable, os.path.join(tree, "tests", "contract_derived.py")], cwd=tree, capture_output=True, + text=True, timeout=300, env=dict(os.environ, PROBE=os.path.join(tree, "plugin-hostd-probe"))) + return p.returncode, p.stdout + ("ok contract\n" if p.returncode == 0 else "") def run_test(tree, name, jack=False): @@ -442,6 +455,8 @@ def run_test(tree, name, jack=False): return run_format_test(tree) if jack == "relay": return run_relay_test(tree) + if jack == "contract": + return run_contract_test(tree) if jack == "pin": return run_pin_test(tree) if jack == "readme": @@ -499,6 +514,15 @@ 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 == "contract": + for d in ("src", "include", "tools", "protocol"): + shutil.copytree(os.path.join(ROOT, d), os.path.join(tree, d)) + os.makedirs(os.path.join(tree, "tests")) + shutil.copy(os.path.join(ROOT, "tests", "contract_derived.py"), os.path.join(tree, "tests")) + for f in ("Makefile",): + shutil.copy(os.path.join(ROOT, f), tree) + os.makedirs(os.path.join(tree, "packaging")) + shutil.copy(os.path.join(ROOT, "packaging", "plugin-hostd.spec"), os.path.join(tree, "packaging")) elif jack == "nopins": for f in ("packaging/plugin-hostd.spec", "debian/control", ".github/workflows/ci.yml"): os.makedirs(os.path.dirname(os.path.join(tree, f)), exist_ok=True) @@ -521,7 +545,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", "format", "relay", "probeschema"): + elif jack not in ("readme", "nopins", "format", "relay", "contract", "probeschema"): 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/tools/protocol-gen.c b/tools/protocol-gen.c index 980b302..5a49e74 100644 --- a/tools/protocol-gen.c +++ b/tools/protocol-gen.c @@ -26,12 +26,47 @@ * 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:" */ +#include #include #include #include +#include #include +/* The parameter scales are omx-contract's PARAM_SCALES, whose words the contract declares for this protocol: the same + * ids in the same order, or nothing generated from the header builds. The contract renders an id as its word in + * capitals, which scales_are_the_contracts() checks of every word at run time. */ +enum { +#define X(id, name, meaning) PHD_SCALE_AT_##id, + PHD_PARAM_SCALES(X) +#undef X + PHD_SCALE_COUNT +}; +#define X(id, name, meaning) \ + _Static_assert((int)PHD_SCALE_AT_##id == (int)OMX_PARAM_SCALES_##id, "PHD_PARAM_SCALES: " #id " is not where omx-contract's PARAM_SCALES has it"); +PHD_PARAM_SCALES(X) +#undef X +_Static_assert((unsigned)PHD_SCALE_COUNT == OMX_PARAM_SCALES_COUNT, "PHD_PARAM_SCALES: not as many scales as omx-contract's PARAM_SCALES"); + +static int scales_are_the_contracts(void) +{ + int ok = 1; + size_t n; + +#define X(id, name, meaning) \ + for (n = 0; n <= strlen(name); n++) \ + if (toupper((unsigned char)name[n]) != #id[n]) \ + { \ + fprintf(stderr, "protocol-gen: PHD_PARAM_SCALES: the word \"%s\" is not omx-contract's PARAM_SCALES id " #id "\n", name); \ + ok = 0; \ + break; \ + } + PHD_PARAM_SCALES(X) +#undef X + return ok; +} + #define PATH_MAX_DOC 512 /* ---------------------------------------------------------------- json */ @@ -626,6 +661,8 @@ static int probe_schema(const char *path) int main(int argc, char **argv) { + if (!scales_are_the_contracts()) + return 1; if (argc == 2 && !strcmp(argv[1], "json")) { json();