diff --git a/src/common/include/displaydevice/audiocontextinterface.h b/src/common/include/displaydevice/audiocontextinterface.h index e2869b6..84ddb55 100644 --- a/src/common/include/displaydevice/audiocontextinterface.h +++ b/src/common/include/displaydevice/audiocontextinterface.h @@ -1,9 +1,5 @@ #pragma once -// system includes -#include -#include - namespace display_device { /** * @brief A class for capturing associated audio context (settings, info or whatever). @@ -11,11 +7,6 @@ namespace display_device { * Some of the display devices have audio devices associated with them. * Turning off and on the devices will not necessarily restore them as the default * audio devices for the system. - * - * While this library does not preserve audio contexts, it does provide this interface - * for notifying which devices are about to be disabled (their audio context should - * probably be "captured") and which ones are to be re-enabled (their audio context should - * probably be "released"). */ class AudioContextInterface { public: @@ -25,32 +16,41 @@ namespace display_device { virtual ~AudioContextInterface() = default; /** - * @brief Capture audio context for the devices that are about to be disabled. - * @param devices_to_be_disabled Devices that might be disabled soon. + * @brief Capture audio context for currently active devices. * @returns True if the contexts could be captured, false otherwise. * * EXAMPLES: * ```cpp - * const std::string device_id { "MY_DEVICE_ID" }; * AudioContextInterface* iface = getIface(...); - * const auto result { iface->capture({ device_id }) }; + * const auto result { iface->capture() }; + * ``` + */ + [[nodiscard]] virtual bool + capture() = 0; + + /** + * @brief Check if the context is already captured. + * @returns True if the context is captured, false otherwise. + * + * EXAMPLES: + * ```cpp + * AudioContextInterface* iface = getIface(...); + * const auto result { iface->isCaptured() }; * ``` */ [[nodiscard]] virtual bool - capture(const std::vector &devices_to_be_disabled) = 0; + isCaptured() const = 0; /** - * @brief Release audio context for the devices that are about to be re-enabled. - * @param devices_to_be_reenabled Devices that were captured before and are about to be re-enabled. + * @brief Release captured audio context for the devices (if any). * * EXAMPLES: * ```cpp - * const std::string device_id { "MY_DEVICE_ID" }; * AudioContextInterface* iface = getIface(...); - * const auto result { iface->release({ device_id }) }; + * const auto result { iface->release() }; * ``` */ virtual void - release(const std::vector &devices_to_be_reenabled) = 0; + release() = 0; }; } // namespace display_device diff --git a/src/common/include/displaydevice/json.h b/src/common/include/displaydevice/json.h index b97670a..58c5c14 100644 --- a/src/common/include/displaydevice/json.h +++ b/src/common/include/displaydevice/json.h @@ -1,5 +1,8 @@ #pragma once +// system includes +#include + // local includes #include "types.h" @@ -18,6 +21,11 @@ // Shared converters (add as needed) namespace display_device { + extern const std::optional JSON_COMPACT; + DD_JSON_DECLARE_CONVERTER(EnumeratedDeviceList) DD_JSON_DECLARE_CONVERTER(SingleDisplayConfiguration) + DD_JSON_DECLARE_CONVERTER(std::set) + DD_JSON_DECLARE_CONVERTER(std::string) + DD_JSON_DECLARE_CONVERTER(bool) } // namespace display_device diff --git a/src/common/include/displaydevice/noopaudiocontext.h b/src/common/include/displaydevice/noopaudiocontext.h index 3096fe5..b9357a0 100644 --- a/src/common/include/displaydevice/noopaudiocontext.h +++ b/src/common/include/displaydevice/noopaudiocontext.h @@ -9,12 +9,19 @@ namespace display_device { */ class NoopAudioContext: public AudioContextInterface { public: - /** Always returns true. */ + /** Always returns true and sets m_is_captured to true. */ [[nodiscard]] bool - capture(const std::vector &) override; + capture() override; - /** Does nothing. */ + /** Returns the m_is_captured value. */ + [[nodiscard]] bool + isCaptured() const override; + + /** Sets m_is_captured to false. */ void - release(const std::vector &) override; + release() override; + + private: + bool m_is_captured {}; }; } // namespace display_device diff --git a/src/common/include/displaydevice/noopsettingspersistence.h b/src/common/include/displaydevice/noopsettingspersistence.h index 3bea753..9f79610 100644 --- a/src/common/include/displaydevice/noopsettingspersistence.h +++ b/src/common/include/displaydevice/noopsettingspersistence.h @@ -17,8 +17,8 @@ namespace display_device { [[nodiscard]] std::optional> load() const override; - /** Does nothing. */ - void + /** Always returns true. */ + [[nodiscard]] bool clear() override; }; } // namespace display_device diff --git a/src/common/include/displaydevice/settingsmanagerinterface.h b/src/common/include/displaydevice/settingsmanagerinterface.h index 93343dd..d663a72 100644 --- a/src/common/include/displaydevice/settingsmanagerinterface.h +++ b/src/common/include/displaydevice/settingsmanagerinterface.h @@ -13,7 +13,10 @@ namespace display_device { * @brief Outcome values when trying to apply settings. */ enum class ApplyResult { - Ok + Ok, + ApiTemporarilyUnavailable, + DevicePrepFailed, + PersistenceSaveFailed, }; /** @@ -28,7 +31,8 @@ namespace display_device { * * EXAMPLES: * ```cpp - * const auto devices { enumAvailableDevices() }; + * const SettingsManagerInterface* iface = getIface(...); + * const auto devices { iface->enumAvailableDevices() }; * ``` */ [[nodiscard]] virtual EnumeratedDeviceList @@ -43,7 +47,7 @@ namespace display_device { * EXAMPLES: * ```cpp * const std::string device_id { "MY_DEVICE_ID" }; - * const WinDisplayDeviceInterface* iface = getIface(...); + * const SettingsManagerInterface* iface = getIface(...); * const std::string display_name = iface->getDisplayName(device_id); * ``` */ @@ -63,8 +67,8 @@ namespace display_device { * const auto result = iface->applySettings(config); * ``` */ - //[[nodiscard]] virtual ApplyResult - // applySettings(const SingleDisplayConfiguration &config) = 0; + [[nodiscard]] virtual ApplyResult + applySettings(const SingleDisplayConfiguration &config) = 0; /** * @brief Revert the applied configuration and restore the previous settings. @@ -76,8 +80,8 @@ namespace display_device { * const auto result = iface->revertSettings(); * ``` */ - //[[nodiscard]] virtual bool - // revertSettings() = 0; + [[nodiscard]] virtual bool + revertSettings() = 0; /** * @brief Reset the persistence in case the settings cannot be reverted. diff --git a/src/common/include/displaydevice/settingspersistenceinterface.h b/src/common/include/displaydevice/settingspersistenceinterface.h index 0ae095d..496bc21 100644 --- a/src/common/include/displaydevice/settingspersistenceinterface.h +++ b/src/common/include/displaydevice/settingspersistenceinterface.h @@ -52,10 +52,10 @@ namespace display_device { * EXAMPLES: * ```cpp * SettingsPersistenceInterface* iface = getIface(...); - * iface->clear(); + * const auto result = iface->clear(); * ``` */ - virtual void + [[nodiscard]] virtual bool clear() = 0; }; } // namespace display_device diff --git a/src/common/include/displaydevice/types.h b/src/common/include/displaydevice/types.h index 1301685..457bec0 100644 --- a/src/common/include/displaydevice/types.h +++ b/src/common/include/displaydevice/types.h @@ -20,6 +20,12 @@ namespace display_device { struct Resolution { unsigned int m_width {}; unsigned int m_height {}; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Resolution &lhs, const Resolution &rhs); }; /** @@ -28,6 +34,12 @@ namespace display_device { struct Point { int m_x {}; int m_y {}; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Point &lhs, const Point &rhs); }; /** @@ -44,12 +56,24 @@ namespace display_device { bool m_primary {}; /**< Indicates whether the device is a primary display. */ Point m_origin_point {}; /**< A starting point of the display. */ std::optional m_hdr_state {}; /**< HDR of an active device. */ + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Info &lhs, const Info &rhs); }; std::string m_device_id {}; /**< A unique device ID used by this API to identify the device. */ std::string m_display_name {}; /**< A logical name representing given by the OS for a display. */ std::string m_friendly_name {}; /**< A human-readable name for the device. */ std::optional m_info {}; /**< Additional information about an active display device. */ + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs); }; /** @@ -74,10 +98,16 @@ namespace display_device { EnsureOnlyDisplay /**< Deactivate other displays and turn on the specified one only. */ }; - std::string m_device_id {}; /**< Device id manually provided by the user via config. */ + std::string m_device_id {}; /**< Device to perform configuration for (can be empty if primary device should be used). */ DevicePreparation m_device_prep {}; /**< Instruction on how to prepare device. */ std::optional m_resolution {}; /**< Resolution to configure. */ std::optional m_refresh_rate {}; /**< Refresh rate to configure. */ std::optional m_hdr_state {}; /**< HDR state to configure (if supported by the display). */ + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs); }; } // namespace display_device diff --git a/src/common/json.cpp b/src/common/json.cpp index 0c1d301..42dd907 100644 --- a/src/common/json.cpp +++ b/src/common/json.cpp @@ -11,4 +11,7 @@ namespace display_device { DD_JSON_DEFINE_CONVERTER(EnumeratedDeviceList) DD_JSON_DEFINE_CONVERTER(SingleDisplayConfiguration) + DD_JSON_DEFINE_CONVERTER(std::set) + DD_JSON_DEFINE_CONVERTER(std::string) + DD_JSON_DEFINE_CONVERTER(bool) } // namespace display_device diff --git a/src/common/noopaudiocontext.cpp b/src/common/noopaudiocontext.cpp index 5e32e38..30b7c7f 100644 --- a/src/common/noopaudiocontext.cpp +++ b/src/common/noopaudiocontext.cpp @@ -3,11 +3,18 @@ namespace display_device { bool - NoopAudioContext::capture(const std::vector &) { + NoopAudioContext::capture() { + m_is_captured = true; return true; } + bool + NoopAudioContext::isCaptured() const { + return m_is_captured; + } + void - NoopAudioContext::release(const std::vector &) { + NoopAudioContext::release() { + m_is_captured = false; } } // namespace display_device diff --git a/src/common/noopsettingspersistence.cpp b/src/common/noopsettingspersistence.cpp index 7e77f00..b07c9d5 100644 --- a/src/common/noopsettingspersistence.cpp +++ b/src/common/noopsettingspersistence.cpp @@ -12,7 +12,8 @@ namespace display_device { return std::vector {}; } - void + bool NoopSettingsPersistence::clear() { + return true; } } // namespace display_device diff --git a/tests/fixtures/comparison.cpp b/src/common/types.cpp similarity index 86% rename from tests/fixtures/comparison.cpp rename to src/common/types.cpp index 288052c..b60314c 100644 --- a/tests/fixtures/comparison.cpp +++ b/src/common/types.cpp @@ -1,10 +1,12 @@ // local includes -#include "fixtures/comparison.h" +#include "displaydevice/types.h" -bool -fuzzyCompare(float lhs, float rhs) { - return std::abs(lhs - rhs) * 100000.f <= std::min(std::abs(lhs), std::abs(rhs)); -} +namespace { + bool + fuzzyCompare(const float lhs, const float rhs) { + return std::abs(lhs - rhs) * 100000.f <= std::min(std::abs(lhs), std::abs(rhs)); + } +} // namespace namespace display_device { bool diff --git a/src/windows/include/displaydevice/windows/detail/jsonserializer.h b/src/windows/include/displaydevice/windows/detail/jsonserializer.h index fa1c823..d1574ef 100644 --- a/src/windows/include/displaydevice/windows/detail/jsonserializer.h +++ b/src/windows/include/displaydevice/windows/detail/jsonserializer.h @@ -8,5 +8,8 @@ namespace display_device { // Structs DD_JSON_DECLARE_SERIALIZE_TYPE(Rational) DD_JSON_DECLARE_SERIALIZE_TYPE(DisplayMode) + DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Initial) + DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState::Modified) + DD_JSON_DECLARE_SERIALIZE_TYPE(SingleDisplayConfigState) } // namespace display_device #endif diff --git a/src/windows/include/displaydevice/windows/json.h b/src/windows/include/displaydevice/windows/json.h index 92b1491..2a48ecd 100644 --- a/src/windows/include/displaydevice/windows/json.h +++ b/src/windows/include/displaydevice/windows/json.h @@ -9,4 +9,5 @@ namespace display_device { DD_JSON_DECLARE_CONVERTER(ActiveTopology) DD_JSON_DECLARE_CONVERTER(DeviceDisplayModeMap) DD_JSON_DECLARE_CONVERTER(HdrStateMap) + DD_JSON_DECLARE_CONVERTER(SingleDisplayConfigState) } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/persistentstate.h b/src/windows/include/displaydevice/windows/persistentstate.h new file mode 100644 index 0000000..479e37a --- /dev/null +++ b/src/windows/include/displaydevice/windows/persistentstate.h @@ -0,0 +1,45 @@ +#pragma once + +// system includes +#include + +// local includes +#include "displaydevice/settingspersistenceinterface.h" +#include "displaydevice/windows/windisplaydeviceinterface.h" + +namespace display_device { + /** + * @brief A simple wrapper around the SettingsPersistenceInterface and cached local state to keep them in sync. + */ + class PersistentState { + public: + /** + * Default constructor for the class. + * @param settings_persistence_api [Optional] A pointer to the Settings Persistence interface. + * @param fallback_state Fallback state to be used if the persistence interface fails to load data (or it is invalid). + * If no fallback is provided in such case, the constructor will throw. + */ + explicit PersistentState(std::shared_ptr settings_persistence_api, const std::optional &fallback_state = SingleDisplayConfigState {}); + + /** + * @brief Store the new state via the interface and cache it. + * @param state New state to be set. + * @return True if the state was succesfully updated, false otherwise. + */ + [[nodiscard]] bool + persistState(const std::optional &state); + + /** + * @brief Get cached state. + * @return Cached state + */ + [[nodiscard]] const std::optional & + getState() const; + + protected: + std::shared_ptr m_settings_persistence_api; + + private: + std::optional m_cached_state; + }; +} // namespace display_device diff --git a/src/windows/include/displaydevice/windows/settingsmanager.h b/src/windows/include/displaydevice/windows/settingsmanager.h index ce00bd1..3c1da4b 100644 --- a/src/windows/include/displaydevice/windows/settingsmanager.h +++ b/src/windows/include/displaydevice/windows/settingsmanager.h @@ -6,8 +6,8 @@ // local includes #include "displaydevice/audiocontextinterface.h" #include "displaydevice/settingsmanagerinterface.h" -#include "displaydevice/settingspersistenceinterface.h" #include "displaydevice/windows/windisplaydeviceinterface.h" +#include "persistentstate.h" namespace display_device { /** @@ -18,13 +18,13 @@ namespace display_device { /** * Default constructor for the class. * @param dd_api A pointer to the Windows Display Device interface. Will throw on nullptr! - * @param settings_persistence_api [Optional] A pointer to the Settings Persistence interface. * @param audio_context_api [Optional] A pointer to the Audio Context interface. + * @param persistent_state A pointer to a class for managing persistence. */ explicit SettingsManager( std::shared_ptr dd_api, - std::shared_ptr settings_persistence_api, - std::shared_ptr audio_context_api); + std::shared_ptr audio_context_api, + std::unique_ptr persistent_state); /** For details @see SettingsManagerInterface::enumAvailableDevices */ [[nodiscard]] EnumeratedDeviceList @@ -34,9 +34,36 @@ namespace display_device { [[nodiscard]] std::string getDisplayName(const std::string &device_id) const override; + /** For details @see SettingsManagerInterface::applySettings */ + [[nodiscard]] ApplyResult + applySettings(const SingleDisplayConfiguration &config) override; + + /** For details @see SettingsManagerInterface::revertSettings */ + [[nodiscard]] bool + revertSettings() override; + protected: + /** + * @brief Preps the topology so that the further settings could be applied. + * @param config Configuration to be used for preparing topology. + * @param topology_before_changes The current topology before any changes. + * @param release_context Specifies whether the audio context should be released at the very end IF everything else has succeeded. + * @return A tuple of (new_state that is to be updated/persisted, device_to_configure, additional_devices_to_configure). + */ + [[nodiscard]] std::optional>> + prepareTopology(const SingleDisplayConfiguration &config, const ActiveTopology &topology_before_changes, bool &release_context); + + /** + * @brief Try to revert the modified settings. + * @returns True on success, false otherwise. + * @warning The method assumes that the caller will ensure restoring the topology + * in case of a failure! + */ + [[nodiscard]] bool + revertModifiedSettings(); + std::shared_ptr m_dd_api; - std::shared_ptr m_settings_persistence_api; std::shared_ptr m_audio_context_api; + std::unique_ptr m_persistence_state; }; } // namespace display_device diff --git a/src/windows/include/displaydevice/windows/settingsutils.h b/src/windows/include/displaydevice/windows/settingsutils.h new file mode 100644 index 0000000..c4d3791 --- /dev/null +++ b/src/windows/include/displaydevice/windows/settingsutils.h @@ -0,0 +1,145 @@ +#pragma once + +// system includes +#include + +// local includes +#include "types.h" +#include "windisplaydeviceinterface.h" + +/** + * @brief Shared "utility-level" code for Settings. + */ +namespace display_device::win_utils { + /** + * @brief Get all the device its in the topology. + * @param topology Topology to be "flattened". + * @return Device ids found in the topology. + * + * EXAMPLES: + * ```cpp + * const ActiveTopology topology { { "DeviceId1", "DeviceId2" }, { "DeviceId3" } }; + * const auto device_ids { flattenTopology(topology) }; + * ``` + */ + std::set + flattenTopology(const ActiveTopology &topology); + + /** + * @brief Compute the new intial state from arbitrary data. + * @param prev_state Previous initial state if available. + * @param topology_before_changes Topology before any changes were made. + * @param devices Currently available device list. + * @return New initial state that should be used. + * + * EXAMPLES: + * ```cpp + * const SingleDisplayConfigState::Initial prev_state { { { "DeviceId1", "DeviceId2" }, { "DeviceId3" } } }; + * const ActiveTopology topology_before_changes { { "DeviceId3" } }; + * const EnumeratedDeviceList devices { ... }; + * + * const auto new_initial_state { computeInitialState(prev_state, topology_before_changes, devices) }; + * ``` + */ + std::optional + computeInitialState(const std::optional &prev_state, + const ActiveTopology &topology_before_changes, + const EnumeratedDeviceList &devices); + + /** + * @brief Strip the initial state of non-existing devices. + * @param initial_state State to be stripped. + * @param devices Currently available device list. + * @return Stripped initial state. + */ + std::optional + stripInitialState(const SingleDisplayConfigState::Initial &initial_state, const EnumeratedDeviceList &devices); + + /** + * @brief Compute new topology from arbitrary. + * @param device_prep Specify how to to compute the new topology. + * @param configuring_primary_devices Specify whether the `device_to_configure` was unspecified (primary device was selected). + * @param device_to_configure Main device to be configured. + * @param additional_devices_to_configure Additional devices that belong to the same group as `device_to_configure`. + * @param initial_topology The initial topology from `computeInitialState(...)`. + * @return New topology that should be set. + */ + ActiveTopology + computeNewTopology(SingleDisplayConfiguration::DevicePreparation device_prep, + bool configuring_primary_devices, + const std::string &device_to_configure, + const std::set &additional_devices_to_configure, + const ActiveTopology &initial_topology); + + /** + * @brief Compute new topology + metadata from config settings and initial state. + * @param device_prep Specify how to to compute the new topology. + * @param device_id Specify which device whould be used for computation (can be empty if primary device should be used). + * @param initial_state The initial topology from `computeInitialState(...)` or `stripInitialState(...)` (or both). + * @return A tuple of (new_topology, device_to_configure, addotional_devices_to_configure). + */ + std::tuple> + computeNewTopologyAndMetadata(SingleDisplayConfiguration::DevicePreparation device_prep, + const std::string &device_id, + const SingleDisplayConfigState::Initial &initial_state); + + /** + * @brief Make guard function for the topology. + * @param win_dd Interface for interacting with the OS. + * @param topology Topology to be used when making a guard. + * @return Function that once called will try to revert topology to the provided one. + * + * EXAMPLES: + * ```cpp + * WinDisplayDeviceInterface* iface = getIface(...); + * boost::scope::scope_exit guard { topologyGuardFn(*iface, iface->getCurrentTopology()) }; + * ``` + */ + DdGuardFn + topologyGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology); + + /** + * @brief Make guard function for the display modes. + * @param win_dd Interface for interacting with the OS. + * @param topology Topology to be used when making a guard. + * @return Function that once called will try to revert display modes to the ones that were initially set for the provided topology. + * + * EXAMPLES: + * ```cpp + * WinDisplayDeviceInterface* iface = getIface(...); + * boost::scope::scope_exit guard { modeGuardFn(*iface, iface->getCurrentTopology()) }; + * ``` + */ + DdGuardFn + modeGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology); + + /** + * @brief Make guard function for the primary display. + * @param win_dd Interface for interacting with the OS. + * @param topology Topology to be used when making a guard. + * @return Function that once called will try to revert primary display to the one that was initially set for the provided topology. + * + * EXAMPLES: + * ```cpp + * WinDisplayDeviceInterface* iface = getIface(...); + * boost::scope::scope_exit guard { primaryGuardFn(*iface, iface->getCurrentTopology()) }; + * ``` + */ + DdGuardFn + primaryGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology); + + /** + * @brief Make guard function for the HDR states. + * @param win_dd Interface for interacting with the OS. + * @param topology Topology to be used when making a guard. + * @return Function that once called will try to revert HDR states to the ones that were initially set for the provided topology. + * + * EXAMPLES: + * ```cpp + * WinDisplayDeviceInterface* iface = getIface(...); + * boost::scope::scope_exit guard { hdrStateGuardFn(*iface, iface->getCurrentTopology()) }; + * ``` + */ + DdGuardFn + hdrStateGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology); +} // namespace display_device::win_utils diff --git a/src/windows/include/displaydevice/windows/types.h b/src/windows/include/displaydevice/windows/types.h index ae53232..080f19b 100644 --- a/src/windows/include/displaydevice/windows/types.h +++ b/src/windows/include/displaydevice/windows/types.h @@ -4,7 +4,9 @@ #include // system includes +#include #include +#include // local includes #include "displaydevice/types.h" @@ -81,6 +83,12 @@ namespace display_device { struct Rational { unsigned int m_numerator {}; unsigned int m_denominator {}; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Rational &lhs, const Rational &rhs); }; /** @@ -89,6 +97,12 @@ namespace display_device { struct DisplayMode { Resolution m_resolution {}; Rational m_refresh_rate {}; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const DisplayMode &lhs, const DisplayMode &rhs); }; /** @@ -100,4 +114,70 @@ namespace display_device { * @brief Ordered map of [DEVICE_ID -> std::optional]. */ using HdrStateMap = std::map>; + + /** + * @brief Arbitrary data for making and undoing changes. + */ + struct SingleDisplayConfigState { + /** + * @brief Data that represents the original system state and is used + * as a base when trying to re-apply settings without reverting settings. + */ + struct Initial { + ActiveTopology m_topology {}; + std::set m_primary_devices {}; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Initial &lhs, const Initial &rhs); + }; + + /** + * @brief Data for tracking the modified changes. + */ + struct Modified { + ActiveTopology m_topology {}; + DeviceDisplayModeMap m_original_modes {}; + HdrStateMap m_original_hdr_states {}; + std::string m_original_primary_device {}; + + /** + * @brief Check if the changed topology has any other modifications. + * @return True if DisplayMode, HDR or primary device has been changed, false otherwise. + * + * EXAMPLES: + * ```cpp + * SingleDisplayConfigState state; + * const no_modifications = state.hasModifications(); + * + * state.modified.original_primary_device = "DeviceId2"; + * const has_modifications = state.hasModifications(); + * ``` + */ + [[nodiscard]] bool + hasModifications() const; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const Modified &lhs, const Modified &rhs); + }; + + Initial m_initial; + Modified m_modified; + + /** + * @brief Comparator for strict equality. + */ + friend bool + operator==(const SingleDisplayConfigState &lhs, const SingleDisplayConfigState &rhs); + }; + + /** + * @brief Default function type used for cleanup/guard functions. + */ + using DdGuardFn = std::function; } // namespace display_device diff --git a/src/windows/json.cpp b/src/windows/json.cpp index d35e17a..bd5bbdd 100644 --- a/src/windows/json.cpp +++ b/src/windows/json.cpp @@ -9,7 +9,10 @@ // clang-format on namespace display_device { + const std::optional JSON_COMPACT { std::nullopt }; + DD_JSON_DEFINE_CONVERTER(ActiveTopology) DD_JSON_DEFINE_CONVERTER(DeviceDisplayModeMap) DD_JSON_DEFINE_CONVERTER(HdrStateMap) + DD_JSON_DEFINE_CONVERTER(SingleDisplayConfigState) } // namespace display_device diff --git a/src/windows/jsonserializer.cpp b/src/windows/jsonserializer.cpp index 75ca9ee..ee9c9e3 100644 --- a/src/windows/jsonserializer.cpp +++ b/src/windows/jsonserializer.cpp @@ -9,4 +9,7 @@ namespace display_device { // Structs DD_JSON_DEFINE_SERIALIZE_STRUCT(Rational, numerator, denominator) DD_JSON_DEFINE_SERIALIZE_STRUCT(DisplayMode, resolution, refresh_rate) + DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Initial, topology, primary_devices) + DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState::Modified, topology, original_modes, original_hdr_states, original_primary_device) + DD_JSON_DEFINE_SERIALIZE_STRUCT(SingleDisplayConfigState, initial, modified) } // namespace display_device diff --git a/src/windows/persistentstate.cpp b/src/windows/persistentstate.cpp new file mode 100644 index 0000000..b135dc0 --- /dev/null +++ b/src/windows/persistentstate.cpp @@ -0,0 +1,73 @@ +// class header include +#include "displaydevice/windows/persistentstate.h" + +// local includes +#include "displaydevice/logging.h" +#include "displaydevice/noopsettingspersistence.h" +#include "displaydevice/windows/json.h" + +namespace display_device { + PersistentState::PersistentState(std::shared_ptr settings_persistence_api, const std::optional &fallback_state): + m_settings_persistence_api { std::move(settings_persistence_api) } { + if (!m_settings_persistence_api) { + m_settings_persistence_api = std::make_shared(); + } + + std::string error_message; + if (const auto persistent_settings { m_settings_persistence_api->load() }) { + if (!persistent_settings->empty()) { + m_cached_state = SingleDisplayConfigState {}; + if (!fromJson({ std::begin(*persistent_settings), std::end(*persistent_settings) }, *m_cached_state, &error_message)) { + error_message = "Failed to parse persistent settings! Error:\n" + error_message; + } + } + } + else { + error_message = "Failed to load persistent settings!"; + } + + if (!error_message.empty()) { + if (!fallback_state) { + throw std::runtime_error { error_message }; + } + + m_cached_state = *fallback_state; + } + } + + bool + PersistentState::persistState(const std::optional &state) { + if (m_cached_state == state) { + return true; + } + + if (!state) { + if (!m_settings_persistence_api->clear()) { + return false; + } + + m_cached_state = std::nullopt; + return true; + } + + bool success { false }; + const auto json_string { toJson(*state, 2, &success) }; + if (!success) { + DD_LOG(error) << "Failed to serialize new persistent state! Error:\n" + << json_string; + return false; + } + + if (!m_settings_persistence_api->store({ std::begin(json_string), std::end(json_string) })) { + return false; + } + + m_cached_state = *state; + return true; + } + + const std::optional & + PersistentState::getState() const { + return m_cached_state; + } +} // namespace display_device diff --git a/src/windows/settingsmanagerapply.cpp b/src/windows/settingsmanagerapply.cpp new file mode 100644 index 0000000..51fefd7 --- /dev/null +++ b/src/windows/settingsmanagerapply.cpp @@ -0,0 +1,173 @@ +// class header include +#include "displaydevice/windows/settingsmanager.h" + +// system includes +#include +#include + +// local includes +#include "displaydevice/logging.h" +#include "displaydevice/windows/json.h" +#include "displaydevice/windows/settingsutils.h" + +namespace display_device { + SettingsManager::ApplyResult + SettingsManager::applySettings(const SingleDisplayConfiguration &config) { + const auto api_access { m_dd_api->isApiAccessAvailable() }; + DD_LOG(info) << "Trying to apply display device settings. API is available: " << toJson(api_access); + + if (!api_access) { + return ApplyResult::ApiTemporarilyUnavailable; + } + DD_LOG(info) << "Using the following configuration:\n" + << toJson(config); + + const auto topology_before_changes { m_dd_api->getCurrentTopology() }; + if (!m_dd_api->isTopologyValid(topology_before_changes)) { + DD_LOG(error) << "Retrieved current topology is invalid:\n" + << toJson(topology_before_changes); + return ApplyResult::DevicePrepFailed; + } + DD_LOG(info) << "Active topology before any changes:\n" + << toJson(topology_before_changes); + + bool release_context { false }; + boost::scope::scope_exit topology_prep_guard { [this, topology = topology_before_changes, was_captured = m_audio_context_api->isCaptured(), &release_context]() { + // It is possible that during topology preparation, some settings will be reverted for the modified topology. + // To keel it simple, these settings will not be restored! + const auto result { m_dd_api->setTopology(topology) }; + if (!result) { + DD_LOG(error) << "Failed to revert back to topology in the topology guard!"; + if (release_context) { + // We are currently in the topology for which the context was captured. + // We have also failed to revert back to some previous one, so we remain in this topology for + // which we have context. There is no reason to keep it around then... + m_audio_context_api->release(); + } + } + + if (!was_captured && m_audio_context_api->isCaptured()) { + // We only want to release context that was not captured before. + m_audio_context_api->release(); + } + } }; + + const auto &prepped_topology_data { prepareTopology(config, topology_before_changes, release_context) }; + if (!prepped_topology_data) { + // Error already logged + return ApplyResult::DevicePrepFailed; + } + const auto &[new_state, device_to_configure, additional_devices_to_configure] = *prepped_topology_data; + + // TODO: + // + // Other device handling goes here that will use device_to_configure and additional_devices_to_configure: + // + // - handle primary device + // - handle display modes + // - handle HDR (need to replicate the HDR bug and find the best place for workaround) + // + + // We will always keep the new state persistently, even if there are no new meaningful changes, because + // we want to preserve the initial state for consistency. + if (!m_persistence_state->persistState(new_state)) { + DD_LOG(error) << "Failed to save reverted settings! Undoing everything..."; + return ApplyResult::PersistenceSaveFailed; + } + + // We can only release the context now as nothing else can fail. + if (release_context) { + m_audio_context_api->release(); + } + + // Disable all guards before returning + topology_prep_guard.set_active(false); + return ApplyResult::Ok; + } + + std::optional>> + SettingsManager::prepareTopology(const SingleDisplayConfiguration &config, const ActiveTopology &topology_before_changes, bool &release_context) { + const EnumeratedDeviceList devices { m_dd_api->enumAvailableDevices() }; + if (devices.empty()) { + DD_LOG(error) << "Failed to enumerate display devices!"; + return std::nullopt; + } + DD_LOG(info) << "Currently available devices:\n" + << toJson(devices); + + if (!config.m_device_id.empty()) { + auto device_it { std::ranges::find_if(devices, [device_id = config.m_device_id](const auto &item) { return item.m_device_id == device_id; }) }; + if (device_it == std::end(devices)) { + // Do not use toJson in case the user entered some BS string... + DD_LOG(error) << "Device \"" << config.m_device_id << "\" is not available in the system!"; + return std::nullopt; + } + } + + const auto &cached_state { m_persistence_state->getState() }; + const auto new_initial_state { win_utils::computeInitialState(cached_state ? std::make_optional(cached_state->m_initial) : std::nullopt, topology_before_changes, devices) }; + if (!new_initial_state) { + // Error already logged + return std::nullopt; + } + SingleDisplayConfigState new_state { *new_initial_state }; + + // In case some devices are no longer available in the system, we could try to strip them from the initial state + // and hope that we are still "safe" to make further changes (to be determined by computeNewTopologyAndMetadata call below). + const auto stripped_initial_state { win_utils::stripInitialState(new_state.m_initial, devices) }; + if (!stripped_initial_state) { + // Error already logged + return std::nullopt; + } + + const auto &[new_topology, device_to_configure, additional_devices_to_configure] = win_utils::computeNewTopologyAndMetadata(config.m_device_prep, config.m_device_id, *stripped_initial_state); + const auto change_is_needed { !m_dd_api->isTopologyTheSame(topology_before_changes, new_topology) }; + DD_LOG(info) << "Newly computed display device topology data:\n" + << " - topology: " << toJson(new_topology, JSON_COMPACT) << "\n" + << " - change is needed: " << toJson(change_is_needed, JSON_COMPACT) << "\n" + << " - additional devices to configure: " << toJson(additional_devices_to_configure, JSON_COMPACT); + + // This check is mainly to cover the case for "config.device_prep == VerifyOnly" as we at least + // have to validate that the device exists, but it doesn't hurt to double-check it in all cases. + if (!win_utils::flattenTopology(new_topology).contains(device_to_configure)) { + DD_LOG(error) << "Device " << toJson(device_to_configure, JSON_COMPACT) << " is not active!"; + return std::nullopt; + } + + if (change_is_needed) { + if (cached_state && !m_dd_api->isTopologyTheSame(cached_state->m_modified.m_topology, new_topology)) { + DD_LOG(warning) << "To apply new display device settings, previous modifications must be undone! Trying to undo them now."; + if (!revertModifiedSettings()) { + DD_LOG(error) << "Failed to apply new configuration, because the previous settings could not be reverted!"; + return std::nullopt; + } + } + + const bool audio_is_captured { m_audio_context_api->isCaptured() }; + if (!audio_is_captured) { + // Non-stripped initial state MUST be checked here as the missing device could have its context captured! + const bool switching_from_initial { m_dd_api->isTopologyTheSame(new_state.m_initial.m_topology, topology_before_changes) }; + const bool new_topology_contains_all_current_topology_devices { std::ranges::includes(win_utils::flattenTopology(new_topology), win_utils::flattenTopology(topology_before_changes)) }; + if (switching_from_initial && !new_topology_contains_all_current_topology_devices) { + // Only capture the context when switching from initial topology. All the other intermediate states, like non-existent + // capture state after system restart are to be avoided. + if (!m_audio_context_api->capture()) { + DD_LOG(error) << "Failed to capture audio context!"; + return std::nullopt; + } + } + } + + if (!m_dd_api->setTopology(new_topology)) { + DD_LOG(error) << "Failed to apply new configuration, because a new topology could not be set!"; + return std::nullopt; + } + + // We can release the context later on if everything is successful as we are switching back to the non-stripped initial state. + release_context = m_dd_api->isTopologyTheSame(new_state.m_initial.m_topology, new_topology) && audio_is_captured; + } + + new_state.m_modified.m_topology = new_topology; + return std::make_tuple(new_state, device_to_configure, additional_devices_to_configure); + } +} // namespace display_device diff --git a/src/windows/settingsmanagergeneral.cpp b/src/windows/settingsmanagergeneral.cpp index ef81bb5..d94f7ce 100644 --- a/src/windows/settingsmanagergeneral.cpp +++ b/src/windows/settingsmanagergeneral.cpp @@ -2,27 +2,28 @@ #include "displaydevice/windows/settingsmanager.h" // local includes +#include "displaydevice/logging.h" #include "displaydevice/noopaudiocontext.h" -#include "displaydevice/noopsettingspersistence.h" namespace display_device { SettingsManager::SettingsManager( std::shared_ptr dd_api, - std::shared_ptr settings_persistence_api, - std::shared_ptr audio_context_api): + std::shared_ptr audio_context_api, + std::unique_ptr persistent_state): m_dd_api { std::move(dd_api) }, - m_settings_persistence_api { std::move(settings_persistence_api) }, m_audio_context_api { std::move(audio_context_api) } { + m_audio_context_api { std::move(audio_context_api) }, + m_persistence_state { std::move(persistent_state) } { if (!m_dd_api) { throw std::logic_error { "Nullptr provided for WinDisplayDeviceInterface in SettingsManager!" }; } - if (!m_settings_persistence_api) { - m_settings_persistence_api = std::make_shared(); - } - if (!m_audio_context_api) { m_audio_context_api = std::make_shared(); } + + if (!m_persistence_state) { + throw std::logic_error { "Nullptr provided for PersistentState in SettingsManager!" }; + } } EnumeratedDeviceList diff --git a/src/windows/settingsmanagerrevert.cpp b/src/windows/settingsmanagerrevert.cpp new file mode 100644 index 0000000..bf0fb3f --- /dev/null +++ b/src/windows/settingsmanagerrevert.cpp @@ -0,0 +1,138 @@ +// class header include +#include "displaydevice/windows/settingsmanager.h" + +// system includes +#include + +// local includes +#include "displaydevice/logging.h" +#include "displaydevice/windows/json.h" +#include "displaydevice/windows/settingsutils.h" + +namespace display_device { + bool + SettingsManager::revertSettings() { + const auto &cached_state { m_persistence_state->getState() }; + if (!cached_state) { + return true; + } + + const auto api_access { m_dd_api->isApiAccessAvailable() }; + DD_LOG(info) << "Trying to revert applied display device settings. API is available: " << toJson(api_access); + + if (!api_access) { + return false; + } + + const auto current_topology { m_dd_api->getCurrentTopology() }; + if (!m_dd_api->isTopologyValid(current_topology)) { + DD_LOG(error) << "Retrieved current topology is invalid:\n" + << toJson(current_topology); + return false; + } + + boost::scope::scope_exit topology_prep_guard { win_utils::topologyGuardFn(*m_dd_api, current_topology) }; + + // We can revert the modified setting independently before playing around with initial topology. + if (!revertModifiedSettings()) { + // Error already logged + return false; + } + + if (!m_dd_api->isTopologyValid(cached_state->m_initial.m_topology)) { + DD_LOG(error) << "Trying to revert to an invalid initial topology:\n" + << toJson(cached_state->m_initial.m_topology); + return false; + } + + if (!m_dd_api->setTopology(cached_state->m_initial.m_topology)) { + DD_LOG(error) << "Failed to change topology to:\n" + << toJson(cached_state->m_initial.m_topology); + return false; + } + + if (!m_persistence_state->persistState(std::nullopt)) { + DD_LOG(error) << "Failed to save reverted settings! Undoing initial topology changes..."; + return false; + } + + if (m_audio_context_api->isCaptured()) { + m_audio_context_api->release(); + } + + // Disable guards + topology_prep_guard.set_active(false); + return true; + } + + bool + SettingsManager::revertModifiedSettings() { + const auto &cached_state { m_persistence_state->getState() }; + if (!cached_state || !cached_state->m_modified.hasModifications()) { + return true; + } + + if (!m_dd_api->isTopologyValid(cached_state->m_modified.m_topology)) { + DD_LOG(error) << "Trying to revert modified settings using invalid topology:\n" + << toJson(cached_state->m_modified.m_topology); + return false; + } + + if (!m_dd_api->setTopology(cached_state->m_modified.m_topology)) { + DD_LOG(error) << "Failed to change topology to:\n" + << toJson(cached_state->m_modified.m_topology); + return false; + } + + DdGuardFn hdr_guard_fn; + boost::scope::scope_exit hdr_guard { hdr_guard_fn, false }; + if (!cached_state->m_modified.m_original_hdr_states.empty()) { + hdr_guard_fn = win_utils::hdrStateGuardFn(*m_dd_api, cached_state->m_modified.m_topology); + hdr_guard.set_active(true); + DD_LOG(info) << "Trying to change back the HDR states to:\n" + << toJson(cached_state->m_modified.m_original_hdr_states); + if (!m_dd_api->setHdrStates(cached_state->m_modified.m_original_hdr_states)) { + // Error already logged + return false; + } + } + + DdGuardFn mode_guard_fn; + boost::scope::scope_exit mode_guard { mode_guard_fn, false }; + if (!cached_state->m_modified.m_original_modes.empty()) { + mode_guard_fn = win_utils::modeGuardFn(*m_dd_api, cached_state->m_modified.m_topology); + mode_guard.set_active(true); + DD_LOG(info) << "Trying to change back the display modes to:\n" + << toJson(cached_state->m_modified.m_original_modes); + if (!m_dd_api->setDisplayModes(cached_state->m_modified.m_original_modes)) { + // Error already logged + return false; + } + } + + DdGuardFn primary_guard_fn; + boost::scope::scope_exit primary_guard { primary_guard_fn, false }; + if (!cached_state->m_modified.m_original_primary_device.empty()) { + primary_guard_fn = win_utils::primaryGuardFn(*m_dd_api, cached_state->m_modified.m_topology); + primary_guard.set_active(true); + DD_LOG(info) << "Trying to change back the original primary device to: " << toJson(cached_state->m_modified.m_original_primary_device); + if (!m_dd_api->setAsPrimary(cached_state->m_modified.m_original_primary_device)) { + // Error already logged + return false; + } + } + + auto cleared_data { *cached_state }; + cleared_data.m_modified = { cleared_data.m_modified.m_topology }; + if (!m_persistence_state->persistState(cleared_data)) { + DD_LOG(error) << "Failed to save reverted settings! Undoing changes to modified topology..."; + return false; + } + + // Disable guards + hdr_guard.set_active(false); + mode_guard.set_active(false); + primary_guard.set_active(false); + return true; + } +} // namespace display_device diff --git a/src/windows/settingsutils.cpp b/src/windows/settingsutils.cpp new file mode 100644 index 0000000..983780f --- /dev/null +++ b/src/windows/settingsutils.cpp @@ -0,0 +1,313 @@ +// header include +#include "displaydevice/windows/settingsutils.h" + +// system includes +#include + +// local includes +#include "displaydevice/logging.h" +#include "displaydevice/windows/json.h" + +namespace display_device::win_utils { + namespace { + /** + * @brief predicate for getDeviceIds. + */ + bool + anyDevice(const EnumeratedDevice &) { + return true; + } + + /** + * @brief predicate for getDeviceIds. + */ + bool + primaryOnlyDevices(const EnumeratedDevice &device) { + return device.m_info && device.m_info->m_primary; + } + + /** + * @brief Get device ids from device list matching the predicate. + * @param devices List of devices. + * @param predicate Predicate to use. + * @return Device ids from device list matching the predicate. + * + * EXAMPLES: + * ```cpp + * const EnumeratedDeviceList devices { ... }; + * + * const auto all_ids { getDeviceIds(devices, anyDevice) }; + * const auto primary_only_ids { getDeviceIds(devices, primaryOnlyDevices) }; + * ``` + */ + std::set + getDeviceIds(const EnumeratedDeviceList &devices, const std::add_lvalue_reference_t &predicate) { + std::set device_ids; + for (const auto &device : devices) { + if (predicate(device)) { + device_ids.insert(device.m_device_id); + } + } + + return device_ids; + } + + /** + * @brief Remove the topology device ids and groups that no longer have valid devices. + * @param topology Topology to be stripped. + * @param devices List of devices. + * @return Topology without missing device ids. + */ + ActiveTopology + stripTopology(const ActiveTopology &topology, const EnumeratedDeviceList &devices) { + const std::set available_device_ids { getDeviceIds(devices, anyDevice) }; + + ActiveTopology stripped_topology; + for (const auto &group : topology) { + std::vector stripped_group; + for (const auto &device_id : group) { + if (available_device_ids.contains(device_id)) { + stripped_group.push_back(device_id); + } + } + + if (!stripped_group.empty()) { + stripped_topology.push_back(stripped_group); + } + } + + return stripped_topology; + } + + /** + * @brief Remove device ids that are no longer available. + * @param device_ids Id list to be stripped. + * @param devices List of devices. + * @return List without missing device ids. + */ + std::set + stripDevices(const std::set &device_ids, const EnumeratedDeviceList &devices) { + std::set available_device_ids { getDeviceIds(devices, anyDevice) }; + + std::set available_devices; + std::ranges::set_intersection(device_ids, available_device_ids, + std::inserter(available_devices, std::begin(available_devices))); + return available_devices; + } + + /** + * @brief Find topology group with matching id and get other ids from the group. + * @param topology Topology to be searched. + * @param target_device_id Device id whose group to search for. + * @return Other ids in the group without (excluding the provided one). + */ + std::set + tryGetOtherDevicesInTheSameGroup(const ActiveTopology &topology, const std::string &target_device_id) { + std::set device_ids; + + for (const auto &group : topology) { + for (const auto &group_device_id : group) { + if (group_device_id == target_device_id) { + std::ranges::copy_if(group, std::inserter(device_ids, std::begin(device_ids)), [&target_device_id](const auto &id) { + return id != target_device_id; + }); + break; + } + } + } + + return device_ids; + } + + /** + * @brief Merge the configurable devices into a vector. + */ + std::vector + joinConfigurableDevices(const std::string &device_to_configure, const std::set &additional_devices_to_configure) { + std::vector devices { device_to_configure }; + devices.insert(std::end(devices), std::begin(additional_devices_to_configure), std::end(additional_devices_to_configure)); + return devices; + } + } // namespace + + std::set + flattenTopology(const ActiveTopology &topology) { + std::set flattened_topology; + for (const auto &group : topology) { + for (const auto &device_id : group) { + flattened_topology.insert(device_id); + } + } + + return flattened_topology; + } + + std::optional + computeInitialState(const std::optional &prev_state, const ActiveTopology &topology_before_changes, const EnumeratedDeviceList &devices) { + // We first need to determine the "initial" state that will be used when reverting + // the changes as the "go-to" final state we need to achieve. It will also be used + // as the base for creating new topology based on the provided config settings. + // + // Having a constant base allows us to re-apply settings with different configuration + // parameters without actually reverting the topology back to the initial one where + // the primary display could have changed in between the first call to this method + // and a next one. + // + // If the user wants to use a "fresh" and "current" system settings, they have to revert + // changes as otherwise we are using the cached state as a base. + if (prev_state) { + return *prev_state; + } + + const auto primary_devices { getDeviceIds(devices, primaryOnlyDevices) }; + if (primary_devices.empty()) { + DD_LOG(error) << "Enumerated device list does not contain primary devices!"; + return std::nullopt; + } + + return SingleDisplayConfigState::Initial { + topology_before_changes, + primary_devices + }; + } + + ActiveTopology + computeNewTopology(const SingleDisplayConfiguration::DevicePreparation device_prep, const bool configuring_primary_devices, const std::string &device_to_configure, const std::set &additional_devices_to_configure, const ActiveTopology &initial_topology) { + using DevicePrep = SingleDisplayConfiguration::DevicePreparation; + + if (device_prep != DevicePrep::VerifyOnly) { + if (device_prep == DevicePrep::EnsureOnlyDisplay) { + // Device needs to be the only one that's active OR if it's a PRIMARY device, + // only the whole PRIMARY group needs to be active (in case they are duplicated) + if (configuring_primary_devices) { + return ActiveTopology { joinConfigurableDevices(device_to_configure, additional_devices_to_configure) }; + } + + return ActiveTopology { { device_to_configure } }; + } + // DevicePrep::EnsureActive || DevicePrep::EnsurePrimary + else { + // The device needs to be active at least. + if (!flattenTopology(initial_topology).contains(device_to_configure)) { + // Create an extended topology as it's probably what makes sense the most... + ActiveTopology new_topology { initial_topology }; + new_topology.push_back({ device_to_configure }); + return new_topology; + } + } + } + + return initial_topology; + } + + std::optional + stripInitialState(const SingleDisplayConfigState::Initial &initial_state, const EnumeratedDeviceList &devices) { + const auto stripped_initial_topology { stripTopology(initial_state.m_topology, devices) }; + auto initial_primary_devices { stripDevices(initial_state.m_primary_devices, devices) }; + + if (stripped_initial_topology.empty()) { + DD_LOG(error) << "Enumerated device list does not contain ANY of the devices from the initial state!"; + return std::nullopt; + } + + if (initial_primary_devices.empty()) { + // The initial primay device is no longer available, so maybe it makes sense to use the current one. Maybe... + initial_primary_devices = getDeviceIds(devices, primaryOnlyDevices); + if (initial_primary_devices.empty()) { + DD_LOG(error) << "Enumerated device list does not contain primary devices!"; + return std::nullopt; + } + } + + if (initial_state.m_topology != stripped_initial_topology || initial_state.m_primary_devices != initial_primary_devices) { + DD_LOG(warning) << "Trying to apply configuration without reverting back to initial topology first, however not all devices from that " + "topology are available.\n" + << "Will try adapting the initial topology that is used as a base:\n" + << " - topology: " << toJson(initial_state.m_topology, JSON_COMPACT) << " -> " << toJson(stripped_initial_topology, JSON_COMPACT) << "\n" + << " - primary devices: " << toJson(initial_state.m_primary_devices, JSON_COMPACT) << " -> " << toJson(initial_primary_devices, JSON_COMPACT); + } + + return SingleDisplayConfigState::Initial { + stripped_initial_topology, + initial_primary_devices + }; + } + + std::tuple> + computeNewTopologyAndMetadata(const SingleDisplayConfiguration::DevicePreparation device_prep, const std::string &device_id, const SingleDisplayConfigState::Initial &initial_state) { + const bool configuring_unspecified_devices { device_id.empty() }; + const auto device_to_configure { configuring_unspecified_devices ? *std::begin(initial_state.m_primary_devices) : device_id }; + auto additional_devices_to_configure { configuring_unspecified_devices ? + std::set { std::next(std::begin(initial_state.m_primary_devices)), std::end(initial_state.m_primary_devices) } : + tryGetOtherDevicesInTheSameGroup(initial_state.m_topology, device_to_configure) }; + DD_LOG(info) << "Will compute new display device topology from the following input:\n" + << " - initial topology: " << toJson(initial_state.m_topology, JSON_COMPACT) << "\n" + << " - initial primary devices: " << toJson(initial_state.m_primary_devices, JSON_COMPACT) << "\n" + << " - configuring unspecified device: " << toJson(configuring_unspecified_devices, JSON_COMPACT) << "\n" + << " - device to configure: " << toJson(device_to_configure, JSON_COMPACT) << "\n" + << " - additional devices to configure: " << toJson(additional_devices_to_configure, JSON_COMPACT); + + const auto new_topology { computeNewTopology(device_prep, configuring_unspecified_devices, device_to_configure, additional_devices_to_configure, initial_state.m_topology) }; + additional_devices_to_configure = tryGetOtherDevicesInTheSameGroup(new_topology, device_to_configure); + return std::make_tuple(new_topology, device_to_configure, additional_devices_to_configure); + } + + DdGuardFn + topologyGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology) { + DD_LOG(debug) << "Got topology in topologyGuardFn:\n" + << toJson(topology); + return [&win_dd, topology]() { + if (!win_dd.setTopology(topology)) { + DD_LOG(error) << "failed to revert topology in topologyGuardFn! Used the following topology:\n" + << toJson(topology); + } + }; + } + + DdGuardFn + modeGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology) { + const auto modes = win_dd.getCurrentDisplayModes(flattenTopology(topology)); + DD_LOG(debug) << "Got modes in modeGuardFn:\n" + << toJson(modes); + return [&win_dd, modes]() { + if (!win_dd.setDisplayModes(modes)) { + DD_LOG(error) << "failed to revert display modes in modeGuardFn! Used the following modes:\n" + << toJson(modes); + } + }; + } + + DdGuardFn + primaryGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology) { + std::string primary_device {}; + const auto flat_topology { flattenTopology(topology) }; + for (const auto &device_id : flat_topology) { + if (win_dd.isPrimary(device_id)) { + primary_device = device_id; + break; + } + } + + DD_LOG(debug) << "Got primary device in primaryGuardFn:\n" + << toJson(primary_device); + return [&win_dd, primary_device]() { + if (!win_dd.setAsPrimary(primary_device)) { + DD_LOG(error) << "failed to revert primary device in primaryGuardFn! Used the following device id:\n" + << toJson(primary_device); + } + }; + } + + DdGuardFn + hdrStateGuardFn(WinDisplayDeviceInterface &win_dd, const ActiveTopology &topology) { + const auto states = win_dd.getCurrentHdrStates(flattenTopology(topology)); + DD_LOG(debug) << "Got states in hdrStateGuardFn:\n" + << toJson(states); + return [&win_dd, states]() { + if (!win_dd.setHdrStates(states)) { + DD_LOG(error) << "failed to revert HDR states in hdrStateGuardFn! Used the following HDR states:\n" + << toJson(states); + } + }; + } +} // namespace display_device::win_utils diff --git a/src/windows/types.cpp b/src/windows/types.cpp new file mode 100644 index 0000000..d4fd343 --- /dev/null +++ b/src/windows/types.cpp @@ -0,0 +1,34 @@ +// header include +#include "displaydevice/windows/types.h" + +namespace display_device { + bool + operator==(const Rational &lhs, const Rational &rhs) { + return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator; + } + + bool + operator==(const DisplayMode &lhs, const DisplayMode &rhs) { + return lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_resolution == rhs.m_resolution; + } + + bool + SingleDisplayConfigState::Modified::hasModifications() const { + return !m_original_modes.empty() || !m_original_hdr_states.empty() || !m_original_primary_device.empty(); + } + + bool + operator==(const SingleDisplayConfigState::Initial &lhs, const SingleDisplayConfigState::Initial &rhs) { + return lhs.m_topology == rhs.m_topology && lhs.m_primary_devices == rhs.m_primary_devices; + } + + bool + operator==(const SingleDisplayConfigState::Modified &lhs, const SingleDisplayConfigState::Modified &rhs) { + return lhs.m_topology == rhs.m_topology && lhs.m_original_modes == rhs.m_original_modes && lhs.m_original_hdr_states == rhs.m_original_hdr_states && lhs.m_original_primary_device == rhs.m_original_primary_device; + } + + bool + operator==(const SingleDisplayConfigState &lhs, const SingleDisplayConfigState &rhs) { + return lhs.m_initial == rhs.m_initial && lhs.m_modified == rhs.m_modified; + } +} // namespace display_device diff --git a/tests/fixtures/include/fixtures/comparison.h b/tests/fixtures/include/fixtures/comparison.h deleted file mode 100644 index 230842e..0000000 --- a/tests/fixtures/include/fixtures/comparison.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -// local includes -#include "displaydevice/types.h" - -// Helper comparison operators -bool -fuzzyCompare(float lhs, float rhs); - -namespace display_device { - bool - operator==(const Point &lhs, const Point &rhs); - - bool - operator==(const Resolution &lhs, const Resolution &rhs); - - bool - operator==(const EnumeratedDevice::Info &lhs, const EnumeratedDevice::Info &rhs); - - bool - operator==(const EnumeratedDevice &lhs, const EnumeratedDevice &rhs); - - bool - operator==(const SingleDisplayConfiguration &lhs, const SingleDisplayConfiguration &rhs); -} // namespace display_device diff --git a/tests/fixtures/include/fixtures/jsonconvertertest.h b/tests/fixtures/include/fixtures/jsonconvertertest.h index abd951b..809a89a 100644 --- a/tests/fixtures/include/fixtures/jsonconvertertest.h +++ b/tests/fixtures/include/fixtures/jsonconvertertest.h @@ -1,7 +1,6 @@ #pragma once // local includes -#include "comparison.h" #include "displaydevice/json.h" #include "fixtures.h" diff --git a/tests/fixtures/include/fixtures/mockaudiocontext.h b/tests/fixtures/include/fixtures/mockaudiocontext.h index b3159d4..d958f1a 100644 --- a/tests/fixtures/include/fixtures/mockaudiocontext.h +++ b/tests/fixtures/include/fixtures/mockaudiocontext.h @@ -9,7 +9,8 @@ namespace display_device { class MockAudioContext: public AudioContextInterface { public: - MOCK_METHOD(bool, capture, (const std::vector &), (override)); - MOCK_METHOD(void, release, (const std::vector &), (override)); + MOCK_METHOD(bool, capture, (), (override)); + MOCK_METHOD(bool, isCaptured, (), (const, override)); + MOCK_METHOD(void, release, (), (override)); }; } // namespace display_device diff --git a/tests/fixtures/include/fixtures/mocksettingspersistence.h b/tests/fixtures/include/fixtures/mocksettingspersistence.h index 537ea4c..50f3e49 100644 --- a/tests/fixtures/include/fixtures/mocksettingspersistence.h +++ b/tests/fixtures/include/fixtures/mocksettingspersistence.h @@ -11,6 +11,6 @@ namespace display_device { public: MOCK_METHOD(bool, store, (const std::vector &), (override)); MOCK_METHOD(std::optional>, load, (), (const, override)); - MOCK_METHOD(void, clear, (), (override)); + MOCK_METHOD(bool, clear, (), (override)); }; } // namespace display_device diff --git a/tests/unit/general/test_comparison.cpp b/tests/unit/general/test_comparison.cpp new file mode 100644 index 0000000..68cc708 --- /dev/null +++ b/tests/unit/general/test_comparison.cpp @@ -0,0 +1,66 @@ +// local includes +#include "displaydevice/types.h" +#include "fixtures/fixtures.h" + +namespace { + // Specialized TEST macro(s) for this test file +#define TEST_S(...) DD_MAKE_TEST(TEST, TypeComparison, __VA_ARGS__) +} // namespace + +TEST_S(Point) { + EXPECT_EQ(display_device::Point({ 1, 1 }), display_device::Point({ 1, 1 })); + EXPECT_NE(display_device::Point({ 1, 1 }), display_device::Point({ 0, 1 })); + EXPECT_NE(display_device::Point({ 1, 1 }), display_device::Point({ 1, 0 })); +} + +TEST_S(Resolution) { + EXPECT_EQ(display_device::Resolution({ 1, 1 }), display_device::Resolution({ 1, 1 })); + EXPECT_NE(display_device::Resolution({ 1, 1 }), display_device::Resolution({ 0, 1 })); + EXPECT_NE(display_device::Resolution({ 1, 1 }), display_device::Resolution({ 1, 0 })); +} + +TEST_S(EnumeratedDevice, Info) { + EXPECT_EQ(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 0 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.1f, 1.f, true, { 1, 1 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.1f, true, { 1, 1 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, false, { 1, 1 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 0 }, std::nullopt })); + EXPECT_NE(display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, std::nullopt }), + display_device::EnumeratedDevice::Info({ { 1, 1 }, 1.f, 1.f, true, { 1, 1 }, display_device::HdrState::Disabled })); +} + +TEST_S(EnumeratedDevice) { + EXPECT_EQ(display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} }), + display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} })); + EXPECT_NE(display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} }), + display_device::EnumeratedDevice({ "0", "1", "1", display_device::EnumeratedDevice::Info {} })); + EXPECT_NE(display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} }), + display_device::EnumeratedDevice({ "1", "0", "1", display_device::EnumeratedDevice::Info {} })); + EXPECT_NE(display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} }), + display_device::EnumeratedDevice({ "1", "1", "0", display_device::EnumeratedDevice::Info {} })); + EXPECT_NE(display_device::EnumeratedDevice({ "1", "1", "1", display_device::EnumeratedDevice::Info {} }), + display_device::EnumeratedDevice({ "1", "1", "1", std::nullopt })); +} + +TEST_S(SingleDisplayConfiguration) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + EXPECT_EQ(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled })); + EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "0", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled })); + EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsurePrimary, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled })); + EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 0 } }, 1.f, display_device::HdrState::Disabled })); + EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.1f, display_device::HdrState::Disabled })); + EXPECT_NE(display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Disabled }), + display_device::SingleDisplayConfiguration({ "1", DevicePrep::EnsureActive, { { 1, 1 } }, 1.f, display_device::HdrState::Enabled })); +} diff --git a/tests/unit/general/test_jsonconverter.cpp b/tests/unit/general/test_jsonconverter.cpp index 9669b58..24662b5 100644 --- a/tests/unit/general/test_jsonconverter.cpp +++ b/tests/unit/general/test_jsonconverter.cpp @@ -52,3 +52,19 @@ TEST_F_S(SingleDisplayConfiguration) { executeTestCase(config_3, R"({"device_id":"ID_3","device_prep":"EnsureOnlyDisplay","hdr_state":null,"refresh_rate":null,"resolution":{"height":123,"width":156}})"); executeTestCase(config_4, R"({"device_id":"ID_4","device_prep":"EnsurePrimary","hdr_state":null,"refresh_rate":null,"resolution":null})"); } + +TEST_F_S(StringSet) { + executeTestCase(std::set {}, R"([])"); + executeTestCase(std::set { "ABC", "DEF" }, R"(["ABC","DEF"])"); + executeTestCase(std::set { "DEF", "ABC" }, R"(["ABC","DEF"])"); +} + +TEST_F_S(String) { + executeTestCase(std::string {}, R"("")"); + executeTestCase(std::string { "ABC" }, R"("ABC")"); +} + +TEST_F_S(Bool) { + executeTestCase(true, R"(true)"); + executeTestCase(false, R"(false)"); +} diff --git a/tests/unit/general/test_noopaudiocontext.cpp b/tests/unit/general/test_noopaudiocontext.cpp index e9dde7c..5d91dd2 100644 --- a/tests/unit/general/test_noopaudiocontext.cpp +++ b/tests/unit/general/test_noopaudiocontext.cpp @@ -14,11 +14,19 @@ namespace { } // namespace TEST_F_S(Capture) { - EXPECT_TRUE(m_impl.capture({})); - EXPECT_TRUE(m_impl.capture({ "DeviceId1" })); + EXPECT_FALSE(m_impl.isCaptured()); + EXPECT_TRUE(m_impl.capture()); + EXPECT_TRUE(m_impl.isCaptured()); + EXPECT_TRUE(m_impl.capture()); + EXPECT_TRUE(m_impl.isCaptured()); } TEST_F_S(Release) { - EXPECT_NO_THROW(m_impl.release({})); - EXPECT_NO_THROW(m_impl.release({ "DeviceId1" })); + EXPECT_FALSE(m_impl.isCaptured()); + EXPECT_NO_THROW(m_impl.release()); + EXPECT_FALSE(m_impl.isCaptured()); + EXPECT_TRUE(m_impl.capture()); + EXPECT_TRUE(m_impl.isCaptured()); + EXPECT_NO_THROW(m_impl.release()); + EXPECT_FALSE(m_impl.isCaptured()); } diff --git a/tests/unit/general/test_noopsettingspersistence.cpp b/tests/unit/general/test_noopsettingspersistence.cpp index a48a365..815fe10 100644 --- a/tests/unit/general/test_noopsettingspersistence.cpp +++ b/tests/unit/general/test_noopsettingspersistence.cpp @@ -23,5 +23,5 @@ TEST_F_S(Load) { } TEST_F_S(Clear) { - EXPECT_NO_THROW(m_impl.clear()); + EXPECT_TRUE(m_impl.clear()); } diff --git a/tests/unit/windows/test_comparison.cpp b/tests/unit/windows/test_comparison.cpp new file mode 100644 index 0000000..cd87154 --- /dev/null +++ b/tests/unit/windows/test_comparison.cpp @@ -0,0 +1,48 @@ +// local includes +#include "displaydevice/windows/types.h" +#include "fixtures/fixtures.h" + +namespace { + // Specialized TEST macro(s) for this test file +#define TEST_S(...) DD_MAKE_TEST(TEST, TypeComparison, __VA_ARGS__) +} // namespace + +TEST_S(Rational) { + EXPECT_EQ(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 1 })); + EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 0, 1 })); + EXPECT_NE(display_device::Rational({ 1, 1 }), display_device::Rational({ 1, 0 })); +} + +TEST_S(DisplayMode) { + EXPECT_EQ(display_device::DisplayMode({ 1, 1 }, { 1, 1 }), display_device::DisplayMode({ 1, 1 }, { 1, 1 })); + EXPECT_NE(display_device::DisplayMode({ 1, 1 }, { 1, 1 }), display_device::DisplayMode({ 1, 0 }, { 1, 1 })); + EXPECT_NE(display_device::DisplayMode({ 1, 1 }, { 1, 1 }), display_device::DisplayMode({ 1, 1 }, { 1, 0 })); +} + +TEST_S(SingleDisplayConfigState, Initial) { + using Initial = display_device::SingleDisplayConfigState::Initial; + EXPECT_EQ(Initial({ { { "1" } } }, { "1" }), Initial({ { { "1" } } }, { "1" })); + EXPECT_NE(Initial({ { { "1" } } }, { "1" }), Initial({ { { "0" } } }, { "1" })); + EXPECT_NE(Initial({ { { "1" } } }, { "1" }), Initial({ { { "1" } } }, { "0" })); +} + +TEST_S(SingleDisplayConfigState, Modified) { + using Modified = display_device::SingleDisplayConfigState::Modified; + EXPECT_EQ(Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1"), + Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1")); + EXPECT_NE(Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1"), + Modified({ { { "0" } } }, { { "1", {} } }, { { "1", {} } }, "1")); + EXPECT_NE(Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1"), + Modified({ { { "1" } } }, { { "0", {} } }, { { "1", {} } }, "1")); + EXPECT_NE(Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1"), + Modified({ { { "1" } } }, { { "1", {} } }, { { "0", {} } }, "1")); + EXPECT_NE(Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "1"), + Modified({ { { "1" } } }, { { "1", {} } }, { { "1", {} } }, "0")); +} + +TEST_S(SingleDisplayConfigState) { + using SDSC = display_device::SingleDisplayConfigState; + EXPECT_EQ(SDSC({ { { "1" } } }, { { { "1" } } }), SDSC({ { { "1" } } }, { { { "1" } } })); + EXPECT_NE(SDSC({ { { "1" } } }, { { { "1" } } }), SDSC({ { { "0" } } }, { { { "1" } } })); + EXPECT_NE(SDSC({ { { "1" } } }, { { { "1" } } }), SDSC({ { { "1" } } }, { { { "0" } } })); +} diff --git a/tests/unit/windows/test_jsonconverter.cpp b/tests/unit/windows/test_jsonconverter.cpp index 9166470..3ce9ca2 100644 --- a/tests/unit/windows/test_jsonconverter.cpp +++ b/tests/unit/windows/test_jsonconverter.cpp @@ -24,3 +24,19 @@ TEST_F_S(HdrStateMap) { executeTestCase(display_device::HdrStateMap { { "DeviceId1", std::nullopt }, { "DeviceId2", display_device::HdrState::Enabled } }, R"({"DeviceId1":null,"DeviceId2":"Enabled"})"); } + +TEST_F_S(SingleDisplayConfigState) { + const display_device::SingleDisplayConfigState valid_input { + { { { "DeviceId1" } }, + { "DeviceId1" } }, + { display_device::SingleDisplayConfigState::Modified { + { { "DeviceId2" } }, + { { "DeviceId2", { { 1920, 1080 }, { 120, 1 } } } }, + { { "DeviceId2", { display_device::HdrState::Disabled } } }, + { "DeviceId2" }, + } } + }; + + executeTestCase(display_device::SingleDisplayConfigState {}, R"({"initial":{"primary_devices":[],"topology":[]},"modified":{"original_hdr_states":{},"original_modes":{},"original_primary_device":"","topology":[]}})"); + executeTestCase(valid_input, R"({"initial":{"primary_devices":["DeviceId1"],"topology":[["DeviceId1"]]},"modified":{"original_hdr_states":{"DeviceId2":"Disabled"},"original_modes":{"DeviceId2":{"refresh_rate":{"denominator":1,"numerator":120},"resolution":{"height":1080,"width":1920}}},"original_primary_device":"DeviceId2","topology":[["DeviceId2"]]}})"); +} diff --git a/tests/unit/windows/test_persistentstate.cpp b/tests/unit/windows/test_persistentstate.cpp new file mode 100644 index 0000000..0e4a96e --- /dev/null +++ b/tests/unit/windows/test_persistentstate.cpp @@ -0,0 +1,173 @@ +// local includes +#include "displaydevice/noopsettingspersistence.h" +#include "displaydevice/windows/settingsmanager.h" +#include "fixtures/fixtures.h" +#include "fixtures/mocksettingspersistence.h" +#include "utils/comparison.h" +#include "utils/helpers.h" +#include "utils/mockwindisplaydevice.h" + +namespace { + // Convenience keywords for GMock + using ::testing::HasSubstr; + using ::testing::Return; + using ::testing::StrictMock; + + // Test fixture(s) for this file + class PersistentStateMocked: public BaseTest { + public: + display_device::PersistentState & + getImpl(const std::optional &fallback_state = display_device::SingleDisplayConfigState {}) { + if (!m_impl) { + m_impl = std::make_unique(m_settings_persistence_api, fallback_state); + } + + return *m_impl; + } + + std::shared_ptr> m_settings_persistence_api { std::make_shared>() }; + + private: + std::unique_ptr m_impl; + }; + + // Specialized TEST macro(s) for this test +#define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, PersistentStateMocked, __VA_ARGS__) +} // namespace + +TEST_F_S_MOCKED(NoopSettingsPersistence) { + class NakedPersistentState: public display_device::PersistentState { + public: + using PersistentState::m_settings_persistence_api; + using PersistentState::PersistentState; + }; + + const NakedPersistentState persistent_state { nullptr }; + EXPECT_TRUE(std::dynamic_pointer_cast(persistent_state.m_settings_persistence_api) != nullptr); +} + +TEST_F_S_MOCKED(FailedToLoadPersitence) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_NULL))); + + EXPECT_THAT([this]() { getImpl(std::nullopt); }, ThrowsMessage(HasSubstr("Failed to load persistent settings!"))); +} + +TEST_F_S_MOCKED(FailedToLoadPersitence, FallbackIsUsed) { + const display_device::SingleDisplayConfigState fallback_value { { { { "FallbackId" } } } }; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_NULL))); + + EXPECT_EQ(getImpl(fallback_value).getState(), fallback_value); +} + +TEST_F_S_MOCKED(InvalidPersitenceData) { + const std::string data_string { "SOMETHING" }; + const std::vector data { std::begin(data_string), std::end(data_string) }; + + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(data)); + + EXPECT_THAT([this]() { getImpl(std::nullopt); }, + ThrowsMessage(HasSubstr("Failed to parse persistent settings! Error:\n" + "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'S'"))); +} + +TEST_F_S_MOCKED(InvalidPersitenceData, FallbackIsUsed) { + const display_device::SingleDisplayConfigState fallback_value { { { { "FallbackId" } } } }; + const std::string data_string { "SOMETHING" }; + const std::vector data { std::begin(data_string), std::end(data_string) }; + + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(data)); + + EXPECT_EQ(getImpl(fallback_value).getState(), fallback_value); +} + +TEST_F_S_MOCKED(FallbackIsNotUsedOnSuccess) { + const display_device::SingleDisplayConfigState fallback_value { { { { "FallbackId" } } } }; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + + EXPECT_EQ(getImpl(fallback_value).getState(), ut_consts::SDCS_FULL); +} + +TEST_F_S_MOCKED(FailedToPersistState, ClearFailed) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + EXPECT_CALL(*m_settings_persistence_api, clear()) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); + EXPECT_FALSE(getImpl().persistState(ut_consts::SDCS_NULL)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); +} + +TEST_F_S_MOCKED(FailedToPersistState, BadJsonEncoding) { + display_device::SingleDisplayConfigState invalid_state; + invalid_state.m_modified.m_original_primary_device = "InvalidDeviceName\xC2"; + + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); + EXPECT_FALSE(getImpl().persistState(invalid_state)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); +} + +TEST_F_S_MOCKED(FailedToPersistState, StoreFailed) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); + EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(ut_consts::SDCS_FULL))) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); + EXPECT_FALSE(getImpl().persistState(ut_consts::SDCS_FULL)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); +} + +TEST_F_S_MOCKED(ClearState) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + EXPECT_CALL(*m_settings_persistence_api, clear()) + .Times(1) + .WillOnce(Return(true)); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); + EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_NULL)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NULL); +} + +TEST_F_S_MOCKED(StoreState) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_NO_MODIFICATIONS))); + EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(ut_consts::SDCS_FULL))) + .Times(1) + .WillOnce(Return(true)); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_NO_MODIFICATIONS); + EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_FULL)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); +} + +TEST_F_S_MOCKED(PersistStateSkippedDueToEqValues) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); + EXPECT_TRUE(getImpl().persistState(ut_consts::SDCS_FULL)); + EXPECT_EQ(getImpl().getState(), ut_consts::SDCS_FULL); +} diff --git a/tests/unit/windows/test_settingsmanagerapply.cpp b/tests/unit/windows/test_settingsmanagerapply.cpp new file mode 100644 index 0000000..2e4768d --- /dev/null +++ b/tests/unit/windows/test_settingsmanagerapply.cpp @@ -0,0 +1,498 @@ +// local includes +#include "displaydevice/windows/settingsmanager.h" +#include "fixtures/fixtures.h" +#include "fixtures/mockaudiocontext.h" +#include "fixtures/mocksettingspersistence.h" +#include "utils/comparison.h" +#include "utils/helpers.h" +#include "utils/mockwindisplaydevice.h" + +namespace { + // Convenience keywords for GMock + using ::testing::_; + using ::testing::HasSubstr; + using ::testing::InSequence; + using ::testing::Return; + using ::testing::StrictMock; + + // Additional convenience global const(s) + const display_device::ActiveTopology DEFAULT_CURRENT_TOPOLOGY { { "DeviceId1", "DeviceId2" }, { "DeviceId3" } }; + const display_device::EnumeratedDeviceList DEFAULT_DEVICES { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId2", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId4" } + }; + const display_device::EnumeratedDeviceList DEVICES_WITH_NO_PRIMARY { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId2", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId4" } + }; + const display_device::SingleDisplayConfigState DEFAULT_PERSISTENCE_INPUT_BASE { { DEFAULT_CURRENT_TOPOLOGY, { "DeviceId1", "DeviceId2" } } }; + + // Test fixture(s) for this file + class SettingsManagerApplyMocked: public BaseTest { + public: + display_device::SettingsManager & + getImpl() { + if (!m_impl) { + m_impl = std::make_unique(m_dd_api, m_audio_context_api, std::make_unique(m_settings_persistence_api)); + } + + return *m_impl; + } + + void + expectedDefaultCallsUntilTopologyPrep(InSequence &sequence /* To ensure that sequence is created outside this scope */, const display_device::ActiveTopology &topology = DEFAULT_CURRENT_TOPOLOGY, const std::optional &state = ut_consts::SDCS_EMPTY) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(state))) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, getCurrentTopology()) + .Times(1) + .WillOnce(Return(topology)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, isTopologyValid(topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedIsCapturedCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const bool is_captured) { + EXPECT_CALL(*m_audio_context_api, isCaptured()) + .Times(1) + .WillOnce(Return(is_captured)) + .RetiresOnSaturation(); + } + + void + expectedCaptureCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const bool success = true) { + EXPECT_CALL(*m_audio_context_api, capture()) + .Times(1) + .WillOnce(Return(success)) + .RetiresOnSaturation(); + } + + void + expectedReleaseCall(InSequence &sequence /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_audio_context_api, release()) + .Times(1) + .RetiresOnSaturation(); + } + + void + expectedTopologyGuardTopologyCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const display_device::ActiveTopology &topology = DEFAULT_CURRENT_TOPOLOGY, const bool success = true) { + EXPECT_CALL(*m_dd_api, setTopology(topology)) + .Times(1) + .WillOnce(Return(success)) + .RetiresOnSaturation(); + } + + void + expectedDeviceEnumCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const display_device::EnumeratedDeviceList &devices = DEFAULT_DEVICES) { + EXPECT_CALL(*m_dd_api, enumAvailableDevices()) + .Times(1) + .WillOnce(Return(devices)) + .RetiresOnSaturation(); + } + + void + expectedIsTopologyTheSameCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const display_device::ActiveTopology &lhs, const display_device::ActiveTopology &rhs) { + EXPECT_CALL(*m_dd_api, isTopologyTheSame(lhs, rhs)) + .Times(1) + .WillOnce(Return(lhs == rhs)) + .RetiresOnSaturation(); + } + + void + expectedSetTopologyCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const display_device::ActiveTopology &topology) { + EXPECT_CALL(*m_dd_api, setTopology(topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedPersistenceCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const std::optional &state, const bool success = true) { + EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(state))) + .Times(1) + .WillOnce(Return(success)) + .RetiresOnSaturation(); + } + + void + expectedTopologyGuardNewlyCapturedContextCall(InSequence &sequence /* To ensure that sequence is created outside this scope */, const bool is_captured) { + EXPECT_CALL(*m_audio_context_api, isCaptured()) + .Times(1) + .WillOnce(Return(is_captured)) + .RetiresOnSaturation(); + + if (is_captured) { + EXPECT_CALL(*m_audio_context_api, release()) + .Times(1) + .RetiresOnSaturation(); + } + } + + std::shared_ptr> m_dd_api { std::make_shared>() }; + std::shared_ptr> m_settings_persistence_api { std::make_shared>() }; + std::shared_ptr> m_audio_context_api { std::make_shared>() }; + + private: + std::unique_ptr m_impl; + }; + + // Specialized TEST macro(s) for this test +#define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, SettingsManagerApplyMocked, __VA_ARGS__) +} // namespace + +TEST_F_S_MOCKED(NoApiAccess) { + InSequence sequence; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(getImpl().applySettings({}), display_device::SettingsManager::ApplyResult::ApiTemporarilyUnavailable); +} + +TEST_F_S_MOCKED(CurrentTopologyIsInvalid) { + InSequence sequence; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(true)); + EXPECT_CALL(*m_dd_api, getCurrentTopology()) + .Times(1) + .WillOnce(Return(DEFAULT_CURRENT_TOPOLOGY)); + EXPECT_CALL(*m_dd_api, isTopologyValid(DEFAULT_CURRENT_TOPOLOGY)) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_EQ(getImpl().applySettings({}), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, FailedToEnumerateDevices) { + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + EXPECT_CALL(*m_dd_api, enumAvailableDevices()) + .Times(1) + .WillOnce(Return(display_device::EnumeratedDeviceList {})) + .RetiresOnSaturation(); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({}), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, DeviceNotAvailable) { + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceIdX" }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, FailedToComputeInitialState) { + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence, DEVICES_WITH_NO_PRIMARY); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1" }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, FailedToStripInitialState) { + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence, { { .m_device_id = "DeviceId4", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } } }); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId4" }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, DeviceNotFoundInNewTopology) { + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId4" }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, NoChangeIsNeeded) { + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { DEFAULT_CURRENT_TOPOLOGY }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedPersistenceCall(sequence, persistence_input); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1" }), display_device::SettingsManager::ApplyResult::Ok); +} + +TEST_F_S_MOCKED(PrepareTopology, RevertingSettingsFailed) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence, DEFAULT_CURRENT_TOPOLOGY, ut_consts::SDCS_FULL); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_modified.m_topology, { { "DeviceId1" } }); + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, AudioContextCaptureFailed) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsCapturedCall(sequence, false); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedCaptureCall(sequence, false); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, TopologyChangeFailed) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsCapturedCall(sequence, false); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedCaptureCall(sequence, true); + EXPECT_CALL(*m_dd_api, setTopology(display_device::ActiveTopology { { "DeviceId1" } })) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, true); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::DevicePrepFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, AudioContextCaptured) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { { { "DeviceId1" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsCapturedCall(sequence, false); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedCaptureCall(sequence, true); + expectedSetTopologyCall(sequence, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedPersistenceCall(sequence, persistence_input); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::Ok); +} + +TEST_F_S_MOCKED(PrepareTopology, AudioContextCaptureSkipped, NotInitialTopologySwitch) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { *ut_consts::SDCS_NO_MODIFICATIONS }; + persistence_input.m_modified = { { { "DeviceId1" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence, DEFAULT_CURRENT_TOPOLOGY, ut_consts::SDCS_NO_MODIFICATIONS); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_modified.m_topology, { { "DeviceId1" } }); + + expectedIsCapturedCall(sequence, false); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_initial.m_topology, DEFAULT_CURRENT_TOPOLOGY); + expectedSetTopologyCall(sequence, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_initial.m_topology, { { "DeviceId1" } }); + expectedPersistenceCall(sequence, persistence_input); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::Ok); +} + +TEST_F_S_MOCKED(PrepareTopology, AudioContextCaptureSkipped, NoDevicesAreGone) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { { { "DeviceId1", "DeviceId2" }, { "DeviceId3" }, { "DeviceId4" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, persistence_input.m_modified.m_topology); + expectedIsCapturedCall(sequence, false); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedSetTopologyCall(sequence, persistence_input.m_modified.m_topology); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, persistence_input.m_modified.m_topology); + expectedPersistenceCall(sequence, persistence_input); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId4", .m_device_prep = DevicePrep::EnsureActive }), display_device::SettingsManager::ApplyResult::Ok); +} + +TEST_F_S_MOCKED(AudioContextDelayedRelease) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { *ut_consts::SDCS_NO_MODIFICATIONS }; + persistence_input.m_modified = { { { "DeviceId1" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence, DEFAULT_CURRENT_TOPOLOGY, ut_consts::SDCS_NO_MODIFICATIONS); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_modified.m_topology, { { "DeviceId1" } }); + + expectedIsCapturedCall(sequence, true); + expectedSetTopologyCall(sequence, persistence_input.m_initial.m_topology); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_initial.m_topology, { { "DeviceId1" } }); + expectedPersistenceCall(sequence, persistence_input); + expectedReleaseCall(sequence); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::Ok); +} + +TEST_F_S_MOCKED(FailedToSaveNewState) { + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { DEFAULT_CURRENT_TOPOLOGY }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedPersistenceCall(sequence, persistence_input, false); + + expectedTopologyGuardTopologyCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1" }), display_device::SettingsManager::ApplyResult::PersistenceSaveFailed); +} + +TEST_F_S_MOCKED(AudioContextDelayedRelease, ViaGuard) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { *ut_consts::SDCS_NO_MODIFICATIONS }; + persistence_input.m_modified = { { { "DeviceId1" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence, DEFAULT_CURRENT_TOPOLOGY, ut_consts::SDCS_NO_MODIFICATIONS); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_modified.m_topology, { { "DeviceId1" } }); + + expectedIsCapturedCall(sequence, true); + expectedSetTopologyCall(sequence, persistence_input.m_initial.m_topology); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_initial.m_topology, { { "DeviceId1" } }); + + expectedPersistenceCall(sequence, persistence_input, false); + + expectedTopologyGuardTopologyCall(sequence, DEFAULT_CURRENT_TOPOLOGY, false); + expectedReleaseCall(sequence); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::PersistenceSaveFailed); +} + +TEST_F_S_MOCKED(AudioContextDelayedRelease, SkippedDueToFailure) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + auto persistence_input { *ut_consts::SDCS_NO_MODIFICATIONS }; + persistence_input.m_modified = { { { "DeviceId1" } } }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence, DEFAULT_CURRENT_TOPOLOGY, ut_consts::SDCS_NO_MODIFICATIONS); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, { { "DeviceId1" } }); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_modified.m_topology, { { "DeviceId1" } }); + + expectedIsCapturedCall(sequence, true); + expectedSetTopologyCall(sequence, persistence_input.m_initial.m_topology); + expectedIsTopologyTheSameCall(sequence, ut_consts::SDCS_FULL->m_initial.m_topology, { { "DeviceId1" } }); + + expectedPersistenceCall(sequence, persistence_input, false); + + expectedTopologyGuardTopologyCall(sequence, DEFAULT_CURRENT_TOPOLOGY, true); + expectedTopologyGuardNewlyCapturedContextCall(sequence, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1", .m_device_prep = DevicePrep::EnsureOnlyDisplay }), display_device::SettingsManager::ApplyResult::PersistenceSaveFailed); +} + +TEST_F_S_MOCKED(TopologyGuardFailedButNoContextIsReleased) { + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { DEFAULT_CURRENT_TOPOLOGY }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, true); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedPersistenceCall(sequence, persistence_input, false); + + expectedTopologyGuardTopologyCall(sequence, DEFAULT_CURRENT_TOPOLOGY, false); + + EXPECT_EQ(getImpl().applySettings({ .m_device_id = "DeviceId1" }), display_device::SettingsManager::ApplyResult::PersistenceSaveFailed); +} + +TEST_F_S_MOCKED(PrepareTopology, PrimaryDeviceIsUsed) { + auto persistence_input { DEFAULT_PERSISTENCE_INPUT_BASE }; + persistence_input.m_modified = { DEFAULT_CURRENT_TOPOLOGY }; + + InSequence sequence; + expectedDefaultCallsUntilTopologyPrep(sequence); + expectedIsCapturedCall(sequence, false); + expectedDeviceEnumCall(sequence); + expectedIsTopologyTheSameCall(sequence, DEFAULT_CURRENT_TOPOLOGY, DEFAULT_CURRENT_TOPOLOGY); + expectedPersistenceCall(sequence, persistence_input); + + EXPECT_EQ(getImpl().applySettings({}), display_device::SettingsManager::ApplyResult::Ok); +} diff --git a/tests/unit/windows/test_settingsmanagergeneral.cpp b/tests/unit/windows/test_settingsmanagergeneral.cpp index 9155572..69b07b7 100644 --- a/tests/unit/windows/test_settingsmanagergeneral.cpp +++ b/tests/unit/windows/test_settingsmanagergeneral.cpp @@ -6,45 +6,60 @@ #include "fixtures/mockaudiocontext.h" #include "fixtures/mocksettingspersistence.h" #include "utils/comparison.h" +#include "utils/helpers.h" #include "utils/mockwindisplaydevice.h" namespace { // Convenience keywords for GMock using ::testing::_; using ::testing::HasSubstr; + using ::testing::InSequence; using ::testing::Return; using ::testing::StrictMock; // Test fixture(s) for this file class SettingsManagerGeneralMocked: public BaseTest { public: + display_device::SettingsManager & + getImpl() { + if (!m_impl) { + m_impl = std::make_unique(m_dd_api, m_audio_context_api, std::make_unique(m_settings_persistence_api)); + } + + return *m_impl; + } + std::shared_ptr> m_dd_api { std::make_shared>() }; std::shared_ptr> m_settings_persistence_api { std::make_shared>() }; std::shared_ptr> m_audio_context_api { std::make_shared>() }; - display_device::SettingsManager m_impl { m_dd_api, m_settings_persistence_api, m_audio_context_api }; + + private: + std::unique_ptr m_impl; }; - // Specialized TEST macro(s) for this test SettingsManagerGeneralMocked + // Specialized TEST macro(s) for this test #define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, SettingsManagerGeneralMocked, __VA_ARGS__) } // namespace -TEST_F_S_MOCKED(NullptrDisplayDeviceApuProvided) { +TEST_F_S_MOCKED(NullptrDisplayDeviceApiProvided) { EXPECT_THAT([]() { const display_device::SettingsManager settings_manager(nullptr, nullptr, nullptr); }, ThrowsMessage(HasSubstr("Nullptr provided for WinDisplayDeviceInterface in SettingsManager!"))); } -TEST_F_S_MOCKED(NoopAudioAndSettingsContext) { +TEST_F_S_MOCKED(NoopAudioContext) { class NakedSettingsManager: public display_device::SettingsManager { public: - using display_device::SettingsManager::SettingsManager; - - using display_device::SettingsManager::m_audio_context_api; - using display_device::SettingsManager::m_settings_persistence_api; + using SettingsManager::m_audio_context_api; + using SettingsManager::SettingsManager; }; - const NakedSettingsManager settings_manager { std::make_shared>(), nullptr, nullptr }; + const NakedSettingsManager settings_manager { m_dd_api, nullptr, std::make_unique(nullptr) }; EXPECT_TRUE(std::dynamic_pointer_cast(settings_manager.m_audio_context_api) != nullptr); - EXPECT_TRUE(std::dynamic_pointer_cast(settings_manager.m_settings_persistence_api) != nullptr); +} + +TEST_F_S_MOCKED(NullptrPersistentStateProvided) { + EXPECT_THAT([this]() { const display_device::SettingsManager settings_manager(m_dd_api, nullptr, nullptr); }, + ThrowsMessage(HasSubstr("Nullptr provided for PersistentState in SettingsManager!"))); } TEST_F_S_MOCKED(EnumAvailableDevices) { @@ -55,17 +70,23 @@ TEST_F_S_MOCKED(EnumAvailableDevices) { std::nullopt } }; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); EXPECT_CALL(*m_dd_api, enumAvailableDevices()) .Times(1) .WillOnce(Return(test_list)); - EXPECT_EQ(m_impl.enumAvailableDevices(), test_list); + EXPECT_EQ(getImpl().enumAvailableDevices(), test_list); } TEST_F_S_MOCKED(GetDisplayName) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); EXPECT_CALL(*m_dd_api, getDisplayName("DeviceId1")) .Times(1) .WillOnce(Return("DeviceName1")); - EXPECT_EQ(m_impl.getDisplayName("DeviceId1"), "DeviceName1"); + EXPECT_EQ(getImpl().getDisplayName("DeviceId1"), "DeviceName1"); } diff --git a/tests/unit/windows/test_settingsmanagerrevert.cpp b/tests/unit/windows/test_settingsmanagerrevert.cpp new file mode 100644 index 0000000..77a0c23 --- /dev/null +++ b/tests/unit/windows/test_settingsmanagerrevert.cpp @@ -0,0 +1,466 @@ +// local includes +#include "displaydevice/windows/settingsmanager.h" +#include "displaydevice/windows/settingsutils.h" +#include "fixtures/fixtures.h" +#include "fixtures/mockaudiocontext.h" +#include "fixtures/mocksettingspersistence.h" +#include "utils/comparison.h" +#include "utils/helpers.h" +#include "utils/mockwindisplaydevice.h" + +namespace { + // Convenience keywords for GMock + using ::testing::_; + using ::testing::HasSubstr; + using ::testing::InSequence; + using ::testing::Return; + using ::testing::StrictMock; + + // Additional convenience global const(s) + const display_device::ActiveTopology CURRENT_TOPOLOGY { { "DeviceId4" } }; + const display_device::HdrStateMap CURRENT_MODIFIED_HDR_STATES { + { "DeviceId2", { display_device::HdrState::Enabled } }, + { "DeviceId3", std::nullopt } + }; + const display_device::DeviceDisplayModeMap CURRENT_MODIFIED_DISPLAY_MODES { + { "DeviceId2", { { 123, 456 }, { 120, 1 } } }, + { "DeviceId3", { { 456, 123 }, { 60, 1 } } } + }; + const std::string CURRENT_MODIFIED_PRIMARY_DEVICE { "DeviceId2" }; + + // Test fixture(s) for this file + class SettingsManagerRevertMocked: public BaseTest { + public: + display_device::SettingsManager & + getImpl() { + if (!m_impl) { + m_impl = std::make_unique(m_dd_api, m_audio_context_api, std::make_unique(m_settings_persistence_api)); + } + + return *m_impl; + } + + void + expectedDefaultCallsUntilModifiedSettings(InSequence &sequence /* To ensure that sequence is created outside this scope */, const std::optional &state = ut_consts::SDCS_FULL) { + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(state))) + .RetiresOnSaturation(); + expectedDefaultCallsUntilModifiedSettingsNoPersistence(sequence); + } + + void + expectedDefaultCallsUntilModifiedSettingsNoPersistence(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, getCurrentTopology()) + .Times(1) + .WillOnce(Return(CURRENT_TOPOLOGY)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, isTopologyValid(CURRENT_TOPOLOGY)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultMofifiedTopologyCalls(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, setTopology(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultHdrStateGuardInitCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, getCurrentHdrStates(display_device::win_utils::flattenTopology(ut_consts::SDCS_FULL->m_modified.m_topology))) + .Times(1) + .WillOnce(Return(CURRENT_MODIFIED_HDR_STATES)) + .RetiresOnSaturation(); + } + + void + expectedDefaultHdrStateGuardCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setHdrStates(CURRENT_MODIFIED_HDR_STATES)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultDisplayModeGuardInitCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, getCurrentDisplayModes(display_device::win_utils::flattenTopology(ut_consts::SDCS_FULL->m_modified.m_topology))) + .Times(1) + .WillOnce(Return(CURRENT_MODIFIED_DISPLAY_MODES)) + .RetiresOnSaturation(); + } + + void + expectedDefaultDisplayModeGuardCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setDisplayModes(CURRENT_MODIFIED_DISPLAY_MODES)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultPrimaryDeviceGuardInitCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, isPrimary(CURRENT_MODIFIED_PRIMARY_DEVICE)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultPrimaryDeviceGuardCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setAsPrimary(CURRENT_MODIFIED_PRIMARY_DEVICE)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultHdrStateCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setHdrStates(ut_consts::SDCS_FULL->m_modified.m_original_hdr_states)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultDisplayModeCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setDisplayModes(ut_consts::SDCS_FULL->m_modified.m_original_modes)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultPrimaryDeviceCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setAsPrimary(ut_consts::SDCS_FULL->m_modified.m_original_primary_device)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultRevertModifiedSettingsCall(InSequence &sequence /* To ensure that sequence is created outside this scope */) { + auto expected_persistent_input { ut_consts::SDCS_FULL }; + expected_persistent_input->m_modified = { expected_persistent_input->m_modified.m_topology }; + + expectedDefaultMofifiedTopologyCalls(sequence); + expectedDefaultHdrStateGuardInitCall(sequence); + expectedDefaultHdrStateCall(sequence); + expectedDefaultDisplayModeGuardInitCall(sequence); + expectedDefaultDisplayModeCall(sequence); + expectedDefaultPrimaryDeviceGuardInitCall(sequence); + expectedDefaultPrimaryDeviceCall(sequence); + EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(expected_persistent_input))) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultInitialTopologyCalls(InSequence &sequence /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, setTopology(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultFinalPersistenceCalls(InSequence &sequence /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_settings_persistence_api, clear()) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + void + expectedDefaultAudioContextCalls(InSequence &sequence /* To ensure that sequence is created outside this scope */, const bool audio_captured) { + EXPECT_CALL(*m_audio_context_api, isCaptured()) + .Times(1) + .WillOnce(Return(audio_captured)) + .RetiresOnSaturation(); + + if (audio_captured) { + EXPECT_CALL(*m_audio_context_api, release()) + .Times(1) + .RetiresOnSaturation(); + } + } + + void + expectedDefaultTopologyGuardCall(InSequence & /* To ensure that sequence is created outside this scope */) { + EXPECT_CALL(*m_dd_api, setTopology(CURRENT_TOPOLOGY)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + } + + std::shared_ptr> m_dd_api { std::make_shared>() }; + std::shared_ptr> m_settings_persistence_api { std::make_shared>() }; + std::shared_ptr> m_audio_context_api { std::make_shared>() }; + + private: + std::unique_ptr m_impl; + }; + + // Specialized TEST macro(s) for this test +#define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, SettingsManagerRevertMocked, __VA_ARGS__) +} // namespace + +TEST_F_S_MOCKED(NoSettingsAvailable) { + InSequence sequence; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_EMPTY))); + + EXPECT_TRUE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(NoApiAccess) { + InSequence sequence; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(InvalidCurrentTopology) { + InSequence sequence; + EXPECT_CALL(*m_settings_persistence_api, load()) + .Times(1) + .WillOnce(Return(serializeState(ut_consts::SDCS_FULL))); + EXPECT_CALL(*m_dd_api, isApiAccessAvailable()) + .Times(1) + .WillOnce(Return(true)); + EXPECT_CALL(*m_dd_api, getCurrentTopology()) + .Times(1) + .WillOnce(Return(CURRENT_TOPOLOGY)); + EXPECT_CALL(*m_dd_api, isTopologyValid(CURRENT_TOPOLOGY)) + .Times(1) + .WillOnce(Return(false)); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, InvalidModifiedTopology) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(false)); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, FailedToSetModifiedTopology) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(true)); + EXPECT_CALL(*m_dd_api, setTopology(ut_consts::SDCS_FULL->m_modified.m_topology)) + .Times(1) + .WillOnce(Return(false)); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, FailedToRevertHdrStates) { + auto sdcs_stripped { ut_consts::SDCS_FULL }; + sdcs_stripped->m_modified.m_original_modes.clear(); + sdcs_stripped->m_modified.m_original_primary_device.clear(); + + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence, sdcs_stripped); + expectedDefaultMofifiedTopologyCalls(sequence); + expectedDefaultHdrStateGuardInitCall(sequence); + EXPECT_CALL(*m_dd_api, setHdrStates(ut_consts::SDCS_FULL->m_modified.m_original_hdr_states)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultHdrStateGuardCall(sequence); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, FailedToRevertDisplayModes) { + auto sdcs_stripped { ut_consts::SDCS_FULL }; + sdcs_stripped->m_modified.m_original_hdr_states.clear(); + sdcs_stripped->m_modified.m_original_primary_device.clear(); + + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence, sdcs_stripped); + expectedDefaultMofifiedTopologyCalls(sequence); + expectedDefaultDisplayModeGuardInitCall(sequence); + EXPECT_CALL(*m_dd_api, setDisplayModes(ut_consts::SDCS_FULL->m_modified.m_original_modes)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultDisplayModeGuardCall(sequence); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, FailedToRevertPrimaryDevice) { + auto sdcs_stripped { ut_consts::SDCS_FULL }; + sdcs_stripped->m_modified.m_original_hdr_states.clear(); + sdcs_stripped->m_modified.m_original_modes.clear(); + + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence, sdcs_stripped); + expectedDefaultMofifiedTopologyCalls(sequence); + expectedDefaultPrimaryDeviceGuardInitCall(sequence); + EXPECT_CALL(*m_dd_api, setAsPrimary(ut_consts::SDCS_FULL->m_modified.m_original_primary_device)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultPrimaryDeviceGuardCall(sequence); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(RevertModifiedSettings, FailedToSetPersistence) { + auto expected_persistent_input { ut_consts::SDCS_FULL }; + expected_persistent_input->m_modified = { expected_persistent_input->m_modified.m_topology }; + + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultMofifiedTopologyCalls(sequence); + expectedDefaultHdrStateGuardInitCall(sequence); + expectedDefaultHdrStateCall(sequence); + expectedDefaultDisplayModeGuardInitCall(sequence); + expectedDefaultDisplayModeCall(sequence); + expectedDefaultPrimaryDeviceGuardInitCall(sequence); + expectedDefaultPrimaryDeviceCall(sequence); + EXPECT_CALL(*m_settings_persistence_api, store(*serializeState(expected_persistent_input))) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultPrimaryDeviceGuardCall(sequence); + expectedDefaultDisplayModeGuardCall(sequence); + expectedDefaultHdrStateGuardCall(sequence); + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(InvalidInitialTopology) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(FailedToSetInitialTopology) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(*m_dd_api, setTopology(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(FailedToClearPersistence) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + expectedDefaultInitialTopologyCalls(sequence); + EXPECT_CALL(*m_settings_persistence_api, clear()) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultTopologyGuardCall(sequence); + + EXPECT_FALSE(getImpl().revertSettings()); +} + +TEST_F_S_MOCKED(SuccesfullyReverted, WithAudioCapture) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + expectedDefaultInitialTopologyCalls(sequence); + expectedDefaultFinalPersistenceCalls(sequence); + expectedDefaultAudioContextCalls(sequence, true); + + EXPECT_TRUE(getImpl().revertSettings()); + EXPECT_TRUE(getImpl().revertSettings()); // Seconds call after success is NOOP +} + +TEST_F_S_MOCKED(SuccesfullyReverted, NoAudioCapture) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + expectedDefaultInitialTopologyCalls(sequence); + expectedDefaultFinalPersistenceCalls(sequence); + expectedDefaultAudioContextCalls(sequence, false); + + EXPECT_TRUE(getImpl().revertSettings()); + EXPECT_TRUE(getImpl().revertSettings()); // Seconds call after success is NOOP +} + +TEST_F_S_MOCKED(RevertModifiedSettings, CachedSettingsAreUpdated) { + InSequence sequence; + expectedDefaultCallsUntilModifiedSettings(sequence); + expectedDefaultRevertModifiedSettingsCall(sequence); + EXPECT_CALL(*m_dd_api, isTopologyValid(ut_consts::SDCS_FULL->m_initial.m_topology)) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + expectedDefaultTopologyGuardCall(sequence); + EXPECT_FALSE(getImpl().revertSettings()); + + expectedDefaultCallsUntilModifiedSettingsNoPersistence(sequence); + // No need for expectedDefaultRevertModifiedSettingsCall anymore. + expectedDefaultInitialTopologyCalls(sequence); + expectedDefaultFinalPersistenceCalls(sequence); + expectedDefaultAudioContextCalls(sequence, false); + + EXPECT_TRUE(getImpl().revertSettings()); +} diff --git a/tests/unit/windows/test_settingsutils.cpp b/tests/unit/windows/test_settingsutils.cpp new file mode 100644 index 0000000..3ed87d3 --- /dev/null +++ b/tests/unit/windows/test_settingsutils.cpp @@ -0,0 +1,292 @@ +// local includes +#include "displaydevice/windows/settingsutils.h" +#include "fixtures/fixtures.h" +#include "utils/comparison.h" +#include "utils/mockwindisplaydevice.h" + +namespace { + // Convenience keywords for GMock + using ::testing::_; + using ::testing::Return; + using ::testing::StrictMock; + + // Test fixture(s) for this file + class SettingsUtilsMocked: public BaseTest { + public: + StrictMock m_dd_api {}; + }; + +// Specialized TEST macro(s) for this test SettingsManagerGeneralMocked +#define TEST_F_S_MOCKED(...) DD_MAKE_TEST(TEST_F, SettingsUtilsMocked, __VA_ARGS__) + + // Additional convenience global const(s) + const display_device::ActiveTopology DEFAULT_INITIAL_TOPOLOGY { { "DeviceId1", "DeviceId2" }, { "DeviceId3" } }; +} // namespace + +TEST_F_S_MOCKED(FlattenTopology) { + EXPECT_EQ(display_device::win_utils::flattenTopology({ { "DeviceId1" }, { "DeviceId2", "DeviceId3" }, {}, { "DeviceId2" } }), (std::set { "DeviceId1", "DeviceId2", "DeviceId3" })); + EXPECT_EQ(display_device::win_utils::flattenTopology({ {}, {}, {} }), std::set {}); + EXPECT_EQ(display_device::win_utils::flattenTopology({}), std::set {}); +} + +TEST_F_S_MOCKED(ComputeInitialState, PreviousStateIsUsed) { + const display_device::SingleDisplayConfigState::Initial prev_state { DEFAULT_INITIAL_TOPOLOGY }; + EXPECT_EQ(display_device::win_utils::computeInitialState(prev_state, {}, {}), std::make_optional(prev_state)); +} + +TEST_F_S_MOCKED(ComputeInitialState, NewStateIsComputed) { + const display_device::ActiveTopology topology_before_changes { DEFAULT_INITIAL_TOPOLOGY }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId2", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId4" } + }; + + EXPECT_EQ(display_device::win_utils::computeInitialState(std::nullopt, topology_before_changes, devices), + (display_device::SingleDisplayConfigState::Initial { topology_before_changes, { "DeviceId1", "DeviceId2" } })); +} + +TEST_F_S_MOCKED(ComputeInitialState, NoPrimaryDevices) { + EXPECT_EQ(display_device::win_utils::computeInitialState(std::nullopt, { { "DeviceId1", "DeviceId2" } }, {}), std::nullopt); +} + +TEST_F_S_MOCKED(ComputeNewTopology, VerifyOnly) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::VerifyOnly, false, "DeviceId4", { "DeviceId5", "DeviceId6" }, DEFAULT_INITIAL_TOPOLOGY), DEFAULT_INITIAL_TOPOLOGY); +} + +TEST_F_S_MOCKED(ComputeNewTopology, EnsureOnlyDisplay) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsureOnlyDisplay, true, "DeviceId4", { "DeviceId5", "DeviceId6" }, DEFAULT_INITIAL_TOPOLOGY), + (display_device::ActiveTopology { { "DeviceId4", "DeviceId5", "DeviceId6" } })); + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsureOnlyDisplay, false, "DeviceId4", { "DeviceId5", "DeviceId6" }, DEFAULT_INITIAL_TOPOLOGY), + display_device::ActiveTopology { { "DeviceId4" } }); +} + +TEST_F_S_MOCKED(ComputeNewTopology, EnsureActive) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsureActive, true, "DeviceId4", { "DeviceId5", "DeviceId6" }, { { "DeviceId4" } }), + display_device::ActiveTopology { { "DeviceId4" } }); + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsureActive, true, "DeviceId4", { "DeviceId5", "DeviceId6" }, { { "DeviceId3" } }), + (display_device::ActiveTopology { { "DeviceId3" }, { "DeviceId4" } })); +} + +TEST_F_S_MOCKED(ComputeNewTopology, EnsurePrimary) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsurePrimary, true, "DeviceId4", { "DeviceId5", "DeviceId6" }, { { "DeviceId4" } }), + display_device::ActiveTopology { { "DeviceId4" } }); + EXPECT_EQ(display_device::win_utils::computeNewTopology(DevicePrep::EnsurePrimary, true, "DeviceId4", { "DeviceId5", "DeviceId6" }, { { "DeviceId3" } }), + (display_device::ActiveTopology { { "DeviceId3" }, { "DeviceId4" } })); +} + +TEST_F_S_MOCKED(StripInitialState, NoStripIsPerformed) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId2", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId4" } + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), initial_state); +} + +TEST_F_S_MOCKED(StripInitialState, AllDevicesAreStripped) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId4" } + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), std::nullopt); +} + +TEST_F_S_MOCKED(StripInitialState, OneNonPrimaryDeviceStripped) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + { .m_device_id = "DeviceId2", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } } + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), (display_device::SingleDisplayConfigState::Initial { { { "DeviceId1", "DeviceId2" } }, { "DeviceId1", "DeviceId2" } })); +} + +TEST_F_S_MOCKED(StripInitialState, OnePrimaryDeviceStripped) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId1", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } }, + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } }, + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), (display_device::SingleDisplayConfigState::Initial { { { "DeviceId1" }, { "DeviceId3" } }, { "DeviceId1" } })); +} + +TEST_F_S_MOCKED(StripInitialState, PrimaryDevicesCompletelyStripped) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = false } } + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), std::nullopt); +} + +TEST_F_S_MOCKED(StripInitialState, PrimaryDevicesCompletelyReplaced) { + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + const display_device::EnumeratedDeviceList devices { + { .m_device_id = "DeviceId3", .m_info = display_device::EnumeratedDevice::Info { .m_primary = true } } + }; + + EXPECT_EQ(display_device::win_utils::stripInitialState(initial_state, devices), (display_device::SingleDisplayConfigState::Initial { { { "DeviceId3" } }, { "DeviceId3" } })); +} + +TEST_F_S_MOCKED(ComputeNewTopologyAndMetadata, EmptyDeviceId, AdditionalDevicesNotStripped) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + const std::string device_id {}; + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + + const auto &[new_topology, device_to_configure, additional_devices_to_configure] = + display_device::win_utils::computeNewTopologyAndMetadata(DevicePrep::EnsureActive, device_id, initial_state); + EXPECT_EQ(new_topology, DEFAULT_INITIAL_TOPOLOGY); + EXPECT_EQ(device_to_configure, "DeviceId1"); + EXPECT_EQ(additional_devices_to_configure, std::set { "DeviceId2" }); +} + +TEST_F_S_MOCKED(ComputeNewTopologyAndMetadata, EmptyDeviceId, AdditionalDevicesStripped) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + const std::string device_id {}; + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId3", "DeviceId4" } }; + + const auto &[new_topology, device_to_configure, additional_devices_to_configure] = + display_device::win_utils::computeNewTopologyAndMetadata(DevicePrep::EnsureActive, device_id, initial_state); + EXPECT_EQ(new_topology, DEFAULT_INITIAL_TOPOLOGY); + EXPECT_EQ(device_to_configure, "DeviceId3"); + EXPECT_EQ(additional_devices_to_configure, std::set {}); +} + +TEST_F_S_MOCKED(ComputeNewTopologyAndMetadata, ValidDeviceId, WithAdditionalDevices) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + const std::string device_id { "DeviceId1" }; + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + + const auto &[new_topology, device_to_configure, additional_devices_to_configure] = + display_device::win_utils::computeNewTopologyAndMetadata(DevicePrep::EnsureActive, device_id, initial_state); + EXPECT_EQ(new_topology, DEFAULT_INITIAL_TOPOLOGY); + EXPECT_EQ(device_to_configure, device_id); + EXPECT_EQ(additional_devices_to_configure, std::set { "DeviceId2" }); +} + +TEST_F_S_MOCKED(ComputeNewTopologyAndMetadata, ValidDeviceId, NoAdditionalDevices) { + using DevicePrep = display_device::SingleDisplayConfiguration::DevicePreparation; + const std::string device_id { "DeviceId1" }; + const display_device::SingleDisplayConfigState::Initial initial_state { DEFAULT_INITIAL_TOPOLOGY, { "DeviceId1", "DeviceId2" } }; + + const auto &[new_topology, device_to_configure, additional_devices_to_configure] = + display_device::win_utils::computeNewTopologyAndMetadata(DevicePrep::EnsureOnlyDisplay, device_id, initial_state); + EXPECT_EQ(new_topology, display_device::ActiveTopology { { "DeviceId1" } }); + EXPECT_EQ(device_to_configure, device_id); + EXPECT_EQ(additional_devices_to_configure, std::set {}); +} + +TEST_F_S_MOCKED(TopologyGuardFn, Success) { + EXPECT_CALL(m_dd_api, setTopology(display_device::ActiveTopology { { "DeviceId1" } })) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::topologyGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(TopologyGuardFn, Failure) { + EXPECT_CALL(m_dd_api, setTopology(display_device::ActiveTopology { { "DeviceId1" } })) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::topologyGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(ModeGuardFn, Success) { + EXPECT_CALL(m_dd_api, getCurrentDisplayModes(std::set { "DeviceId1" })) + .Times(1) + .WillOnce(Return(display_device::DeviceDisplayModeMap { { "DeviceId1", {} } })) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setDisplayModes(display_device::DeviceDisplayModeMap { { "DeviceId1", {} } })) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::modeGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(ModeGuardFn, Failure) { + EXPECT_CALL(m_dd_api, getCurrentDisplayModes(std::set { "DeviceId1" })) + .Times(1) + .WillOnce(Return(display_device::DeviceDisplayModeMap { { "DeviceId1", {} } })) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setDisplayModes(display_device::DeviceDisplayModeMap { { "DeviceId1", {} } })) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::modeGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(PrimaryGuardFn, Success) { + EXPECT_CALL(m_dd_api, isPrimary("DeviceId1")) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setAsPrimary("DeviceId1")) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::primaryGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(PrimaryGuardFn, Failure) { + EXPECT_CALL(m_dd_api, isPrimary("DeviceId1")) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setAsPrimary("")) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::primaryGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(HdrStateGuardFn, Success) { + EXPECT_CALL(m_dd_api, getCurrentHdrStates(std::set { "DeviceId1" })) + .Times(1) + .WillOnce(Return(display_device::HdrStateMap { { "DeviceId1", {} } })) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setHdrStates(display_device::HdrStateMap { { "DeviceId1", {} } })) + .Times(1) + .WillOnce(Return(true)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::hdrStateGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} + +TEST_F_S_MOCKED(HdrStateGuardFn, Failure) { + EXPECT_CALL(m_dd_api, getCurrentHdrStates(std::set { "DeviceId1" })) + .Times(1) + .WillOnce(Return(display_device::HdrStateMap { { "DeviceId1", {} } })) + .RetiresOnSaturation(); + EXPECT_CALL(m_dd_api, setHdrStates(display_device::HdrStateMap { { "DeviceId1", {} } })) + .Times(1) + .WillOnce(Return(false)) + .RetiresOnSaturation(); + + const auto guard_fn { display_device::win_utils::hdrStateGuardFn(m_dd_api, { { "DeviceId1" } }) }; + EXPECT_NO_THROW(guard_fn()); +} diff --git a/tests/unit/windows/test_windisplaydevicegeneral.cpp b/tests/unit/windows/test_windisplaydevicegeneral.cpp index a0a8b1e..79bf052 100644 --- a/tests/unit/windows/test_windisplaydevicegeneral.cpp +++ b/tests/unit/windows/test_windisplaydevicegeneral.cpp @@ -1,4 +1,5 @@ // local includes +#include "displaydevice/windows/settingsutils.h" #include "displaydevice/windows/winapilayer.h" #include "displaydevice/windows/winapiutils.h" #include "displaydevice/windows/windisplaydevice.h" @@ -92,7 +93,7 @@ TEST_F_S(EnumAvailableDevices) { const auto enum_devices { m_win_dd.enumAvailableDevices() }; ASSERT_EQ(available_devices->size(), enum_devices.size()); - const auto topology { flattenTopology(m_win_dd.getCurrentTopology()) }; + const auto topology { display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology()) }; for (const auto &device_id : *available_devices) { auto enum_it { std::find_if(std::begin(enum_devices), std::end(enum_devices), [&device_id](const auto &entry) { return entry.m_device_id == device_id; diff --git a/tests/unit/windows/test_windisplaydevicehdr.cpp b/tests/unit/windows/test_windisplaydevicehdr.cpp index a1f3620..283fc0c 100644 --- a/tests/unit/windows/test_windisplaydevicehdr.cpp +++ b/tests/unit/windows/test_windisplaydevicehdr.cpp @@ -1,4 +1,5 @@ // local includes +#include "displaydevice/windows/settingsutils.h" #include "displaydevice/windows/winapilayer.h" #include "displaydevice/windows/windisplaydevice.h" #include "fixtures/fixtures.h" @@ -71,7 +72,7 @@ TEST_F_S(GetSetHdrStates) { const auto topology_guard { makeTopologyGuard(m_win_dd) }; ASSERT_TRUE(m_win_dd.setTopology(makeExtendedTopology(*available_devices))); - const auto hdr_states { m_win_dd.getCurrentHdrStates(flattenTopology(m_win_dd.getCurrentTopology())) }; + const auto hdr_states { m_win_dd.getCurrentHdrStates(display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology())) }; if (!std::ranges::any_of(hdr_states, [](auto entry) -> bool { return static_cast(entry.second); })) { GTEST_SKIP_("No HDR display is available in the system."); } diff --git a/tests/unit/windows/test_windisplaydevicemodes.cpp b/tests/unit/windows/test_windisplaydevicemodes.cpp index cb13c17..8a79254 100644 --- a/tests/unit/windows/test_windisplaydevicemodes.cpp +++ b/tests/unit/windows/test_windisplaydevicemodes.cpp @@ -2,6 +2,7 @@ #include // local includes +#include "displaydevice/windows/settingsutils.h" #include "displaydevice/windows/winapilayer.h" #include "displaydevice/windows/winapiutils.h" #include "displaydevice/windows/windisplaydevice.h" @@ -149,7 +150,7 @@ namespace { } // namespace TEST_F_S(GetCurrentDisplayModes) { - const auto flattened_topology { flattenTopology(m_win_dd.getCurrentTopology()) }; + const auto flattened_topology { display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology()) }; const auto current_modes { m_win_dd.getCurrentDisplayModes(flattened_topology) }; // Can't really compare anything else without knowing system specs diff --git a/tests/unit/windows/test_windisplaydeviceprimary.cpp b/tests/unit/windows/test_windisplaydeviceprimary.cpp index ee8eb50..65437e2 100644 --- a/tests/unit/windows/test_windisplaydeviceprimary.cpp +++ b/tests/unit/windows/test_windisplaydeviceprimary.cpp @@ -1,4 +1,5 @@ // local includes +#include "displaydevice/windows/settingsutils.h" #include "displaydevice/windows/winapilayer.h" #include "displaydevice/windows/winapiutils.h" #include "displaydevice/windows/windisplaydevice.h" @@ -67,7 +68,7 @@ namespace { } // namespace TEST_F_S(IsPrimary) { - const auto flat_topology { flattenTopology(m_win_dd.getCurrentTopology()) }; + const auto flat_topology { display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology()) }; EXPECT_TRUE(std::ranges::any_of(flat_topology, [&](auto device_id) { return m_win_dd.isPrimary(device_id); })); } diff --git a/tests/unit/windows/test_windisplaydevicetopology.cpp b/tests/unit/windows/test_windisplaydevicetopology.cpp index 79fcf91..2964538 100644 --- a/tests/unit/windows/test_windisplaydevicetopology.cpp +++ b/tests/unit/windows/test_windisplaydevicetopology.cpp @@ -1,4 +1,5 @@ // local includes +#include "displaydevice/windows/settingsutils.h" #include "displaydevice/windows/winapilayer.h" #include "displaydevice/windows/winapiutils.h" #include "displaydevice/windows/windisplaydevice.h" @@ -90,7 +91,7 @@ TEST_F_S(GetCurrentTopology) { } // It is enough to check whether the topology contains expected ids - others test cases check the structure. - EXPECT_EQ(flattenTopology(m_win_dd.getCurrentTopology()), expected_devices); + EXPECT_EQ(display_device::win_utils::flattenTopology(m_win_dd.getCurrentTopology()), expected_devices); } TEST_F_S(SetCurrentTopology, ExtendedTopology) { diff --git a/tests/unit/windows/utils/comparison.cpp b/tests/unit/windows/utils/comparison.cpp index 3a1c7fc..d5e078f 100644 --- a/tests/unit/windows/utils/comparison.cpp +++ b/tests/unit/windows/utils/comparison.cpp @@ -111,14 +111,4 @@ namespace display_device { 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; } - - bool - operator==(const Rational &lhs, const Rational &rhs) { - return lhs.m_numerator == rhs.m_numerator && lhs.m_denominator == rhs.m_denominator; - } - - bool - operator==(const DisplayMode &lhs, const DisplayMode &rhs) { - return lhs.m_refresh_rate == rhs.m_refresh_rate && lhs.m_resolution == rhs.m_resolution; - } } // namespace display_device diff --git a/tests/unit/windows/utils/comparison.h b/tests/unit/windows/utils/comparison.h index fa39d18..fd8c9a8 100644 --- a/tests/unit/windows/utils/comparison.h +++ b/tests/unit/windows/utils/comparison.h @@ -2,7 +2,6 @@ // local includes #include "displaydevice/windows/types.h" -#include "fixtures/comparison.h" // Helper comparison operators bool @@ -47,10 +46,4 @@ operator==(const DISPLAYCONFIG_MODE_INFO &lhs, const DISPLAYCONFIG_MODE_INFO &rh namespace display_device { bool operator==(const PathSourceIndexData &lhs, const PathSourceIndexData &rhs); - - bool - operator==(const Rational &lhs, const Rational &rhs); - - bool - operator==(const DisplayMode &lhs, const DisplayMode &rhs); } // namespace display_device diff --git a/tests/unit/windows/utils/guards.cpp b/tests/unit/windows/utils/guards.cpp new file mode 100644 index 0000000..b4b16e1 --- /dev/null +++ b/tests/unit/windows/utils/guards.cpp @@ -0,0 +1,25 @@ +// header include +#include "guards.h" + +// local includes +#include "displaydevice/windows/settingsutils.h" + +boost::scope::scope_exit +makeTopologyGuard(display_device::WinDisplayDeviceInterface &win_dd) { + return boost::scope::scope_exit(display_device::win_utils::topologyGuardFn(win_dd, win_dd.getCurrentTopology())); +} + +boost::scope::scope_exit +makeModeGuard(display_device::WinDisplayDeviceInterface &win_dd) { + return boost::scope::scope_exit(display_device::win_utils::modeGuardFn(win_dd, win_dd.getCurrentTopology())); +} + +boost::scope::scope_exit +makePrimaryGuard(display_device::WinDisplayDeviceInterface &win_dd) { + return boost::scope::scope_exit(display_device::win_utils::primaryGuardFn(win_dd, win_dd.getCurrentTopology())); +} + +boost::scope::scope_exit +makeHdrStateGuard(display_device::WinDisplayDeviceInterface &win_dd) { + return boost::scope::scope_exit(display_device::win_utils::hdrStateGuardFn(win_dd, win_dd.getCurrentTopology())); +} diff --git a/tests/unit/windows/utils/guards.h b/tests/unit/windows/utils/guards.h index 5cabfd5..db329f8 100644 --- a/tests/unit/windows/utils/guards.h +++ b/tests/unit/windows/utils/guards.h @@ -4,43 +4,18 @@ #include // local includes -#include "displaydevice/windows/windisplaydevice.h" +#include "displaydevice/windows/windisplaydeviceinterface.h" #include "helpers.h" // Helper functions to make guards for restoring previous state -inline 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)); - }); -} +boost::scope::scope_exit +makeTopologyGuard(display_device::WinDisplayDeviceInterface &win_dd); -inline auto -makeModeGuard(display_device::WinDisplayDevice &win_dd) { - return boost::scope::make_scope_exit([&win_dd, modes = win_dd.getCurrentDisplayModes(flattenTopology(win_dd.getCurrentTopology()))]() { - static_cast(win_dd.setDisplayModes(modes)); - }); -} +boost::scope::scope_exit +makeModeGuard(display_device::WinDisplayDeviceInterface &win_dd); -inline auto -makePrimaryGuard(display_device::WinDisplayDevice &win_dd) { - return boost::scope::make_scope_exit([&win_dd, primary_device = [&win_dd]() -> std::string { - const auto flat_topology { flattenTopology(win_dd.getCurrentTopology()) }; - for (const auto &device_id : flat_topology) { - if (win_dd.isPrimary(device_id)) { - return device_id; - } - } +boost::scope::scope_exit +makePrimaryGuard(display_device::WinDisplayDeviceInterface &win_dd); - return {}; - }()]() { - static_cast(win_dd.setAsPrimary(primary_device)); - }); -} - -inline auto -makeHdrStateGuard(display_device::WinDisplayDevice &win_dd) { - return boost::scope::make_scope_exit([&win_dd, states = win_dd.getCurrentHdrStates(flattenTopology(win_dd.getCurrentTopology()))]() { - static_cast(win_dd.setHdrStates(states)); - }); -} +boost::scope::scope_exit +makeHdrStateGuard(display_device::WinDisplayDeviceInterface &win_dd); diff --git a/tests/unit/windows/utils/helpers.cpp b/tests/unit/windows/utils/helpers.cpp index 02afd7b..d27692a 100644 --- a/tests/unit/windows/utils/helpers.cpp +++ b/tests/unit/windows/utils/helpers.cpp @@ -1,16 +1,6 @@ // local includes #include "helpers.h" - -std::set -flattenTopology(const display_device::ActiveTopology &topology) { - std::set flattened_topology; - for (const auto &group : topology) { - for (const auto &device_id : group) { - flattened_topology.insert(device_id); - } - } - return flattened_topology; -} +#include "displaydevice/windows/json.h" std::optional> getAvailableDevices(display_device::WinApiLayer &layer, const bool only_valid_output) { @@ -33,3 +23,20 @@ getAvailableDevices(display_device::WinApiLayer &layer, const bool only_valid_ou return std::vector { device_ids.begin(), device_ids.end() }; } + +std::optional> +serializeState(const std::optional &state) { + if (state) { + if (state->m_initial.m_topology.empty() && state->m_initial.m_primary_devices.empty() && state->m_modified.m_topology.empty() && !state->m_modified.hasModifications()) { + return std::vector {}; + } + + bool is_ok { false }; + const auto data_string { toJson(*state, 2, &is_ok) }; + if (is_ok) { + return std::vector { std::begin(data_string), std::end(data_string) }; + } + } + + return std::nullopt; +} diff --git a/tests/unit/windows/utils/helpers.h b/tests/unit/windows/utils/helpers.h index 468ada9..164f9d1 100644 --- a/tests/unit/windows/utils/helpers.h +++ b/tests/unit/windows/utils/helpers.h @@ -1,15 +1,15 @@ #pragma once // system includes -#include +#include // local includes #include "displaydevice/windows/types.h" #include "displaydevice/windows/winapilayer.h" // Generic helper functions -std::set -flattenTopology(const display_device::ActiveTopology &topology); - std::optional> getAvailableDevices(display_device::WinApiLayer &layer, bool only_valid_output = true); + +std::optional> +serializeState(const std::optional &state); diff --git a/tests/unit/windows/utils/mockwindisplaydevice.cpp b/tests/unit/windows/utils/mockwindisplaydevice.cpp new file mode 100644 index 0000000..3728c62 --- /dev/null +++ b/tests/unit/windows/utils/mockwindisplaydevice.cpp @@ -0,0 +1,34 @@ +// local includes +#include "mockwindisplaydevice.h" +#include "helpers.h" + +namespace ut_consts { + const std::optional SDCS_NULL { std::nullopt }; + const std::optional SDCS_EMPTY { display_device::SingleDisplayConfigState {} }; + const std::optional SDCS_FULL { []() { + const display_device::SingleDisplayConfigState state { + { { { "DeviceId1" } }, + { "DeviceId1" } }, + { display_device::SingleDisplayConfigState::Modified { + { { "DeviceId2" }, { "DeviceId3" } }, + { { "DeviceId2", { { 1920, 1080 }, { 120, 1 } } }, + { "DeviceId3", { { 1920, 1080 }, { 60, 1 } } } }, + { { "DeviceId2", { display_device::HdrState::Disabled } }, + { "DeviceId3", std::nullopt } }, + { "DeviceId3" }, + } } + }; + + return state; + }() }; + const std::optional SDCS_NO_MODIFICATIONS { []() { + const display_device::SingleDisplayConfigState state { + { { { "DeviceId1" } }, + { "DeviceId1" } }, + { display_device::SingleDisplayConfigState::Modified { + { { "DeviceId2" }, { "DeviceId3" } } } } + }; + + return state; + }() }; +} // namespace ut_consts diff --git a/tests/unit/windows/utils/mockwindisplaydevice.h b/tests/unit/windows/utils/mockwindisplaydevice.h index 02f3405..7be8f77 100644 --- a/tests/unit/windows/utils/mockwindisplaydevice.h +++ b/tests/unit/windows/utils/mockwindisplaydevice.h @@ -24,3 +24,14 @@ namespace display_device { MOCK_METHOD(bool, setHdrStates, (const HdrStateMap &), (override)); }; } // namespace display_device + +/** + * @brief Contains some useful predefined structures for UTs. + * @note Data is to be extended with relevant information as needed. + */ +namespace ut_consts { + extern const std::optional SDCS_NULL; + extern const std::optional SDCS_EMPTY; + extern const std::optional SDCS_FULL; + extern const std::optional SDCS_NO_MODIFICATIONS; +} // namespace ut_consts