diff --git a/AK/Random.cpp b/AK/Random.cpp index fc5dd268ca..229ead9e6e 100644 --- a/AK/Random.cpp +++ b/AK/Random.cpp @@ -5,6 +5,8 @@ */ #include +#include +#include namespace AK { @@ -28,4 +30,16 @@ u32 get_random_uniform(u32 max_bounds) return random_value % max_bounds; } +u64 get_random_uniform_64(u64 max_bounds) +{ + // Uses the same algorithm as `get_random_uniform`, + // by replacing u64 with u128 and u32 with u64. + const u64 max_usable = UINT64_MAX - static_cast((static_cast(UINT64_MAX) + 1) % max_bounds); + auto random_value = get_random(); + for (int i = 0; i < 20 && random_value > max_usable; ++i) { + random_value = get_random(); + } + return random_value % max_bounds; +} + } diff --git a/AK/Random.h b/AK/Random.h index 4598652dce..c2badd3e6a 100644 --- a/AK/Random.h +++ b/AK/Random.h @@ -60,6 +60,7 @@ inline T get_random() } u32 get_random_uniform(u32 max_bounds); +u64 get_random_uniform_64(u64 max_bounds); template inline void shuffle(Collection& collection) diff --git a/Tests/AK/TestAllOf.cpp b/Tests/AK/TestAllOf.cpp index adfae35fcf..dd8bd16969 100644 --- a/Tests/AK/TestAllOf.cpp +++ b/Tests/AK/TestAllOf.cpp @@ -28,13 +28,13 @@ TEST_CASE(all_but_one_false) RANDOMIZED_TEST_CASE(trivial_all_true) { - GEN(vec, Gen::vector(0, 10, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(0, 10, []() { return Gen::number_u64(); })); EXPECT(all_of(vec.begin(), vec.end(), [](auto) { return true; })); } RANDOMIZED_TEST_CASE(trivial_all_false) { - GEN(vec, Gen::vector(1, 10, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(1, 10, []() { return Gen::number_u64(); })); EXPECT(!all_of(vec.begin(), vec.end(), [](auto) { return false; })); } diff --git a/Tests/AK/TestAnyOf.cpp b/Tests/AK/TestAnyOf.cpp index f8a9c04254..3aee2eea77 100644 --- a/Tests/AK/TestAnyOf.cpp +++ b/Tests/AK/TestAnyOf.cpp @@ -28,13 +28,13 @@ TEST_CASE(all_false) RANDOMIZED_TEST_CASE(trivial_all_true) { - GEN(vec, Gen::vector(1, 10, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(1, 10, []() { return Gen::number_u64(); })); EXPECT(any_of(vec.begin(), vec.end(), [](auto) { return true; })); } RANDOMIZED_TEST_CASE(trivial_all_false) { - GEN(vec, Gen::vector(0, 10, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(0, 10, []() { return Gen::number_u64(); })); EXPECT(!any_of(vec.begin(), vec.end(), [](auto) { return false; })); } diff --git a/Tests/AK/TestBinaryHeap.cpp b/Tests/AK/TestBinaryHeap.cpp index 6b21590a2d..89127e7bb9 100644 --- a/Tests/AK/TestBinaryHeap.cpp +++ b/Tests/AK/TestBinaryHeap.cpp @@ -70,35 +70,35 @@ TEST_CASE(large_populate_reverse) RANDOMIZED_TEST_CASE(pop_min_is_min) { - GEN(vec, Gen::vector(1, 10, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(1, 10, []() { return Gen::number_u64(); })); auto sorted { vec }; AK::quick_sort(sorted); - BinaryHeap heap; + BinaryHeap heap; // insert in a non-sorted order - for (u32 n : vec) { + for (u64 n : vec) { heap.insert(n, n); } // check in a sorted order - for (u32 sorted_n : sorted) { + for (u64 sorted_n : sorted) { EXPECT_EQ(heap.pop_min(), sorted_n); } } RANDOMIZED_TEST_CASE(peek_min_same_as_pop_min) { - GEN(vec, Gen::vector(1, 10, []() { return Gen::unsigned_int(); })); - BinaryHeap heap; - for (u32 n : vec) { + GEN(vec, Gen::vector(1, 10, []() { return Gen::number_u64(); })); + BinaryHeap heap; + for (u64 n : vec) { heap.insert(n, n); } while (!heap.is_empty()) { - u32 peeked = heap.peek_min(); - u32 popped = heap.pop_min(); + u64 peeked = heap.peek_min(); + u64 popped = heap.pop_min(); EXPECT_EQ(peeked, popped); } } diff --git a/Tests/AK/TestBinarySearch.cpp b/Tests/AK/TestBinarySearch.cpp index 4f41ac260f..fbed0862e1 100644 --- a/Tests/AK/TestBinarySearch.cpp +++ b/Tests/AK/TestBinarySearch.cpp @@ -122,10 +122,10 @@ TEST_CASE(unsigned_to_signed_regression) RANDOMIZED_TEST_CASE(finds_number_that_is_present) { - GEN(vec, Gen::vector(1, 16, []() { return Gen::unsigned_int(); })); - GEN(i, Gen::unsigned_int(0, vec.size() - 1)); + GEN(vec, Gen::vector(1, 16, []() { return Gen::number_u64(); })); + GEN(i, Gen::number_u64(0, vec.size() - 1)); AK::quick_sort(vec); - u32 n = vec[i]; + u64 n = vec[i]; auto ptr = binary_search(vec, n); EXPECT_NE(ptr, nullptr); EXPECT_EQ(*ptr, n); @@ -133,10 +133,10 @@ RANDOMIZED_TEST_CASE(finds_number_that_is_present) RANDOMIZED_TEST_CASE(doesnt_find_number_that_is_not_present) { - GEN(vec, Gen::vector(1, 16, []() { return Gen::unsigned_int(); })); + GEN(vec, Gen::vector(1, 16, []() { return Gen::number_u64(); })); AK::quick_sort(vec); - u32 not_present = 0; + u64 not_present = 0; while (!vec.find(not_present).is_end()) { ++not_present; } diff --git a/Tests/AK/TestBitStream.cpp b/Tests/AK/TestBitStream.cpp index 06a4383314..b1873a951c 100644 --- a/Tests/AK/TestBitStream.cpp +++ b/Tests/AK/TestBitStream.cpp @@ -201,7 +201,7 @@ TEST_CASE(bit_reads_beyond_stream_limits) RANDOMIZED_TEST_CASE(roundtrip_u8_little_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); LittleEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; @@ -216,7 +216,7 @@ RANDOMIZED_TEST_CASE(roundtrip_u8_little_endian) RANDOMIZED_TEST_CASE(roundtrip_u16_little_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); LittleEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; @@ -231,7 +231,7 @@ RANDOMIZED_TEST_CASE(roundtrip_u16_little_endian) RANDOMIZED_TEST_CASE(roundtrip_u32_little_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); LittleEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; @@ -246,7 +246,7 @@ RANDOMIZED_TEST_CASE(roundtrip_u32_little_endian) RANDOMIZED_TEST_CASE(roundtrip_u8_big_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); BigEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; @@ -260,7 +260,7 @@ RANDOMIZED_TEST_CASE(roundtrip_u8_big_endian) RANDOMIZED_TEST_CASE(roundtrip_u16_big_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); BigEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; @@ -274,7 +274,7 @@ RANDOMIZED_TEST_CASE(roundtrip_u16_big_endian) RANDOMIZED_TEST_CASE(roundtrip_u32_big_endian) { - GEN(n, Gen::unsigned_int(NumericLimits::max())); + GEN(n, Gen::number_u64(NumericLimits::max())); auto memory_stream = make(); BigEndianOutputBitStream sut_write { MaybeOwned(*memory_stream) }; diff --git a/Tests/AK/TestBitmap.cpp b/Tests/AK/TestBitmap.cpp index b0727c4c70..5d6d90c32b 100644 --- a/Tests/AK/TestBitmap.cpp +++ b/Tests/AK/TestBitmap.cpp @@ -283,8 +283,8 @@ RANDOMIZED_TEST_CASE(set_get) { GEN(init, Gen::boolean()); GEN(new_value, Gen::boolean()); - GEN(size, Gen::unsigned_int(1, 64)); - GEN(i, Gen::unsigned_int(size - 1)); + GEN(size, Gen::number_u64(1, 64)); + GEN(i, Gen::number_u64(size - 1)); auto bitmap = MUST(Bitmap::create(size, init)); bitmap.set(i, new_value); @@ -295,11 +295,11 @@ RANDOMIZED_TEST_CASE(set_get) RANDOMIZED_TEST_CASE(set_range) { GEN(init, Gen::boolean()); - GEN(size, Gen::unsigned_int(1, 64)); + GEN(size, Gen::number_u64(1, 64)); GEN(new_value, Gen::boolean()); - GEN(start, Gen::unsigned_int(size - 1)); - GEN(len, Gen::unsigned_int(size - start - 1)); + GEN(start, Gen::number_u64(size - 1)); + GEN(len, Gen::number_u64(size - start - 1)); auto bitmap = MUST(Bitmap::create(size, init)); bitmap.set_range(start, len, new_value); @@ -313,7 +313,7 @@ RANDOMIZED_TEST_CASE(set_range) RANDOMIZED_TEST_CASE(fill) { GEN(init, Gen::boolean()); - GEN(size, Gen::unsigned_int(1, 64)); + GEN(size, Gen::number_u64(1, 64)); GEN(new_value, Gen::boolean()); auto bitmap = MUST(Bitmap::create(size, init)); @@ -334,11 +334,11 @@ TEST_CASE(find_one_anywhere_edge_case) RANDOMIZED_TEST_CASE(find_one_anywhere) { GEN(init, Gen::boolean()); - GEN(size, Gen::unsigned_int(1, 64)); - GEN(hint, Gen::unsigned_int(size - 1)); + GEN(size, Gen::number_u64(1, 64)); + GEN(hint, Gen::number_u64(size - 1)); GEN(new_value, Gen::boolean()); - GEN(i, Gen::unsigned_int(size - 1)); + GEN(i, Gen::number_u64(size - 1)); auto bitmap = MUST(Bitmap::create(size, init)); bitmap.set(i, new_value); @@ -363,10 +363,10 @@ TEST_CASE(find_first_edge_case) RANDOMIZED_TEST_CASE(find_first) { GEN(init, Gen::boolean()); - GEN(size, Gen::unsigned_int(1, 64)); + GEN(size, Gen::number_u64(1, 64)); GEN(new_value, Gen::boolean()); - GEN(i, Gen::unsigned_int(size - 1)); + GEN(i, Gen::number_u64(size - 1)); auto bitmap = MUST(Bitmap::create(size, init)); bitmap.set(i, new_value); diff --git a/Tests/AK/TestBuiltinWrappers.cpp b/Tests/AK/TestBuiltinWrappers.cpp index 44d8e9d09e..fa809fcd86 100644 --- a/Tests/AK/TestBuiltinWrappers.cpp +++ b/Tests/AK/TestBuiltinWrappers.cpp @@ -72,13 +72,13 @@ RANDOMIZED_TEST_CASE(count_leading_zeroes) { // count_leading_zeroes(0b000...0001000...000) // == count_leading_zeroes(0b000...0001___...___) (where _ is 0 or 1) - GEN(e, Gen::unsigned_int(0, 63)); + GEN(e, Gen::number_u64(0, 63)); auto power_of_two = 1ULL << e; // We add random one-bits below the leftmost (and only) one-bit. // This shouldn't change the output of count_leading_zeroes because // the function should only care about the most significant one. - GEN(below, Gen::unsigned_int(0, power_of_two - 1)); + GEN(below, Gen::number_u64(0, power_of_two - 1)); auto n = power_of_two + below; EXPECT_EQ(count_leading_zeroes(n), count_leading_zeroes(power_of_two)); @@ -89,7 +89,7 @@ RANDOMIZED_TEST_CASE(count_required_bits) // count_required_bits(n) == log2(n) + 1 // log2(0) is -infinity, we don't care about that - GEN(n, Gen::unsigned_int(1, NumericLimits::max())); + GEN(n, Gen::number_u32(1, NumericLimits::max())); size_t expected = AK::log2(static_cast(n)) + 1; @@ -99,6 +99,6 @@ RANDOMIZED_TEST_CASE(count_required_bits) RANDOMIZED_TEST_CASE(bit_scan_forward_count_trailing_zeroes) { // Behaviour for 0 differs, so we skip it. - GEN(n, Gen::unsigned_int(1, 1 << 31)); + GEN(n, Gen::number_u32(1, 1 << 31)); EXPECT_EQ(bit_scan_forward(n), count_trailing_zeroes(n) + 1); } diff --git a/Tests/AK/TestCharacterTypes.cpp b/Tests/AK/TestCharacterTypes.cpp index 1f64f77ae4..2e4d26767f 100644 --- a/Tests/AK/TestCharacterTypes.cpp +++ b/Tests/AK/TestCharacterTypes.cpp @@ -42,7 +42,7 @@ void randomized_compare_bool_output_over(u32 range, auto& old_function, auto& ne for (u32 n = 0; n < 100; ++n) { bool result1 = false; bool result2 = false; - GEN(i, Gen::unsigned_int(range - 1)); + GEN(i, Gen::number_u64(range - 1)); EXPECT_EQ(result1 = (old_function(i) > 0), result2 = (new_function(i) > 0)); if (result1 != result2) FAIL(String::formatted("New result {} does not match old result {} for input {}.", result1, result2, i)); @@ -55,7 +55,7 @@ void randomized_compare_value_output_over(u32 range, auto& old_function, auto& n for (u32 n = 0; n < 100; ++n) { i64 result1 = false; i64 result2 = false; - GEN(i, Gen::unsigned_int(range - 1)); + GEN(i, Gen::number_u64(range - 1)); EXPECT_EQ(result1 = old_function(i), result2 = new_function(i)); if (result1 != result2) FAIL(String::formatted("New result {} does not match old result {} for input {}.", result1, result2, i)); diff --git a/Tests/LibTest/TestGenerator.cpp b/Tests/LibTest/TestGenerator.cpp index efc1a72ee3..8c0a573bb1 100644 --- a/Tests/LibTest/TestGenerator.cpp +++ b/Tests/LibTest/TestGenerator.cpp @@ -7,46 +7,47 @@ #include #include #include +#include using namespace Test::Randomized; -RANDOMIZED_TEST_CASE(unsigned_int_max_bounds) +RANDOMIZED_TEST_CASE(number_u64_max_bounds) { - GEN(n, Gen::unsigned_int(10)); + GEN(n, Gen::number_u64(10)); EXPECT(n <= 10); } -RANDOMIZED_TEST_CASE(unsigned_int_min_max_bounds) +RANDOMIZED_TEST_CASE(number_u64_min_max_bounds) { - GEN(n, Gen::unsigned_int(3, 6)); + GEN(n, Gen::number_u64(3, 6)); EXPECT(n >= 3 && n <= 6); } RANDOMIZED_TEST_CASE(assume) { - GEN(n, Gen::unsigned_int(10)); + GEN(n, Gen::number_u64(10)); ASSUME(n % 2 == 0); // This will try to generate until it finds an even number EXPECT(n % 2 == 0); // This will then succeed // It will give up if the value doesn't pass the ASSUME(...) predicate 15 times in a row. } -// TODO find a way to test that a test "unsigned_int(3) can't reach 0" fails -// TODO find a way to test that a test "unsigned_int(3) can't reach 3" fails -// TODO find a way to test that a test "unsigned_int(3,6) can't reach 3" fails -// TODO find a way to test that a test "unsigned_int(3,6) can't reach 6" fails -// TODO find a way to test that a test "unsigned_int(10) can reach n>10" fails +// TODO find a way to test that a test "number_u64(3) can't reach 0" fails +// TODO find a way to test that a test "number_u64(3) can't reach 3" fails +// TODO find a way to test that a test "number_u64(3,6) can't reach 3" fails +// TODO find a way to test that a test "number_u64(3,6) can't reach 6" fails +// TODO find a way to test that a test "number_u64(10) can reach n>10" fails RANDOMIZED_TEST_CASE(map_like) { - GEN(n1, Gen::unsigned_int(10)); + GEN(n1, Gen::number_u64(10)); GEN(n2, n1 * 2); EXPECT(n2 % 2 == 0); } RANDOMIZED_TEST_CASE(bind_like) { - GEN(n1, Gen::unsigned_int(1, 9)); - GEN(n2, Gen::unsigned_int(n1 * 10, n1 * 100)); + GEN(n1, Gen::number_u64(1, 9)); + GEN(n2, Gen::number_u64(n1 * 10, n1 * 100)); EXPECT(n2 >= 10 && n2 <= 900); } @@ -68,7 +69,7 @@ RANDOMIZED_TEST_CASE(bind_like) template Vector> vector_suboptimal(FN item_gen) { - u32 length = Gen::unsigned_int(5); + u32 length = Gen::number_u64(5); Vector> acc; for (u32 i = 0; i < length; ++i) { acc.append(item_gen()); @@ -79,7 +80,7 @@ Vector> vector_suboptimal(FN item_gen) RANDOMIZED_TEST_CASE(bind_vector_suboptimal) { u32 max_item = 5; - GEN(vec, vector_suboptimal([&]() { return Gen::unsigned_int(max_item); })); + GEN(vec, vector_suboptimal([&]() { return Gen::number_u64(max_item); })); u32 sum = 0; for (u32 n : vec) { sum += n; @@ -90,21 +91,21 @@ RANDOMIZED_TEST_CASE(bind_vector_suboptimal) RANDOMIZED_TEST_CASE(vector) { u32 max_item = 5; - GEN(vec, Gen::vector([&]() { return Gen::unsigned_int(max_item); })); + GEN(vec, Gen::vector([&]() { return Gen::number_u64(max_item); })); EXPECT(vec.size() <= 32); } RANDOMIZED_TEST_CASE(vector_length) { u32 max_item = 5; - GEN(vec, Gen::vector(3, [&]() { return Gen::unsigned_int(max_item); })); + GEN(vec, Gen::vector(3, [&]() { return Gen::number_u64(max_item); })); EXPECT(vec.size() == 3); } RANDOMIZED_TEST_CASE(vector_min_max) { u32 max_item = 5; - GEN(vec, Gen::vector(1, 4, [&]() { return Gen::unsigned_int(max_item); })); + GEN(vec, Gen::vector(1, 4, [&]() { return Gen::number_u64(max_item); })); EXPECT(vec.size() >= 1 && vec.size() <= 4); } @@ -160,9 +161,57 @@ RANDOMIZED_TEST_CASE(boolean_false) EXPECT(b == false); } +RANDOMIZED_TEST_CASE(one_of_int) +{ + GEN(x, Gen::one_of(1, 2)); + EXPECT(x == 1 || x == 2); +} + RANDOMIZED_TEST_CASE(frequency_int) { GEN(x, Gen::frequency(Gen::Choice { 5, 'x' }, Gen::Choice { 1, 'o' })); ASSUME(x == 'x'); EXPECT(x == 'x'); } + +RANDOMIZED_TEST_CASE(percentage) +{ + GEN(x, Gen::percentage()); + EXPECT(x >= 0 && x <= 1); +} + +RANDOMIZED_TEST_CASE(number_f64_max_bounds) +{ + GEN(x, Gen::number_f64(10)); + EXPECT(x <= 10); +} + +RANDOMIZED_TEST_CASE(number_f64_min_max_bounds) +{ + GEN(x, Gen::number_f64(-10, 10)); + EXPECT(x >= -10 && x <= 10); +} + +RANDOMIZED_TEST_CASE(number_f64_never_nan) +{ + GEN(x, Gen::number_f64()); + EXPECT(!isnan(x)); +} + +RANDOMIZED_TEST_CASE(number_f64_never_infinite) +{ + GEN(x, Gen::number_f64()); + EXPECT(!isinf(x)); +} + +RANDOMIZED_TEST_CASE(number_u32_max_bounds) +{ + GEN(n, Gen::number_u32(10)); + EXPECT(n <= 10); +} + +RANDOMIZED_TEST_CASE(number_u32_min_max_bounds) +{ + GEN(n, Gen::number_u32(3, 6)); + EXPECT(n >= 3 && n <= 6); +} diff --git a/Userland/Libraries/LibTest/Randomized/Generator.h b/Userland/Libraries/LibTest/Randomized/Generator.h index 82bde2c793..a15bddd7c7 100644 --- a/Userland/Libraries/LibTest/Randomized/Generator.h +++ b/Userland/Libraries/LibTest/Randomized/Generator.h @@ -15,22 +15,24 @@ #include #include +#include + namespace Test { namespace Randomized { // Returns a random double value in range 0..1. -inline double get_random_probability() +// This is not a generator. It is meant to be used inside RandomnessSource::draw_value(). +// Based on: https://dotat.at/@/2023-06-23-random-double.html +inline f64 get_random_probability() { - static constexpr u32 max_u32 = NumericLimits::max(); - u32 random_u32 = AK::get_random_uniform(max_u32); - return static_cast(random_u32) / static_cast(max_u32); + return static_cast(AK::get_random() >> 11) * 0x1.0p-53; } // Generators take random bits from the RandomnessSource and return a value // back. // // Example: -// - Gen::u32(5,10) --> 9, 7, 5, 10, 8, ... +// - Gen::number_u64(5,10) --> 9, 7, 5, 10, 8, ... namespace Gen { // An unsigned integer generator. @@ -38,41 +40,41 @@ namespace Gen { // The minimum value will always be 0. // The maximum value is given by user in the argument. // -// Gen::unsigned_int(10) -> value 5, RandomRun [5] -// -> value 8, RandomRun [8] -// etc. +// Gen::number_u64(10) -> value 5, RandomRun [5] +// -> value 8, RandomRun [8] +// etc. // // Shrinks towards 0. -inline u32 unsigned_int(u32 max) +inline u64 number_u64(u64 max) { if (max == 0) return 0; - u32 random = Test::randomness_source().draw_value(max, [&]() { - // `clamp` to guard against integer overflow and calling get_random_uniform(0). - u32 exclusive_bound = AK::clamp(max + 1, max, NumericLimits::max()); - return AK::get_random_uniform(exclusive_bound); + u64 random = Test::randomness_source().draw_value(max, [&]() { + // `clamp` to guard against integer overflow + u64 exclusive_bound = AK::clamp(max + 1, max, NumericLimits::max()); + return AK::get_random_uniform_64(exclusive_bound); }); return random; } // An unsigned integer generator in a particular range. // -// Gen::unsigned_int(3,10) -> value 3, RandomRun [0] -// -> value 8, RandomRun [5] -// -> value 10, RandomRun [7] -// etc. +// Gen::number_u64(3,10) -> value 3, RandomRun [0] +// -> value 8, RandomRun [5] +// -> value 10, RandomRun [7] +// etc. // // In case `min == max`, the RandomRun footprint will be smaller: no randomness // is needed. // -// Gen::unsigned_int(3,3) -> value 3, RandomRun [] (always) +// Gen::number_u64(3,3) -> value 3, RandomRun [] (always) // // Shrinks towards the minimum. -inline u32 unsigned_int(u32 min, u32 max) +inline u64 number_u64(u64 min, u64 max) { VERIFY(max >= min); - return unsigned_int(max - min) + min; + return number_u64(max - min) + min; } // Randomly (uniformly) selects a value out of the given arguments. @@ -89,7 +91,7 @@ CommonType one_of(Ts... choices) Vector> choices_vec { choices... }; constexpr size_t count = sizeof...(choices); - size_t i = unsigned_int(count - 1); + size_t i = number_u64(count - 1); return choices_vec[i]; } @@ -118,16 +120,16 @@ CommonType frequency(Choice... choices) { Vector>> choices_vec { choices... }; - u32 sum = 0; + u64 sum = 0; for (auto const& choice : choices_vec) { VERIFY(choice.weight > 0); - sum += static_cast(choice.weight); + sum += static_cast(choice.weight); } - u32 target = unsigned_int(sum); + u64 target = number_u64(sum); size_t i = 0; for (auto const& choice : choices_vec) { - u32 weight = static_cast(choice.weight); + u64 weight = static_cast(choice.weight); if (weight >= target) { return choice.value; } @@ -137,46 +139,47 @@ CommonType frequency(Choice... choices) return choices_vec[i - 1].value; } -// An unsigned integer generator in the full u32 range. +// An unsigned integer generator in the full u64 range. // -// 8/17 (47%) of the time it will bias towards 8bit numbers, -// 4/17 (23%) towards 4bit numbers, -// 2/17 (12%) towards 16bit numbers, -// 1/17 (6%) towards 32bit numbers, -// 2/17 (12%) towards edge cases like 0 and NumericLimits::max() of various unsigned int types. +// Prefers 8bit numbers, then 4bit, 16bit, 32bit and 64bit ones. +// Around 11% of the time it tries edge cases like 0 and various NumericLimits::max(). // -// Gen::unsigned_int() -> value 3, RandomRun [0,3] -// -> value 8, RandomRun [1,8] -// -> value 100, RandomRun [2,100] -// -> value 5, RandomRun [3,5] -// -> value 255, RandomRun [4,1] -// -> value 65535, RandomRun [4,2] -// etc. +// Gen::number_u64() -> value 3, RandomRun [0,3] +// -> value 8, RandomRun [1,8] +// -> value 100, RandomRun [2,100] +// -> value 5, RandomRun [3,5] +// -> value 255, RandomRun [4,1] +// -> value 65535, RandomRun [4,2] +// etc. // // Shrinks towards 0. -inline u32 unsigned_int() +inline u64 number_u64() { - u32 bits = frequency( + u64 bits = frequency( // weight, bits Choice { 4, 4 }, Choice { 8, 8 }, Choice { 2, 16 }, Choice { 1, 32 }, + Choice { 1, 64 }, Choice { 2, 0 }); // The special cases go last as they can be the most extreme (large) values. if (bits == 0) { - // Special cases, eg. max integers for u8, u16, u32. + // Special cases, eg. max integers for u8, u16, u32, u64. return one_of( 0U, NumericLimits::max(), NumericLimits::max(), - NumericLimits::max()); + NumericLimits::max(), + NumericLimits::max()); } - u32 max = ((u64)1 << bits) - 1; - return unsigned_int(max); + u64 max = bits == 64 + ? NumericLimits::max() + : ((u64)1 << bits) - 1; + return number_u64(max); } // A generator returning `true` with the given `probability` (0..1). @@ -190,15 +193,15 @@ inline u32 unsigned_int() // -> value true, RandomRun [1] // // Shrinks towards false. -inline bool weighted_boolean(double probability) +inline bool weighted_boolean(f64 probability) { if (probability <= 0) return false; if (probability >= 1) return true; - u32 random_int = Test::randomness_source().draw_value(1, [&]() { - double drawn_probability = get_random_probability(); + u64 random_int = Test::randomness_source().draw_value(1, [&]() { + f64 drawn_probability = get_random_probability(); return drawn_probability <= probability ? 1 : 0; }); bool random_bool = random_int == 1; @@ -219,7 +222,7 @@ inline bool boolean() // A vector generator of a random length between the given limits. // -// Gen::vector(2,3,[]() { return Gen::unsigned_int(5); }) +// Gen::vector(2,3,[]() { return Gen::number_u64(5); }) // -> value [1,5], RandomRun [1,1,1,5,0] // -> value [1,5,0], RandomRun [1,1,1,5,1,0,0] // etc. @@ -227,7 +230,7 @@ inline bool boolean() // In case `min == max`, the RandomRun footprint will be smaller, as there will // be no randomness involved in figuring out the length: // -// Gen::vector(3,3,[]() { return Gen::unsigned_int(5); }) +// Gen::vector(3,3,[]() { return Gen::number_u64(5); }) // -> value [1,3], RandomRun [1,3] // -> value [5,2], RandomRun [5,2] // etc. @@ -266,7 +269,7 @@ inline Vector> vector(size_t min, size_t max, Fn item_gen) ++size; } - double average = static_cast(min + max) / 2.0; + f64 average = static_cast(min + max) / 2.0; VERIFY(average > 0); // A geometric distribution: https://en.wikipedia.org/wiki/Geometric_distribution#Moments_and_cumulants @@ -279,7 +282,7 @@ inline Vector> vector(size_t min, size_t max, Fn item_gen) // That gives us `1 - 1/p`. Then, E(X) also contains the final success, so we // need to say `1 + average` instead of `average`, as it will mean "our X // items + the final failure that stops the process". - double probability = 1.0 - 1.0 / (1.0 + average); + f64 probability = 1.0 - 1.0 / (1.0 + average); while (size < max) { if (weighted_boolean(probability)) { @@ -295,7 +298,7 @@ inline Vector> vector(size_t min, size_t max, Fn item_gen) // A vector generator of a given length. // -// Gen::vector_of_length(3,[]() { return Gen::unsigned_int(5); }) +// Gen::vector_of_length(3,[]() { return Gen::number_u64(5); }) // -> value [1,5,0], RandomRun [1,1,1,5,1,0,0] // -> value [2,9,3], RandomRun [1,2,1,9,1,3,0] // etc. @@ -312,7 +315,7 @@ inline Vector> vector(size_t length, Fn item_gen) // If you need a different length, use vector(max,item_gen) or // vector(min,max,item_gen). // -// Gen::vector([]() { return Gen::unsigned_int(5); }) +// Gen::vector([]() { return Gen::number_u64(5); }) // -> value [], RandomRun [0] // -> value [1], RandomRun [1,1,0] // -> value [1,5], RandomRun [1,1,1,5,0] @@ -327,6 +330,118 @@ inline Vector> vector(Fn item_gen) return vector(0, 32, item_gen); } +// A double generator in the [0,1) range. +// +// RandomRun footprint: a single number. +// +// Shrinks towards 0. +// +// Based on: https://dotat.at/@/2023-06-23-random-double.html +inline f64 percentage() +{ + return static_cast(number_u64() >> 11) * 0x1.0p-53; +} + +// An internal double generator. This one won't make any attempt to shrink nicely. +// Test writers should use number_f64(f64 min, f64 max) instead. +inline f64 number_f64_scaled(f64 min, f64 max) +{ + VERIFY(max >= min); + + if (min == max) + return min; + + f64 p = percentage(); + return min * (1.0 - p) + max * p; +} + +inline f64 number_f64(f64 min, f64 max) +{ + // FIXME: after we figure out how to use frequency() with lambdas, + // do edge cases and nicely shrinking float generators here + + return number_f64_scaled(min, max); +} + +inline f64 number_f64() +{ + // FIXME: this could be much nicer to the user, at the expense of code complexity + // We could follow Hypothesis' lead and remap integers 0..MAXINT to _simple_ + // floats rather than small floats. Meaning, we would like to prefer integers + // over floats with decimal digits, positive numbers over negative numbers etc. + // As a result, users would get failures with floats like 0, 1, or 0.5 instead of + // ones like 1.175494e-38. + // Check the doc comment in Hypothesis: https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/internal/conjecture/floats.py + + return number_f64(NumericLimits::lowest(), NumericLimits::max()); +} + +// A double generator. +// +// The minimum value will always be NumericLimits::lowest(). +// The maximum value is given by user in the argument. +// +// Prefers positive numbers, then negative numbers, then edge cases. +// +// Shrinks towards 0. +inline f64 number_f64(f64 max) +{ + // FIXME: after we figure out how to use frequency() with lambdas, + // do edge cases and nicely shrinking float generators here + + return number_f64_scaled(NumericLimits::lowest(), max); +} + +// TODO +inline u32 number_u32(u32 max) +{ + if (max == 0) + return 0; + + u32 random = Test::randomness_source().draw_value(max, [&]() { + // `clamp` to guard against integer overflow + u32 exclusive_bound = AK::clamp(max + 1, max, NumericLimits::max()); + return AK::get_random_uniform(exclusive_bound); + }); + return random; +} + +// TODO +inline u32 number_u32(u32 min, u32 max) +{ + VERIFY(max >= min); + return number_u32(max - min) + min; +} + +// TODO +inline u32 number_u32() +{ + u32 bits = frequency( + // weight, bits + Choice { 4, 4 }, + Choice { 8, 8 }, + Choice { 2, 16 }, + Choice { 1, 32 }, + Choice { 1, 64 }, + Choice { 2, 0 }); + + // The special cases go last as they can be the most extreme (large) values. + + if (bits == 0) { + // Special cases, eg. max integers for u8, u16, u32. + return one_of( + 0U, + NumericLimits::max(), + NumericLimits::max(), + NumericLimits::max()); + } + + u32 max = bits == 32 + ? NumericLimits::max() + : ((u32)1 << bits) - 1; + return number_u32(max); +} + } // namespace Gen } // namespace Randomized } // namespace Test diff --git a/Userland/Libraries/LibTest/Randomized/RandomRun.h b/Userland/Libraries/LibTest/Randomized/RandomRun.h index 1656be502f..72408e2968 100644 --- a/Userland/Libraries/LibTest/Randomized/RandomRun.h +++ b/Userland/Libraries/LibTest/Randomized/RandomRun.h @@ -29,24 +29,24 @@ public: RandomRun() = default; RandomRun(RandomRun const& rhs) = default; RandomRun& operator=(RandomRun const& rhs) = default; - explicit RandomRun(Vector const& data) + explicit RandomRun(Vector const& data) : m_data(move(data)) { } bool is_empty() const { return m_data.is_empty(); } bool contains_chunk(Chunk const& c) const { return c.index + c.size <= m_data.size(); } - void append(u32 n) { m_data.append(n); } + void append(u64 n) { m_data.append(n); } size_t size() const { return m_data.size(); } - Optional next() + Optional next() { if (m_current_index < m_data.size()) { return m_data[m_current_index++]; } - return Optional {}; + return Optional {}; } - u32& operator[](size_t index) { return m_data[index]; } - u32 const& operator[](size_t index) const { return m_data[index]; } - Vector data() const { return m_data; } + u64& operator[](size_t index) { return m_data[index]; } + u64 const& operator[](size_t index) const { return m_data[index]; } + Vector data() const { return m_data; } // Shortlex sorting // @@ -85,7 +85,7 @@ public: RandomRun with_sorted(Chunk c) const { - Vector new_data = m_data; + Vector new_data = m_data; AK::dual_pivot_quick_sort( new_data, c.index, @@ -95,13 +95,13 @@ public: } RandomRun with_deleted(Chunk c) const { - Vector new_data(m_data); + Vector new_data(m_data); new_data.remove(c.index, c.size); return RandomRun(move(new_data)); } private: - Vector m_data; + Vector m_data; size_t m_current_index = 0; }; diff --git a/Userland/Libraries/LibTest/Randomized/RandomnessSource.h b/Userland/Libraries/LibTest/Randomized/RandomnessSource.h index af3f34fee0..7c8da66963 100644 --- a/Userland/Libraries/LibTest/Randomized/RandomnessSource.h +++ b/Userland/Libraries/LibTest/Randomized/RandomnessSource.h @@ -26,11 +26,11 @@ public: static RandomnessSource live() { return RandomnessSource(RandomRun(), true); } static RandomnessSource recorded(RandomRun const& run) { return RandomnessSource(run, false); } RandomRun& run() { return m_run; } - u32 draw_value(u32 max, Function random_generator) + u64 draw_value(u64 max, Function random_generator) { // Live: use the random generator and remember the value. if (m_is_live) { - u32 value = random_generator(); + u64 value = random_generator(); m_run.append(value); return value; } diff --git a/Userland/Libraries/LibTest/Randomized/Shrink.h b/Userland/Libraries/LibTest/Randomized/Shrink.h index b405265dd6..2a3ab5622e 100644 --- a/Userland/Libraries/LibTest/Randomized/Shrink.h +++ b/Userland/Libraries/LibTest/Randomized/Shrink.h @@ -77,15 +77,15 @@ ShrinkResult keep_if_better(RandomRun const& new_run, RandomRun const& current_b } template -ShrinkResult binary_shrink(u32 orig_low, u32 orig_high, UpdateRunFn update_run, RandomRun const& orig_run, Fn const& test_function) +ShrinkResult binary_shrink(u64 orig_low, u64 orig_high, UpdateRunFn update_run, RandomRun const& orig_run, Fn const& test_function) { if (orig_low == orig_high) { return no_improvement(orig_run); } RandomRun current_best = orig_run; - u32 low = orig_low; - u32 high = orig_high; + u64 low = orig_low; + u64 high = orig_high; // Let's try with the best case (low = most shrunk) first RandomRun run_with_low = update_run(low, current_best); @@ -111,7 +111,7 @@ ShrinkResult binary_shrink(u32 orig_low, u32 orig_high, UpdateRunFn update_run, // pass/reject/overrun. ShrinkResult result = after_low; while (low + 1 < high) { - u32 mid = low + (high - low) / 2; + u64 mid = low + (high - low) / 2; RandomRun run_with_mid = update_run(mid, current_best); ShrinkResult after_mid = keep_if_better(run_with_mid, current_best, test_function); switch (after_mid.was_improvement) { @@ -194,7 +194,7 @@ ShrinkResult shrink_delete(DeleteChunkAndMaybeDecPrevious command, RandomRun con template ShrinkResult shrink_minimize(MinimizeChoice command, RandomRun const& run, Fn const& test_function) { - u32 value = run[command.index]; + u64 value = run[command.index]; // We can't minimize 0! Already the best possible case. if (value == 0) { @@ -204,7 +204,7 @@ ShrinkResult shrink_minimize(MinimizeChoice command, RandomRun const& run, Fn co return binary_shrink( 0, value, - [&](u32 new_value, RandomRun const& run) { + [&](u64 new_value, RandomRun const& run) { RandomRun copied_run = run; copied_run[command.index] = new_value; return copied_run; @@ -236,12 +236,12 @@ ShrinkResult shrink_redistribute(RedistributeChoicesAndMaybeInc command, RandomR ShrinkResult after_swap = keep_if_better(run_after_swap, current_best, test_function); current_best = after_swap.run; - u32 constant_sum = current_best[command.right_index] + current_best[command.left_index]; + u64 constant_sum = current_best[command.right_index] + current_best[command.left_index]; ShrinkResult after_redistribute = binary_shrink( 0, current_best[command.left_index], - [&](u32 new_value, RandomRun const& run) { + [&](u64 new_value, RandomRun const& run) { RandomRun copied_run = run; copied_run[command.left_index] = new_value; copied_run[command.right_index] = constant_sum - new_value; @@ -275,7 +275,7 @@ ShrinkResult shrink_redistribute(RedistributeChoicesAndMaybeInc command, RandomR ShrinkResult after_inc_redistribute = binary_shrink( 0, current_best[command.left_index], - [&](u32 new_value, RandomRun const& run) { + [&](u64 new_value, RandomRun const& run) { RandomRun copied_run = run; copied_run[command.left_index] = new_value; copied_run[command.right_index] = constant_sum - new_value;