- Remove the wave-2 lint suppressions from substrate/mod.rs; the six genuinely dead surfaces the removal exposed are cut, not suppressed, and registered D-32..D-36 in PROVENANCE.md (arg_opt_i64, ops::now_unix, queue_next_claim_at, Writer::try_acquire, UpdateWatcher::spawn, SharedUpdateWatcher::new); test-observation items (subscriber_count, the poll-interval default re-export) are honestly #[cfg(test)]-gated - Contract suite: eight new version-stamped backlog rows (extent-clamp + boundary totality, duration-refusal, encode_payload round-trip, PayloadTooLarge-never-produced SQLite arm, drop=rollback no-ghosts, in-tx read-your-own-writes, enqueue-opts resolution, receiver close/save arms) - Fix the exemplar row's real-engine sequencing defect: the held tx handle across the with_tx leg deadlocked any single-writer factory (mock-invisible; ADR-007's parking is the pinned behavior) - SQLite factory: SqliteFactory in the new tests/contract_suite.rs target; all nine rows green against it; the factory contract (isolation + idempotent teardown) pinned - Engine lib docs: the single-host and writer-parking posture statements surfaced under # Posture - Gates: build/test/clippy -D warnings/fmt green; coverage 93.6% lines, misses confined to error arms
153 lines
5.3 KiB
Rust
153 lines
5.3 KiB
Rust
//! 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<Box<dyn Store>>> {
|
|
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");
|
|
}
|
|
}
|