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();