//! The `open` constructor's acceptance tests (`sqlite-engine-open-opts`): //! the connection architecture boots, opts flow, the plain-path //! posture holds, and close/drop teardown releases the file. use std::path::PathBuf; use std::time::Duration; use crate::opts::{DEFAULT_MAX_READERS, SqliteOpts}; use crate::seam::blocking; use crate::store::{open, open_store}; fn temp_dir(tag: &str) -> PathBuf { let d = std::env::temp_dir().join(format!( "alkstore-open-{tag}-{}-{:?}", std::process::id(), std::thread::current().id() )); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); d } fn cleanup(dir: &PathBuf) { let _ = std::fs::remove_dir_all(dir); } /// Acceptance: `open(path, opts)` boots the full connection /// architecture — the writer slot takes writes, the reader pool /// serves reads, and the substrate's bootstrap ran on every /// connection (the `__alkstore_*` table family exists; the notify and /// ops SQL functions respond on a fresh pooled reader). #[tokio::test(flavor = "multi_thread")] async fn open_boots_the_full_connection_architecture() { let dir = temp_dir("boot"); let path = dir.join("store.db"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let writer_conn = store.writer.acquire().unwrap(); let mode: String = writer_conn .pragma_query_value(None, "journal_mode", |r| r.get(0)) .unwrap(); assert!( mode.eq_ignore_ascii_case("wal"), "writer not in WAL: {mode}" ); store.writer.release(writer_conn); let reader_conn = store.readers.acquire().unwrap(); let reader_mode: String = reader_conn .pragma_query_value(None, "journal_mode", |r| r.get(0)) .unwrap(); assert!( reader_mode.eq_ignore_ascii_case("wal"), "pooled reader not in WAL: {reader_mode}" ); let notify_fn: i64 = reader_conn .query_row("SELECT notify('__alkstore_probe', 'x')", [], |r| r.get(0)) .unwrap(); assert!(notify_fn >= 1, "pooled reader missing the function set"); let publish_offset: i64 = reader_conn .query_row( "SELECT alkstore_stream_publish('__alkstore_probe', NULL, '{}')", [], |r| r.get(0), ) .unwrap(); assert!(publish_offset >= 1, "pooled reader missing the ops surface"); store.readers.release(reader_conn); assert!(store.db_path.exists(), "db file at the plain path"); drop(store); cleanup(&dir); } /// Acceptance: the watcher is up after open — a commit from the /// writer slot fans out to a raw substrate subscriber (the guard the /// death plumbing rides; the consumer-visible receiver bridge lands /// with `listen`). #[tokio::test(flavor = "multi_thread")] async fn open_leaves_the_watcher_running_and_fanning_out() { let dir = temp_dir("watcher"); let path = dir.join("store.db"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let (_id, rx) = store.watcher.subscribe(); let writer_conn = store.writer.acquire().unwrap(); writer_conn .execute("CREATE TABLE t(x INTEGER PRIMARY KEY)", []) .unwrap(); writer_conn.execute("INSERT INTO t VALUES (1)", []).unwrap(); store.writer.release(writer_conn); let deadline = std::time::Instant::now() + Duration::from_secs(2); let got = loop { match rx.try_recv() { Ok(()) => break true, Err(std::sync::mpsc::TryRecvError::Empty) => { assert!( std::time::Instant::now() < deadline, "watcher not fanning out after a commit" ); std::thread::sleep(Duration::from_millis(5)); } Err(std::sync::mpsc::TryRecvError::Disconnected) => break false, } }; assert!( got, "subscriber channel disconnected instead of fanning out" ); store.close(); cleanup(&dir); } /// Acceptance: watcher death handling wired — closing the shared /// watcher disconnects every subscriber receiver (the death-guard /// plumbing `listen` bridges into `recv() -> None`). The mechanism /// lands with `listen`; the plumbing exists here. #[tokio::test(flavor = "multi_thread")] async fn closing_the_store_closes_watcher_subscribers() { let dir = temp_dir("death"); let path = dir.join("store.db"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let (_id, rx) = store.watcher.subscribe(); store.close(); assert!( rx.recv().is_err(), "subscriber receiver must close with the watcher (WatcherDeathGuard)" ); cleanup(&dir); } /// Acceptance: `poll_interval` flows to the watcher's config (the /// config-surface accounting test — asserting the configured cadence, /// not wall-clock timing; ADR-023 §4's backlog row; `None` = the 1 ms /// default, zero rejected at open). #[tokio::test(flavor = "multi_thread")] async fn poll_interval_flows_to_the_watcher_config() { use crate::substrate::DEFAULT_WATCHER_POLL_INTERVAL; assert_eq!( DEFAULT_WATCHER_POLL_INTERVAL, Duration::from_millis(1), "the substrate's shipping default is 1 ms (ADR-023 §4)" ); assert_eq!( SqliteOpts::default().poll_interval, None, "None = the substrate's 1 ms default" ); let opts = SqliteOpts { poll_interval: Some(Duration::from_millis(25)), ..Default::default() }; let config = opts.watcher_config().unwrap(); assert_eq!( config.poll_interval, Duration::from_millis(25), "the knob must flow to the watcher's configured cadence" ); let zero_err = SqliteOpts { poll_interval: Some(Duration::ZERO), ..Default::default() } .watcher_config() .unwrap_err(); assert!( matches!(zero_err, alkstore::Error::Database(_)), "the W-2 mapping lands in Error::Database, got: {zero_err:?}" ); let source = match &zero_err { alkstore::Error::Database(source) => source.to_string(), _ => unreachable!("matched above"), }; assert!( source.contains("poll interval must be positive"), "the substrate's zero-interval rejection must ride the source chain, got: {source}" ); } /// The 1 ms default resolves at open without an explicit interval, /// and a generous explicit interval opens just the same. #[tokio::test(flavor = "multi_thread")] async fn open_succeeds_with_default_and_explicit_poll_intervals() { let dir = temp_dir("cadence"); for opts in [ SqliteOpts::default(), SqliteOpts { poll_interval: Some(Duration::from_millis(50)), ..Default::default() }, ] { let path = dir.join(format!( "cadence-{}.db", opts.poll_interval.map(|d| d.as_millis()).unwrap_or(0) )); let store = open_store(path.to_str().unwrap(), opts).unwrap(); assert!(store.db_path.exists()); store.close(); } cleanup(&dir); } /// Acceptance: the plain-path posture holds (ADR-023 §3 pin — a /// `?name=value`-suffixed path creates a file with that literal /// name; no URI flag anywhere in the open path). #[tokio::test(flavor = "multi_thread")] async fn uri_shaped_open_path_is_a_literal_filename() { let dir = temp_dir("plain-path"); let path = dir.join("plain.db?mode=memory&cache=shared"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let writer_conn = store.writer.acquire().unwrap(); writer_conn.execute("CREATE TABLE t(x)", []).unwrap(); store.writer.release(writer_conn); store.close(); assert!( path.exists(), "the file must exist under its literal `?name=value` name (no URI interpretation)" ); cleanup(&dir); } /// `:memory:` works without URI mode (ADR-023 §3): each connection is /// its own private memory db, so the writer carries the bootstrap and /// a fresh pooled reader opens the same way. #[tokio::test(flavor = "multi_thread")] async fn memory_paths_open_without_uri_mode() { let store = open_store(":memory:", SqliteOpts::default()).unwrap(); let reader_conn = store.readers.acquire().unwrap(); let tables: i64 = reader_conn .query_row( "SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name LIKE '__alkstore_%'", [], |r| r.get(0), ) .unwrap(); store.readers.release(reader_conn); assert!(tables >= 7, ":memory: open must carry the bootstrap"); store.close(); } /// Acceptance: reader-pool sizing — `max_readers` bounds the pool /// (excess acquirers block until release) and `0` clamps to 1 /// (the substrate's max discipline). #[tokio::test(flavor = "multi_thread")] async fn reader_pool_size_is_bounded_by_opts() { let dir = temp_dir("pool"); let path = dir.join("store.db"); let store = open_store( path.to_str().unwrap(), SqliteOpts { max_readers: 2, ..Default::default() }, ) .unwrap(); let c1 = store.readers.acquire().unwrap(); let c2 = store.readers.acquire().unwrap(); let readers = store.readers.clone(); let blocked = std::thread::spawn(move || readers.acquire()); std::thread::sleep(Duration::from_millis(100)); assert!( !blocked.is_finished(), "acquire above max_readers must block (pool bound enforced)" ); store.readers.release(c2); let c3 = blocked.join().unwrap().unwrap(); store.readers.release(c3); store.readers.release(c1); store.close(); let store = open_store( path.to_str().unwrap(), SqliteOpts { max_readers: 0, ..Default::default() }, ) .unwrap(); let only = store.readers.acquire().unwrap(); store.readers.release(only); store.close(); cleanup(&dir); } /// The default reader count is the documented constant. #[test] fn default_max_readers_is_the_documented_constant() { assert_eq!(DEFAULT_MAX_READERS, 8, "the documented default pool size"); assert_eq!( SqliteOpts::default().max_readers, DEFAULT_MAX_READERS, "`Default` resolves the reader-pool knob to the documented default" ); } /// A pool beyond its bound opens fresh readers on demand up to the /// bound; reads on those readers see a writer's committed data /// (the WAL cross-connection read path the reader pool exists for). #[tokio::test(flavor = "multi_thread")] async fn pooled_readers_see_writer_commits() { let dir = temp_dir("read-visibility"); let path = dir.join("store.db"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let writer_conn = store.writer.acquire().unwrap(); writer_conn .execute("CREATE TABLE t(x INTEGER)", []) .unwrap(); writer_conn .execute_batch("INSERT INTO t VALUES (42);") .unwrap(); store.writer.release(writer_conn); let reader_conn = store.readers.acquire().unwrap(); let x: i64 = reader_conn .query_row("SELECT x FROM t", [], |r| r.get(0)) .unwrap(); store.readers.release(reader_conn); assert_eq!(x, 42, "pooled reader reads the writer's committed row"); store.close(); cleanup(&dir); } /// Explicit close: the watcher joins (its thread exits), the file /// handle is released, and the store is gone-but-reopenable. #[tokio::test(flavor = "multi_thread")] async fn explicit_close_releases_the_db_file_and_permits_reopen() { let dir = temp_dir("close"); let path = dir.join("store.db"); let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); store.close(); let reopened = open(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); drop(reopened); cleanup(&dir); } /// `Drop` stops the watcher and closes connections — the same teardown /// as explicit close, reached through the drop path. #[tokio::test(flavor = "multi_thread")] async fn drop_runs_the_full_teardown() { let dir = temp_dir("drop"); let path = dir.join("store.db"); { let store = open_store(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); let (_id, rx) = store.watcher.subscribe(); drop(store); assert!( rx.recv().is_err(), "drop must run the same teardown as close (subscriber closed)" ); } let reopened = open(path.to_str().unwrap(), SqliteOpts::default()).unwrap(); drop(reopened); cleanup(&dir); } /// Acceptance: the `spawn_blocking` seam — every engine call's /// substrate round trip rides this helper; smoked here through the /// real bridge with a rusqlite-shaped closure (the pattern the /// mechanism tasks reuse one-for-one). #[tokio::test(flavor = "multi_thread")] async fn the_blocking_bridge_round_trips_and_maps_errors() { let ok = blocking(|| Ok::<_, alkstore::Error>(7i64)).await; assert_eq!(ok.unwrap(), 7); let err = blocking(|| -> Result<(), alkstore::Error> { Err(alkstore::Error::database(std::io::Error::other( "smoke failure", ))) }) .await; assert!(matches!(err, Err(alkstore::Error::Database(_)))); let panicking: Result<(), alkstore::Error> = blocking(|| -> Result<(), alkstore::Error> { panic!("bridge smoke panic") }).await; assert!( matches!(&panicking, Err(alkstore::Error::Database(_))), "a panic inside a bridged closure maps to Database (no panics cross the seam), got {panicking:?}" ); } /// `open` on an unopenable path fails cleanly — the substrate's /// bootstrap errors map into `Error::Database` at the constructor, not /// panics. #[tokio::test(flavor = "multi_thread")] async fn open_fails_cleanly_on_an_unopenable_path() { let result = open( "/this/parent/does/not/exist/alkstore-open-fail.db", SqliteOpts::default(), ); match result { Err(alkstore::Error::Database(source)) => { assert!( !source.to_string().is_empty(), "the source chain carries the substrate's detail" ); } Ok(_) => panic!("expected Err(Database) from an unopenable path, got Ok"), Err(e) => panic!("expected Err(Database) from an unopenable path, got {e}"), } }