// CLI Data Directory Resolution tests // Catch2 migration from GTest (yams-3s4 / yams-cli) // // Tests for data directory resolution precedence. #include #include #include #include #include #include #include #include #include "../../common/test_helpers_catch2.h" namespace fs = std::filesystem; namespace { using ScopedEnv = yams::test::ScopedEnvVar; /** * RAII helper to create a temporary config file. */ class TempConfig { public: explicit TempConfig(const std::string& content) { auto pid = getpid(); tempDir_ = fs::temp_directory_path() / ("yams_test_" + std::to_string(pid)); fs::create_directories(tempDir_); configPath_ = tempDir_ / "config.toml"; std::ofstream f(configPath_); f << content; } ~TempConfig() { std::error_code ec; fs::remove_all(tempDir_, ec); } fs::path path() const { return configPath_; } std::string pathStr() const { return configPath_.string(); } TempConfig(const TempConfig&) = delete; TempConfig& operator=(const TempConfig&) = delete; private: fs::path tempDir_; fs::path configPath_; }; } // namespace // ============================================================================ // get_data_dir() - Platform default tests // ============================================================================ TEST_CASE("DataDirResolution - get_data_dir returns non-empty path", "[cli][data_dir][catch2]") { auto dataDir = yams::config::get_data_dir(); CHECK_FALSE(dataDir.empty()); } TEST_CASE("DataDirResolution - get_data_dir contains yams", "[cli][data_dir][catch2]") { auto dataDir = yams::config::get_data_dir(); CHECK(dataDir.string().find("yams") != std::string::npos); } #ifndef _WIN32 TEST_CASE("DataDirResolution - get_data_dir respects XDG_DATA_HOME", "[cli][data_dir][catch2]") { ScopedEnv clearDataDir("YAMS_DATA_DIR", std::nullopt); ScopedEnv clearStorage("YAMS_STORAGE", std::nullopt); ScopedEnv xdg("XDG_DATA_HOME", "/custom/data"); auto dataDir = yams::config::get_data_dir(); CHECK(dataDir == fs::path("/custom/data/yams")); } #else TEST_CASE("DataDirResolution - get_data_dir respects LOCALAPPDATA", "[cli][data_dir][catch2]") { const char* localAppData = std::getenv("LOCALAPPDATA"); if (localAppData) { auto dataDir = yams::config::get_data_dir(); CHECK(dataDir.string().find("yams") != std::string::npos); } } #endif // ============================================================================ // get_config_dir() - Platform default tests // ============================================================================ TEST_CASE("DataDirResolution - get_config_dir returns non-empty path", "[cli][data_dir][catch2]") { auto configDir = yams::config::get_config_dir(); CHECK_FALSE(configDir.empty()); } TEST_CASE("DataDirResolution - get_config_dir contains yams", "[cli][data_dir][catch2]") { auto configDir = yams::config::get_config_dir(); CHECK(configDir.string().find("yams") != std::string::npos); } // ============================================================================ // resolve_data_dir_from_config() - Precedence tests // ============================================================================ TEST_CASE("DataDirResolution - env YAMS_STORAGE takes precedence", "[cli][data_dir][catch2]") { ScopedEnv storage("YAMS_STORAGE", "/env/storage/path"); ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path"); ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml"); REQUIRE(std::getenv("YAMS_STORAGE") != nullptr); CHECK(std::string(std::getenv("YAMS_STORAGE")) == "/env/storage/path"); auto resolved = yams::config::resolve_data_dir_from_config(); CHECK(resolved == fs::path("/env/storage/path")); } TEST_CASE("DataDirResolution - env YAMS_DATA_DIR used if no storage", "[cli][data_dir][catch2]") { ScopedEnv storage("YAMS_STORAGE", ""); ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path"); ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml"); REQUIRE(std::getenv("YAMS_DATA_DIR") != nullptr); CHECK(std::string(std::getenv("YAMS_DATA_DIR")) == "/env/data/path"); auto resolved = yams::config::resolve_data_dir_from_config(); CHECK(resolved == fs::path("/env/data/path")); } TEST_CASE("DataDirResolution - config data_dir used with YAMS_CONFIG env", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/from/config/file" )"); ScopedEnv storage("YAMS_STORAGE", ""); ScopedEnv dataDir("YAMS_DATA_DIR", ""); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); auto resolved = yams::config::resolve_data_dir_from_config(); CHECK(resolved == fs::path("/from/config/file")); } TEST_CASE("DataDirResolution - config takes precedence over env", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/from/config/file" )"); ScopedEnv storage("YAMS_STORAGE", "/env/wins"); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); auto resolved = yams::config::resolve_data_dir_from_config(); CHECK(resolved == fs::path("/from/config/file")); } TEST_CASE("DataDirResolution - falls back to platform default", "[cli][data_dir][catch2]") { ScopedEnv storage("YAMS_STORAGE", ""); ScopedEnv dataDir("YAMS_DATA_DIR", ""); ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml"); auto resolved = yams::config::resolve_data_dir_from_config(); CHECK_FALSE(resolved.empty()); CHECK(resolved.string().find("yams") != std::string::npos); } TEST_CASE("DataDirResolution - explicit CLI data dir overrides config and env", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/from/config/file" )"); ScopedEnv storage("YAMS_STORAGE", "/env/storage/path"); ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path"); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); yams::cli::YamsCLI cli; std::string arg0 = "yams"; std::string arg1 = "--data-dir"; std::string arg2 = "/from/cli/flag"; std::string arg3 = "completion"; std::string arg4 = "bash"; char* argv[] = {arg0.data(), arg1.data(), arg2.data(), arg3.data(), arg4.data()}; const int rc = cli.run(5, argv); REQUIRE(rc == 0); CHECK(cli.hasExplicitDataDir()); CHECK(cli.getDataPath() == fs::path("/from/cli/flag")); } TEST_CASE("DataDirResolution - CLI uses config before env when no explicit override", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/from/config/file" )"); ScopedEnv storage("YAMS_STORAGE", "/env/storage/path"); ScopedEnv dataDir("YAMS_DATA_DIR", "/env/data/path"); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); yams::cli::YamsCLI cli; std::string arg0 = "yams"; std::string arg1 = "completion"; std::string arg2 = "bash"; char* argv[] = {arg0.data(), arg1.data(), arg2.data()}; const int rc = cli.run(3, argv); REQUIRE(rc == 0); CHECK_FALSE(cli.hasExplicitDataDir()); CHECK(cli.getDataPath() == fs::path("/from/config/file")); } TEST_CASE("DataDirResolution - daemon pid file uses config when present", "[cli][data_dir][catch2]") { TempConfig config(R"( [daemon] pid_file = "/tmp/from-config.pid" )"); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); auto resolved = yams::cli::YamsCLI::resolveConfiguredDaemonPidFilePath(); CHECK(resolved == fs::path("/tmp/from-config.pid")); } TEST_CASE("DataDirResolution - daemon pid file falls back when config missing", "[cli][data_dir][catch2]") { ScopedEnv configEnv("YAMS_CONFIG", "/nonexistent/config.toml"); auto resolved = yams::cli::YamsCLI::resolveConfiguredDaemonPidFilePath(); CHECK_FALSE(resolved.empty()); CHECK(resolved.filename().string().find("yams-daemon") != std::string::npos); } // ============================================================================ // parse_config_value() - Config file parsing tests // ============================================================================ TEST_CASE("DataDirResolution - parse_config_value finds key", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/test/path" )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value == "/test/path"); } TEST_CASE("DataDirResolution - parse_config_value handles quoted values", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/path/with spaces" )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value == "/path/with spaces"); } TEST_CASE("DataDirResolution - parse_config_value handles single quotes", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = '/single/quoted' )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value == "/single/quoted"); } TEST_CASE("DataDirResolution - parse_config_value ignores comments", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] # This is a comment data_dir = "/actual/value" # inline comment )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value == "/actual/value"); } TEST_CASE("DataDirResolution - parse_config_value returns empty for missing key", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] other_key = "value" )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value.empty()); } TEST_CASE("DataDirResolution - parse_config_value returns empty for missing section", "[cli][data_dir][catch2]") { TempConfig config(R"( [other] data_dir = "/wrong/section" )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value.empty()); } TEST_CASE("DataDirResolution - parse_config_value handles empty file", "[cli][data_dir][catch2]") { TempConfig config(""); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value.empty()); } TEST_CASE("DataDirResolution - parse_config_value returns empty for nonexistent file", "[cli][data_dir][catch2]") { auto value = yams::config::parse_config_value("/nonexistent/config.toml", "core", "data_dir"); CHECK(value.empty()); } // ============================================================================ // Tilde expansion tests // ============================================================================ #ifndef _WIN32 TEST_CASE("DataDirResolution - expand_tilde expands home", "[cli][data_dir][catch2]") { ScopedEnv home("HOME", "/home/testuser"); auto expanded = yams::config::expand_tilde("~/some/path"); CHECK(expanded == fs::path("/home/testuser/some/path")); } TEST_CASE("DataDirResolution - expand_tilde leaves absolute paths unchanged", "[cli][data_dir][catch2]") { auto expanded = yams::config::expand_tilde("/absolute/path"); CHECK(expanded == fs::path("/absolute/path")); } TEST_CASE("DataDirResolution - expand_tilde leaves relative paths unchanged", "[cli][data_dir][catch2]") { auto expanded = yams::config::expand_tilde("relative/path"); CHECK(expanded == fs::path("relative/path")); } #endif // ============================================================================ // Values must NOT contain description strings // ============================================================================ TEST_CASE("DataDirResolution - resolved path does not contain descriptions", "[cli][data_dir][catch2]") { std::vector badPatterns = {"[Deprecated]", "Single path to file", "use '-' for stdin", "Data directory for storage", "especially useful for", "File or directory paths"}; auto dataDir = yams::config::get_data_dir(); auto configDir = yams::config::get_config_dir(); auto cacheDir = yams::config::get_cache_dir(); auto runtimeDir = yams::config::get_runtime_dir(); for (const auto& pattern : badPatterns) { INFO("Checking pattern: " << pattern); CHECK(dataDir.string().find(pattern) == std::string::npos); CHECK(configDir.string().find(pattern) == std::string::npos); CHECK(cacheDir.string().find(pattern) == std::string::npos); CHECK(runtimeDir.string().find(pattern) == std::string::npos); } } TEST_CASE("DataDirResolution - config value does not contain descriptions", "[cli][data_dir][catch2]") { TempConfig config(R"( [core] data_dir = "/proper/path" )"); auto value = yams::config::parse_config_value(config.path(), "core", "data_dir"); CHECK(value == "/proper/path"); CHECK(value.find("[Deprecated]") == std::string::npos); CHECK(value.find("file/directory") == std::string::npos); CHECK(value.find("Data directory") == std::string::npos); } // ============================================================================ // socket_path resolution tests // ============================================================================ TEST_CASE("DataDirResolution - socket_path env takes precedence", "[cli][data_dir][catch2]") { ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "/env/yams.sock"); auto resolved = yams::config::resolve_socket_path_from_config(); CHECK(resolved == fs::path("/env/yams.sock")); } TEST_CASE("DataDirResolution - socket_path from config file", "[cli][data_dir][catch2]") { TempConfig config(R"( [daemon] socket_path = "/from/config/yams.sock" )"); ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", ""); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); auto resolved = yams::config::resolve_socket_path_from_config(); CHECK(resolved == fs::path("/from/config/yams.sock")); } TEST_CASE("DataDirResolution - socket_path env overrides config", "[cli][data_dir][catch2]") { TempConfig config(R"( [daemon] socket_path = "/from/config/yams.sock" )"); ScopedEnv socketEnv("YAMS_DAEMON_SOCKET", "/env/wins.sock"); ScopedEnv configEnv("YAMS_CONFIG", config.pathStr().c_str()); auto resolved = yams::config::resolve_socket_path_from_config(); CHECK(resolved == fs::path("/env/wins.sock")); } // ============================================================================ // parse_path_list() tests // ============================================================================ TEST_CASE("DataDirResolution - parse_path_list handles comma-separated", "[cli][data_dir][catch2]") { auto paths = yams::config::parse_path_list("/path/a,/path/b,/path/c"); REQUIRE(paths.size() == 3); CHECK(paths[0] == fs::path("/path/a")); CHECK(paths[1] == fs::path("/path/b")); CHECK(paths[2] == fs::path("/path/c")); } TEST_CASE("DataDirResolution - parse_path_list handles TOML array", "[cli][data_dir][catch2]") { auto paths = yams::config::parse_path_list(R"(["/path/a", "/path/b"])"); REQUIRE(paths.size() == 2); CHECK(paths[0] == fs::path("/path/a")); CHECK(paths[1] == fs::path("/path/b")); } TEST_CASE("DataDirResolution - parse_path_list handles empty string", "[cli][data_dir][catch2]") { auto paths = yams::config::parse_path_list(""); CHECK(paths.empty()); } TEST_CASE("DataDirResolution - parse_path_list handles whitespace", "[cli][data_dir][catch2]") { auto paths = yams::config::parse_path_list(" /path/a , /path/b "); REQUIRE(paths.size() == 2); CHECK(paths[0] == fs::path("/path/a")); CHECK(paths[1] == fs::path("/path/b")); } #ifndef _WIN32 TEST_CASE("DataDirResolution - parse_path_list expands tilde", "[cli][data_dir][catch2]") { ScopedEnv home("HOME", "/home/testuser"); auto paths = yams::config::parse_path_list("~/path/a,~/path/b"); REQUIRE(paths.size() == 2); CHECK(paths[0] == fs::path("/home/testuser/path/a")); CHECK(paths[1] == fs::path("/home/testuser/path/b")); } #endif