yams/tests/unit/cli/data_dir_resolution_catch2_test.cpp
2026-04-14 10:25:54 -04:00

456 lines
16 KiB
C++

// CLI Data Directory Resolution tests
// Catch2 migration from GTest (yams-3s4 / yams-cli)
//
// Tests for data directory resolution precedence.
#include <catch2/catch_test_macros.hpp>
#include <yams/cli/yams_cli.h>
#include <yams/config/config_helpers.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <yams/compat/unistd.h>
#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<std::string> 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