From af0abbe43b9123e21031df0375daf9bc81e0c867 Mon Sep 17 00:00:00 2001 From: Pau Aliagas Date: Mon, 19 Oct 2026 19:03:29 +0200 Subject: [PATCH] ci: run mod-host in the test job so the LV2 worker path is proven (#100) The test job installs mod-host from the FreeMixer channel and runs the LV2 steps of test-jack against it: mod-host's scenario corpus through the daemon with an LV2 worker, and the own-worker test that kills the worker and reads a parameter back. The bundle is the fixture of this repository, now with a stereo gain (urn:plugin-hostd:test:stereo), copied into a world of its own. The step fails when either LV2 test did not run or an LV2 sabotage was skipped. One more sabotage breaks the replay of parameter writes to a respawned mod-host. --- .github/workflows/ci.yml | 40 ++++++++++++---- BUILDING.md | 6 +++ tests/lv2_fixture.c | 46 ++++++++++++++++++- tests/plugin-hostd-fixture.lv2/manifest.ttl | 5 ++ .../plugin-hostd-fixture.ttl | 16 +++++++ tests/sabotage.py | 4 ++ 6 files changed, 106 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e4f437..5a6599d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,26 +65,46 @@ jobs: - name: Test the daemon against workers that die on cue, and the declared protocol run: make test - # The daemon with the installed worker over jack, in a PipeWire of its own. It needs a private user - # namespace with its own /proc; a runner that refuses one cannot run it, and the step says so. - - name: Test the daemon with a real worker over jack, then break every guard on purpose + # mod-host itself, the LV2 worker, from the FreeMixer channel: the build the RPM job packages, with the patches + # FreeMixer/.github carries. The floor is the one the spec requires of the protocol library. + - name: Install mod-host from the channel run: | + set -euo pipefail + dnf install -y --setopt=retries=10 mod-host + rpm -q mod-host mod-host-protocol + floor=$(sed -n 's/^Requires:.*mod-host-protocol.*>=[[:space:]]*\([^[:space:]]*\).*/\1/p' packaging/plugin-hostd.spec | head -n 1) + test -n "$floor" + have=$(rpm -q --qf '%{VERSION}-%{RELEASE}' mod-host) + test "$(printf '%s\n%s\n' "$floor" "$have" | sort -V | head -n 1)" = "$floor" || { echo "mod-host $have is below the floor $floor"; exit 1; } + + # The daemon with the installed workers over jack, in a PipeWire of its own: omx-clap-host for CLAP and mod-host + # for LV2, driving the fixture bundle of this repository (a stereo gain, tests/lv2_fixture.c) from a world of its + # own, so that the worker's bundle set is one directory. It needs a private user namespace with its own /proc; a + # runner that refuses one cannot run it, and the step says so. The LV2 steps are required to have run: a run that + # skipped them fails here. + - name: Test the daemon with real workers over jack, CLAP and LV2, then break every guard on purpose + run: | + set -euo pipefail export FAKE_COMPRESSOR_CLAP=/usr/libexec/omx-clap-host/tests/fake_compressor.clap export JACK_METER_SOURCE=/usr/libexec/omx-clap-host/tests/jack_meter_source export FAKE_CLAP=/usr/libexec/omx-clap-host/tests/fake.clap + make tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.so + mkdir -p "$PWD/lv2-world" + cp -r tests/plugin-hostd-fixture.lv2 "$PWD/lv2-world/" + export MOD_HOST=/usr/bin/mod-host LV2_DIR=$PWD/lv2-world LV2_URI=urn:plugin-hostd:test:stereo + export LV2_BUNDLE=plugin-hostd-fixture.lv2 LV2_PARAM=gain if unshare --user --map-root-user --net --pid --fork --mount-proc true; then - make test-jack OMX_CLAP_HOST=/usr/bin/omx-clap-host - OMX_CLAP_HOST=/usr/bin/omx-clap-host make sabotage + make test-jack OMX_CLAP_HOST=/usr/bin/omx-clap-host 2>&1 | tee test-jack.log + for t in mod_host_scenarios_pass_unchanged_with_an_lv2_worker lv2_own_worker_sees_one_bundle_and_replays_after_a_kill; do + grep -qx "ok $t" test-jack.log || { echo "the LV2 step $t did not run"; exit 1; } + done + OMX_CLAP_HOST=/usr/bin/omx-clap-host make sabotage 2>&1 | tee sabotage.log + if grep -q 'skip sabotage (no LV2 bundle' sabotage.log; then echo 'an LV2 sabotage was skipped'; exit 1; fi else echo "::warning::test-jack SKIPPED: this runner refuses a private user and pid namespace with its own /proc" make sabotage fi - - name: Name what this job does not run - run: | - echo "The LV2 steps of test-jack are NOT run here: they need mod-host and an LV2 bundle (MOD_HOST, LV2_DIR, LV2_URI, LV2_BUNDLE, LV2_PARAM)." - echo "They stay in the Makefile for the desk." - jack2: name: build and test over JACK2's jackd (x86_64) runs-on: ubuntu-latest diff --git a/BUILDING.md b/BUILDING.md index 83746f0..d8c6db1 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -69,6 +69,12 @@ installed by the omx-clap-host-tests package, which the spec and `debian/control also reads the meters of a CLAP compressor as `output_set` on the daemon's feedback port, before and after its worker is killed. +Given `MOD_HOST` and a bundle, it also runs mod-host's scenario corpus against the daemon with an LV2 worker, and kills +an LV2 worker to read the graph and a parameter back. The CI job does this with the mod-host package of the FreeMixer +channel and this repository's fixture bundle: build `tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.so`, copy the +bundle into a directory of its own, and give `MOD_HOST=/usr/bin/mod-host LV2_DIR= +LV2_URI=urn:plugin-hostd:test:stereo LV2_BUNDLE=plugin-hostd-fixture.lv2 LV2_PARAM=gain`. + make test-jack2 OMX_CLAP_HOST= [...] is `test-jack` against JACK2's `jackd` (`-d dummy`) instead of pipewire's own jack implementation: the server diff --git a/tests/lv2_fixture.c b/tests/lv2_fixture.c index 36fab3b..934fdf1 100644 --- a/tests/lv2_fixture.c +++ b/tests/lv2_fixture.c @@ -8,7 +8,8 @@ * under http://plugin-hostd.invalid/fixture/based, named in a manifest the test writes with an @base, so lilv finds it * and the adapter's search of the manifest text does not (resp -101). "stress" is "gain" that breaks on cue, for the * probe runner's LV2 tests (tests/probe_test.py). The ports, by index: 0 in, 1 out, 2 gain (a factor), 3 events - * (atom-in only). */ + * (atom-in only). "stereo" is the stereo effect of the LV2 steps of tests/jack_e2e.py, which run it in mod-host: the + * gain on two channels, ports 0 and 1 in, 2 and 3 out, 4 gain. */ #include #include @@ -50,6 +51,48 @@ static void connect_port(LV2_Handle instance, uint32_t port, void *data) f->gain = data; } +typedef struct { + const float *in[2]; + float *out[2]; + const float *gain; +} stereo_t; + +static LV2_Handle instantiate_stereo(const LV2_Descriptor *descriptor, double rate, const char *bundle, const LV2_Feature *const *features) +{ + (void)descriptor; + (void)rate; + (void)bundle; + (void)features; + return calloc(1, sizeof(stereo_t)); +} + +static void connect_port_stereo(LV2_Handle instance, uint32_t port, void *data) +{ + stereo_t *f = instance; + + if (port < 2) + f->in[port] = data; + else if (port < 4) + f->out[port - 2] = data; + else if (port == 4) + f->gain = data; +} + +static void run_stereo(LV2_Handle instance, uint32_t frames) +{ + stereo_t *f = instance; + float gain = f->gain ? *f->gain : 1.0f; + uint32_t c, n; + + for (c = 0; c < 2; c++) + { + if (!f->in[c] || !f->out[c]) + continue; + for (n = 0; n < frames; n++) + f->out[c][n] = f->in[c][n] * gain; + } +} + static void run(LV2_Handle instance, uint32_t frames) { fixture_t *f = instance; @@ -114,6 +157,7 @@ static const LV2_Descriptor DESCRIPTORS[] = { { FIXTURE_URI "atom-in", instantiate, connect_port, NULL, run, NULL, cleanup, NULL }, { FIXTURE_URI "fixed-block", instantiate, connect_port, NULL, run, NULL, cleanup, NULL }, { FIXTURE_URI "stress", instantiate, connect_port, NULL, run_stress, NULL, cleanup, NULL }, + { FIXTURE_URI "stereo", instantiate_stereo, connect_port_stereo, NULL, run_stereo, NULL, cleanup, NULL }, { FIXTURE_BASE "based", instantiate, connect_port, NULL, run, NULL, cleanup, NULL }, }; diff --git a/tests/plugin-hostd-fixture.lv2/manifest.ttl b/tests/plugin-hostd-fixture.lv2/manifest.ttl index d821494..a1f6750 100644 --- a/tests/plugin-hostd-fixture.lv2/manifest.ttl +++ b/tests/plugin-hostd-fixture.lv2/manifest.ttl @@ -22,3 +22,8 @@ a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . diff --git a/tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.ttl b/tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.ttl index 7f1d3f7..f270fb4 100644 --- a/tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.ttl +++ b/tests/plugin-hostd-fixture.lv2/plugin-hostd-fixture.ttl @@ -55,3 +55,19 @@ a lv2:ControlPort , lv2:InputPort ; lv2:index 2 ; lv2:symbol "gain" ; lv2:name "Gain" ; lv2:default 1.0 ; lv2:minimum 0.0 ; lv2:maximum 2.0 ] . + + + a lv2:Plugin , lv2:AmplifierPlugin ; + doap:name "plugin-hostd fixture gain on two channels" ; + lv2:port [ + a lv2:AudioPort , lv2:InputPort ; lv2:index 0 ; lv2:symbol "lv2_audio_in_1" ; lv2:name "In 1" + ] , [ + a lv2:AudioPort , lv2:InputPort ; lv2:index 1 ; lv2:symbol "lv2_audio_in_2" ; lv2:name "In 2" + ] , [ + a lv2:AudioPort , lv2:OutputPort ; lv2:index 2 ; lv2:symbol "lv2_audio_out_1" ; lv2:name "Out 1" + ] , [ + a lv2:AudioPort , lv2:OutputPort ; lv2:index 3 ; lv2:symbol "lv2_audio_out_2" ; lv2:name "Out 2" + ] , [ + a lv2:ControlPort , lv2:InputPort ; lv2:index 4 ; lv2:symbol "gain" ; lv2:name "Gain" ; + lv2:default 1.0 ; lv2:minimum 0.0 ; lv2:maximum 2.0 + ] . diff --git a/tests/sabotage.py b/tests/sabotage.py index be41f56..529b5f0 100644 --- a/tests/sabotage.py +++ b/tests/sabotage.py @@ -67,6 +67,10 @@ "audio_of_the_other_strip_never_changes_when_one_worker_is_killed", "jack"), ("an own lv2 worker keeps the whole bundle set", "src/supervisor.c", " if (!f->narrow_world || w->pool || !uri)\n return NULL;", " return NULL;", "lv2_own_worker_sees_one_bundle_and_replays_after_a_kill", "lv2"), + ("an LV2 strip's parameter writes are not replayed to the respawned mod-host", "src/supervisor.c", + " for (m = 0; m < i->ntail; m++)\n if (replay_line(w, i->tail[m].line) != RPC_OK)", + " for (m = 0; m < 0; m++)\n if (replay_line(w, i->tail[m].line) != RPC_OK)", + "lv2_own_worker_sees_one_bundle_and_replays_after_a_kill", "lv2"), ("the checkpoint is not loaded after a real respawn", "src/supervisor.c", " if (any_ckpt)\n", " if (0 && any_ckpt)\n", "clap_own_crash_replays_bit_identically_and_the_neighbour_never_notices", "jack"), ("add checks no pin", "src/supervisor.c", "rc = pins_check(format, fwd, search_of(fmt), &layout);\n /* a plugin", "rc = SUCCESS;\n /* a plugin",