yams/tests/unit/cli/time_parser_catch2_test.cpp

360 lines
12 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <yams/cli/time_parser.h>
#include <chrono>
#include <cmath>
#include <ctime>
#include <string>
using namespace yams::cli;
using namespace std::chrono;
namespace {
constexpr auto kTolerance = seconds(5);
static system_clock::duration absDuration(system_clock::duration d) {
return (d < system_clock::duration::zero()) ? -d : d;
}
static std::tm toLocalTm(const system_clock::time_point& tp) {
const std::time_t t = system_clock::to_time_t(tp);
return *std::localtime(&t);
}
static bool contains(const std::string& s, const std::string& needle) {
return s.find(needle) != std::string::npos;
}
} // namespace
TEST_CASE("TimeParser::parse() main entry point", "[cli][time_parser][catch2]") {
SECTION("Empty string returns error") {
const auto res = TimeParser::parse("");
CHECK_FALSE(res.has_value());
CHECK(res.error().code == yams::ErrorCode::InvalidArgument);
CHECK(contains(res.error().message, "Empty time string"));
}
SECTION("Valid relative '7d' succeeds") {
const auto before = system_clock::now();
const auto res = TimeParser::parse("7d");
const auto after = system_clock::now();
REQUIRE(res.has_value());
const auto tp = res.value();
const auto minExpected = before - hours(24 * 7) - kTolerance;
const auto maxExpected = after - hours(24 * 7) + kTolerance;
CHECK(tp >= minExpected);
CHECK(tp <= maxExpected);
}
SECTION("Invalid format returns error with message") {
const auto res = TimeParser::parse("not-a-time-format");
CHECK_FALSE(res.has_value());
CHECK(res.error().code == yams::ErrorCode::InvalidArgument);
CHECK(contains(res.error().message, "Invalid time format"));
}
}
TEST_CASE("TimeParser::parseRelative()", "[cli][time_parser][catch2]") {
SECTION("Hours: '24h' -> ~24 hours ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseRelative("24h");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24) - kTolerance);
CHECK(*tp <= after - hours(24) + kTolerance);
}
SECTION("Days: '7d' -> ~7 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseRelative("7d");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 7) - kTolerance);
CHECK(*tp <= after - hours(24 * 7) + kTolerance);
}
SECTION("Weeks: '2w' -> ~14 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseRelative("2w");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 14) - kTolerance);
CHECK(*tp <= after - hours(24 * 14) + kTolerance);
}
SECTION("Months: '3m' -> ~90 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseRelative("3m");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 90) - kTolerance);
CHECK(*tp <= after - hours(24 * 90) + kTolerance);
}
SECTION("Years: '1y' -> ~365 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseRelative("1y");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 365) - kTolerance);
CHECK(*tp <= after - hours(24 * 365) + kTolerance);
}
SECTION("Case insensitive: '7D' works same as '7d'") {
const auto before = system_clock::now();
const auto tpUpper = TimeParser::parseRelative("7D");
const auto tpLower = TimeParser::parseRelative("7d");
const auto after = system_clock::now();
REQUIRE(tpUpper);
REQUIRE(tpLower);
const auto expectedMin = before - hours(24 * 7) - kTolerance;
const auto expectedMax = after - hours(24 * 7) + kTolerance;
CHECK(*tpUpper >= expectedMin);
CHECK(*tpUpper <= expectedMax);
CHECK(*tpLower >= expectedMin);
CHECK(*tpLower <= expectedMax);
CHECK(absDuration(*tpUpper - *tpLower) < kTolerance);
}
SECTION("Invalid unit: '5x' -> nullopt") {
CHECK_FALSE(TimeParser::parseRelative("5x").has_value());
}
SECTION("Empty string -> nullopt") {
CHECK_FALSE(TimeParser::parseRelative("").has_value());
}
SECTION("Missing number: 'd' -> nullopt") {
CHECK_FALSE(TimeParser::parseRelative("d").has_value());
}
}
TEST_CASE("TimeParser::parseISO8601()", "[cli][time_parser][catch2]") {
SECTION("Date-only '2024-01-01' succeeds and is start of day") {
const auto tp = TimeParser::parseISO8601("2024-01-01");
REQUIRE(tp);
const auto tm = toLocalTm(*tp);
CHECK((tm.tm_year + 1900) == 2024);
CHECK((tm.tm_mon + 1) == 1);
CHECK(tm.tm_mday == 1);
CHECK(tm.tm_hour == 0);
CHECK(tm.tm_min == 0);
CHECK(tm.tm_sec == 0);
}
SECTION("Invalid string returns nullopt") {
CHECK_FALSE(TimeParser::parseISO8601("not-a-date").has_value());
}
SECTION("Full ISO with T format works") {
const auto tp = TimeParser::parseISO8601("2024-01-02T03:04:05Z");
REQUIRE(tp);
const auto tm = toLocalTm(*tp);
CHECK((tm.tm_year + 1900) == 2024);
CHECK((tm.tm_mon + 1) == 1);
CHECK(tm.tm_mday == 2);
CHECK(tm.tm_min == 4);
CHECK(tm.tm_sec == 5);
}
}
TEST_CASE("TimeParser::parseUnixTimestamp()", "[cli][time_parser][catch2]") {
SECTION("10-digit seconds timestamp parsed as seconds") {
const auto tp = TimeParser::parseUnixTimestamp("1704067200");
REQUIRE(tp);
const auto secs = duration_cast<seconds>(tp->time_since_epoch()).count();
CHECK(secs == 1704067200);
}
SECTION("13-digit milliseconds timestamp parsed as milliseconds (same instant)") {
const auto tpSec = TimeParser::parseUnixTimestamp("1704067200");
const auto tpMs = TimeParser::parseUnixTimestamp("1704067200000");
REQUIRE(tpSec);
REQUIRE(tpMs);
CHECK(duration_cast<seconds>(tpSec->time_since_epoch()).count() ==
duration_cast<seconds>(tpMs->time_since_epoch()).count());
CHECK(duration_cast<milliseconds>(tpMs->time_since_epoch()).count() == 1704067200000LL);
}
SECTION("Non-numeric 'abc' returns nullopt") {
CHECK_FALSE(TimeParser::parseUnixTimestamp("abc").has_value());
}
SECTION("Mixed '123abc' returns nullopt") {
CHECK_FALSE(TimeParser::parseUnixTimestamp("123abc").has_value());
}
}
TEST_CASE("TimeParser::parseNatural()", "[cli][time_parser][catch2]") {
SECTION("'now' is close to system_clock::now()") {
const auto tp = TimeParser::parseNatural("now");
REQUIRE(tp);
CHECK(absDuration(system_clock::now() - *tp) < kTolerance);
}
SECTION("'today' is start of current day") {
const auto now = system_clock::now();
const auto expected = TimeParser::startOfDay(now);
const auto tp = TimeParser::parseNatural("today");
REQUIRE(tp);
CHECK(absDuration(*tp - expected) < kTolerance);
const auto tm = toLocalTm(*tp);
CHECK(tm.tm_hour == 0);
CHECK(tm.tm_min == 0);
CHECK(tm.tm_sec == 0);
}
SECTION("'yesterday' is start of day minus 24h") {
const auto now = system_clock::now();
const auto expected = TimeParser::startOfDay(now) - hours(24);
const auto tp = TimeParser::parseNatural("yesterday");
REQUIRE(tp);
CHECK(absDuration(*tp - expected) < kTolerance);
const auto tm = toLocalTm(*tp);
CHECK(tm.tm_hour == 0);
CHECK(tm.tm_min == 0);
CHECK(tm.tm_sec == 0);
}
SECTION("'tomorrow' is start of day plus 24h") {
const auto now = system_clock::now();
const auto expected = TimeParser::startOfDay(now) + hours(24);
const auto tp = TimeParser::parseNatural("tomorrow");
REQUIRE(tp);
CHECK(absDuration(*tp - expected) < kTolerance);
const auto tm = toLocalTm(*tp);
CHECK(tm.tm_hour == 0);
CHECK(tm.tm_min == 0);
CHECK(tm.tm_sec == 0);
}
SECTION("'last-week' is ~7 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseNatural("last-week");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 7) - kTolerance);
CHECK(*tp <= after - hours(24 * 7) + kTolerance);
}
SECTION("'last_month' and 'lastmonth' variants work") {
const auto before = system_clock::now();
const auto tpUnderscore = TimeParser::parseNatural("last_month");
const auto tpNoSep = TimeParser::parseNatural("lastmonth");
const auto after = system_clock::now();
REQUIRE(tpUnderscore);
REQUIRE(tpNoSep);
const auto expectedMin = before - hours(24 * 30) - kTolerance;
const auto expectedMax = after - hours(24 * 30) + kTolerance;
CHECK(*tpUnderscore >= expectedMin);
CHECK(*tpUnderscore <= expectedMax);
CHECK(*tpNoSep >= expectedMin);
CHECK(*tpNoSep <= expectedMax);
}
SECTION("'last-year' is ~365 days ago") {
const auto before = system_clock::now();
const auto tp = TimeParser::parseNatural("last-year");
const auto after = system_clock::now();
REQUIRE(tp);
CHECK(*tp >= before - hours(24 * 365) - kTolerance);
CHECK(*tp <= after - hours(24 * 365) + kTolerance);
}
SECTION("Case insensitive: 'YESTERDAY' works") {
const auto now = system_clock::now();
const auto expected = TimeParser::startOfDay(now) - hours(24);
const auto tp = TimeParser::parseNatural("YESTERDAY");
REQUIRE(tp);
CHECK(absDuration(*tp - expected) < kTolerance);
}
SECTION("Unrecognized 'nextmonth' returns nullopt") {
CHECK_FALSE(TimeParser::parseNatural("nextmonth").has_value());
}
}
TEST_CASE("TimeParser::formatISO8601()", "[cli][time_parser][catch2]") {
SECTION("Known epoch (time_t=0) formats as UTC with Z") {
const auto tp = system_clock::from_time_t(0);
CHECK(TimeParser::formatISO8601(tp) == "1970-01-01T00:00:00Z");
}
SECTION("Specific time formats correctly with Z suffix") {
const auto tp = system_clock::from_time_t(1704067200); // 2024-01-01T00:00:00Z
const auto s = TimeParser::formatISO8601(tp);
CHECK(s == "2024-01-01T00:00:00Z");
CHECK_FALSE(s.empty());
CHECK(s.back() == 'Z');
}
}
TEST_CASE("TimeParser::formatRelative()", "[cli][time_parser][catch2]") {
SECTION("Just now (< 1 second ago) -> 'just now'") {
const auto tp = system_clock::now() - milliseconds(100);
CHECK(TimeParser::formatRelative(tp) == "just now");
}
SECTION("~30 seconds ago -> contains 'second' and 'ago'") {
const auto tp = system_clock::now() - seconds(30);
const auto s = TimeParser::formatRelative(tp);
CHECK(contains(s, "second"));
CHECK(contains(s, "ago"));
}
SECTION("~2 hours ago -> contains 'hour' and 'ago'") {
const auto tp = system_clock::now() - hours(2);
const auto s = TimeParser::formatRelative(tp);
CHECK(contains(s, "hour"));
CHECK(contains(s, "ago"));
}
SECTION("Yesterday (~36 hours ago) -> 'yesterday'") {
const auto tp = system_clock::now() - hours(36);
CHECK(TimeParser::formatRelative(tp) == "yesterday");
}
SECTION("Future time -> contains 'in'") {
const auto tp = system_clock::now() + hours(2);
const auto s = TimeParser::formatRelative(tp);
CHECK(contains(s, "in"));
}
}
TEST_CASE("TimeParser::startOfDay()/endOfDay()", "[cli][time_parser][catch2]") {
SECTION("startOfDay returns time at 00:00:00") {
const auto now = system_clock::now();
const auto tp = TimeParser::startOfDay(now);
const auto tm = toLocalTm(tp);
CHECK(tm.tm_hour == 0);
CHECK(tm.tm_min == 0);
CHECK(tm.tm_sec == 0);
}
SECTION("endOfDay returns time at 23:59:59") {
const auto now = system_clock::now();
const auto tp = TimeParser::endOfDay(now);
const auto tm = toLocalTm(tp);
CHECK(tm.tm_hour == 23);
CHECK(tm.tm_min == 59);
CHECK(tm.tm_sec == 59);
}
}