Make the user-facing random Generators reproducible

Thanks to the new distributions, this is almost trivial change.
This commit is contained in:
Martin Hořeňovský 2023-12-10 16:24:46 +01:00
parent 28c66fdc5a
commit ae4fe16b81
No known key found for this signature in database
GPG Key ID: DE48307B8B0D381A
4 changed files with 85 additions and 17 deletions

View File

@ -84,9 +84,14 @@ and it is also generally repeatable across versions, but we might break
it from time to time. E.g. we broke repeatability with previous versions
in v2.13.4 so that test cases with similar names are shuffled better.
Random generators currently rely on platform's stdlib, specifically
the distributions from `<random>`. We thus provide no extra guarantee
above what your platform does. **Important: `<random>`'s distributions
Since Catch2 vX.Y.Z the random generators use custom distributions,
that should be repeatable across different platforms, with few caveats.
For details see the section on random generators in the [Generator
documentation](generators.md#random-number-generators-details).
Before this version, random generators relied on distributions from
platform's stdlib. We thus can provide no extra guarantee on top of the
ones given by your platform. **Important: `<random>`'s distributions
are not specified to be repeatable across different platforms.**

View File

@ -189,6 +189,31 @@ TEST_CASE("type conversion", "[generators]") {
}
```
### Random number generators: details
> This section applies from Catch2 vX.Y.Z. Before that, random generators
> were a thin wrapper around distributions from `<random>`.
All of the `random(a, b)` generators in Catch2 currently generate uniformly
distributed number in closed interval \[a; b\]. This is different from
`std::uniform_real_distribution`, which should return numbers in interval
\[a; b) (but due to rounding can end up returning b anyway), but the
difference is intentional, so that `random(a, a)` makes sense. If there is
enough interest from users, we can provide API to pick any of CC, CO, OC,
or OO ranges.
Unlike `std::uniform_int_distribution`, Catch2's generators also support
various single-byte integral types, such as `char` or `bool`.
Given the same seed, the output from the integral generators is
reproducible across different platforms. For floating point generators,
we only promise reproducibility on platforms that obey the IEEE 754
standard, and where `float` is 4 bytes and `double` is 8 bytes. We provide
no guarantees for `long double`, as the internals of `long double` can
vary wildly across different platforms.
## Generator interface
You can also implement your own generators, by deriving from the

View File

@ -7,7 +7,35 @@
// SPDX-License-Identifier: BSL-1.0
#include <catch2/generators/catch_generators_random.hpp>
#include <catch2/internal/catch_context.hpp>
std::uint32_t Catch::Generators::Detail::getSeed() { return sharedRng()(); }
#include <random>
namespace Catch {
namespace Generators {
namespace Detail {
std::uint32_t getSeed() { return sharedRng()(); }
} // namespace Detail
struct RandomFloatingGenerator<long double>::PImpl {
PImpl( long double a, long double b, uint32_t seed ):
rng( seed ), dist( a, b ) {}
Catch::SimplePcg32 rng;
std::uniform_real_distribution<long double> dist;
};
RandomFloatingGenerator<long double>::RandomFloatingGenerator(
long double a, long double b, std::uint32_t seed) :
m_pimpl(Catch::Detail::make_unique<PImpl>(a, b, seed)) {
static_cast<void>( next() );
}
RandomFloatingGenerator<long double>::~RandomFloatingGenerator() =
default;
bool RandomFloatingGenerator<long double>::next() {
m_current_number = m_pimpl->dist( m_pimpl->rng );
return true;
}
} // namespace Generators
} // namespace Catch

View File

@ -11,8 +11,9 @@
#include <catch2/internal/catch_context.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/internal/catch_random_number_generator.hpp>
#include <random>
#include <catch2/internal/catch_uniform_integer_distribution.hpp>
#include <catch2/internal/catch_uniform_floating_point_distribution.hpp>
#include <catch2/internal/catch_unique_ptr.hpp>
namespace Catch {
namespace Generators {
@ -26,7 +27,7 @@ namespace Detail {
template <typename Float>
class RandomFloatingGenerator final : public IGenerator<Float> {
Catch::SimplePcg32 m_rng;
std::uniform_real_distribution<Float> m_dist;
Catch::uniform_floating_point_distribution<Float> m_dist;
Float m_current_number;
public:
RandomFloatingGenerator( Float a, Float b, std::uint32_t seed ):
@ -44,10 +45,27 @@ public:
}
};
template <>
class RandomFloatingGenerator<long double> final : public IGenerator<long double> {
// We still rely on <random> for this specialization, but we don't
// want to drag it into the header.
struct PImpl;
Catch::Detail::unique_ptr<PImpl> m_pimpl;
long double m_current_number;
public:
RandomFloatingGenerator( long double a, long double b, std::uint32_t seed );
long double const& get() const override { return m_current_number; }
bool next() override;
~RandomFloatingGenerator() override; // = default
};
template <typename Integer>
class RandomIntegerGenerator final : public IGenerator<Integer> {
Catch::SimplePcg32 m_rng;
std::uniform_int_distribution<Integer> m_dist;
Catch::uniform_integer_distribution<Integer> m_dist;
Integer m_current_number;
public:
RandomIntegerGenerator( Integer a, Integer b, std::uint32_t seed ):
@ -68,14 +86,6 @@ public:
template <typename T>
std::enable_if_t<std::is_integral<T>::value, GeneratorWrapper<T>>
random(T a, T b) {
static_assert(
!std::is_same<T, char>::value &&
!std::is_same<T, int8_t>::value &&
!std::is_same<T, uint8_t>::value &&
!std::is_same<T, signed char>::value &&
!std::is_same<T, unsigned char>::value &&
!std::is_same<T, bool>::value,
"The requested type is not supported by the underlying random distributions from std" );
return GeneratorWrapper<T>(
Catch::Detail::make_unique<RandomIntegerGenerator<T>>(a, b, Detail::getSeed())
);