diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h index 1f9f99ef..f92f4c76 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h @@ -339,6 +339,8 @@ static const auto& mappingByCategory(const mpe::SoundCategory category) { { mpe::SoundId::Quena, {} }, { midi::Program(0, 67) } }, + { { mpe::SoundId::Dulzaina, { mpe::SoundSubCategory::Castilian } }, { midi::Program(0, 68) } }, + { { mpe::SoundId::Heckelphone, {} }, { midi::Program(0, 68) } }, { { mpe::SoundId::Oboe, { mpe::SoundSubCategory::Baroque } }, { midi::Program(0, 69) } }, { { mpe::SoundId::Oboe, {} }, { midi::Program(0, 68) } }, diff --git a/framework/mpe/soundid.h b/framework/mpe/soundid.h index c6b7de8b..c1ad6a2a 100644 --- a/framework/mpe/soundid.h +++ b/framework/mpe/soundid.h @@ -92,6 +92,7 @@ enum class SoundId WindsGroup, Piccolo, + Dulzaina, Heckelphone, HeckelphoneClarinet, Oboe, @@ -288,6 +289,7 @@ enum class SoundSubCategory African, Indian, Spanish, + Castilian, Swedish, Hungarian, Romanian, @@ -503,6 +505,7 @@ inline const std::unordered_map ID_STRINGS { SoundId::WindsGroup, String(u"winds_group") }, { SoundId::Piccolo, String(u"piccolo") }, + { SoundId::Dulzaina, String(u"dulzaina") }, { SoundId::Heckelphone, String(u"heckelphone") }, { SoundId::HeckelphoneClarinet, String(u"heckelphone_clarinet") }, { SoundId::Oboe, String(u"oboe") }, @@ -746,6 +749,7 @@ inline const std::unordered_map SUBCATEGORY_STRINGS { SoundSubCategory::African, String(u"african") }, { SoundSubCategory::Indian, String(u"indian") }, { SoundSubCategory::Spanish, String(u"spanish") }, + { SoundSubCategory::Castilian, String(u"castilian") }, { SoundSubCategory::Swedish, String(u"swedish") }, { SoundSubCategory::Hungarian, String(u"hungarian") }, { SoundSubCategory::Romanian, String(u"romanian") }, diff --git a/framework/workspace/tests/CMakeLists.txt b/framework/workspace/tests/CMakeLists.txt index 0869d0ec..6ad7f78e 100644 --- a/framework/workspace/tests/CMakeLists.txt +++ b/framework/workspace/tests/CMakeLists.txt @@ -22,7 +22,10 @@ set(MODULE_TEST muse_workspace_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/mocks/workspaceconfigurationmock.h + ${CMAKE_CURRENT_LIST_DIR}/workspace_test_helpers.h + ${CMAKE_CURRENT_LIST_DIR}/workspace_test_helpers.cpp ${CMAKE_CURRENT_LIST_DIR}/environment.cpp + ${CMAKE_CURRENT_LIST_DIR}/workspaceconfig_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/workspacemanager_tests.cpp ) @@ -36,8 +39,30 @@ set(MODULE_TEST_INCLUDE ${MUSE_FRAMEWORK_SRC_PATH}/multiwindows ) +# Paths injected as compile-time defines for use by the test environment: +# BUILTIN_WORKSPACES_DIR - path to directory containing builtin .mws files (always tested) +# WORKSPACE_CONFIG_FILE - framework-level workspace config (always tested) +# APP_BUILTIN_WORKSPACES_DIR - optional app-level workspaces dir (tested additionally if set) +# e.g. set(APP_BUILTIN_WORKSPACES_DIR "${CMAKE_SOURCE_DIR}/share/workspaces") +# APP_WORKSPACE_CONFIG_FILE - optional app-level config (tested additionally if set) +# e.g. set(APP_WORKSPACE_CONFIG_FILE "${CMAKE_SOURCE_DIR}/src/app/configs/workspaces.cfg") set(MODULE_TEST_DEF - BUILTIN_WORKSPACES_DIR="${CMAKE_SOURCE_DIR}/share/workspaces" + BUILTIN_WORKSPACES_DIR="${CMAKE_CURRENT_LIST_DIR}/testdata/workspaces" + WORKSPACE_CONFIG_FILE="${CMAKE_CURRENT_LIST_DIR}/testdata/workspaces.cfg" ) +message(STATUS " [workspace tests] Framework config: ${CMAKE_CURRENT_LIST_DIR}/testdata/workspaces.cfg") +message(STATUS " [workspace tests] Framework workspaces: ${CMAKE_CURRENT_LIST_DIR}/testdata/workspaces") + +if (APP_WORKSPACE_CONFIG_FILE AND APP_BUILTIN_WORKSPACES_DIR) + list(APPEND MODULE_TEST_DEF + APP_WORKSPACE_CONFIG_FILE="${APP_WORKSPACE_CONFIG_FILE}" + APP_BUILTIN_WORKSPACES_DIR="${APP_BUILTIN_WORKSPACES_DIR}" + ) + message(STATUS " [workspace tests] App config: ${APP_WORKSPACE_CONFIG_FILE}") + message(STATUS " [workspace tests] App workspaces: ${APP_BUILTIN_WORKSPACES_DIR}") +elseif (APP_WORKSPACE_CONFIG_FILE OR APP_BUILTIN_WORKSPACES_DIR) + message(FATAL_ERROR "APP_WORKSPACE_CONFIG_FILE and APP_BUILTIN_WORKSPACES_DIR must both be set or both be unset") +endif() + include(SetupGTest) diff --git a/framework/workspace/tests/environment.cpp b/framework/workspace/tests/environment.cpp index c64641bb..630b0728 100644 --- a/framework/workspace/tests/environment.cpp +++ b/framework/workspace/tests/environment.cpp @@ -20,8 +20,11 @@ * along with this program. If not, see . */ +#include + #include "testing/environment.h" +#include "workspace/tests/workspace_test_helpers.h" #include "workspace/tests/mocks/workspaceconfigurationmock.h" using namespace ::testing; @@ -29,12 +32,17 @@ using namespace ::testing; static muse::testing::SuiteEnvironment workspace_se = muse::testing::SuiteEnvironment() .setPreInit([](){ - auto workspaceConfig = std::make_shared<::testing::NiceMock >(); + auto& testCfg = muse::workspace::WorkspaceTestConfig::instance(); + if (!testCfg.load(muse::io::path_t(WORKSPACE_CONFIG_FILE), BUILTIN_WORKSPACES_DIR)) { + FAIL() << "Failed to load workspace config from: " << WORKSPACE_CONFIG_FILE; + } + + auto workspaceConfigMock = std::make_shared<::testing::NiceMock >(); - ON_CALL(*workspaceConfig, defaultWorkspaceName()) - .WillByDefault(Return("Default")); + ON_CALL(*workspaceConfigMock, defaultWorkspaceName()) + .WillByDefault(Return(testCfg.defaultWorkspaceName())); - muse::modularity::globalIoc()->registerExport("utests", workspaceConfig); + muse::modularity::globalIoc()->registerExport("utests", workspaceConfigMock); }).setDeInit([](){ muse::modularity::globalIoc()->unregister("utests"); }); diff --git a/framework/workspace/tests/testdata/workspaces.cfg b/framework/workspace/tests/testdata/workspaces.cfg new file mode 100644 index 00000000..cb9265f2 --- /dev/null +++ b/framework/workspace/tests/testdata/workspaces.cfg @@ -0,0 +1,6 @@ +{ + "default_workspace_name": "Default", + "builtin_workspace_files": [ + "Default.mws" + ] +} diff --git a/framework/workspace/tests/testdata/workspaces/Default.mws b/framework/workspace/tests/testdata/workspaces/Default.mws new file mode 100644 index 00000000..38808f9d Binary files /dev/null and b/framework/workspace/tests/testdata/workspaces/Default.mws differ diff --git a/framework/workspace/tests/workspace_test_helpers.cpp b/framework/workspace/tests/workspace_test_helpers.cpp new file mode 100644 index 00000000..fef5b603 --- /dev/null +++ b/framework/workspace/tests/workspace_test_helpers.cpp @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "workspace_test_helpers.h" + +#include "global/configreader.h" + +using namespace muse; +using namespace muse::workspace; + +WorkspaceTestConfig& WorkspaceTestConfig::instance() +{ + static WorkspaceTestConfig s; + return s; +} + +bool WorkspaceTestConfig::load(const io::path_t& configPath, const std::string& builtinWorkspacesDir) +{ + m_defaultWorkspaceName.clear(); + m_builtinFiles.clear(); + + m_builtinWorkspacesDir = builtinWorkspacesDir; + m_config = ConfigReader::read(configPath); + + m_defaultWorkspaceName = m_config.value("default_workspace_name").toString(); + if (m_defaultWorkspaceName.empty()) { + return false; + } + + ValList files = m_config.value("builtin_workspace_files").toList(); + for (const Val& v : files) { + m_builtinFiles.push_back(v.toString()); + } + + return !m_builtinFiles.empty(); +} + +const Config& WorkspaceTestConfig::config() const +{ + return m_config; +} + +const std::string& WorkspaceTestConfig::defaultWorkspaceName() const +{ + return m_defaultWorkspaceName; +} + +const std::vector& WorkspaceTestConfig::builtinFiles() const +{ + return m_builtinFiles; +} + +const std::string& WorkspaceTestConfig::builtinWorkspacesDir() const +{ + return m_builtinWorkspacesDir; +} diff --git a/framework/workspace/tests/workspace_test_helpers.h b/framework/workspace/tests/workspace_test_helpers.h new file mode 100644 index 00000000..8f2303d1 --- /dev/null +++ b/framework/workspace/tests/workspace_test_helpers.h @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include + +#include "global/types/config.h" +#include "io/path.h" + +namespace muse::workspace { +class WorkspaceTestConfig +{ +public: + WorkspaceTestConfig() = default; + + WorkspaceTestConfig(const WorkspaceTestConfig&) = delete; + WorkspaceTestConfig& operator=(const WorkspaceTestConfig&) = delete; + WorkspaceTestConfig(WorkspaceTestConfig&&) = delete; + WorkspaceTestConfig& operator=(WorkspaceTestConfig&&) = delete; + + static WorkspaceTestConfig& instance(); + + bool load(const io::path_t& configPath, const std::string& builtinWorkspacesDir); + + const Config& config() const; + const std::string& defaultWorkspaceName() const; + const std::vector& builtinFiles() const; + const std::string& builtinWorkspacesDir() const; + +private: + Config m_config; + std::string m_defaultWorkspaceName; + std::vector m_builtinFiles; + std::string m_builtinWorkspacesDir; +}; +} diff --git a/framework/workspace/tests/workspaceconfig_tests.cpp b/framework/workspace/tests/workspaceconfig_tests.cpp new file mode 100644 index 00000000..21d786fd --- /dev/null +++ b/framework/workspace/tests/workspaceconfig_tests.cpp @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include + +#include + +#include "io/fileinfo.h" +#include "workspace/tests/workspace_test_helpers.h" + +using namespace muse; +using namespace muse::workspace; + +struct ConfigTestParam { + std::string configFile; + std::string workspacesDir; +}; + +static void PrintTo(const ConfigTestParam& param, std::ostream* os) +{ + *os << param.configFile; +} + +static std::vector configTestParams() +{ + std::vector params = { + { WORKSPACE_CONFIG_FILE, BUILTIN_WORKSPACES_DIR }, + }; +#if defined(APP_WORKSPACE_CONFIG_FILE) && defined(APP_BUILTIN_WORKSPACES_DIR) + params.push_back({ APP_WORKSPACE_CONFIG_FILE, APP_BUILTIN_WORKSPACES_DIR }); +#endif + return params; +} + +class Workspace_ConfigTests : public ::testing::TestWithParam +{ +}; + +TEST_P(Workspace_ConfigTests, ConfigIsValid) +{ + const auto& param = GetParam(); + WorkspaceTestConfig cfg; + ASSERT_TRUE(cfg.load(io::path_t(param.configFile), param.workspacesDir)) + << "failed to load: " << param.configFile; + + //! [THEN] default_workspace_name is a non-empty string + EXPECT_FALSE(cfg.defaultWorkspaceName().empty()); + + //! [THEN] builtin_workspace_files is a non-empty list + EXPECT_FALSE(cfg.builtinFiles().empty()); + + //! [THEN] Each builtin file exists on disk + bool defaultFoundInBuiltins = false; + for (const auto& filename : cfg.builtinFiles()) { + io::path_t fullPath = io::path_t(cfg.builtinWorkspacesDir() + "/" + filename); + EXPECT_TRUE(io::FileInfo::exists(fullPath)) << "missing: " << fullPath.toStdString(); + + std::string baseName = io::completeBasename(fullPath).toStdString(); + if (baseName == cfg.defaultWorkspaceName()) { + defaultFoundInBuiltins = true; + } + } + + EXPECT_TRUE(defaultFoundInBuiltins) + << "default workspace \"" << cfg.defaultWorkspaceName() << "\" not found in builtin files"; +} + +//! If this test fails, a new field was added to the config. +//! Please add validation for it in ConfigIsValid above, then update EXPECTED_KEYS. +TEST_P(Workspace_ConfigTests, NoUntestedConfigKeys) +{ + const auto& param = GetParam(); + WorkspaceTestConfig cfg; + ASSERT_TRUE(cfg.load(io::path_t(param.configFile), param.workspacesDir)) + << "failed to load: " << param.configFile; + + const std::set EXPECTED_KEYS = { + "default_workspace_name", + "builtin_workspace_files", + }; + + std::set actualKeys; + for (const auto& [key, val] : cfg.config().data()) { + actualKeys.insert(key); + } + + EXPECT_EQ(actualKeys, EXPECTED_KEYS) + << "config has changed — add validation in ConfigIsValid and update EXPECTED_KEYS"; +} + +INSTANTIATE_TEST_SUITE_P( + Configs, + Workspace_ConfigTests, + ::testing::ValuesIn(configTestParams()) + ); diff --git a/framework/workspace/tests/workspacemanager_tests.cpp b/framework/workspace/tests/workspacemanager_tests.cpp index f533d076..bba67b78 100644 --- a/framework/workspace/tests/workspacemanager_tests.cpp +++ b/framework/workspace/tests/workspacemanager_tests.cpp @@ -28,6 +28,7 @@ #include "workspace/internal/workspacemanager.h" #include "workspace/iworkspaceconfiguration.h" #include "workspace/tests/mocks/workspaceconfigurationmock.h" +#include "workspace/tests/workspace_test_helpers.h" #include "multiwindows/tests/mocks/multiwindowsprovidermock.h" using ::testing::Return; @@ -35,14 +36,17 @@ using ::testing::Return; using namespace muse; using namespace muse::workspace; -static const std::string BUILTIN_WORKSPACE_DIR = BUILTIN_WORKSPACES_DIR; - namespace muse::workspace { class Workspace_WorkspaceManagerTests : public ::testing::Test { public: void SetUp() override { + const auto& testCfg = WorkspaceTestConfig::instance(); + m_defaultWorkspaceName = testCfg.defaultWorkspaceName(); + m_builtinFiles = testCfg.builtinFiles(); + m_builtinWorkspacesDir = testCfg.builtinWorkspacesDir(); + m_userWorkspacesDir = std::make_unique(); ASSERT_TRUE(m_userWorkspacesDir->isValid()); @@ -69,7 +73,7 @@ class Workspace_WorkspaceManagerTests : public ::testing::Test { io::paths_t paths; for (const auto& name : filenames) { - paths.push_back(io::path_t(BUILTIN_WORKSPACE_DIR + "/" + name)); + paths.push_back(io::path_t(m_builtinWorkspacesDir + "/" + name)); } ON_CALL(*m_workspaceConfig, builtinWorkspacesFilePaths()) @@ -86,6 +90,10 @@ class Workspace_WorkspaceManagerTests : public ::testing::Test } protected: + std::string m_defaultWorkspaceName; + std::vector m_builtinFiles; + std::string m_builtinWorkspacesDir; + std::unique_ptr m_userWorkspacesDir; std::string m_userWorkspacesPath; @@ -97,39 +105,39 @@ class Workspace_WorkspaceManagerTests : public ::testing::Test TEST_F(Workspace_WorkspaceManagerTests, BuiltinWorkspaceLoadsOnInit) { - //! [GIVEN] Builtin workspace file exists - setupBuiltinPaths({ "Default.mws" }); + //! [GIVEN] All builtin workspace files exist + setupBuiltinPaths(m_builtinFiles); - //! [GIVEN] Current workspace is "Default" + //! [GIVEN] Current workspace is the default ON_CALL(*m_workspaceConfig, currentWorkspaceName()) - .WillByDefault(Return("Default")); + .WillByDefault(Return(m_defaultWorkspaceName)); //! [WHEN] WorkspaceManager is initialized initManager(); //! [THEN] Default workspace is loaded EXPECT_NE(m_manager->defaultWorkspace(), nullptr); - EXPECT_EQ(m_manager->defaultWorkspace()->name(), "Default"); + EXPECT_EQ(m_manager->defaultWorkspace()->name(), m_defaultWorkspaceName); //! [THEN] Current workspace matches default EXPECT_NE(m_manager->currentWorkspace(), nullptr); - EXPECT_EQ(m_manager->currentWorkspace()->name(), "Default"); + EXPECT_EQ(m_manager->currentWorkspace()->name(), m_defaultWorkspaceName); - //! [THEN] Builtin workspace is available - EXPECT_EQ(m_manager->workspaces().size(), 1); + //! [THEN] All builtin workspaces are available + EXPECT_EQ(m_manager->workspaces().size(), m_builtinFiles.size()); } TEST_F(Workspace_WorkspaceManagerTests, FallbackToDefaultOnMissingCurrentWorkspace) { - //! [GIVEN] Builtin workspace file exists - setupBuiltinPaths({ "Default.mws" }); + //! [GIVEN] All builtin workspace files exist + setupBuiltinPaths(m_builtinFiles); //! [GIVEN] Current workspace name references a workspace that no longer exists ON_CALL(*m_workspaceConfig, currentWorkspaceName()) .WillByDefault(Return("MyDeletedWorkspace")); //! [GIVEN] Manager will correct the setting to default - EXPECT_CALL(*m_workspaceConfig, setCurrentWorkspaceName("Default")) + EXPECT_CALL(*m_workspaceConfig, setCurrentWorkspaceName(m_defaultWorkspaceName)) .Times(1); //! [WHEN] WorkspaceManager is initialized @@ -137,29 +145,29 @@ TEST_F(Workspace_WorkspaceManagerTests, FallbackToDefaultOnMissingCurrentWorkspa //! [THEN] Falls back to default workspace EXPECT_NE(m_manager->currentWorkspace(), nullptr); - EXPECT_EQ(m_manager->currentWorkspace()->name(), "Default"); + EXPECT_EQ(m_manager->currentWorkspace()->name(), m_defaultWorkspaceName); EXPECT_NE(m_manager->defaultWorkspace(), nullptr); - EXPECT_EQ(m_manager->defaultWorkspace()->name(), "Default"); + EXPECT_EQ(m_manager->defaultWorkspace()->name(), m_defaultWorkspaceName); } TEST_F(Workspace_WorkspaceManagerTests, EmptyUserDirUsesBuiltinWorkspaces) { - //! [GIVEN] Builtin workspace file exists, user dir is empty - setupBuiltinPaths({ "Default.mws" }); + //! [GIVEN] All builtin workspace files exist, user dir is empty + setupBuiltinPaths(m_builtinFiles); ON_CALL(*m_workspaceConfig, currentWorkspaceName()) - .WillByDefault(Return("Default")); + .WillByDefault(Return(m_defaultWorkspaceName)); //! [WHEN] WorkspaceManager is initialized initManager(); //! [THEN] Workspaces are loaded from builtins only - EXPECT_EQ(m_manager->workspaces().size(), 1); + EXPECT_EQ(m_manager->workspaces().size(), m_builtinFiles.size()); //! [THEN] Current workspace is loaded and usable auto current = m_manager->currentWorkspace(); ASSERT_NE(current, nullptr); - EXPECT_EQ(current->name(), "Default"); + EXPECT_EQ(current->name(), m_defaultWorkspaceName); //! [THEN] Reading workspace data does not crash auto data = current->rawData("ui_settings");