rust/library/alloc/tests/sort/patterns.rs
Lukas Bergdoll 71bb0e72ce Port sort-research-rs test suite Rust stdlib tests
This commit is a followup to https://github.com/rust-lang/rust/pull/124032. It
replaces the tests that test the various sort functions in the standard library
with a test-suite developed as part of
https://github.com/Voultapher/sort-research-rs. The current tests suffer a
couple of problems:

- They don't cover important real world patterns that the implementations take
  advantage of and execute special code for.
- The input lengths tested miss out on code paths. For example, important safety
  property tests never reach the quicksort part of the implementation.
- The miri side is often limited to `len <= 20` which means it very thoroughly
  tests the insertion sort, which accounts for 19 out of 1.5k LoC.
- They are split into to core and alloc, causing code duplication and uneven
  coverage.
- The randomness is not repeatable, as it
  relies on `std:#️⃣:RandomState::new().build_hasher()`.

Most of these issues existed before
https://github.com/rust-lang/rust/pull/124032, but they are intensified by it.
One thing that is new and requires additional testing, is that the new sort
implementations specialize based on type properties. For example `Freeze` and
non `Freeze` execute different code paths.

Effectively there are three dimensions that matter:

- Input type
- Input length
- Input pattern

The ported test-suite tests various properties along all three dimensions,
greatly improving test coverage. It side-steps the miri issue by preferring
sampled approaches. For example the test that checks if after a panic the set of
elements is still the original one, doesn't do so for every single possible
panic opportunity but rather it picks one at random, and performs this test
across a range of input length, which varies the panic point across them. This
allows regular execution to easily test inputs of length 10k, and miri execution
up to 100 which covers significantly more code. The randomness used is tied to a
fixed - but random per process execution - seed. This allows for fully
repeatable tests and fuzzer like exploration across multiple runs.

Structure wise, the tests are previously found in the core integration tests for
`sort_unstable` and alloc unit tests for `sort`. The new test-suite was
developed to be a purely black-box approach, which makes integration testing the
better place, because it can't accidentally rely on internal access. Because
unwinding support is required the tests can't be in core, even if the
implementation is, so they are now part of the alloc integration tests. Are
there architectures that can only build and test core and not alloc? If so, do
such platforms require sort testing? For what it's worth the current
implementation state passes miri `--target mips64-unknown-linux-gnuabi64` which
is big endian.

The test-suite also contains tests for properties that were and are given by the
current and previous implementations, and likely relied upon by users but
weren't tested. For example `self_cmp` tests that the two parameters `a` and `b`
passed into the comparison function are never references to the same object,
which if the user is sorting for example a `&mut [Mutex<i32>]` could lead to a
deadlock.

Instead of using the hashed caller location as rand seed, it uses seconds since
unix epoch / 10, which given timestamps in the CI should be reasonably easy to
reproduce, but also allows fuzzer like space exploration.
2024-09-30 15:05:30 +02:00

212 lines
5.4 KiB
Rust

use std::env;
use std::hash::Hash;
use std::str::FromStr;
use std::sync::OnceLock;
use rand::prelude::*;
use rand_xorshift::XorShiftRng;
use crate::sort::zipf::ZipfDistribution;
/// Provides a set of patterns useful for testing and benchmarking sorting algorithms.
/// Currently limited to i32 values.
// --- Public ---
pub fn random(len: usize) -> Vec<i32> {
// .
// : . : :
// :.:::.::
random_vec(len)
}
pub fn random_uniform<R>(len: usize, range: R) -> Vec<i32>
where
R: Into<rand::distributions::Uniform<i32>> + Hash,
{
// :.:.:.::
let mut rng: XorShiftRng = rand::SeedableRng::seed_from_u64(get_or_init_rand_seed());
// Abstracting over ranges in Rust :(
let dist: rand::distributions::Uniform<i32> = range.into();
(0..len).map(|_| dist.sample(&mut rng)).collect()
}
pub fn random_zipf(len: usize, exponent: f64) -> Vec<i32> {
// https://en.wikipedia.org/wiki/Zipf's_law
let mut rng: XorShiftRng = rand::SeedableRng::seed_from_u64(get_or_init_rand_seed());
// Abstracting over ranges in Rust :(
let dist = ZipfDistribution::new(len, exponent).unwrap();
(0..len).map(|_| dist.sample(&mut rng) as i32).collect()
}
pub fn random_sorted(len: usize, sorted_percent: f64) -> Vec<i32> {
// .:
// .:::. :
// .::::::.::
// [----][--]
// ^ ^
// | |
// sorted |
// unsorted
// Simulate pre-existing sorted slice, where len - sorted_percent are the new unsorted values
// and part of the overall distribution.
let mut v = random_vec(len);
let sorted_len = ((len as f64) * (sorted_percent / 100.0)).round() as usize;
v[0..sorted_len].sort_unstable();
v
}
pub fn all_equal(len: usize) -> Vec<i32> {
// ......
// ::::::
(0..len).map(|_| 66).collect::<Vec<_>>()
}
pub fn ascending(len: usize) -> Vec<i32> {
// .:
// .:::
// .:::::
(0..len as i32).collect::<Vec<_>>()
}
pub fn descending(len: usize) -> Vec<i32> {
// :.
// :::.
// :::::.
(0..len as i32).rev().collect::<Vec<_>>()
}
pub fn saw_mixed(len: usize, saw_count: usize) -> Vec<i32> {
// :. :. .::. .:
// :::.:::..::::::..:::
if len == 0 {
return Vec::new();
}
let mut vals = random_vec(len);
let chunks_size = len / saw_count.max(1);
let saw_directions = random_uniform((len / chunks_size) + 1, 0..=1);
for (i, chunk) in vals.chunks_mut(chunks_size).enumerate() {
if saw_directions[i] == 0 {
chunk.sort_unstable();
} else if saw_directions[i] == 1 {
chunk.sort_unstable_by_key(|&e| std::cmp::Reverse(e));
} else {
unreachable!();
}
}
vals
}
pub fn saw_mixed_range(len: usize, range: std::ops::Range<usize>) -> Vec<i32> {
// :.
// :. :::. .::. .:
// :::.:::::..::::::..:.:::
// ascending and descending randomly picked, with length in `range`.
if len == 0 {
return Vec::new();
}
let mut vals = random_vec(len);
let max_chunks = len / range.start;
let saw_directions = random_uniform(max_chunks + 1, 0..=1);
let chunk_sizes = random_uniform(max_chunks + 1, (range.start as i32)..(range.end as i32));
let mut i = 0;
let mut l = 0;
while l < len {
let chunk_size = chunk_sizes[i] as usize;
let chunk_end = std::cmp::min(l + chunk_size, len);
let chunk = &mut vals[l..chunk_end];
if saw_directions[i] == 0 {
chunk.sort_unstable();
} else if saw_directions[i] == 1 {
chunk.sort_unstable_by_key(|&e| std::cmp::Reverse(e));
} else {
unreachable!();
}
i += 1;
l += chunk_size;
}
vals
}
pub fn pipe_organ(len: usize) -> Vec<i32> {
// .:.
// .:::::.
let mut vals = random_vec(len);
let first_half = &mut vals[0..(len / 2)];
first_half.sort_unstable();
let second_half = &mut vals[(len / 2)..len];
second_half.sort_unstable_by_key(|&e| std::cmp::Reverse(e));
vals
}
pub fn get_or_init_rand_seed() -> u64 {
*SEED_VALUE.get_or_init(|| {
env::var("OVERRIDE_SEED")
.ok()
.map(|seed| u64::from_str(&seed).unwrap())
.unwrap_or_else(rand_root_seed)
})
}
// --- Private ---
static SEED_VALUE: OnceLock<u64> = OnceLock::new();
#[cfg(not(miri))]
fn rand_root_seed() -> u64 {
// Other test code hashes `panic::Location::caller()` and constructs a seed from that, in these
// tests we want to have a fuzzer like exploration of the test space, if we used the same caller
// based construction we would always test the same.
//
// Instead we use the seconds since UNIX epoch / 10, given CI log output this value should be
// reasonably easy to re-construct.
use std::time::{SystemTime, UNIX_EPOCH};
let epoch_seconds = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
epoch_seconds / 10
}
#[cfg(miri)]
fn rand_root_seed() -> u64 {
// Miri is usually run with isolation with gives us repeatability but also permutations based on
// other code that runs before.
use core::hash::{BuildHasher, Hash, Hasher};
let mut hasher = std::hash::RandomState::new().build_hasher();
core::panic::Location::caller().hash(&mut hasher);
hasher.finish()
}
fn random_vec(len: usize) -> Vec<i32> {
let mut rng: XorShiftRng = rand::SeedableRng::seed_from_u64(get_or_init_rand_seed());
(0..len).map(|_| rng.gen::<i32>()).collect()
}