tests: cover the QML protocol bindings and QML application use cases - #2278
Merged
Merged
Conversation
jcelerier
force-pushed
the
tests/qml-protocols-and-sat-usecases
branch
2 times, most recently
from
September 14, 2026 14:58
40ea6be to
4c5db55
Compare
…ebugging BOOST_ASIO_ENABLE_BUFFER_DEBUGGING changes the layout of asio's internal types. libossia sets it on its own target in Debug while the vendored liblsl compiles boost.asio without it, and score is what links the two together, so score is where it has to be made uniform. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
… into Two ways the engine survived what it reads and writes. Queued push and receive slots, and the polling timers, kept firing after stop() released the QQmlEngine: they now bail on a null engine, which is set before it goes away. And a script's resolved addresses were cached with nothing invalidating them, so a device's destruction left dangling parameter_base* that a script's onClose then wrote through. The cache libossia keeps is dropped on every device-list mutation, and the mapper maintains that list synchronously from the main thread -- rootsChanged is queued, which is already too late, and its raw node pointers have crossed a thread by the time they arrive. Script access is disabled before the tree is destroyed, which waits for any script currently inside read or write. The script-facing object is also told which device owns it before the script loads, so an unqualified address resolves in that device rather than in a sibling exposing the same leaf name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
…hat made it The contract QML scripts are written against is to read the enumerated sources once and reuse their settings later. Every re-enumeration deleted the identifiers and allocated new ones, so a script holding one was reading freed memory -- the QML wrapper does not keep the C++ object alive. The enumerator owns them now and refreshes an existing one in place, matched on (protocol, category, name), which is what a source is. Leaving the enumerated list no longer destroys an identifier, only unlists it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
… is not
Score.enumerateDevices() returned an enumerator with no parent, which the QML
garbage collector was free to take while it was still discovering: an
asynchronous backend such as NDI or Spout would then simply never report. It
belongs to the scripting context, for as long as that lives.
Score.createDevice() with a name already in the document added a second device
under it. A name is the only handle the rest of the document has on a device --
"Camera:/" on a port, Score.device("Camera"), the explorer tree -- so the second
does not replace the first, it shadows it, and the addresses of the shadowed one
stay live and unreachable. Refused on the name; adding a device during execution
is still allowed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
…process The runtime was keyed on (thread, QRhi). Two sinks on one node -- two QML TextureSources previewing the same process, or a preview panel open in two views -- are two render lists that may share both, so the second was handed the first one's runtime, whose QQuickWindow is bound to the other's render target, and tripped an assert. Keyed on the renderer instead, which also means a renderer only ever inserts and erases its own entry: under SCORE_THREADED_GFX the release can run on a different thread than the acquire. Several runtimes are several instances of the script, but still one process. Its value outlets and its messages to the UI are written by exactly one of them -- the first to run claims the role until its render list goes away -- or opening a second preview of a mapping doubles every value it sends. The others still clear their own outlets, or a non-publishing script's `values` would grow without bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
The renderer unregistered its preview node through the item, but Qt destroys the renderer after the item and on the render thread, so that pointer was already null and the node and its render list stayed in the graph for the rest of the session. It remembers the document plugin it registered with instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
avendish's contract is that what work() returns is invoked back in the processing thread against the object, which may write its outlets from there. The execution queue cannot honour that: it is drained before the graph runs, and init_node then clears every outlet of the node and sets the tick's frame indices, so a result's writes were erased and carried the previous tick's frame index. Objects had to work around it by re-emitting from operator(). The results are queued on the node and drained at the top of its run() instead -- outlets cleared, frame indices this tick's, operator() not yet run -- so what a result writes to a port is delivered for this tick, and what it writes to an output field is flushed by the usual finish_run(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
An object whose only gfx ports are CPU buffer outputs never touches the RHI: everything a renderer owns for it is renderer-side, and nothing in the object is. It was still constructed once per RenderList, so two output windows meant two of whatever the object owns -- two TCP listeners, two of any other CPU identity -- and rebuilding a renderer restarted it. Such a node now gets a single instance shared by every renderer of it, while a node that does keep renderer-side state still gets one each. The buffers stay per renderer, so the shared object's upload callbacks are rebound to the renderer about to run it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
45 scenarios written by hand against the Protocols.* bindings, vendored as they are apart from four `import QtQml` lines they need to parse at all, plus the fixture that runs one: it stands a Mapper device up on a real socket, drives it and reads the values back out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
Each scenario runs against an independent local peer, and the framing, encoding and OSC oracles are written out by hand in the test rather than reused from score, so its encoder and decoder cannot agree on the same wrong bytes. Covers the line, size-prefix, SLIP, Intel HEX and S-record framings, the hex, base64 and ascii85 encodings, HTTP verbs, status codes, query parameters and bearer auth, UDP and Unix datagrams, virtual MIDI and UMP, and nine hardware control protocols emulated by their own peers. This is what the mapper teardown defects were found with: the use-after-free through a script's cached addresses crashed about one run in three here while every assertion passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
Worker-thread jobs and the callback outlets they write, container payloads, messages dispatch, a buffer outlet whose payload is resized every tick, and destroy/recreate under MALLOC_PERTURB_. Covers both avnd fixes: a worker result reaching an outlet for the tick it is applied in, and a CPU-only buffer producer being constructed once rather than once per render list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
A laid-out source that resizes, at integer and fractional sizes; a large fixed-size source rendered while hidden; device pixel ratio; two sources on one process; and the process destroyed and rebuilt underneath them. This is what the two texture-source defects were found with: two sources on one process tripped an assert, and a destroyed source left its preview node and render list in the graph. Reaching the JS application plugin across the plug-in boundary needs its typeinfo exported, like the other plugins' already is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
Enumerate, read the settings back, hand them to createDevice; the enumerator collected mid-discovery; a device added while the transport plays; a backend switch as one undo step. Covers the three enumeration defects: identifiers deleted under a script that kept one, an enumerator collectible while still discovering, and createDevice shadowing an existing name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
The --ui console engine is a different QQmlEngine on a different io_context from a Mapper device's. OSC in and out byte-exact, and ports rebound across listen toggles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
The three protocol suites were given a timeout and the four use-case ones were not, so a test that blocks holds the whole run for the default twenty-five minutes. The enumeration suite stands real capture devices up, and a machine that has one reaches driver teardown paths that can block outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q
jcelerier
force-pushed
the
tests/qml-protocols-and-sat-usecases
branch
from
September 14, 2026 16:21
4c5db55 to
1713909
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebased on master and split into one commit per change. Depends on
ossia/libossia#939, now merged: the submodule pointer here is libossia master.
Fixes, and the test that covers each
build:asio buffer debuggingBOOST_ASIO_ENABLE_BUFFER_DEBUGGINGin Debug while the vendored liblsl does not, and the flag changes asio's internal layout — an ODR violation in any binary linking bothprotocols:a mapper's script cannot outlive its device treestop()released theQQmlEngine; a script's resolved addresses were cached with no invalidation, so a device's destruction left danglingparameter_base*itsonClosethen wrote throughtest_unit_mapper_protocols— "read and write callbacks survive repeated device removal", "an unqualified mapper address resolves in the script's own device"js:a device identifier outlives the enumeration that made ittest_integration_device_enumeration_lifecyclejs:an enumerator is a subscription, and a name is taken or it is notcreateDeviceshadowed an existing name instead of refusingjs:one Qt Quick runtime per renderer, one publisher(thread, QRhi), so a second render list on the same RHI got the first one's runtime and tripped an assert; and several runtimes of one process each published its outputstest_integration_texture_source_lifecyclejs:a destroyed texture source takes its preview nodeavnd:a worker result is applied at the start of the node's own tickinit_nodecleared the outlets, with the previous tick's frame index, so objects had to re-emit fromoperator()test_integration_avnd_worker_lifecycleavnd:a CPU-only buffer producer is one objectTests
tests/testdata/protocols-qml, byte-identical to the originals apart from fourimport QtQmllines needed to parse, run by real Mapper devices over real sockets against independent peers. Framing, encoding and OSC oracles are written out in the tests, so score's encoder and decoder cannot agree on the same wrong bytes.Protocols.*from the--uiconsole engine, and the avendish worker/buffer contract.Changes from the original single commit
src/plugins/score-plugin-avnd/AvndProcesses/Test123.hppremoved: a scratchDistortionobject, referenced by nothing.TIMEOUT 180the three protocol targets already had, so a blocking test fails the run instead of holding it for the default twenty-five minutes.Verification
Full build clean. 174 tests across the changed areas pass; the only failure is
test_integration_js_remove_cable, which is red on master as well.One case does not pass on a machine with a webcam, and it is not this branch's doing:
test_integration_device_enumeration_lifecycle, "a device name already taken is refused rather than shadowed", stands two live Camera devices up and hangs on stop, inVideo::CameraInput::close_file()joining its decode thread —close_file()does callm_interrupt.abort()before joining, so the buffer thread is blocked somewhere the interrupt callback does not reach — plausibly the v4l2 open of a second stream on a busy device. None of that code is touched by this branch, and the sibling case that stands up a single camera during playback passes. Reproducible 3/3 here. Worth fixing before this merges, since the new suite is the first thing to reach it.🤖 Generated with Claude Code
https://claude.ai/code/session_019QWYTEhFJonuUNTEmBfv7Q