From 766a0eb8351af8f29dfa5f28c106c5638921847c Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sat, 19 Jul 2025 22:11:19 -0500 Subject: [PATCH] Tests: Add tests for timestamp-based backups --- .../CompilerChecksAndSetups.cmake | 2 + tests/src/App/BackupPolicy.cpp | 327 +++++++++++++++++- 2 files changed, 328 insertions(+), 1 deletion(-) diff --git a/cMake/FreeCAD_Helpers/CompilerChecksAndSetups.cmake b/cMake/FreeCAD_Helpers/CompilerChecksAndSetups.cmake index 89e239a9b2..932e9f553a 100644 --- a/cMake/FreeCAD_Helpers/CompilerChecksAndSetups.cmake +++ b/cMake/FreeCAD_Helpers/CompilerChecksAndSetups.cmake @@ -46,6 +46,8 @@ macro(CompilerChecksAndSetups) else() set(CMAKE_CXX_STANDARD 20) endif() + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) # Log the compiler and version message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}, version: ${CMAKE_CXX_COMPILER_VERSION}") diff --git a/tests/src/App/BackupPolicy.cpp b/tests/src/App/BackupPolicy.cpp index 266934c001..38f3627d87 100644 --- a/tests/src/App/BackupPolicy.cpp +++ b/tests/src/App/BackupPolicy.cpp @@ -29,10 +29,22 @@ #include #include +#include #include #include +#include #include +#if defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907L && defined(_LIBCPP_VERSION) \ + && _LIBCPP_VERSION >= 13000 +// Apple's clang compiler did not support timezones fully until a quite recent version: +// before removing this preprocessor check, verify that it compiles on our oldest-supported +// macOS version. +#define CAN_USE_CHRONO_AND_FORMAT +#include +#include +#endif + class BackupPolicyTest: public ::testing::Test { @@ -79,7 +91,7 @@ protected: } -private: +protected: std::string randomString(size_t length) { static constexpr std::string_view chars = "0123456789" @@ -100,6 +112,39 @@ private: return result; } + std::string filenameFromDateFormatString(const std::string& fmt) + { +#if CAN_USE_CHRONO_AND_FORMAT + std::chrono::zoned_time local_time {std::chrono::current_zone(), + std::chrono::system_clock::now()}; + std::string fmt_str = "{:" + fmt + "}"; + std::string result = std::vformat(fmt_str, std::make_format_args(local_time)); +#else + std::time_t now = std::time(nullptr); + std::tm local_tm {}; +#if defined(_WIN32) + localtime_s(&local_tm, &now); // Windows +#else + localtime_r(&now, &local_tm); // POSIX +#endif + constexpr size_t bufferLength = 128; + std::array buffer {}; + size_t bytes = std::strftime(buffer.data(), bufferLength, fmt.c_str(), &local_tm); + if (bytes == 0) { + throw std::runtime_error("failed to format time"); + } + std::string result {buffer.data()}; +#endif + + return result; + } + + std::filesystem::path getTempPath() + { + return _tempDir; + } + +private: App::BackupPolicy _policy; std::filesystem::path _tempDir; }; @@ -225,3 +270,283 @@ TEST_F(BackupPolicyTest, StandardWithFCBakSet) // Assert EXPECT_TRUE(std::filesystem::exists(target.string() + "1")); // No FCBak extension for Standard } + +TEST_F(BackupPolicyTest, TimestampSourceDoesNotExist) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, false, "%Y-%m-%d_%H-%M-%S"); + + // Act & Assert + EXPECT_THROW(apply("nonexistent.fcstd", "backup.fcstd"), Base::FileException); +} + +TEST_F(BackupPolicyTest, TimestampNoSourceGiven) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, false, "%Y-%m-%d_%H-%M-%S"); + auto target = createTempFile("target.fcstd"); + + // Act & Assert + EXPECT_THROW(apply("nonexistent.fcstd", target.string()), Base::FileException); +} + +TEST_F(BackupPolicyTest, TimestampNoTargetGiven) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, false, "%Y-%m-%d_%H-%M-%S"); + auto source = createTempFile("source.fcstd"); + + // Act & Assert + EXPECT_THROW(apply(source.string(), ""), Base::FileException); +} + +TEST_F(BackupPolicyTest, TimestampWithZeroFilesDeletesExisting) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 0, false, "%Y-%m-%d_%H-%M-%S"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert + GTEST_SKIP(); // Can't test on a real filesystem, too much caching for reliable results + EXPECT_FALSE(std::filesystem::exists(target)); +} + +TEST_F(BackupPolicyTest, TimestampWithOneFileAndNoneExistingNotFCBakCreatesNumberedFile) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, false, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert + // Without the .FCBak extension, the date stuff is completely ignored, even if the policy is set + // to "Timestamp" + EXPECT_TRUE(std::filesystem::exists(target.string() + "1")); +} + +TEST_F(BackupPolicyTest, TimestampSourceHasNoExtension) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d"); + auto source = createTempFile("source"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert + auto expected = "target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"; + EXPECT_TRUE(std::filesystem::exists(getTempPath() / expected)); +} + +TEST_F(BackupPolicyTest, TimestampTargetHasNoExtension) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target"); + + // Act + apply(source.string(), target.string()); + + // Assert + auto expected = "target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"; + EXPECT_TRUE(std::filesystem::exists(getTempPath() / expected)); +} + +TEST_F(BackupPolicyTest, TimestampWithOneFileAndNoneExistingFCBakCreatesDatedFile) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert + auto expected = "target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"; + EXPECT_TRUE(std::filesystem::exists(getTempPath() / expected)); +} + +TEST_F(BackupPolicyTest, TimestampReplacesDotsWithDashes) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y.%m.%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert + auto expected = "target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"; + EXPECT_TRUE(std::filesystem::exists(getTempPath() / expected)); +} + +TEST_F(BackupPolicyTest, DISABLED_TimestampWithInvalidFormatStringThrows) +{ + // THIS TEST IS DISABLED BECAUSE THE CURRENT CODE DOES NOT CORRECTLY HANDLE INVALID FORMAT + // OPERATIONS, AND CRASHES WHEN GIVEN ONE. FIXME. + + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Q-%W-%E"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act and Assert + EXPECT_THROW(apply(source.string(), target.string()), Base::FileException); +} + +TEST_F(BackupPolicyTest, DISABLED_TimestampWithAbsurdlyLongFormatStringThrows) +{ + // THIS TEST IS DISABLED BECAUSE THE CURRENT CODE DOES NOT CORRECTLY HANDLE OVER-LENGTH FORMAT + // OPERATIONS, AND GENERATES AN INVALID FILENAME. FIXME. + + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, + 1, + true, + "%A, %B %d, %Y at %H:%M:%S %Z (Day %j of the year, Week %U/%W) — This is a " + "verbose date string for demonstration purposes."); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act and Assert + EXPECT_THROW(apply(source.string(), target.string()), Base::FileException); +} + +TEST_F(BackupPolicyTest, TimestampDetectsOldBackupFormat) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + auto backup = createTempFile("target.fcstd12345"); + + // Act + apply(source.string(), target.string()); + + // Assert + bool check1 = std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak")); + bool check2 = std::filesystem::exists(backup); + EXPECT_NE(check1, check2); +} + +TEST_F(BackupPolicyTest, TimestampDetectsOldBackupFormatIgnoresOther) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + auto backup = createTempFile("target.fcstd12345"); + auto weird = createTempFile("target.fcstd12345abc"); + + // Act + apply(source.string(), target.string()); + + // Assert + bool check1 = std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak")); + bool check2 = std::filesystem::exists(backup); + EXPECT_NE(check1, check2); + EXPECT_TRUE(std::filesystem::exists(weird)); +} + +TEST_F(BackupPolicyTest, TimestampDetectsAndRetainsOldBackupWhenAllowed) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 2, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + auto backup = createTempFile("target.fcstd12345"); + + // Act + apply(source.string(), target.string()); + + // Assert + EXPECT_TRUE(std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"))); + EXPECT_TRUE(std::filesystem::exists(backup)); +} + +TEST_F(BackupPolicyTest, TimestampFormatStringEndsWithSpace) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d "); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert (the space is stripped, and an index is added) + EXPECT_TRUE(std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + "1.FCBak"))); +} + +TEST_F(BackupPolicyTest, TimestampFormatStringEndsWithDash) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 1, true, "%Y-%m-%d-"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + + // Act + apply(source.string(), target.string()); + + // Assert (the dash is left, and an index is added) + EXPECT_TRUE(std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + "-1.FCBak"))); +} + +TEST_F(BackupPolicyTest, TimestampFormatFileAlreadyExists) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 2, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + auto backup = createTempFile("target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"); + + // Act + apply(source.string(), target.string()); + + // Assert (An index is appended) + EXPECT_TRUE(std::filesystem::exists(backup)); + EXPECT_TRUE(std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + "-1.FCBak"))); +} + +TEST_F(BackupPolicyTest, TimestampFormatFileAlreadyExistsMultipleTimes) +{ + // Arrange + setPolicyTerms(App::BackupPolicy::Policy::TimeStamp, 5, true, "%Y-%m-%d"); + auto source = createTempFile("source.fcstd"); + auto target = createTempFile("target.fcstd"); + auto backup = createTempFile("target." + filenameFromDateFormatString("%Y-%m-%d") + ".FCBak"); + auto backup1 = + createTempFile("target." + filenameFromDateFormatString("%Y-%m-%d") + "-1.FCBak"); + auto backup2 = + createTempFile("target." + filenameFromDateFormatString("%Y-%m-%d") + "-2.FCBak"); + auto backup3 = + createTempFile("target." + filenameFromDateFormatString("%Y-%m-%d") + "-3.FCBak"); + + // Act + apply(source.string(), target.string()); + + // Assert (An index is appended) + EXPECT_TRUE(std::filesystem::exists(backup)); + EXPECT_TRUE(std::filesystem::exists(backup1)); + EXPECT_TRUE(std::filesystem::exists(backup2)); + EXPECT_TRUE(std::filesystem::exists(backup3)); + EXPECT_TRUE(std::filesystem::exists( + getTempPath() / ("target." + filenameFromDateFormatString("%Y-%m-%d") + "-4.FCBak"))); +}