App: Add directory versioning for data and config
At the application level, support the existence of subdirectories inside the original config and data paths for a specific version of the software. For a new installation, create them, but for an existing installation use the most recent available version, even if it's not the current one (and even if it's not versioned at all). Any migration must be done at the GUI level due to the amount of data that is potentially being copied during that process.
This commit is contained in:
committed by
Chris Hennes
parent
3426c9ff21
commit
bce7ff8708
@@ -119,7 +119,8 @@ const fs::path& ApplicationDirectories::getLibraryDir() const {
|
||||
/*!
|
||||
* \brief findPath
|
||||
* Returns the path where to store application files to.
|
||||
* If \a customHome is not empty, it will be used, otherwise a path starting from \a stdHome will be used.
|
||||
* If \a customHome is not empty, it will be used, otherwise a path starting from \a stdHome will be
|
||||
* used.
|
||||
*/
|
||||
fs::path ApplicationDirectories::findPath(const fs::path& stdHome, const fs::path& customHome,
|
||||
const std::vector<std::string>& subdirs, bool create) {
|
||||
@@ -147,6 +148,26 @@ fs::path ApplicationDirectories::findPath(const fs::path& stdHome, const fs::pat
|
||||
return appData;
|
||||
}
|
||||
|
||||
void ApplicationDirectories::appendVersionIfPossible(const fs::path& basePath, std::vector<std::string> &subdirs) const
|
||||
{
|
||||
fs::path pathToCheck = basePath;
|
||||
for (const auto& it : subdirs) {
|
||||
pathToCheck = pathToCheck / it;
|
||||
}
|
||||
if (isVersionedPath(pathToCheck)) {
|
||||
return; // Bail out if it's already versioned
|
||||
}
|
||||
if (fs::exists(pathToCheck)) {
|
||||
std::string version = mostRecentAvailableConfigVersion(pathToCheck);
|
||||
if (!version.empty()) {
|
||||
subdirs.emplace_back(std::move(version));
|
||||
}
|
||||
} else {
|
||||
auto [major, minor] = _currentVersion;
|
||||
subdirs.emplace_back(versionStringForPath(major, minor));
|
||||
}
|
||||
}
|
||||
|
||||
void ApplicationDirectories::configurePaths(std::map<std::string,std::string>& mConfig)
|
||||
{
|
||||
bool keepDeprecatedPaths = mConfig.contains("KeepDeprecatedPaths");
|
||||
@@ -158,6 +179,7 @@ void ApplicationDirectories::configurePaths(std::map<std::string,std::string>& m
|
||||
|
||||
// this is to support a portable version of FreeCAD
|
||||
auto [customHome, customData, customTemp] = getCustomPaths();
|
||||
_usingCustomDirectories = !customHome.empty() || !customData.empty();
|
||||
|
||||
// get the system standard paths
|
||||
auto [configHome, dataHome, cacheHome, tempPath] = getStandardPaths();
|
||||
@@ -179,18 +201,23 @@ void ApplicationDirectories::configurePaths(std::map<std::string,std::string>& m
|
||||
getSubDirectories(mConfig, subdirs);
|
||||
}
|
||||
|
||||
|
||||
// User data path
|
||||
//
|
||||
fs::path data = findPath(dataHome, customData, subdirs, true);
|
||||
auto dataSubdirs = subdirs;
|
||||
appendVersionIfPossible(dataHome, dataSubdirs);
|
||||
fs::path data = findPath(dataHome, customData, dataSubdirs, true);
|
||||
_userAppData = data;
|
||||
mConfig["UserAppData"] = Base::FileInfo::pathToString(_userAppData) + PATHSEP;
|
||||
mConfig["UserAppData"] = Base::FileInfo::pathToString(data) + PATHSEP;
|
||||
|
||||
|
||||
// User config path
|
||||
//
|
||||
fs::path config = findPath(configHome, customHome, subdirs, true);
|
||||
auto configSubdirs = subdirs;
|
||||
appendVersionIfPossible(configHome, configSubdirs);
|
||||
fs::path config = findPath(configHome, customHome, configSubdirs, true);
|
||||
_userConfig = config;
|
||||
mConfig["UserConfigPath"] = Base::FileInfo::pathToString(_userConfig) + PATHSEP;
|
||||
mConfig["UserConfigPath"] = Base::FileInfo::pathToString(config) + PATHSEP;
|
||||
|
||||
|
||||
// User cache path
|
||||
@@ -212,9 +239,8 @@ void ApplicationDirectories::configurePaths(std::map<std::string,std::string>& m
|
||||
|
||||
// Set the default macro directory
|
||||
//
|
||||
std::vector<std::string> macrodirs = std::move(subdirs); // Last use in this method, just move
|
||||
macrodirs.emplace_back("Macro");
|
||||
fs::path macro = findPath(dataHome, customData, macrodirs, true);
|
||||
std::vector<std::string> macrodirs{"Macro"};
|
||||
fs::path macro = findPath(_userAppData, customData, macrodirs, true);
|
||||
_userMacro = macro;
|
||||
mConfig["UserMacroPath"] = Base::FileInfo::pathToString(macro) + PATHSEP;
|
||||
}
|
||||
@@ -284,6 +310,11 @@ fs::path ApplicationDirectories::getUserHome()
|
||||
return path;
|
||||
}
|
||||
|
||||
bool ApplicationDirectories::usingCustomDirectories() const
|
||||
{
|
||||
return _usingCustomDirectories;
|
||||
}
|
||||
|
||||
#if defined(FC_OS_WIN32) // This is ONLY used on Windows now, so don't even compile it elsewhere
|
||||
#include <codecvt>
|
||||
#include "ShlObj.h"
|
||||
@@ -403,6 +434,118 @@ std::tuple<fs::path, fs::path, fs::path, fs::path> ApplicationDirectories::getSt
|
||||
qstringToPath(tempPath)};
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::string ApplicationDirectories::versionStringForPath(int major, int minor)
|
||||
{
|
||||
// NOTE: This is intended to be stable over time, so if the format changes, a condition should be added to check for
|
||||
// older versions and return this format for them, even if the new format differs.
|
||||
return std::format("v{}-{}", major, minor);
|
||||
}
|
||||
|
||||
bool ApplicationDirectories::isVersionedPath(const fs::path &startingPath) const {
|
||||
for (int major = std::get<0>(_currentVersion); major >= 1; --major) {
|
||||
constexpr int largestPossibleMinor = 99; // We have to start someplace
|
||||
int startingMinor = largestPossibleMinor;
|
||||
if (major == std::get<0>(_currentVersion)) {
|
||||
startingMinor = std::get<1>(_currentVersion);
|
||||
}
|
||||
for (int minor = startingMinor; minor >= 0; --minor) {
|
||||
if (startingPath.filename() == versionStringForPath(major, minor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string ApplicationDirectories::mostRecentAvailableConfigVersion(const fs::path &startingPath) const {
|
||||
for (int major = std::get<0>(_currentVersion); major >= 1; --major) {
|
||||
constexpr int largestPossibleMinor = 99; // We have to start someplace
|
||||
int startingMinor = largestPossibleMinor;
|
||||
if (major == std::get<0>(_currentVersion)) {
|
||||
startingMinor = std::get<1>(_currentVersion);
|
||||
}
|
||||
for (int minor = startingMinor; minor >= 0; --minor) {
|
||||
auto version = startingPath / versionStringForPath(major, minor);
|
||||
if (fs::is_directory(version)) {
|
||||
return versionStringForPath(major, minor);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
fs::path ApplicationDirectories::mostRecentConfigFromBase(const fs::path &startingPath) const {
|
||||
// Starting in FreeCAD v1.1, we switched to using a versioned config path for the three configuration
|
||||
// directories:
|
||||
// UserAppData
|
||||
// UserConfigPath
|
||||
// UserMacroPath
|
||||
//
|
||||
// Migration to the versioned structured is NOT automatic: at the App level, we just find the most
|
||||
// recent directory and use it, regardless of which version of the program is currently running.
|
||||
// It is up to user-facing code in Gui to determine whether to ask a user if they want to migrate
|
||||
// and to call the App-level functions that do that work.
|
||||
|
||||
// The simplest and most common case is if the current version subfolder already exists
|
||||
auto current = startingPath / versionStringForPath(std::get<0>(_currentVersion), std::get<1>(_currentVersion));
|
||||
if (fs::is_directory(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
// If the current version doesn't exist, see if a previous version does
|
||||
std::string bestVersion = mostRecentAvailableConfigVersion(startingPath);
|
||||
if (!bestVersion.empty()) {
|
||||
return startingPath / bestVersion;
|
||||
}
|
||||
return startingPath; // No versioned config found
|
||||
}
|
||||
|
||||
bool ApplicationDirectories::usingCurrentVersionConfig(fs::path config) const {
|
||||
if (config.filename().empty()) {
|
||||
config = config.parent_path();
|
||||
}
|
||||
auto version = config.filename().string();
|
||||
return version == versionStringForPath(std::get<0>(_currentVersion), std::get<1>(_currentVersion));
|
||||
}
|
||||
|
||||
void ApplicationDirectories::migrateConfig(const fs::path &oldPath, const fs::path &newPath)
|
||||
{
|
||||
fs::create_directories(newPath);
|
||||
for (auto& file : fs::directory_iterator(oldPath)) {
|
||||
if (file == newPath) {
|
||||
// Handle the case where newPath is a subdirectory of oldPath
|
||||
continue;
|
||||
}
|
||||
fs::copy(file.path(),
|
||||
newPath / file.path().filename(),
|
||||
fs::copy_options::recursive | fs::copy_options::copy_symlinks);
|
||||
}
|
||||
}
|
||||
|
||||
void ApplicationDirectories::migrateAllPaths(const std::vector<fs::path> &paths) const {
|
||||
auto [major, minor] = _currentVersion;
|
||||
std::set<fs::path> uniquePaths (paths.begin(), paths.end());
|
||||
for (auto path : uniquePaths) {
|
||||
if (path.filename().empty()) {
|
||||
// Handle the case where the path was constructed from a std::string with a trailing /
|
||||
path = path.parent_path();
|
||||
}
|
||||
fs::path newPath;
|
||||
if (isVersionedPath(path)) {
|
||||
newPath = path.parent_path() / versionStringForPath(major, minor);
|
||||
} else {
|
||||
newPath = path / versionStringForPath(major, minor);
|
||||
}
|
||||
if (fs::exists(newPath)) {
|
||||
throw Base::RuntimeError("Cannot migrate config - path already exists: " + newPath.string());
|
||||
}
|
||||
fs::create_directories(newPath);
|
||||
migrateConfig(path, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider using this for all UNIX-like OSes
|
||||
#if defined(__OpenBSD__)
|
||||
#include <cstdio>
|
||||
|
||||
@@ -32,17 +32,22 @@
|
||||
namespace App {
|
||||
|
||||
/// A helper class to handle application-wide directory management on behalf of the main
|
||||
/// App::Application class
|
||||
/// App::Application class. Most of this class's methods were originally in Application, and
|
||||
/// were extracted here to be more easily testable (and to better follow the single-
|
||||
/// responsibility principle, though further work is required in that area).
|
||||
class AppExport ApplicationDirectories {
|
||||
|
||||
public:
|
||||
|
||||
/// Constructor
|
||||
/// \param currentVersion A tuple {major, minor} used to create/manage the versioned
|
||||
/// directories, if any
|
||||
/// \param config The base configuration dictionary. Used to create the appropriate
|
||||
/// directories, and updated to reflect their locations. New code should not directly access
|
||||
/// elements of this dictionary to determine directory locations, but should instead use
|
||||
/// the public methods of this class to determine the locations needed.
|
||||
explicit ApplicationDirectories(std::map<std::string,std::string> &config);
|
||||
|
||||
/// Given argv[0], figure out the home path
|
||||
/// Given argv[0], figure out the home path. Used to initialize the AppHomePath element of
|
||||
/// the configuration map prior to instantiating this class.
|
||||
static std::filesystem::path findHomePath(const char* sCall);
|
||||
|
||||
/// "Home" here is the parent directory of the actual executable file being run. It is
|
||||
@@ -103,6 +108,55 @@ namespace App {
|
||||
/// Get the user's home directory
|
||||
static std::filesystem::path getUserHome();
|
||||
|
||||
/// Returns true if the current active directory set is custom, and false if it's the
|
||||
/// standard system default.
|
||||
bool usingCustomDirectories() const;
|
||||
|
||||
|
||||
// Versioned-Path Management Methods:
|
||||
|
||||
/// Determine if a given config path is for the current version of the program
|
||||
/// \param config The path to check
|
||||
bool usingCurrentVersionConfig(std::filesystem::path config) const;
|
||||
|
||||
/// Migrate a set of versionable configuration directories from the given path to a new
|
||||
/// version. The new version's directories cannot exist yet, and the old ones *must* exist.
|
||||
/// If the old paths are themselves versioned, then the new paths will be placed at the same
|
||||
/// level in the directory structure (e.g., they will be siblings of each entry in paths).
|
||||
/// If paths are NOT versioned, the new (versioned) copies will be placed *inside* the
|
||||
/// original paths.
|
||||
void migrateAllPaths(const std::vector<std::filesystem::path> &paths) const;
|
||||
|
||||
/// A utility method to generate the versioned directory name for a given version. This only
|
||||
/// returns the version string, not an entire path. As of FreeCAD 1.1, the string is of the
|
||||
/// form "vX-Y" where X is the major version and Y is the minor, but external code should
|
||||
/// not assume that is always the form, and should instead use this method, which is
|
||||
/// guaranteed stable (that is, given "1" and "1" as the major and minor, it will always
|
||||
/// return the string "v1-1", even if in later versions the format changes.
|
||||
static std::string versionStringForPath(int major, int minor);
|
||||
|
||||
/// Given an arbitrary path, determine if it has the correct form to be a versioned path
|
||||
/// (e.g. is the last component a recognized version of the code). DOES NOT recognize
|
||||
/// versions *newer* than the current version, even if the directory name matches the path
|
||||
/// naming convention used by the previous version.
|
||||
bool isVersionedPath(const std::filesystem::path &startingPath) const;
|
||||
|
||||
/// Given a base path that is expected to contained versioned subdirectories, locate the
|
||||
/// directory name (*not* the path, only the final component, the version string itself)
|
||||
/// corresponding to the most recent version of the software, up to and including the
|
||||
/// current version, but NOT exceeding it.
|
||||
std::string mostRecentAvailableConfigVersion(const std::filesystem::path &startingPath) const;
|
||||
|
||||
/// Given a base path that is expected to contained versioned subdirectories, locate the
|
||||
/// directory corresponding to the most recent version of the software, up to and including
|
||||
/// the current version, but NOT exceeding it.
|
||||
std::filesystem::path mostRecentConfigFromBase(const std::filesystem::path &startingPath) const;
|
||||
|
||||
/// A utility method to copy all files and directories from oldPath to newPath, handling the
|
||||
/// case where newPath might itself be a subdirectory of oldPath (and *not* attempting that
|
||||
/// otherwise-recursive copy).
|
||||
static void migrateConfig(const std::filesystem::path &oldPath, const std::filesystem::path &newPath);
|
||||
|
||||
#ifdef FC_OS_WIN32
|
||||
/// On Windows, gets the location of the user's "AppData" directory. Invalid on other OSes.
|
||||
QString getOldGenericDataLocation();
|
||||
@@ -118,14 +172,15 @@ namespace App {
|
||||
/// be returned.
|
||||
static std::filesystem::path findUserHomePath(const std::filesystem::path& userHome);
|
||||
|
||||
bool isVersionedPath(const std::filesystem::path &startingPath) const;
|
||||
std::string mostRecentAvailableConfigVersion(const std::filesystem::path &startingPath) const;
|
||||
std::filesystem::path mostRecentConfigFromBase(const std::filesystem::path &startingPath) const;
|
||||
|
||||
static void migrateConfig(const std::filesystem::path &oldPath, const std::filesystem::path &newPath);
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
/// Take a path and add a version to it, if it's possible to do so. A version can be
|
||||
/// appended only if a) the versioned subdirectory already exists, or b) pathToCheck/subdirs
|
||||
/// does NOT yet exist. This does not actually create any directories, just determines
|
||||
/// if we can append the versioned directory name to subdirs.
|
||||
void appendVersionIfPossible(const std::filesystem::path& basePath,
|
||||
std::vector<std::string> &subdirs) const;
|
||||
|
||||
static std::filesystem::path findPath(
|
||||
const std::filesystem::path& stdHome,
|
||||
const std::filesystem::path& customHome,
|
||||
@@ -172,6 +227,8 @@ namespace App {
|
||||
std::filesystem::path _resource;
|
||||
std::filesystem::path _library;
|
||||
std::filesystem::path _help;
|
||||
|
||||
bool _usingCustomDirectories {false};
|
||||
};
|
||||
|
||||
} // App
|
||||
|
||||
682
tests/src/App/ApplicationDirectories.cpp
Normal file
682
tests/src/App/ApplicationDirectories.cpp
Normal file
@@ -0,0 +1,682 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <App/ApplicationDirectories.h>
|
||||
|
||||
#include <random>
|
||||
#include <fstream>
|
||||
#include "Base/Exception.h"
|
||||
|
||||
/* NOLINTBEGIN(
|
||||
readability-magic-numbers,
|
||||
cppcoreguidelines-avoid-magic-numbers,
|
||||
readability-function-cognitive-complexity
|
||||
) */
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static fs::path MakeUniqueTempDir()
|
||||
{
|
||||
constexpr int maxTries = 128;
|
||||
const fs::path base = fs::temp_directory_path();
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen(rd());
|
||||
std::uniform_int_distribution<unsigned long long> dist;
|
||||
|
||||
for (int i = 0; i < maxTries; ++i) {
|
||||
auto name = "app_directories_test-" + std::to_string(dist(gen));
|
||||
fs::path candidate = base / name;
|
||||
std::error_code ec;
|
||||
if (fs::create_directory(candidate, ec)) {
|
||||
return candidate;
|
||||
}
|
||||
if (ec && ec != std::make_error_code(std::errc::file_exists)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error("Failed to create unique temp directory");
|
||||
}
|
||||
|
||||
/// A subclass to expose protected members for unit testing
|
||||
class ApplicationDirectoriesTestClass: public App::ApplicationDirectories
|
||||
{
|
||||
using App::ApplicationDirectories::ApplicationDirectories;
|
||||
|
||||
public:
|
||||
void wrapAppendVersionIfPossible(const fs::path& basePath,
|
||||
std::vector<std::string>& subdirs) const
|
||||
{
|
||||
appendVersionIfPossible(basePath, subdirs);
|
||||
}
|
||||
};
|
||||
|
||||
class ApplicationDirectoriesTest: public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void SetUp() override
|
||||
{
|
||||
_tempDir = MakeUniqueTempDir();
|
||||
}
|
||||
|
||||
std::map<std::string, std::string>
|
||||
generateConfig(const std::map<std::string, std::string>& overrides) const
|
||||
{
|
||||
std::map<std::string, std::string> config {{"AppHomePath", _tempDir.string()},
|
||||
{"ExeVendor", "Vendor"},
|
||||
{"ExeName", "Test"},
|
||||
{"BuildVersionMajor", "4"},
|
||||
{"BuildVersionMinor", "2"}};
|
||||
for (const auto& override : overrides) {
|
||||
config[override.first] = override.second;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
std::shared_ptr<ApplicationDirectoriesTestClass> makeAppDirsForVersion(int major, int minor)
|
||||
{
|
||||
auto configuration = generateConfig({{"BuildVersionMajor", std::to_string(major)},
|
||||
{"BuildVersionMinor", std::to_string(minor)}});
|
||||
return std::make_shared<ApplicationDirectoriesTestClass>(configuration);
|
||||
}
|
||||
|
||||
fs::path makePathForVersion(const fs::path& base, int major, int minor)
|
||||
{
|
||||
return base / App::ApplicationDirectories::versionStringForPath(major, minor);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
fs::remove_all(_tempDir);
|
||||
}
|
||||
|
||||
fs::path tempDir()
|
||||
{
|
||||
return _tempDir;
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path _tempDir;
|
||||
};
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionConfigTrueWhenDirMatchesVersion)
|
||||
{
|
||||
// Arrange
|
||||
constexpr int major = 3;
|
||||
constexpr int minor = 7;
|
||||
const fs::path testPath = fs::path("some_kind_of_config")
|
||||
/ App::ApplicationDirectories::versionStringForPath(major, minor);
|
||||
|
||||
// Act: generate a directory structure with the same version
|
||||
auto appDirs = makeAppDirsForVersion(major, minor);
|
||||
|
||||
// Assert
|
||||
EXPECT_TRUE(appDirs->usingCurrentVersionConfig(testPath));
|
||||
}
|
||||
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionConfigFalseWhenDirDoesntMatchVersion)
|
||||
{
|
||||
// Arrange
|
||||
constexpr int major = 3;
|
||||
constexpr int minor = 7;
|
||||
const fs::path testPath = fs::path("some_kind_of_config")
|
||||
/ App::ApplicationDirectories::versionStringForPath(major, minor);
|
||||
|
||||
// Act: generate a directory structure with the same version
|
||||
auto configuration = generateConfig({{"BuildVersionMajor", std::to_string(major + 1)},
|
||||
{"BuildVersionMinor", std::to_string(minor)}});
|
||||
auto appDirs = std::make_shared<App::ApplicationDirectories>(configuration);
|
||||
|
||||
// Assert
|
||||
EXPECT_FALSE(appDirs->usingCurrentVersionConfig(testPath));
|
||||
}
|
||||
|
||||
// Exact current version (hits: major==currentMajor path; inner if true)
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathMatchesCurrentMajorAndMinor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path p = makePathForVersion(tempDir(), 5, 4);
|
||||
EXPECT_TRUE(appDirs->isVersionedPath(p));
|
||||
}
|
||||
|
||||
// Lower minor within the same major (major==currentMajor path; iterates down to a smaller minor)
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathMatchesLowerMinorSameMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path p = makePathForVersion(tempDir(), 5, 2);
|
||||
EXPECT_TRUE(appDirs->isVersionedPath(p));
|
||||
}
|
||||
|
||||
// Lower major (major!=currentMajor path)
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathMatchesLowerMajorAnyMinor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path p = makePathForVersion(tempDir(), 4, 7);
|
||||
EXPECT_TRUE(appDirs->isVersionedPath(p));
|
||||
}
|
||||
|
||||
// Boundary: minor==0 within the same major
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathMatchesZeroMinorSameMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path p = makePathForVersion(tempDir(), 5, 0);
|
||||
EXPECT_TRUE(appDirs->isVersionedPath(p));
|
||||
}
|
||||
|
||||
// Negative: higher minor than current for the same major is never iterated
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathDoesNotMatchHigherMinorSameMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path p = makePathForVersion(tempDir(), 5, 5);
|
||||
EXPECT_FALSE(appDirs->isVersionedPath(p));
|
||||
}
|
||||
|
||||
// Negative: higher major than current is never iterated; also covers "non-version" style
|
||||
TEST_F(ApplicationDirectoriesTest, isVersionedPathDoesNotMatchHigherMajorOrRandomName)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path higherMajor = makePathForVersion(tempDir(), 6, 1);
|
||||
EXPECT_FALSE(appDirs->isVersionedPath(higherMajor));
|
||||
|
||||
fs::path randomName = tempDir() / "not-a-version";
|
||||
EXPECT_FALSE(appDirs->isVersionedPath(randomName));
|
||||
}
|
||||
|
||||
// Convenience: path under base for a version folder name.
|
||||
fs::path versionedPath(const fs::path& base, int major, int minor)
|
||||
{
|
||||
return base / App::ApplicationDirectories::versionStringForPath(major, minor);
|
||||
}
|
||||
|
||||
// Create a regular file (used to prove non-directories are ignored).
|
||||
void touchFile(const fs::path& p)
|
||||
{
|
||||
fs::create_directories(p.parent_path());
|
||||
std::ofstream ofs(p.string());
|
||||
ofs << "x";
|
||||
}
|
||||
|
||||
// The exact current version exists -> returned immediately (current-major branch).
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailReturnsExactCurrentVersionIfDirectoryExists)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 4));
|
||||
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()),
|
||||
App::ApplicationDirectories::versionStringForPath(5, 4));
|
||||
}
|
||||
|
||||
// No exact match in the current major: choose the highest available minor <= current
|
||||
// and prefer the current major over lower majors.
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailPrefersSameMajorAndPicksHighestLowerMinor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 1));
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 3));
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 99)); // distractor in lower major
|
||||
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()),
|
||||
App::ApplicationDirectories::versionStringForPath(5, 3));
|
||||
}
|
||||
|
||||
// No directories in current major: scan next lower major from 99 downward,
|
||||
// returning the highest available minor present (demonstrates descending search).
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailForLowerMajorPicksHighestAvailableMinor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 3));
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 42));
|
||||
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()),
|
||||
App::ApplicationDirectories::versionStringForPath(4, 42));
|
||||
}
|
||||
|
||||
// If the candidate path exists but is a regular file, it must be ignored and
|
||||
// the search must fall back to the next available directory.
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailSkipsFilesAndFallsBackToNextDirectory)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
touchFile(versionedPath(tempDir(), 5, 4)); // file at the current version
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 3)); // directory at next lower minor
|
||||
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()),
|
||||
App::ApplicationDirectories::versionStringForPath(5, 3));
|
||||
}
|
||||
|
||||
// Higher minor in the current major is not considered (loop starts at the current minor);
|
||||
// should fall through to the lower major when nothing <= the current minor exists.
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailIgnoresHigherMinorThanCurrentInSameMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(
|
||||
versionedPath(tempDir(), 5, 7)); // higher than the current minor; ignored
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 1)); // fallback target
|
||||
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()),
|
||||
App::ApplicationDirectories::versionStringForPath(4, 1));
|
||||
}
|
||||
|
||||
// No candidates anywhere -> empty string returned.
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentAvailReturnsEmptyStringWhenNoVersionsPresent)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
EXPECT_EQ(appDirs->mostRecentAvailableConfigVersion(tempDir()), "");
|
||||
}
|
||||
|
||||
|
||||
// The current version directory exists -> returned immediately
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentConfigReturnsCurrentVersionDirectoryIfPresent)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 4));
|
||||
|
||||
fs::path got = appDirs->mostRecentConfigFromBase(tempDir());
|
||||
EXPECT_EQ(got, versionedPath(tempDir(), 5, 4));
|
||||
}
|
||||
|
||||
// The current version missing -> falls back to most recent available in same major
|
||||
TEST_F(ApplicationDirectoriesTest,
|
||||
mostRecentConfigFallsBackToMostRecentInSameMajorWhenCurrentMissing)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
// There is no directory called "5.4"; provide candidates 5.3 and 5.1; also a distractor in a
|
||||
// lower major.
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 1));
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 3));
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 99));
|
||||
|
||||
fs::path got = appDirs->mostRecentConfigFromBase(tempDir());
|
||||
EXPECT_EQ(got, versionedPath(tempDir(), 5, 3));
|
||||
}
|
||||
|
||||
// The current version path exists as a file (not directory) -> ignored, fallback used
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentConfigSkipsFileAtCurrentVersionAndFallsBack)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
touchFile(versionedPath(tempDir(), 5, 4)); // file, not dir
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 2));
|
||||
|
||||
fs::path got = appDirs->mostRecentConfigFromBase(tempDir());
|
||||
EXPECT_EQ(got, versionedPath(tempDir(), 5, 2));
|
||||
}
|
||||
|
||||
// There are no eligible versions in the current major -> choose the highest available in lower
|
||||
// majors
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentConfigFallsBackToHighestVersionInLowerMajors)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
// No 5.x minor <= 4 exists. (Optionally add a higher minor to prove it's ignored.)
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 7)); // ignored (higher than current minor)
|
||||
|
||||
// Provide multiple lower-major candidates; should pick 4.90 over 3.99.
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 3));
|
||||
fs::create_directories(versionedPath(tempDir(), 4, 90));
|
||||
fs::create_directories(versionedPath(tempDir(), 3, 99));
|
||||
|
||||
fs::path got = appDirs->mostRecentConfigFromBase(tempDir());
|
||||
EXPECT_EQ(got, versionedPath(tempDir(), 4, 90));
|
||||
}
|
||||
|
||||
// There is nothing available anywhere -> returns startingPath
|
||||
TEST_F(ApplicationDirectoriesTest, mostRecentConfigReturnsStartingPathWhenNoVersionedConfigExists)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path got = appDirs->mostRecentConfigFromBase(tempDir());
|
||||
EXPECT_EQ(got, tempDir());
|
||||
}
|
||||
|
||||
// True: exact current version directory
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionExactVersionDir)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 4));
|
||||
|
||||
EXPECT_TRUE(appDirs->usingCurrentVersionConfig(versionedPath(tempDir(), 5, 4)));
|
||||
}
|
||||
|
||||
// True: current version directory with trailing separator (exercises filename().empty() branch)
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionVersionDirWithTrailingSlash)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 4));
|
||||
fs::path p = versionedPath(tempDir(), 5, 4) / ""; // ensure trailing separator
|
||||
|
||||
EXPECT_TRUE(appDirs->usingCurrentVersionConfig(p));
|
||||
}
|
||||
|
||||
// False: a file inside the current version directory (filename != version string)
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionFileInsideVersionDirIsFalse)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::path filePath = versionedPath(tempDir(), 5, 4) / "config.yaml";
|
||||
touchFile(filePath);
|
||||
|
||||
EXPECT_FALSE(appDirs->usingCurrentVersionConfig(filePath));
|
||||
}
|
||||
|
||||
// False: lower version directory
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionLowerVersionDirIsFalse)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 5, 3));
|
||||
|
||||
EXPECT_FALSE(appDirs->usingCurrentVersionConfig(versionedPath(tempDir(), 5, 3)));
|
||||
}
|
||||
|
||||
// False: higher version directory
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionHigherVersionDirIsFalse)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(versionedPath(tempDir(), 6, 0));
|
||||
|
||||
EXPECT_FALSE(appDirs->usingCurrentVersionConfig(versionedPath(tempDir(), 6, 0)));
|
||||
}
|
||||
|
||||
// False: non-version directory (e.g., base dir)
|
||||
TEST_F(ApplicationDirectoriesTest, usingCurrentVersionNonVersionDirIsFalse)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
fs::create_directories(tempDir());
|
||||
|
||||
EXPECT_FALSE(appDirs->usingCurrentVersionConfig(tempDir()));
|
||||
}
|
||||
|
||||
void writeFile(const fs::path& p, std::string_view contents)
|
||||
{
|
||||
fs::create_directories(p.parent_path());
|
||||
std::ofstream ofs(p.string(), std::ios::binary);
|
||||
ofs << contents;
|
||||
}
|
||||
|
||||
std::string readFile(const fs::path& p)
|
||||
{
|
||||
std::ifstream ifs(p.string(), std::ios::binary);
|
||||
return {std::istreambuf_iterator<char>(ifs), {}};
|
||||
}
|
||||
|
||||
// Creates destination and copies flat files over
|
||||
TEST_F(ApplicationDirectoriesTest, migrateConfigCreatesDestinationAndCopiesFiles)
|
||||
{
|
||||
// Arrange
|
||||
fs::path oldPath = tempDir() / "old";
|
||||
fs::path newPath = tempDir() / "new";
|
||||
writeFile(oldPath / "a.txt", "alpha");
|
||||
writeFile(oldPath / "b.ini", "bravo");
|
||||
|
||||
// Act
|
||||
App::ApplicationDirectories::migrateConfig(oldPath, newPath);
|
||||
|
||||
// Assert
|
||||
ASSERT_TRUE(fs::exists(newPath));
|
||||
ASSERT_TRUE(fs::is_directory(newPath));
|
||||
|
||||
EXPECT_TRUE(fs::exists(newPath / "a.txt"));
|
||||
EXPECT_TRUE(fs::exists(newPath / "b.ini"));
|
||||
EXPECT_EQ(readFile(newPath / "a.txt"), "alpha");
|
||||
EXPECT_EQ(readFile(newPath / "b.ini"), "bravo");
|
||||
|
||||
EXPECT_TRUE(fs::exists(oldPath / "a.txt"));
|
||||
EXPECT_TRUE(fs::exists(oldPath / "b.ini"));
|
||||
}
|
||||
|
||||
// newPath is a subdirectory of oldPath -> skip copying newPath into itself
|
||||
TEST_F(ApplicationDirectoriesTest, migrateConfigSkipsSelfWhenNewIsSubdirectoryOfOld)
|
||||
{
|
||||
// Arrange
|
||||
fs::path oldPath = tempDir() / "container";
|
||||
fs::path newPath = oldPath / "migrated";
|
||||
writeFile(oldPath / "c.yaml", "charlie");
|
||||
writeFile(oldPath / "d.cfg", "delta");
|
||||
|
||||
// Act
|
||||
App::ApplicationDirectories::migrateConfig(oldPath, newPath);
|
||||
|
||||
// Assert
|
||||
ASSERT_TRUE(fs::exists(newPath));
|
||||
ASSERT_TRUE(fs::is_directory(newPath));
|
||||
EXPECT_TRUE(fs::exists(newPath / "c.yaml"));
|
||||
EXPECT_TRUE(fs::exists(newPath / "d.cfg"));
|
||||
|
||||
// Do not copy the destination back into itself (no nested 'migrated/migrated')
|
||||
EXPECT_FALSE(fs::exists(newPath / newPath.filename()));
|
||||
}
|
||||
|
||||
// oldPath empty -> still creates the destination and does nothing else
|
||||
TEST_F(ApplicationDirectoriesTest, migrateConfigEmptyOldPathJustCreatesDestination)
|
||||
{
|
||||
fs::path oldPath = tempDir() / "empty_old";
|
||||
fs::path newPath = tempDir() / "dest_only";
|
||||
|
||||
fs::create_directories(oldPath);
|
||||
App::ApplicationDirectories::migrateConfig(oldPath, newPath);
|
||||
|
||||
ASSERT_TRUE(fs::exists(newPath));
|
||||
ASSERT_TRUE(fs::is_directory(newPath));
|
||||
|
||||
// No files expected
|
||||
bool hasEntries = (fs::directory_iterator(newPath) != fs::directory_iterator {});
|
||||
EXPECT_FALSE(hasEntries);
|
||||
}
|
||||
|
||||
// Case: recursively copy nested directories and files
|
||||
TEST_F(ApplicationDirectoriesTest, migrateConfigRecursivelyCopiesDirectoriesAndFiles)
|
||||
{
|
||||
fs::path oldPath = tempDir() / "old_tree";
|
||||
fs::path newPath = tempDir() / "new_tree";
|
||||
|
||||
// old_tree/
|
||||
// config/
|
||||
// env/
|
||||
// a.txt
|
||||
// empty_dir/
|
||||
writeFile(oldPath / "config" / "env" / "a.txt", "alpha");
|
||||
fs::create_directories(oldPath / "empty_dir");
|
||||
|
||||
App::ApplicationDirectories::migrateConfig(oldPath, newPath);
|
||||
|
||||
// Expect structure replicated under newPath
|
||||
ASSERT_TRUE(fs::exists(newPath));
|
||||
EXPECT_TRUE(fs::exists(newPath / "config"));
|
||||
EXPECT_TRUE(fs::exists(newPath / "config" / "env"));
|
||||
EXPECT_TRUE(fs::exists(newPath / "config" / "env" / "a.txt"));
|
||||
EXPECT_EQ(readFile(newPath / "config" / "env" / "a.txt"), "alpha");
|
||||
|
||||
// Empty directories should be created as well
|
||||
EXPECT_TRUE(fs::exists(newPath / "empty_dir"));
|
||||
EXPECT_TRUE(fs::is_directory(newPath / "empty_dir"));
|
||||
}
|
||||
|
||||
// Case: newPath is subdir of oldPath -> recursively copy, but do NOT copy newPath into itself
|
||||
TEST_F(ApplicationDirectoriesTest, migrateConfigNewPathSubdirRecursivelyCopiesAndSkipsSelf)
|
||||
{
|
||||
fs::path oldPath = tempDir() / "src_tree";
|
||||
fs::path newPath = oldPath / "migrated"; // destination under source
|
||||
|
||||
// src_tree/
|
||||
// folderA/
|
||||
// child/
|
||||
// f.txt
|
||||
writeFile(oldPath / "folderA" / "child" / "f.txt", "filedata");
|
||||
|
||||
App::ApplicationDirectories::migrateConfig(oldPath, newPath);
|
||||
|
||||
// Copied recursively into destination
|
||||
EXPECT_TRUE(fs::exists(newPath / "folderA" / "child" / "f.txt"));
|
||||
EXPECT_EQ(readFile(newPath / "folderA" / "child" / "f.txt"), "filedata");
|
||||
|
||||
// Do not copy the destination back into itself (no migrated/migrated)
|
||||
EXPECT_FALSE(fs::exists(newPath / newPath.filename()));
|
||||
}
|
||||
|
||||
// Versioned input: `path` == base/<some older version>` -> newPath == base/<current>
|
||||
TEST_F(ApplicationDirectoriesTest, migrateAllPathsVersionedInputChoosesParentPlusCurrent)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "v_branch";
|
||||
fs::path older = versionedPath(base, 5, 1); // versioned input (isVersionedPath == true)
|
||||
fs::create_directories(older);
|
||||
writeFile(older / "sentinel.txt", "s");
|
||||
|
||||
std::vector<fs::path> inputs {older};
|
||||
appDirs->migrateAllPaths(inputs);
|
||||
|
||||
fs::path expectedDest = versionedPath(base, 5, 4);
|
||||
EXPECT_TRUE(fs::exists(expectedDest));
|
||||
EXPECT_TRUE(fs::is_directory(expectedDest));
|
||||
}
|
||||
|
||||
// Non-versioned input: `path` == base -> newPath == base/<current>
|
||||
TEST_F(ApplicationDirectoriesTest, migrateAllPathsNonVersionedInputAppendsCurrent)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "plain_base";
|
||||
fs::create_directories(base);
|
||||
writeFile(base / "config.yaml", "x");
|
||||
|
||||
std::vector<fs::path> inputs {base};
|
||||
appDirs->migrateAllPaths(inputs);
|
||||
|
||||
fs::path expectedDest = versionedPath(base, 5, 4);
|
||||
EXPECT_TRUE(fs::exists(expectedDest));
|
||||
EXPECT_TRUE(fs::is_directory(expectedDest));
|
||||
}
|
||||
|
||||
// Pre-existing destination -> throws Base::RuntimeError
|
||||
TEST_F(ApplicationDirectoriesTest, migrateAllPathsThrowsIfDestinationAlreadyExists_NonVersioned)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "exists_case";
|
||||
fs::create_directories(base);
|
||||
fs::create_directories(versionedPath(base, 5, 4)); // destination already exists
|
||||
|
||||
std::vector<fs::path> inputs {base};
|
||||
EXPECT_THROW(appDirs->migrateAllPaths(inputs), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Multiple inputs: one versioned, one non-versioned -> both destinations created
|
||||
TEST_F(ApplicationDirectoriesTest, migrateAllPathsProcessesMultipleInputs)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
// Versioned input A: baseA/5.2
|
||||
fs::path baseA = tempDir() / "multiA";
|
||||
fs::path olderA = versionedPath(baseA, 5, 2);
|
||||
fs::create_directories(olderA);
|
||||
writeFile(olderA / "a.txt", "a");
|
||||
|
||||
// Non-versioned input B: baseB
|
||||
fs::path baseB = tempDir() / "multiB";
|
||||
fs::create_directories(baseB);
|
||||
writeFile(baseB / "b.txt", "b");
|
||||
|
||||
std::vector<fs::path> inputs {olderA, baseB};
|
||||
appDirs->migrateAllPaths(inputs);
|
||||
|
||||
EXPECT_TRUE(fs::exists(versionedPath(baseA, 5, 4))); // parent_path() / current
|
||||
EXPECT_TRUE(fs::exists(versionedPath(baseB, 5, 4))); // base / current
|
||||
}
|
||||
|
||||
// Already versioned (final component is a version) -> no change
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_AlreadyVersioned_Bails)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "bail_vec";
|
||||
std::vector<std::string> sub {
|
||||
"configs",
|
||||
App::ApplicationDirectories::versionStringForPath(5, 2)}; // versioned tail
|
||||
fs::create_directories(base / sub[0] / sub[1]);
|
||||
|
||||
auto before = sub;
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
EXPECT_EQ(sub, before); // unchanged
|
||||
}
|
||||
|
||||
// Base exists & current version dir present -> append current
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_BaseExists_AppendsCurrentWhenPresent)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "vec_current";
|
||||
std::vector<std::string> sub {"configs"};
|
||||
fs::create_directories(base / "configs");
|
||||
fs::create_directories(versionedPath(base / "configs", 5, 4));
|
||||
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
|
||||
ASSERT_EQ(sub.size(), 2u);
|
||||
EXPECT_EQ(sub.back(), App::ApplicationDirectories::versionStringForPath(5, 4));
|
||||
}
|
||||
|
||||
// Base exists, no current; lower minors exist -> append highest ≤ current in same major
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_PicksHighestLowerMinorInSameMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "vec_lower_minor";
|
||||
std::vector<std::string> sub {"configs"};
|
||||
fs::create_directories(versionedPath(base / "configs", 5, 1));
|
||||
fs::create_directories(versionedPath(base / "configs", 5, 3));
|
||||
// distractor in lower major
|
||||
fs::create_directories(versionedPath(base / "configs", 4, 99));
|
||||
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
|
||||
ASSERT_EQ(sub.size(), 2u);
|
||||
EXPECT_EQ(sub.back(), App::ApplicationDirectories::versionStringForPath(5, 3));
|
||||
}
|
||||
|
||||
// Base exists, nothing in current major; lower major exists -> append highest available lower major
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_FallsBackToLowerMajor)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "vec_lower_major";
|
||||
std::vector<std::string> sub {"configs"};
|
||||
fs::create_directories(versionedPath(base / "configs", 4, 90));
|
||||
fs::create_directories(versionedPath(base / "configs", 3, 99));
|
||||
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
|
||||
ASSERT_EQ(sub.size(), 2u);
|
||||
EXPECT_EQ(sub.back(), App::ApplicationDirectories::versionStringForPath(4, 90));
|
||||
}
|
||||
|
||||
// Base exists but contains no versioned subdirs -> append nothing (vector unchanged)
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_NoVersionedChildren_LeavesVectorUnchanged)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "vec_noversions";
|
||||
std::vector<std::string> sub {"configs"};
|
||||
fs::create_directories(base / "configs"); // but no versioned children
|
||||
|
||||
auto before = sub;
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
EXPECT_EQ(sub, before);
|
||||
}
|
||||
|
||||
// Base does not exist -> append current version string
|
||||
TEST_F(ApplicationDirectoriesTest, AppendVec_BaseMissing_AppendsCurrentSuffix)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
|
||||
fs::path base = tempDir() / "vec_missing";
|
||||
std::vector<std::string> sub {"configs"}; // base/configs doesn't exist
|
||||
|
||||
appDirs->wrapAppendVersionIfPossible(base, sub);
|
||||
|
||||
ASSERT_EQ(sub.size(), 2u);
|
||||
EXPECT_EQ(sub.back(), App::ApplicationDirectories::versionStringForPath(5, 4));
|
||||
}
|
||||
|
||||
/* NOLINTEND(
|
||||
readability-magic-numbers,
|
||||
cppcoreguidelines-avoid-magic-numbers,
|
||||
readability-function-cognitive-complexity
|
||||
) */
|
||||
@@ -1,5 +1,6 @@
|
||||
add_executable(App_tests_run
|
||||
Application.cpp
|
||||
ApplicationDirectories.cpp
|
||||
BackupPolicy.cpp
|
||||
Branding.cpp
|
||||
ComplexGeoData.cpp
|
||||
|
||||
Reference in New Issue
Block a user