test: consolidate 25 integration crates into one it binary

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.
This commit is contained in:
claude@clouddev1
2026-06-02 22:13:03 +00:00
parent 42f95533ac
commit 9efae59c3c
27 changed files with 122 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
//! Iteration-4a integration tests: the explicit `rebuild`
//! app-level command (ADR-0015 §7, §11).
//!
//! Covers the App-level dispatch (typing `rebuild` opens the
//! confirmation modal) and the worker-level wipe-and-rebuild
//! against a populated database. The runtime's spawn glue
//! is exercised manually here since we don't boot a Tokio
//! event loop in tests; we drive `Database::rebuild_from_text`
//! directly to verify it works on a populated db.
use std::fs;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use rdbms_playground::action::Action;
use rdbms_playground::app::{App, Modal, RebuildConfirmModal};
use rdbms_playground::db::Database;
use rdbms_playground::dsl::{ColumnSpec, Type, Value};
use rdbms_playground::event::AppEvent;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;
const fn key(code: KeyCode) -> AppEvent {
AppEvent::Key(KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: crossterm::event::KeyEventState::NONE,
})
}
fn type_str(app: &mut App, s: &str) {
for c in s.chars() {
app.update(key(KeyCode::Char(c)));
}
}
fn submit(app: &mut App) -> Vec<Action> {
app.update(key(KeyCode::Enter))
}
fn tempdir() -> tempfile::TempDir {
tempfile::tempdir().expect("create tempdir")
}
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio rt")
}
#[test]
fn typing_rebuild_emits_prepare_action() {
let mut app = App::new();
type_str(&mut app, "rebuild");
let actions = submit(&mut app);
assert_eq!(actions, vec![Action::PrepareRebuild]);
// No modal yet — the runtime still has to compute the
// summary and post `RebuildPrepared` back.
assert!(app.modal.is_none());
}
#[test]
fn rebuild_prepared_event_opens_modal_with_summary() {
let mut app = App::new();
app.update(AppEvent::RebuildPrepared {
summary: "3 tables and 47 rows will be reconstructed".to_string(),
});
match app.modal.as_ref() {
Some(Modal::RebuildConfirm(RebuildConfirmModal { summary })) => {
assert!(summary.contains("3 tables"));
}
other => panic!("expected RebuildConfirm modal, got {other:?}"),
}
}
#[test]
fn modal_y_emits_rebuild_action_and_closes() {
let mut app = App::new();
app.update(AppEvent::RebuildPrepared {
summary: "summary".to_string(),
});
let actions = app.update(key(KeyCode::Char('Y')));
assert_eq!(actions.len(), 1);
let Action::Rebuild { source } = &actions[0] else {
panic!("expected Rebuild action, got {:?}", actions[0]);
};
assert_eq!(source, "rebuild");
assert!(app.modal.is_none(), "modal should close on confirm");
}
#[test]
fn modal_n_or_esc_dismisses_without_action() {
for code in [KeyCode::Char('N'), KeyCode::Esc] {
let mut app = App::new();
app.update(AppEvent::RebuildPrepared {
summary: "summary".to_string(),
});
let actions = app.update(key(code));
assert!(actions.is_empty(), "no actions emitted on dismiss");
assert!(app.modal.is_none(), "modal should close on dismiss");
}
}
#[test]
fn modal_swallows_unrelated_keys() {
let mut app = App::new();
app.update(AppEvent::RebuildPrepared {
summary: "summary".to_string(),
});
// A regular character key should not type into the input
// field while the modal is up.
app.update(key(KeyCode::Char('x')));
assert!(app.input.is_empty(), "modal should swallow key input");
assert!(app.modal.is_some(), "modal still active after unrelated key");
}
#[test]
fn rebuild_against_populated_db_wipes_and_reloads() {
let data = tempdir();
let project_path = {
let project = project::open_or_create(None, Some(data.path())).unwrap();
let path = project.path().to_path_buf();
let db = Database::open_with_persistence(
project.db_path(),
Persistence::new(path.clone()),
)
.unwrap();
rt().block_on(async {
db.create_table(
"Customers".to_string(),
vec![
ColumnSpec::new("id".to_string(), Type::Serial),
ColumnSpec::new("Name".to_string(), Type::Text),
],
vec!["id".to_string()],
Some("create".to_string()),
)
.await
.unwrap();
db.insert(
"Customers".to_string(),
None,
vec![Value::Text("Alice".to_string())],
Some("insert".to_string()),
)
.await
.unwrap();
});
drop(db);
drop(project);
path
};
// Hand-edit the CSV to introduce a different row content.
// Rebuild should pick up the edited content.
let csv_path = project_path.join("data").join("Customers.csv");
let edited = fs::read_to_string(&csv_path).unwrap().replace("Alice", "Edna");
fs::write(&csv_path, edited).unwrap();
// Reopen with persistence (the .db still exists but has
// "Alice"). Run rebuild — it should wipe and reload.
let project = project::Project::open(&project_path).unwrap();
let db = Database::open_with_persistence(
project.db_path(),
Persistence::new(project.path().to_path_buf()),
)
.unwrap();
rt().block_on(async {
db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))
.await
.expect("rebuild");
});
let rows = rt()
.block_on(async { db.query_data("Customers".to_string(), None, None, None).await })
.unwrap();
assert_eq!(rows.rows.len(), 1);
assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));
// history.log should contain the rebuild entry.
let history = fs::read_to_string(project_path.join("history.log")).unwrap();
assert!(
history.lines().any(|l| l.ends_with("|ok|rebuild")),
"history.log missing rebuild entry:\n{history}",
);
}