Introducing testing suite using google test (#4170)

* initial tests for testing Setting<T>

* argg

* keep switch off

* added ci

* fix ci

* merging tests to build.yml

* fixing linux ttests/?

* one more try for ci

* more linux fix for ci

* more ci fixes

* try to fix ctests now

* should fix now

* trying fixing linux tests

* some more tests

* more tests (68 tests) and clang format
This commit is contained in:
georgemoralis
2026-03-26 00:31:53 +02:00
committed by GitHub
parent 650652db42
commit 31b2d9ccd7
8 changed files with 1332 additions and 0 deletions
+77
View File
@@ -14,6 +14,8 @@ on:
- "documents/**"
- "**/*.md"
workflow_dispatch:
concurrency:
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
@@ -65,6 +67,81 @@ jobs:
echo "shorthash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "fullhash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
test:
name: Run C++ Tests on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
include:
- os: windows-latest
compiler_cxx: clang-cl
compiler_c: clang-cl
- os: ubuntu-latest
compiler_cxx: clang++
compiler_c: clang
- os: macos-latest
compiler_cxx: clang++
compiler_c: clang
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup CMake
uses: lukka/get-cmake@latest
- name: Setup Visual Studio shell (Windows only)
if: runner.os == 'Windows'
uses: egor-tensin/vs-shell@v2
with:
arch: x64
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y ninja-build libx11-dev libxext-dev libwayland-dev libdecor-0-dev libxkbcommon-dev libxcursor-dev libxi-dev libxss-dev libxtst-dev libxrandr-dev libxfixes-dev libudev-dev uuid-dev uuid-dev
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install ninja
- name: Configure CMake
run: |
cmake -B build -G Ninja \
-DCMAKE_CXX_COMPILER="${{ matrix.compiler_cxx }}" \
-DCMAKE_C_COMPILER="${{ matrix.compiler_c }}" \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_TESTS=ON \
${{ runner.os == 'macOS' && '-DCMAKE_OSX_ARCHITECTURES=x86_64' || '' }}
shell: bash
- name: Create shadPS4 user data directory (Linux)
if: runner.os == 'Linux'
run: mkdir -p ~/.local/share/shadPS4
- name: Create shadPS4 user data directory (macOS)
if: runner.os == 'macOS'
run: mkdir -p ~/Library/Application\ Support/shadPS4
- name: Create shadPS4 user data directory (Windows)
if: runner.os == 'Windows'
run: mkdir -p "$APPDATA/shadPS4"
shell: bash
- name: Build all tests
run: cmake --build build
shell: bash
- name: Run tests with CTest
run: ctest --test-dir build --output-on-failure --progress
shell: bash
windows-sdl:
runs-on: windows-2025
needs: get-info
+8
View File
@@ -33,6 +33,7 @@ endif()
option(ENABLE_DISCORD_RPC "Enable the Discord RPC integration" ON)
option(ENABLE_UPDATER "Enables the options to updater" ON)
option(ENABLE_TESTS "Build unit tests (requires GTest)" OFF)
# First, determine whether to use CMAKE_OSX_ARCHITECTURES or CMAKE_SYSTEM_PROCESSOR.
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
@@ -1114,6 +1115,8 @@ set(EMULATOR src/emulator.cpp
src/sdl_window.cpp
)
if(NOT ENABLE_TESTS)
add_executable(shadps4
${AUDIO_CORE}
${IMGUI}
@@ -1267,3 +1270,8 @@ endif()
# Install rules
install(TARGETS shadps4 BUNDLE DESTINATION .)
else()
enable_testing()
add_subdirectory(tests)
endif()
+259
View File
@@ -0,0 +1,259 @@
# SPDX-FileCopyrightText: 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
# SPDX-License-Identifier: GPL-2.0-or-later
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlinesLeft: false
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: true
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 100
CommentPragmas: '^ IWYU pragma:'
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
IncludeCategories:
- Regex: '^\<[^Q][^/.>]*\>'
Priority: -2
- Regex: '^\<'
Priority: -1
- Regex: '^\"'
Priority: 0
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 150
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 4
UseTab: Never
---
Language: Java
# BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlinesLeft: false
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: true
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 100
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
IncludeCategories:
- Regex: '^\<[^Q][^/.>]*\>'
Priority: -2
- Regex: '^\<'
Priority: -1
- Regex: '^\"'
Priority: 0
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 150
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Never
---
Language: ObjC
# BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlinesLeft: false
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: true
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 100
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
IncludeCategories:
- Regex: '^\<[^Q][^/.>]*\>'
Priority: -2
- Regex: '^\<'
Priority: -1
- Regex: '^\"'
Priority: 0
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 150
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Never
...
+84
View File
@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Find or download Google Test
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip
)
FetchContent_MakeAvailable(googletest)
set(SETTINGS_TEST_SOURCES
# Under test
${CMAKE_SOURCE_DIR}/src/core/emulator_settings.cpp
${CMAKE_SOURCE_DIR}/src/core/emulator_state.cpp
# Minimal common support
${CMAKE_SOURCE_DIR}/src/common/path_util.cpp
${CMAKE_SOURCE_DIR}/src/common/assert.cpp
${CMAKE_SOURCE_DIR}/src/common/error.cpp
${CMAKE_SOURCE_DIR}/src/common/string_util.cpp
${CMAKE_SOURCE_DIR}/src/common/logging/filter.cpp
${CMAKE_SOURCE_DIR}/src/common/logging/text_formatter.cpp
# Stubs that replace dependencies
stubs/log_stub.cpp
stubs/scm_rev_stub.cpp
stubs/sdl_stub.cpp
# Tests
test_emulator_settings.cpp
)
add_executable(shadps4_settings_test ${SETTINGS_TEST_SOURCES})
target_include_directories(shadps4_settings_test PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}
)
target_link_libraries(shadps4_settings_test PRIVATE
GTest::gtest_main
fmt::fmt
nlohmann_json::nlohmann_json
toml11::toml11
SDL3::SDL3
)
target_compile_features(shadps4_settings_test PRIVATE cxx_std_23)
target_compile_definitions(shadps4_settings_test PRIVATE BOOST_ASIO_STANDALONE)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
include(CheckCXXSymbolExists)
check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP)
if (LIBCPP)
target_compile_options(shadps4_settings_test PRIVATE -fexperimental-library)
endif()
endif()
if (WIN32)
target_compile_definitions(shadps4_settings_test PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
NTDDI_VERSION=0x0A000006
_WIN32_WINNT=0x0A00
WINVER=0x0A00
)
if (MSVC)
target_compile_definitions(shadps4_settings_test PRIVATE
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_DEPRECATE
_SCL_SECURE_NO_WARNINGS
_TIMESPEC_DEFINED
)
endif()
endif()
include(GoogleTest)
gtest_discover_tests(shadps4_settings_test
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
PROPERTIES TIMEOUT 60
)
+27
View File
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstdio>
#include <string_view>
#include <fmt/format.h>
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
namespace Common::Log {
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
unsigned int line_num, const char* function, const char* format,
const fmt::format_args& args) {
}
void Initialize(std::string_view) {}
bool IsActive() { return false; }
void SetGlobalFilter(const Filter&) {}
void SetColorConsoleBackendEnabled(bool) {}
void Start() {}
void Stop() {}
void Denitializer() {}
void SetAppend() {}
} // namespace Common::Log
+22
View File
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include "common/scm_rev.h"
namespace Common {
constexpr char g_version[] = "0.0.0 TEST";
constexpr bool g_is_release = false;
constexpr char g_scm_rev[] = "test_rev_hash";
constexpr char g_scm_branch[] = "test_branch";
constexpr char g_scm_desc[] = "test_desc";
constexpr char g_scm_remote_name[] = "origin";
constexpr char g_scm_remote_url[] = "https://github.com/test/shadPS4";
constexpr char g_scm_date[] = "2026-03-23";
const std::string GetRemoteNameFromLink() {
return "test";
}
} // namespace Common
+18
View File
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <SDL3/SDL_messagebox.h>
extern "C" {
bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) {
if (buttonid) *buttonid = 0; // "No",skip migration
return true;
}
bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */,
const char* /* message */, SDL_Window* /* window */) {
return true;
}
} // extern "C"
+837
View File
@@ -0,0 +1,837 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <memory>
#include <string>
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include "common/path_util.h"
#include "common/scm_rev.h"
#include "core/emulator_settings.h"
#include "core/emulator_state.h"
namespace fs = std::filesystem;
using json = nlohmann::json;
class TempDir {
public:
TempDir() {
auto ns = std::chrono::steady_clock::now().time_since_epoch().count();
temp_path = fs::temp_directory_path() / ("shadps4_test_" + std::to_string(ns) + "_" +
std::to_string(reinterpret_cast<uintptr_t>(this)));
fs::create_directories(temp_path);
}
~TempDir() {
std::error_code ec;
fs::remove_all(temp_path, ec);
}
const fs::path& path() const {
return temp_path;
}
private:
fs::path temp_path;
};
static void WriteJson(const fs::path& p, const json& j) {
std::ofstream out(p);
ASSERT_TRUE(out.is_open()) << "Cannot write: " << p;
out << std::setw(2) << j;
}
static json ReadJson(const fs::path& p) {
std::ifstream in(p);
EXPECT_TRUE(in.is_open()) << "Cannot read: " << p;
json j;
in >> j;
return j;
}
class EmulatorSettingsTest : public ::testing::Test {
protected:
void SetUp() override {
temp_dir = std::make_unique<TempDir>();
const fs::path root = temp_dir->path();
using PT = Common::FS::PathType;
const struct {
PT type;
const char* sub;
} dirs[] = {
{PT::UserDir, ""},
{PT::LogDir, "log"},
{PT::ScreenshotsDir, "screenshots"},
{PT::ShaderDir, "shader"},
{PT::GameDataDir, "data"},
{PT::TempDataDir, "temp"},
{PT::SysModuleDir, "sys_modules"},
{PT::DownloadDir, "download"},
{PT::CapturesDir, "captures"},
{PT::CheatsDir, "cheats"},
{PT::PatchesDir, "patches"},
{PT::MetaDataDir, "game_data"},
{PT::CustomTrophy, "custom_trophy"},
{PT::CustomConfigs, "custom_configs"},
{PT::CacheDir, "cache"},
{PT::FontsDir, "fonts"},
{PT::HomeDir, "home"},
};
for (const auto& d : dirs) {
fs::path p = d.sub[0] ? (root / d.sub) : root;
fs::create_directories(p);
Common::FS::SetUserPath(d.type, p);
}
temp_state = std::make_shared<EmulatorState>();
EmulatorState::SetInstance(temp_state);
temp_settings = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(temp_settings);
}
void TearDown() override {
EmulatorSettingsImpl::SetInstance(nullptr);
EmulatorState::SetInstance(nullptr);
temp_settings.reset();
temp_state.reset();
temp_dir.reset();
}
fs::path ConfigJson() const {
return temp_dir->path() / "config.json";
}
fs::path GameConfig(const std::string& serial) const {
return temp_dir->path() / "custom_configs" / (serial + ".json");
}
std::unique_ptr<TempDir> temp_dir;
std::shared_ptr<EmulatorSettingsImpl> temp_settings;
std::shared_ptr<EmulatorState> temp_state;
};
// tests Settting<T> template , default , Global override modes
TEST(SettingTest, DefaultCtorZeroInitialises) {
Setting<int> s;
EXPECT_EQ(s.value, 0);
EXPECT_EQ(s.default_value, 0);
EXPECT_FALSE(s.game_specific_value.has_value());
}
TEST(SettingTest, ValueCtorSetsBothValueAndDefault) {
Setting<int> s{42};
EXPECT_EQ(s.value, 42);
EXPECT_EQ(s.default_value, 42);
}
TEST(SettingTest, GetDefaultPrefersGameSpecificOverBase) {
Setting<int> s{10};
s.value = 20;
s.game_specific_value = 99;
EXPECT_EQ(s.get(ConfigMode::Default), 99);
}
TEST(SettingTest, GetDefaultFallsBackToBaseWhenNoOverride) {
Setting<int> s{10};
s.value = 20;
EXPECT_EQ(s.get(ConfigMode::Default), 20);
}
TEST(SettingTest, GetGlobalIgnoresGameSpecific) {
Setting<int> s{10};
s.value = 20;
s.game_specific_value = 99;
EXPECT_EQ(s.get(ConfigMode::Global), 20);
}
TEST(SettingTest, GetCleanAlwaysReturnsFactoryDefault) {
Setting<int> s{10};
s.value = 20;
s.game_specific_value = 99;
EXPECT_EQ(s.get(ConfigMode::Clean), 10);
}
TEST(SettingTest, SetWritesToBaseOnly) {
Setting<int> s{0};
s.game_specific_value = 55;
s.set(77);
EXPECT_EQ(s.value, 77);
EXPECT_EQ(s.game_specific_value.value(), 55); // override untouched
}
TEST(SettingTest, ResetGameSpecificClearsOverride) {
Setting<int> s{0};
s.game_specific_value = 55;
s.reset_game_specific();
EXPECT_FALSE(s.game_specific_value.has_value());
// base and default must be intact
EXPECT_EQ(s.value, 0);
EXPECT_EQ(s.default_value, 0);
}
TEST(SettingTest, BoolSettingAllModes) {
Setting<bool> s{false};
s.value = true;
s.game_specific_value = false;
EXPECT_FALSE(s.get(ConfigMode::Default));
EXPECT_TRUE(s.get(ConfigMode::Global));
EXPECT_FALSE(s.get(ConfigMode::Clean));
}
TEST(SettingTest, StringSettingAllModes) {
Setting<std::string> s{"shadow"};
s.value = "rule";
s.game_specific_value = "override";
EXPECT_EQ(s.get(ConfigMode::Default), "override");
EXPECT_EQ(s.get(ConfigMode::Global), "rule");
EXPECT_EQ(s.get(ConfigMode::Clean), "shadow");
}
TEST(SettingTest, NoGameSpecificDefaultAndGlobalAgree) {
Setting<int> s{7};
s.value = 7;
EXPECT_EQ(s.get(ConfigMode::Default), s.get(ConfigMode::Global));
}
// tests for default settings
TEST_F(EmulatorSettingsTest, SetDefaultValuesResetsAllGroupsToFactory) {
// set random values
temp_settings->SetNeo(true);
temp_settings->SetWindowWidth(3840u);
temp_settings->SetGpuId(2);
temp_settings->SetDebugDump(true);
temp_settings->SetCursorState(HideCursorState::Always);
temp_settings->SetDefaultValues(); // reset to defaults
// check if values are reset to defaults
EXPECT_FALSE(temp_settings->IsNeo());
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u);
EXPECT_EQ(temp_settings->GetGpuId(), -1);
EXPECT_FALSE(temp_settings->IsDebugDump());
EXPECT_EQ(temp_settings->GetCursorState(), static_cast<int>(HideCursorState::Idle));
}
TEST_F(EmulatorSettingsTest, SetDefaultValuesClearsGameSpecificOverrides) {
// check that game-specific overrides are cleared by SetDefaultValues
json game;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA00001"), game);
temp_settings->Load("CUSA00001");
temp_settings->SetDefaultValues();
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_FALSE(temp_settings->IsNeo()); // default is false should be loaded instead of override
}
// configModes tests
TEST_F(EmulatorSettingsTest, ConfigModeSetAndGetRoundTrips) {
temp_settings->SetConfigMode(ConfigMode::Clean);
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Clean);
temp_settings->SetConfigMode(ConfigMode::Global);
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Global);
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_EQ(temp_settings->GetConfigMode(), ConfigMode::Default);
}
TEST_F(EmulatorSettingsTest, ConfigModeCleanReturnFactoryDefaults) {
temp_settings->SetWindowWidth(3840u);
json game;
game["GPU"]["window_width"] = 2560;
WriteJson(GameConfig("CUSA00001"), game);
temp_settings->Load("CUSA00001");
temp_settings->SetConfigMode(ConfigMode::Clean);
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // factory default
}
TEST_F(EmulatorSettingsTest, ConfigModeGlobalIgnoresGameSpecific) {
temp_settings->SetNeo(false);
json game;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA00001"), game);
temp_settings->Load("CUSA00001");
temp_settings->SetConfigMode(ConfigMode::Global);
EXPECT_FALSE(temp_settings->IsNeo());
}
TEST_F(EmulatorSettingsTest, ConfigModeDefaultResolvesGameSpecificWhenPresent) {
temp_settings->SetNeo(false);
json game;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA00001"), game);
temp_settings->Load("CUSA00001");
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_TRUE(temp_settings->IsNeo());
}
// tests for global config.json file
TEST_F(EmulatorSettingsTest, SaveCreatesConfigJson) {
ASSERT_TRUE(temp_settings->Save());
EXPECT_TRUE(fs::exists(ConfigJson()));
}
TEST_F(EmulatorSettingsTest, SaveWritesAllExpectedSections) {
ASSERT_TRUE(temp_settings->Save());
json j = ReadJson(ConfigJson());
for (const char* section : {"General", "Debug", "Input", "Audio", "GPU", "Vulkan"})
EXPECT_TRUE(j.contains(section)) << "Missing section: " << section;
}
TEST_F(EmulatorSettingsTest, LoadReturnsTrueForExistingFile) {
temp_settings->Save();
auto fresh = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(fresh);
EXPECT_TRUE(fresh->Load());
}
TEST_F(EmulatorSettingsTest, RoundTripAllGroups) {
temp_settings->SetNeo(true);
temp_settings->SetDebugDump(true);
temp_settings->SetWindowWidth(1920u);
temp_settings->SetGpuId(1);
temp_settings->SetCursorState(HideCursorState::Always);
temp_settings->SetAudioBackend(AudioBackend::OpenAL);
temp_settings->Save();
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load();
EXPECT_TRUE(f->IsNeo());
EXPECT_TRUE(f->IsDebugDump());
EXPECT_EQ(f->GetWindowWidth(), 1920u);
EXPECT_EQ(f->GetGpuId(), 1);
EXPECT_EQ(f->GetCursorState(), static_cast<int>(HideCursorState::Always));
EXPECT_EQ(f->GetAudioBackend(), static_cast<u32>(AudioBackend::OpenAL));
}
TEST_F(EmulatorSettingsTest, LoadMissingFileCreatesDefaultsOnDisk) {
ASSERT_FALSE(fs::exists(ConfigJson()));
temp_settings->Load();
EXPECT_TRUE(fs::exists(ConfigJson()));
EXPECT_FALSE(temp_settings->IsNeo()); // defaults
}
TEST_F(EmulatorSettingsTest, LoadMissingSectionDoesNotZeroOtherSections) {
temp_settings->SetNeo(true);
temp_settings->Save();
json j = ReadJson(ConfigJson());
j.erase("GPU");
WriteJson(ConfigJson(), j);
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load();
EXPECT_TRUE(f->IsNeo()); // belongs to General, should be loaded
EXPECT_EQ(f->GetWindowWidth(), 1280); // GPU fell back to default
}
TEST_F(EmulatorSettingsTest, LoadPreservesUnknownKeysOnResave) {
temp_settings->Save();
json j = ReadJson(ConfigJson());
j["General"]["future_feature"] = "preserved";
WriteJson(ConfigJson(), j);
// A fresh load + save (triggered by version mismatch) must keep the key
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load();
f->Save();
json after = ReadJson(ConfigJson());
EXPECT_EQ(after["General"]["future_feature"], "preserved");
}
TEST_F(EmulatorSettingsTest, LoadUnknownTopLevelSectionPreserved) {
temp_settings->Save();
json j = ReadJson(ConfigJson());
j["FutureSection"]["key"] = 42;
WriteJson(ConfigJson(), j);
temp_settings->SetNeo(true);
temp_settings->Save(); // merge path
json after = ReadJson(ConfigJson());
EXPECT_TRUE(after.contains("FutureSection"));
EXPECT_EQ(after["FutureSection"]["key"], 42);
}
TEST_F(EmulatorSettingsTest, LoadCorruptJsonDoesNotCrash) {
{
std::ofstream out(ConfigJson());
out << "{NOT VALID JSON!!!";
}
EXPECT_NO_THROW(temp_settings->Load());
}
TEST_F(EmulatorSettingsTest, LoadEmptyJsonObjectDoesNotCrash) {
WriteJson(ConfigJson(), json::object());
EXPECT_NO_THROW(temp_settings->Load());
}
// tests for per game config
TEST_F(EmulatorSettingsTest, SaveSerialCreatesPerGameFile) {
ASSERT_TRUE(temp_settings->Save("CUSA01234"));
EXPECT_TRUE(fs::exists(GameConfig("CUSA01234")));
}
TEST_F(EmulatorSettingsTest, LoadSerialReturnsFalseWhenFileAbsent) {
EXPECT_FALSE(temp_settings->Load("CUSA99999"));
}
TEST_F(EmulatorSettingsTest, LoadSerialAppliesOverrideToGameSpecificValue) {
temp_settings->SetNeo(false);
json game;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA01234"), game);
ASSERT_TRUE(temp_settings->Load("CUSA01234"));
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_TRUE(temp_settings->IsNeo());
}
TEST_F(EmulatorSettingsTest, LoadSerialBaseValueUntouched) {
temp_settings->SetWindowWidth(1280);
json game;
game["GPU"]["window_width"] = 3840;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->SetConfigMode(ConfigMode::Global);
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
}
TEST_F(EmulatorSettingsTest, LoadSerialOverridesMultipleGroups) {
temp_settings->SetNeo(false);
temp_settings->SetWindowWidth(1280u);
temp_settings->SetDebugDump(false);
json game;
game["General"]["neo_mode"] = true;
game["GPU"]["window_width"] = 3840;
game["Debug"]["debug_dump"] = true;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_TRUE(temp_settings->IsNeo());
EXPECT_EQ(temp_settings->GetWindowWidth(), 3840);
EXPECT_TRUE(temp_settings->IsDebugDump());
}
TEST_F(EmulatorSettingsTest, LoadSerialUnrecognisedKeyIgnored) {
json game;
game["GPU"]["key_that_does_not_exist"] = 999;
WriteJson(GameConfig("CUSA01234"), game);
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
}
TEST_F(EmulatorSettingsTest, LoadSerialTypeMismatch_DoesNotCrash) {
json game;
game["GPU"]["window_width"] = "not_a_number";
WriteJson(GameConfig("CUSA01234"), game);
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
// base unchanged
temp_settings->SetConfigMode(ConfigMode::Global);
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280u);
}
TEST_F(EmulatorSettingsTest, LoadSerialCorruptFileDoesNotCrash) {
{
std::ofstream out(GameConfig("CUSA01234"));
out << "{{{{totally broken";
}
EXPECT_NO_THROW(temp_settings->Load("CUSA01234"));
}
TEST_F(EmulatorSettingsTest, SaveSerialWritesGameSpecificValueWhenOverrideLoaded) {
temp_settings->SetWindowWidth(1280);
json game;
game["GPU"]["window_width"] = 3840;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->Save("CUSA01234");
json saved = ReadJson(GameConfig("CUSA01234"));
EXPECT_EQ(saved["GPU"]["window_width"].get<unsigned>(), 3840);
}
TEST_F(EmulatorSettingsTest, SaveSerialWritesBaseValueWhenNoOverrideSet) {
temp_settings->SetWindowWidth(2560);
temp_settings->Save("CUSA01234");
json saved = ReadJson(GameConfig("CUSA01234"));
EXPECT_EQ(saved["GPU"]["window_width"].get<unsigned>(), 2560);
}
TEST_F(EmulatorSettingsTest, MultipleSerialsDoNotInterfere) {
json g1;
g1["General"]["neo_mode"] = true;
g1["GPU"]["window_width"] = 3840;
WriteJson(GameConfig("CUSA00001"), g1);
json g2;
g2["General"]["neo_mode"] = false;
g2["GPU"]["window_width"] = 1920;
WriteJson(GameConfig("CUSA00002"), g2);
{
auto s = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(s);
s->Load();
s->Load("CUSA00001");
s->SetConfigMode(ConfigMode::Default);
EXPECT_TRUE(s->IsNeo());
EXPECT_EQ(s->GetWindowWidth(), 3840);
}
{
auto s = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(s);
s->Load();
s->Load("CUSA00002");
s->SetConfigMode(ConfigMode::Default);
EXPECT_FALSE(s->IsNeo());
EXPECT_EQ(s->GetWindowWidth(), 1920);
}
}
// ClearGameSpecificOverrides tests
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesRemovesAllGroups) {
json game;
game["General"]["neo_mode"] = true;
game["GPU"]["window_width"] = 3840;
game["Debug"]["debug_dump"] = true;
game["Input"]["cursor_state"] = 2;
game["Audio"]["audio_backend"] = 1;
game["Vulkan"]["gpu_id"] = 2;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->ClearGameSpecificOverrides();
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_FALSE(temp_settings->IsNeo());
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
EXPECT_FALSE(temp_settings->IsDebugDump());
EXPECT_EQ(temp_settings->GetCursorState(), static_cast<int>(HideCursorState::Idle));
EXPECT_EQ(temp_settings->GetGpuId(), -1);
}
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverridesDoesNotTouchBaseValues) {
temp_settings->SetWindowWidth(1920);
json game;
game["GPU"]["window_width"] = 3840;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->ClearGameSpecificOverrides();
temp_settings->SetConfigMode(ConfigMode::Global);
EXPECT_EQ(temp_settings->GetWindowWidth(), 1920);
}
TEST_F(EmulatorSettingsTest, ClearGameSpecificOverrides_NoopWhenNothingLoaded) {
EXPECT_NO_THROW(temp_settings->ClearGameSpecificOverrides());
}
// ResetGameSpecificValue tests
TEST_F(EmulatorSettingsTest, ResetGameSpecificValue_ClearsNamedKey) {
temp_settings->SetWindowWidth(1280);
json game;
game["GPU"]["window_width"] = 3840;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->SetConfigMode(ConfigMode::Default);
ASSERT_EQ(temp_settings->GetWindowWidth(), 3840);
temp_settings->ResetGameSpecificValue("window_width");
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280);
}
TEST_F(EmulatorSettingsTest, ResetGameSpecificValueOnlyAffectsTargetKey) {
json game;
game["GPU"]["window_width"] = 3840;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
temp_settings->ResetGameSpecificValue("window_width");
temp_settings->SetConfigMode(ConfigMode::Default);
EXPECT_EQ(temp_settings->GetWindowWidth(), 1280); // cleared
EXPECT_TRUE(temp_settings->IsNeo()); // still set
}
TEST_F(EmulatorSettingsTest, ResetGameSpecificValueUnknownKeyNoOp) {
EXPECT_NO_THROW(temp_settings->ResetGameSpecificValue("does_not_exist"));
}
// GameInstallDir tests
TEST_F(EmulatorSettingsTest, AddGameInstallDirAddsEnabled) {
fs::path dir = temp_dir->path() / "games";
fs::create_directories(dir);
EXPECT_TRUE(temp_settings->AddGameInstallDir(dir));
ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], dir);
}
TEST_F(EmulatorSettingsTest, AddGameInstallDirRejectsDuplicate) {
fs::path dir = temp_dir->path() / "games";
fs::create_directories(dir);
temp_settings->AddGameInstallDir(dir);
EXPECT_FALSE(temp_settings->AddGameInstallDir(dir));
EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
}
TEST_F(EmulatorSettingsTest, RemoveGameInstallDirRemovesEntry) {
fs::path dir = temp_dir->path() / "games";
fs::create_directories(dir);
temp_settings->AddGameInstallDir(dir);
temp_settings->RemoveGameInstallDir(dir);
EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty());
}
TEST_F(EmulatorSettingsTest, RemoveGameInstallDirNoopForMissing) {
EXPECT_NO_THROW(temp_settings->RemoveGameInstallDir("/nonexistent/path"));
}
TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledDisablesDir) {
fs::path dir = temp_dir->path() / "games";
fs::create_directories(dir);
temp_settings->AddGameInstallDir(dir, true);
temp_settings->SetGameInstallDirEnabled(dir, false);
EXPECT_TRUE(temp_settings->GetGameInstallDirs().empty());
}
TEST_F(EmulatorSettingsTest, SetGameInstallDirEnabledReEnablesDir) {
fs::path dir = temp_dir->path() / "games";
fs::create_directories(dir);
temp_settings->AddGameInstallDir(dir, false);
ASSERT_TRUE(temp_settings->GetGameInstallDirs().empty());
temp_settings->SetGameInstallDirEnabled(dir, true);
EXPECT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
}
TEST_F(EmulatorSettingsTest, SetAllGameInstallDirsReplacesExistingList) {
fs::path d1 = temp_dir->path() / "g1";
fs::path d2 = temp_dir->path() / "g2";
fs::create_directories(d1);
fs::create_directories(d2);
temp_settings->AddGameInstallDir(d1);
temp_settings->SetAllGameInstallDirs({{d2, true}});
ASSERT_EQ(temp_settings->GetGameInstallDirs().size(), 1u);
EXPECT_EQ(temp_settings->GetGameInstallDirs()[0], d2);
}
TEST_F(EmulatorSettingsTest, GameInstallDirsFullRoundTripWithEnabledFlags) {
fs::path d1 = temp_dir->path() / "g1";
fs::path d2 = temp_dir->path() / "g2";
fs::create_directories(d1);
fs::create_directories(d2);
temp_settings->AddGameInstallDir(d1, true);
temp_settings->AddGameInstallDir(d2, false);
temp_settings->Save();
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load();
const auto& all = f->GetAllGameInstallDirs();
ASSERT_EQ(all.size(), 2u);
EXPECT_EQ(all[0].path, d1);
EXPECT_TRUE(all[0].enabled);
EXPECT_EQ(all[1].path, d2);
EXPECT_FALSE(all[1].enabled);
}
TEST_F(EmulatorSettingsTest, GetGameInstallDirsEnabledReflectsState) {
fs::path d1 = temp_dir->path() / "g1";
fs::path d2 = temp_dir->path() / "g2";
fs::create_directories(d1);
fs::create_directories(d2);
temp_settings->AddGameInstallDir(d1, true);
temp_settings->AddGameInstallDir(d2, false);
auto enabled = temp_settings->GetGameInstallDirsEnabled();
ASSERT_EQ(enabled.size(), 2u);
EXPECT_TRUE(enabled[0]);
EXPECT_FALSE(enabled[1]);
}
// GetAllOverrideableKeys tests
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysIsNonEmpty) {
EXPECT_FALSE(temp_settings->GetAllOverrideableKeys().empty());
}
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysContainsRepresentativeKeys) {
auto keys = temp_settings->GetAllOverrideableKeys();
auto has = [&](const char* k) { return std::find(keys.begin(), keys.end(), k) != keys.end(); };
// General
EXPECT_TRUE(has("neo_mode"));
EXPECT_TRUE(has("volume_slider"));
// GPU
EXPECT_TRUE(has("window_width"));
EXPECT_TRUE(has("null_gpu"));
EXPECT_TRUE(has("vblank_frequency"));
// Vulkan
EXPECT_TRUE(has("gpu_id"));
EXPECT_TRUE(has("pipeline_cache_enabled"));
// Debug
EXPECT_TRUE(has("debug_dump"));
EXPECT_TRUE(has("log_enabled"));
// Input
EXPECT_TRUE(has("cursor_state"));
// Audio
EXPECT_TRUE(has("audio_backend"));
}
TEST_F(EmulatorSettingsTest, GetAllOverrideableKeysNoDuplicates) {
auto keys = temp_settings->GetAllOverrideableKeys();
std::vector<std::string> sorted = keys;
std::sort(sorted.begin(), sorted.end());
auto it = std::unique(sorted.begin(), sorted.end());
EXPECT_EQ(it, sorted.end()) << "Duplicate key found in overrideable keys list";
}
// Per-group GetOverrideableFields tests
TEST_F(EmulatorSettingsTest, GetGeneralOverrideableFieldsNonEmpty) {
EXPECT_FALSE(temp_settings->GetGeneralOverrideableFields().empty());
}
TEST_F(EmulatorSettingsTest, GetGPUOverrideableFieldsContainsWindowAndFullscreen) {
auto fields = temp_settings->GetGPUOverrideableFields();
auto has = [&](const char* k) {
return std::any_of(fields.begin(), fields.end(),
[k](const OverrideItem& f) { return std::string(f.key) == k; });
};
EXPECT_TRUE(has("window_width"));
EXPECT_TRUE(has("window_height"));
EXPECT_TRUE(has("full_screen"));
EXPECT_TRUE(has("vblank_frequency"));
}
TEST_F(EmulatorSettingsTest, GetVulkanOverrideableFieldsContainsGpuId) {
auto fields = temp_settings->GetVulkanOverrideableFields();
bool found = std::any_of(fields.begin(), fields.end(),
[](const OverrideItem& f) { return std::string(f.key) == "gpu_id"; });
EXPECT_TRUE(found);
}
// Path accessors tests
TEST_F(EmulatorSettingsTest, GetHomeDirReturnsCustomWhenSet) {
fs::path dir = temp_dir->path() / "custom_home";
fs::create_directories(dir);
temp_settings->SetHomeDir(dir);
EXPECT_EQ(temp_settings->GetHomeDir(), dir);
}
TEST_F(EmulatorSettingsTest, GetSysModulesDirFallsBackToPathUtilWhenEmpty) {
// default_value is empty; GetSysModulesDir falls back to GetUserPath(SysModuleDir)
auto result = temp_settings->GetSysModulesDir();
EXPECT_FALSE(result.empty());
}
TEST_F(EmulatorSettingsTest, GetFontsDirFallsBackToPathUtilWhenEmpty) {
auto result = temp_settings->GetFontsDir();
EXPECT_FALSE(result.empty());
}
// edge cases tests
TEST_F(EmulatorSettingsTest, VersionMismatchPreservesSettings) {
temp_settings->SetNeo(true);
temp_settings->SetWindowWidth(2560u);
temp_settings->Save();
// Force a stale version string so the mismatch branch fires
json j = ReadJson(ConfigJson());
j["Debug"]["config_version"] = "old_hash_0000";
WriteJson(ConfigJson(), j);
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load(); // triggers version-bump Save() internally
EXPECT_TRUE(f->IsNeo());
EXPECT_EQ(f->GetWindowWidth(), 2560u);
}
TEST_F(EmulatorSettingsTest, DoubleGlobalLoadIsIdempotent) {
temp_settings->SetNeo(true);
temp_settings->SetWindowWidth(2560u);
temp_settings->Save();
auto f = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(f);
f->Load(""); // first — loads from disk
f->Load(""); // second — must not reset anything
EXPECT_TRUE(f->IsNeo());
EXPECT_EQ(f->GetWindowWidth(), 2560u);
}
TEST_F(EmulatorSettingsTest, ConfigUsedFlagTrueWhenFileExists) {
json game;
game["General"]["neo_mode"] = true;
WriteJson(GameConfig("CUSA01234"), game);
temp_settings->Load("CUSA01234");
EXPECT_TRUE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed());
}
TEST_F(EmulatorSettingsTest, ConfigUsedFlagFalseWhenFileAbsent) {
temp_settings->Load("CUSA99999");
EXPECT_FALSE(EmulatorState::GetInstance()->IsGameSpecifigConfigUsed());
}
TEST_F(EmulatorSettingsTest, DestructorNoSaveIfLoadNeverCalled) {
temp_settings->SetNeo(true);
temp_settings->Save();
auto t0 = fs::last_write_time(ConfigJson());
{
// Create and immediately destroy without calling Load()
auto untouched = std::make_shared<EmulatorSettingsImpl>();
// destructor fires here
}
auto t1 = fs::last_write_time(ConfigJson());
EXPECT_EQ(t0, t1) << "Destructor wrote config.json without a prior Load()";
}
TEST_F(EmulatorSettingsTest, DestructorSavesAfterSuccessfulLoad) {
temp_settings->SetNeo(true);
temp_settings->Save();
{
auto s = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(s);
s->Load();
s->SetWindowWidth(2560u); // mutate after successful load
// destructor should write this change
}
auto verify = std::make_shared<EmulatorSettingsImpl>();
EmulatorSettingsImpl::SetInstance(verify);
verify->Load();
EXPECT_EQ(verify->GetWindowWidth(), 2560);
}