Files
alkstore/alkstore-sqlite/tests/contract_suite.rs
T
glm-5.3-flash 36023914b2 Contract-suite rows (task suite-opts-backoff-rows): the backoff-curve equivalence row and the deferred enqueue-opts clock legs. backoff_curve_equivalence — the equal-jitter exponential pinned as the range, not a jitter label (ADR-010 §3): claim → retry(err, None) cycles on a backoff_base_s = 2 queue land the ranges [1,2], [2,4], [4,8] across attempts 1–3, each computed delay read back through get_job's resolved run_at minus a clock read taken before the retry — the lower bound race-free (the retry's internal clock read cannot precede the suite's, so the observed value over-reads the delay, never under-reads) and the upper bound carrying a one-second straddle tolerance for the integer-second stamp crossing a wall second; identical bounds on both engines is the equivalence pin (ADR-012 §2, the row body shared). The explicit-delay override legs ride the same row: retry(err, Some(5)) honored verbatim ([5, 6] under the same straddle tolerance) and retry(err, Some(0)) resolving ready-now per the boundary-total rule (ADR-023 §2), claimed within a bounded wait. enqueue_opts_resolution extended in place with the wave-5 legs its deferral note named — the run_at-alone literal leg (a future run_at is the row's ready time verbatim, a deterministic equality with no clock read involved; a past run_at stores the literal too and is claimable now through the run_at <= now predicate within a bounded wait, the run_at-ASC ordering making the observation unambiguous among the row's other legs) and the neither-field leg (ready at the enqueue instant, abs_diff(now) <= 5 tolerance-bounded proximity, never a tight timing assert); the deferral note replaced by the completion statement, ADR-023 §2 stamp accumulated per the convention (ADR-020 §1 governs the resolutions). Cap-leg disposition recorded in Notes for the review gate: the 1-hour cap is not suite-pinnable (capped attempts need minute-scale waits) and stays pinned engine-side on both engines' unit tests per the wave-3/4 reviews. Ready-now waits are bounded claim_one poll loops (deadline asserts only, sequential single-store drive), with the one draft defect the finding surfaced (a re-claim after the bounded wait consumed nothing; the helper returns the claimed handle) noted. Wired into both engines' suite targets (SQLite tokio test, pg harness_row!), 25 rows per column up from 24. Verified: SQLite column green server-less (25/25 suite, 189 lib), pg column green vs harness (postgres/poc@:15432 — 25/25 suite, 121 lib + 9 schema), 3 solo re-runs of each new/extended row per engine (determinism), server-less pg skips cleanly via the reachability gate, cargo test -p alkstore-sqlite -p alkstore-postgres green, clippy -D warnings, fmt clean
2026-10-10 07:26:55 +00:00

234 lines
8.4 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::{
backoff_curve_equivalence, concurrent_try_lock_loser_is_a_value,
drop_rollback_leaves_no_ghosts, duration_refusal_on_non_positive_ttl,
enqueue_opts_resolution, extent_clamp_semantics, in_tx_reads_see_own_writes,
job_handle_validity_predicate, lock_ttl_expiry_and_reacquisition,
name_validation_rejects_empty_and_reserved, outbox_enqueue_tx_commit_atomicity,
payload_round_trip_stores_exact_encoding, payload_too_large_never_produced_on_sqlite,
publish_with_key_tx_commit_atomicity, queue_depth_reclaim_and_dead_letter,
receiver_close_and_save_arms, scheduler_boundary_fires, scheduler_bounded_catchup,
scheduler_leadership_discipline, stream_ordering_equivalence, sweep_no_stranded_rows,
trim_to_semantics, wake_receiver_shapes, with_tx_panicking_closure_rolls_back,
};
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_lock_ttl_expiry_and_reacquisition() {
lock_ttl_expiry_and_reacquisition(&factory("row-lock-ttl-expiry")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_concurrent_try_lock_loser_is_a_value() {
concurrent_try_lock_loser_is_a_value(&factory("row-lock-loser-value")).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_backoff_curve_equivalence() {
backoff_curve_equivalence(&factory("row-backoff-curve")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_receiver_close_and_save_arms() {
receiver_close_and_save_arms(&factory("row-receiver-arms")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_job_handle_validity_predicate() {
job_handle_validity_predicate(&factory("row-handle-predicate")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_queue_depth_reclaim_and_dead_letter() {
queue_depth_reclaim_and_dead_letter(&factory("row-queue-depth")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_sweep_no_stranded_rows() {
sweep_no_stranded_rows(&factory("row-sweep-stranded")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_scheduler_boundary_fires() {
scheduler_boundary_fires(&factory("row-scheduler-boundary")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_scheduler_bounded_catchup() {
scheduler_bounded_catchup(&factory("row-scheduler-catchup")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_scheduler_leadership_discipline() {
scheduler_leadership_discipline(&factory("row-scheduler-leadership")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_outbox_enqueue_tx_commit_atomicity() {
outbox_enqueue_tx_commit_atomicity(&factory("row-outbox-tx-atomicity")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_publish_with_key_tx_commit_atomicity() {
publish_with_key_tx_commit_atomicity(&factory("row-keyed-tx-atomicity")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_stream_ordering_equivalence() {
stream_ordering_equivalence(&factory("row-stream-ordering")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_trim_to_semantics() {
trim_to_semantics(&factory("row-trim-to")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_wake_receiver_shapes() {
wake_receiver_shapes(&factory("row-wake-shapes")).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn row_with_tx_panicking_closure_rolls_back() {
with_tx_panicking_closure_rolls_back(&factory("row-panic-rollback")).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");
}
}