//! The suite-driving test target: the SQLite engine's backlog column //! (ADR-022's option (a) — the rows live in `alkstore-contract-suite`, //! this target is the executor that runs them against the SQLite //! factory; wave 5 runs the same rows against pg's). //! //! The factory: a fresh temp file under a fresh instance directory per //! `open` (every call gets its own never-before-used path — the //! isolation guarantee), teardown deleting the directory (idempotent — //! already-deleted is already-torn-down, `Ok`). Close-arm rows drop //! the store handle (the dispose carrier) — deleting files under a //! live store is not a close mechanism. use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use alkstore::{BoxedFuture, Error, Result, Store}; use alkstore_contract_suite::StoreFactory; use alkstore_sqlite::open; /// The SQLite suite factory: fresh temp-file store per `open`, /// idempotent directory-delete teardown. `Send + Sync` — instance /// state is a path prefix and a counter. struct SqliteFactory { dir: PathBuf, instance: AtomicU64, } impl SqliteFactory { fn new(tag: &str) -> Self { let dir = std::env::temp_dir().join(format!( "alkstore-suite-{tag}-{}-{:?}", std::process::id(), std::thread::current().id() )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); SqliteFactory { dir, instance: AtomicU64::new(0), } } } impl StoreFactory for SqliteFactory { fn open(&self) -> BoxedFuture<'_, Result>> { let n = self.instance.fetch_add(1, Ordering::SeqCst); let path = self .dir .join(format!("store-{n}.db")) .to_str() .unwrap_or_default() .to_string(); Box::pin(async move { open(&path, Default::default()) }) } fn teardown(&self) -> BoxedFuture<'_, Result<()>> { let dir = self.dir.clone(); Box::pin(async move { match std::fs::remove_dir_all(&dir) { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(e) => Err(Error::database(e)), } }) } } mod rows { use alkstore_contract_suite::properties::{ drop_rollback_leaves_no_ghosts, duration_refusal_on_non_positive_ttl, enqueue_opts_resolution, extent_clamp_semantics, in_tx_reads_see_own_writes, name_validation_rejects_empty_and_reserved, payload_round_trip_stores_exact_encoding, payload_too_large_never_produced_on_sqlite, receiver_close_and_save_arms, }; use super::SqliteFactory; fn factory(tag: &str) -> SqliteFactory { SqliteFactory::new(tag) } #[tokio::test(flavor = "multi_thread")] async fn row_name_validation_rejects_empty_and_reserved() { name_validation_rejects_empty_and_reserved(&factory("row-name-validation")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_extent_clamp_semantics() { extent_clamp_semantics(&factory("row-extent-clamp")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_duration_refusal_on_non_positive_ttl() { duration_refusal_on_non_positive_ttl(&factory("row-duration-refusal")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_payload_round_trip_stores_exact_encoding() { payload_round_trip_stores_exact_encoding(&factory("row-payload-round-trip")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_payload_too_large_never_produced_on_sqlite() { payload_too_large_never_produced_on_sqlite(&factory("row-no-too-large")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_drop_rollback_leaves_no_ghosts() { drop_rollback_leaves_no_ghosts(&factory("row-drop-rollback")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_in_tx_reads_see_own_writes() { in_tx_reads_see_own_writes(&factory("row-in-tx-ryow")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_enqueue_opts_resolution() { enqueue_opts_resolution(&factory("row-opts-resolution")).await; } #[tokio::test(flavor = "multi_thread")] async fn row_receiver_close_and_save_arms() { receiver_close_and_save_arms(&factory("row-receiver-arms")).await; } } mod factory_shape { use super::SqliteFactory; use alkstore_contract_suite::StoreFactory; /// ADR-022's factory contract: every open yield is isolated (no two /// opens share rows) and teardown is idempotent per instance; a /// failing assertion early-exit (the panic path) leaves teardown /// safe against the abandoned store (the documented open/close/ /// delete order is safe at any point — the row pins the /// teardown-side half). #[tokio::test(flavor = "multi_thread")] async fn opens_are_isolated_and_teardown_is_idempotent() { let factory = SqliteFactory::new("factory-shape"); let a = factory.open().await.unwrap(); let b = factory.open().await.unwrap(); factory.teardown().await.unwrap(); factory.teardown().await.unwrap(); drop(a); drop(b); assert!(!factory.dir.exists(), "teardown removed the backing dir"); } }