diff --git a/include/rapidcheck/gen/Numeric.h b/include/rapidcheck/gen/Numeric.h index 6b6d378d..be48af1f 100644 --- a/include/rapidcheck/gen/Numeric.h +++ b/include/rapidcheck/gen/Numeric.h @@ -5,7 +5,7 @@ namespace rc { namespace gen { -/// Generates an integer in a given range. +/// Generates an integer or floating point value in a given range. /// /// @param min The minimum value, inclusive. /// @param max The maximum value, exclusive. diff --git a/include/rapidcheck/gen/Numeric.hpp b/include/rapidcheck/gen/Numeric.hpp index 6954c973..61e05c41 100644 --- a/include/rapidcheck/gen/Numeric.hpp +++ b/include/rapidcheck/gen/Numeric.hpp @@ -6,6 +6,8 @@ #include "rapidcheck/gen/Transform.h" #include "rapidcheck/gen/detail/ScaleInteger.h" +#include + namespace rc { namespace gen { namespace detail { @@ -85,22 +87,25 @@ struct DefaultArbitrary { static Gen arbitrary() { return boolean; } }; -} // namespace detail +template +void assertValidRange(T min, T max) { + if (max <= min) { + std::string msg; + msg += "Invalid range [" + std::to_string(min); + msg += ", " + std::to_string(max) + ")"; + throw GenerationFailure(msg); + } +} template -Gen inRange(T min, T max) { +Gen inRange(T min, T max, std::false_type) { return [=](const Random &random, int size) { - if (max <= min) { - std::string msg; - msg += "Invalid range [" + std::to_string(min); - msg += ", " + std::to_string(max) + ")"; - throw GenerationFailure(msg); - } + assertValidRange(min, max); const auto rangeSize = - detail::scaleInteger(static_cast(max) - - static_cast(min) - 1, - size) + + scaleInteger(static_cast(max) - + static_cast(min) - 1, + size) + 1; const auto value = static_cast((Random(random).next() % rangeSize) + min); @@ -110,5 +115,49 @@ Gen inRange(T min, T max) { }; } +template +void assertFiniteRange(T min, T max) { + if (!std::isfinite(min) || !std::isfinite(max)) { + std::string msg; + msg += "Non-finite range [" + std::to_string(min); + msg += ", " + std::to_string(max) + ")"; + throw GenerationFailure(msg); + } +} + +template +Gen inRange(T min, T max, std::true_type) { + return [=](const Random &random, int size) { + assertValidRange(min, max); + assertFiniteRange(min, max); + + const auto unit = + static_cast(Random(random).next()) / + (static_cast(std::numeric_limits::max()) + 1); + // As for integrals, `size` decides how much of the range is used, with a + // size of zero yielding only `min`. + const auto scale = + std::min(size, kNominalSize) / static_cast(kNominalSize); + + // Interpolate the value and limit it to the range [min, max) + const auto t = unit * scale; + auto value = ((1 - t) * min) + (t * max); + if (value >= max) { + value = std::nextafter(max, min); + } + + assert(value >= min && value < max); + return shrinkable::shrinkRecur( + value, [=](T x) { return shrink::towards(x, min); }); + }; +} + +} // namespace detail + +template +Gen inRange(T min, T max) { + return detail::inRange(min, max, std::is_floating_point()); +} + } // namespace gen } // namespace rc diff --git a/include/rapidcheck/shrink/Shrink.h b/include/rapidcheck/shrink/Shrink.h index fd770a18..430b89e7 100644 --- a/include/rapidcheck/shrink/Shrink.h +++ b/include/rapidcheck/shrink/Shrink.h @@ -28,10 +28,11 @@ Seq removeChunks(Container elements); template Seq eachElement(Container elements, Shrink shrink); -/// Shrinks an integral value towards another integral value. +/// Shrinks an integral or floating point value towards another value of the +/// same type by repeated bisection. The target is always tried first. /// /// @param value The value to shrink. -/// @param target The integer to shrink towards. +/// @param target The value to shrink towards. template Seq towards(T value, T target); diff --git a/include/rapidcheck/shrink/Shrink.hpp b/include/rapidcheck/shrink/Shrink.hpp index 9ac4bc2e..78254628 100644 --- a/include/rapidcheck/shrink/Shrink.hpp +++ b/include/rapidcheck/shrink/Shrink.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -11,7 +12,7 @@ namespace rc { namespace shrink { namespace detail { -template +template class TowardsSeq { public: using UInt = typename std::make_unsigned::type; @@ -37,6 +38,35 @@ class TowardsSeq { bool m_down; }; +template +class TowardsSeq< + T, + typename std::enable_if::value>::type> { +public: + TowardsSeq(T value, T target) + : m_value(value) + , m_target(target) {} + + Maybe operator()() { + if (m_value == m_target || std::isnan(m_value) || std::isnan(m_target)) { + return Nothing; + } + + T new_value = (m_value / 2) + (m_target / 2); + + if (new_value == m_value || std::isnan(new_value)) { + new_value = m_target; + } + + m_value = new_value; + return m_value; + } + +private: + T m_value; + T m_target; +}; + template class RemoveChunksSeq { public: diff --git a/test/gen/NumericTests.cpp b/test/gen/NumericTests.cpp index 1a003bb7..94820fa9 100644 --- a/test/gen/NumericTests.cpp +++ b/test/gen/NumericTests.cpp @@ -174,16 +174,21 @@ TEST_CASE("arbitrary reals") { namespace { +template +Gen> genRangeOf() { + // TODO proper range generator + return gen::exec([] { + const auto a = *gen::arbitrary(); + const auto b = *gen::distinctFrom(a); + return std::make_pair(std::min(a, b), std::max(a, b)); + }); +} + struct InRangeProperties { template static void exec() { - // TODO proper range generator - static const auto genRange = gen::exec([] { - const auto a = *gen::arbitrary(); - const auto b = *gen::distinctFrom(a); - return std::make_pair(std::min(a, b), std::max(a, b)); - }); + static const auto genRange = genRangeOf(); templatedProp( "never generates values outside of range", @@ -211,6 +216,22 @@ struct InRangeProperties { GenerationFailure); }); + templatedProp("when size == 0, generates only min", + [](const Random &random) { + const auto range = *genRange; + RC_ASSERT( + gen::inRange(range.first, range.second)(random, 0) == + shrinkable::just(range.first)); + }); + } +}; + +struct InRangeIntegralProperties { + template + static void exec() { + + static const auto genRange = genRangeOf(); + templatedProp("first shrink is min", [](const GenParams ¶ms) { // TODO range generator @@ -249,14 +270,6 @@ struct InRangeProperties { RC_FAIL("Gave up"); }); - templatedProp("when size == 0, generates only min", - [](const Random &random) { - const auto range = *genRange; - RC_ASSERT( - gen::inRange(range.first, range.second)(random, 0) == - shrinkable::just(range.first)); - }); - templatedProp("finds shrink where value must be larger than some value", [](const Random &random) { const auto range = *genRange; @@ -273,8 +286,54 @@ struct InRangeProperties { } }; +struct InRangeRealProperties { + template + static void exec() { + + static const auto genRange = genRangeOf(); + + templatedProp( + "spreads values over the whole range", + [](const Random &random) { + // Unlike the integral case a continuous range cannot be covered + // exhaustively, so check that every decile gets hit instead. + const auto min = *gen::inRange(-1000, 1000); + const auto max = min + *gen::inRange(1, 1000); + + const auto gen = gen::inRange(min, max); + auto r = random; + std::vector counts(10, 0); + for (std::size_t i = 0; i < 100000; i++) { + const auto x = gen(r.split(), kNominalSize).value(); + const auto decile = static_cast( + (10 * (x - min)) / (max - min)); + counts[std::min(decile, 9)]++; + if (std::find(begin(counts), end(counts), 0) == end(counts)) { + RC_SUCCEED("All deciles generated"); + } + } + + RC_FAIL("Gave up"); + }); + + templatedProp( + "shrinks towards min", + [](const GenParams ¶ms) { + const auto range = *genRange; + const auto shrinkable = + gen::inRange(range.first, range.second)(params.random, + params.size); + const auto result = shrinkable::findLocalMin( + shrinkable, [](T) { return true; }); + RC_ASSERT(result.first == range.first); + }); + } +}; + } // namespace TEST_CASE("gen::inRange") { - forEachType(); + forEachType(); + forEachType(); + forEachType(); } diff --git a/test/shrink/ShrinkTests.cpp b/test/shrink/ShrinkTests.cpp index 6889deed..6f4b44eb 100644 --- a/test/shrink/ShrinkTests.cpp +++ b/test/shrink/ShrinkTests.cpp @@ -121,6 +121,28 @@ TEST_CASE("shrink::eachElement") { namespace { struct ShrinkTowardsProperties { + template + static void exec() { + templatedProp( + "shrinking towards self yields empty shrink", + [](T target) { RC_ASSERT(!shrink::towards(target, target).next()); }); + + templatedProp( + "never contains original value", + [](T x, T y) { RC_ASSERT(!seq::contains(shrink::towards(x, y), x)); }); + + templatedProp("never leaves the interval between target and value", + [](T target) { + T value = *gen::distinctFrom(target); + const auto lo = std::min(value, target); + const auto hi = std::max(value, target); + seq::forEach(shrink::towards(value, target), + [=](T x) { RC_ASSERT(x >= lo && x <= hi); }); + }); + } +}; + +struct ShrinkTowardsIntegralProperties { template static void exec() { templatedProp("first tries target immediately", @@ -142,21 +164,40 @@ struct ShrinkTowardsProperties { (value > target) ? (value - *fin) : (*fin - value); RC_ASSERT(diff == T(1)); }); + } +}; - templatedProp( - "shrinking towards self yields empty shrink", - [](T target) { RC_ASSERT(!shrink::towards(target, target).next()); }); +struct ShrinkTowardsRealProperties { + template + static void exec() { + templatedProp("each shrink is closer to the target than the last", + [](T target) { + T value = *gen::distinctFrom(target); + T previous = value; + seq::forEach(shrink::towards(value, target), + [&](T x) { + RC_ASSERT(std::abs(x - target) < + std::abs(previous - target)); + previous = x; + }); + }); - templatedProp( - "never contains original value", - [](T x, T y) { RC_ASSERT(!seq::contains(shrink::towards(x, y), x)); }); + templatedProp("reaches the target", + [](T target) { + T value = *gen::distinctFrom(target); + const auto fin = seq::last(shrink::towards(value, target)); + RC_ASSERT(fin); + RC_ASSERT(*fin == target); + }); } }; } // namespace TEST_CASE("shrink::towards") { - forEachType(); + forEachType(); + forEachType(); + forEachType(); } namespace {