9efae59c3c
Each top-level tests/*.rs was its own crate → its own binary, each statically linking the bundled engine + every dep. 26 of them, so an edit to the lib relinked all 26. Moved the 25 standalone files into tests/it/ under one tests/it/main.rs (the pattern typing_surface already uses); cargo auto-detects it as the `it` target. End state: 2 integration-test binaries instead of 26. Result: target/debug/deps 1.5 GB → 629 MB (-58%). Build time barely moved (clean 22.9s→22.4s, lib-edit relink 13.3s→12.4s) — wall-clock is dominated by compiling, not linking, so this is a disk win, not a speed win (see docs/plans/20260602-test-consolidation.md). Tests unchanged at 2151/0/1; clippy clean; no fixups needed. typing_surface_matrix stays its own already-consolidated binary. Tradeoff: the 25 files now share one crate (a compile error fails the whole `it` binary; module-scoped namespaces, no clashes) — negligible for a solo project.
241 lines
8.1 KiB
Rust
241 lines
8.1 KiB
Rust
//! Iteration-6 integration tests: `--resume` + persistent
|
|
//! input history + migration framework scaffold (ADR-0015 §7,
|
|
//! §9, §12).
|
|
//!
|
|
//! Boots no Tokio runtime and no terminal — these tests
|
|
//! exercise the persistent state behind `--resume` (the
|
|
//! `last_project` file under the data root) and the input
|
|
//! history hydration off `history.log`.
|
|
|
|
use std::fs;
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
|
|
use rdbms_playground::app::App;
|
|
use rdbms_playground::cli::{Args, ArgsError};
|
|
use rdbms_playground::event::AppEvent;
|
|
use rdbms_playground::persistence::Persistence;
|
|
use rdbms_playground::project::{
|
|
self, LAST_PROJECT_FILE, Project, read_last_project, write_last_project,
|
|
};
|
|
|
|
fn tempdir() -> tempfile::TempDir {
|
|
tempfile::tempdir().expect("create tempdir")
|
|
}
|
|
|
|
// --- Args parsing for --resume ---------------------------------
|
|
|
|
#[test]
|
|
fn args_parses_resume_flag() {
|
|
let a = Args::parse(["--resume"]).unwrap();
|
|
assert!(a.resume);
|
|
assert!(a.project_path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn args_resume_with_positional_path_is_an_error() {
|
|
let err = Args::parse(["--resume", "/tmp/foo"]).unwrap_err();
|
|
assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn args_resume_after_positional_path_also_errors() {
|
|
let err = Args::parse(["/tmp/foo", "--resume"]).unwrap_err();
|
|
assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn args_help_listing_mentions_resume() {
|
|
assert!(rdbms_playground::cli::help_text().contains("--resume"));
|
|
}
|
|
|
|
// --- last_project read/write ----------------------------------
|
|
|
|
#[test]
|
|
fn last_project_round_trips_through_disk() {
|
|
let tmp = tempdir();
|
|
let target = tmp.path().join("MyProject");
|
|
fs::create_dir(&target).unwrap();
|
|
write_last_project(tmp.path(), &target).unwrap();
|
|
|
|
let on_disk = fs::read_to_string(tmp.path().join(LAST_PROJECT_FILE)).unwrap();
|
|
assert!(on_disk.contains("MyProject"));
|
|
|
|
assert_eq!(read_last_project(tmp.path()).unwrap(), Some(target));
|
|
}
|
|
|
|
#[test]
|
|
fn last_project_is_overwritten_each_call() {
|
|
let tmp = tempdir();
|
|
let a = tmp.path().join("A");
|
|
let b = tmp.path().join("B");
|
|
fs::create_dir(&a).unwrap();
|
|
fs::create_dir(&b).unwrap();
|
|
write_last_project(tmp.path(), &a).unwrap();
|
|
write_last_project(tmp.path(), &b).unwrap();
|
|
assert_eq!(read_last_project(tmp.path()).unwrap(), Some(b));
|
|
}
|
|
|
|
#[test]
|
|
fn last_project_create_temp_path_resolves_to_existing_dir() {
|
|
// Sanity: the path we record is in fact something that
|
|
// exists when --resume tries to reopen it. This protects
|
|
// against future refactors that might write a placeholder.
|
|
let tmp = tempdir();
|
|
let project = Project::create_temp(tmp.path()).unwrap();
|
|
write_last_project(tmp.path(), project.path()).unwrap();
|
|
let read_back = read_last_project(tmp.path()).unwrap();
|
|
assert_eq!(read_back.as_deref(), Some(project.path()));
|
|
assert!(read_back.unwrap().exists());
|
|
}
|
|
|
|
#[test]
|
|
fn read_last_project_handles_missing_data_root_directory() {
|
|
let tmp = tempdir();
|
|
let nested = tmp.path().join("does/not/exist/yet");
|
|
// Reading from a directory that hasn't been created at
|
|
// all should be Ok(None), not an error — the runtime's
|
|
// first launch lands here.
|
|
assert!(read_last_project(&nested).unwrap().is_none());
|
|
}
|
|
|
|
// --- Stale path on resume: read returns Some(path) but the
|
|
// path does not exist. The runtime is responsible for
|
|
// surfacing this; we verify the building block here.
|
|
|
|
#[test]
|
|
fn last_project_returns_stale_path_verbatim_for_runtime_to_detect() {
|
|
let tmp = tempdir();
|
|
let stale = tmp.path().join("Vanished");
|
|
write_last_project(tmp.path(), &stale).unwrap();
|
|
let read_back = read_last_project(tmp.path()).unwrap();
|
|
assert_eq!(read_back.as_deref(), Some(stale.as_path()));
|
|
assert!(!stale.exists());
|
|
}
|
|
|
|
// --- Project lifecycle writes last_project ---------------------
|
|
// (Smoke test: launching open_or_create then opening again
|
|
// should be the same as write_last_project + reopen.)
|
|
|
|
// --- History hydration on project open ----------------------
|
|
|
|
const fn key(code: KeyCode) -> AppEvent {
|
|
AppEvent::Key(KeyEvent {
|
|
code,
|
|
modifiers: KeyModifiers::NONE,
|
|
kind: KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn read_recent_history_returns_empty_when_log_missing() {
|
|
let tmp = tempdir();
|
|
let p = Persistence::new(tmp.path().to_path_buf());
|
|
let entries = p.read_recent_history(10).unwrap();
|
|
assert!(entries.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn read_recent_history_returns_appended_entries_in_order() {
|
|
let tmp = tempdir();
|
|
let project = Project::create_temp(tmp.path()).unwrap();
|
|
let p = Persistence::new(project.path().to_path_buf());
|
|
p.append_history("create table A with pk").unwrap();
|
|
p.append_history("create table B with pk").unwrap();
|
|
p.append_history("create table C with pk").unwrap();
|
|
let entries = p.read_recent_history(10).unwrap();
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
"create table A with pk".to_string(),
|
|
"create table B with pk".to_string(),
|
|
"create table C with pk".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hydration_reads_both_ok_and_err_records() {
|
|
// ADR-0034 §1/§2: failed commands are journalled `err`, and
|
|
// input-history hydration reads ALL records (ok + err) so a
|
|
// typo'd / rejected command from a previous session is
|
|
// recallable after restart — matching the in-session ring's
|
|
// "record everything" behaviour.
|
|
let tmp = tempdir();
|
|
let project = Project::create_temp(tmp.path()).unwrap();
|
|
let p = Persistence::new(project.path().to_path_buf());
|
|
p.append_history("create table A with pk").unwrap();
|
|
p.append_history_failure("insert into A (1, 2, 3)").unwrap();
|
|
p.append_history("show data A").unwrap();
|
|
let entries = p.read_recent_history(10).unwrap();
|
|
assert_eq!(
|
|
entries,
|
|
vec![
|
|
"create table A with pk".to_string(),
|
|
"insert into A (1, 2, 3)".to_string(), // the err record is recalled
|
|
"show data A".to_string(),
|
|
],
|
|
"hydration includes the err record",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn seed_history_replaces_in_memory_history() {
|
|
let mut app = App::new();
|
|
// Pre-existing in-session entries — should be replaced.
|
|
for c in "abc".chars() {
|
|
app.update(key(KeyCode::Char(c)));
|
|
}
|
|
app.update(key(KeyCode::Enter));
|
|
assert_eq!(app.history, vec!["abc".to_string()]);
|
|
|
|
app.seed_history(vec!["x".to_string(), "y".to_string()]);
|
|
assert_eq!(app.history, vec!["x".to_string(), "y".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn seed_history_preserves_chronological_order_for_navigation() {
|
|
let mut app = App::new();
|
|
app.seed_history(vec![
|
|
"old".to_string(),
|
|
"middle".to_string(),
|
|
"newest".to_string(),
|
|
]);
|
|
// Up should recall "newest" first (the most recent
|
|
// entry, which is at the back of the vec by convention).
|
|
app.update(key(KeyCode::Up));
|
|
assert_eq!(app.input, "newest");
|
|
app.update(key(KeyCode::Up));
|
|
assert_eq!(app.input, "middle");
|
|
app.update(key(KeyCode::Up));
|
|
assert_eq!(app.input, "old");
|
|
}
|
|
|
|
#[test]
|
|
fn project_switched_event_seeds_history_from_payload() {
|
|
let mut app = App::new();
|
|
app.update(AppEvent::ProjectSwitched {
|
|
display_name: "Foo".to_string(),
|
|
is_temp: false,
|
|
history_entries: vec!["aa".to_string(), "bb".to_string()],
|
|
mode: rdbms_playground::mode::Mode::Simple,
|
|
});
|
|
assert_eq!(app.history, vec!["aa".to_string(), "bb".to_string()]);
|
|
// Up navigates within the seeded entries.
|
|
app.update(key(KeyCode::Up));
|
|
assert_eq!(app.input, "bb");
|
|
}
|
|
|
|
#[test]
|
|
fn data_root_with_no_last_project_is_resume_safe() {
|
|
let tmp = tempdir();
|
|
// Fresh data root with no projects, no last_project.
|
|
let _project = project::open_or_create(None, Some(tmp.path())).unwrap();
|
|
// open_or_create itself doesn't write last_project (the
|
|
// runtime does, after a successful open). That's fine —
|
|
// the runtime test would write it. Verify that
|
|
// read_last_project here returns None as expected.
|
|
assert!(read_last_project(tmp.path()).unwrap().is_none());
|
|
}
|