mod common;
use common::MockConnectionFactory;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use sz_orm_core::TransactOptions;
use sz_orm_core::Transaction;
use sz_orm_core::{Pool, PoolConfigBuilder};
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum FormalTxState {
Active,
Committed,
RolledBack,
}
impl FormalTxState {
fn is_terminal(&self) -> bool {
matches!(self, FormalTxState::Committed | FormalTxState::RolledBack)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum FormalOp {
Execute,
Commit,
Rollback,
Savepoint,
Query,
RollbackToSavepoint,
ReleaseSavepoint,
}
const ALL_OPS: [FormalOp; 7] = [
FormalOp::Execute,
FormalOp::Commit,
FormalOp::Rollback,
FormalOp::Savepoint,
FormalOp::Query,
FormalOp::RollbackToSavepoint,
FormalOp::ReleaseSavepoint,
];
const ALL_STATES: [FormalTxState; 3] = [
FormalTxState::Active,
FormalTxState::Committed,
FormalTxState::RolledBack,
];
fn formal_transition(state: FormalTxState, op: FormalOp) -> (FormalTxState, bool) {
match (state, op) {
(FormalTxState::Active, FormalOp::Execute) => (FormalTxState::Active, true),
(FormalTxState::Active, FormalOp::Query) => (FormalTxState::Active, true),
(FormalTxState::Active, FormalOp::Savepoint) => (FormalTxState::Active, true),
(FormalTxState::Active, FormalOp::RollbackToSavepoint) => (FormalTxState::Active, true),
(FormalTxState::Active, FormalOp::ReleaseSavepoint) => (FormalTxState::Active, true),
(FormalTxState::Active, FormalOp::Commit) => (FormalTxState::Committed, true),
(FormalTxState::Active, FormalOp::Rollback) => (FormalTxState::RolledBack, true),
(FormalTxState::Committed, _) => (FormalTxState::Committed, false),
(FormalTxState::RolledBack, _) => (FormalTxState::RolledBack, false),
}
}
async fn actual_transition(
tx: &mut Transaction,
state_before: FormalTxState,
op: FormalOp,
) -> (FormalTxState, bool) {
let result = match op {
FormalOp::Execute => tx.execute("SELECT 1").await.map(|_| ()),
FormalOp::Query => tx.query("SELECT 1").await.map(|_| ()),
FormalOp::Savepoint => tx.savepoint().await.map(|_| ()),
FormalOp::Commit => tx.commit().await,
FormalOp::Rollback => tx.rollback().await,
FormalOp::RollbackToSavepoint => tx.rollback_to_savepoint("sp_test").await.map(|_| ()),
FormalOp::ReleaseSavepoint => tx.release_savepoint("sp_test").await.map(|_| ()),
};
let success = result.is_ok();
let state_after = match tx.state() {
sz_orm_core::TransactionState::Active => FormalTxState::Active,
sz_orm_core::TransactionState::Committed => FormalTxState::Committed,
sz_orm_core::TransactionState::RolledBack => FormalTxState::RolledBack,
};
if state_before.is_terminal() {
assert_eq!(
state_after, state_before,
"I7 violated: terminal state {:?} changed to {:?} after {:?}",
state_before, state_after, op
);
}
if state_before.is_terminal() {
assert!(
!success,
"I1 violated: operation {:?} succeeded in terminal state {:?}",
op, state_before
);
}
(state_after, success)
}
fn make_tx() -> Transaction {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = common::MockConnection::new(db);
Transaction::new(Box::new(conn), TransactOptions::default())
}
#[tokio::test]
async fn formal_committed_state_rejects_all_ops() {
for op in ALL_OPS.iter() {
let mut tx = make_tx();
tx.commit().await.unwrap();
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Committed,
"precondition: should be Committed"
);
let state_before = FormalTxState::Committed;
let (state_after, success) = actual_transition(&mut tx, state_before, *op).await;
assert!(
!success,
"I1 failed: {:?} should not succeed in Committed state",
op
);
assert_eq!(
state_after,
FormalTxState::Committed,
"I2 failed: state changed from Committed to {:?} after {:?}",
state_after,
op
);
}
}
#[tokio::test]
async fn formal_rolledback_state_rejects_all_ops() {
for op in ALL_OPS.iter() {
let mut tx = make_tx();
tx.rollback().await.unwrap();
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::RolledBack,
"precondition: should be RolledBack"
);
let state_before = FormalTxState::RolledBack;
let (state_after, success) = actual_transition(&mut tx, state_before, *op).await;
assert!(
!success,
"I1 failed: {:?} should not succeed in RolledBack state",
op
);
assert_eq!(
state_after,
FormalTxState::RolledBack,
"I2 failed: state changed from RolledBack to {:?} after {:?}",
state_after,
op
);
}
}
#[tokio::test]
async fn formal_state_monotonic_progress() {
let mut tx = make_tx();
assert!(tx.is_active());
tx.commit().await.unwrap();
assert_eq!(tx.state(), sz_orm_core::TransactionState::Committed);
for op in ALL_OPS.iter() {
let _ = actual_transition(&mut tx, FormalTxState::Committed, *op).await;
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Committed,
"monotonicity violated after {:?}",
op
);
}
let mut tx2 = make_tx();
tx2.rollback().await.unwrap();
assert_eq!(tx2.state(), sz_orm_core::TransactionState::RolledBack);
for op in ALL_OPS.iter() {
let _ = actual_transition(&mut tx2, FormalTxState::RolledBack, *op).await;
assert_eq!(
tx2.state(),
sz_orm_core::TransactionState::RolledBack,
"monotonicity violated after {:?}",
op
);
}
}
#[tokio::test]
async fn formal_state_machine_matches_specification() {
for &initial_state in ALL_STATES.iter() {
for &op in ALL_OPS.iter() {
let mut tx = make_tx();
match initial_state {
FormalTxState::Active => { }
FormalTxState::Committed => {
tx.commit().await.unwrap();
}
FormalTxState::RolledBack => {
tx.rollback().await.unwrap();
}
}
let actual_initial = match tx.state() {
sz_orm_core::TransactionState::Active => FormalTxState::Active,
sz_orm_core::TransactionState::Committed => FormalTxState::Committed,
sz_orm_core::TransactionState::RolledBack => FormalTxState::RolledBack,
};
assert_eq!(
actual_initial, initial_state,
"failed to set up initial state {:?}",
initial_state
);
let (actual_new_state, actual_success) =
actual_transition(&mut tx, initial_state, op).await;
let (expected_new_state, expected_success) = formal_transition(initial_state, op);
assert_eq!(
actual_success, expected_success,
"success mismatch: state={:?} op={:?} expected_success={} actual_success={}",
initial_state, op, expected_success, actual_success
);
assert_eq!(
actual_new_state, expected_new_state,
"state mismatch: state={:?} op={:?} expected={:?} actual={:?}",
initial_state, op, expected_new_state, actual_new_state
);
}
}
}
#[tokio::test]
async fn formal_savepoint_counter_monotonic() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let conn = common::MockConnection::new(db);
let opts = TransactOptions::default().with_max_nesting_depth(20);
let mut tx = Transaction::new(Box::new(conn), opts);
let mut prev_n: u32 = 0;
for _ in 0..10 {
let name = tx.savepoint().await.unwrap();
let n: u32 = name.trim_start_matches("sp_").parse().unwrap();
assert!(
n > prev_n,
"savepoint counter not monotonic: prev={} current={}",
prev_n,
n
);
prev_n = n;
}
}
#[tokio::test]
async fn formal_random_operation_sequence_invariants() {
let mut rng = common::Rng::new(12345);
for seq_id in 0..20 {
let mut tx = make_tx();
let mut current_state = FormalTxState::Active;
let seq_len = 5 + rng.next_usize(10);
for step in 0..seq_len {
let op = ALL_OPS[rng.next_usize(ALL_OPS.len())];
let (new_state, success) = actual_transition(&mut tx, current_state, op).await;
let (expected_state, expected_success) = formal_transition(current_state, op);
assert_eq!(
success, expected_success,
"seq={} step={} op={:?}: success mismatch",
seq_id, step, op
);
assert_eq!(
new_state, expected_state,
"seq={} step={} op={:?}: state mismatch",
seq_id, step, op
);
current_state = new_state;
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn formal_pool_active_never_exceeds_max_size() {
let max_size: u32 = 5;
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(max_size)
.min_idle(0)
.acquire_timeout(1)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let max_observed = Arc::new(AtomicU32::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let max_obs = max_observed.clone();
handles.push(tokio::spawn(async move {
for _ in 0..30 {
if let Ok(conn) = pool.acquire().await {
let status = pool.status().await;
max_obs.fetch_max(status.active, Ordering::SeqCst);
assert!(
status.active <= max_size,
"I3 violated: active={} > max_size={}",
status.active,
max_size
);
tokio::task::yield_now().await;
pool.release(conn).await;
}
}
}));
}
for h in handles {
h.await.unwrap();
}
assert!(
max_observed.load(Ordering::SeqCst) <= max_size,
"I3 violated: max observed active = {}",
max_observed.load(Ordering::SeqCst)
);
}
#[tokio::test]
async fn formal_pool_active_equals_idle_plus_borrowed() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(10)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let mut borrowed = Vec::new();
for _ in 0..3 {
borrowed.push(pool.acquire().await.unwrap());
}
let status = pool.status().await;
assert_eq!(status.active, 3, "active should be 3 after borrowing 3");
assert_eq!(status.idle, 0, "idle should be 0");
let borrowed_count = status.active - status.idle;
assert_eq!(
borrowed_count, 3,
"borrowed count = active - idle should be 3"
);
let conn1 = borrowed.pop().unwrap();
let conn2 = borrowed.pop().unwrap();
pool.release(conn1).await;
pool.release(conn2).await;
let status = pool.status().await;
assert_eq!(status.active, 3, "active should be 3 (2 idle + 1 borrowed)");
assert_eq!(status.idle, 2, "idle should be 2");
let borrowed_count = status.active - status.idle;
assert_eq!(borrowed_count, 1, "borrowed count should be 1");
let conn3 = borrowed.pop().unwrap();
pool.release(conn3).await;
let status = pool.status().await;
assert_eq!(status.active, 3, "active should be 3 (3 idle + 0 borrowed)");
assert_eq!(status.idle, 3, "idle should be 3");
let borrowed_count = status.active - status.idle;
assert_eq!(borrowed_count, 0, "borrowed count should be 0");
}
#[tokio::test]
async fn formal_pool_close_all_invariant() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(5)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let c1 = pool.acquire().await.unwrap();
let c2 = pool.acquire().await.unwrap();
let c3 = pool.acquire().await.unwrap();
let status = pool.status().await;
assert_eq!(status.active, 3);
assert_eq!(status.idle, 0);
pool.close_all().await;
let status = pool.status().await;
assert_eq!(status.idle, 0, "idle should be 0 after close_all");
assert_eq!(
status.active, 3,
"active should still be 3 (borrowed not returned)"
);
pool.release(c1).await;
let status = pool.status().await;
assert_eq!(status.active, 2, "active should be 2 after releasing 1");
assert_eq!(
status.idle, 0,
"idle should still be 0 (closed, not returned)"
);
pool.release(c2).await;
pool.release(c3).await;
let status = pool.status().await;
assert_eq!(status.active, 0, "active should be 0 after releasing all");
assert_eq!(status.idle, 0, "idle should be 0");
}
#[tokio::test]
async fn formal_pool_close_all_then_acquire_creates_new() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(5)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let conn = pool.acquire().await.unwrap();
pool.release(conn).await;
let status = pool.status().await;
assert_eq!(status.idle, 1);
pool.close_all().await;
let status = pool.status().await;
assert_eq!(status.idle, 0);
assert_eq!(status.active, 0);
let conn = pool.acquire().await;
assert!(
conn.is_err(),
"acquire after close_all should be rejected (pool closed)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn formal_pool_conservation_under_random_ops() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(10)
.min_idle(0)
.acquire_timeout(2)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let ops_completed = Arc::new(AtomicU32::new(0));
let mut handles = Vec::new();
let mut rng = common::Rng::new(42);
for _ in 0..6 {
let ops_c = ops_completed.clone();
let seed = rng.next_u64();
handles.push(tokio::spawn(async move {
let mut local_rng = common::Rng::new(seed);
let mut held: Vec<sz_orm_core::PooledConnection> = Vec::new();
for _ in 0..40 {
let action = local_rng.next_usize(3);
match action {
0 => {
if let Ok(conn) = pool.acquire().await {
let status = pool.status().await;
assert!(
status.active <= 10,
"I3 violated: active={} > 10",
status.active
);
assert!(
status.active >= status.idle,
"I4 violated: active={} < idle={}",
status.active,
status.idle
);
held.push(conn);
}
}
1 => {
if let Some(conn) = held.pop() {
pool.release(conn).await;
let status = pool.status().await;
assert!(
status.active >= status.idle,
"I4 violated after release: active={} < idle={}",
status.active,
status.idle
);
}
}
_ => {
let status = pool.status().await;
assert!(
status.active <= 10,
"I3 violated on check: active={}",
status.active
);
}
}
ops_c.fetch_add(1, Ordering::Relaxed);
}
for conn in held {
pool.release(conn).await;
}
}));
}
for h in handles {
h.await.unwrap();
}
let status = pool.status().await;
assert_eq!(
status.active, status.idle,
"I4 final: active should equal idle (no borrowed), got active={} idle={}",
status.active, status.idle
);
assert!(
status.active <= 10,
"I3 final: active={} > max_size=10",
status.active
);
}
#[tokio::test]
async fn formal_state_machine_depth_3_exhaustive() {
for &op1 in ALL_OPS.iter() {
for &op2 in ALL_OPS.iter() {
for &op3 in ALL_OPS.iter() {
let mut tx = make_tx();
let mut state = FormalTxState::Active;
for (step, &op) in [op1, op2, op3].iter().enumerate() {
let (new_state, success) = actual_transition(&mut tx, state, op).await;
let (expected_state, expected_success) = formal_transition(state, op);
assert_eq!(
success, expected_success,
"depth=3 seq=({:?},{:?},{:?}) step={}: success mismatch",
op1, op2, op3, step
);
assert_eq!(
new_state, expected_state,
"depth=3 seq=({:?},{:?},{:?}) step={}: state mismatch",
op1, op2, op3, step
);
state = new_state;
}
}
}
}
}
#[tokio::test]
async fn formal_pool_reap_idle_invariant() {
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let factory = Arc::new(MockConnectionFactory::new(db));
let config = PoolConfigBuilder::new()
.max_size(5)
.min_idle(0)
.max_lifetime(1)
.idle_timeout(1)
.acquire_timeout(2)
.build()
.unwrap();
let pool: &'static Pool = Box::leak(Box::new(Pool::new(config, factory).unwrap()));
let mut conns = Vec::new();
for _ in 0..3 {
conns.push(pool.acquire().await.unwrap());
}
for conn in conns {
pool.release(conn).await;
}
tokio::time::sleep(Duration::from_millis(500)).await;
let mut conns2 = Vec::new();
for _ in 0..2 {
conns2.push(pool.acquire().await.unwrap());
}
for conn in conns2 {
pool.release(conn).await;
}
tokio::time::sleep(Duration::from_millis(700)).await;
pool.reap_idle().await;
let status = pool.status().await;
assert!(
status.idle <= 2,
"after reap_idle, idle should be at most 2 (the newer ones), got {}",
status.idle
);
assert_eq!(
status.active, status.idle,
"after reap_idle: active should equal idle, got active={} idle={}",
status.active, status.idle
);
}
#[tokio::test]
async fn formal_active_failure_preserves_state() {
{
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut conn = common::FaultyConnection::new(db);
conn.fail_on_execute_n = Some(1); let mut tx = Transaction::new(Box::new(conn), TransactOptions::default());
let result = tx.execute("SELECT 1").await;
assert!(result.is_err(), "execute should fail");
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Active,
"I7: state must stay Active after execute failure"
);
}
{
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut conn = common::FaultyConnection::new(db);
conn.fail_on_commit = true;
let mut tx = Transaction::new(Box::new(conn), TransactOptions::default());
let result = tx.commit().await;
assert!(result.is_err(), "commit should fail");
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Active,
"I7: state must stay Active after commit failure"
);
}
{
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut conn = common::FaultyConnection::new(db);
conn.fail_on_rollback = true;
let mut tx = Transaction::new(Box::new(conn), TransactOptions::default());
let result = tx.rollback().await;
assert!(result.is_err(), "rollback should fail");
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Active,
"I7: state must stay Active after rollback failure"
);
}
{
let db = Arc::new(Mutex::new(common::InMemoryDb::new()));
let mut conn = common::FaultyConnection::new(db);
conn.fail_on_execute_n = Some(1); let mut tx = Transaction::new(Box::new(conn), TransactOptions::default());
let result = tx.savepoint().await;
assert!(result.is_err(), "savepoint should fail (execute fails)");
assert_eq!(
tx.state(),
sz_orm_core::TransactionState::Active,
"I7: state must stay Active after savepoint failure"
);
}
}