Files
psxrecomp/recompiler/tests/portable_settings_paths_test.cpp
Shokunin 760be0c6c0 config: a packaged game folder is its own project root
find_project_root walked up from a release's game.toml to the first
ancestor with a root marker. With a global ~/.gitignore (common) and the
game under ~/Games/<title>, that was the home folder, so [runtime]
memcard_dir = "saves" put memory cards and savestates in ~/saves instead
of beside the game. A folder whose licenses/psxrecomp-LICENSE was staged
by package_game_release.sh now stops the walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:06:46 -07:00

123 lines
5.8 KiB
C++

/* portable_settings_paths_test
*
* A portable game folder must keep working after it is moved or copied to
* another PC. The launcher saves the disc, BIOS and memory-card paths the
* player used; saved as absolute paths they pinned the folder to its first
* location. What this pins:
*
* 1. a path inside the settings folder is written relative to it;
* 2. a path outside it (a NAS disc, another drive) stays absolute;
* 3. the relative form reads back unchanged, so the reader's exe-directory
* anchoring resolves it on the new PC.
*/
#include "config_loader.h"
#include "host_path.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
namespace fs = std::filesystem;
using PSXRecompV4::relative_to_folder;
static int failures = 0;
static void check(bool cond, const char* what) {
if (!cond) {
std::fprintf(stderr, "FAIL: %s\n", what);
++failures;
}
}
int main() {
const fs::path game = fs::temp_directory_path() / "psx portable paths test" / "game";
std::error_code ec;
fs::remove_all(game.parent_path(), ec);
fs::create_directories(game / "inputs" / "disc-1");
fs::create_directories(game.parent_path() / "elsewhere");
const fs::path disc = game / "inputs" / "disc-1" / "Game (Europe).chd";
const fs::path outside = game.parent_path() / "elsewhere" / "bios.bin";
check(relative_to_folder(disc, game).generic_string() == "inputs/disc-1/Game (Europe).chd",
"a path inside the folder becomes relative");
check(relative_to_folder(outside, game) == outside, "a path outside the folder stays absolute");
check(relative_to_folder(fs::path("inputs/x.bin"), game) == fs::path("inputs/x.bin"),
"an already relative path is unchanged");
check(relative_to_folder(fs::path(), game).empty(), "an empty path stays empty");
PSXRecompV4::UserSettings s{};
s.bios_path = game / "inputs" / "[EU] SCPH5552.BIN"; s.has_bios_path = true;
s.disc_path = disc; s.has_disc_path = true;
s.memcard_dir = game / "saves"; s.has_memcard_dir = true;
s.memcard1_path = game / "saves" / "card1.mcd"; s.has_memcard1_path = true;
s.memcard2_path = outside; s.has_memcard2_path = true;
const fs::path settings = game / "settings.toml";
check(PSXRecompV4::save_user_settings(settings, s), "settings save");
std::stringstream text;
text << std::ifstream(settings).rdbuf();
const std::string body = text.str();
check(body.find("path = \"inputs/[EU] SCPH5552.BIN\"") != std::string::npos, "BIOS saved relative");
check(body.find("path = \"inputs/disc-1/Game (Europe).chd\"") != std::string::npos, "disc saved relative");
check(body.find("dir = \"saves\"") != std::string::npos, "memory card folder saved relative");
check(body.find("card1 = \"saves/card1.mcd\"") != std::string::npos, "card 1 saved relative");
check(body.find(outside.generic_string()) != std::string::npos, "outside card stays absolute");
const PSXRecompV4::UserSettings back = PSXRecompV4::load_user_settings(settings);
check(back.has_disc_path && back.disc_path == fs::path("inputs/disc-1/Game (Europe).chd"),
"relative disc path reads back unchanged");
check(back.has_memcard1_path && back.memcard1_path == fs::path("saves/card1.mcd"),
"relative card path reads back unchanged");
#ifdef _WIN32
// A NAS selection must survive a launcher save and reopen. No network
// asset is opened: only the task-local settings file is written/read.
const fs::path expected("//server/share/Game files/disc.chd");
for (const char* spelling : {
"//server/share/Game files/disc.chd",
R"(\\server\share\Game files\disc.chd)"}) {
PSXRecompV4::UserSettings network{};
network.disc_path = spelling; network.has_disc_path = true;
network.bios_path = spelling; network.has_bios_path = true;
network.memcard1_path = spelling; network.has_memcard1_path = true;
check(PSXRecompV4::save_user_settings(settings, network), "UNC settings save");
const auto restored = PSXRecompV4::load_user_settings(settings);
check(restored.has_disc_path &&
restored.disc_path.native() == expected.native(),
"saved UNC disc keeps both leading separators");
check(restored.has_bios_path &&
restored.bios_path.native() == expected.native(),
"saved UNC BIOS keeps both leading separators");
check(restored.has_memcard1_path &&
restored.memcard1_path.native() == expected.native(),
"saved UNC memory card keeps both leading separators");
check(PSXRecompV4::host_resolve(game, restored.disc_path).native() == expected.native(),
"reopened UNC disc never becomes a path on the current drive");
}
#endif
// A packaged game is its own project root even when an ancestor carries
// a root marker (a global ~/.gitignore): its relative [runtime] paths
// (memcard_dir = "saves") must stay inside the game folder.
{
const fs::path home = game.parent_path() / "home";
const fs::path pkg = home / "Games" / "R4";
fs::create_directories(pkg / "licenses");
std::ofstream(home / ".gitignore") << "*.o\n";
std::ofstream(pkg / "game.toml") << "\n";
check(PSXRecompV4::find_project_root(pkg / "game.toml") == home,
"precondition: a plain folder walks up to the marked ancestor");
std::ofstream(pkg / "licenses" / "psxrecomp-LICENSE") << "MIT\n";
check(PSXRecompV4::find_project_root(pkg / "game.toml") == PSXRecompV4::host_absolute(pkg),
"a packaged game folder is its own root");
}
fs::remove_all(game.parent_path(), ec);
if (failures) return 1;
std::printf("portable_settings_paths_test: all checks passed\n");
return 0;
}