Skip to content

tests: make the test suite work on macOS and Windows#2135

Open
jcelerier wants to merge 2 commits into
masterfrom
fix/cross-platform-tests
Open

tests: make the test suite work on macOS and Windows#2135
jcelerier wants to merge 2 commits into
masterfrom
fix/cross-platform-tests

Conversation

@jcelerier

Copy link
Copy Markdown
Member

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 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 (SCORE_ASSERT(m_focusConditions.find(key) == end())). This is exactly what test_regression_minimal_app_twice was 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.

MinimalApplication default arguments were a shared static, but QApplication takes argc by reference and edits argc/argv in 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=1 in 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_PLUGIN registrars. Constructed at test-run start instead.

tests/nodes used 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

jcelerier and others added 2 commits July 20, 2026 18:54
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
# 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move these tests out of score-lib-device inside our tests/ folder


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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove all the slop comments added

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant