1use std::sync::atomic::{AtomicU64, Ordering};
8
9use bsql_driver_postgres::{Config, Connection};
10
11use crate::error::{BsqlError, ConnectError};
12use crate::pool::Pool;
13
14static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
15
16pub struct TestContext {
19 pub pool: Pool,
21 schema_name: String,
22 db_url: String,
23}
24
25impl std::fmt::Debug for TestContext {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("TestContext")
28 .field("schema", &self.schema_name)
29 .finish()
30 }
31}
32
33impl Drop for TestContext {
34 fn drop(&mut self) {
35 if let Ok(config) = Config::from_url(&self.db_url) {
38 if let Ok(mut conn) = Connection::connect(&config) {
39 let _ = conn.simple_query(&format!(
40 "DROP SCHEMA IF EXISTS \"{}\" CASCADE",
41 self.schema_name
42 ));
43 }
44 }
45 }
46}
47
48pub async fn setup_test_schema(fixtures_sql: &[&str]) -> Result<TestContext, BsqlError> {
54 let db_url = std::env::var("BSQL_DATABASE_URL")
55 .or_else(|_| std::env::var("DATABASE_URL"))
56 .map_err(|_| {
57 ConnectError::create("BSQL_DATABASE_URL or DATABASE_URL must be set for #[bsql::test]")
58 })?;
59
60 let schema_name = format!(
61 "__bsql_test_{}_{}",
62 std::process::id(),
63 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
64 );
65
66 let config = Config::from_url(&db_url)
68 .map_err(|e| ConnectError::create(format!("invalid database URL: {e}")))?;
69 let mut conn = Connection::connect(&config)
70 .map_err(|e| ConnectError::create(format!("connection failed: {e}")))?;
71
72 conn.simple_query(&format!("CREATE SCHEMA \"{}\"", schema_name))
74 .map_err(|e| ConnectError::create(format!("failed to create test schema: {e}")))?;
75
76 conn.simple_query(&format!("SET search_path TO \"{}\", public", schema_name))
78 .map_err(|e| ConnectError::create(format!("failed to set search_path: {e}")))?;
79
80 for fixture_sql in fixtures_sql {
82 if !fixture_sql.trim().is_empty() {
83 conn.simple_query(fixture_sql)
84 .map_err(|e| ConnectError::create(format!("fixture failed: {e}")))?;
85 }
86 }
87
88 drop(conn); let pool = Pool::connect(&db_url).await?;
93
94 pool.raw_execute(&format!("SET search_path TO \"{}\", public", schema_name))
97 .await?;
98
99 let warmup_sql = format!("SET search_path TO \"{}\", public", schema_name);
103 pool.set_warmup_sqls([warmup_sql]);
106
107 Ok(TestContext {
108 pool,
109 schema_name,
110 db_url,
111 })
112}
113
114#[cfg(feature = "sqlite")]
123pub struct SqliteTestContext {
124 pub pool: crate::sqlite_pool::SqlitePool,
126 pub db_path: std::path::PathBuf,
128}
129
130#[cfg(feature = "sqlite")]
131impl Drop for SqliteTestContext {
132 fn drop(&mut self) {
133 self.pool.close();
135 let _ = std::fs::remove_file(&self.db_path);
137 let _ = std::fs::remove_file(format!("{}-wal", self.db_path.display()));
138 let _ = std::fs::remove_file(format!("{}-shm", self.db_path.display()));
139 }
140}
141
142#[cfg(feature = "sqlite")]
150pub fn setup_sqlite_test(
151 fixtures_sql: &[&str],
152) -> Result<SqliteTestContext, crate::error::BsqlError> {
153 use crate::error::ConnectError;
154
155 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
157 let db_path = std::env::temp_dir().join(format!(
158 "bsql_test_{}_{}.db",
159 std::process::id(),
160 COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
161 ));
162
163 let pool = crate::sqlite_pool::SqlitePool::connect(db_path.to_str().unwrap_or("bsql_test.db"))?;
165
166 for fixture_sql in fixtures_sql {
168 if !fixture_sql.trim().is_empty() {
169 pool.simple_exec(fixture_sql)
170 .map_err(|e| ConnectError::create(format!("SQLite fixture failed: {e}")))?;
171 }
172 }
173
174 Ok(SqliteTestContext { pool, db_path })
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::collections::HashSet;
181
182 #[test]
187 fn schema_name_is_unique() {
188 let name1 = format!(
189 "__bsql_test_{}_{}",
190 std::process::id(),
191 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
192 );
193 let name2 = format!(
194 "__bsql_test_{}_{}",
195 std::process::id(),
196 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
197 );
198 assert_ne!(name1, name2);
199 }
200
201 #[test]
202 fn schema_name_contains_pid() {
203 let name = format!(
204 "__bsql_test_{}_{}",
205 std::process::id(),
206 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
207 );
208 assert!(name.contains(&std::process::id().to_string()));
209 }
210
211 #[test]
212 fn schema_name_starts_with_prefix() {
213 let name = format!(
214 "__bsql_test_{}_{}",
215 std::process::id(),
216 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
217 );
218 assert!(name.starts_with("__bsql_test_"));
219 }
220
221 #[test]
222 fn schema_names_never_collide_100_sequential() {
223 let mut names = HashSet::new();
224 for _ in 0..100 {
225 let name = format!(
226 "__bsql_test_{}_{}",
227 std::process::id(),
228 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
229 );
230 assert!(names.insert(name.clone()), "duplicate schema name: {name}");
231 }
232 assert_eq!(names.len(), 100);
233 }
234
235 #[test]
236 fn schema_name_is_valid_sql_identifier() {
237 let name = format!(
238 "__bsql_test_{}_{}",
239 std::process::id(),
240 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
241 );
242 assert!(
244 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'),
245 "schema name contains invalid chars: {name}"
246 );
247 assert!(
248 name.starts_with('_') || name.starts_with(|c: char| c.is_ascii_alphabetic()),
249 "schema name must start with letter or underscore: {name}"
250 );
251 }
252
253 #[test]
258 fn test_counter_is_monotonic() {
259 let a = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
260 let b = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
261 let c = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
262 assert!(a < b);
263 assert!(b < c);
264 }
265
266 #[test]
267 fn counter_increments_atomically_across_threads() {
268 use std::sync::Arc;
269 let results: Arc<std::sync::Mutex<Vec<u64>>> = Arc::new(std::sync::Mutex::new(Vec::new()));
270 let mut handles = Vec::new();
271 for _ in 0..10 {
272 let results = Arc::clone(&results);
273 handles.push(std::thread::spawn(move || {
274 for _ in 0..10 {
275 let val = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
276 results.lock().unwrap().push(val);
277 }
278 }));
279 }
280 for h in handles {
281 h.join().unwrap();
282 }
283 let mut vals = results.lock().unwrap().clone();
284 assert_eq!(vals.len(), 100, "expected 100 counter values");
285 let set: HashSet<u64> = vals.iter().copied().collect();
287 assert_eq!(
288 set.len(),
289 100,
290 "counter values must be unique across threads"
291 );
292 vals.sort();
294 for window in vals.windows(2) {
295 assert!(window[0] < window[1], "counter must be strictly increasing");
296 }
297 }
298
299 #[test]
304 fn multiple_schema_names_created_simultaneously_are_different() {
305 let names: Vec<String> = (0..50)
307 .map(|_| {
308 format!(
309 "__bsql_test_{}_{}",
310 std::process::id(),
311 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
312 )
313 })
314 .collect();
315 let set: HashSet<&String> = names.iter().collect();
316 assert_eq!(set.len(), names.len(), "all schema names must be unique");
317 }
318
319 #[tokio::test]
324 async fn missing_db_url_returns_clear_error() {
325 let orig_bsql = std::env::var("BSQL_DATABASE_URL").ok();
327 let orig_db = std::env::var("DATABASE_URL").ok();
328 std::env::remove_var("BSQL_DATABASE_URL");
329 std::env::remove_var("DATABASE_URL");
330
331 let result = setup_test_schema(&[]).await;
332 assert!(result.is_err());
333 let err = result.unwrap_err();
334 let msg = err.to_string();
335 assert!(
336 msg.contains("BSQL_DATABASE_URL") && msg.contains("DATABASE_URL"),
337 "error should mention both env vars, got: {msg}"
338 );
339
340 if let Some(v) = orig_bsql {
342 std::env::set_var("BSQL_DATABASE_URL", v);
343 }
344 if let Some(v) = orig_db {
345 std::env::set_var("DATABASE_URL", v);
346 }
347 }
348
349 #[tokio::test]
350 async fn missing_bsql_database_url_falls_back_to_database_url() {
351 let orig_bsql = std::env::var("BSQL_DATABASE_URL").ok();
352 let orig_db = std::env::var("DATABASE_URL").ok();
353 std::env::remove_var("BSQL_DATABASE_URL");
354 std::env::set_var("DATABASE_URL", "not-a-url");
356
357 let result = setup_test_schema(&[]).await;
358 assert!(result.is_err());
360 let msg = result.unwrap_err().to_string();
361 assert!(
362 msg.contains("invalid database URL"),
363 "should fail on URL parse after falling back to DATABASE_URL, got: {msg}"
364 );
365
366 std::env::remove_var("DATABASE_URL");
368 if let Some(v) = orig_bsql {
369 std::env::set_var("BSQL_DATABASE_URL", v);
370 }
371 if let Some(v) = orig_db {
372 std::env::set_var("DATABASE_URL", v);
373 }
374 }
375
376 #[tokio::test]
377 async fn invalid_db_url_returns_clear_error() {
378 let orig_bsql = std::env::var("BSQL_DATABASE_URL").ok();
379 let orig_db = std::env::var("DATABASE_URL").ok();
380 std::env::set_var("BSQL_DATABASE_URL", "not-a-valid-url");
381 std::env::remove_var("DATABASE_URL");
382
383 let result = setup_test_schema(&[]).await;
384 assert!(result.is_err());
385 let err = result.unwrap_err();
386 let msg = err.to_string();
387 assert!(
388 msg.contains("invalid database URL"),
389 "error should mention invalid URL, got: {msg}"
390 );
391
392 std::env::remove_var("BSQL_DATABASE_URL");
394 if let Some(v) = orig_bsql {
395 std::env::set_var("BSQL_DATABASE_URL", v);
396 }
397 if let Some(v) = orig_db {
398 std::env::set_var("DATABASE_URL", v);
399 }
400 }
401
402 #[tokio::test]
403 async fn invalid_db_url_not_postgres_scheme() {
404 let orig_bsql = std::env::var("BSQL_DATABASE_URL").ok();
405 let orig_db = std::env::var("DATABASE_URL").ok();
406 std::env::set_var("BSQL_DATABASE_URL", "mysql://user:pass@localhost/db");
407 std::env::remove_var("DATABASE_URL");
408
409 let result = setup_test_schema(&[]).await;
410 assert!(result.is_err());
411 let msg = result.unwrap_err().to_string();
412 assert!(
413 msg.contains("invalid database URL"),
414 "non-postgres scheme should fail with clear error, got: {msg}"
415 );
416
417 std::env::remove_var("BSQL_DATABASE_URL");
418 if let Some(v) = orig_bsql {
419 std::env::set_var("BSQL_DATABASE_URL", v);
420 }
421 if let Some(v) = orig_db {
422 std::env::set_var("DATABASE_URL", v);
423 }
424 }
425
426 #[test]
427 fn connection_refused_unreachable_host() {
428 let url = "postgres://user:pass@127.0.0.1:1/testdb";
431 let config = Config::from_url(url).expect("URL should parse");
432 let conn_result = Connection::connect(&config);
433 assert!(conn_result.is_err(), "connection to port 1 should fail");
434 let err = ConnectError::create(format!("connection failed: {}", conn_result.unwrap_err()));
437 let msg = err.to_string();
438 assert!(
439 msg.contains("connection failed"),
440 "unreachable host should produce 'connection failed' error, got: {msg}"
441 );
442 }
443
444 #[test]
449 fn test_context_has_debug_impl() {
450 fn assert_debug<T: std::fmt::Debug>() {}
452 assert_debug::<TestContext>();
453 }
454
455 #[test]
456 fn test_context_debug_shows_schema_name() {
457 let schema = "__bsql_test_12345_0";
461 let expected = format!("TestContext {{ schema: {:?} }}", schema);
462 assert!(expected.contains("TestContext"));
464 assert!(expected.contains("schema"));
465 assert!(expected.contains(schema));
466 }
467
468 #[test]
473 fn drop_code_path_with_invalid_url_does_not_panic() {
474 let db_url = "garbage-url";
478 let schema_name = "__bsql_test_fake_0";
479 if let Ok(config) = Config::from_url(db_url) {
481 if let Ok(mut conn) = Connection::connect(&config) {
483 let _ = conn.simple_query(&format!(
484 "DROP SCHEMA IF EXISTS \"{}\" CASCADE",
485 schema_name
486 ));
487 }
488 }
489 }
491
492 #[test]
493 fn drop_with_garbage_url_does_not_panic() {
494 let db_url = "not-a-postgres-url";
501 let config_result = Config::from_url(db_url);
502 assert!(config_result.is_err(), "garbage URL should not parse");
503 }
505
506 #[test]
507 fn drop_with_valid_url_but_unreachable_host_does_not_panic() {
508 let db_url = "postgres://user:pass@127.0.0.1:1/testdb";
511 let config = Config::from_url(db_url);
512 assert!(config.is_ok(), "URL should parse");
513 let conn_result = Connection::connect(&config.unwrap());
514 assert!(conn_result.is_err(), "connection to port 1 should fail");
515 }
517
518 #[test]
523 fn empty_fixture_string_is_skipped() {
524 let fixture = "";
527 assert!(fixture.trim().is_empty(), "empty string should be skipped");
528 }
529
530 #[test]
531 fn whitespace_only_fixture_is_skipped() {
532 let fixture = " \n\t \n ";
533 assert!(
534 fixture.trim().is_empty(),
535 "whitespace-only fixture should be skipped"
536 );
537 }
538
539 #[test]
540 fn fixture_with_only_comments_is_not_empty() {
541 let fixture = "-- just a comment\n/* block comment */";
544 assert!(
545 !fixture.trim().is_empty(),
546 "comment-only fixture should NOT be skipped (PG handles it)"
547 );
548 }
549
550 #[test]
551 fn fixture_with_multiple_statements_passes_trim_check() {
552 let fixture = "CREATE TABLE a (id INT);\nCREATE TABLE b (id INT);";
553 assert!(!fixture.trim().is_empty());
554 }
555
556 #[test]
561 fn missing_env_error_is_connect_variant() {
562 let err =
563 ConnectError::create("BSQL_DATABASE_URL or DATABASE_URL must be set for #[bsql::test]");
564 match err {
565 BsqlError::Connect(ref ce) => {
566 assert!(ce.message.contains("BSQL_DATABASE_URL"));
567 }
568 _ => panic!("expected Connect variant"),
569 }
570 }
571
572 #[test]
573 fn invalid_url_error_is_connect_variant() {
574 let err = ConnectError::create("invalid database URL: missing postgres:// prefix");
575 match err {
576 BsqlError::Connect(ref ce) => {
577 assert!(ce.message.contains("invalid database URL"));
578 }
579 _ => panic!("expected Connect variant"),
580 }
581 }
582
583 #[test]
584 fn connection_failed_error_is_connect_variant() {
585 let err = ConnectError::create("connection failed: Connection refused");
586 match err {
587 BsqlError::Connect(ref ce) => {
588 assert!(ce.message.contains("connection failed"));
589 }
590 _ => panic!("expected Connect variant"),
591 }
592 }
593
594 #[test]
595 fn fixture_failed_error_is_connect_variant() {
596 let err = ConnectError::create("fixture failed: syntax error at position 5");
597 match err {
598 BsqlError::Connect(ref ce) => {
599 assert!(ce.message.contains("fixture failed"));
600 }
601 _ => panic!("expected Connect variant"),
602 }
603 }
604
605 #[test]
606 fn schema_creation_failed_error_is_connect_variant() {
607 let err = ConnectError::create("failed to create test schema: permission denied");
608 match err {
609 BsqlError::Connect(ref ce) => {
610 assert!(ce.message.contains("failed to create test schema"));
611 }
612 _ => panic!("expected Connect variant"),
613 }
614 }
615
616 #[test]
621 fn schema_name_has_three_parts() {
622 let counter = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
623 let pid = std::process::id();
624 let name = format!("__bsql_test_{}_{}", pid, counter);
625 assert!(name.starts_with("__bsql_test_"));
627 let suffix = &name["__bsql_test_".len()..];
628 let parts: Vec<&str> = suffix.split('_').collect();
629 assert_eq!(parts.len(), 2, "expected PID_COUNTER suffix, got: {suffix}");
630 assert_eq!(parts[0], pid.to_string());
631 assert_eq!(parts[1], counter.to_string());
632 }
633
634 #[test]
635 fn schema_name_counter_part_increases() {
636 let c1 = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
637 let c2 = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
638 let pid = std::process::id();
639 let name1 = format!("__bsql_test_{}_{}", pid, c1);
640 let name2 = format!("__bsql_test_{}_{}", pid, c2);
641 let counter1: u64 = name1.rsplit('_').next().unwrap().parse().unwrap();
643 let counter2: u64 = name2.rsplit('_').next().unwrap().parse().unwrap();
644 assert!(counter2 > counter1);
645 }
646
647 #[tokio::test]
652 async fn bsql_database_url_takes_priority_over_database_url() {
653 let orig_bsql = std::env::var("BSQL_DATABASE_URL").ok();
654 let orig_db = std::env::var("DATABASE_URL").ok();
655
656 std::env::set_var("BSQL_DATABASE_URL", "not-postgres-bsql");
659 std::env::set_var("DATABASE_URL", "postgres://user:pass@127.0.0.1:1/realdb");
660
661 let result = setup_test_schema(&[]).await;
662 assert!(result.is_err());
663 let msg = result.unwrap_err().to_string();
664 assert!(
666 msg.contains("invalid database URL"),
667 "BSQL_DATABASE_URL should take priority, got: {msg}"
668 );
669
670 std::env::remove_var("BSQL_DATABASE_URL");
672 std::env::remove_var("DATABASE_URL");
673 if let Some(v) = orig_bsql {
674 std::env::set_var("BSQL_DATABASE_URL", v);
675 }
676 if let Some(v) = orig_db {
677 std::env::set_var("DATABASE_URL", v);
678 }
679 }
680
681 #[test]
686 fn pg_schema_names_100_unique_across_threads() {
687 use std::sync::Arc;
688 let results: Arc<std::sync::Mutex<Vec<String>>> =
689 Arc::new(std::sync::Mutex::new(Vec::new()));
690 let handles: Vec<_> = (0..100)
691 .map(|_| {
692 let results = Arc::clone(&results);
693 std::thread::spawn(move || {
694 let name = format!(
695 "__bsql_test_{}_{}",
696 std::process::id(),
697 TEST_COUNTER.fetch_add(1, Ordering::Relaxed),
698 );
699 results.lock().unwrap().push(name);
700 })
701 })
702 .collect();
703 for h in handles {
704 h.join().unwrap();
705 }
706 let names = results.lock().unwrap();
707 let unique: HashSet<&String> = names.iter().collect();
708 assert_eq!(
709 unique.len(),
710 100,
711 "all 100 schema names must be unique across threads"
712 );
713 }
714
715 #[test]
720 fn test_context_debug_format_matches_pattern() {
721 let schema = "__bsql_test_99999_42";
725 let expected = format!("TestContext {{ schema: {:?} }}", schema);
726 assert!(expected.starts_with("TestContext { schema: \""));
728 assert!(expected.ends_with("\" }"));
729 assert!(expected.contains("__bsql_test_99999_42"));
730 }
731
732 #[cfg(feature = "sqlite")]
737 mod sqlite_tests {
738 use super::super::*;
739
740 #[test]
741 fn sqlite_test_context_creates_file() {
742 let ctx = setup_sqlite_test(&["CREATE TABLE t (id INTEGER PRIMARY KEY)"]).unwrap();
743 assert!(ctx.db_path.exists());
744 }
745
746 #[test]
747 fn sqlite_test_context_drop_removes_file() {
748 let path;
749 {
750 let ctx = setup_sqlite_test(&[]).unwrap();
751 path = ctx.db_path.clone();
752 assert!(path.exists());
753 }
754 assert!(!path.exists(), "temp db should be deleted on drop");
756 }
757
758 #[test]
759 fn sqlite_fixtures_applied() {
760 let ctx = setup_sqlite_test(&[
761 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
762 "INSERT INTO users (name) VALUES ('Alice')",
763 ])
764 .unwrap();
765 let sql = "SELECT name FROM users";
767 let hash = crate::rapid_hash_str(sql);
768 let (result, arena) = ctx
769 .pool
770 .query_readonly(sql, hash, smallvec::SmallVec::new())
771 .unwrap();
772 assert_eq!(result.len(), 1);
773 assert_eq!(result.get_str(0, 0, &arena), Some("Alice"));
774 }
775
776 #[test]
777 fn sqlite_unique_paths() {
778 let ctx1 = setup_sqlite_test(&[]).unwrap();
779 let ctx2 = setup_sqlite_test(&[]).unwrap();
780 assert_ne!(ctx1.db_path, ctx2.db_path);
781 }
782
783 #[test]
784 fn sqlite_empty_fixture_works() {
785 let ctx = setup_sqlite_test(&[""]).unwrap();
786 assert!(ctx.db_path.exists());
787 }
788
789 #[test]
790 fn sqlite_multiple_fixtures_order() {
791 let ctx = setup_sqlite_test(&[
792 "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)",
793 "INSERT INTO t (val) VALUES ('first')",
794 "INSERT INTO t (val) VALUES ('second')",
795 ])
796 .unwrap();
797 let sql = "SELECT val FROM t ORDER BY id";
798 let hash = crate::rapid_hash_str(sql);
799 let (result, arena) = ctx
800 .pool
801 .query_readonly(sql, hash, smallvec::SmallVec::new())
802 .unwrap();
803 assert_eq!(result.len(), 2);
804 assert_eq!(result.get_str(0, 0, &arena), Some("first"));
805 assert_eq!(result.get_str(1, 0, &arena), Some("second"));
806 }
807
808 #[test]
809 fn sqlite_fixture_error_propagates() {
810 let result = setup_sqlite_test(&["NOT VALID SQL AT ALL !@#"]);
811 assert!(result.is_err(), "bad SQL should propagate as Err");
812 }
813
814 #[test]
815 fn sqlite_wal_shm_cleaned() {
816 let path;
817 {
818 let ctx = setup_sqlite_test(&[
819 "CREATE TABLE t (id INTEGER PRIMARY KEY)",
820 "INSERT INTO t VALUES (1)",
821 ])
822 .unwrap();
823 path = ctx.db_path.clone();
824 }
825 assert!(
827 !std::path::Path::new(&format!("{}-wal", path.display())).exists(),
828 "WAL file should be removed"
829 );
830 assert!(
831 !std::path::Path::new(&format!("{}-shm", path.display())).exists(),
832 "SHM file should be removed"
833 );
834 }
835
836 #[test]
837 fn sqlite_whitespace_only_fixture_skipped() {
838 let ctx = setup_sqlite_test(&[" \n\t "]).unwrap();
840 assert!(ctx.db_path.exists());
841 }
842
843 #[test]
844 fn sqlite_path_contains_pid() {
845 let ctx = setup_sqlite_test(&[]).unwrap();
846 let path_str = ctx.db_path.to_string_lossy().to_string();
847 assert!(
848 path_str.contains(&std::process::id().to_string()),
849 "path should contain PID: {path_str}"
850 );
851 }
852
853 #[test]
854 fn sqlite_path_has_db_extension() {
855 let ctx = setup_sqlite_test(&[]).unwrap();
856 let path_str = ctx.db_path.to_string_lossy().to_string();
857 assert!(
858 path_str.ends_with(".db"),
859 "path should end with .db: {path_str}"
860 );
861 }
862
863 #[test]
868 fn sqlite_cleanup_on_panic() {
869 use std::sync::Arc;
870 use std::sync::Mutex;
871
872 let captured_path = Arc::new(Mutex::new(None));
873 let path_clone = captured_path.clone();
874
875 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
876 let ctx = setup_sqlite_test(&["CREATE TABLE t (id INTEGER)"]).unwrap();
877 *path_clone.lock().unwrap() = Some(ctx.db_path.clone());
878 panic!("simulated failure");
879 }));
880
881 assert!(result.is_err());
882 let path = captured_path.lock().unwrap().clone().unwrap();
883 assert!(
885 !path.exists(),
886 "temp file should be cleaned even after panic"
887 );
888 }
889
890 #[test]
895 fn sqlite_drop_with_open_file_handle() {
896 let ctx = setup_sqlite_test(&[]).unwrap();
898 let path = ctx.db_path.clone();
899 drop(ctx);
901 let _ = path;
904 }
905
906 #[test]
911 fn sqlite_setup_with_invalid_sql_propagates_error() {
912 let result = setup_sqlite_test(&["NOT VALID SQL AT ALL !!!"]);
914 assert!(result.is_err());
915 }
920
921 #[test]
926 fn sqlite_concurrent_100_temp_files() {
927 let handles: Vec<_> = (0..100)
928 .map(|_| {
929 std::thread::spawn(|| {
930 let ctx = setup_sqlite_test(&[
931 "CREATE TABLE t (id INTEGER PRIMARY KEY)",
932 "INSERT INTO t VALUES (1)",
933 ])
934 .unwrap();
935 let path = ctx.db_path.clone();
936 assert!(path.exists());
937 drop(ctx);
939 assert!(!path.exists());
940 path
941 })
942 })
943 .collect();
944
945 let paths: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
946
947 let unique: std::collections::HashSet<_> = paths.iter().collect();
949 assert_eq!(unique.len(), 100, "all 100 paths should be unique");
950 }
951
952 #[test]
957 fn sqlite_error_message_is_descriptive() {
958 let result = setup_sqlite_test(&["INVALID SQL"]);
959 match result {
960 Err(e) => {
961 let msg = e.to_string();
962 assert!(
963 msg.contains("fixture"),
964 "error should mention fixture: {msg}"
965 );
966 }
967 Ok(_) => panic!("should have failed"),
968 }
969 }
970
971 #[test]
976 fn sqlite_fixtures_order_dependent_with_fk() {
977 let ctx = setup_sqlite_test(&[
979 "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
980 "CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER REFERENCES users(id))",
981 "INSERT INTO users VALUES (1, 'Alice')",
982 "INSERT INTO orders VALUES (1, 1)",
983 ])
984 .unwrap();
985 assert!(ctx.db_path.exists());
987 }
988
989 #[test]
994 fn sqlite_large_fixture() {
995 let mut sql = String::from("CREATE TABLE big (id INTEGER PRIMARY KEY, data TEXT);\n");
996 for i in 0..1000 {
997 sql.push_str(&format!("INSERT INTO big VALUES ({i}, 'row_{i}');\n"));
998 }
999 let ctx = setup_sqlite_test(&[&sql]).unwrap();
1000 assert!(ctx.db_path.exists());
1001 }
1002
1003 #[test]
1008 fn sqlite_fixture_with_pragma() {
1009 let ctx = setup_sqlite_test(&[
1010 "PRAGMA foreign_keys = ON",
1011 "CREATE TABLE t (id INTEGER PRIMARY KEY)",
1012 ])
1013 .unwrap();
1014 assert!(ctx.db_path.exists());
1015 }
1016 }
1017}