feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,324 @@
|
||||
//! Tests for concurrent access patterns and race conditions
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_result_retrieval() -> DagResult<()> {
|
||||
// Test concurrent get() calls after execution
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
|
||||
// Add tasks with minimal delays
|
||||
let tasks: Vec<dagx::TaskHandle<usize>> = (0..10)
|
||||
.map(|i| {
|
||||
let task = dag.add_task(task_fn(move |_: ()| async move {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
||||
i as usize
|
||||
}));
|
||||
(&task).into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Execute the DAG
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Now try to get results concurrently from multiple threads
|
||||
let mut get_handles = Vec::new();
|
||||
for (i, task) in tasks.iter().enumerate() {
|
||||
let dag_clone = dag.clone();
|
||||
let task = *task;
|
||||
get_handles.push(tokio::spawn(async move {
|
||||
// Results should be available immediately
|
||||
dag_clone.get(task).map(|v| (i, v))
|
||||
}));
|
||||
}
|
||||
|
||||
// Verify all gets succeeded
|
||||
for handle in get_handles {
|
||||
let (i, result) = handle.await.unwrap()?;
|
||||
assert_eq!(result, i);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_simultaneous_dag_building_and_execution() -> DagResult<()> {
|
||||
// Test building DAG while another part is executing
|
||||
let dag = Arc::new(DagRunner::new());
|
||||
let building_complete = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Start with a few initial tasks
|
||||
let initial: Vec<_> = (0..5)
|
||||
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
|
||||
.collect();
|
||||
|
||||
// Start building more tasks in background
|
||||
let dag_build = dag.clone();
|
||||
let building = building_complete.clone();
|
||||
let build_handle = tokio::spawn(async move {
|
||||
let mut tasks: Vec<dagx::TaskHandle<i32>> = Vec::new();
|
||||
for i in 5..50 {
|
||||
let task = dag_build.add_task(task_fn(move |_: ()| async move { i }));
|
||||
tasks.push((&task).into());
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
|
||||
}
|
||||
building.store(true, Ordering::SeqCst);
|
||||
tasks
|
||||
});
|
||||
|
||||
// Wait a bit for some tasks to be added
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
|
||||
// Execute the DAG
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Get the additional tasks
|
||||
let additional_tasks = build_handle.await.unwrap();
|
||||
|
||||
// Initial tasks should have results
|
||||
for (i, task) in initial.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, i);
|
||||
}
|
||||
|
||||
// Additional tasks added during execution won't have results yet
|
||||
// (they need another run cycle)
|
||||
if building_complete.load(Ordering::SeqCst) {
|
||||
// Run again for the new tasks
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Now they should have results
|
||||
for (i, task) in additional_tasks.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, (i + 5) as i32);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_dependency_resolution() -> DagResult<()> {
|
||||
// Test multiple tasks resolving dependencies concurrently
|
||||
let dag = DagRunner::new();
|
||||
let resolution_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create a source task
|
||||
let source = dag.add_task(task_fn({
|
||||
let counter = resolution_counter.clone();
|
||||
move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
42
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Create many tasks depending on the same source
|
||||
let dependents: Vec<_> = (0..100)
|
||||
.map(|i| {
|
||||
let counter = resolution_counter.clone();
|
||||
dag.add_task(task_fn(move |val: i32| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
val + i
|
||||
}
|
||||
}))
|
||||
.depends_on(&source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check all completed
|
||||
assert_eq!(resolution_counter.load(Ordering::SeqCst), 101);
|
||||
|
||||
// Verify results
|
||||
for (i, task) in dependents.iter().enumerate() {
|
||||
assert_eq!(dag.get(task)?, 42 + i as i32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_read_write_race_patterns() -> DagResult<()> {
|
||||
// Test read-write patterns with shared state
|
||||
let dag = DagRunner::new();
|
||||
let shared_state = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
// Writers
|
||||
let writers: Vec<_> = (0..10)
|
||||
.map(|i| {
|
||||
let state = shared_state.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
let mut guard = state.write().await;
|
||||
guard.push(i);
|
||||
tokio::task::yield_now().await; // Force potential race
|
||||
guard.push(i * 10);
|
||||
i
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Readers (depend on writers to ensure ordering)
|
||||
let readers: Vec<_> = writers
|
||||
.iter()
|
||||
.map(|writer| {
|
||||
let state = shared_state.clone();
|
||||
dag.add_task(task_fn(move |writer_id: i32| {
|
||||
let state = state.clone();
|
||||
async move {
|
||||
let guard = state.read().await;
|
||||
let contains_writer = guard.contains(&writer_id);
|
||||
let len = guard.len();
|
||||
(writer_id, contains_writer, len)
|
||||
}
|
||||
}))
|
||||
.depends_on(writer)
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Verify readers saw their writer's data
|
||||
for reader in &readers {
|
||||
let (writer_id, saw_writer, _len) = dag.get(reader)?;
|
||||
assert!(saw_writer, "Reader didn't see writer {}", writer_id);
|
||||
}
|
||||
|
||||
// Final state should have all writes
|
||||
let final_state = shared_state.read().await;
|
||||
assert_eq!(final_state.len(), 20); // 10 writers * 2 writes each
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_atomic_counter_races() -> DagResult<()> {
|
||||
// Test atomic operations under high concurrency
|
||||
let dag = DagRunner::new();
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
let max_seen = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Create tasks that increment and track max
|
||||
let _tasks: Vec<_> = (0..200)
|
||||
.map(|_| {
|
||||
let counter = counter.clone();
|
||||
let max_seen = max_seen.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
let max_seen = max_seen.clone();
|
||||
async move {
|
||||
let mut local_max = 0;
|
||||
for _ in 0..100 {
|
||||
let val = counter.fetch_add(1, Ordering::SeqCst);
|
||||
local_max = local_max.max(val);
|
||||
|
||||
// Update global max
|
||||
let mut current_max = max_seen.load(Ordering::SeqCst);
|
||||
while val > current_max {
|
||||
match max_seen.compare_exchange_weak(
|
||||
current_max,
|
||||
val,
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(x) => current_max = x,
|
||||
}
|
||||
}
|
||||
}
|
||||
local_max
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Final counter should be 200 * 100 = 20000
|
||||
assert_eq!(counter.load(Ordering::SeqCst), 20000);
|
||||
|
||||
// Max seen should be at least 19999 (last increment - 1)
|
||||
assert!(max_seen.load(Ordering::SeqCst) >= 19999);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_dag_runners_interaction() -> DagResult<()> {
|
||||
// Test multiple DAG runners potentially interfering
|
||||
let shared_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for dag_id in 0..5 {
|
||||
let counter = shared_counter.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let tasks: Vec<_> = (0..20)
|
||||
.map(|task_id| {
|
||||
let counter = counter.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let counter = counter.clone();
|
||||
async move {
|
||||
counter.fetch_add(1, Ordering::SeqCst);
|
||||
(dag_id, task_id)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Collect results
|
||||
let mut results = Vec::new();
|
||||
for task in tasks {
|
||||
results.push(dag.get(&task)?);
|
||||
}
|
||||
|
||||
Ok::<_, dagx::DagError>(results)
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all DAGs to complete
|
||||
for handle in handles {
|
||||
handle.await.unwrap()?;
|
||||
}
|
||||
|
||||
// Should have executed 5 * 20 = 100 tasks total
|
||||
assert_eq!(shared_counter.load(Ordering::SeqCst), 100);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
//! Tests for forced execution interleaving patterns
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forced_interleaving_with_barriers() -> DagResult<()> {
|
||||
// Force specific interleaving using barriers
|
||||
let dag = DagRunner::new();
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let order = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Three parallel tasks that synchronize at barrier
|
||||
let t1 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t1_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t1_end");
|
||||
1
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let t2 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t2_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t2_end");
|
||||
2
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let t3 = dag.add_task(task_fn({
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
move |_: ()| {
|
||||
let barrier = barrier.clone();
|
||||
let order = order.clone();
|
||||
async move {
|
||||
order.lock().push("t3_start");
|
||||
barrier.wait().await;
|
||||
order.lock().push("t3_end");
|
||||
3
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Collector
|
||||
let collector = dag
|
||||
.add_task(task_fn(
|
||||
|(a, b, c): (i32, i32, i32)| async move { a + b + c },
|
||||
))
|
||||
.depends_on((&t1, &t2, &t3));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(collector)?, 6);
|
||||
|
||||
// Check interleaving pattern
|
||||
let events = order.lock().clone();
|
||||
assert_eq!(events.len(), 6);
|
||||
|
||||
// All starts should come before all ends due to barrier
|
||||
let start_count = events[0..3]
|
||||
.iter()
|
||||
.filter(|e| e.ends_with("_start"))
|
||||
.count();
|
||||
assert_eq!(start_count, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alternating_execution_pattern() -> DagResult<()> {
|
||||
// Create an alternating execution pattern
|
||||
let dag = DagRunner::new();
|
||||
let turn = Arc::new(AtomicUsize::new(0));
|
||||
let events: Arc<parking_lot::Mutex<Vec<String>>> =
|
||||
Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Two chains that alternate execution
|
||||
let first_a = dag.add_task(task_fn({
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
while turn.load(Ordering::SeqCst) != 0 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("A0".to_string());
|
||||
turn.store(1, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut chain_a: dagx::TaskHandle<i32> = (&first_a).into();
|
||||
|
||||
let first_b = dag.add_task(task_fn({
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let turn = turn.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
while turn.load(Ordering::SeqCst) != 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B0".to_string());
|
||||
turn.store(2, Ordering::SeqCst);
|
||||
0
|
||||
}
|
||||
}
|
||||
}));
|
||||
let mut chain_b: dagx::TaskHandle<i32> = (&first_b).into();
|
||||
|
||||
for i in 1..5 {
|
||||
let turn_clone = turn.clone();
|
||||
let events_clone = events.clone();
|
||||
chain_a = dag
|
||||
.add_task(task_fn(move |x: i32| {
|
||||
let turn_clone = turn_clone.clone();
|
||||
let events_clone = events_clone.clone();
|
||||
async move {
|
||||
while turn_clone.load(Ordering::SeqCst) != i * 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events_clone.lock().push(format!("A{}", i));
|
||||
turn_clone.store(i * 2 + 1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(chain_a);
|
||||
|
||||
let turn_clone = turn.clone();
|
||||
let events_clone = events.clone();
|
||||
chain_b = dag
|
||||
.add_task(task_fn(move |x: i32| {
|
||||
let turn_clone = turn_clone.clone();
|
||||
let events_clone = events_clone.clone();
|
||||
async move {
|
||||
while turn_clone.load(Ordering::SeqCst) != i * 2 + 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events_clone.lock().push(format!("B{}", i));
|
||||
turn_clone.store(i * 2 + 2, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}))
|
||||
.depends_on(chain_b);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Check alternating pattern
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(
|
||||
event_list,
|
||||
vec!["A0", "B0", "A1", "B1", "A2", "B2", "A3", "B3", "A4", "B4"]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layered_interleaving() -> DagResult<()> {
|
||||
// Test interleaving across DAG layers
|
||||
let dag = DagRunner::new();
|
||||
let layer_active = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// Layer 1: 4 tasks
|
||||
let layer1: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
let active = layer_active.clone();
|
||||
dag.add_task(task_fn(move |_: ()| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
assert_eq!(active.fetch_add(1, Ordering::SeqCst), i);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
i as i32
|
||||
}
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: 2 tasks, each depends on 2 from layer1
|
||||
let layer2: Vec<_> = (0..2)
|
||||
.map(|i| {
|
||||
let active = layer_active.clone();
|
||||
let idx = i * 2;
|
||||
dag.add_task(task_fn(move |(a, b): (i32, i32)| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
active.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
a + b
|
||||
}
|
||||
}))
|
||||
.depends_on((&layer1[idx], &layer1[idx + 1]))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 3: 1 task depends on both layer2
|
||||
let layer3 = dag
|
||||
.add_task(task_fn({
|
||||
let active = layer_active.clone();
|
||||
move |(a, b): (i32, i32)| {
|
||||
let active = active.clone();
|
||||
async move {
|
||||
active.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::task::yield_now().await;
|
||||
active.fetch_sub(1, Ordering::SeqCst);
|
||||
a + b
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on((&layer2[0], &layer2[1]));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Sum of 0+1+2+3 = 6
|
||||
assert_eq!(dag.get(layer3)?, 6);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cross_branch_synchronization() -> DagResult<()> {
|
||||
// Test synchronization between parallel branches
|
||||
let dag = DagRunner::new();
|
||||
let sync_point = Arc::new(AtomicUsize::new(0));
|
||||
let events = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
// Branch A
|
||||
let a1 = dag.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
events.lock().push("A1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
1
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let a2 = dag
|
||||
.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |x: i32| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for B1
|
||||
while sync.load(Ordering::SeqCst) < 2 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("A2");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&a1);
|
||||
|
||||
// Branch B
|
||||
let b1 = dag.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |_: ()| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for A1
|
||||
while sync.load(Ordering::SeqCst) < 1 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B1");
|
||||
sync.fetch_add(1, Ordering::SeqCst);
|
||||
10
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let b2 = dag
|
||||
.add_task(task_fn({
|
||||
let sync = sync_point.clone();
|
||||
let events = events.clone();
|
||||
move |x: i32| {
|
||||
let sync = sync.clone();
|
||||
let events = events.clone();
|
||||
async move {
|
||||
// Wait for A2
|
||||
while sync.load(Ordering::SeqCst) < 3 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
events.lock().push("B2");
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
}))
|
||||
.depends_on(&b1);
|
||||
|
||||
// Merge
|
||||
let merge = dag
|
||||
.add_task(task_fn(|(a, b): (i32, i32)| async move { a + b }))
|
||||
.depends_on((&a2, &b2));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(merge)?, 13); // 2 + 11
|
||||
|
||||
// Check forced interleaving
|
||||
let event_list = events.lock().clone();
|
||||
assert_eq!(event_list, vec!["A1", "B1", "A2", "B2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Timing and race condition tests
|
||||
//!
|
||||
//! Tests for temporal edge cases, execution ordering, and race conditions.
|
||||
|
||||
mod concurrent_access;
|
||||
mod interleaving;
|
||||
Reference in new issue
Block a user