From: Adam Simpkins Date: Sun, 19 Nov 2017 23:14:20 +0000 (-0800) Subject: implement to() conversions for std::chrono to timespec/timeval X-Git-Tag: v2017.11.20.00~1 X-Git-Url: http://demsky.eecs.uci.edu/git/?a=commitdiff_plain;h=dc91b004d384ba57887b38d4668845bffee0ba08;p=folly.git implement to() conversions for std::chrono to timespec/timeval Summary: Add folly::to() conversions to convert between std::chrono::duration or std::chrono::time_point types and struct timespec or struct timeval types. To conform to the behavior of the existing arithmetic-to-arithmetic conversions, this code performs proper overflow checking and throws a `ConversionError` on overflow. This unfortunately does make the code rather complicated compared to a non-checking implementation. Conversions between some unusual duration types is not implemented yet, and will fail at compile time if someone tries to use it. This happens for durations where neither the numerator nor the denominator of the ratio is 1. For instance, 7/13ths of a second. Reviewed By: yfeldblum Differential Revision: D6356700 fbshipit-source-id: 9dce8ab8f32d8c18089f32c7176a8abf3c3f11f7 --- diff --git a/CMakeLists.txt b/CMakeLists.txt index 699489bf..3d7c1533 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -327,6 +327,9 @@ if (BUILD_TESTS) apply_folly_compile_options_to_target(folly_test_support) folly_define_tests( + DIRECTORY chrono/test/ + TEST chrono_conv_test SOURCES ConvTest.cpp + DIRECTORY compression/test/ TEST compression_test SOURCES CompressionTest.cpp diff --git a/folly/Makefile.am b/folly/Makefile.am index 0639ff90..2bf6bae7 100644 --- a/folly/Makefile.am +++ b/folly/Makefile.am @@ -5,7 +5,7 @@ endif # Note that the order of SUBDIRS matters. # Many subdirectories depend on libfollytest from the test directory, # so it must appear before other directories -SUBDIRS = . test experimental $(MAYBE_INIT) io/test stats/test +SUBDIRS = . test experimental $(MAYBE_INIT) chrono/test io/test stats/test ACLOCAL_AMFLAGS = -I m4 @@ -42,6 +42,7 @@ nobase_follyinclude_HEADERS = \ Bits.h \ CachelinePadded.h \ Chrono.h \ + chrono/Conv.h \ ClockGettimeWrappers.h \ ConcurrentSkipList.h \ ConcurrentSkipList-inl.h \ diff --git a/folly/chrono/Conv.h b/folly/chrono/Conv.h new file mode 100644 index 00000000..95aff32a --- /dev/null +++ b/folly/chrono/Conv.h @@ -0,0 +1,638 @@ +/* + * Copyright 2017 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Conversions between std::chrono types and POSIX time types. + * + * These conversions will fail with a ConversionError if an overflow would + * occur performing the conversion. (e.g., if the input value cannot fit in + * the destination type). However they allow loss of precision (e.g., + * converting nanoseconds to a struct timeval which only has microsecond + * granularity, or a struct timespec to std::chrono::minutes). + */ + +#pragma once + +#include +#include + +#include +#include + +namespace folly { +namespace detail { + +template +struct is_duration : std::false_type {}; +template +struct is_duration> : std::true_type {}; +template +struct is_time_point : std::false_type {}; +template +struct is_time_point> + : std::true_type {}; +template +struct is_std_chrono_type { + static constexpr bool value = + is_duration::value || is_time_point::value; +}; +template +struct is_posix_time_type { + static constexpr bool value = std::is_same::value || + std::is_same::value; +}; +template +struct is_chrono_conversion { + static constexpr bool value = + ((is_std_chrono_type::value && is_posix_time_type::value) || + (is_posix_time_type::value && is_std_chrono_type::value)); +}; + +/** + * This converts a number in some input type to time_t while ensuring that it + * fits in the range of numbers representable by time_t. + * + * This is similar to the normal folly::tryTo() behavior when converting + * arthmetic types to an integer type, except that it does not complain about + * floating point conversions losing precision. + */ +template +Expected chronoRangeCheck(Src value) { + if (value > std::numeric_limits::max()) { + return makeUnexpected(ConversionCode::POSITIVE_OVERFLOW); + } + if (std::is_signed::value) { + if (value < std::numeric_limits::lowest()) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + } + + return static_cast(value); +} + +/** + * Convert a std::chrono::duration with second granularity to a pair of + * (seconds, subseconds) + * + * The SubsecondRatio template parameter specifies what type of subseconds to + * return. This must have a numerator of 1. + */ +template +static Expected, ConversionCode> durationToPosixTime( + const std::chrono::duration>& duration) { + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + auto sec = chronoRangeCheck(duration.count()); + if (sec.hasError()) { + return makeUnexpected(sec.error()); + } + + time_t secValue = sec.value(); + long subsec = 0L; + if (std::is_floating_point::value) { + auto fraction = (duration.count() - secValue); + subsec = static_cast(fraction * SubsecondRatio::den); + if (duration.count() < 0 && fraction < 0) { + if (secValue == std::numeric_limits::lowest()) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + secValue -= 1; + subsec += SubsecondRatio::den; + } + } + return std::pair{secValue, subsec}; +} + +/** + * Convert a std::chrono::duration with subsecond granularity to a pair of + * (seconds, subseconds) + */ +template +static Expected, ConversionCode> durationToPosixTime( + const std::chrono::duration>& duration) { + static_assert(Denominator != 1, "special case expecting den != 1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + auto sec = chronoRangeCheck(duration.count() / Denominator); + if (sec.hasError()) { + return makeUnexpected(sec.error()); + } + auto secTimeT = sec.value(); + + auto remainder = duration.count() - (secTimeT * Denominator); + auto subsec = (remainder * SubsecondRatio::den) / Denominator; + if (UNLIKELY(duration.count() < 0) && remainder != 0) { + if (secTimeT == std::numeric_limits::lowest()) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + secTimeT -= 1; + subsec += SubsecondRatio::den; + } + + return std::pair{secTimeT, subsec}; +} + +/** + * Convert a std::chrono::duration with coarser-than-second granularity to a + * pair of (seconds, subseconds) + */ +template +static Expected, ConversionCode> durationToPosixTime( + const std::chrono::duration>& duration) { + static_assert(Numerator != 1, "special case expecting num!=1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + constexpr auto maxValue = std::numeric_limits::max() / Numerator; + constexpr auto minValue = std::numeric_limits::lowest() / Numerator; + if (duration.count() > maxValue) { + return makeUnexpected(ConversionCode::POSITIVE_OVERFLOW); + } + if (duration.count() < minValue) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + + // Note that we can't use chronoRangeCheck() here since we have to check + // if (duration.count() * Numerator) would overflow (which we do above). + auto secOriginalRep = (duration.count() * Numerator); + auto sec = static_cast(secOriginalRep); + + long subsec = 0L; + if (std::is_floating_point::value) { + auto fraction = secOriginalRep - sec; + subsec = static_cast(fraction * SubsecondRatio::den); + if (duration.count() < 0 && fraction < 0) { + if (sec == std::numeric_limits::lowest()) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + sec -= 1; + subsec += SubsecondRatio::den; + } + } + return std::pair{sec, subsec}; +} + +/** + * Convert a std::chrono::duration to a pair of (seconds, subseconds) + * + * This overload is only used for unusual durations where neither the numerator + * nor denominator are 1. + */ +template +Expected, ConversionCode> durationToPosixTime( + const std::chrono::duration& duration) { + static_assert(Period::num != 1, "should use special-case code when num==1"); + static_assert(Period::den != 1, "should use special-case code when den==1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + // TODO: We need to implement an overflow-checking tryTo() function for + // duration-to-duration casts for the code above to work. + // + // For now this is unimplemented, and we just have a static_assert that + // will always fail if someone tries to instantiate this. Unusual duration + // types should be extremely rare, and I'm not aware of any code at the + // moment that actually needs this. + static_assert( + Period::num == 1, + "conversion from unusual duration types is not implemented yet"); + (void)duration; + return makeUnexpected(ConversionCode::SUCCESS); +} + +/** + * Check for overflow when converting to a duration type that is second + * granularity or finer (e.g., nanoseconds, milliseconds, seconds) + * + * This assumes the input is normalized, with subseconds >= 0 and subseconds + * less than 1 second. + */ +template +struct CheckOverflowToDuration { + template < + typename Tgt, + typename SubsecondRatio, + typename Seconds, + typename Subseconds> + static ConversionCode check(Seconds seconds, Subseconds subseconds) { + static_assert( + Tgt::period::num == 1, + "this implementation should only be used for subsecond granularity " + "duration types"); + static_assert( + !std::is_floating_point::value, "incorrect usage"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + if (LIKELY(seconds >= 0)) { + constexpr auto maxCount = std::numeric_limits::max(); + constexpr auto maxSeconds = maxCount / Tgt::period::den; + + auto unsignedSeconds = + static_cast::type>(seconds); + if (LIKELY(unsignedSeconds < maxSeconds)) { + return ConversionCode::SUCCESS; + } + + if (UNLIKELY(unsignedSeconds == maxSeconds)) { + constexpr auto maxRemainder = + maxCount - (maxSeconds * Tgt::period::den); + constexpr auto maxSubseconds = + (maxRemainder * SubsecondRatio::den) / Tgt::period::den; + if (subseconds <= 0) { + return ConversionCode::SUCCESS; + } + if (static_cast::type>( + subseconds) <= maxSubseconds) { + return ConversionCode::SUCCESS; + } + } + return ConversionCode::POSITIVE_OVERFLOW; + } else if (std::is_unsigned::value) { + return ConversionCode::NEGATIVE_OVERFLOW; + } else { + constexpr auto minCount = + static_cast::type>( + std::numeric_limits::lowest()); + constexpr auto minSeconds = (minCount / Tgt::period::den); + if (LIKELY(seconds >= minSeconds)) { + return ConversionCode::SUCCESS; + } + + if (UNLIKELY(seconds == minSeconds - 1)) { + constexpr auto maxRemainder = + minCount - (minSeconds * Tgt::period::den) + Tgt::period::den; + constexpr auto maxSubseconds = + (maxRemainder * SubsecondRatio::den) / Tgt::period::den; + if (subseconds <= 0) { + return ConversionCode::NEGATIVE_OVERFLOW; + } + if (subseconds >= maxSubseconds) { + return ConversionCode::SUCCESS; + } + } + return ConversionCode::NEGATIVE_OVERFLOW; + } + } +}; + +template <> +struct CheckOverflowToDuration { + template < + typename Tgt, + typename SubsecondRatio, + typename Seconds, + typename Subseconds> + static ConversionCode check( + Seconds /* seconds */, + Subseconds /* subseconds */) { + static_assert( + std::is_floating_point::value, "incorrect usage"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + // We expect floating point types to have much a wider representable range + // than integer types, so we don't bother actually checking the input + // integer value here. + static_assert( + std::numeric_limits::max() >= + std::numeric_limits::max(), + "unusually limited floating point type"); + static_assert( + std::numeric_limits::lowest() <= + std::numeric_limits::lowest(), + "unusually limited floating point type"); + + return ConversionCode::SUCCESS; + } +}; + +/** + * Helper class to convert a POSIX-style pair of (seconds, subseconds) + * to a std::chrono::duration type. + * + * The SubsecondRatio template parameter specifies what type of subseconds to + * return. This must have a numerator of 1. + * + * The input must be in normalized form: the subseconds field must be greater + * than or equal to 0, and less than SubsecondRatio::den (i.e., less than 1 + * second). + * + * This default implementation is only used for unusual std::chrono::duration + * types where neither the numerator nor denominator are 1. + */ +template +struct PosixTimeToDuration { + template + static Expected cast( + Seconds seconds, + Subseconds subseconds); +}; + +/** + * Convert a timeval or a timespec to a std::chrono::duration with second + * granularity. + */ +template +struct PosixTimeToDuration>> { + using Tgt = std::chrono::duration>; + + template + static Expected cast( + Seconds seconds, + Subseconds subseconds) { + static_assert(Tgt::period::num == 1, "special case expecting num==1"); + static_assert(Tgt::period::den == 1, "special case expecting den==1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + auto outputSeconds = tryTo(seconds); + if (outputSeconds.hasError()) { + return makeUnexpected(outputSeconds.error()); + } + + if (std::is_floating_point::value) { + return Tgt{typename Tgt::rep(seconds) + + (typename Tgt::rep(subseconds) / SubsecondRatio::den)}; + } + + // If the value is negative, we have to round up a non-zero subseconds value + if (UNLIKELY(outputSeconds.value() < 0) && subseconds > 0) { + if (UNLIKELY( + outputSeconds.value() == + std::numeric_limits::lowest())) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + return Tgt{outputSeconds.value() + 1}; + } + + return Tgt{outputSeconds.value()}; + } +}; + +/** + * Convert a timeval or a timespec to a std::chrono::duration with subsecond + * granularity + */ +template +struct PosixTimeToDuration< + std::chrono::duration>> { + using Tgt = std::chrono::duration>; + + template + static Expected cast( + Seconds seconds, + Subseconds subseconds) { + static_assert(Tgt::period::num == 1, "special case expecting num==1"); + static_assert(Tgt::period::den != 1, "special case expecting den!=1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + auto errorCode = detail::CheckOverflowToDuration< + std::is_floating_point::value>:: + template check(seconds, subseconds); + if (errorCode != ConversionCode::SUCCESS) { + return makeUnexpected(errorCode); + } + + if (LIKELY(seconds >= 0)) { + return std::chrono::duration_cast( + std::chrono::duration{seconds}) + + std::chrono::duration_cast( + std::chrono::duration{ + subseconds}); + } else { + // For negative numbers we have to round subseconds up towards zero, even + // though it is a positive value, since the overall value is negative. + return std::chrono::duration_cast( + std::chrono::duration{seconds + 1}) - + std::chrono::duration_cast( + std::chrono::duration{ + SubsecondRatio::den - subseconds}); + } + } +}; + +/** + * Convert a timeval or a timespec to a std::chrono::duration with + * granularity coarser than 1 second. + */ +template +struct PosixTimeToDuration< + std::chrono::duration>> { + using Tgt = std::chrono::duration>; + + template + static Expected cast( + Seconds seconds, + Subseconds subseconds) { + static_assert(Tgt::period::num != 1, "special case expecting num!=1"); + static_assert(Tgt::period::den == 1, "special case expecting den==1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + if (UNLIKELY(seconds < 0) && subseconds > 0) { + // Increment seconds by one to handle truncation of negative numbers + // properly. + if (UNLIKELY(seconds == std::numeric_limits::lowest())) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + seconds += 1; + } + + if (std::is_floating_point::value) { + // Convert to the floating point type before performing the division + return Tgt{static_cast(seconds) / Tgt::period::num}; + } else { + // Perform the division as an integer, and check that the result fits in + // the output integer type + auto outputValue = (seconds / Tgt::period::num); + auto expectedOuput = tryTo(outputValue); + if (expectedOuput.hasError()) { + return makeUnexpected(expectedOuput.error()); + } + + return Tgt{expectedOuput.value()}; + } + } +}; + +/** + * PosixTimeToDuration::cast() implementation for the default case + * with unusual durations where neither the numerator nor denominator are 1. + */ +template +template +Expected PosixTimeToDuration::cast( + Seconds seconds, + Subseconds subseconds) { + static_assert( + Tgt::period::num != 1, "should use special-case code when num==1"); + static_assert( + Tgt::period::den != 1, "should use special-case code when den==1"); + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + // TODO: We need to implement an overflow-checking tryTo() function for + // duration-to-duration casts for the code above to work. + // + // For now this is unimplemented, and we just have a static_assert that + // will always fail if someone tries to instantiate this. Unusual duration + // types should be extremely rare, and I'm not aware of any code at the + // moment that actually needs this. + static_assert( + Tgt::period::num == 1, + "conversion to unusual duration types is not implemented yet"); + (void)seconds; + (void)subseconds; + return makeUnexpected(ConversionCode::SUCCESS); +} + +template < + typename Tgt, + typename SubsecondRatio, + typename Seconds, + typename Subseconds> +Expected tryPosixTimeToDuration( + Seconds seconds, + Subseconds subseconds) { + static_assert( + SubsecondRatio::num == 1, "subsecond numerator should always be 1"); + + // Normalize the input if required + if (UNLIKELY(subseconds < 0)) { + const auto overflowSeconds = (subseconds / SubsecondRatio::den); + const auto remainder = (subseconds % SubsecondRatio::den); + if (std::numeric_limits::lowest() + 1 - overflowSeconds > + seconds) { + return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW); + } + seconds = seconds - 1 + overflowSeconds; + subseconds = remainder + SubsecondRatio::den; + } else if (UNLIKELY(subseconds >= SubsecondRatio::den)) { + const auto overflowSeconds = (subseconds / SubsecondRatio::den); + const auto remainder = (subseconds % SubsecondRatio::den); + if (std::numeric_limits::max() - overflowSeconds < seconds) { + return makeUnexpected(ConversionCode::POSITIVE_OVERFLOW); + } + seconds += overflowSeconds; + subseconds = remainder; + } + + using Converter = PosixTimeToDuration; + return Converter::template cast(seconds, subseconds); +} + +} // namespace detail + +/** + * struct timespec to std::chrono::duration + */ +template +typename std::enable_if< + detail::is_duration::value, + Expected>::type +tryTo(const struct timespec& ts) { + return detail::tryPosixTimeToDuration(ts.tv_sec, ts.tv_nsec); +} + +/** + * struct timeval to std::chrono::duration + */ +template +typename std::enable_if< + detail::is_duration::value, + Expected>::type +tryTo(const struct timeval& tv) { + return detail::tryPosixTimeToDuration(tv.tv_sec, tv.tv_usec); +} + +/** + * timespec or timeval to std::chrono::time_point + */ +template +typename std::enable_if< + detail::is_time_point::value && detail::is_posix_time_type::value, + Expected>::type +tryTo(const Src& value) { + return tryTo(value).then( + [](typename Tgt::duration result) { return Tgt(result); }); +} + +/** + * std::chrono::duration to struct timespec + */ +template +typename std::enable_if< + std::is_same::value, + Expected>::type +tryTo(const std::chrono::duration& duration) { + auto result = detail::durationToPosixTime(duration); + if (result.hasError()) { + return makeUnexpected(result.error()); + } + + struct timespec ts; + ts.tv_sec = result.value().first; + ts.tv_nsec = result.value().second; + return ts; +} + +/** + * std::chrono::duration to struct timeval + */ +template +typename std::enable_if< + std::is_same::value, + Expected>::type +tryTo(const std::chrono::duration& duration) { + auto result = detail::durationToPosixTime(duration); + if (result.hasError()) { + return makeUnexpected(result.error()); + } + + struct timeval tv; + tv.tv_sec = result.value().first; + tv.tv_usec = result.value().second; + return tv; +} + +/** + * std::chrono::time_point to timespec or timeval + */ +template +typename std::enable_if< + detail::is_posix_time_type::value, + Expected>::type +tryTo(const std::chrono::time_point& timePoint) { + return tryTo(timePoint.time_since_epoch()); +} + +/** + * For all chrono conversions, to() wraps tryTo() + */ +template +typename std::enable_if::value, Tgt>:: + type + to(const Src& value) { + return tryTo(value).thenOrThrow( + [](Tgt res) { return res; }, + [&](ConversionCode e) { return makeConversionError(e, StringPiece{}); }); +} + +} // namespace folly diff --git a/folly/chrono/test/ConvTest.cpp b/folly/chrono/test/ConvTest.cpp new file mode 100644 index 00000000..172bdd67 --- /dev/null +++ b/folly/chrono/test/ConvTest.cpp @@ -0,0 +1,446 @@ +/* + * Copyright 2004-present Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include + +using namespace folly; +using namespace std::chrono; +using namespace std::chrono_literals; + +namespace { +/** + * A helper function to create a time_point even if the input duration type has + * finer resolution than the clock duration type. + */ +template +typename Clock::time_point createTimePoint(const Duration& d) { + return typename Clock::time_point( + std::chrono::duration_cast(d)); +} +} // namespace + +TEST(Conv, timespecToStdChrono) { + struct timespec ts; + + ts.tv_sec = 0; + ts.tv_nsec = 10; + EXPECT_EQ(10ns, to(ts)); + EXPECT_EQ(0us, to(ts)); + EXPECT_EQ(0ms, to(ts)); + EXPECT_EQ(0s, to(ts)); + + ts.tv_sec = 1; + ts.tv_nsec = 10; + EXPECT_EQ(1000000010ns, to(ts)); + EXPECT_EQ(1000000us, to(ts)); + EXPECT_EQ(1000ms, to(ts)); + EXPECT_EQ(1s, to(ts)); + EXPECT_EQ( + createTimePoint(1000000010ns), + to(ts)); + EXPECT_EQ( + createTimePoint(1000000010ns), + to(ts)); + + // Test a non-canonical value with tv_nsec larger than 1 second + ts.tv_sec = 5; + ts.tv_nsec = 3219876543; + // Beware about using std::chrono_literals suffixes with very literals: + // older versions of GCC are buggy and would truncate these to 32-bits. + EXPECT_EQ(8219876543LL, to(ts).count()); + EXPECT_EQ(8219876us, to(ts)); + EXPECT_EQ(8219ms, to(ts)); + EXPECT_EQ(8s, to(ts)); + EXPECT_EQ( + createTimePoint(nanoseconds(8219876543LL)), + to(ts)); + EXPECT_EQ( + createTimePoint(nanoseconds(8219876543LL)), + to(ts)); + + // Test negative values + // When going to coarser grained types these should be rounded up towards 0. + ts.tv_sec = -5; + ts.tv_nsec = 123456; + EXPECT_EQ(-4999876544, to(ts).count()); + EXPECT_EQ(-4999876544, duration_cast(-5s + 123456ns).count()); + EXPECT_EQ(-4999876, to(ts).count()); + EXPECT_EQ(-4999876, duration_cast(-5s + 123456ns).count()); + EXPECT_EQ(-4999, to(ts).count()); + EXPECT_EQ(-4999, duration_cast(-5s + 123456ns).count()); + EXPECT_EQ(-4s, to(ts)); + EXPECT_EQ(-4, duration_cast(-5s + 123456ns).count()); + ts.tv_sec = -7200; + ts.tv_nsec = 123456; + EXPECT_EQ(-1h, to(ts)); + EXPECT_EQ( + -1, + duration_cast(seconds{ts.tv_sec} + nanoseconds{ts.tv_nsec}) + .count()); + ts.tv_sec = -7000; + ts.tv_nsec = 123456; + EXPECT_EQ(-1h, to(ts)); + EXPECT_EQ( + -1, + duration_cast(seconds{ts.tv_sec} + nanoseconds{ts.tv_nsec}) + .count()); + ts.tv_sec = -7201; + ts.tv_nsec = 123456; + EXPECT_EQ(-2h, to(ts)); + EXPECT_EQ( + -2, + duration_cast(seconds{ts.tv_sec} + nanoseconds{ts.tv_nsec}) + .count()); + + // Test converions to floating point durations + ts.tv_sec = 1; + ts.tv_nsec = 500000000; + EXPECT_EQ(1.5, to>(ts).count()); + ts.tv_sec = -1; + ts.tv_nsec = 500000000; + EXPECT_EQ(-0.5, to>(ts).count()); + ts.tv_sec = -1; + ts.tv_nsec = -500000000; + EXPECT_EQ(-1.5, to>(ts).count()); + ts.tv_sec = 1; + ts.tv_nsec = 500000000; + auto doubleNanos = to>(ts); + EXPECT_EQ(1500000000, doubleNanos.count()); + ts.tv_sec = 90; + ts.tv_nsec = 0; + auto doubleMinutes = to>>(ts); + EXPECT_EQ(1.5, doubleMinutes.count()); +} + +TEST(Conv, timespecToStdChronoOverflow) { + struct timespec ts; + + // All of our boundary conditions below assume time_t is int64_t. + // This is true on most modern platforms. + if (!std::is_same::value) { + LOG(INFO) << "skipping most overflow tests: time_t is not int64_t"; + } else { + // Test the upper boundary of conversion to uint64_t nanoseconds + using nsec_u64 = std::chrono::duration; + ts.tv_sec = 18446744073; + ts.tv_nsec = 709551615; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + + ts.tv_nsec += 1; + EXPECT_THROW(to(ts), std::range_error); + + // Test the lower boundary of conversion to uint64_t nanoseconds + ts.tv_sec = 0; + ts.tv_nsec = 0; + EXPECT_EQ(0, to(ts).count()); + ts.tv_sec = -1; + ts.tv_nsec = 0; + EXPECT_THROW(to(ts), std::range_error); + + // Test the upper boundary of conversion to int64_t microseconds + using usec_i64 = std::chrono::duration; + ts.tv_sec = 9223372036854LL; + ts.tv_nsec = 775807000; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + + ts.tv_nsec += 1; + EXPECT_THROW(to(ts), std::range_error); + + // Test the lower boundary of conversion to int64_t microseconds + ts.tv_sec = -9223372036855LL; + ts.tv_nsec = 224192000; + EXPECT_EQ(std::numeric_limits::min(), to(ts).count()); + + ts.tv_nsec -= 1; + EXPECT_THROW(to(ts), std::range_error); + + // Test the boundaries of conversion to int32_t seconds + using sec_i32 = std::chrono::duration; + ts.tv_sec = 2147483647; + ts.tv_nsec = 0; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + ts.tv_nsec = 1000000000; + EXPECT_THROW(to(ts), std::range_error); + ts.tv_sec = -2147483648; + ts.tv_nsec = 0; + EXPECT_EQ(std::numeric_limits::min(), to(ts).count()); + ts.tv_sec = -2147483649; + ts.tv_nsec = 999999999; + EXPECT_THROW(to(ts), std::range_error); + ts.tv_sec = -2147483649; + ts.tv_nsec = 0; + EXPECT_THROW(to(ts), std::range_error); + ts.tv_sec = -2147483650; + ts.tv_nsec = 0; + EXPECT_THROW(to(ts), std::range_error); + + // Test the upper boundary of conversion to uint32_t hours + using hours_u32 = std::chrono::duration>; + ts.tv_sec = 15461882262000LL; + ts.tv_nsec = 0; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + ts.tv_sec = 15461882265599LL; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + ts.tv_sec = 15461882265600LL; + EXPECT_THROW(to(ts), std::range_error); + + using nsec_i64 = std::chrono::duration; + ts.tv_sec = std::numeric_limits::max(); + ts.tv_nsec = std::numeric_limits::max(); + EXPECT_THROW(to(ts), std::range_error); + + ts.tv_sec = std::numeric_limits::min(); + ts.tv_nsec = std::numeric_limits::min(); + EXPECT_THROW(to(ts), std::range_error); + + // Test some non-normal inputs near the int64_t limit + ts.tv_sec = 0; + ts.tv_nsec = std::numeric_limits::min(); + EXPECT_EQ(std::numeric_limits::min(), to(ts).count()); + ts.tv_sec = -1; + ts.tv_nsec = std::numeric_limits::min() + std::nano::den; + EXPECT_EQ(std::numeric_limits::min(), to(ts).count()); + ts.tv_sec = -1; + ts.tv_nsec = std::numeric_limits::min() + std::nano::den - 1; + EXPECT_THROW(to(ts), std::range_error); + + ts.tv_sec = 0; + ts.tv_nsec = std::numeric_limits::max(); + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + ts.tv_sec = 1; + ts.tv_nsec = std::numeric_limits::max() - std::nano::den; + EXPECT_EQ(std::numeric_limits::max(), to(ts).count()); + ts.tv_sec = 1; + ts.tv_nsec = std::numeric_limits::max() - std::nano::den + 1; + EXPECT_THROW(to(ts), std::range_error); + } + + // Theoretically conversion is representable in the output type, + // but we normalize the input first, and normalization would trigger an + // overflow. + using hours_u64 = std::chrono::duration>; + ts.tv_sec = std::numeric_limits::max(); + ts.tv_nsec = 1000000000; + EXPECT_THROW(to(ts), std::range_error); + // If we drop it back down to the normal range it should succeed + ts.tv_nsec = 999999999; + EXPECT_EQ( + std::numeric_limits::max() / 3600, + to(ts).count()); +} + +TEST(Conv, timevalToStdChrono) { + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = 10; + EXPECT_EQ(10000ns, to(tv)); + EXPECT_EQ(10us, to(tv)); + EXPECT_EQ(0ms, to(tv)); + EXPECT_EQ(0s, to(tv)); + + tv.tv_sec = 1; + tv.tv_usec = 10; + EXPECT_EQ(1000010000ns, to(tv)); + EXPECT_EQ(1000010us, to(tv)); + EXPECT_EQ(1000ms, to(tv)); + EXPECT_EQ(1s, to(tv)); + EXPECT_EQ( + createTimePoint(1000010000ns), + to(tv)); + EXPECT_EQ( + createTimePoint(1000010000ns), + to(tv)); + + // Test a non-canonical value with tv_usec larger than 1 second + tv.tv_sec = 5; + tv.tv_usec = 3219876; + EXPECT_EQ(8219876000LL, to(tv).count()); + EXPECT_EQ(8219876us, to(tv)); + EXPECT_EQ(8219ms, to(tv)); + EXPECT_EQ(8s, to(tv)); + EXPECT_EQ( + createTimePoint(nanoseconds(8219876000LL)), + to(tv)); + EXPECT_EQ( + createTimePoint(nanoseconds(8219876000LL)), + to(tv)); + + // Test for overflow. + if (std::numeric_limits::max() >= + std::numeric_limits::max()) { + // Use our own type alias here rather than std::chrono::nanoseconds + // to ensure we have 64-bit rep type. + using nsec_i64 = std::chrono::duration; + tv.tv_sec = std::numeric_limits::max(); + tv.tv_usec = std::numeric_limits::max(); + EXPECT_THROW(to(tv), std::range_error); + + tv.tv_sec = std::numeric_limits::min(); + tv.tv_usec = std::numeric_limits::max(); + EXPECT_THROW(to(tv), std::range_error); + } +} + +TEST(Conv, stdChronoToTimespec) { + auto ts = to(10ns); + EXPECT_EQ(0, ts.tv_sec); + EXPECT_EQ(10, ts.tv_nsec); + + // We don't use std::chrono_literals suffixes here since older + // gcc versions silently truncate the literals to 32-bits. + ts = to(nanoseconds(987654321012LL)); + EXPECT_EQ(987, ts.tv_sec); + EXPECT_EQ(654321012, ts.tv_nsec); + + ts = to(nanoseconds(-987654321012LL)); + EXPECT_EQ(-988, ts.tv_sec); + EXPECT_EQ(345678988, ts.tv_nsec); + + ts = to(microseconds(987654321012LL)); + EXPECT_EQ(987654, ts.tv_sec); + EXPECT_EQ(321012000, ts.tv_nsec); + + ts = to(milliseconds(987654321012LL)); + EXPECT_EQ(987654321, ts.tv_sec); + EXPECT_EQ(12000000, ts.tv_nsec); + + ts = to(seconds(987654321012LL)); + EXPECT_EQ(987654321012, ts.tv_sec); + EXPECT_EQ(0, ts.tv_nsec); + + ts = to(10h); + EXPECT_EQ(36000, ts.tv_sec); + EXPECT_EQ(0, ts.tv_nsec); + + ts = to(createTimePoint(123ns)); + EXPECT_EQ(0, ts.tv_sec); + EXPECT_EQ(123, ts.tv_nsec); + + ts = to(createTimePoint(123ns)); + EXPECT_EQ(0, ts.tv_sec); + EXPECT_EQ(123, ts.tv_nsec); +} + +TEST(Conv, stdChronoToTimespecOverflow) { + EXPECT_THROW(to(1234), std::range_error); + + struct timespec ts; + if (!std::is_same::value) { + LOG(INFO) << "skipping most overflow tests: time_t is not int64_t"; + } else { + // Check for overflow converting from uint64_t seconds to time_t + using sec_u64 = duration; + ts = to(sec_u64(9223372036854775807ULL)); + EXPECT_EQ(ts.tv_sec, 9223372036854775807ULL); + EXPECT_EQ(ts.tv_nsec, 0); + + EXPECT_THROW( + to(sec_u64(9223372036854775808ULL)), std::range_error); + + // Check for overflow converting from int64_t hours to time_t + using hours_i64 = duration>; + ts = to(hours_i64(2562047788015215LL)); + EXPECT_EQ(ts.tv_sec, 9223372036854774000LL); + EXPECT_EQ(ts.tv_nsec, 0); + EXPECT_THROW( + to(hours_i64(2562047788015216LL)), std::range_error); + } + + // Test for overflow. + // Use a custom hours type using time_t as the underlying storage type to + // guarantee that we can overflow. + using hours_timet = std::chrono::duration>; + EXPECT_THROW( + to(hours_timet(std::numeric_limits::max())), + std::range_error); +} + +TEST(Conv, stdChronoToTimeval) { + auto tv = to(10ns); + EXPECT_EQ(0, tv.tv_sec); + EXPECT_EQ(0, tv.tv_usec); + + tv = to(10us); + EXPECT_EQ(0, tv.tv_sec); + EXPECT_EQ(10, tv.tv_usec); + + tv = to(nanoseconds(987654321012LL)); + EXPECT_EQ(987, tv.tv_sec); + EXPECT_EQ(654321, tv.tv_usec); + + tv = to(nanoseconds(-987654321012LL)); + EXPECT_EQ(-988, tv.tv_sec); + EXPECT_EQ(345679, tv.tv_usec); + + tv = to(microseconds(987654321012LL)); + EXPECT_EQ(987654, tv.tv_sec); + EXPECT_EQ(321012, tv.tv_usec); + + tv = to(milliseconds(987654321012LL)); + EXPECT_EQ(987654321, tv.tv_sec); + EXPECT_EQ(12000, tv.tv_usec); + + tv = to(seconds(987654321012LL)); + EXPECT_EQ(987654321012, tv.tv_sec); + EXPECT_EQ(0, tv.tv_usec); + + // Try converting fractional seconds + tv = to(duration{3.456789}); + EXPECT_EQ(3, tv.tv_sec); + EXPECT_EQ(456789, tv.tv_usec); + tv = to(duration{-3.456789}); + EXPECT_EQ(-4, tv.tv_sec); + EXPECT_EQ(543211, tv.tv_usec); + + // Try converting fractional hours + tv = to(duration>{3.456789}); + EXPECT_EQ(12444, tv.tv_sec); + // The usec field is generally off-by-one due to + // floating point rounding error + EXPECT_NEAR(440400, tv.tv_usec, 1); + tv = to(duration>{-3.456789}); + EXPECT_EQ(-12445, tv.tv_sec); + EXPECT_NEAR(559600, tv.tv_usec, 1); + + // Try converting fractional milliseconds + tv = to(duration{9123.456789}); + EXPECT_EQ(9, tv.tv_sec); + EXPECT_EQ(123456, tv.tv_usec); + tv = to(duration{-9123.456789}); + EXPECT_EQ(-10, tv.tv_sec); + EXPECT_NEAR(876544, tv.tv_usec, 1); + + tv = to(duration>{3}); + EXPECT_EQ(10800, tv.tv_sec); + EXPECT_EQ(0, tv.tv_usec); + + tv = to(duration{3123}); + EXPECT_EQ(0, tv.tv_sec); + EXPECT_EQ(3, tv.tv_usec); + tv = to(duration{-3123}); + EXPECT_EQ(-1, tv.tv_sec); + EXPECT_EQ(999997, tv.tv_usec); + + tv = to(createTimePoint(123us)); + EXPECT_EQ(0, tv.tv_sec); + EXPECT_EQ(123, tv.tv_usec); + + tv = to(createTimePoint(123us)); + EXPECT_EQ(0, tv.tv_sec); + EXPECT_EQ(123, tv.tv_usec); +} diff --git a/folly/chrono/test/Makefile.am b/folly/chrono/test/Makefile.am new file mode 100644 index 00000000..5ea95a0a --- /dev/null +++ b/folly/chrono/test/Makefile.am @@ -0,0 +1,12 @@ +ACLOCAL_AMFLAGS = -I m4 + +CPPFLAGS = -I$(top_srcdir)/test/gtest/googletest/include +ldadd = $(top_builddir)/test/libfollytestmain.la + +check_PROGRAMS = \ + conv_test + +TESTS = $(check_PROGRAMS) + +conv_test_SOURCES = ConvTest.cpp +conv_test_LDADD = $(ldadd) diff --git a/folly/configure.ac b/folly/configure.ac index 826972e2..0e6aa379 100644 --- a/folly/configure.ac +++ b/folly/configure.ac @@ -632,6 +632,7 @@ FB_FILTER_PKG_LIBS([$AM_LDFLAGS $LIBS]) # Output AC_CONFIG_FILES([Makefile + chrono/test/Makefile io/test/Makefile libfolly.pc test/Makefile