diff --git a/Doxyfile b/Doxyfile index 419b742a3ebf4acbc6929bfea78c59b610269b4d..713a0c961b63058f89cd68f9fbb437732586a0fa 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1534,7 +1534,7 @@ FORMULA_TRANSPARENT = YES # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. -USE_MATHJAX = NO +USE_MATHJAX = YES # When MathJax is enabled you can set the default output format to be used for # the MathJax output. See the MathJax site (see: diff --git a/inc/ErrorHandler.hpp b/inc/ErrorHandler.hpp index e9b341bf3c9ef3871855236202cd8bee64912e0f..55a37bd038e2b746c0d67eb6cf915d9659bc77f5 100644 --- a/inc/ErrorHandler.hpp +++ b/inc/ErrorHandler.hpp @@ -93,6 +93,10 @@ public: * Invalid TimeStamp parameters at creation */ InvalidTimeStampInput = 15, + /** + * Timestamp out of bounds to be stored or converted + */ + TimeStampOutOfBounds = 16, }; /** diff --git a/inc/Platform/x86/ECSS_Configuration.hpp b/inc/Platform/x86/ECSS_Configuration.hpp index 1871be15d8593871f00866fb9b167262d55948f9..c6963bad21e628bdb4a038fe4ecfc2cd8e022fb0 100644 --- a/inc/Platform/x86/ECSS_Configuration.hpp +++ b/inc/Platform/x86/ECSS_Configuration.hpp @@ -10,7 +10,7 @@ */ /** - * @defgroup ServiceDefinitions Service compilation switches + * @name ServiceDefinitions Service compilation switches * These preprocessor defines control whether the compilation of each ECSS service is enabled. By not defining one of * those, the service will not be compiled, and no RAM or ROM will be spent storing it. * diff --git a/inc/Time/Time.hpp b/inc/Time/Time.hpp index b7c803339ad5bcebb1c566bb948a7f56abd84470..3e2665819e902c98b8f060dfe67879792630a2ab 100644 --- a/inc/Time/Time.hpp +++ b/inc/Time/Time.hpp @@ -1,6 +1,7 @@ #ifndef ECSS_TIMEHPP #define ECSS_TIMEHPP +#include <chrono> #include <cstdint> #include "ErrorHandler.hpp" #include "etl/String.hpp" @@ -79,15 +80,15 @@ namespace Time { /** * Number of bytes used for the basic time units of the CUC header for this mission */ - inline constexpr uint8_t CUCSecondsBytes = 2; + inline constexpr uint8_t CUCSecondsBytes = 4; /** * Number of bytes used for the fractional time units of the CUC header for this mission */ - inline constexpr uint8_t CUCFractionalBytes = 2; + inline constexpr uint8_t CUCFractionalBytes = 1; /** - * The system epoch (clock measurement starting time) + * The system epoch (clock measurement starting time). * All timestamps emitted by the ECSS services will show the elapsed time (seconds, days etc.) from this epoch. */ inline constexpr struct { @@ -238,7 +239,6 @@ namespace Time { */ template <typename T, int secondsBytes, int fractionalBytes> inline constexpr T buildCUCHeader() { - // TODO: Gitlab issue #106 static_assert((secondsBytes + fractionalBytes) <= 8, "Complete arbitrary precision not supported"); // cppcheck-suppress syntaxError // cppcheck-suppress redundantCondition @@ -315,6 +315,25 @@ namespace Time { return time1.elapsed100msTicks >= time2.elapsed100msTicks; } + /** + * is_duration definition to check if a variable is std::chrono::duration + */ + template <typename T> + struct is_duration + : std::false_type {}; + + /** + * is_duration definition to check if a variable is std::chrono::duration + */ + template <typename Rep, typename Period> + struct is_duration<std::chrono::duration<Rep, Period>> + : std::true_type {}; + + /** + * True if T is std::chrono::duration, false if not + */ + template <class T> + inline constexpr bool is_duration_v = is_duration<T>::value; } // namespace Time #endif diff --git a/inc/Time/TimeStamp.hpp b/inc/Time/TimeStamp.hpp index 52fade25ad8dd9ff8c8fd078bf6f940e6d8b3bc7..52880509fc3fe11a09bf34ac3c0748929d8ed719 100644 --- a/inc/Time/TimeStamp.hpp +++ b/inc/Time/TimeStamp.hpp @@ -1,37 +1,96 @@ #ifndef ECSS_SERVICES_TIME_HPP #define ECSS_SERVICES_TIME_HPP -#include <cstdint> #include <algorithm> -#include "macros.hpp" +#include <chrono> +#include <cstdint> #include <etl/array.h> #include "Time.hpp" #include "UTCTimestamp.hpp" +#include "macros.hpp" /** * A class that represents an instant in time, with convenient conversion * to and from usual time and date representations * + * This class is compatible with the CUC (Unsegmented Time Code) format defined in CCSDS 301.0-B-4. It allows specifying: + * - Different amount of bytes for the basic time unit + * - Different amount of bytes for the fractional time unit + * - Different basic time units + * + * The timestamp is defined in relation to a user-defined epoch, set in @ref Time::Epoch. + * + * @section baseunit Setting the base time unit + * By default, this class measures time in the order of **seconds**. Binary fractions of a second can be specified by increasing the FractionBytes. + * However, the user can change the base time unit by setting the @p Num and @p Denom template parameters. + * + * The base time unit (or period) is then represented by the following: + * \f[ + * \text{time unit} = \frac{Num}{Denom} \cdot \text{second} + * \f] + * * @note * This class uses internally TAI time, and handles UTC leap seconds at conversion to and * from UTC time system. * + * @tparam BaseBytes The number of bytes used for the basic time units. This essentially defines the maximum duration from Epoch that this timestamp can represent. + * @tparam FractionBytes The number of bytes used for the fraction of one basic time unit. This essentially defines the precision of the timestamp. + * @tparam Num The numerator of the base type ratio (see @ref baseunit) + * @tparam Denom The numerator of the base type ratio (see @ref baseunit) + * * @ingroup Time * @author Baptiste Fournier + * @author Konstantinos Kanavouras * @see [CCSDS 301.0-B-4](https://public.ccsds.org/Pubs/301x0b4e1.pdf) */ -template <uint8_t secondsBytes, uint8_t fractionalBytes> +template <uint8_t BaseBytes, uint8_t FractionBytes = 0, int Num = 1, int Denom = 1> class TimeStamp { +public: + /** + * The period of the base type, in relation to the second + * + * This type represents the base type of the timestamp. + * + * A ratio of `<1, 1>` (or 1/1) means that this timestamp represents seconds. A ratio of `<60, 1>` (or 60/1) means + * that this class represents 60s of seconds, or minutes. A ratio of `<1, 1000>` (or 1/1000) means that this class + * represents 1000ths of seconds, or milliseconds. + * + * This type has essentially the same meaning of `Rep` in [std::chrono::duration](https://en.cppreference.com/w/cpp/chrono/duration). + * + * @note std::ratio will simplify the fractions numerator and denominator + */ + using Ratio = std::ratio<Num, Denom>; + private: - static_assert(secondsBytes + fractionalBytes <= 8, + static_assert(BaseBytes + FractionBytes <= 8, "Currently, this class is not suitable for storage on internal counter larger than uint64_t"); - typedef typename std::conditional<(secondsBytes < 4 && fractionalBytes < 3), uint8_t, uint16_t>::type CUCHeader_t; - typedef typename std::conditional<(secondsBytes + fractionalBytes < 4), uint32_t, uint64_t>::type TAICounter_t; + using CUCHeader_t = typename std::conditional<(BaseBytes < 4 && FractionBytes < 3), uint8_t, uint16_t>::type; + using TAICounter_t = typename std::conditional<(BaseBytes + FractionBytes <= 4), uint32_t, uint64_t>::type; + + /** + * The period of the internal counter + * + * Same as @ref Ratio, but instead of representing the Base bytes, it represents the entire value held by @ref taiCounter. + */ + using RawRatio = std::ratio<Num, Denom * 1UL << (8 * FractionBytes)>; + + /** + * An std::chrono::duration representation of the base type (without the fractional part) + */ + using BaseDuration = std::chrono::duration<TAICounter_t, Ratio>; + + /** + * An std::chrono::duration representation of the complete @ref taiCounter (including the fractional part) + */ + using RawDuration = std::chrono::duration<TAICounter_t, RawRatio>; + + template <uint8_t, uint8_t, int, int> + friend class TimeStamp; /** * Integer counter of time units since the @ref Time::Epoch. This number essentially represents the timestamp. * - * The unit represented by this variable depends on `secondsBytes` and `fractionalBytes`. The fractional + * The unit represented by this variable depends on `BaseBytes` and `FractionBytes`. The fractional * part is included as the least significant bits of this variable, and the base part follows. */ TAICounter_t taiCounter; @@ -39,13 +98,17 @@ private: /** * The constant header ("P-value") of the timestamp, if needed to be attached to any message */ - static constexpr CUCHeader_t CUCHeader = Time::buildCUCHeader<CUCHeader_t, secondsBytes, fractionalBytes>(); + static constexpr CUCHeader_t CUCHeader = Time::buildCUCHeader<CUCHeader_t, BaseBytes, FractionBytes>(); + + /** + * The maximum value of the base type (seconds, larger or smaller) that can fit in @ref taiCounter + */ + static constexpr uint64_t MaxBase = (BaseBytes == 8) ? std::numeric_limits<uint64_t>::max() : (1UL << 8 * BaseBytes) - 1; /** - * The maximum value that can fit in @ref taiCounter, or the maximum number of seconds since epoch that can be - * represented in this base class + * The maximum number of seconds since epoch that can be represented in this class */ - static constexpr uint64_t maxSecondCounterValue = (uint64_t{1U} << (8U * secondsBytes)) - 1; + static constexpr uint64_t MaxSeconds = std::chrono::duration_cast<std::chrono::duration<uint64_t>>(BaseDuration(MaxBase)).count(); /** * Returns whether the amount of `seconds` can be represented by this TimeStamp. @@ -79,8 +142,7 @@ public: /** * Initialize the TimeStamp from the bytes of a CUC time stamp * - * @param timestamp A complete CUC timestamp including header, of the maximum possible size, zero padded to the - * right + * @param timestamp A complete CUC timestamp including header, of the maximum possible size, zero padded to the right */ explicit TimeStamp(etl::array<uint8_t, Time::CUCTimestampMaximumSize> timestamp); @@ -91,6 +153,26 @@ public: */ explicit TimeStamp(const UTCTimestamp& timestamp); + /** + * Convert a TimeStamp to a TimeStamp with different parameters + * + * This constructor will convert based on the number of bytes, and base units + * + * @note Internally uses double-precision floating point to allow for arbitrary ratios + */ + template <uint8_t BaseBytesIn, uint8_t FractionBytesIn, int NumIn = 1, int DenomIn = 1> + explicit TimeStamp(TimeStamp<BaseBytesIn, FractionBytesIn, NumIn, DenomIn>); + + /** + * Convert an [std::chrono::duration](https://en.cppreference.com/w/cpp/chrono/duration) representing seconds from @ref Time::Epoch + * to a timestamp + * + * @warning This function does not perform overflow calculations. It is up to the user to ensure that the types are + * compatible so that no overflow occurs. + */ + template <class Duration, typename = std::enable_if_t<Time::is_duration_v<Duration>>> + explicit TimeStamp(Duration duration); + /** * Get the representation as seconds from epoch in TAI * @@ -110,19 +192,26 @@ public: * Get the representation as seconds from epoch in TAI, for a floating-point representation. * For an integer result, see the overloaded @ref asTAIseconds function. * - * @todo Implement integer seconds in this function * @tparam T The return type of the seconds (float or double). * @return The seconds elapsed in TAI since @ref Time::Epoch */ template <typename T> T asTAIseconds(); + /** + * Converts a TimeStamp to a duration of seconds since the @ref Time::Epoch. + * + * @warning This function does not perform overflow calculations. It is up to the user to ensure that the types are compatible so that no overflow occurs. + */ + template <class Duration = std::chrono::seconds> + Duration asDuration(); + /** * Get the representation as CUC formatted bytes * * @return The TimeStamp, represented in the CCSDS CUC format */ - etl::array<uint8_t, Time::CUCTimestampMaximumSize> toCUCtimestamp(); + etl::array<uint8_t, Time::CUCTimestampMaximumSize> formatAsCUC(); /** * Get the representation as a UTC timestamp @@ -132,37 +221,43 @@ public: UTCTimestamp toUTCtimestamp(); /** - * Compare two timestamps. - * - * @param timestamp the date that will be compared with the pointer `this` - * @return true if the condition is satisfied + * @name Comparison operators between timestamps + * @{ */ - bool operator<(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter < timestamp.taiCounter; + template <class OtherTimestamp> + bool operator<(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) < typename OtherTimestamp::RawDuration(timestamp.taiCounter); } - bool operator>(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter > timestamp.taiCounter; + template <class OtherTimestamp> + bool operator>(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) > typename OtherTimestamp::RawDuration(timestamp.taiCounter); } - bool operator==(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter == timestamp.taiCounter; + template <class OtherTimestamp> + bool operator==(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) == typename OtherTimestamp::RawDuration(timestamp.taiCounter); } - bool operator!=(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter != timestamp.taiCounter; + template <class OtherTimestamp> + bool operator!=(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) != typename OtherTimestamp::RawDuration(timestamp.taiCounter); } - bool operator<=(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter <= timestamp.taiCounter; + template <class OtherTimestamp> + bool operator<=(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) <= typename OtherTimestamp::RawDuration(timestamp.taiCounter); } - bool operator>=(const TimeStamp<secondsBytes, fractionalBytes>& timestamp) const { - return taiCounter >= timestamp.taiCounter; + template <class OtherTimestamp> + bool operator>=(const OtherTimestamp& timestamp) const { + return RawDuration(taiCounter) >= typename OtherTimestamp::RawDuration(timestamp.taiCounter); } + /** + * @} + */ }; #include "TimeStamp.tpp" -typedef TimeStamp<Time::CUCSecondsBytes, Time::CUCFractionalBytes> AcubeSATTimeStamp_t; #endif diff --git a/inc/Time/TimeStamp.tpp b/inc/Time/TimeStamp.tpp index 34edf5a2c291153b1a38778ebc0462ec661d9fd6..e4b33b4f40fe30f1fd82269ffedf4974386869f2 100644 --- a/inc/Time/TimeStamp.tpp +++ b/inc/Time/TimeStamp.tpp @@ -1,46 +1,48 @@ -template <uint8_t secondsBytes, uint8_t fractionalBytes> -constexpr bool TimeStamp<secondsBytes, fractionalBytes>::areSecondsValid(TimeStamp::TAICounter_t seconds) { - return seconds < maxSecondCounterValue; +#include <cmath> +#include "TimeStamp.hpp" + +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +constexpr bool TimeStamp<BaseBytes, FractionBytes, Num, Denom>::areSecondsValid(TimeStamp::TAICounter_t seconds) { + return seconds <= MaxSeconds; } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -TimeStamp<secondsBytes, fractionalBytes>::TimeStamp(uint64_t taiSecondsFromEpoch) { - ASSERT_INTERNAL(areSecondsValid((taiSecondsFromEpoch)), ErrorHandler::InternalErrorType::InvalidTimeStampInput); +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(uint64_t taiSecondsFromEpoch) { + ASSERT_INTERNAL(areSecondsValid((taiSecondsFromEpoch)), ErrorHandler::InternalErrorType::TimeStampOutOfBounds); + + using FromDuration = std::chrono::duration<uint64_t>; + const auto duration = FromDuration(taiSecondsFromEpoch); - taiCounter = static_cast<TAICounter_t>(taiSecondsFromEpoch) << 8 * fractionalBytes; + taiCounter = std::chrono::duration_cast<RawDuration>(duration).count(); } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -TimeStamp<secondsBytes, fractionalBytes>::TimeStamp(Time::CustomCUC_t customCUCTimestamp) { - ASSERT_INTERNAL(areSecondsValid((customCUCTimestamp.elapsed100msTicks / 10)), - ErrorHandler::InternalErrorType::InvalidTimeStampInput); - taiCounter = static_cast<TAICounter_t>(customCUCTimestamp.elapsed100msTicks / 10); - if (fractionalBytes > 0) { - TAICounter_t fractionalPart = static_cast<TAICounter_t>(customCUCTimestamp.elapsed100msTicks) - 10 * taiCounter; - taiCounter = taiCounter << 8; - taiCounter += fractionalPart * 256 / 10; - taiCounter = taiCounter << 8 * (fractionalBytes - 1); - } +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(Time::CustomCUC_t customCUCTimestamp) { + //TODO Remove CustomCUC_t class + TimeStamp<8, 0, 1, 10> input; + input.taiCounter = customCUCTimestamp.elapsed100msTicks; + + new (this) TimeStamp<BaseBytes, FractionBytes, Num, Denom>(input); } -template <uint8_t secondsCounter, uint8_t fractionalBytes> -TimeStamp<secondsCounter, fractionalBytes>::TimeStamp(etl::array<uint8_t, Time::CUCTimestampMaximumSize> timestamp) { +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(etl::array<uint8_t, Time::CUCTimestampMaximumSize> timestamp) { // process header uint8_t headerSize = 1; if ((timestamp[0] & 0b10000000U) != 0) { headerSize = 2; } - uint8_t inputSecondsBytes = ((timestamp[0] & 0b00001100U) >> 2U) + 1U; - uint8_t inputFractionalBytes = (timestamp[0] & 0b00000011U) >> 0U; + uint8_t inputBaseBytes = ((timestamp[0] & 0b00001100U) >> 2U) + 1U; + uint8_t inputFractionBytes = (timestamp[0] & 0b00000011U) >> 0U; if (headerSize == 2) { - inputSecondsBytes += (timestamp[1] & 0b01100000U) >> 5U; - inputFractionalBytes += (timestamp[1] & 0b00011100U) >> 2U; + inputBaseBytes += (timestamp[1] & 0b01100000U) >> 5U; + inputFractionBytes += (timestamp[1] & 0b00011100U) >> 2U; } // check input validity (useless bytes set to 0) - for (int i = headerSize + inputSecondsBytes + inputFractionalBytes; i < Time::CUCTimestampMaximumSize; i++) { + for (int i = headerSize + inputBaseBytes + inputFractionBytes; i < Time::CUCTimestampMaximumSize; i++) { if (timestamp[i] != 0) { ErrorHandler::reportInternalError(ErrorHandler::InternalErrorType::InvalidTimeStampInput); break; @@ -48,78 +50,89 @@ TimeStamp<secondsCounter, fractionalBytes>::TimeStamp(etl::array<uint8_t, Time:: } // do checks wrt template precision parameters - ASSERT_INTERNAL(inputSecondsBytes <= secondsCounter, ErrorHandler::InternalErrorType::InvalidTimeStampInput); - ASSERT_INTERNAL(inputFractionalBytes <= fractionalBytes, ErrorHandler::InternalErrorType::InvalidTimeStampInput); + ASSERT_INTERNAL(inputBaseBytes <= BaseBytes, ErrorHandler::InternalErrorType::InvalidTimeStampInput); + ASSERT_INTERNAL(inputFractionBytes <= FractionBytes, ErrorHandler::InternalErrorType::InvalidTimeStampInput); // put timestamp into internal counter taiCounter = 0; // add seconds until run out of bytes on input array - for (auto i = 0; i < inputSecondsBytes + inputFractionalBytes; i++) { + for (auto i = 0; i < inputBaseBytes + inputFractionBytes; i++) { taiCounter = taiCounter << 8; taiCounter += timestamp[headerSize + i]; } // pad rightmost bytes to full length - taiCounter = taiCounter << 8 * (fractionalBytes - inputFractionalBytes); + taiCounter = taiCounter << 8 * (FractionBytes - inputFractionBytes); } -template <uint8_t seconds_counter_bytes, uint8_t fractional_counter_bytes> -TimeStamp<seconds_counter_bytes, fractional_counter_bytes>::TimeStamp(const UTCTimestamp& timestamp) { +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(const UTCTimestamp& timestamp) { TAICounter_t seconds = 0; + + /** + * Add to the seconds variable, with an overflow check + */ + auto secondsAdd = [&seconds](TAICounter_t value) { + seconds += value; + if (seconds < value) { + ErrorHandler::reportInternalError(ErrorHandler::TimeStampOutOfBounds); + } + }; + for (int year = Time::Epoch.year; year < timestamp.year; ++year) { - seconds += (Time::isLeapYear(year) ? 366 : 365) * Time::SecondsPerDay; + secondsAdd((Time::isLeapYear(year) ? 366 : 365) * Time::SecondsPerDay); } for (int month = Time::Epoch.month; month < timestamp.month; ++month) { - seconds += Time::DaysOfMonth[month - 1] * Time::SecondsPerDay; + secondsAdd(Time::DaysOfMonth[month - 1] * Time::SecondsPerDay); if ((month == 2U) && Time::isLeapYear(timestamp.year)) { - seconds += Time::SecondsPerDay; + secondsAdd(Time::SecondsPerDay); } } - seconds += (timestamp.day - Time::Epoch.day) * Time::SecondsPerDay; - seconds += timestamp.hour * Time::SecondsPerHour; - seconds += timestamp.minute * Time::SecondsPerMinute; - seconds += timestamp.second; - // TODO: Add check that `seconds` is within bounds (?) - taiCounter = static_cast<TAICounter_t>(seconds) << 8 * fractional_counter_bytes; + + secondsAdd((timestamp.day - Time::Epoch.day) * Time::SecondsPerDay); + secondsAdd(timestamp.hour * Time::SecondsPerHour); + secondsAdd(timestamp.minute * Time::SecondsPerMinute); + secondsAdd(timestamp.second); + + ASSERT_INTERNAL(areSecondsValid(seconds), ErrorHandler::TimeStampOutOfBounds); + + taiCounter = static_cast<TAICounter_t>(seconds) << (8 * FractionBytes); } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -typename TimeStamp<secondsBytes, fractionalBytes>::TAICounter_t -TimeStamp<secondsBytes, fractionalBytes>::asTAIseconds() { - return taiCounter >> (8 * fractionalBytes); +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +typename TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TAICounter_t +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::asTAIseconds() { + const auto duration = RawDuration(taiCounter); + using ToDuration = std::chrono::duration<TAICounter_t>; + + return std::chrono::duration_cast<ToDuration>(duration).count(); } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -Time::CustomCUC_t TimeStamp<secondsBytes, fractionalBytes>::asCustomCUCTimestamp() { - TAICounter_t temp = taiCounter; - Time::CustomCUC_t return_s = {0}; - if (fractionalBytes > 0) { - temp = temp >> 8 * (fractionalBytes - 1); - return_s.elapsed100msTicks += temp * 10 / 256; - } else { - return_s.elapsed100msTicks += temp * 10; - } - return return_s; +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +Time::CustomCUC_t TimeStamp<BaseBytes, FractionBytes, Num, Denom>::asCustomCUCTimestamp() { + //TODO: Remove CustomCUC_t class + TimeStamp<8, 0, 1, 10> converted(*this); + return {converted.taiCounter}; } -template <uint8_t secondsBytes, uint8_t fractionalBytes> +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> template <typename T> -T TimeStamp<secondsBytes, fractionalBytes>::asTAIseconds() { +T TimeStamp<BaseBytes, FractionBytes, Num, Denom>::asTAIseconds() { static_assert(std::is_floating_point_v<T>, "TimeStamp::asTAIseconds() only accepts numeric types."); - static_assert(std::numeric_limits<T>::max() >= maxSecondCounterValue); + static_assert(std::numeric_limits<T>::max() >= MaxSeconds); - TAICounter_t decimalPart = taiCounter >> (8 * fractionalBytes); + TAICounter_t decimalPart = taiCounter >> (8 * FractionBytes); - T fractionalPart = taiCounter - (decimalPart << (8 * fractionalBytes)); - T fractionalPartMax = (1U << (8U * fractionalBytes)) - 1U; + T fractionalPart = taiCounter - (decimalPart << (8 * FractionBytes)); + T fractionalPartMax = (1U << (8U * FractionBytes)) - 1U; return decimalPart + fractionalPart / fractionalPartMax; } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -etl::array<uint8_t, Time::CUCTimestampMaximumSize> TimeStamp<secondsBytes, fractionalBytes>::toCUCtimestamp() { +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +etl::array<uint8_t, Time::CUCTimestampMaximumSize> TimeStamp<BaseBytes, FractionBytes, Num, Denom>::formatAsCUC() { etl::array<uint8_t, Time::CUCTimestampMaximumSize> returnArray = {0}; - static constexpr uint8_t headerBytes = (secondsBytes < 4 && fractionalBytes < 3) ? 1 : 2; + static constexpr uint8_t headerBytes = (BaseBytes < 4 && FractionBytes < 3) ? 1 : 2; if (headerBytes == 1) { returnArray[0] = static_cast<uint8_t>(CUCHeader); @@ -128,16 +141,16 @@ etl::array<uint8_t, Time::CUCTimestampMaximumSize> TimeStamp<secondsBytes, fract returnArray[0] = static_cast<uint8_t>(CUCHeader >> 8); } - for (auto byte = 0; byte < secondsBytes + fractionalBytes; byte++) { - uint8_t taiCounterIndex = 8 * (secondsBytes + fractionalBytes - byte - 1); + for (auto byte = 0; byte < BaseBytes + FractionBytes; byte++) { + uint8_t taiCounterIndex = 8 * (BaseBytes + FractionBytes - byte - 1); returnArray[headerBytes + byte] = taiCounter >> taiCounterIndex; } return returnArray; } -template <uint8_t secondsBytes, uint8_t fractionalBytes> -UTCTimestamp TimeStamp<secondsBytes, fractionalBytes>::toUTCtimestamp() { +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +UTCTimestamp TimeStamp<BaseBytes, FractionBytes, Num, Denom>::toUTCtimestamp() { using namespace Time; uint32_t totalSeconds = asTAIseconds(); @@ -185,3 +198,38 @@ UTCTimestamp TimeStamp<secondsBytes, fractionalBytes>::toUTCtimestamp() { return {yearUTC, monthUTC, dayUTC, hour, minute, second}; } +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +template <uint8_t BaseBytesIn, uint8_t FractionBytesIn, int NumIn, int DenomIn> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(TimeStamp<BaseBytesIn, FractionBytesIn, NumIn, DenomIn> input) { + if constexpr (std::is_same_v<decltype(*this), decltype(input)>) { + taiCounter = input.taiCounter; + return; + } + + constexpr double InputRatio = static_cast<double>(NumIn) / DenomIn; + constexpr double OutputRatio = static_cast<double>(Num) / Denom; + + double inputSeconds = input.taiCounter / static_cast<double>(1 << (8 * FractionBytesIn)); + inputSeconds *= InputRatio; + + ErrorHandler::assertInternal(inputSeconds <= MaxSeconds, ErrorHandler::TimeStampOutOfBounds); + + double output = inputSeconds / OutputRatio * (1UL << (8 * FractionBytes)); + + taiCounter = static_cast<TAICounter_t>(round(output)); +} + +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +template <class Duration> +Duration TimeStamp<BaseBytes, FractionBytes, Num, Denom>::asDuration() { + auto duration = RawDuration(taiCounter); + + return std::chrono::duration_cast<Duration>(duration); +} + +template <uint8_t BaseBytes, uint8_t FractionBytes, int Num, int Denom> +template <class Duration, typename> +TimeStamp<BaseBytes, FractionBytes, Num, Denom>::TimeStamp(Duration duration) { + auto outputDuration = std::chrono::duration_cast<RawDuration>(duration); + taiCounter = outputDuration.count(); +} diff --git a/test/Services/ParameterStatisticsServiceTests.cpp b/test/Services/ParameterStatisticsServiceTests.cpp index 74a0e81b0c7fc17f31fe5c70b3e1c5a81d80374a..3ec7031866804325b5b35c9b3a8c0f6bf12d761b 100644 --- a/test/Services/ParameterStatisticsServiceTests.cpp +++ b/test/Services/ParameterStatisticsServiceTests.cpp @@ -27,10 +27,6 @@ void initializeStatistics(uint16_t interval1, uint16_t interval2) { Services.parameterStatistics.statisticsMap.insert({id2, stat2}); } -void resetSystem() { - Services.parameterStatistics.statisticsMap.clear(); -} - TEST_CASE("Reporting of statistics") { SECTION("Report statistics, with auto statistic reset disabled with TC") { initializeStatistics(6, 7); @@ -97,9 +93,7 @@ TEST_CASE("Reporting of statistics") { CHECK(Services.parameterStatistics.statisticsMap[7].statisticsAreInitialized()); } - resetSystem(); ServiceTests::reset(); - Services.reset(); SECTION("Report statistics, with auto statistic reset disabled without TC") { initializeStatistics(6, 7); @@ -157,11 +151,7 @@ TEST_CASE("Reporting of statistics") { CHECK(Services.parameterStatistics.statisticsMap[5].statisticsAreInitialized()); CHECK(Services.parameterStatistics.statisticsMap[7].statisticsAreInitialized()); - } - resetSystem(); - ServiceTests::reset(); - Services.reset(); } TEST_CASE("Resetting the parameter statistics") { @@ -178,9 +168,7 @@ TEST_CASE("Resetting the parameter statistics") { CHECK(Services.parameterStatistics.statisticsMap[5].statisticsAreInitialized()); CHECK(Services.parameterStatistics.statisticsMap[7].statisticsAreInitialized()); - resetSystem(); ServiceTests::reset(); - Services.reset(); } SECTION("Reset without TC") { @@ -194,9 +182,7 @@ TEST_CASE("Resetting the parameter statistics") { CHECK(Services.parameterStatistics.statisticsMap[5].statisticsAreInitialized()); CHECK(Services.parameterStatistics.statisticsMap[7].statisticsAreInitialized()); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } @@ -231,9 +217,7 @@ TEST_CASE("Enable the periodic reporting of statistics") { CHECK(Services.parameterStatistics.getPeriodicReportingStatus() == false); CHECK(Services.parameterStatistics.getReportingIntervalMs() == 6); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } @@ -248,9 +232,7 @@ TEST_CASE("Disabling the periodic reporting of statistics") { MessageParser::execute(request); REQUIRE(Services.parameterStatistics.getPeriodicReportingStatus() == false); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } @@ -280,9 +262,7 @@ TEST_CASE("Add/Update statistics definitions") { CHECK(Services.parameterStatistics.statisticsMap.size() == 3); CHECK(Services.parameterStatistics.statisticsMap[0].selfSamplingInterval == 1400); - resetSystem(); ServiceTests::reset(); - Services.reset(); } SECTION("Add new statistic definition") { @@ -306,9 +286,7 @@ TEST_CASE("Add/Update statistics definitions") { CHECK(Services.parameterStatistics.statisticsMap.size() == 3); CHECK(Services.parameterStatistics.statisticsMap[1].selfSamplingInterval == 3200); - resetSystem(); ServiceTests::reset(); - Services.reset(); } SECTION("All possible invalid requests combined with add/update") { @@ -362,9 +340,7 @@ TEST_CASE("Add/Update statistics definitions") { CHECK(Services.parameterStatistics.statisticsMap[0].selfSamplingInterval == 14000); CHECK(Services.parameterStatistics.statisticsMap[1].selfSamplingInterval == 32000); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } @@ -410,12 +386,9 @@ TEST_CASE("Delete statistics definitions") { MessageParser::execute(request); CHECK(Services.parameterStatistics.getPeriodicReportingStatus() == false); - CHECK(ServiceTests::countThrownErrors(ErrorHandler::GetNonExistingParameter) == 1); CHECK(Services.parameterStatistics.statisticsMap.empty()); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } @@ -441,8 +414,6 @@ TEST_CASE("Parameter statistics definition report") { CHECK(report.readUint16() == 7); CHECK(report.readUint16() == 0); - resetSystem(); ServiceTests::reset(); - Services.reset(); } } diff --git a/test/Services/ServiceTests.hpp b/test/Services/ServiceTests.hpp index d5b76b882b1f788b7da4267f5ab4d3f4841f5168..f45726d85f89bf15afa216d3fdadd684dec47239 100644 --- a/test/Services/ServiceTests.hpp +++ b/test/Services/ServiceTests.hpp @@ -84,13 +84,17 @@ public: return count() == 1; } + static void resetErrors() { + queuedMessages.clear(); + thrownErrors.clear(); + expectingErrors = false; + } + /** * Reset the testing environment, starting from zero for all parameters */ static void reset() { - queuedMessages.clear(); - thrownErrors.clear(); - expectingErrors = false; + resetErrors(); Services.reset(); } @@ -150,6 +154,19 @@ public: return thrownErrors.count(std::make_pair(errorSource, errorType)); } + + /** + * Get the list of all thrown errors + */ + static std::vector<std::pair<ErrorHandler::ErrorSource, uint16_t>> getThrownErrors() { + std::vector<std::pair<ErrorHandler::ErrorSource, uint16_t>> errors; + + for (auto error : thrownErrors) { + errors.push_back(error.first); + } + + return errors; + } }; #endif // ECSS_SERVICES_TESTS_SERVICES_SERVICETESTS_HPP diff --git a/test/TestPlatform.cpp b/test/TestPlatform.cpp index 29a222854d4f48ccd437b8b64dfc468ed5fd032f..a2a2d80f1fb09d8aed88242b88a8e35f516d210d 100644 --- a/test/TestPlatform.cpp +++ b/test/TestPlatform.cpp @@ -5,6 +5,7 @@ #include <Message.hpp> #include <Service.hpp> #include <catch2/catch_all.hpp> +#include <cxxabi.h> #include "Helpers/Parameter.hpp" #include "Helpers/TimeGetter.hpp" #include "Parameters/PlatformParameters.hpp" @@ -51,10 +52,18 @@ void ErrorHandler::logError(const Message& message, ErrorType errorType) { template <typename ErrorType> void ErrorHandler::logError(ErrorType errorType) { ServiceTests::addError(ErrorHandler::findErrorSource(errorType), errorType); + + auto errorCategory = abi::__cxa_demangle(typeid(ErrorType).name(), nullptr, nullptr, nullptr); + auto errorNumber = std::underlying_type_t<ErrorType>(errorType); + + LOG_ERROR << "Error " << errorCategory << " with number " << errorNumber; } void Logger::log(Logger::LogLevel level, etl::istring& message) { - // Logs while testing are completely ignored + // Logs while testing are passed on to Catch2, if they are important enough + if (level >= Logger::warning) { + UNSCOPED_INFO(message.c_str()); + } } struct ServiceTestsListener : Catch::EventListenerBase { @@ -65,9 +74,14 @@ struct ServiceTestsListener : Catch::EventListenerBase { if (not ServiceTests::isExpectingErrors()) { // An Error was thrown with this Message. If you expected this to happen, please call a // corresponding assertion function from ServiceTests to silence this message. - + UNSCOPED_INFO("Found " << ServiceTests::countErrors() << " errors at end of section: "); + for (auto error: ServiceTests::getThrownErrors()) { + UNSCOPED_INFO(" Error " << error.second << " (type " << error.first << ")"); + } CHECK(ServiceTests::hasNoErrors()); } + + ServiceTests::resetErrors(); } void testCaseEnded(Catch::TestCaseStats const& testCaseStats) override { diff --git a/test/Time/TimeStampTests.cpp b/test/Time/TimeStampTests.cpp index f01a462ec53c55b38a1df9eb152690bda26671cd..daf3df672d0ef6e12ce647836b8156feeb6bf6a3 100644 --- a/test/Time/TimeStampTests.cpp +++ b/test/Time/TimeStampTests.cpp @@ -1,7 +1,9 @@ +#include "../Services/ServiceTests.hpp" #include "Time/TimeStamp.hpp" #include "catch2/catch_all.hpp" using namespace Time; +using Catch::Approx; TEST_CASE("TimeStamp class construction") { // SECTION("Initialize with excessive precision, breaks at compile time"){ @@ -40,7 +42,7 @@ TEST_CASE("TAI idempotence") { TEST_CASE("CUC idempotence") { etl::array<uint8_t, 9> input1 = {0b00101010, 0, 1, 1, 3, 0, 0, 0, 0}; TimeStamp<3, 2> time1(input1); - etl::array<uint8_t, 9> cuc1 = time1.toCUCtimestamp(); + etl::array<uint8_t, 9> cuc1 = time1.formatAsCUC(); for (uint8_t i = 0; i < 9; i++) { CHECK(input1[i] == cuc1[i]); @@ -48,7 +50,7 @@ TEST_CASE("CUC idempotence") { etl::array<uint8_t, 9> input2 = {0b10101101, 0b10100000, 218, 103, 11, 0, 3, 23, 0}; TimeStamp<5, 1> time2(input2); - etl::array<uint8_t, 9> cuc2 = time2.toCUCtimestamp(); + etl::array<uint8_t, 9> cuc2 = time2.formatAsCUC(); for (auto i = 0; i < 9; i++) { CHECK(input2[i] == cuc2[i]); @@ -56,7 +58,7 @@ TEST_CASE("CUC idempotence") { etl::array<uint8_t, 9> input3 = {0b10100011, 0b10001100, 218, 103, 11, 0, 3, 23, 2}; TimeStamp<1, 6> time3(input3); - etl::array<uint8_t, 9> cuc3 = time3.toCUCtimestamp(); + etl::array<uint8_t, 9> cuc3 = time3.formatAsCUC(); for (auto i = 0; i < 9; i++) { CHECK(input3[i] == cuc3[i]); @@ -66,7 +68,7 @@ TEST_CASE("CUC idempotence") { TEST_CASE("Conversion between CUC formats") { SECTION("Base unit conversion") { TimeStamp<2, 2> time1(20123); - TimeStamp<5, 2> time2(time1.toCUCtimestamp()); + TimeStamp<5, 2> time2(time1.formatAsCUC()); CHECK(time1.asTAIseconds() == time2.asTAIseconds()); } @@ -74,7 +76,7 @@ TEST_CASE("Conversion between CUC formats") { etl::array<uint8_t, 9> timeInput = {0b00101010, 0, 1, 1, 3, 0, 0, 0, 0}; TimeStamp<3, 2> time1(timeInput); - TimeStamp<3, 5> time2(time1.toCUCtimestamp()); + TimeStamp<3, 5> time2(time1.formatAsCUC()); CHECK(time1.asTAIseconds() == time2.asTAIseconds()); } @@ -82,7 +84,7 @@ TEST_CASE("Conversion between CUC formats") { etl::array<uint8_t, 9> timeInput = {0b00101010, 0, 1, 1, 3, 0, 0, 0, 0}; TimeStamp<3, 2> time1(timeInput); - TimeStamp<4, 4> time2(time1.toCUCtimestamp()); + TimeStamp<4, 4> time2(time1.formatAsCUC()); CHECK(time1.asTAIseconds() == time2.asTAIseconds()); } } @@ -95,7 +97,7 @@ TEST_CASE("Use of custom Acubesat CUC format") { CHECK(time1.asCustomCUCTimestamp().elapsed100msTicks == 1000); TimeStamp<3, 2> time2(customCUC1); CHECK(time2.asTAIseconds() == 100); - CHECK(time2.asCustomCUCTimestamp().elapsed100msTicks == 1000); + CHECK(time2.asCustomCUCTimestamp().elapsed100msTicks == 1001); // check rounding errors Time::CustomCUC_t customCUC2 = {1004}; @@ -104,13 +106,13 @@ TEST_CASE("Use of custom Acubesat CUC format") { CHECK(time3.asCustomCUCTimestamp().elapsed100msTicks == 1000); TimeStamp<3, 2> time4(customCUC2); CHECK(time4.asTAIseconds() == 100); - CHECK(time4.asCustomCUCTimestamp().elapsed100msTicks == 1003); + CHECK(time4.asCustomCUCTimestamp().elapsed100msTicks == 1004); // check rounding errors Time::CustomCUC_t customCUC3 = {1005}; TimeStamp<3, 0> time5(customCUC3); - CHECK(time5.asTAIseconds() == 100); - CHECK(time5.asCustomCUCTimestamp().elapsed100msTicks == 1000); + CHECK(time5.asTAIseconds() == 101); + CHECK(time5.asCustomCUCTimestamp().elapsed100msTicks == 1010); TimeStamp<3, 2> time6(customCUC3); CHECK(time6.asTAIseconds() == 100); CHECK(time6.asCustomCUCTimestamp().elapsed100msTicks == 1005); @@ -143,22 +145,43 @@ TEST_CASE("UTC idempotence") { TEST_CASE("UTC conversion to and from seconds timestamps") { { - UTCTimestamp timestamp1(2020, 12, 5, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(timestamp1); + UTCTimestamp utc(2020, 12, 5, 0, 0, 0); + TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); REQUIRE(time.asTAIseconds() == 29289600); } { - UTCTimestamp timestamp1(2020, 2, 29, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(timestamp1); + UTCTimestamp utc(2020, 2, 29, 0, 0, 0); + TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); REQUIRE(time.asTAIseconds() == 5097600); } { - UTCTimestamp timestamp1(2025, 3, 10, 0, 0, 0); - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(timestamp1); + UTCTimestamp utc(2025, 3, 10, 0, 0, 0); + TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(utc); REQUIRE(time.asTAIseconds() == 163728000); } } +TEST_CASE("UTC overflow tests") { + SECTION("Year too high") { + UTCTimestamp utc(2999, 3, 11, 0, 0, 0); + TimeStamp<2, 1> time(utc); + REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); + ServiceTests::reset(); + } + SECTION("Seconds too high, small variable") { + UTCTimestamp utc(Epoch.year, Epoch.month, Epoch.day, 0, 7, 0); + TimeStamp<1, 1> time(utc); + REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); + ServiceTests::reset(); + } + SECTION("Seconds too high, wide variable") { + UTCTimestamp utc(Epoch.year, Epoch.month, Epoch.day, 0, 7, 0); + TimeStamp<1, 4> time(utc); + REQUIRE(ServiceTests::thrownError(ErrorHandler::TimeStampOutOfBounds)); + ServiceTests::reset(); + } +} + // SECTION("Check different templates, should break at compile"){ // TimeStamp<1, 2> time1; // TimeStamp<4, 4> time2; @@ -166,25 +189,143 @@ TEST_CASE("UTC conversion to and from seconds timestamps") { // } TEST_CASE("Time operators") { - TimeStamp<1, 2> time1; - TimeStamp<1, 2> time2; - TimeStamp<1, 2> time3(10); - TimeStamp<1, 2> time4(12); - TimeStamp<1, 2> time5(10); - TimeStamp<2, 2> time6; - REQUIRE(time1 == time2); - REQUIRE(time2 == time1); - REQUIRE(time3 == time5); - REQUIRE(time1 != time3); - REQUIRE(time3 != time4); - REQUIRE(time3 <= time4); - REQUIRE(time3 < time4); - - // REQUIRE(time1 == time6); //should fail at compile, different templates + SECTION("Same type") { + TimeStamp<1, 2> time1; + TimeStamp<1, 2> time2; + TimeStamp<1, 2> time3(10); + TimeStamp<1, 2> time4(12); + TimeStamp<1, 2> time5(10); + CHECK(time1 == time2); + CHECK(time2 == time1); + CHECK(time3 == time5); + CHECK(time1 != time3); + CHECK(time3 != time4); + CHECK(time3 <= time4); + CHECK(time3 < time4); + } + + SECTION("Different size") { + TimeStamp<1, 2> time1(10); + TimeStamp<2, 1> time2(10); + TimeStamp<3, 2> time3(15); + TimeStamp<2, 2> time4(5); + CHECK(time1 == time2); + CHECK(time3 != time2); + CHECK(time4 < time2); + CHECK(time3 > time4); + CHECK(time2 >= time1); + CHECK(time1 <= time3); + } + + SECTION("Different units") { + TimeStamp<1, 2, 10> time1(10); + TimeStamp<1, 2, 1, 10> time2(10); + TimeStamp<3, 2, 3, 2> time3(15); + TimeStamp<2, 2, 57, 89> time4(5); + CHECK(time1 == time2); + CHECK(time3 != time2); + CHECK(time4 < time2); + CHECK(time3 > time4); + CHECK(time2 >= time1); + CHECK(time1 <= time3); + } + + SECTION("Overflow") { + TimeStamp<1, 0> time1(1); + TimeStamp<4, 4> time2(std::numeric_limits<uint32_t>::max()); + + CHECK(time1 != time2); + CHECK_FALSE(time1 == time2); + CHECK(time1 < time2); + CHECK(time1 <= time2); + CHECK(time2 > time1); + CHECK(time2 >= time1); + } } TEST_CASE("Time runtime class size") { - int input_time = 1000; - TimeStamp<CUCSecondsBytes, CUCFractionalBytes> time(input_time); - REQUIRE(sizeof(time) < 32); + REQUIRE(sizeof(TimeStamp<CUCSecondsBytes, CUCFractionalBytes>) <= 8); +} + +TEST_CASE("CUC conversions") { + SECTION("Base unit, without fractions") { + TimeStamp<2, 0, 10, 1> time1(100); + CHECK(time1.asTAIseconds() == 100); + + TimeStamp<2, 0, 1, 10> time2(time1); + CHECK(time2.asTAIseconds() == 100); + } + + SECTION("Base unit, with fractions") { + TimeStamp<2, 2, 10, 1> time1(100); + CHECK(time1.asTAIseconds() == 100); + + TimeStamp<2, 2, 1, 10> time2(time1); + CHECK(time2.asTAIseconds() == 100); + } + + SECTION("Addition of fraction") { + TimeStamp<2, 0, 1, 1> time1(100); + CHECK(time1.asTAIseconds() == 100); + + TimeStamp<2, 2, 1, 1> time2(time1); + CHECK(time2.asTAIseconds() == 100); + } + + SECTION("Removal of fraction") { + TimeStamp<2, 2, 1, 1> time1(100); + CHECK(time1.asTAIseconds() == 100); + + TimeStamp<2, 0, 1, 1> time2(time1); + CHECK(time2.asTAIseconds() == 100); + } + + SECTION("Many changes") { + TimeStamp<2, 2, 3, 2> time1(1000); + CHECK(time1.asTAIseconds() == Approx(1000).epsilon(1)); + + TimeStamp<3, 4, 100, 29> time2(time1); + CHECK(time2.asTAIseconds() == Approx(1000).epsilon(1)); + + TimeStamp<2, 1, 1, 1> time3(time1); + CHECK(time3.asTAIseconds() == Approx(1000).epsilon(1)); + } + + SECTION("Large numbers") { + TimeStamp<4, 0, 1, 1> time1(10000); + CHECK(time1.asTAIseconds() == 10000); + + TimeStamp<2, 1, 7907, 7559> time2(time1); + CHECK(time2.asTAIseconds() == 9999); + } +} + +TEST_CASE("Duration conversions") { + using namespace std::chrono_literals; + + SECTION("Conversion to duration") { + TimeStamp<2, 2, 1, 1> time(3600); + auto duration = time.asDuration<std::chrono::hours>(); + CHECK(duration == 1h); + } + + SECTION("Conversion from duration") { + auto duration = 90min; + TimeStamp<2, 2, 1, 1> time(duration); + CHECK(time.asTAIseconds() == 5400); + } + + SECTION("Duration idempotence") { + auto duration = 13532s; + TimeStamp<2, 2, 1, 1> time(duration); + + CHECK(time.asDuration() == duration); + } + + SECTION("Overflow") { + auto duration = 24h; + TimeStamp<2, 2, 1, 1> time(duration); + + CHECK(time.asTAIseconds() == 20864); + } }