diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ccd34a..1a1cb6b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -118,3 +118,27 @@ jobs: - name: Run memory tests run: make test-memory + + test-32bit-time-t: + runs-on: ubuntu-latest + name: Test with 32-bit time_t + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || 'Eppo-exp/cpp-sdk' }} + ref: ${{ env.SDK_BRANCH_NAME }} + + - name: Set up QEMU for multi-arch + uses: docker/setup-qemu-action@v3 + + - name: Test with 32-bit time_t + run: | + docker run --rm --platform linux/386 -v $PWD:/workspace -w /workspace debian:bookworm bash -c " + apt-get update && + apt-get install -y build-essential g++ git make && + git clone -b ${{env.TEST_DATA_BRANCH_NAME}} --depth 1 --single-branch https://github.com/Eppo-exp/sdk-test-data.git test/data/ && + make clean && + make test + " diff --git a/src/config_response.cpp b/src/config_response.cpp index 62aadd1..218b5fd 100644 --- a/src/config_response.cpp +++ b/src/config_response.cpp @@ -312,13 +312,12 @@ void to_json(nlohmann::json& j, const Allocation& a) { j = nlohmann::json{{"key", a.key}, {"rules", a.rules}, {"splits", a.splits}}; if (a.startAt.has_value()) { - auto time = std::chrono::system_clock::to_time_t(a.startAt.value()); - j["startAt"] = time; + j["startAt"] = formatISOTimestamp(a.startAt.value()); } if (a.endAt.has_value()) { - auto time = std::chrono::system_clock::to_time_t(a.endAt.value()); - j["endAt"] = time; + // Use special formatting for endAt to handle year 9999 sentinel value + j["endAt"] = formatISOTimestampForConfigEndTime(a.endAt.value()); } if (a.doLog.has_value()) { @@ -344,7 +343,7 @@ void from_json(const nlohmann::json& j, Allocation& a) { if (j.contains("endAt")) { auto timeStr = j.at("endAt").get(); - a.endAt = parseISOTimestamp(timeStr); + a.endAt = parseISOTimestampForConfigEndTime(timeStr); } if (j.contains("doLog")) { diff --git a/src/time_utils.cpp b/src/time_utils.cpp index 5c19ba4..f87e75b 100644 --- a/src/time_utils.cpp +++ b/src/time_utils.cpp @@ -5,7 +5,22 @@ namespace eppoclient { -std::chrono::system_clock::time_point parseISOTimestamp(const std::string& timestamp) { +std::chrono::system_clock::time_point parseISOTimestampForConfigEndTime( + const std::string& timestamp) { + // Check for the sentinel value that represents "no end date" + if (timestamp == "9999-12-31T00:00:00.000Z") { + return std::chrono::system_clock::time_point::max(); + } + return parseISOTimestamp(timestamp, std::chrono::system_clock::time_point::max()); +} + +std::chrono::system_clock::time_point parseISOTimestampForConfigStartTime( + const std::string& timestamp) { + return parseISOTimestamp(timestamp, std::chrono::system_clock::time_point()); +} + +std::chrono::system_clock::time_point parseISOTimestamp( + const std::string& timestamp, std::chrono::system_clock::time_point errorValue) { std::tm tm = {}; char dot = '\0'; int milliseconds = 0; @@ -52,16 +67,33 @@ std::chrono::system_clock::time_point parseISOTimestamp(const std::string& times #endif if (time_c == -1) { - return std::chrono::system_clock::time_point(); + // timegm failed - could be out of range for time_t + // Return the supplied error value + return errorValue; } auto tp = std::chrono::system_clock::from_time_t(time_c); tp += std::chrono::milliseconds(milliseconds); return tp; } +std::string formatISOTimestampForConfigEndTime(const std::chrono::system_clock::time_point& tp) { + // Special handling for max time_point (year 9999 dates) + if (tp == std::chrono::system_clock::time_point::max()) { + return "9999-12-31T00:00:00.000Z"; + } + return formatISOTimestamp(tp); +} + std::string formatISOTimestamp(const std::chrono::system_clock::time_point& tp) { auto tt = std::chrono::system_clock::to_time_t(tp); - std::tm tm = *std::gmtime(&tt); + std::tm* tm_ptr = std::gmtime(&tt); + + // Handle gmtime failure (can happen on 32-bit systems with out-of-range dates) + if (!tm_ptr) { + return "1970-01-01T00:00:00.000Z"; // Return epoch as fallback + } + + std::tm tm = *tm_ptr; // Add milliseconds auto ms = std::chrono::duration_cast(tp.time_since_epoch()) % 1000; diff --git a/src/time_utils.hpp b/src/time_utils.hpp index c069acf..e4e66d5 100644 --- a/src/time_utils.hpp +++ b/src/time_utils.hpp @@ -13,10 +13,34 @@ namespace eppoclient { * Examples: "2024-06-09T14:23:11", "2024-06-09T14:23:11.123" * * @param timestamp The ISO 8601 formatted timestamp string - * @return A time_point representing the parsed timestamp, or a default-constructed - * time_point if parsing fails + * @param errorValue Optional value to return when time_c == -1 (defaults to epoch) + * @return A time_point representing the parsed timestamp, or errorValue if parsing fails */ -std::chrono::system_clock::time_point parseISOTimestamp(const std::string& timestamp); +std::chrono::system_clock::time_point parseISOTimestamp( + const std::string& timestamp, + std::chrono::system_clock::time_point errorValue = std::chrono::system_clock::time_point()); + +/** + * Parse an ISO 8601 timestamp string for configuration endAt field. + * + * Returns time_point::max() on parsing failures, which represents "no end date". + * + * @param timestamp The ISO 8601 formatted timestamp string + * @return A time_point representing the parsed timestamp, or time_point::max() on failure + */ +std::chrono::system_clock::time_point parseISOTimestampForConfigEndTime( + const std::string& timestamp); + +/** + * Parse an ISO 8601 timestamp string for configuration startAt field. + * + * Returns epoch (default time_point) on parsing failures. + * + * @param timestamp The ISO 8601 formatted timestamp string + * @return A time_point representing the parsed timestamp, or epoch on failure + */ +std::chrono::system_clock::time_point parseISOTimestampForConfigStartTime( + const std::string& timestamp); /** * Format a system_clock::time_point into an ISO 8601 timestamp string. @@ -28,6 +52,17 @@ std::chrono::system_clock::time_point parseISOTimestamp(const std::string& times */ std::string formatISOTimestamp(const std::chrono::system_clock::time_point& tp); +/** + * Format a system_clock::time_point for configuration endAt field. + * + * Handles the special case of time_point::max() which represents "no end date" + * and formats it as "9999-12-31T00:00:00.000Z" for configuration consistency. + * + * @param tp The time_point to format + * @return An ISO 8601 formatted timestamp string, or "9999-12-31T00:00:00.000Z" for max + */ +std::string formatISOTimestampForConfigEndTime(const std::chrono::system_clock::time_point& tp); + } // namespace eppoclient #endif // EPPOCLIENT_TIME_UTILS_HPP_ diff --git a/test/test_time_utils.cpp b/test/test_time_utils.cpp index b577d9c..8c93fb2 100644 --- a/test/test_time_utils.cpp +++ b/test/test_time_utils.cpp @@ -180,3 +180,42 @@ TEST_CASE("parseISOTimestamp and formatISOTimestamp - round trip preserves milli std::chrono::duration_cast(parsed.time_since_epoch()) - seconds; REQUIRE(milliseconds.count() == 123); } + +TEST_CASE("parseISOTimestampForConfigEndTime - year 9999 sentinel returns max time_point", + "[time_utils]") { + std::string timestamp = "9999-12-31T00:00:00.000Z"; + auto result = parseISOTimestampForConfigEndTime(timestamp); + + // The exact sentinel value should be treated as "no end date" and return time_point::max() + REQUIRE(result == std::chrono::system_clock::time_point::max()); + + // Verify that current time is always less than max + auto now = std::chrono::system_clock::now(); + REQUIRE(now < result); +} + +TEST_CASE("formatISOTimestampForConfigEndTime - time_point::max() formats to year 9999", + "[time_utils]") { + auto maxTime = std::chrono::system_clock::time_point::max(); + std::string result = formatISOTimestampForConfigEndTime(maxTime); + + // time_point::max() should format to the sentinel year 9999 date + REQUIRE(result == "9999-12-31T00:00:00.000Z"); +} + +TEST_CASE( + "parseISOTimestampForConfigEndTime and formatISOTimestampForConfigEndTime - year 9999 " + "round trip", + "[time_utils]") { + std::string original = "9999-12-31T00:00:00.000Z"; + + // Parse year 9999 sentinel date + auto parsed = parseISOTimestampForConfigEndTime(original); + REQUIRE(parsed == std::chrono::system_clock::time_point::max()); + + // Format back to string + std::string formatted = formatISOTimestampForConfigEndTime(parsed); + + // Should format to the exact same sentinel value + REQUIRE(formatted == "9999-12-31T00:00:00.000Z"); +}