//! Iteration-1 integration tests: end-to-end project lifecycle //! through the public API the runtime uses on startup. //! //! These tests do NOT run the Tokio loop or the terminal; they //! exercise the same `project::open_or_create` entry point the //! runtime calls, plus a `Database::open` against the resulting //! path, to confirm the file-backed SQLite database actually //! lands inside the project directory and is queryable. use std::fs; use rdbms_playground::cli::Args; use rdbms_playground::db::Database; use rdbms_playground::project::{ self, GITIGNORE, HISTORY_LOG, PLAYGROUND_DB, PROJECT_YAML, PROJECTS_SUBDIR, }; fn tempdir() -> tempfile::TempDir { tempfile::tempdir().expect("create tempdir") } #[test] fn no_args_creates_temp_project_under_data_root() { let data = tempdir(); let project = project::open_or_create(None, Some(data.path())) .expect("open_or_create with empty CLI"); let path = project.path(); assert!(path.exists(), "project dir should exist"); assert!(path.starts_with(data.path())); assert_eq!( path.parent().and_then(|p| p.file_name()).map(|s| s.to_string_lossy().into_owned()), Some(PROJECTS_SUBDIR.to_string()), ); // Skeleton files. assert!(path.join(PROJECT_YAML).exists()); assert!(path.join("data").is_dir()); assert!(path.join(HISTORY_LOG).exists()); assert!(path.join(GITIGNORE).exists()); assert!(path.join(".rdbms-playground.lock").exists()); // .gitignore must NOT include history.log (ADR-0007 amendment). let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap(); assert!(!gi.contains("history.log")); } #[test] fn db_opens_inside_project_and_creates_the_file() { let data = tempdir(); let project = project::open_or_create(None, Some(data.path())).unwrap(); let db_path = project.db_path(); // Before opening, the .db file does not exist. assert!(!db_path.exists()); let _db = Database::open(&db_path).expect("open db at project path"); // After opening, sqlite has created the file. assert!(db_path.exists()); assert_eq!(db_path.parent(), Some(project.path())); } #[test] fn second_open_of_same_project_is_refused_by_lock() { let data = tempdir(); let first = project::open_or_create(None, Some(data.path())).unwrap(); let path = first.path().to_path_buf(); let err = project::Project::open(&path).expect_err("second open should fail"); let msg = format!("{err}"); assert!( msg.contains("already open"), "expected lock-held error, got: {msg}" ); } #[test] fn open_succeeds_after_first_project_is_dropped() { let data = tempdir(); let path = { let p = project::open_or_create(None, Some(data.path())).unwrap(); p.path().to_path_buf() }; // Lock should have been released; reopen succeeds. let _reopened = project::Project::open(&path).expect("reopen after drop"); } #[test] fn positional_path_opens_existing_project() { let data = tempdir(); let path = { let p = project::open_or_create(None, Some(data.path())).unwrap(); p.path().to_path_buf() }; // Now drive open_or_create with the path as if it were a // CLI positional argument. let project = project::open_or_create(Some(&path), None) .expect("open via positional path"); assert_eq!(project.path(), path); } #[test] fn positional_nonexistent_path_is_refused() { let data = tempdir(); let bogus = data.path().join("nope"); let err = project::open_or_create(Some(&bogus), Some(data.path())) .expect_err("must refuse nonexistent path"); let msg = format!("{err}"); assert!(msg.contains("does not exist"), "got: {msg}"); } #[test] fn cli_args_thread_through_to_project_creation() { // End-to-end: CLI parsing → open_or_create → on-disk project. let data = tempdir(); let data_str = data.path().to_string_lossy().into_owned(); let args = Args::parse(["--data-dir", data_str.as_str()]).expect("parse args"); assert_eq!(args.data_dir.as_deref(), Some(data.path())); assert!(args.project_path.is_none()); let project = project::open_or_create(args.project_path.as_deref(), args.data_dir.as_deref()) .expect("create temp via parsed CLI"); assert!(project.path().starts_with(data.path())); } #[test] fn data_dir_override_does_not_touch_default_os_dir() { // Sanity check that --data-dir really replaces the default — // creating two temp projects under the override should leave // them both there, and the OS-standard data dir is not // touched. We can't easily inspect the OS-standard dir // without actually creating things in it, so we settle for // confirming the override directory is the active one. let data = tempdir(); let p1 = project::open_or_create(None, Some(data.path())).unwrap(); let p1_path = p1.path().to_path_buf(); drop(p1); let p2 = project::open_or_create(None, Some(data.path())).unwrap(); let p2_path = p2.path().to_path_buf(); assert!(p1_path.starts_with(data.path())); assert!(p2_path.starts_with(data.path())); assert_ne!(p1_path, p2_path, "two temp projects must have distinct names"); } #[test] fn db_persists_across_open_close_cycles() { // Iteration 1's headline UX win: quitting no longer loses // work. With a file-backed database, data written in one // session is visible after re-opening the project. let data = tempdir(); let project = project::open_or_create(None, Some(data.path())).unwrap(); let path = project.path().to_path_buf(); let db_path = project.db_path(); // Write something via SQLite directly. (The DSL/runtime path // would do the same but isn't reachable from a sync test.) { let db = Database::open(&db_path).expect("open db"); let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); rt.block_on(async { db.create_table( "Customers".to_string(), vec![ rdbms_playground::dsl::ColumnSpec { name: "id".to_string(), ty: rdbms_playground::dsl::Type::Serial, }, rdbms_playground::dsl::ColumnSpec { name: "Name".to_string(), ty: rdbms_playground::dsl::Type::Text, }, ], vec!["id".to_string()], ) .await .expect("create_table"); }); } // Drop the project (releases the lock). drop(project); // Re-open and confirm the table is still there. let reopened = project::Project::open(&path).expect("reopen"); let db = Database::open(reopened.db_path()).expect("re-open db"); let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); let tables = rt.block_on(async { db.list_tables().await }).expect("list_tables"); assert!(tables.iter().any(|t| t == "Customers"), "got: {tables:?}"); // Sanity: the project.yaml and history.log are still empty // skeleton files (Iteration 2 will populate them). assert!(reopened.path().join(PROJECT_YAML).exists()); assert!(reopened.path().join(PLAYGROUND_DB).exists()); }