tests: make the test suite work on macOS and Windows#2135
Open
jcelerier wants to merge 2 commits into
Open
Conversation
The SCORE_TESTING suite had only ever been built and run on Linux with
dynamic plugins. Running it on the macOS SDK build and on Windows/MSYS2
(both static-plugin, static-Qt) surfaced several genuine bugs:
- score_init_static_plugins() was not idempotent. It is generated as
`{ static X p; score::staticPlugins().push_back(&p); }` per plug-in: the
object is static but the push_back runs on every call, and it is called
from every application instance. A second score::MinimalApplication in
one process therefore registered every plug-in twice, so every factory
and action condition was registered twice and the second application
aborted in ActionManager::onFocusChange. Guard with an early return.
This is what test_regression_minimal_app_twice was written to catch; it
could not fail on Linux because dynamic-plugin builds skip that path.
- MinimalApplication's default arguments were a shared static, but
QApplication takes argc by reference and edits argc/argv in place. The
second application in a process was handed the leftovers of the first.
Give each instance its own (intentionally leaked, since QApplication
keeps the reference) argument block.
- Static-Qt builds have no runtime QPA plugin discovery: every executable
must import the platform plugins itself, like the main app does.
Otherwise every test dies with "Could not find the Qt platform plugin".
Static-plugin builds likewise need the plug-in set linked in.
- The application under test scanned the developer machine's real
VST/CLAP/AU library, spawning a puppet process per plug-in: invisible on
CI, but minutes per test — or a timeout — on a workstation. Set
SCORE_DISABLE_AUDIOPLUGINS in the test environment.
- score-lib-state / score-lib-device tests built their application in a
file-scope static initializer, which with a static Qt runs before the
Q_IMPORT_PLUGIN registrars: construct it at test-run start instead.
- tests/nodes used sigsetjmp/siglongjmp, alarm() and SIGBUS/SIGTRAP/SIGALRM,
none of which exist on Windows: fall back to setjmp/longjmp with a
reduced signal set there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
score::View::event displays status tips through m_status, but that label is only created when the first panel delegate is registered (View::setupPanel). Headless and minimal application setups - the test harnesses among them - register no panel, so m_status stays null while menus and actions still emit QStatusTipEvents. Triggering any action then crashed in QLabel::setText(this=0x0), reached via QMenuPrivate::_q_actionTriggered -> hideUpToMenuBar -> setCurrentAction -> QActionPrivate::showStatusText. This crashed 20 of the gfx/threedim test executables on Windows during startup, identically in every render backend (OpenGL, Vulkan, D3D11, D3D12) and under both the offscreen and windows platform plugins - the backend was never involved. With the guard, all 160 segfaults across that matrix are gone and the suites either run or skip cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE
jcelerier
commented
Jul 21, 2026
| # With dynamic plugins (Linux) the plugin is discovered at runtime from | ||
| # <build>/plugins, but in static-plugin builds (macOS SDK) | ||
| # score_init_static_plugins() only registers plugins whose headers are visible | ||
| # to the test target, i.e. linked ones. |
Member
Author
There was a problem hiding this comment.
Move these tests out of score-lib-device inside our tests/ folder
jcelerier
commented
Jul 21, 2026
|
|
||
| string(APPEND SCORE_PLUGINS_FILE_DATA "#include <score/plugins/PluginInstances.hpp>\n") | ||
| string(APPEND SCORE_PLUGINS_FILE_DATA "void score_init_static_plugins() {\n") | ||
| # Idempotent: the static plug-in set is process-global, but the function is |
Member
Author
There was a problem hiding this comment.
Remove all the slop comments added
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.
The SCORE_TESTING suite had only ever been built and run on Linux with dynamic plugins. Running it on the macOS SDK build and on Windows/MSYS2 (both static-plugin, static-Qt) surfaced several genuine bugs. All fixes verified on Linux (no regression), macOS (arm64, SDK static Qt, ASAN+UBSan) and Windows (clang64, static).
score_init_static_plugins()was not idempotent — the headline bug. It is generated as{ static X p; score::staticPlugins().push_back(&p); }per plug-in: the object isstatic, but thepush_backruns on every call, and it is called from every application instance. A secondscore::MinimalApplicationin one process therefore registered every plug-in twice, so every factory and action condition was registered twice and the second application aborted inActionManager::onFocusChange(SCORE_ASSERT(m_focusConditions.find(key) == end())). This is exactly whattest_regression_minimal_app_twicewas written to catch — it could not fail on Linux because dynamic-plugin builds skip that path entirely. Verified: that test went from abort (exit 3 / SIGTRAP) to green on Windows.MinimalApplicationdefault arguments were a shared static, butQApplicationtakesargcby reference and editsargc/argvin place, so the second application got the leftovers of the first (argc == 0). Each instance now gets its own block.Static-Qt builds have no runtime QPA plugin discovery — every executable must import the platform plugins itself, as the main app does; otherwise every test dies with "Could not find the Qt platform plugin". Static-plugin builds likewise need the plug-in set linked in.
The app scanned the machine's real VST/CLAP/AU library during tests, spawning a puppet process per plug-in. Invisible on CI (no plug-ins installed), but minutes per test — or a timeout — on a workstation: 300 s vs 1 s for one test. Now
SCORE_DISABLE_AUDIOPLUGINS=1in the ctest environment.score-lib-state / score-lib-device tests built their QApplication in a file-scope static initializer, which with a static Qt runs before the
Q_IMPORT_PLUGINregistrars. Constructed at test-run start instead.tests/nodesused POSIX-only APIs (sigsetjmp/siglongjmp,alarm(),SIGBUS/SIGTRAP/SIGALRM) so the suite did not compile on Windows at all — 31 errors. Portable shim with a reduced signal set there.🤖 Generated with Claude Code
https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE