CI / Test (beta, Linux) (push) Canceled after 0s
CI / Test (nightly, Linux) (push) Canceled after 0s
CI / Test (stable, Linux) (push) Canceled after 0s
CI / Test (macos-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / MSRV (push) Canceled after 0s
CI / Documentation (push) Canceled after 0s
CI / Coverage (push) Canceled after 0s
CI / Security Audit (push) Canceled after 0s
CI / Fuzz Testing (push) Canceled after 0s
CI / Examples (push) Canceled after 0s
CI / Benchmarks (push) Canceled after 0s
1763 lines
52 KiB
Rust
1763 lines
52 KiB
Rust
//! Property-based testing for the announcement crate.
|
|
//! Uses proptest to generate random test cases and verify invariants.
|
|
|
|
use announcement::Announcement;
|
|
use proptest::prelude::*;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
// Strategy for generating various types
|
|
fn arb_string() -> impl Strategy<Value = String> {
|
|
prop::string::string_regex("[a-zA-Z0-9]{0,100}").unwrap()
|
|
}
|
|
|
|
fn arb_vec() -> impl Strategy<Value = Vec<i32>> {
|
|
prop::collection::vec(any::<i32>(), 0..100)
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: All N listeners receive the same announced value
|
|
///
|
|
/// Verifies: Broadcast semantics hold for arbitrary listener counts
|
|
/// Strategy: Generates 1-100 listeners with random i32 values
|
|
#[test]
|
|
fn prop_all_listeners_receive_value(value: i32, listener_count in 1usize..100) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let mut listeners = Vec::new();
|
|
for _ in 0..listener_count {
|
|
listeners.push(announcement.listener());
|
|
}
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: try_listen transitions from None to Some exactly once
|
|
///
|
|
/// Verifies: State transition is clean and irreversible
|
|
/// Strategy: Random string values
|
|
#[test]
|
|
fn prop_try_listen_none_then_some(value: String) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
assert_eq!(listener.try_listen(), None);
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: is_announced is monotonic - false then true, never reverses
|
|
///
|
|
/// Verifies: Announcement state is permanent once set
|
|
/// Strategy: Multiple checks before and after announce with u64 values
|
|
#[test]
|
|
fn prop_is_announced_monotonic(value: u64, checks_before in 1usize..50, checks_after in 1usize..50) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
// Check false before
|
|
for _ in 0..checks_before {
|
|
assert!(!announcement.is_announced());
|
|
}
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
// Check true after
|
|
for _ in 0..checks_after {
|
|
assert!(announcement.is_announced());
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Broadcast scales to large listener counts
|
|
///
|
|
/// Verifies: All listeners receive value regardless of count (1-1000)
|
|
/// Strategy: Variable listener counts with Vec<u8> values
|
|
#[test]
|
|
fn prop_n_listeners_all_receive(
|
|
value: Vec<u8>,
|
|
listener_count in 1usize..1000
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let listeners: Vec<_> = (0..listener_count)
|
|
.map(|_| announcement.listener())
|
|
.collect();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Arc refcount equals initial + storage + received refs
|
|
///
|
|
/// Verifies: No reference leaks in Arc-wrapped values
|
|
/// Strategy: 0-50 listeners with Arc<i32> values
|
|
#[test]
|
|
fn prop_arc_refcount_correct(
|
|
value: i32,
|
|
listener_count in 0usize..50
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let arc_value = Arc::new(value);
|
|
let initial_count = Arc::strong_count(&arc_value);
|
|
|
|
let mut listeners = Vec::new();
|
|
for _ in 0..listener_count {
|
|
listeners.push(announcement.listener());
|
|
}
|
|
|
|
announcer.announce(arc_value.clone()).unwrap();
|
|
|
|
let mut received = Vec::new();
|
|
for listener in &listeners {
|
|
received.push(listener.try_listen().unwrap());
|
|
}
|
|
|
|
if listener_count > 0 {
|
|
// initial + internal storage + all received refs
|
|
let expected = initial_count + 1 + listener_count;
|
|
assert_eq!(Arc::strong_count(&received[0]), expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Listener creation timing doesn't affect value received
|
|
///
|
|
/// Verifies: Both early and late listeners get the same value
|
|
/// Strategy: Variable pre/post-announce listener counts with strings
|
|
#[test]
|
|
fn prop_listeners_before_after_same(
|
|
value: String,
|
|
before_count in 0usize..20,
|
|
after_count in 0usize..20
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let before_listeners: Vec<_> = (0..before_count)
|
|
.map(|_| announcement.listener())
|
|
.collect();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
let after_listeners: Vec<_> = (0..after_count)
|
|
.map(|_| announcement.listener())
|
|
.collect();
|
|
|
|
for listener in &before_listeners {
|
|
assert_eq!(listener.try_listen(), Some(value.clone()));
|
|
}
|
|
|
|
for listener in &after_listeners {
|
|
assert_eq!(listener.try_listen(), Some(value.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Cloning listeners and announcements preserves value access
|
|
///
|
|
/// Verifies: Clone semantics maintain broadcast behavior
|
|
/// Strategy: Random Vec<i32> with multiple clone operations
|
|
#[test]
|
|
fn prop_clone_preserves_value(value: Vec<i32>) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let original_listener = announcement.listener();
|
|
let cloned_listener = original_listener.clone();
|
|
let cloned_announcement = announcement.clone();
|
|
let listener_from_clone = cloned_announcement.listener();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
assert_eq!(original_listener.try_listen(), Some(value.clone()));
|
|
assert_eq!(cloned_listener.try_listen(), Some(value.clone()));
|
|
assert_eq!(listener_from_clone.try_listen(), Some(value));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: try_listen is idempotent across multiple calls
|
|
///
|
|
/// Verifies: Same value returned on every call without side effects
|
|
/// Strategy: 1-100 calls with u32 values
|
|
#[test]
|
|
fn prop_try_listen_idempotent(value: u32, call_count in 1usize..100) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
for _ in 0..call_count {
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: HashMap structure and contents preserved through broadcast
|
|
///
|
|
/// Verifies: All keys, values, and length maintained
|
|
/// Strategy: Random HashMaps with 0-50 string keys and i32 values
|
|
#[test]
|
|
fn prop_hashmap_integrity(
|
|
data in prop::collection::hash_map(
|
|
arb_string(),
|
|
any::<i32>(),
|
|
0..50
|
|
)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, data);
|
|
assert_eq!(received.len(), data.len());
|
|
|
|
for (k, v) in &data {
|
|
assert_eq!(received.get(k), Some(v));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Option<T> values (Some/None) preserved correctly
|
|
///
|
|
/// Verifies: Both Some and None variants handled properly
|
|
/// Strategy: Random Option<i32> values
|
|
#[test]
|
|
fn prop_option_handling(opt in prop::option::of(any::<i32>())) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(opt).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(opt));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Result<T, E> values (Ok/Err) preserved correctly
|
|
///
|
|
/// Verifies: Both Ok and Err variants handled properly
|
|
/// Strategy: Random Result<i32, String> values
|
|
#[test]
|
|
fn prop_result_handling(
|
|
result in prop::result::maybe_ok(any::<i32>(), arb_string())
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(result.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(result));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Thread safety holds despite random timing variations
|
|
///
|
|
/// Verifies: Value delivery works across threads with variable delays
|
|
/// Strategy: 2-10 threads with 0-50ms random delays
|
|
#[test]
|
|
fn prop_thread_safety_random_delays(
|
|
value: i32,
|
|
_thread_count in 2usize..10,
|
|
delays in prop::collection::vec(0u64..50, 2..10)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let mut handles = vec![];
|
|
|
|
for delay_ms in delays {
|
|
let announcement_clone = announcement.clone();
|
|
handles.push(thread::spawn(move || {
|
|
thread::sleep(Duration::from_millis(delay_ms));
|
|
|
|
announcement_clone.listener()
|
|
}));
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(25));
|
|
announcer.announce(value).unwrap();
|
|
|
|
for handle in handles {
|
|
let listener = handle.join().unwrap();
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Vector length and element order preserved
|
|
///
|
|
/// Verifies: Vec structure maintained including first/last elements
|
|
/// Strategy: Random Vec<i32> with 0-100 elements
|
|
#[test]
|
|
fn prop_vec_operations(vec in arb_vec()) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let original_len = vec.len();
|
|
let first = vec.first().cloned();
|
|
let last = vec.last().cloned();
|
|
|
|
announcer.announce(vec.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.len(), original_len);
|
|
assert_eq!(received.first().cloned(), first);
|
|
assert_eq!(received.last().cloned(), last);
|
|
assert_eq!(received, vec);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: String length and emptiness preserved
|
|
///
|
|
/// Verifies: String contents and properties maintained
|
|
/// Strategy: Random strings with any characters
|
|
#[test]
|
|
fn prop_string_operations(s in ".*") {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let original_len = s.len();
|
|
let is_empty = s.is_empty();
|
|
|
|
announcer.announce(s.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.len(), original_len);
|
|
assert_eq!(received.is_empty(), is_empty);
|
|
assert_eq!(received, s);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: String length and emptiness preserved
|
|
///
|
|
/// Verifies: String contents and properties maintained
|
|
/// Strategy: Random strings with any characters
|
|
#[test]
|
|
fn prop_tuple_preservation(
|
|
a: i32,
|
|
b: String,
|
|
c: Vec<u8>
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let tuple = (a, b.clone(), c.clone());
|
|
|
|
announcer.announce(tuple.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.0, a);
|
|
assert_eq!(received.1, b);
|
|
assert_eq!(received.2, c);
|
|
assert_eq!(received, tuple);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Tuple structure and elements preserved
|
|
///
|
|
/// Verifies: All tuple components maintained with correct types
|
|
/// Strategy: Random (i32, String, Vec<u8>) tuples
|
|
#[test]
|
|
fn prop_is_announced_thread_consistency(
|
|
value: i32,
|
|
thread_count in 2usize..20
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
let mut handles = vec![];
|
|
|
|
for _ in 0..thread_count {
|
|
let announcement_clone = announcement.clone();
|
|
handles.push(thread::spawn(move || {
|
|
announcement_clone.is_announced()
|
|
}));
|
|
}
|
|
|
|
for handle in handles {
|
|
assert!(handle.join().unwrap());
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: is_announced returns consistent value across all threads
|
|
///
|
|
/// Verifies: Thread-safe state visibility after announce
|
|
/// Strategy: 2-20 concurrent threads checking state
|
|
#[test]
|
|
fn prop_listener_count_independent(
|
|
value: Vec<String>,
|
|
counts in prop::collection::vec(1usize..10, 1..5)
|
|
) {
|
|
for count in counts {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let listeners: Vec<_> = (0..count)
|
|
.map(|_| announcement.listener())
|
|
.collect();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Value received is independent of listener count
|
|
///
|
|
/// Verifies: Same value delivered regardless of N
|
|
/// Strategy: Multiple runs with varying listener counts 1-10
|
|
#[test]
|
|
fn prop_nested_collections(
|
|
data in prop::collection::vec(
|
|
prop::collection::vec(any::<i32>(), 0..10),
|
|
0..10
|
|
)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, data);
|
|
assert_eq!(received.len(), data.len());
|
|
|
|
for (i, inner) in data.iter().enumerate() {
|
|
assert_eq!(&received[i], inner);
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Nested collection structure preserved
|
|
///
|
|
/// Verifies: Vec<Vec<i32>> maintains all levels correctly
|
|
/// Strategy: Random nested vectors with variable dimensions
|
|
#[test]
|
|
fn prop_hashset_preservation(
|
|
data in prop::collection::hash_set(any::<i32>(), 0..50)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.len(), data.len());
|
|
|
|
for item in &data {
|
|
assert!(received.contains(item));
|
|
}
|
|
|
|
for item in &received {
|
|
assert!(data.contains(item));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: HashSet elements and cardinality preserved
|
|
///
|
|
/// Verifies: All set elements present in both directions
|
|
/// Strategy: Random HashSet<i32> with 0-50 elements
|
|
#[test]
|
|
fn prop_box_preservation(value: i32) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let boxed = Box::new(value);
|
|
|
|
announcer.announce(boxed.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(*received, value);
|
|
assert_eq!(received, boxed);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Box<T> wrapper and inner value preserved
|
|
///
|
|
/// Verifies: Boxed values work correctly
|
|
/// Strategy: Random Box<i32> values
|
|
#[test]
|
|
fn prop_channels_independent(
|
|
values in prop::collection::vec(any::<i32>(), 1..10)
|
|
) {
|
|
let mut channels = Vec::new();
|
|
|
|
for value in &values {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
channels.push((Some(announcer), announcement, listener, *value));
|
|
}
|
|
|
|
// Announce in order
|
|
for (announcer, _, _, value) in channels.iter_mut() {
|
|
if let Some(a) = announcer.take() {
|
|
a.announce(*value).unwrap();
|
|
}
|
|
}
|
|
|
|
// Verify each channel independently
|
|
for (_, _, listener, expected) in &channels {
|
|
assert_eq!(listener.try_listen(), Some(*expected));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Independent announcement channels don't interfere
|
|
///
|
|
/// Verifies: Each channel maintains its own state independently
|
|
/// Strategy: 1-10 separate channels with different values
|
|
#[test]
|
|
fn prop_float_special_values(
|
|
use_nan: bool,
|
|
use_infinity: bool,
|
|
use_neg_infinity: bool,
|
|
regular in any::<f64>()
|
|
) {
|
|
let value = if use_nan {
|
|
f64::NAN
|
|
} else if use_infinity {
|
|
f64::INFINITY
|
|
} else if use_neg_infinity {
|
|
f64::NEG_INFINITY
|
|
} else {
|
|
regular
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
|
|
if value.is_nan() {
|
|
assert!(received.is_nan());
|
|
} else {
|
|
assert_eq!(received, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Float special values (NaN, infinity) handled correctly
|
|
///
|
|
/// Verifies: NaN, INFINITY, NEG_INFINITY, and regular floats work
|
|
/// Strategy: Random selection of special and regular f64 values
|
|
#[test]
|
|
fn prop_array_preservation(arr: [i32; 10]) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(arr).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, arr);
|
|
|
|
for i in 0..10 {
|
|
assert_eq!(received[i], arr[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Fixed-size array structure preserved
|
|
///
|
|
/// Verifies: All array elements maintained at correct indices
|
|
/// Strategy: Random [i32; 10] arrays
|
|
#[test]
|
|
fn prop_arc_mutex_preservation(value: i32) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let mutex_value = Arc::new(Mutex::new(value));
|
|
|
|
announcer.announce(mutex_value.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(*received.lock().unwrap(), value);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Arc<Mutex<T>> wrapper and locked value preserved
|
|
///
|
|
/// Verifies: Thread-safe wrapped values work correctly
|
|
/// Strategy: Random Arc<Mutex<i32>> values
|
|
#[test]
|
|
fn prop_unicode_preservation(s in "\\PC*") {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(s.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, s);
|
|
assert_eq!(received.len(), s.len());
|
|
assert_eq!(received.chars().count(), s.chars().count());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Unicode strings preserved with correct character counts
|
|
///
|
|
/// Verifies: Multi-byte characters handled correctly
|
|
/// Strategy: Random Unicode strings
|
|
#[test]
|
|
fn prop_enum_with_data(
|
|
choice: bool,
|
|
i_val: i32,
|
|
s_val: String
|
|
) {
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
enum TestEnum {
|
|
Variant1(i32),
|
|
Variant2(String),
|
|
}
|
|
|
|
let value = if choice {
|
|
TestEnum::Variant1(i_val)
|
|
} else {
|
|
TestEnum::Variant2(s_val)
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Enum variants with data preserved correctly
|
|
///
|
|
/// Verifies: Both enum variant and associated data maintained
|
|
/// Strategy: Random choice between two enum variants with data
|
|
#[test]
|
|
fn prop_rapid_listener_creation(
|
|
value: i32,
|
|
creation_count in 10usize..100
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let mut listeners = Vec::new();
|
|
for _ in 0..creation_count {
|
|
listeners.push(announcement.listener());
|
|
// Some may be dropped immediately
|
|
if listeners.len() > 10 {
|
|
let _ = listeners.remove(0);
|
|
}
|
|
}
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Rapid listener creation and dropping doesn't break system
|
|
///
|
|
/// Verifies: High churn rate of listener lifecycle handled correctly
|
|
/// Strategy: Create 10-100 listeners with some dropped before announce
|
|
#[test]
|
|
fn prop_mixed_tuple(
|
|
i: i32,
|
|
f: f64,
|
|
s: String,
|
|
v: Vec<u8>,
|
|
o: Option<bool>
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let tuple = (i, f, s.clone(), v.clone(), o);
|
|
|
|
announcer.announce(tuple.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.0, i);
|
|
assert_eq!(received.1, f);
|
|
assert_eq!(received.2, s);
|
|
assert_eq!(received.3, v);
|
|
assert_eq!(received.4, o);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Complex heterogeneous tuple preserved
|
|
///
|
|
/// Verifies: Mixed-type tuple (i32, f64, String, Vec, Option) works
|
|
/// Strategy: Random 5-element tuple with diverse types
|
|
#[test]
|
|
fn prop_bounded_integers(
|
|
u8_val: u8,
|
|
u16_val: u16,
|
|
u32_val: u32,
|
|
u64_val: u64,
|
|
i8_val: i8,
|
|
i16_val: i16,
|
|
i32_val: i32,
|
|
i64_val: i64
|
|
) {
|
|
let tuple = (u8_val, u16_val, u32_val, u64_val, i8_val, i16_val, i32_val, i64_val);
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(tuple).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, tuple);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: All integer types preserved correctly
|
|
///
|
|
/// Verifies: Signed and unsigned integers of all sizes work
|
|
/// Strategy: Random tuple of all integer types (u8-u64, i8-i64)
|
|
#[test]
|
|
fn prop_collection_size_preserved(
|
|
vec_size in 0usize..1000,
|
|
hash_size in 0usize..100,
|
|
set_size in 0usize..100
|
|
) {
|
|
let vec: Vec<i32> = (0..vec_size as i32).collect();
|
|
let mut hash = HashMap::new();
|
|
for i in 0..hash_size {
|
|
hash.insert(i, i * 2);
|
|
}
|
|
let set: HashSet<i32> = (0..set_size as i32).collect();
|
|
|
|
let data = (vec.clone(), hash.clone(), set.clone());
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.0.len(), vec_size);
|
|
assert_eq!(received.1.len(), hash_size);
|
|
assert_eq!(received.2.len(), set_size);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Multiple collection sizes preserved independently
|
|
///
|
|
/// Verifies: Vec, HashMap, and HashSet sizes all maintained
|
|
/// Strategy: Random sizes for three different collection types
|
|
#[test]
|
|
#[cfg(target_os = "linux")]
|
|
fn prop_listener_timing_independent(
|
|
value: i32,
|
|
delays in prop::collection::vec(0u64..10, 1..10)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
thread::spawn(move || {
|
|
thread::sleep(Duration::from_millis(5));
|
|
announcer.announce(value).unwrap();
|
|
});
|
|
|
|
let mut listeners = Vec::new();
|
|
|
|
for delay in delays {
|
|
thread::sleep(Duration::from_millis(delay));
|
|
listeners.push(announcement.listener());
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(20));
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Listener creation timing relative to announce doesn't matter
|
|
///
|
|
/// Verifies: Variable delays don't affect value reception
|
|
/// Strategy: Random delays 0-10ms for listener creation
|
|
#[test]
|
|
fn prop_error_message_consistency(value: i32) {
|
|
use announcement::AnnounceError;
|
|
|
|
let err = AnnounceError::AlreadyAnnounced(value);
|
|
let message = err.to_string();
|
|
|
|
assert!(message.contains("already"));
|
|
assert!(message.contains("announced"));
|
|
|
|
let inner = err.into_inner();
|
|
assert_eq!(inner, value);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Error messages contain expected keywords
|
|
///
|
|
/// Verifies: AnnounceError message format is consistent
|
|
/// Strategy: Random i32 values for error construction
|
|
#[test]
|
|
fn prop_char_validity(c: char) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(c).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, c);
|
|
assert!(received.is_ascii() == c.is_ascii());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Character values and properties preserved
|
|
///
|
|
/// Verifies: All valid char values including ASCII status maintained
|
|
/// Strategy: Random char values
|
|
#[test]
|
|
fn prop_path_strings(
|
|
components in prop::collection::vec(
|
|
prop::string::string_regex("[a-zA-Z0-9_-]+").unwrap(),
|
|
1..10
|
|
)
|
|
) {
|
|
let path = components.join("/");
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(path.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, path);
|
|
assert_eq!(received.split('/').count(), components.len());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Path-like strings with separators preserved
|
|
///
|
|
/// Verifies: Component count and structure maintained
|
|
/// Strategy: Random path strings with 1-10 components
|
|
#[test]
|
|
fn prop_recursive_depth(depth in 0usize..20) {
|
|
#[derive(Clone, Debug)]
|
|
#[allow(dead_code)]
|
|
struct Node {
|
|
value: i32,
|
|
next: Option<Box<Node>>,
|
|
}
|
|
|
|
let mut node = Node {
|
|
value: depth as i32,
|
|
next: None,
|
|
};
|
|
|
|
for i in (0..depth).rev() {
|
|
node = Node {
|
|
value: i as i32,
|
|
next: Some(Box::new(node)),
|
|
};
|
|
}
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(node).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
|
|
// Verify depth
|
|
let mut current = &received;
|
|
let mut count = 0;
|
|
while let Some(ref next) = current.next {
|
|
count += 1;
|
|
current = next;
|
|
}
|
|
assert_eq!(count, depth);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Recursive linked structures preserved to arbitrary depth
|
|
///
|
|
/// Verifies: Deep nesting via Option<Box<Node>> works correctly
|
|
/// Strategy: Random depth 0-20 for recursive structures
|
|
#[test]
|
|
fn prop_binary_data_integrity(
|
|
data in prop::collection::vec(any::<u8>(), 0..1000)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
let checksum: u32 = data.iter().map(|&b| b as u32).sum();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
let received_checksum: u32 = received.iter().map(|&b| b as u32).sum();
|
|
|
|
assert_eq!(received, data);
|
|
assert_eq!(received_checksum, checksum);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Binary data integrity via checksum verification
|
|
///
|
|
/// Verifies: All bytes preserved without corruption
|
|
/// Strategy: Random Vec<u8> with 0-1000 bytes and checksum
|
|
#[test]
|
|
fn prop_thread_ordering(
|
|
value: i32,
|
|
thread_count in 2usize..10
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let counter = Arc::new(Mutex::new(0));
|
|
|
|
let mut handles = vec![];
|
|
|
|
for i in 0..thread_count {
|
|
let announcement_clone = announcement.clone();
|
|
let counter_clone = counter.clone();
|
|
|
|
handles.push(thread::spawn(move || {
|
|
let listener = announcement_clone.listener();
|
|
*counter_clone.lock().unwrap() += 1;
|
|
(i, listener)
|
|
}));
|
|
}
|
|
|
|
thread::sleep(Duration::from_millis(10));
|
|
announcer.announce(value).unwrap();
|
|
|
|
for handle in handles {
|
|
let (_, listener) = handle.join().unwrap();
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
|
|
assert_eq!(*counter.lock().unwrap(), thread_count);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Thread spawning order doesn't affect correctness
|
|
///
|
|
/// Verifies: All threads receive value regardless of spawn order
|
|
/// Strategy: 2-10 threads with counter verification
|
|
#[test]
|
|
fn prop_empty_string_patterns(
|
|
prefix in ".*",
|
|
suffix in ".*",
|
|
use_empty: bool
|
|
) {
|
|
let value = if use_empty {
|
|
String::new()
|
|
} else {
|
|
format!("{}{}", prefix, suffix)
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(value.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, value);
|
|
assert_eq!(received.is_empty(), value.is_empty());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Empty strings and string concatenations preserved
|
|
///
|
|
/// Verifies: Empty string handling and is_empty() property maintained
|
|
/// Strategy: Random empty or concatenated strings
|
|
#[test]
|
|
fn prop_hashmap_key_order_independent(
|
|
keys in prop::collection::vec(
|
|
prop::string::string_regex("[a-z]+").unwrap(),
|
|
1..20
|
|
),
|
|
values in prop::collection::vec(any::<i32>(), 1..20)
|
|
) {
|
|
let len = keys.len().min(values.len());
|
|
let mut map = HashMap::new();
|
|
|
|
for i in 0..len {
|
|
map.insert(keys[i].clone(), values[i]);
|
|
}
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(map.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
|
|
assert_eq!(received.len(), map.len());
|
|
for (k, v) in &map {
|
|
assert_eq!(received.get(k), Some(v));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: HashMap correctness independent of insertion order
|
|
///
|
|
/// Verifies: All key-value pairs present regardless of order
|
|
/// Strategy: Random key-value pairs with 1-20 entries
|
|
#[test]
|
|
fn prop_type_size_handling(
|
|
small: u8,
|
|
medium: [u8; 100],
|
|
large_size in 1000usize..10000
|
|
) {
|
|
let large = vec![0u8; large_size];
|
|
let data = (small, medium, large.clone());
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.0, small);
|
|
assert_eq!(received.1, medium);
|
|
assert_eq!(received.2, large);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Various data sizes (small, medium, large) handled correctly
|
|
///
|
|
/// Verifies: Size doesn't affect correctness
|
|
/// Strategy: Small (u8), medium (array), large (Vec) data
|
|
#[test]
|
|
fn prop_clone_chain_integrity(
|
|
value: i32,
|
|
chain_length in 1usize..20
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let mut current = announcement.listener();
|
|
let mut listeners = vec![current.clone()];
|
|
|
|
for _ in 1..chain_length {
|
|
current = current.clone();
|
|
listeners.push(current.clone());
|
|
}
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
for listener in &listeners {
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Long clone chains maintain value access
|
|
///
|
|
/// Verifies: Chained listener clones all work correctly
|
|
/// Strategy: 1-20 length clone chains
|
|
#[test]
|
|
fn prop_concurrent_is_announced(
|
|
value: i32,
|
|
check_count in 100usize..1000
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let ann_clone = announcement.clone();
|
|
let handle = thread::spawn(move || {
|
|
let mut saw_false = false;
|
|
let mut saw_true = false;
|
|
|
|
for _ in 0..check_count {
|
|
if ann_clone.is_announced() {
|
|
saw_true = true;
|
|
} else {
|
|
saw_false = true;
|
|
}
|
|
}
|
|
|
|
(saw_false, saw_true)
|
|
});
|
|
|
|
thread::sleep(Duration::from_millis(1));
|
|
announcer.announce(value).unwrap();
|
|
|
|
let (saw_false, saw_true) = handle.join().unwrap();
|
|
|
|
// Should have seen at least one true (after announce)
|
|
assert!(saw_true || saw_false); // At least one observation
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Concurrent is_announced checks see consistent state transition
|
|
///
|
|
/// Verifies: Thread-safe state observation during announce
|
|
/// Strategy: 100-1000 rapid checks concurrent with announce
|
|
#[test]
|
|
fn prop_option_chain(
|
|
depth in 0usize..5,
|
|
value: i32
|
|
) {
|
|
type Level0 = i32;
|
|
type Level1 = Option<Level0>;
|
|
type Level2 = Option<Level1>;
|
|
type Level3 = Option<Level2>;
|
|
type Level4 = Option<Level3>;
|
|
type Level5 = Option<Level4>;
|
|
|
|
match depth {
|
|
0 => {
|
|
let (announcer, announcement) = Announcement::<Level0>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(value).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
},
|
|
1 => {
|
|
let (announcer, announcement) = Announcement::<Level1>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(Some(value)).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(Some(value)));
|
|
},
|
|
2 => {
|
|
let (announcer, announcement) = Announcement::<Level2>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(Some(Some(value))).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(Some(Some(value))));
|
|
},
|
|
3 => {
|
|
let (announcer, announcement) = Announcement::<Level3>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(Some(Some(Some(value)))).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(Some(Some(Some(value)))));
|
|
},
|
|
4 => {
|
|
let (announcer, announcement) = Announcement::<Level4>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(Some(Some(Some(Some(value))))).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(Some(Some(Some(Some(value))))));
|
|
},
|
|
_ => {
|
|
let (announcer, announcement) = Announcement::<Level5>::new();
|
|
let listener = announcement.listener();
|
|
announcer.announce(Some(Some(Some(Some(Some(value)))))).unwrap();
|
|
assert_eq!(listener.try_listen(), Some(Some(Some(Some(Some(Some(value)))))));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Nested Option<Option<...>> chains preserved
|
|
///
|
|
/// Verifies: Deep Option nesting works correctly
|
|
/// Strategy: 0-5 levels of Option wrapping
|
|
#[test]
|
|
fn prop_result_chain(
|
|
success: bool,
|
|
value: i32,
|
|
error: String
|
|
) {
|
|
type Inner = Result<i32, String>;
|
|
type Outer = Result<Inner, String>;
|
|
|
|
let (announcer, announcement) = Announcement::<Outer>::new();
|
|
let listener = announcement.listener();
|
|
|
|
let result = if success {
|
|
Ok(Ok(value))
|
|
} else {
|
|
Ok(Err(error.clone()))
|
|
};
|
|
|
|
announcer.announce(result.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(result));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Nested Result<Result<...>> chains preserved
|
|
///
|
|
/// Verifies: Deep Result nesting maintains Ok/Err structure
|
|
/// Strategy: Random nested Result with bool choice
|
|
#[test]
|
|
fn prop_complex_generics(
|
|
data in prop::collection::vec(
|
|
prop::option::of(
|
|
prop::result::maybe_ok(any::<i32>(), any::<bool>())
|
|
),
|
|
0..10
|
|
)
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, data);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Complex nested generics Vec<Option<Result<T,E>>> work
|
|
///
|
|
/// Verifies: Deeply composed generic types preserved correctly
|
|
/// Strategy: Random Vec<Option<Result<i32,bool>>>
|
|
#[test]
|
|
fn prop_string_whitespace(s in "\\s*\\S*\\s*") {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(s.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, s);
|
|
assert_eq!(received.trim(), s.trim());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: String whitespace patterns preserved exactly
|
|
///
|
|
/// Verifies: Leading/trailing whitespace maintained
|
|
/// Strategy: Random strings with whitespace patterns
|
|
#[test]
|
|
fn prop_numeric_boundaries(
|
|
add_to_max: bool,
|
|
sub_from_min: bool
|
|
) {
|
|
let value = if add_to_max {
|
|
(u32::MAX, i32::MAX, u64::MAX)
|
|
} else if sub_from_min {
|
|
(0u32, i32::MIN, 0u64)
|
|
} else {
|
|
(u32::MAX / 2, 0, u64::MAX / 2)
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received, value);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Numeric boundary values (MAX, MIN, mid) preserved
|
|
///
|
|
/// Verifies: Extreme integer values handled correctly
|
|
/// Strategy: Random choice of MAX, MIN, or mid-range values
|
|
#[test]
|
|
fn prop_capacity_preservation(
|
|
initial_capacity in 0usize..100,
|
|
actual_size in 0usize..50
|
|
) {
|
|
let actual_size = actual_size.min(initial_capacity);
|
|
|
|
let mut vec = Vec::with_capacity(initial_capacity);
|
|
for i in 0..actual_size {
|
|
vec.push(i as i32);
|
|
}
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(vec.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.len(), actual_size);
|
|
assert_eq!(received, vec);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Vec length preserved regardless of capacity
|
|
///
|
|
/// Verifies: Only length matters, not internal capacity
|
|
/// Strategy: Random capacity 0-100, size 0-50
|
|
#[test]
|
|
fn prop_thread_pool_saturation(
|
|
value: i32,
|
|
batch_size in 10usize..50
|
|
) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let mut handles = vec![];
|
|
|
|
// Create many threads quickly
|
|
for _ in 0..batch_size {
|
|
let ann = announcement.clone();
|
|
if let Ok(handle) = thread::Builder::new()
|
|
.stack_size(32 * 1024)
|
|
.spawn(move || {
|
|
ann.listener()
|
|
})
|
|
{
|
|
handles.push(handle);
|
|
}
|
|
}
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
for handle in handles {
|
|
let listener = handle.join().unwrap();
|
|
assert_eq!(listener.try_listen(), Some(value));
|
|
}
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: High thread count doesn't break system
|
|
///
|
|
/// Verifies: System handles many concurrent threads gracefully
|
|
/// Strategy: 10-50 threads spawned rapidly
|
|
#[test]
|
|
fn prop_memory_pattern(
|
|
pattern: u8,
|
|
size in 1usize..1000
|
|
) {
|
|
let data = vec![pattern; size];
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(data.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
assert_eq!(received.len(), size);
|
|
assert!(received.iter().all(|&b| b == pattern));
|
|
}
|
|
}
|
|
|
|
// =====================================================================
|
|
// New tests ported from quickcheck to achieve proptest supremacy
|
|
// =====================================================================
|
|
|
|
/// Property: All printable ASCII characters in string preserved
|
|
///
|
|
/// Verifies: Full ASCII printable character set transmitted correctly
|
|
/// Strategy: Generates string with all characters from ASCII 32-126
|
|
/// Category: unit
|
|
#[test]
|
|
fn prop_all_ascii_chars() {
|
|
// Test with all printable ASCII characters
|
|
let s: String = (32..127).map(|c| c as u8 as char).collect();
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(s.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(s));
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Boolean values preserved exactly
|
|
///
|
|
/// Verifies: Both true and false transmitted without modification
|
|
/// Strategy: Random bool values
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_bool_preservation(b: bool) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(b).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(b));
|
|
}
|
|
}
|
|
|
|
// Custom struct for testing with proptest Strategy
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
struct CustomStruct {
|
|
id: i32,
|
|
name: String,
|
|
active: bool,
|
|
}
|
|
|
|
// Strategy for generating CustomStruct
|
|
fn arb_custom_struct() -> impl Strategy<Value = CustomStruct> {
|
|
(any::<i32>(), arb_string(), any::<bool>()).prop_map(|(id, name, active)| CustomStruct {
|
|
id,
|
|
name,
|
|
active,
|
|
})
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Custom user-defined structs preserved with all fields
|
|
///
|
|
/// Verifies: User-defined struct maintains all field values
|
|
/// Strategy: Random CustomStruct with arbitrary fields
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_custom_struct_preservation(custom in arb_custom_struct()) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(custom.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(custom));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Empty and non-empty vectors handled correctly
|
|
///
|
|
/// Verifies: Both empty and populated vectors preserved correctly
|
|
/// Strategy: Boolean choice between empty vec and populated vec
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_empty_vec_handled(empty: bool) {
|
|
let vec: Vec<i32> = if empty { Vec::new() } else { vec![1, 2, 3] };
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(vec.clone()).unwrap();
|
|
|
|
let received = listener.try_listen().unwrap();
|
|
|
|
assert_eq!(received.is_empty(), vec.is_empty());
|
|
assert_eq!(received, vec);
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Fixed-size vector simulation preserved
|
|
///
|
|
/// Verifies: Vectors of exact length transmitted correctly
|
|
/// Strategy: Generates vec, takes exactly 5 elements to simulate array
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_fixed_size_vec_preservation(values in prop::collection::vec(any::<i32>(), 5..20)) {
|
|
// Use exactly 5 elements to simulate fixed-size array behavior
|
|
let vec: Vec<i32> = values.into_iter().take(5).collect();
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(vec.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(vec));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: is_announced consistent across multiple announcement clones
|
|
///
|
|
/// Verifies: All cloned announcements show identical is_announced state
|
|
/// Strategy: Random i32 value, checks 3 announcement clones
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_is_announced_consistent_across_clones(value: i32) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
let announcement2 = announcement.clone();
|
|
let announcement3 = announcement.clone();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
assert!(announcement.is_announced());
|
|
assert!(announcement2.is_announced());
|
|
assert!(announcement3.is_announced());
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Listeners dropped before announce don't prevent new ones
|
|
///
|
|
/// Verifies: Listener lifecycle independence and cleanup correct
|
|
/// Strategy: Random i32 value, drop listener then create new one
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_listener_dropped_before_announce(value: i32) {
|
|
let (announcer, announcement) = Announcement::new();
|
|
|
|
// Create and drop listener
|
|
{
|
|
let _listener = announcement.listener();
|
|
}
|
|
|
|
// Create new listener after drop
|
|
let listener2 = announcement.listener();
|
|
|
|
announcer.announce(value).unwrap();
|
|
|
|
assert_eq!(listener2.try_listen(), Some(value));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Nested custom type structures preserved
|
|
///
|
|
/// Verifies: Nested user-defined types maintain structure and values
|
|
/// Strategy: Random outer id and inner CustomStruct
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_nested_custom_types(outer_id: i32, inner in arb_custom_struct()) {
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
struct Nested {
|
|
id: i32,
|
|
inner: CustomStruct,
|
|
}
|
|
|
|
let nested = Nested {
|
|
id: outer_id,
|
|
inner: inner.clone(),
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(nested.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(nested));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Option with explicit Some and None variants work
|
|
///
|
|
/// Verifies: Both Some(value) and None cases handled correctly
|
|
/// Strategy: Boolean choice and random i32 value
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_option_explicit_some_none(has_value: bool, value: i32) {
|
|
let opt = if has_value { Some(value) } else { None };
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(opt).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(opt));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Result with explicit Ok and Err variants work
|
|
///
|
|
/// Verifies: Both Ok(value) and Err(error) cases handled correctly
|
|
/// Strategy: Boolean choice, random i32 and String
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_result_explicit_ok_err(is_ok: bool, ok_val: i32, err_val in arb_string()) {
|
|
let result: Result<i32, String> = if is_ok {
|
|
Ok(ok_val)
|
|
} else {
|
|
Err(err_val.clone())
|
|
};
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(result.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(result));
|
|
}
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(1024))]
|
|
|
|
/// Property: Vector of custom types preserved
|
|
///
|
|
/// Verifies: Collections of user-defined types transmitted correctly
|
|
/// Strategy: Random count 0-20, generates CustomStruct instances
|
|
/// Category: property
|
|
#[test]
|
|
fn prop_vec_of_custom_types(count in 0usize..20) {
|
|
let customs: Vec<CustomStruct> = (0..count)
|
|
.map(|i| CustomStruct {
|
|
id: i as i32,
|
|
name: format!("item_{}", i),
|
|
active: i % 2 == 0,
|
|
})
|
|
.collect();
|
|
|
|
let (announcer, announcement) = Announcement::new();
|
|
let listener = announcement.listener();
|
|
|
|
announcer.announce(customs.clone()).unwrap();
|
|
|
|
assert_eq!(listener.try_listen(), Some(customs));
|
|
}
|
|
}
|