diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58d84ce..aa2b30d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,8 +112,9 @@ jobs: Set-Location -Path usbmmidd_v2/usbmmidd_v2 ./deviceinstaller64 install usbmmidd.inf usbmmidd - # create up to 4 virtual displays - for ($i = 1; $i -le 4; $i++) { + # create 2 virtual displays, using 4 can crash the runner + # see: https://github.com/LizardByte/libdisplaydevice/pull/36 + for ($i = 1; $i -le 2; $i++) { ./deviceinstaller64 enableidd 1 } diff --git a/src/windows/include/displaydevice/windows/winapilayer.h b/src/windows/include/displaydevice/windows/winapilayer.h index 512c838..7b67648 100644 --- a/src/windows/include/displaydevice/windows/winapilayer.h +++ b/src/windows/include/displaydevice/windows/winapilayer.h @@ -32,5 +32,9 @@ namespace display_device { /** For details @see WinApiLayerInterface::getDisplayName */ [[nodiscard]] std::string getDisplayName(const DISPLAYCONFIG_PATH_INFO &path) const override; + + /** For details @see WinApiLayerInterface::setDisplayConfig */ + [[nodiscard]] LONG + setDisplayConfig(std::vector paths, std::vector modes, UINT32 flags) override; }; } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/winapilayerinterface.h b/src/windows/include/displaydevice/windows/winapilayerinterface.h index cfb6c58..551757d 100644 --- a/src/windows/include/displaydevice/windows/winapilayerinterface.h +++ b/src/windows/include/displaydevice/windows/winapilayerinterface.h @@ -146,5 +146,25 @@ namespace display_device { */ [[nodiscard]] virtual std::string getDisplayName(const DISPLAYCONFIG_PATH_INFO &path) const = 0; + + /** + * @brief Direct wrapper around the SetDisplayConfig WinAPI. + * + * It implements no additional logic, just a direct pass-trough. + * + * @param paths List of paths to pass. + * @param modes List of modes to pass. + * @param flags Flags to pass. + * @returns The return error code of the API. + * + * EXAMPLES: + * ```cpp + * std::vector paths; + * const WinApiLayerInterface* iface = getIface(...); + * const auto result = iface->setDisplayConfig(paths, {}, 0); + * ``` + */ + [[nodiscard]] virtual LONG + setDisplayConfig(std::vector paths, std::vector modes, UINT32 flags) = 0; }; } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/windisplaydevice.h b/src/windows/include/displaydevice/windows/windisplaydevice.h index cd56fee..1cea632 100644 --- a/src/windows/include/displaydevice/windows/windisplaydevice.h +++ b/src/windows/include/displaydevice/windows/windisplaydevice.h @@ -31,6 +31,10 @@ namespace display_device { [[nodiscard]] bool isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const override; + /** For details @see WinDisplayDevice::setTopology */ + [[nodiscard]] bool + setTopology(const ActiveTopology &new_topology) override; + private: std::shared_ptr m_w_api; }; diff --git a/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h b/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h index b3d7ece..39bfc45 100644 --- a/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h +++ b/src/windows/include/displaydevice/windows/windisplaydeviceinterface.h @@ -65,5 +65,20 @@ namespace display_device { */ [[nodiscard]] virtual bool isTopologyTheSame(const ActiveTopology &lhs, const ActiveTopology &rhs) const = 0; + + /** + * @brief Set a new active topology for the OS. + * @param new_topology New device topology to set. + * @returns True if the new topology has been set, false otherwise. + * + * EXAMPLES: + * ```cpp + * auto current_topology { getCurrentTopology() }; + * // Modify the current_topology + * const bool success = setTopology(current_topology); + * ``` + */ + [[nodiscard]] virtual bool + setTopology(const ActiveTopology &new_topology) = 0; }; } // namespace display_device diff --git a/src/windows/winapilayer.cpp b/src/windows/winapilayer.cpp index 0ed9f6a..89f91bb 100644 --- a/src/windows/winapilayer.cpp +++ b/src/windows/winapilayer.cpp @@ -244,7 +244,7 @@ namespace display_device { status = RegQueryValueExW(reg_key, L"EDID", nullptr, nullptr, edid.data(), &required_size_in_bytes); if (status != ERROR_SUCCESS) { - DD_LOG(error) << w_api.getErrorString(status) << " \"RegQueryValueExW\" failed when getting size."; + DD_LOG(error) << w_api.getErrorString(status) << " \"RegQueryValueExW\" failed when getting data."; return false; } @@ -528,4 +528,15 @@ namespace display_device { return toUtf8(*this, source_name.viewGdiDeviceName); } + + LONG + WinApiLayer::setDisplayConfig(std::vector paths, std::vector modes, UINT32 flags) { + // std::vector::data() "may or may not return a null pointer, if size() is 0", therefore we want to enforce nullptr... + return ::SetDisplayConfig( + paths.size(), + paths.empty() ? nullptr : paths.data(), + modes.size(), + modes.empty() ? nullptr : modes.data(), + flags); + } } // namespace display_device diff --git a/src/windows/winapiutils.cpp b/src/windows/winapiutils.cpp index 763205b..73bfc7b 100644 --- a/src/windows/winapiutils.cpp +++ b/src/windows/winapiutils.cpp @@ -371,6 +371,9 @@ namespace display_device::win_utils { group_id++; } + if (new_paths.empty()) { + DD_LOG(error) << "Failed to make paths for new topology!"; + } return new_paths; } } // namespace display_device::win_utils diff --git a/src/windows/windisplaydevicetopology.cpp b/src/windows/windisplaydevicetopology.cpp index b25952f..0475e46 100644 --- a/src/windows/windisplaydevicetopology.cpp +++ b/src/windows/windisplaydevicetopology.cpp @@ -10,6 +10,51 @@ #include "displaydevice/windows/winapiutils.h" namespace display_device { + namespace { + /** + * @see set_topology for a description as this was split off to reduce cognitive complexity. + */ + bool + doSetTopology(WinApiLayerInterface &w_api, const ActiveTopology &new_topology) { + auto display_data { w_api.queryDisplayConfig(QueryType::All) }; + if (!display_data) { + // Error already logged + return false; + } + + const auto path_data { win_utils::collectSourceDataForMatchingPaths(w_api, display_data->m_paths) }; + if (path_data.empty()) { + // Error already logged + return false; + } + + auto paths { win_utils::makePathsForNewTopology(new_topology, path_data, display_data->m_paths) }; + if (paths.empty()) { + // Error already logged + return false; + } + + UINT32 flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + LONG result { w_api.setDisplayConfig(paths, {}, flags) }; + if (result == ERROR_GEN_FAILURE) { + DD_LOG(warning) << w_api.getErrorString(result) << " failed to change topology using the topology from Windows DB! Asking Windows to create the topology."; + + flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES /* This flag is probably not needed, but who knows really... (not MSDOCS at least) */ | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE; + result = w_api.setDisplayConfig(paths, {}, flags); + if (result != ERROR_SUCCESS) { + DD_LOG(error) << w_api.getErrorString(result) << " failed to create new topology configuration!"; + return false; + } + } + else if (result != ERROR_SUCCESS) { + DD_LOG(error) << w_api.getErrorString(result) << " failed to change topology configuration!"; + return false; + } + + return true; + } + } // namespace + ActiveTopology WinDisplayDevice::getCurrentTopology() const { const auto display_data { m_w_api->queryDisplayConfig(QueryType::Active) }; @@ -97,4 +142,67 @@ namespace display_device { return lhs_copy == rhs_copy; } + + bool + WinDisplayDevice::setTopology(const ActiveTopology &new_topology) { + if (!isTopologyValid(new_topology)) { + DD_LOG(error) << "Topology input is invalid!"; + return false; + } + + const auto current_topology { getCurrentTopology() }; + if (!isTopologyValid(current_topology)) { + DD_LOG(error) << "Failed to get current topology!"; + return false; + } + + if (isTopologyTheSame(current_topology, new_topology)) { + DD_LOG(debug) << "Same topology provided."; + return true; + } + + if (doSetTopology(*m_w_api, new_topology)) { + const auto updated_topology { getCurrentTopology() }; + if (isTopologyValid(updated_topology)) { + if (isTopologyTheSame(new_topology, updated_topology)) { + return true; + } + else { + // There is an interesting bug in Windows when you have nearly + // identical devices, drivers or something. For example, imagine you have: + // AM - Actual Monitor + // IDD1 - Virtual display 1 + // IDD2 - Virtual display 2 + // + // You can have the following topology: + // [[AM, IDD1]] + // but not this: + // [[AM, IDD2]] + // + // Windows API will just default to: + // [[AM, IDD1]] + // even if you provide the second variant. Windows API will think + // it's OK and just return ERROR_SUCCESS in this case and there is + // nothing you can do. Even the Windows' settings app will not + // be able to set the desired topology. + // + // There seems to be a workaround - you need to make sure the IDD1 + // device is used somewhere else in the topology, like: + // [[AM, IDD2], [IDD1]] + // + // However, since we have this bug an additional sanity check is needed + // regardless of what Windows report back to us. + DD_LOG(error) << "Failed to change topology due to Windows bug or because the display is in deep sleep!"; + } + } + else { + DD_LOG(error) << "Failed to get updated topology!"; + } + + // Revert back to the original topology + doSetTopology(*m_w_api, current_topology); // Return value does not matter + } + + return false; + } } // namespace display_device diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 577040d..f6d631c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,6 +55,7 @@ get_property(libraries GLOBAL PROPERTY DD_TEST_LIBRARIES) add_executable(${TEST_BINARY} ${sources}) target_link_libraries(${TEST_BINARY} + PUBLIC gmock_main # if we use this we don't need our own main function libdisplaydevice # we are always testing at least the public API so it's safe to always link this libfixtures # these are our fixtures/helpers for the tests diff --git a/tests/unit/windows/CMakeLists.txt b/tests/unit/windows/CMakeLists.txt index 5b5e3fe..7bc84aa 100644 --- a/tests/unit/windows/CMakeLists.txt +++ b/tests/unit/windows/CMakeLists.txt @@ -1,9 +1,10 @@ # Add the test files in this directory add_dd_test_dir( ADDITIONAL_LIBRARIES + Boost::scope libwindows ADDITIONAL_SOURCES - mocks/*.h - mocks/*.cpp + utils/*.h + utils/*.cpp ) diff --git a/tests/unit/windows/test_winapiutils.cpp b/tests/unit/windows/test_winapiutils.cpp index e65767a..b9aaba8 100644 --- a/tests/unit/windows/test_winapiutils.cpp +++ b/tests/unit/windows/test_winapiutils.cpp @@ -1,7 +1,8 @@ // local includes #include "displaydevice/windows/winapiutils.h" #include "fixtures.h" -#include "mocks/mockwinapilayer.h" +#include "utils/comparison.h" +#include "utils/mockwinapilayer.h" namespace { // Convenience keywords for GMock @@ -128,57 +129,6 @@ namespace { } // namespace -// Helper comparison operators -bool -operator==(const LUID &lhs, const LUID &rhs) { - return lhs.HighPart == rhs.HighPart && lhs.LowPart == rhs.LowPart; -} - -bool -operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs) { - return lhs.Denominator == rhs.Denominator && lhs.Numerator == rhs.Numerator; -} - -bool -operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs) { - // clang-format off - return lhs.adapterId == rhs.adapterId && - lhs.id == rhs.id && - lhs.cloneGroupId == rhs.cloneGroupId && - lhs.sourceModeInfoIdx == rhs.sourceModeInfoIdx && - lhs.statusFlags == rhs.statusFlags; - // clang-format on -} - -bool -operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs) { - // clang-format off - return lhs.adapterId == rhs.adapterId && - lhs.id == rhs.id && - lhs.desktopModeInfoIdx == rhs.desktopModeInfoIdx && - lhs.targetModeInfoIdx == rhs.targetModeInfoIdx && - lhs.outputTechnology == rhs.outputTechnology && - lhs.rotation == rhs.rotation && - lhs.scaling == rhs.scaling && - lhs.refreshRate == rhs.refreshRate && - lhs.scanLineOrdering == rhs.scanLineOrdering && - lhs.targetAvailable == rhs.targetAvailable && - lhs.statusFlags == rhs.statusFlags; - // clang-format on -} - -bool -operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs) { - return lhs.sourceInfo == rhs.sourceInfo && lhs.targetInfo == rhs.targetInfo && lhs.flags == rhs.flags; -} - -namespace display_device { - bool - operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs) { - return lhs.m_source_id_to_path_index == rhs.m_source_id_to_path_index && lhs.m_adapter_id == rhs.m_adapter_id && lhs.m_active_source == rhs.m_active_source; - } -} // namespace display_device - TEST_F_S_MOCKED(IsAvailable) { DISPLAYCONFIG_PATH_INFO available_path; DISPLAYCONFIG_PATH_INFO unavailable_path; @@ -745,3 +695,10 @@ TEST_F_S_MOCKED(MakePathsForNewTopology, IndexOutOfRange) { const std::vector expected_paths {}; EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, {}), expected_paths); } + +TEST_F_S_MOCKED(MakePathsForNewTopology, EmptyList) { + const display_device::ActiveTopology new_topology {}; + + const std::vector expected_paths {}; + EXPECT_EQ(display_device::win_utils::makePathsForNewTopology(new_topology, EXPECTED_SOURCE_INDEX_DATA, PATHS_WITH_SOURCE_IDS), expected_paths); +} diff --git a/tests/unit/windows/test_windisplaydevicetopology.cpp b/tests/unit/windows/test_windisplaydevicetopology.cpp index 76af5c1..8a5f816 100644 --- a/tests/unit/windows/test_windisplaydevicetopology.cpp +++ b/tests/unit/windows/test_windisplaydevicetopology.cpp @@ -1,24 +1,82 @@ // local includes #include "displaydevice/windows/winapilayer.h" +#include "displaydevice/windows/winapiutils.h" #include "displaydevice/windows/windisplaydevice.h" #include "fixtures.h" -#include "mocks/mockwinapilayer.h" +#include "utils/comparison.h" +#include "utils/guards.h" +#include "utils/mockwinapilayer.h" namespace { // Convenience keywords for GMock using ::testing::_; + using ::testing::InSequence; using ::testing::Return; using ::testing::StrictMock; // Test fixture(s) for this file class WinDisplayDeviceTopology: public BaseTest { public: + std::optional> + getAvailableDevices() { + const auto all_devices { m_layer->queryDisplayConfig(display_device::QueryType::All) }; + if (!all_devices) { + return std::nullopt; + } + + std::set device_ids; + for (const auto &path : all_devices->m_paths) { + const auto device_id { m_layer->getDeviceId(path) }; + if (!device_id.empty()) { + device_ids.insert(device_id); + } + } + + return std::vector { device_ids.begin(), device_ids.end() }; + } + std::shared_ptr m_layer { std::make_shared() }; display_device::WinDisplayDevice m_win_dd { m_layer }; }; class WinDisplayDeviceTopologyMocked: public BaseTest { public: + void + setupExpectCallFor3ActivePathsAndModes(const display_device::QueryType query_type, InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_layer, queryDisplayConfig(query_type)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_3_ACTIVE)) + .RetiresOnSaturation(); + + for (int i = 1; i <= 3; ++i) { + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillOnce(Return("Path" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId" + std::to_string(i))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillOnce(Return("DisplayName" + std::to_string(i))) + .RetiresOnSaturation(); + } + } + + static std::vector + getExpectedPathToBeSet() { + auto path { ut_consts::PAM_3_ACTIVE->m_paths.at(0) }; + + display_device::win_utils::setCloneGroupId(path, 0); + display_device::win_utils::setSourceIndex(path, std::nullopt); + display_device::win_utils::setTargetIndex(path, std::nullopt); + display_device::win_utils::setDesktopIndex(path, std::nullopt); + display_device::win_utils::setActive(path); + + return std::vector { path }; + }; + std::shared_ptr> m_layer { std::make_shared>() }; display_device::WinDisplayDevice m_win_dd { m_layer }; }; @@ -26,9 +84,10 @@ namespace { // Specialized TEST macro(s) for this test file #define TEST_F_S(...) DD_MAKE_TEST(TEST_F, WinDisplayDeviceTopology, __VA_ARGS__) #define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, WinDisplayDeviceTopologyMocked, __VA_ARGS__) + } // namespace -TEST_F_S(GetCurrentTopology, FromSystem) { +TEST_F_S(GetCurrentTopology) { const auto active_devices { m_layer->queryDisplayConfig(display_device::QueryType::Active) }; ASSERT_TRUE(active_devices); @@ -56,21 +115,67 @@ TEST_F_S(GetCurrentTopology, FromSystem) { EXPECT_EQ(flattened_topology, expected_devices); } +TEST_F_S(SetCurrentTopology, ExtendedTopology) { + const auto available_devices { getAvailableDevices() }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 2) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto cleanup_guard { makeTopologyGuard(m_win_dd) }; + + // We are changing to a single device to ensure that we are not in the "final" state + const display_device::ActiveTopology single_device_topology { { available_devices->at(0) } }; + ASSERT_TRUE(m_win_dd.setTopology(single_device_topology)); + + // We are limiting ourselves to 3 devices only to avoid GPU limitation issues (even if very unlikely) + display_device::ActiveTopology multiple_device_topology { { available_devices->at(0) }, { available_devices->at(1) } }; + if (available_devices->size() > 2) { + multiple_device_topology.push_back({ available_devices->at(2) }); + } + ASSERT_TRUE(m_win_dd.setTopology(multiple_device_topology)); +} + +TEST_F_S(SetCurrentTopology, DuplicatedTopology) { + const auto available_devices { getAvailableDevices() }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 2) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto cleanup_guard { makeTopologyGuard(m_win_dd) }; + + // We are changing to a single device to ensure that we are not in the "final" state + const display_device::ActiveTopology single_device_topology { { available_devices->at(0) } }; + ASSERT_TRUE(m_win_dd.setTopology(single_device_topology)); + + display_device::ActiveTopology multiple_device_topology { { available_devices->at(0), available_devices->at(1) } }; + ASSERT_TRUE(m_win_dd.setTopology(multiple_device_topology)); +} + +TEST_F_S(SetCurrentTopology, MixedTopology) { + const auto available_devices { getAvailableDevices() }; + ASSERT_TRUE(available_devices); + + if (available_devices->size() < 3) { + GTEST_SKIP_("Not enough devices are available in the system."); + } + + const auto cleanup_guard { makeTopologyGuard(m_win_dd) }; + + // We are changing to a single device to ensure that we are not in the "final" state + const display_device::ActiveTopology single_device_topology { { available_devices->at(0) } }; + ASSERT_TRUE(m_win_dd.setTopology(single_device_topology)); + + display_device::ActiveTopology multiple_device_topology { { available_devices->at(0), available_devices->at(1) }, { available_devices->at(2) } }; + ASSERT_TRUE(m_win_dd.setTopology(multiple_device_topology)); +} + TEST_F_S_MOCKED(GetCurrentTopology, ExtendedDisplaysOnly) { - EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) - .Times(3) - .WillRepeatedly(Return("PathX")); - EXPECT_CALL(*m_layer, getDisplayName(_)) - .Times(3) - .WillRepeatedly(Return("DisplayNameX")); - EXPECT_CALL(*m_layer, getDeviceId(_)) - .Times(3) - .WillOnce(Return("DeviceId1")) - .WillOnce(Return("DeviceId2")) - .WillOnce(Return("DeviceId3")); - EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) - .Times(1) - .WillOnce(Return(ut_consts::PAM_3_ACTIVE)); + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); const display_device::ActiveTopology expected_topology { { "DeviceId1" }, { "DeviceId2" }, { "DeviceId3" } }; EXPECT_EQ(m_win_dd.getCurrentTopology(), expected_topology); @@ -156,8 +261,8 @@ TEST_F_S_MOCKED(GetCurrentTopology, NullDeviceList) { } TEST_F_S_MOCKED(IsTopologyValid) { - EXPECT_EQ(m_win_dd.isTopologyValid({ /* no groups */ }), false); // TODO: come back to decide whether this can be valid once `set_topology` is implemented - EXPECT_EQ(m_win_dd.isTopologyValid({ { /* empty group */ } }), false); // TODO: come back to decide whether this can be valid once `set_topology` is implemented + EXPECT_EQ(m_win_dd.isTopologyValid({ /* no groups */ }), false); + EXPECT_EQ(m_win_dd.isTopologyValid({ { /* empty group */ } }), false); EXPECT_EQ(m_win_dd.isTopologyValid({ { "ID_1" } }), true); EXPECT_EQ(m_win_dd.isTopologyValid({ { "ID_1" }, { "ID_2" } }), true); EXPECT_EQ(m_win_dd.isTopologyValid({ { "ID_1", "ID_2" } }), true); @@ -182,3 +287,178 @@ TEST_F_S_MOCKED(isTopologyTheSame) { EXPECT_EQ(m_win_dd.isTopologyTheSame({ { "ID_1", "ID_2" }, { "ID_3" } }, { { "ID_2", "ID_1" }, { "ID_3" } }), true); EXPECT_EQ(m_win_dd.isTopologyTheSame({ { "ID_3" }, { "ID_1", "ID_2" } }, { { "ID_2", "ID_1" }, { "ID_3" } }), true); } + +TEST_F_S_MOCKED(SetCurrentTopology) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + auto expected_path { ut_consts::PAM_3_ACTIVE->m_paths.at(0) }; + display_device::win_utils::setCloneGroupId(expected_path, 0); + display_device::win_utils::setSourceIndex(expected_path, std::nullopt); + display_device::win_utils::setTargetIndex(expected_path, std::nullopt); + display_device::win_utils::setDesktopIndex(expected_path, std::nullopt); + display_device::win_utils::setActive(expected_path); + + UINT32 expected_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + EXPECT_CALL(*m_layer, setDisplayConfig(std::vector { expected_path }, std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)); + + // Report only 1 active device + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(display_device::PathAndModeData { + { ut_consts::PAM_3_ACTIVE->m_paths.at(0) }, + { ut_consts::PAM_3_ACTIVE->m_modes.at(0) } })) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getMonitorDevicePath(_)) + .Times(1) + .WillOnce(Return("Path1")) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDeviceId(_)) + .Times(1) + .WillOnce(Return("DeviceId1")) + .RetiresOnSaturation(); + EXPECT_CALL(*m_layer, getDisplayName(_)) + .Times(1) + .WillOnce(Return("DisplayName1")) + .RetiresOnSaturation(); + + EXPECT_TRUE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, InvalidTopologyProvided) { + EXPECT_FALSE(m_win_dd.setTopology({})); +} + +TEST_F_S_MOCKED(SetCurrentTopology, FailedToGetCurrentTopology) { + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, CurrentTopologyIsTheSame) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + + const display_device::ActiveTopology current_topology { { "DeviceId1" }, { "DeviceId2" }, { "DeviceId3" } }; + EXPECT_TRUE(m_win_dd.setTopology(current_topology)); +} + +TEST_F_S_MOCKED(SetCurrentTopology, FailedToQueryForAllDevices) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, DevicePathsAreNoLongerAvailable) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_EMPTY)); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, FailedToMakePathSourceData) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceIdUnknown" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, WindowsDoesNotKnowAboutTheTopology, FailedToSetTopology) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + UINT32 expected_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + EXPECT_CALL(*m_layer, setDisplayConfig(getExpectedPathToBeSet(), std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_GEN_FAILURE)); + + EXPECT_CALL(*m_layer, getErrorString(ERROR_GEN_FAILURE)) + .Times(1) + .WillRepeatedly(Return("ErrorDesc")); + + expected_flags = SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES | SDC_VIRTUAL_MODE_AWARE | SDC_SAVE_TO_DATABASE; + EXPECT_CALL(*m_layer, setDisplayConfig(getExpectedPathToBeSet(), std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_GEN_FAILURE)); + + EXPECT_CALL(*m_layer, getErrorString(ERROR_GEN_FAILURE)) + .Times(1) + .WillRepeatedly(Return("ErrorDesc")); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, FailedToSetTopology, NoRecovery) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + UINT32 expected_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + EXPECT_CALL(*m_layer, setDisplayConfig(getExpectedPathToBeSet(), std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_INVALID_PARAMETER)); + + EXPECT_CALL(*m_layer, getErrorString(ERROR_INVALID_PARAMETER)) + .Times(1) + .WillRepeatedly(Return("ErrorDesc")); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, TopologyWasSetAccordingToWinApi, CouldNotGetCurrentTopologyToVerify) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + UINT32 expected_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + EXPECT_CALL(*m_layer, setDisplayConfig(getExpectedPathToBeSet(), std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)); + + // Called when getting the topology + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::Active)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + // Called when doing the undo + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(SetCurrentTopology, TopologyWasSetAccordingToWinApi, WinApiLied) { + InSequence sequence; + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::All, sequence); + + UINT32 expected_flags { SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_PATH_ORDER_CHANGES | SDC_VIRTUAL_MODE_AWARE }; + EXPECT_CALL(*m_layer, setDisplayConfig(getExpectedPathToBeSet(), std::vector {}, expected_flags)) + .Times(1) + .WillOnce(Return(ERROR_SUCCESS)); + + // Called when getting the topology + setupExpectCallFor3ActivePathsAndModes(display_device::QueryType::Active, sequence); + + // Called when doing the undo + EXPECT_CALL(*m_layer, queryDisplayConfig(display_device::QueryType::All)) + .Times(1) + .WillOnce(Return(ut_consts::PAM_NULL)); + + EXPECT_FALSE(m_win_dd.setTopology({ { "DeviceId1" } })); +} diff --git a/tests/unit/windows/utils/comparison.cpp b/tests/unit/windows/utils/comparison.cpp new file mode 100644 index 0000000..d5e078f --- /dev/null +++ b/tests/unit/windows/utils/comparison.cpp @@ -0,0 +1,114 @@ +// local includes +#include "comparison.h" + +bool +operator==(const LUID &lhs, const LUID &rhs) { + return lhs.HighPart == rhs.HighPart && lhs.LowPart == rhs.LowPart; +} + +bool +operator==(const POINTL &lhs, const POINTL &rhs) { + return lhs.x == rhs.x && lhs.y == rhs.y; +} + +bool +operator==(const RECTL &lhs, const RECTL &rhs) { + return lhs.bottom == rhs.bottom && lhs.left == rhs.left && lhs.right == rhs.right && lhs.top == rhs.top; +} + +bool +operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs) { + return lhs.Denominator == rhs.Denominator && lhs.Numerator == rhs.Numerator; +} + +bool +operator==(const DISPLAYCONFIG_2DREGION &lhs, const DISPLAYCONFIG_2DREGION &rhs) { + return lhs.cx == rhs.cx && lhs.cy == rhs.cy; +} + +bool +operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs) { + // clang-format off + return lhs.adapterId == rhs.adapterId && + lhs.id == rhs.id && + lhs.cloneGroupId == rhs.cloneGroupId && + lhs.sourceModeInfoIdx == rhs.sourceModeInfoIdx && + lhs.statusFlags == rhs.statusFlags; + // clang-format on +} + +bool +operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs) { + // clang-format off + return lhs.adapterId == rhs.adapterId && + lhs.id == rhs.id && + lhs.desktopModeInfoIdx == rhs.desktopModeInfoIdx && + lhs.targetModeInfoIdx == rhs.targetModeInfoIdx && + lhs.outputTechnology == rhs.outputTechnology && + lhs.rotation == rhs.rotation && + lhs.scaling == rhs.scaling && + lhs.refreshRate == rhs.refreshRate && + lhs.scanLineOrdering == rhs.scanLineOrdering && + lhs.targetAvailable == rhs.targetAvailable && + lhs.statusFlags == rhs.statusFlags; + // clang-format on +} + +bool +operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs) { + return lhs.sourceInfo == rhs.sourceInfo && lhs.targetInfo == rhs.targetInfo && lhs.flags == rhs.flags; +} + +bool +operator==(const DISPLAYCONFIG_SOURCE_MODE &lhs, const DISPLAYCONFIG_SOURCE_MODE &rhs) { + return lhs.width == rhs.width && lhs.height == rhs.height && lhs.pixelFormat == rhs.pixelFormat && lhs.position == rhs.position; +} +bool +operator==(const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &lhs, const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &rhs) { + // clang-format on + return lhs.pixelRate == rhs.pixelRate && + lhs.hSyncFreq == rhs.hSyncFreq && + lhs.vSyncFreq == rhs.vSyncFreq && + lhs.activeSize == rhs.activeSize && + lhs.totalSize == rhs.totalSize && + lhs.videoStandard == rhs.videoStandard && + lhs.scanLineOrdering == rhs.scanLineOrdering; + // clang-format oon +} + +bool +operator==(const DISPLAYCONFIG_TARGET_MODE &lhs, const DISPLAYCONFIG_TARGET_MODE &rhs) { + return lhs.targetVideoSignalInfo == rhs.targetVideoSignalInfo; +} + +bool +operator==(const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &lhs, const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &rhs) { + return lhs.PathSourceSize == rhs.PathSourceSize && lhs.DesktopImageRegion == rhs.DesktopImageRegion && lhs.DesktopImageClip == rhs.DesktopImageClip; +} + +bool +operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rhs) { + if (lhs.infoType == rhs.infoType && lhs.id == rhs.id && lhs.adapterId == rhs.adapterId) { + if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE) { + return lhs.sourceMode == rhs.sourceMode; + } + else if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) { + return lhs.targetMode == rhs.targetMode; + } + else if (lhs.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_DESKTOP_IMAGE) { + // TODO: fix once implemented + return false; + } + else { + return true; + } + } + return false; +} + +namespace display_device { + bool + operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs) { + return lhs.m_source_id_to_path_index == rhs.m_source_id_to_path_index && lhs.m_adapter_id == rhs.m_adapter_id && lhs.m_active_source == rhs.m_active_source; + } +} // namespace display_device diff --git a/tests/unit/windows/utils/comparison.h b/tests/unit/windows/utils/comparison.h new file mode 100644 index 0000000..fd8c9a8 --- /dev/null +++ b/tests/unit/windows/utils/comparison.h @@ -0,0 +1,49 @@ +#pragma once + +// local includes +#include "displaydevice/windows/types.h" + +// Helper comparison operators +bool +operator==(const LUID &lhs, const LUID &rhs); + +bool +operator==(const POINTL &lhs, const POINTL &rhs); + +bool +operator==(const RECTL &lhs, const RECTL &rhs); + +bool +operator==(const DISPLAYCONFIG_RATIONAL &lhs, const DISPLAYCONFIG_RATIONAL &rhs); + +bool +operator==(const DISPLAYCONFIG_2DREGION &lhs, const DISPLAYCONFIG_2DREGION &rhs); + +bool +operator==(const DISPLAYCONFIG_PATH_SOURCE_INFO &lhs, const DISPLAYCONFIG_PATH_SOURCE_INFO &rhs); + +bool +operator==(const DISPLAYCONFIG_PATH_TARGET_INFO &lhs, const DISPLAYCONFIG_PATH_TARGET_INFO &rhs); + +bool +operator==(const DISPLAYCONFIG_PATH_INFO &lhs, const DISPLAYCONFIG_PATH_INFO &rhs); + +bool +operator==(const DISPLAYCONFIG_SOURCE_MODE &lhs, const DISPLAYCONFIG_SOURCE_MODE &rhs); + +bool +operator==(const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &lhs, const DISPLAYCONFIG_VIDEO_SIGNAL_INFO &rhs); + +bool +operator==(const DISPLAYCONFIG_TARGET_MODE &lhs, const DISPLAYCONFIG_TARGET_MODE &rhs); + +bool +operator==(const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &lhs, const DISPLAYCONFIG_DESKTOP_IMAGE_INFO &rhs); + +bool +operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rhs); + +namespace display_device { + bool + operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs); +} // namespace display_device diff --git a/tests/unit/windows/utils/guards.h b/tests/unit/windows/utils/guards.h new file mode 100644 index 0000000..8130166 --- /dev/null +++ b/tests/unit/windows/utils/guards.h @@ -0,0 +1,15 @@ +#pragma once + +// system includes +#include + +// local includes +#include "displaydevice/windows/windisplaydevice.h" + +// Helper functions to make guards for restoring previous state +auto +makeTopologyGuard(display_device::WinDisplayDevice &win_dd) { + return boost::scope::make_scope_exit([&win_dd, topology = win_dd.getCurrentTopology()]() { + static_cast(win_dd.setTopology(topology)); + }); +} diff --git a/tests/unit/windows/mocks/mockwinapilayer.cpp b/tests/unit/windows/utils/mockwinapilayer.cpp similarity index 88% rename from tests/unit/windows/mocks/mockwinapilayer.cpp rename to tests/unit/windows/utils/mockwinapilayer.cpp index ec0b0d4..f03231e 100644 --- a/tests/unit/windows/mocks/mockwinapilayer.cpp +++ b/tests/unit/windows/utils/mockwinapilayer.cpp @@ -11,6 +11,8 @@ namespace { data.m_paths.push_back({}); data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); + data.m_paths.back().sourceInfo.adapterId = { 1, 1 }; + data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; data.m_modes.push_back({}); @@ -26,6 +28,8 @@ namespace { data.m_paths.push_back({}); data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); + data.m_paths.back().sourceInfo.adapterId = { 2, 2 }; + data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; data.m_modes.push_back({}); @@ -39,6 +43,8 @@ namespace { data.m_paths.push_back({}); data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); + data.m_paths.back().sourceInfo.adapterId = { 3, 3 }; + data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; data.m_modes.push_back({}); @@ -55,6 +61,8 @@ namespace { data.m_paths.push_back({}); data.m_paths.back().flags = DISPLAYCONFIG_PATH_ACTIVE; data.m_paths.back().sourceInfo.sourceModeInfoIdx = data.m_modes.size(); + data.m_paths.back().sourceInfo.adapterId = { 4, 4 }; + data.m_paths.back().sourceInfo.id = 0; data.m_paths.back().targetInfo.targetAvailable = TRUE; data.m_modes.push_back({}); @@ -71,7 +79,7 @@ namespace { namespace ut_consts { const std::optional PAM_NULL { std::nullopt }; - const std::optional PAM_EMPTY {}; + const std::optional PAM_EMPTY { display_device::PathAndModeData {} }; const std::optional PAM_3_ACTIVE { make3ActiveDeviceGroups(false) }; const std::optional PAM_3_ACTIVE_WITH_INVALID_MODE_IDX { []() { auto data { make3ActiveDeviceGroups(false) }; diff --git a/tests/unit/windows/mocks/mockwinapilayer.h b/tests/unit/windows/utils/mockwinapilayer.h similarity index 91% rename from tests/unit/windows/mocks/mockwinapilayer.h rename to tests/unit/windows/utils/mockwinapilayer.h index 880c9cc..c7c2cc1 100644 --- a/tests/unit/windows/mocks/mockwinapilayer.h +++ b/tests/unit/windows/utils/mockwinapilayer.h @@ -15,6 +15,7 @@ namespace display_device { MOCK_METHOD(std::string, getMonitorDevicePath, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); MOCK_METHOD(std::string, getFriendlyName, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); MOCK_METHOD(std::string, getDisplayName, (const DISPLAYCONFIG_PATH_INFO &), (const, override)); + MOCK_METHOD(LONG, setDisplayConfig, (std::vector, std::vector, UINT32), (override)); }; } // namespace display_device