449 lines
12 KiB
C++
449 lines
12 KiB
C++
|
#include "quill/backend/StringFromTime.h"
|
||
|
#include "DocTestExtensions.h"
|
||
|
#include "doctest/doctest.h"
|
||
|
#include <ctime>
|
||
|
|
||
|
TEST_SUITE_BEGIN("StringFromTime");
|
||
|
|
||
|
using namespace quill;
|
||
|
using namespace quill::detail;
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_format_time")
|
||
|
{
|
||
|
std::string fmt2 = "%H:%M:%S";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_format_I")
|
||
|
{
|
||
|
std::string fmt2 = "%I:%M:%S%p";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_fallback_to_strftime")
|
||
|
{
|
||
|
// In this edge case we pass a timestamp that is back in time from our cached timestamp value.
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// This will create the StringFromTime class and pre-format a string for the timestamp now
|
||
|
std::string fmt2 = "%Y-%m-%dT%H:%M:%SZ";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::GmtTime);
|
||
|
|
||
|
// Ask StringFromTime to format timestamps in the past
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::gmtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Decrement the timestamp for the next loop
|
||
|
raw_ts -= 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_main_format")
|
||
|
{
|
||
|
std::string fmt2 = "%Y-%m-%dT%H:%M:%SZ";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 600'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_gmtime_main_format")
|
||
|
{
|
||
|
std::string fmt2 = "%Y-%m-%dT%H:%M:%SZ";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::GmtTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 600'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::gmtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TEST_CASE("string_from_time_localtime_main_format_increment_ts")
|
||
|
{
|
||
|
std::string fmt2 = "%Y-%m-%dT%H:%M:%SZ";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 10'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
// This test increments the ts by a huge amount trying to mimic e.g. system clock changes
|
||
|
raw_ts += 7200;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_empty_cached_indexes")
|
||
|
{
|
||
|
// try with a format that doesn't have hours, minutes, seconds
|
||
|
std::string fmt2 = "%Y-%m-%d";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), &time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if !defined(_WIN32)
|
||
|
// The following tests don't run on windows because the format identifiers are not supported.
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_format_l")
|
||
|
{
|
||
|
std::string fmt2 = "%l:%M:%S%p";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm* time_info;
|
||
|
time_info = std::localtime(&raw_ts);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_format_k")
|
||
|
{
|
||
|
std::string fmt2 = "%k:%M:%S%p";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 500'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm* time_info;
|
||
|
time_info = std::localtime(&raw_ts);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("string_from_time_localtime_format_s")
|
||
|
{
|
||
|
std::string fmt2 = "%Y-%m-%d %s";
|
||
|
StringFromTime string_from_time;
|
||
|
|
||
|
string_from_time.init(fmt2, Timezone::LocalTime);
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
// Try for a few timestamps
|
||
|
for (uint32_t i = 0; i <= 100'000; ++i)
|
||
|
{
|
||
|
// Get the time from string from time
|
||
|
auto const& time_s1 = string_from_time.format_timestamp(raw_ts);
|
||
|
|
||
|
// Get the time from strftime
|
||
|
std::tm* time_info;
|
||
|
time_info = std::localtime(&raw_ts);
|
||
|
char buffer[256];
|
||
|
std::strftime(buffer, 256, fmt2.data(), time_info);
|
||
|
auto const time_s2 = std::string{buffer};
|
||
|
|
||
|
REQUIRE_STREQ(time_s1.data(), time_s2.data());
|
||
|
|
||
|
// Increment the timestamp for the next loop
|
||
|
raw_ts += 1;
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
class StringFromTimeMock : public quill::detail::StringFromTime
|
||
|
{
|
||
|
public:
|
||
|
static time_t next_noon_or_midnight_timestamp(time_t timestamp, Timezone timezone) noexcept
|
||
|
{
|
||
|
return quill::detail::StringFromTime::_next_noon_or_midnight_timestamp(timestamp, timezone);
|
||
|
}
|
||
|
|
||
|
static time_t nearest_hour_timestamp(time_t timestamp) noexcept
|
||
|
{
|
||
|
return quill::detail::StringFromTime::_nearest_hour_timestamp(timestamp);
|
||
|
}
|
||
|
|
||
|
static time_t next_hour_timestamp(time_t timestamp) noexcept
|
||
|
{
|
||
|
return quill::detail::StringFromTime::_next_hour_timestamp(timestamp);
|
||
|
}
|
||
|
|
||
|
static std::vector<char> safe_strftime(char const* format_string, time_t timestamp, Timezone timezone)
|
||
|
{
|
||
|
return quill::detail::StringFromTime::_safe_strftime(format_string, timestamp, timezone);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("next_noon_or_midnight_timestamp")
|
||
|
{
|
||
|
// We do not test local because if we hardcode an expected_timestamp in our local timezone
|
||
|
// it can be different in another system
|
||
|
|
||
|
{
|
||
|
// Noon utc
|
||
|
time_t constexpr timestamp{1599033200};
|
||
|
time_t constexpr expected_timestamp{1599048000};
|
||
|
time_t const res = StringFromTimeMock::next_noon_or_midnight_timestamp(timestamp, Timezone::GmtTime);
|
||
|
REQUIRE_EQ(res, expected_timestamp);
|
||
|
}
|
||
|
|
||
|
{
|
||
|
// Midnight utc
|
||
|
time_t constexpr timestamp{1599079200};
|
||
|
time_t constexpr expected_timestamp{1599091200};
|
||
|
time_t const res = StringFromTimeMock::next_noon_or_midnight_timestamp(timestamp, Timezone::GmtTime);
|
||
|
REQUIRE_EQ(res, expected_timestamp);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("nearest_hour_timestamp")
|
||
|
{
|
||
|
time_t constexpr timestamp = 1599473669;
|
||
|
time_t constexpr expected_timestamp = 1599472800;
|
||
|
REQUIRE_EQ(StringFromTimeMock::nearest_hour_timestamp(timestamp), expected_timestamp);
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("next_hour_timestamp")
|
||
|
{
|
||
|
time_t constexpr timestamp = 1599473669;
|
||
|
time_t constexpr expected_timestamp = 1599476400;
|
||
|
REQUIRE_EQ(StringFromTimeMock::next_hour_timestamp(timestamp), expected_timestamp);
|
||
|
}
|
||
|
|
||
|
/***/
|
||
|
TEST_CASE("safe_strftime_resize")
|
||
|
{
|
||
|
// e.g. "Monday September 2020 (09/07/20) 15:37 EEST"
|
||
|
constexpr char const* format_string = "%A %B %Y (%x) %R %Z";
|
||
|
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
|
||
|
// we will format a string greater than 32
|
||
|
char expected_result[256];
|
||
|
std::strftime(expected_result, 256, format_string, &time_info);
|
||
|
|
||
|
// Also try our version
|
||
|
std::string const safe_strftime_result =
|
||
|
std::string{StringFromTimeMock::safe_strftime(format_string, raw_ts, Timezone::LocalTime).data()};
|
||
|
|
||
|
REQUIRE_STREQ(expected_result, safe_strftime_result.data());
|
||
|
}
|
||
|
|
||
|
TEST_CASE("safe_strftime_empty")
|
||
|
{
|
||
|
// Get the timestamp now
|
||
|
time_t raw_ts;
|
||
|
std::time(&raw_ts);
|
||
|
|
||
|
std::tm time_info{};
|
||
|
quill::detail::localtime_rs(&raw_ts, &time_info);
|
||
|
|
||
|
// we will format a string greater than 32
|
||
|
char expected_result[256];
|
||
|
std::strftime(expected_result, 256, "", &time_info);
|
||
|
|
||
|
// Also try our version
|
||
|
std::string const safe_strftime_result =
|
||
|
std::string{StringFromTimeMock::safe_strftime("", raw_ts, Timezone::LocalTime).data()};
|
||
|
|
||
|
REQUIRE_STREQ(expected_result, safe_strftime_result.data());
|
||
|
}
|
||
|
|
||
|
TEST_SUITE_END();
|