Merge pull request #23321 from chennes/refactorApplicationAndAddVersioning
Refactor App::Application directory handling and add versioning
This commit is contained in:
@@ -102,6 +102,7 @@
|
||||
|
||||
#include "Annotation.h"
|
||||
#include "Application.h"
|
||||
#include "ApplicationDirectories.h"
|
||||
#include "CleanupProcess.h"
|
||||
#include "ComplexGeoData.h"
|
||||
#include "Services.h"
|
||||
@@ -185,6 +186,7 @@ Base::ConsoleObserverStd *Application::_pConsoleObserverStd = nullptr;
|
||||
Base::ConsoleObserverFile *Application::_pConsoleObserverFile = nullptr;
|
||||
|
||||
AppExport std::map<std::string, std::string> Application::mConfig;
|
||||
std::unique_ptr<ApplicationDirectories> Application::_appDirs;
|
||||
|
||||
|
||||
//**************************************************************************
|
||||
@@ -1113,7 +1115,7 @@ int64_t Application::applicationPid()
|
||||
|
||||
std::string Application::getHomePath()
|
||||
{
|
||||
return mConfig["AppHomePath"];
|
||||
return Base::FileInfo::pathToString(Application::directories()->getHomePath()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getExecutableName()
|
||||
@@ -1140,78 +1142,53 @@ bool Application::isDevelopmentVersion()
|
||||
return suffix == "dev";
|
||||
}
|
||||
|
||||
const std::unique_ptr<ApplicationDirectories>& Application::directories() {
|
||||
return _appDirs;
|
||||
}
|
||||
|
||||
std::string Application::getTempPath()
|
||||
{
|
||||
return mConfig["AppTempPath"];
|
||||
return Base::FileInfo::pathToString(_appDirs->getTempPath()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getTempFileName(const char* FileName)
|
||||
{
|
||||
return Base::FileInfo::getTempFileName(FileName, getTempPath().c_str());
|
||||
return Base::FileInfo::pathToString(_appDirs->getTempFileName(FileName ? FileName : std::string()));
|
||||
}
|
||||
|
||||
std::string Application::getUserCachePath()
|
||||
{
|
||||
return mConfig["UserCachePath"];
|
||||
return Base::FileInfo::pathToString(_appDirs->getUserCachePath()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getUserConfigPath()
|
||||
{
|
||||
return mConfig["UserConfigPath"];
|
||||
return Base::FileInfo::pathToString(_appDirs->getUserConfigPath()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getUserAppDataDir()
|
||||
{
|
||||
return mConfig["UserAppData"];
|
||||
return Base::FileInfo::pathToString(_appDirs->getUserAppDataDir()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getUserMacroDir()
|
||||
{
|
||||
return mConfig["UserMacroPath"];
|
||||
return Base::FileInfo::pathToString(_appDirs->getUserMacroDir()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getResourceDir()
|
||||
{
|
||||
#ifdef RESOURCEDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
std::string path = std::string(RESOURCEDIR).c_str();
|
||||
path += PATHSEP;
|
||||
const QDir dir(QString::fromStdString(path));
|
||||
if (dir.isAbsolute())
|
||||
return path;
|
||||
return mConfig["AppHomePath"] + path;
|
||||
#else
|
||||
return mConfig["AppHomePath"];
|
||||
#endif
|
||||
return Base::FileInfo::pathToString(_appDirs->getResourceDir()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getLibraryDir()
|
||||
{
|
||||
#ifdef LIBRARYDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
std::string path = std::string(LIBRARYDIR).c_str();
|
||||
const QDir dir(QString::fromStdString(path));
|
||||
if (dir.isAbsolute())
|
||||
return path;
|
||||
return mConfig["AppHomePath"] + path;
|
||||
#else
|
||||
return mConfig["AppHomePath"] + "lib";
|
||||
#endif
|
||||
return Base::FileInfo::pathToString(_appDirs->getLibraryDir()) + PATHSEP;
|
||||
}
|
||||
|
||||
std::string Application::getHelpDir()
|
||||
{
|
||||
#ifdef DOCDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
std::string path = std::string(DOCDIR).c_str();
|
||||
path += PATHSEP;
|
||||
const QDir dir(QString::fromStdString(path));
|
||||
if (dir.isAbsolute())
|
||||
return path;
|
||||
return mConfig["AppHomePath"] + path;
|
||||
#else
|
||||
return mConfig["DocPath"];
|
||||
#endif
|
||||
return Base::FileInfo::pathToString(_appDirs->getHelpDir()) + PATHSEP;
|
||||
}
|
||||
|
||||
int Application::checkLinkDepth(int depth, MessageOption option)
|
||||
@@ -2535,7 +2512,7 @@ void processProgramOptions(const boost::program_options::variables_map& vm, std:
|
||||
void Application::initConfig(int argc, char ** argv)
|
||||
{
|
||||
// find the home path....
|
||||
mConfig["AppHomePath"] = FindHomePath(argv[0]);
|
||||
mConfig["AppHomePath"] = ApplicationDirectories::findHomePath(argv[0]).string();
|
||||
|
||||
// Version of the application extracted from SubWCRef into src/Build/Version.h
|
||||
// We only set these keys if not yet defined. Therefore it suffices to search
|
||||
@@ -2593,7 +2570,7 @@ void Application::initConfig(int argc, char ** argv)
|
||||
}
|
||||
|
||||
// extract home paths
|
||||
ExtractUserPath();
|
||||
_appDirs = std::make_unique<ApplicationDirectories>(mConfig);
|
||||
|
||||
if (vm.contains("safe-mode")) {
|
||||
SafeMode::StartSafeMode();
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace App
|
||||
|
||||
class Document;
|
||||
class DocumentObject;
|
||||
class ApplicationDirectories;
|
||||
class ApplicationObserver;
|
||||
class Property;
|
||||
class AutoTransaction;
|
||||
@@ -423,6 +424,10 @@ public:
|
||||
static std::string getExecutableName();
|
||||
static std::string getNameWithVersion();
|
||||
static bool isDevelopmentVersion();
|
||||
|
||||
/// Access to the various directories for the program a replacement for the get*Path methods below
|
||||
static const std::unique_ptr<ApplicationDirectories>& directories();
|
||||
|
||||
/*!
|
||||
Returns the temporary directory. By default, this is set to the
|
||||
system's temporary directory but can be customized by the user.
|
||||
@@ -626,6 +631,8 @@ private:
|
||||
static void SaveEnv(const char *);
|
||||
/// startup configuration container
|
||||
static std::map<std::string,std::string> mConfig;
|
||||
/// Management of and access to applications directories
|
||||
static std::unique_ptr<ApplicationDirectories> _appDirs;
|
||||
static int _argc;
|
||||
static char ** _argv;
|
||||
//@}
|
||||
|
||||
733
src/App/ApplicationDirectories.cpp
Normal file
733
src/App/ApplicationDirectories.cpp
Normal file
@@ -0,0 +1,733 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/***************************************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
|
||||
* Copyright (c) 2025 The FreeCAD project association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it under the terms of the *
|
||||
* GNU Lesser General Public License as published by the Free Software Foundation, either *
|
||||
* version 2.1 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without *
|
||||
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public License along with FreeCAD. *
|
||||
* If not, see <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
**************************************************************************************************/
|
||||
|
||||
#include "PreCompiled.h"
|
||||
#ifndef _PreComp_
|
||||
#include <fmt/format.h>
|
||||
#include <utility>
|
||||
#include <QDir>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QCoreApplication>
|
||||
#endif
|
||||
|
||||
#include "ApplicationDirectories.h"
|
||||
|
||||
#if defined(FC_OS_LINUX) || defined(FC_OS_MACOSX) || defined(FC_OS_BSD)
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
|
||||
#include <Base/FileInfo.h>
|
||||
#include <Base/Exception.h>
|
||||
|
||||
#include <Python.h>
|
||||
#include <QString>
|
||||
|
||||
|
||||
using namespace App;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path qstringToPath(const QString& path)
|
||||
{
|
||||
#if defined(FC_OS_WIN32)
|
||||
return {path.toStdWString()};
|
||||
#else
|
||||
return {path.toStdString()};
|
||||
#endif
|
||||
}
|
||||
|
||||
ApplicationDirectories::ApplicationDirectories(std::map<std::string,std::string> &config)
|
||||
{
|
||||
_currentVersion = extractVersionFromConfigMap(config);
|
||||
configurePaths(config);
|
||||
configureResourceDirectory(config);
|
||||
configureLibraryDirectory(config);
|
||||
configureHelpDirectory(config);
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getHomePath() const
|
||||
{
|
||||
return this->_home;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getTempPath() const {
|
||||
return this->_temp;
|
||||
}
|
||||
|
||||
fs::path ApplicationDirectories::getTempFileName(const std::string & filename) const {
|
||||
auto tempPath = Base::FileInfo::pathToString(getTempPath());
|
||||
if (filename.empty()) {
|
||||
return Base::FileInfo::getTempFileName(nullptr, tempPath.c_str());
|
||||
}
|
||||
return Base::FileInfo::getTempFileName(filename.c_str(), tempPath.c_str());
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getUserCachePath() const
|
||||
{
|
||||
return this->_userCache;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getUserAppDataDir() const
|
||||
{
|
||||
return this->_userAppData;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getUserMacroDir() const
|
||||
{
|
||||
return this->_userMacro;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getResourceDir() const
|
||||
{
|
||||
return this->_resource;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getHelpDir() const
|
||||
{
|
||||
return this->_help;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getUserConfigPath() const {
|
||||
return this->_userConfig;
|
||||
}
|
||||
|
||||
const fs::path& ApplicationDirectories::getLibraryDir() const {
|
||||
return this->_library;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* \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.
|
||||
*/
|
||||
fs::path ApplicationDirectories::findPath(const fs::path& stdHome, const fs::path& customHome,
|
||||
const std::vector<std::string>& subdirs, bool create) {
|
||||
fs::path appData = customHome;
|
||||
if (appData.empty()) {
|
||||
appData = stdHome;
|
||||
}
|
||||
|
||||
// If a custom user home path is given, then don't modify it
|
||||
if (customHome.empty()) {
|
||||
for (const auto& it : subdirs) {
|
||||
appData = appData / it;
|
||||
}
|
||||
}
|
||||
|
||||
// To write to our data path, we must create some directories, first.
|
||||
if (create && !fs::exists(appData) && !Py_IsInitialized()) {
|
||||
try {
|
||||
fs::create_directories(appData);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
throw Base::FileSystemError("Could not create directories. Failed with: " + e.code().message());
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// std paths
|
||||
_home = fs::path(mConfig.at("AppHomePath"));
|
||||
mConfig["BinPath"] = mConfig.at("AppHomePath") + "bin" + PATHSEP;
|
||||
mConfig["DocPath"] = mConfig.at("AppHomePath") + "doc" + PATHSEP;
|
||||
|
||||
// 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();
|
||||
|
||||
// User home path
|
||||
//
|
||||
fs::path homePath = findUserHomePath(customHome);
|
||||
mConfig["UserHomePath"] = Base::FileInfo::pathToString(homePath);
|
||||
|
||||
// the old path name to save config and data files
|
||||
std::vector<std::string> subdirs;
|
||||
if (keepDeprecatedPaths) {
|
||||
configHome = homePath;
|
||||
dataHome = homePath;
|
||||
cacheHome = homePath;
|
||||
getOldDataLocation(mConfig, subdirs);
|
||||
}
|
||||
else {
|
||||
getSubDirectories(mConfig, subdirs);
|
||||
}
|
||||
|
||||
|
||||
// User data path
|
||||
//
|
||||
auto dataSubdirs = subdirs;
|
||||
appendVersionIfPossible(dataHome, dataSubdirs);
|
||||
fs::path data = findPath(dataHome, customData, dataSubdirs, true);
|
||||
_userAppData = data;
|
||||
mConfig["UserAppData"] = Base::FileInfo::pathToString(data) + PATHSEP;
|
||||
|
||||
|
||||
// User config path
|
||||
//
|
||||
auto configSubdirs = subdirs;
|
||||
appendVersionIfPossible(configHome, configSubdirs);
|
||||
fs::path config = findPath(configHome, customHome, configSubdirs, true);
|
||||
_userConfig = config;
|
||||
mConfig["UserConfigPath"] = Base::FileInfo::pathToString(config) + PATHSEP;
|
||||
|
||||
|
||||
// User cache path
|
||||
//
|
||||
std::vector<std::string> cachedirs = subdirs;
|
||||
cachedirs.emplace_back("Cache");
|
||||
fs::path cache = findPath(cacheHome, customTemp, cachedirs, true);
|
||||
_userCache = cache;
|
||||
mConfig["UserCachePath"] = Base::FileInfo::pathToString(cache) + PATHSEP;
|
||||
|
||||
|
||||
// Set application temporary directory
|
||||
//
|
||||
std::vector<std::string> empty;
|
||||
fs::path tmp = findPath(tempPath, customTemp, empty, true);
|
||||
_temp = tmp;
|
||||
mConfig["AppTempPath"] = Base::FileInfo::pathToString(tmp) + PATHSEP;
|
||||
|
||||
|
||||
// Set the default macro directory
|
||||
//
|
||||
std::vector<std::string> macrodirs{"Macro"};
|
||||
fs::path macro = findPath(_userAppData, customData, macrodirs, true);
|
||||
_userMacro = macro;
|
||||
mConfig["UserMacroPath"] = Base::FileInfo::pathToString(macro) + PATHSEP;
|
||||
}
|
||||
|
||||
void ApplicationDirectories::configureResourceDirectory(const std::map<std::string,std::string>& mConfig) {
|
||||
#ifdef RESOURCEDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
fs::path path {std::string(RESOURCEDIR).c_str()};
|
||||
if (path.is_absolute()) {
|
||||
_resource = path;
|
||||
} else {
|
||||
_resource = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path;
|
||||
}
|
||||
#else
|
||||
_resource = fs::path(mConfig.at("AppHomePath"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void ApplicationDirectories::configureLibraryDirectory(const std::map<std::string,std::string>& mConfig) {
|
||||
#ifdef LIBRARYDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
fs::path path {std::string(LIBRARYDIR).c_str()};
|
||||
if (path.is_absolute()) {
|
||||
_library = path;
|
||||
} else {
|
||||
_library = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path;
|
||||
}
|
||||
#else
|
||||
_library = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / "lib";
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void ApplicationDirectories::configureHelpDirectory(const std::map<std::string,std::string>& mConfig)
|
||||
{
|
||||
#ifdef DOCDIR
|
||||
// #6892: Conda may inject null characters => remove them using c_str()
|
||||
fs::path path {std::string(DOCDIR).c_str()};
|
||||
if (path.is_absolute()) {
|
||||
_help = path;
|
||||
} else {
|
||||
_help = Base::FileInfo::stringToPath(mConfig.at("AppHomePath")) / path;
|
||||
}
|
||||
#else
|
||||
_help = Base::FileInfo::stringToPath(mConfig.at("DocPath"));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
fs::path ApplicationDirectories::getUserHome()
|
||||
{
|
||||
fs::path path;
|
||||
#if defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD) || defined(FC_OS_MACOSX)
|
||||
// Default paths for the user-specific stuff
|
||||
struct passwd pwd {};
|
||||
struct passwd *result {};
|
||||
constexpr std::size_t bufferLength = 16384;
|
||||
std::vector<char> buffer(bufferLength);
|
||||
const int error = getpwuid_r(getuid(), &pwd, buffer.data(), buffer.size(), &result);
|
||||
if (!result || error != 0) {
|
||||
throw Base::RuntimeError("Getting HOME path from system failed!");
|
||||
}
|
||||
path = Base::FileInfo::stringToPath(result->pw_dir);
|
||||
#else
|
||||
path = Base::FileInfo::stringToPath(QStandardPaths::writableLocation(QStandardPaths::HomeLocation).toStdString());
|
||||
#endif
|
||||
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"
|
||||
QString ApplicationDirectories::getOldGenericDataLocation()
|
||||
{
|
||||
WCHAR szPath[MAX_PATH];
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
|
||||
if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, szPath))) {
|
||||
return QString::fromStdString(converter.to_bytes(szPath));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
#endif
|
||||
|
||||
void ApplicationDirectories::getSubDirectories(const std::map<std::string,std::string>& mConfig,
|
||||
std::vector<std::string>& appData)
|
||||
{
|
||||
// If 'AppDataSkipVendor' is defined, the value of 'ExeVendor' must not be part of
|
||||
// the path.
|
||||
if (!mConfig.contains("AppDataSkipVendor") && mConfig.contains("ExeVendor")) {
|
||||
appData.push_back(mConfig.at("ExeVendor"));
|
||||
}
|
||||
appData.push_back(mConfig.at("ExeName"));
|
||||
}
|
||||
|
||||
void ApplicationDirectories::getOldDataLocation(const std::map<std::string,std::string>& mConfig,
|
||||
std::vector<std::string>& appData)
|
||||
{
|
||||
// The name of the directory where the parameters are stored should be the name of
|
||||
// the application (for branding reasons).
|
||||
#if defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD)
|
||||
// If 'AppDataSkipVendor' is defined, the value of 'ExeVendor' must not be part of
|
||||
// the path.
|
||||
if (!mConfig.contains("AppDataSkipVendor")) {
|
||||
appData.push_back(std::string(".") + mConfig.at("ExeVendor"));
|
||||
appData.push_back(mConfig.at("ExeName"));
|
||||
} else {
|
||||
appData.push_back(std::string(".") + mConfig.at("ExeName"));
|
||||
}
|
||||
|
||||
#elif defined(FC_OS_MACOSX) || defined(FC_OS_WIN32)
|
||||
getSubDirectories(mConfig, appData);
|
||||
#endif
|
||||
}
|
||||
|
||||
fs::path ApplicationDirectories::findUserHomePath(const fs::path& userHome)
|
||||
{
|
||||
return userHome.empty() ? getUserHome() : userHome;
|
||||
}
|
||||
|
||||
std::tuple<fs::path, fs::path, fs::path> ApplicationDirectories::getCustomPaths()
|
||||
{
|
||||
const QProcessEnvironment env(QProcessEnvironment::systemEnvironment());
|
||||
QString userHome = env.value(QStringLiteral("FREECAD_USER_HOME"));
|
||||
QString userData = env.value(QStringLiteral("FREECAD_USER_DATA"));
|
||||
QString userTemp = env.value(QStringLiteral("FREECAD_USER_TEMP"));
|
||||
|
||||
auto toNativePath = [](QString& path) {
|
||||
if (!path.isEmpty()) {
|
||||
if (const QDir dir(path); dir.exists()) {
|
||||
path = QDir::toNativeSeparators(dir.canonicalPath());
|
||||
}
|
||||
else {
|
||||
path.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// verify env. variables
|
||||
toNativePath(userHome);
|
||||
toNativePath(userData);
|
||||
toNativePath(userTemp);
|
||||
|
||||
// if FREECAD_USER_HOME is set but not FREECAD_USER_DATA
|
||||
if (!userHome.isEmpty() && userData.isEmpty()) {
|
||||
userData = userHome;
|
||||
}
|
||||
|
||||
// if FREECAD_USER_HOME is set but not FREECAD_USER_TEMP
|
||||
if (!userHome.isEmpty() && userTemp.isEmpty()) {
|
||||
const QDir dir(userHome);
|
||||
dir.mkdir(QStringLiteral("temp"));
|
||||
const QFileInfo fi(dir, QStringLiteral("temp"));
|
||||
userTemp = fi.absoluteFilePath();
|
||||
}
|
||||
|
||||
return {qstringToPath(userHome),
|
||||
qstringToPath(userData),
|
||||
qstringToPath(userTemp)};
|
||||
}
|
||||
|
||||
std::tuple<fs::path, fs::path, fs::path, fs::path> ApplicationDirectories::getStandardPaths()
|
||||
{
|
||||
QString configHome = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
|
||||
QString dataHome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
|
||||
QString cacheHome = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
|
||||
QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
|
||||
|
||||
// Keep the old behaviour
|
||||
#if defined(FC_OS_WIN32)
|
||||
configHome = getOldGenericDataLocation();
|
||||
dataHome = configHome;
|
||||
|
||||
// On systems with non-7-bit-ASCII application data directories,
|
||||
// GetTempPathW will return a path in DOS format. This path will be
|
||||
// accepted by boost's file_lock class.
|
||||
// Since boost 1.76, there is now a version that accepts a wide string.
|
||||
#if (BOOST_VERSION < 107600)
|
||||
tempPath = QString::fromStdString(Base::FileInfo::getTempPath());
|
||||
cacheHome = tempPath;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return {qstringToPath(configHome),
|
||||
qstringToPath(dataHome),
|
||||
qstringToPath(cacheHome),
|
||||
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 fmt::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 = Base::FileInfo::pathToString(config.filename());
|
||||
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: " + Base::FileInfo::pathToString(newPath));
|
||||
}
|
||||
fs::create_directories(newPath);
|
||||
migrateConfig(path, newPath);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider using this for all UNIX-like OSes
|
||||
#if defined(__OpenBSD__)
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <sys/param.h>
|
||||
#include <QCoreApplication>
|
||||
|
||||
fs::path ApplicationDirectories::findHomePath(const char* sCall)
|
||||
{
|
||||
// We have three ways to start this application either use one of the two executables or
|
||||
// import the FreeCAD.so module from a running Python session. In the latter case the
|
||||
// Python interpreter is already initialized.
|
||||
std::string absPath;
|
||||
std::string homePath;
|
||||
if (Py_IsInitialized()) {
|
||||
// Note: `realpath` is known to cause a buffer overflow because it
|
||||
// expands the given path to an absolute path of unknown length.
|
||||
// Even setting PATH_MAX does not necessarily solve the problem
|
||||
// for sure, but the risk of overflow is rather small.
|
||||
char resolved[PATH_MAX];
|
||||
char* path = realpath(sCall, resolved);
|
||||
if (path)
|
||||
absPath = path;
|
||||
}
|
||||
else {
|
||||
int argc = 1;
|
||||
QCoreApplication app(argc, (char**)(&sCall));
|
||||
absPath = QCoreApplication::applicationFilePath().toStdString();
|
||||
}
|
||||
|
||||
// should be an absolute path now
|
||||
std::string::size_type pos = absPath.find_last_of("/");
|
||||
homePath.assign(absPath,0,pos);
|
||||
pos = homePath.find_last_of("/");
|
||||
homePath.assign(homePath,0,pos+1);
|
||||
|
||||
return Base::FileInfo::stringToPath(homePath);
|
||||
}
|
||||
|
||||
#elif defined (FC_OS_LINUX) || defined(FC_OS_CYGWIN) || defined(FC_OS_BSD)
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <sys/param.h>
|
||||
|
||||
fs::path ApplicationDirectories::findHomePath(const char* sCall)
|
||||
{
|
||||
// We have three ways to start this application either use one of the two executables or
|
||||
// import the FreeCAD.so module from a running Python session. In the latter case the
|
||||
// Python interpreter is already initialized.
|
||||
std::string absPath;
|
||||
std::string homePath;
|
||||
if (Py_IsInitialized()) {
|
||||
// Note: `realpath` is known to cause a buffer overflow because it
|
||||
// expands the given path to an absolute path of unknown length.
|
||||
// Even setting PATH_MAX does not necessarily solve the problem
|
||||
// for sure, but the risk of overflow is rather small.
|
||||
char resolved[PATH_MAX];
|
||||
char* path = realpath(sCall, resolved);
|
||||
if (path)
|
||||
absPath = path;
|
||||
}
|
||||
else {
|
||||
// Find the path of the executable. Theoretically, there could occur a
|
||||
// race condition when using readlink, but we only use this method to
|
||||
// get the absolute path of the executable to compute the actual home
|
||||
// path. In the worst case we simply get q wrong path, and FreeCAD is not
|
||||
// able to load its modules.
|
||||
char resolved[PATH_MAX];
|
||||
#if defined(FC_OS_BSD)
|
||||
int mib[4];
|
||||
mib[0] = CTL_KERN;
|
||||
mib[1] = KERN_PROC;
|
||||
mib[2] = KERN_PROC_PATHNAME;
|
||||
mib[3] = -1;
|
||||
size_t cb = sizeof(resolved);
|
||||
sysctl(mib, 4, resolved, &cb, NULL, 0);
|
||||
int nchars = strlen(resolved);
|
||||
#else
|
||||
int nchars = readlink("/proc/self/exe", resolved, PATH_MAX);
|
||||
#endif
|
||||
if (nchars < 0 || nchars >= PATH_MAX)
|
||||
throw Base::FileSystemError("Cannot determine the absolute path of the executable");
|
||||
resolved[nchars] = '\0'; // enforce null termination
|
||||
absPath = resolved;
|
||||
}
|
||||
|
||||
// should be an absolute path now
|
||||
std::string::size_type pos = absPath.find_last_of("/");
|
||||
homePath.assign(absPath,0,pos);
|
||||
pos = homePath.find_last_of("/");
|
||||
homePath.assign(homePath,0,pos+1);
|
||||
|
||||
return Base::FileInfo::stringToPath(homePath);
|
||||
}
|
||||
|
||||
#elif defined(FC_OS_MACOSX)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
#include <sys/param.h>
|
||||
|
||||
fs::path ApplicationDirectories::findHomePath(const char* sCall)
|
||||
{
|
||||
// If Python is initialized at this point, then we're being run from
|
||||
// MainPy.cpp, which hopefully rewrote argv[0] to point at the
|
||||
// FreeCAD shared library.
|
||||
if (!Py_IsInitialized()) {
|
||||
uint32_t sz = 0;
|
||||
|
||||
// function only returns "sz" if the first arg is too small to hold value
|
||||
_NSGetExecutablePath(nullptr, &sz);
|
||||
|
||||
if (const auto buf = new char[++sz]; _NSGetExecutablePath(buf, &sz) == 0) {
|
||||
std::array<char, PATH_MAX> resolved{};
|
||||
const char* path = realpath(buf, resolved.data());
|
||||
delete [] buf;
|
||||
|
||||
if (path) {
|
||||
const std::string Call(resolved.data());
|
||||
std::string TempHomePath;
|
||||
std::string::size_type pos = Call.find_last_of(fs::path::preferred_separator);
|
||||
TempHomePath.assign(Call,0,pos);
|
||||
pos = TempHomePath.find_last_of(fs::path::preferred_separator);
|
||||
TempHomePath.assign(TempHomePath,0,pos+1);
|
||||
return Base::FileInfo::stringToPath(TempHomePath);
|
||||
}
|
||||
} else {
|
||||
delete [] buf;
|
||||
}
|
||||
}
|
||||
|
||||
return Base::FileInfo::stringToPath(sCall);
|
||||
}
|
||||
|
||||
#elif defined (FC_OS_WIN32)
|
||||
fs::path ApplicationDirectories::findHomePath(const char* sCall)
|
||||
{
|
||||
// We have several ways to start this application:
|
||||
// * use one of the two executables
|
||||
// * import the FreeCAD.pyd module from a running Python session. In this case the
|
||||
// Python interpreter is already initialized.
|
||||
// * use a custom dll that links FreeCAD core dlls and that is loaded by its host application
|
||||
// In this case the calling name should be set to FreeCADBase.dll or FreeCADApp.dll in order
|
||||
// to locate the correct home directory
|
||||
wchar_t szFileName [MAX_PATH];
|
||||
QString dll(QString::fromUtf8(sCall));
|
||||
if (Py_IsInitialized() || dll.endsWith(QLatin1String(".dll"))) {
|
||||
GetModuleFileNameW(GetModuleHandleA(sCall),szFileName, MAX_PATH-1);
|
||||
}
|
||||
else {
|
||||
GetModuleFileNameW(0, szFileName, MAX_PATH-1);
|
||||
}
|
||||
|
||||
std::wstring Call(szFileName), homePath;
|
||||
std::wstring::size_type pos = Call.find_last_of(fs::path::preferred_separator);
|
||||
homePath.assign(Call,0,pos);
|
||||
pos = homePath.find_last_of(fs::path::preferred_separator);
|
||||
homePath.assign(homePath,0,pos+1);
|
||||
|
||||
// fixes #0001638 to avoid loading DLLs from Windows' system directories before FreeCAD's bin folder
|
||||
std::wstring binPath = homePath;
|
||||
binPath += L"bin";
|
||||
SetDllDirectoryW(binPath.c_str());
|
||||
|
||||
// https://stackoverflow.com/questions/5625884/conversion-of-stdwstring-to-qstring-throws-linker-error
|
||||
#ifdef _MSC_VER
|
||||
QString str = QString::fromUtf16(reinterpret_cast<const ushort *>(homePath.c_str()));
|
||||
#else
|
||||
QString str = QString::fromStdWString(homePath);
|
||||
#endif
|
||||
return qstringToPath(str);
|
||||
}
|
||||
|
||||
#else
|
||||
# error "std::string ApplicationDirectories::findHomePath(const char*) not implemented"
|
||||
#endif
|
||||
|
||||
std::tuple<int, int> ApplicationDirectories::extractVersionFromConfigMap(const std::map<std::string,std::string> &config)
|
||||
{
|
||||
try {
|
||||
int major = std::stoi(config.at("BuildVersionMajor"));
|
||||
int minor = std::stoi(config.at("BuildVersionMinor"));
|
||||
return std::make_tuple(major, minor);
|
||||
} catch (const std::exception& e) {
|
||||
throw Base::RuntimeError("Failed to parse version from config: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
240
src/App/ApplicationDirectories.h
Normal file
240
src/App/ApplicationDirectories.h
Normal file
@@ -0,0 +1,240 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
/***************************************************************************************************
|
||||
* *
|
||||
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
|
||||
* Copyright (c) 2025 The FreeCAD project association AISBL *
|
||||
* *
|
||||
* This file is part of FreeCAD. *
|
||||
* *
|
||||
* FreeCAD is free software: you can redistribute it and/or modify it under the terms of the *
|
||||
* GNU Lesser General Public License as published by the Free Software Foundation, either *
|
||||
* version 2.1 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* FreeCAD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without *
|
||||
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Lesser General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Lesser General Public License along with FreeCAD. *
|
||||
* If not, see <https://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
**************************************************************************************************/
|
||||
|
||||
#ifndef SRC_APP_APPLICATIONDIRECTORIES_H_
|
||||
#define SRC_APP_APPLICATIONDIRECTORIES_H_
|
||||
|
||||
#include "FCGlobal.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace App {
|
||||
|
||||
/// A helper class to handle application-wide directory management on behalf of the main
|
||||
/// 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 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. 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
|
||||
/// exposed here primarily for historical compatibility reasons, and new code should almost
|
||||
/// certainly NOT use this path for anything. See alternatives in the
|
||||
/// `App::ApplicationDirectories` class.
|
||||
const std::filesystem::path& getHomePath() const;
|
||||
|
||||
/// Temp path is the location of all temporary files: it is not guaranteed to preserve
|
||||
/// information between runs of the program, but *is* guaranteed to exist for the duration
|
||||
/// of a single program execution (that is, files are not deleted from it *during* the run).
|
||||
const std::filesystem::path& getTempPath() const;
|
||||
|
||||
/// Get a file in the temp directory. WARNING: NOT THREAD-SAFE! Currently just forwards to
|
||||
/// the FileInfo class. TODO: Rewrite to be thread safe
|
||||
std::filesystem::path getTempFileName(const std::string & filename = "") const;
|
||||
|
||||
/// The user cache path can be used to store files that the program *prefers* not be deleted
|
||||
/// between runs, but that will be recreated or otherwise handled if they do not exist due
|
||||
/// to the cache being cleared. There is no guarantee that the files will exist from
|
||||
/// run-to-run, but an effort is made to preserve them (unlike the temp directory, which
|
||||
/// should never be used to save data between runs).
|
||||
const std::filesystem::path& getUserCachePath() const;
|
||||
|
||||
/// The primary directory used to store per-user application data. This is the parent
|
||||
/// directory of all installed addons, per-user configuration files, etc. Developers looking
|
||||
/// for a place to put per-user data should begin here. Common subdirectories include "Mod",
|
||||
/// "Macros", "Materials" and many others that don't begin with the letter "M". This is
|
||||
/// typically a versioned directory, though users may choose to use a single path for
|
||||
/// multiple versions of the software.
|
||||
const std::filesystem::path& getUserAppDataDir() const;
|
||||
|
||||
/// Historically, a single directory was used to store user-created (or user-installed)
|
||||
/// macro files. This is the path to that directory. Note that going forward it should *not*
|
||||
/// be assumed that all installed macros are located in this directory. This is typically a
|
||||
/// versioned directory, though users may choose to use a single path for multiple versions
|
||||
/// of the software.
|
||||
const std::filesystem::path& getUserMacroDir() const;
|
||||
|
||||
/// The "resource" directory should be used to store non-ephemeral resources such as icons,
|
||||
/// templates, hardware setup, etc. -- items that should be preserved from run-to-run of the
|
||||
/// program, and between versions. This is *not* a versioned directory, and multiple
|
||||
/// versions of the software may access the same data.
|
||||
const std::filesystem::path& getResourceDir() const;
|
||||
|
||||
/// Nominally, this is the directory where "help" files are stored, though for historical
|
||||
/// reasons several other informational files are stored here as well. It should only be
|
||||
/// used for user-facing informational files.
|
||||
const std::filesystem::path& getHelpDir() const;
|
||||
|
||||
/// The root path of user config files `user.cfg` and `system.cfg`.
|
||||
const std::filesystem::path& getUserConfigPath() const;
|
||||
|
||||
/// The directory of all extension modules. Added to `sys.path` during Python
|
||||
/// initialization.
|
||||
const std::filesystem::path& getLibraryDir() const;
|
||||
|
||||
/// 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();
|
||||
#endif
|
||||
/// Adds subdirectory information to the appData vector for use in constructing full paths to config files, etc.
|
||||
static void getSubDirectories(const std::map<std::string,std::string>& mConfig,
|
||||
std::vector<std::string>& appData);
|
||||
/// To a given path it adds the subdirectories where to store application-specific files.
|
||||
/// On Linux or BSD a hidden directory (i.e. starting with a dot) is added.
|
||||
static void getOldDataLocation(const std::map<std::string,std::string>& mConfig,
|
||||
std::vector<std::string>& appData);
|
||||
/// If the passed path name is not empty, it will be returned, otherwise the user home path of the system will
|
||||
/// be returned.
|
||||
static std::filesystem::path findUserHomePath(const std::filesystem::path& userHome);
|
||||
|
||||
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,
|
||||
const std::vector<std::string>& paths,
|
||||
bool create);
|
||||
|
||||
void configurePaths(std::map<std::string,std::string> &config);
|
||||
void configureResourceDirectory(const std::map<std::string,std::string>& mConfig);
|
||||
void configureLibraryDirectory(const std::map<std::string,std::string>& mConfig);
|
||||
void configureHelpDirectory(const std::map<std::string,std::string>& mConfig);
|
||||
|
||||
/*!
|
||||
* Returns a tuple of path names where to store config, data, and temp. files.
|
||||
* The method therefore reads the environment variables:
|
||||
* \list
|
||||
* \li FREECAD_USER_HOME
|
||||
* \li FREECAD_USER_DATA
|
||||
* \li FREECAD_USER_TEMP
|
||||
* \endlist
|
||||
*/
|
||||
static std::tuple<std::filesystem::path, std::filesystem::path, std::filesystem::path> getCustomPaths();
|
||||
|
||||
/*!
|
||||
* Returns a tuple of XDG-compliant standard paths names where to store config, data and cached files.
|
||||
* The method therefore reads the environment variables:
|
||||
* \list
|
||||
* \li XDG_CONFIG_HOME
|
||||
* \li XDG_DATA_HOME
|
||||
* \li XDG_CACHE_HOME
|
||||
* \endlist
|
||||
*/
|
||||
std::tuple<std::filesystem::path, std::filesystem::path, std::filesystem::path, std::filesystem::path> getStandardPaths();
|
||||
|
||||
/// Find the BuildVersionMajor, BuildVersionMinor pair in the config map, convert them to an int tuple, and
|
||||
/// return it. If the pair is not found, or cannot be converted to integers, a RuntimeError is raised.
|
||||
/// \param config The config map to search.
|
||||
/// \return The version tuple.
|
||||
static std::tuple<int, int> extractVersionFromConfigMap(const std::map<std::string,std::string> &config);
|
||||
|
||||
private:
|
||||
std::tuple<int, int> _currentVersion;
|
||||
std::filesystem::path _home;
|
||||
std::filesystem::path _temp;
|
||||
std::filesystem::path _userCache;
|
||||
std::filesystem::path _userConfig;
|
||||
std::filesystem::path _userAppData;
|
||||
std::filesystem::path _userMacro;
|
||||
std::filesystem::path _resource;
|
||||
std::filesystem::path _library;
|
||||
std::filesystem::path _help;
|
||||
|
||||
bool _usingCustomDirectories {false};
|
||||
};
|
||||
|
||||
} // App
|
||||
|
||||
#endif //SRC_APP_APPLICATIONDIRECTORIES_H_
|
||||
@@ -274,6 +274,7 @@ SET(FreeCADApp_CPP_SRCS
|
||||
${Document_CPP_SRCS}
|
||||
${Properties_CPP_SRCS}
|
||||
Application.cpp
|
||||
ApplicationDirectories.cpp
|
||||
ApplicationPy.cpp
|
||||
AutoTransaction.cpp
|
||||
Branding.cpp
|
||||
@@ -304,6 +305,7 @@ SET(FreeCADApp_HPP_SRCS
|
||||
${Document_HPP_SRCS}
|
||||
${Properties_HPP_SRCS}
|
||||
Application.h
|
||||
ApplicationDirectories.h
|
||||
AutoTransaction.h
|
||||
Branding.h
|
||||
CleanupProcess.h
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
// STL
|
||||
#include <bitset>
|
||||
#include <chrono>
|
||||
#if defined(FC_OS_WIN32)
|
||||
#include <codecvt>
|
||||
#endif
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
@@ -101,6 +104,12 @@
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
// Qt -- only QtCore
|
||||
#include <QDir>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QString>
|
||||
|
||||
#endif //_PreComp_
|
||||
|
||||
#endif // APP_PRECOMPILED_H
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
#include <sstream>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <ranges>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -24,14 +24,21 @@
|
||||
#include "PreCompiled.h"
|
||||
#ifndef _PreComp_
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QImageReader>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QOpenGLContext>
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QProcess>
|
||||
#include <QStatusBar>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
#include <Inventor/SoDB.h>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <ranges>
|
||||
#endif
|
||||
|
||||
#include "StartupProcess.h"
|
||||
@@ -43,6 +50,7 @@
|
||||
#include "MainWindow.h"
|
||||
#include "Language/Translator.h"
|
||||
#include <App/Application.h>
|
||||
#include <App/ApplicationDirectories.h>
|
||||
#include <Base/Console.h>
|
||||
|
||||
|
||||
@@ -220,6 +228,7 @@ void StartupPostProcess::execute()
|
||||
showMainWindow();
|
||||
activateWorkbench();
|
||||
checkParameters();
|
||||
runWelcomeScreen();
|
||||
}
|
||||
|
||||
void StartupPostProcess::setWindowTitle()
|
||||
@@ -544,3 +553,146 @@ void StartupPostProcess::checkParameters()
|
||||
"Continue with an empty configuration that won't be saved.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void StartupPostProcess::runWelcomeScreen()
|
||||
{
|
||||
// If the user is running a custom directory set, there is no migration to versioned directories
|
||||
if (App::Application::directories()->usingCustomDirectories()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto prefGroup = App::GetApplication().GetParameterGroupByPath(
|
||||
"User parameter:BaseApp/Preferences/Migration");
|
||||
|
||||
// Split our comma-separated list of already-migrated-to version directories into a set for easy
|
||||
// searching
|
||||
auto splitCommas = [](const std::string &input) {
|
||||
std::set<std::string> result;
|
||||
std::stringstream ss(input);
|
||||
std::string token;
|
||||
|
||||
while (std::getline(ss, token, ',')) {
|
||||
result.insert(token);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
std::string offeredToMigrateToVersionedConfig =
|
||||
prefGroup->GetASCII("OfferedToMigrateToVersionedConfig", "");
|
||||
std::set<std::string> knownVersions;
|
||||
if (!offeredToMigrateToVersionedConfig.empty()) {
|
||||
knownVersions = splitCommas(offeredToMigrateToVersionedConfig);
|
||||
}
|
||||
|
||||
auto joinCommas = [](const std::set<std::string>& s) {
|
||||
std::ostringstream oss;
|
||||
for (auto it = s.begin(); it != s.end(); ++it) {
|
||||
if (it != s.begin()) {
|
||||
oss << ',';
|
||||
}
|
||||
oss << *it;
|
||||
}
|
||||
return oss.str();
|
||||
};
|
||||
|
||||
int major = std::stoi(App::Application::Config()["BuildVersionMajor"]);
|
||||
int minor = std::stoi(App::Application::Config()["BuildVersionMinor"]);
|
||||
std::string currentVersionedDirName = App::ApplicationDirectories::versionStringForPath(major, minor);
|
||||
if (!knownVersions.contains(currentVersionedDirName)
|
||||
&& !App::Application::directories()->usingCurrentVersionConfig(
|
||||
App::Application::directories()->getUserAppDataDir())) {
|
||||
auto programName = QString::fromStdString(App::Application::getExecutableName());
|
||||
auto result = QMessageBox::question(
|
||||
mainWindow,
|
||||
QObject::tr("Welcome to %1 v%2.%3").arg(programName, QString::number(major), QString::number(minor)),
|
||||
QObject::tr("Welcome to %1 v%2.%3\n\n").arg(programName, QString::number(major), QString::number(minor))
|
||||
+ QObject::tr("Configuration data and addons from previous program version found. "
|
||||
"Migrate the old configuration to this version?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
knownVersions.insert(currentVersionedDirName);
|
||||
prefGroup->SetASCII("OfferedToMigrateToVersionedConfig", joinCommas(knownVersions));
|
||||
if (result == QMessageBox::Yes) {
|
||||
migrateToCurrentVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PathMigrationWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
void run () {
|
||||
try {
|
||||
App::GetApplication().GetUserParameter().SaveDocument();
|
||||
App::Application::directories()->migrateAllPaths(
|
||||
{App::Application::getUserAppDataDir(), App::Application::getUserConfigPath()});
|
||||
Q_EMIT(complete());
|
||||
} catch (const Base::Exception& e) {
|
||||
Base::Console().error("Error migrating configuration data: %s\n", e.what());
|
||||
Q_EMIT(failed());
|
||||
} catch (const std::exception& e) {
|
||||
Base::Console().error("Unrecognized error migrating configuration data: %s\n", e.what());
|
||||
Q_EMIT(failed());
|
||||
} catch (...) {
|
||||
Base::Console().error("Error migrating configuration data\n");
|
||||
Q_EMIT(failed());
|
||||
}
|
||||
}
|
||||
|
||||
Q_SIGNALS:
|
||||
void complete();
|
||||
void failed();
|
||||
};
|
||||
|
||||
void StartupPostProcess::migrateToCurrentVersion()
|
||||
{
|
||||
auto *workerThread = new QThread(mainWindow);
|
||||
auto *worker = new PathMigrationWorker();
|
||||
worker->moveToThread(workerThread);
|
||||
QObject::connect(workerThread, &QThread::started, worker, &PathMigrationWorker::run);
|
||||
|
||||
auto migrationRunning = new QMessageBox(mainWindow);
|
||||
migrationRunning->setWindowTitle(QObject::tr("Migrating"));
|
||||
migrationRunning->setText(QObject::tr("Migrating configuration data and addons..."));
|
||||
migrationRunning->setStandardButtons(QMessageBox::NoButton);
|
||||
QObject::connect(worker, &PathMigrationWorker::complete, migrationRunning, &QMessageBox::accept);
|
||||
QObject::connect(worker, &PathMigrationWorker::failed, migrationRunning, &QMessageBox::reject);
|
||||
|
||||
workerThread->start();
|
||||
migrationRunning->exec();
|
||||
|
||||
if (migrationRunning->result() == QDialog::Accepted) {
|
||||
auto* restarting = new QMessageBox(mainWindow);
|
||||
restarting->setText(
|
||||
QObject::tr("Migration complete. Restarting..."));
|
||||
restarting->setWindowTitle(QObject::tr("Restarting"));
|
||||
restarting->setStandardButtons(QMessageBox::NoButton);
|
||||
auto closeNotice = [restarting]() {
|
||||
restarting->reject();
|
||||
};
|
||||
|
||||
// Insert a short delay before restart so the user can see the success message, and
|
||||
// knows it's a restart and not a crash...
|
||||
const int delayRestartMillis {2000};
|
||||
QTimer::singleShot(delayRestartMillis, closeNotice);
|
||||
restarting->exec();
|
||||
|
||||
QObject::connect(qApp, &QCoreApplication::aboutToQuit, [=] {
|
||||
if (getMainWindow()->close()) {
|
||||
auto args = QApplication::arguments();
|
||||
args.removeFirst();
|
||||
QProcess::startDetached(QApplication::applicationFilePath(),
|
||||
args,
|
||||
QApplication::applicationDirPath());
|
||||
}
|
||||
});
|
||||
QCoreApplication::exit(0);
|
||||
_exit(0); // No really. Die.
|
||||
} else {
|
||||
QMessageBox::critical(mainWindow, QObject::tr("Migration failed"),QObject::tr("Migration failed. See the Report View for details."));
|
||||
}
|
||||
}
|
||||
|
||||
#include "StartupProcess.moc"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <QStringList>
|
||||
|
||||
class QApplication;
|
||||
class QMessageBox;
|
||||
|
||||
namespace Gui {
|
||||
|
||||
@@ -75,6 +76,8 @@ private:
|
||||
void showMainWindow();
|
||||
void activateWorkbench();
|
||||
void checkParameters();
|
||||
void runWelcomeScreen();
|
||||
void migrateToCurrentVersion();
|
||||
|
||||
private:
|
||||
bool loadFromPythonModule = false;
|
||||
|
||||
752
tests/src/App/ApplicationDirectories.cpp
Normal file
752
tests/src/App/ApplicationDirectories.cpp
Normal file
@@ -0,0 +1,752 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
std::tuple<int, int>
|
||||
wrapExtractVersionFromConfigMap(const std::map<std::string, std::string>& config)
|
||||
{
|
||||
return extractVersionFromConfigMap(config);
|
||||
}
|
||||
};
|
||||
|
||||
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::unique_ptr<ApplicationDirectoriesTestClass> makeAppDirsForVersion(int major, int minor)
|
||||
{
|
||||
auto configuration = generateConfig({{"BuildVersionMajor", std::to_string(major)},
|
||||
{"BuildVersionMinor", std::to_string(minor)}});
|
||||
return std::make_unique<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_unique<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, appendVecAlreadyVersionedBails)
|
||||
{
|
||||
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, appendVecBaseExistsAppendsCurrentWhenPresent)
|
||||
{
|
||||
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, appendVecPicksHighestLowerMinorInSameMajor)
|
||||
{
|
||||
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, appendVecFallsBackToLowerMajor)
|
||||
{
|
||||
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, appendVecNoVersionedChildrenLeavesVectorUnchanged)
|
||||
{
|
||||
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, appendVecBaseMissingAppendsCurrentSuffix)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
// Happy path: exact integers
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionSucceedsWithPlainIntegers)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", "7"}, {"BuildVersionMinor", "2"}};
|
||||
auto [maj, min] = appDirs->wrapExtractVersionFromConfigMap(m);
|
||||
EXPECT_EQ(maj, 7);
|
||||
EXPECT_EQ(min, 2);
|
||||
}
|
||||
|
||||
// Whitespace tolerated by std::stoi
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionSucceedsWithWhitespace)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", " 10 "},
|
||||
{"BuildVersionMinor", "\t3\n"}};
|
||||
auto [maj, min] = appDirs->wrapExtractVersionFromConfigMap(m);
|
||||
EXPECT_EQ(maj, 10);
|
||||
EXPECT_EQ(min, 3);
|
||||
}
|
||||
|
||||
// Missing major key -> rethrows as Base::RuntimeError
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionMissingMajorThrowsRuntimeError)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMinor", "1"}};
|
||||
EXPECT_THROW(appDirs->wrapExtractVersionFromConfigMap(m), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Missing minor key -> rethrows as Base::RuntimeError
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionMissingMinorThrowsRuntimeError)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", "1"}};
|
||||
EXPECT_THROW(appDirs->wrapExtractVersionFromConfigMap(m), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Non-numeric -> std::stoi throws invalid_argument, rethrown as Base::RuntimeError
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionNonNumericThrowsRuntimeError)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", "abc"}, {"BuildVersionMinor", "2"}};
|
||||
EXPECT_THROW(appDirs->wrapExtractVersionFromConfigMap(m), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Overflow -> std::stoi throws out_of_range, rethrown as Base::RuntimeError
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionOverflowThrowsRuntimeError)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", "9999999999999999999999999"},
|
||||
{"BuildVersionMinor", "1"}};
|
||||
EXPECT_THROW(appDirs->wrapExtractVersionFromConfigMap(m), Base::RuntimeError);
|
||||
}
|
||||
|
||||
// Document current behavior: negative numbers are accepted and returned as-is
|
||||
TEST_F(ApplicationDirectoriesTest, extractVersionNegativeNumbersPassThrough)
|
||||
{
|
||||
auto appDirs = makeAppDirsForVersion(5, 4);
|
||||
std::map<std::string, std::string> m {{"BuildVersionMajor", "-2"}, {"BuildVersionMinor", "-7"}};
|
||||
auto [maj, min] = appDirs->wrapExtractVersionFromConfigMap(m);
|
||||
EXPECT_EQ(maj, -2);
|
||||
EXPECT_EQ(min, -7);
|
||||
}
|
||||
|
||||
/* 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