#![cfg(loom)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering::Relaxed};
use char_str::{CharStr, CharString};
use loom::thread;
const EXACT_TEXT_LEN: usize = 4099;
const COW_TEXT_LEN: usize = 2053;
const COW_CAPACITY: usize = 8209;
const COW_DETACHED_CAPACITY: usize = COW_TEXT_LEN * 3 / 2;
const CONVERSION_TEXT_LEN: usize = 6151;
const CONVERSION_CAPACITY: usize = 12_347;
const EXACT_HEADER_SIZE: usize = size_of::<loom::sync::atomic::AtomicUsize>();
const GROWABLE_HEADER_SIZE: usize = EXACT_HEADER_SIZE + size_of::<usize>();
const EXACT_ALLOCATION_SIZE: usize = EXACT_HEADER_SIZE + EXACT_TEXT_LEN;
const COW_ALLOCATION_SIZE: usize = GROWABLE_HEADER_SIZE + COW_CAPACITY;
const COW_DETACHED_ALLOCATION_SIZE: usize = GROWABLE_HEADER_SIZE + COW_DETACHED_CAPACITY;
const CONVERSION_ALLOCATION_SIZE: usize = GROWABLE_HEADER_SIZE + CONVERSION_CAPACITY;
const CONVERSION_EXACT_ALLOCATION_SIZE: usize = EXACT_HEADER_SIZE + CONVERSION_TEXT_LEN;
const CONVERSION_THAWED_ALLOCATION_SIZE: usize = GROWABLE_HEADER_SIZE + CONVERSION_TEXT_LEN;
const _: () = {
let sizes = [
EXACT_ALLOCATION_SIZE,
COW_ALLOCATION_SIZE,
COW_DETACHED_ALLOCATION_SIZE,
CONVERSION_ALLOCATION_SIZE,
CONVERSION_EXACT_ALLOCATION_SIZE,
CONVERSION_THAWED_ALLOCATION_SIZE,
];
let mut first = 0;
while first < sizes.len() {
let mut second = first + 1;
while second < sizes.len() {
assert!(sizes[first] != sizes[second]);
second += 1;
}
first += 1;
}
};
struct AllocationTracker {
size: usize,
seen: AtomicBool,
live: AtomicIsize,
}
impl AllocationTracker {
const fn new(size: usize) -> Self {
Self { size, seen: AtomicBool::new(false), live: AtomicIsize::new(0) }
}
}
static TRACKERS: [AllocationTracker; 6] = [
AllocationTracker::new(EXACT_ALLOCATION_SIZE),
AllocationTracker::new(COW_ALLOCATION_SIZE),
AllocationTracker::new(COW_DETACHED_ALLOCATION_SIZE),
AllocationTracker::new(CONVERSION_ALLOCATION_SIZE),
AllocationTracker::new(CONVERSION_EXACT_ALLOCATION_SIZE),
AllocationTracker::new(CONVERSION_THAWED_ALLOCATION_SIZE),
];
struct TrackingAllocator;
#[global_allocator]
static ALLOCATOR: TrackingAllocator = TrackingAllocator;
static TRACK_ALLOCATIONS: AtomicBool = AtomicBool::new(false);
fn tracker(size: usize) -> Option<&'static AllocationTracker> {
TRACKERS.iter().find(|tracker| tracker.size == size)
}
fn track_allocation(size: usize) {
if let Some(tracker) = tracker(size) {
tracker.seen.store(true, Relaxed);
tracker.live.fetch_add(1, Relaxed);
}
}
fn track_deallocation(size: usize) {
if let Some(tracker) = tracker(size) {
tracker.live.fetch_sub(1, Relaxed);
}
}
fn start_tracking() {
for tracker in &TRACKERS {
tracker.seen.store(false, Relaxed);
tracker.live.store(0, Relaxed);
}
TRACK_ALLOCATIONS.store(true, Relaxed);
}
fn stop_tracking(expected_sizes: &[usize]) {
TRACK_ALLOCATIONS.store(false, Relaxed);
for tracker in &TRACKERS {
assert_eq!(tracker.live.load(Relaxed), 0, "allocation size {} was leaked", tracker.size);
}
for size in expected_sizes {
assert!(tracker(*size).unwrap().seen.load(Relaxed), "allocation size {size} was not seen");
}
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() && TRACK_ALLOCATIONS.load(Relaxed) {
track_allocation(layout.size());
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if TRACK_ALLOCATIONS.load(Relaxed) {
track_deallocation(layout.size());
}
unsafe { System.dealloc(ptr, layout) };
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_ptr = unsafe { System.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() && TRACK_ALLOCATIONS.load(Relaxed) {
track_deallocation(layout.size());
track_allocation(new_size);
}
new_ptr
}
}
macro_rules! loom_test {
(fn $name:ident() { $($tt:tt)* }) => {
#[test]
fn $name() {
loom::model(|| {
$($tt)*
});
}
};
}
#[test]
fn concurrent_exact_clone_and_drop() {
loom::model(|| {
let text = "a".repeat(EXACT_TEXT_LEN);
start_tracking();
{
let original = CharStr::from(text.as_str());
let shared = original.clone();
let thread = thread::spawn(move || {
let clone = shared.clone();
assert_eq!(clone.len(), EXACT_TEXT_LEN);
assert!(clone.bytes().all(|byte| byte == b'a'));
});
let clone = original.clone();
assert_eq!(clone.len(), EXACT_TEXT_LEN);
assert!(clone.bytes().all(|byte| byte == b'a'));
thread.join().unwrap();
}
stop_tracking(&[EXACT_ALLOCATION_SIZE]);
});
}
#[test]
fn concurrent_growable_cow_and_drop() {
loom::model(|| {
let text = "a".repeat(COW_TEXT_LEN);
start_tracking();
{
let mut original = CharString::with_capacity(COW_CAPACITY);
original.push_str(&text);
let shared = original.clone();
let thread = thread::spawn(move || {
let mut detached = shared.clone();
detached.push('b');
assert_eq!(shared.len(), COW_TEXT_LEN);
assert_eq!(detached.len(), COW_TEXT_LEN + 1);
});
original.push('c');
assert_eq!(original.len(), COW_TEXT_LEN + 1);
thread.join().unwrap();
}
stop_tracking(&[COW_ALLOCATION_SIZE, COW_DETACHED_ALLOCATION_SIZE]);
});
}
#[test]
fn concurrent_freeze_thaw_and_drop() {
loom::model(|| {
let text = "a".repeat(CONVERSION_TEXT_LEN);
start_tracking();
{
let mut growable = CharString::with_capacity(CONVERSION_CAPACITY);
growable.push_str(&text);
let shared_growable = growable.clone();
let thread = thread::spawn(move || drop(shared_growable));
let frozen = growable.freeze();
assert_eq!(frozen.len(), CONVERSION_TEXT_LEN);
thread.join().unwrap();
let shared_frozen = frozen.clone();
let thread = thread::spawn(move || drop(shared_frozen));
let thawed = frozen.into_char_string();
assert_eq!(thawed.len(), CONVERSION_TEXT_LEN);
thread.join().unwrap();
}
stop_tracking(&[
CONVERSION_ALLOCATION_SIZE,
CONVERSION_EXACT_ALLOCATION_SIZE,
CONVERSION_THAWED_ALLOCATION_SIZE,
]);
});
}
loom_test! {
fn concurrent_frozen_clone_and_thaw() {
let frozen = CharStr::from("a frozen string longer than the inline limit");
let shared = frozen.clone();
let th = thread::spawn(move || {
let clone = shared.clone();
assert_eq!(clone, "a frozen string longer than the inline limit");
});
let mut thawed = frozen.into_char_string();
thawed.push('!');
assert_eq!(thawed, "a frozen string longer than the inline limit!");
th.join().unwrap();
}
}
loom_test! {
fn concurrent_drop_and_thaw() {
let frozen = CharStr::from("a frozen string longer than the inline limit");
let shared = frozen.clone();
let th = thread::spawn(move || {
drop(shared);
});
let thawed = frozen.into_char_string();
assert_eq!(thawed, "a frozen string longer than the inline limit");
th.join().unwrap();
}
}
loom_test! {
fn concurrent_drop_and_freeze() {
let mut string = CharString::with_capacity(128);
string.push_str("a string longer than the inline limit");
let shared = string.clone();
let th = thread::spawn(move || {
drop(shared);
});
let frozen = string.freeze();
assert_eq!(frozen, "a string longer than the inline limit");
th.join().unwrap();
}
}
loom_test! {
fn concurrent_push() {
let mut one = CharString::from("12345678901234567890");
let two = one.clone();
let th = thread::spawn(move || {
let mut three = two.clone();
three.push('a');
assert_eq!(two, "12345678901234567890");
assert_eq!(three, "12345678901234567890a");
});
one.push('a');
assert_eq!(one, "12345678901234567890a");
th.join().unwrap();
}
}
loom_test! {
fn concurrent_remove() {
let mut one = CharString::from("abcdefghijklmnopqrstuvwxyz");
let two = one.clone();
let th = thread::spawn(move || {
let mut three = two.clone();
assert_eq!(three.remove(3), 'd');
assert_eq!(two, "abcdefghijklmnopqrstuvwxyz");
assert_eq!(three, "abcefghijklmnopqrstuvwxyz");
});
assert_eq!(one.remove(3), 'd');
assert_eq!(one, "abcefghijklmnopqrstuvwxyz");
th.join().unwrap();
}
}