// SPDX-License-Identifier: LGPL-2.1-or-later #include #include #include #include #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 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& subdirs) const { appendVersionIfPossible(basePath, subdirs); } std::tuple wrapExtractVersionFromConfigMap( const std::map& config ) { return extractVersionFromConfigMap(config); } static std::filesystem::path wrapSanitizePath(const std::string& pathAsString) { return sanitizePath(pathAsString); } }; class ApplicationDirectoriesTest: public ::testing::Test { protected: void SetUp() override { _tempDir = MakeUniqueTempDir(); } std::map generateConfig( const std::map& overrides ) const { std::map 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 makeAppDirsForVersion(int major, int minor) { auto configuration = generateConfig( {{"BuildVersionMajor", std::to_string(major)}, {"BuildVersionMinor", std::to_string(minor)}} ); return std::make_unique(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(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(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/` -> newPath == base/ 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 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/ 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 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, migrateAllPathsIgnoresIfDestinationAlreadyExists_NonVersioned) { auto appDirs = makeAppDirsForVersion(5, 4); fs::path base = tempDir() / "exists_case"; fs::create_directories(base); fs::path dest = versionedPath(base, 5, 4); fs::create_directories(dest); // destination already exists std::vector inputs {base}; ASSERT_NO_THROW(appDirs->migrateAllPaths(inputs)); } // 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 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 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 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 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 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 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 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 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 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 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 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 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 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 m {{"BuildVersionMajor", "-2"}, {"BuildVersionMinor", "-7"}}; auto [maj, min] = appDirs->wrapExtractVersionFromConfigMap(m); EXPECT_EQ(maj, -2); EXPECT_EQ(min, -7); } TEST_F(ApplicationDirectoriesTest, sanitizeRemovesNullCharacterAtEnd) { std::string input = std::string("valid_path") + '\0' + "junk_after"; std::filesystem::path result = ApplicationDirectoriesTestClass::wrapSanitizePath(input); EXPECT_EQ(result.string(), "valid_path"); EXPECT_EQ(result.string().find('\0'), std::string::npos); } TEST_F(ApplicationDirectoriesTest, sanitizeReturnsUnchangedIfNoNullCharacter) { std::string input = "clean_path/without_nulls"; std::filesystem::path result = ApplicationDirectoriesTestClass::wrapSanitizePath(input); EXPECT_EQ(result.string(), input); EXPECT_EQ(result.string().find('\0'), std::string::npos); } /* NOLINTEND( readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers, readability-function-cognitive-complexity ) */