Cleanup pass: --help, in-app help, post-rebuild message, unmodified-temp cleanup
Four post-Iteration-4 polish items surfaced by manual testing.
1. `--help` / `-h` CLI flag prints a usage banner (options +
app-level commands + DSL grammar reference) and exits. Parse
errors also print the banner to stderr.
2. `help` app-level command notes the same list of supported
commands to the output panel -- a simple stand-in for the
richer H3 help system, kept in sync with what's actually
wired up.
3. The silent rebuild that runs when playground.db is missing
now surfaces a system message in the output panel ("[ok]
rebuild -- N tables, M rows reconstructed; ...") via a new
initial_events plumbing. The user no longer wonders whether
the .db was magically restored or whether anything happened
on launch.
4. Unmodified empty temp projects (kind=Temp, project.yaml has
tables: [] and relationships: []) are now auto-deleted when
the user switches away (load / new / save as) or quits. This
addresses the "launch app, load existing project, quit"
pattern that was leaving an empty temp directory behind
every time. Modified temps (with any user-created tables or
relationships) are never auto-deleted; corrupted projects
are also never auto-deleted (defensive default-to-false on
yaml read/parse errors).
Tests: 338 passing (272 lib + 9 + 5 + 6 + 20 + 9 + 17),
0 failing, 0 skipped. Clippy clean.
This commit is contained in:
+41
@@ -598,6 +598,10 @@ impl App {
|
|||||||
// 5 (export, import).
|
// 5 (export, import).
|
||||||
match effective_input.as_str() {
|
match effective_input.as_str() {
|
||||||
"quit" | "q" => return vec![Action::Quit],
|
"quit" | "q" => return vec![Action::Quit],
|
||||||
|
"help" => {
|
||||||
|
self.note_help();
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
"rebuild" => return vec![Action::PrepareRebuild],
|
"rebuild" => return vec![Action::PrepareRebuild],
|
||||||
"save" => {
|
"save" => {
|
||||||
return self.handle_save_command(false);
|
return self.handle_save_command(false);
|
||||||
@@ -1024,6 +1028,43 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Note a flat list of currently-supported app-level
|
||||||
|
/// commands to the output panel.
|
||||||
|
///
|
||||||
|
/// This is the simple Iteration-4 stand-in for a richer
|
||||||
|
/// help system (H3 in the requirements doc); it gives the
|
||||||
|
/// user a quick "what can I type?" reference that's
|
||||||
|
/// always accurate against the build they're running. As
|
||||||
|
/// new commands land, append them here.
|
||||||
|
fn note_help(&mut self) {
|
||||||
|
self.note_system("Supported commands:");
|
||||||
|
for line in [
|
||||||
|
" quit / q — exit",
|
||||||
|
" help — this list",
|
||||||
|
" mode simple|advanced — switch input mode",
|
||||||
|
" rebuild — rebuild .db from project.yaml + data/ (with confirmation)",
|
||||||
|
" save — save current temp project under a name",
|
||||||
|
" save as — copy current project to a new name/path",
|
||||||
|
" new — close current, start a fresh temp project",
|
||||||
|
" load — open the project picker",
|
||||||
|
"DSL data commands (in simple mode):",
|
||||||
|
" create table <T> with pk [<col>:<type>...]",
|
||||||
|
" drop table <T>",
|
||||||
|
" add column [to table] <T>: <col> (<type>)",
|
||||||
|
" add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>",
|
||||||
|
" [on delete <action>] [on update <action>] [--create-fk]",
|
||||||
|
" drop relationship <name>",
|
||||||
|
" insert into <T> [(cols)] [values] (vals)",
|
||||||
|
" update <T> set <c>=<v>... where <c>=<v> | --all-rows",
|
||||||
|
" delete from <T> where <c>=<v> | --all-rows",
|
||||||
|
" show table <T>",
|
||||||
|
" show data <T>",
|
||||||
|
"Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid",
|
||||||
|
] {
|
||||||
|
self.note_system(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_mode_command(&mut self, raw: &str) {
|
fn handle_mode_command(&mut self, raw: &str) {
|
||||||
let arg = raw.strip_prefix("mode").unwrap_or(raw).trim();
|
let arg = raw.strip_prefix("mode").unwrap_or(raw).trim();
|
||||||
match arg {
|
match arg {
|
||||||
|
|||||||
+54
@@ -20,8 +20,45 @@ pub struct Args {
|
|||||||
/// this path (L1, ADR-0015 §1). Mutually exclusive with
|
/// this path (L1, ADR-0015 §1). Mutually exclusive with
|
||||||
/// `--resume` once that lands.
|
/// `--resume` once that lands.
|
||||||
pub project_path: Option<PathBuf>,
|
pub project_path: Option<PathBuf>,
|
||||||
|
/// `--help` / `-h`: print usage to stdout and exit. The
|
||||||
|
/// runtime checks this flag before doing any other work.
|
||||||
|
pub help: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Usage banner printed by `--help`. Kept as one block so the
|
||||||
|
/// formatting is reviewable on its own.
|
||||||
|
pub const HELP_TEXT: &str = "\
|
||||||
|
rdbms-playground — a TUI playground for relational database concepts
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
rdbms-playground [options] [<project-path>]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<project-path> Path to an existing project directory.
|
||||||
|
Without this, a fresh auto-named temp
|
||||||
|
project is created in the data dir.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Print this help and exit.
|
||||||
|
--theme <light|dark> Override theme (default: auto-detect).
|
||||||
|
--data-dir <PATH> Use PATH as the data root instead of
|
||||||
|
the OS-standard location for this run.
|
||||||
|
--log-file <PATH> Write tracing output to PATH.
|
||||||
|
|
||||||
|
App-level commands (typed inside the app, available in both modes):
|
||||||
|
quit / q Exit cleanly.
|
||||||
|
mode simple|advanced Switch input mode.
|
||||||
|
help Show this list of commands in-app.
|
||||||
|
save Save the current temp project under a
|
||||||
|
chosen name (or `save as` to copy a
|
||||||
|
named project to a new location).
|
||||||
|
save as Always prompt for a target name/path.
|
||||||
|
new Close current, create a fresh temp.
|
||||||
|
load Open the project picker.
|
||||||
|
rebuild Rebuild playground.db from project.yaml
|
||||||
|
+ data/, with confirmation.
|
||||||
|
";
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ArgsError {
|
pub enum ArgsError {
|
||||||
#[error("missing value for --{0}")]
|
#[error("missing value for --{0}")]
|
||||||
@@ -54,9 +91,13 @@ impl Args {
|
|||||||
let mut log_path = env::var_os("RDBMS_PLAYGROUND_LOG_FILE").map(PathBuf::from);
|
let mut log_path = env::var_os("RDBMS_PLAYGROUND_LOG_FILE").map(PathBuf::from);
|
||||||
let mut data_dir: Option<PathBuf> = None;
|
let mut data_dir: Option<PathBuf> = None;
|
||||||
let mut project_path: Option<PathBuf> = None;
|
let mut project_path: Option<PathBuf> = None;
|
||||||
|
let mut help = false;
|
||||||
let mut iter = iter.into_iter().map(Into::into);
|
let mut iter = iter.into_iter().map(Into::into);
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
|
"--help" | "-h" => {
|
||||||
|
help = true;
|
||||||
|
}
|
||||||
"--theme" => {
|
"--theme" => {
|
||||||
let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
|
let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
|
||||||
theme = match value.as_str() {
|
theme = match value.as_str() {
|
||||||
@@ -98,6 +139,7 @@ impl Args {
|
|||||||
log_path,
|
log_path,
|
||||||
data_dir,
|
data_dir,
|
||||||
project_path,
|
project_path,
|
||||||
|
help,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +245,18 @@ mod tests {
|
|||||||
assert!(matches!(err, ArgsError::MultiplePaths { .. }), "got: {err:?}");
|
assert!(matches!(err, ArgsError::MultiplePaths { .. }), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_flag_long_form_sets_help() {
|
||||||
|
let args = Args::parse(["--help"]).unwrap();
|
||||||
|
assert!(args.help);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_flag_short_form_sets_help() {
|
||||||
|
let args = Args::parse(["-h"]).unwrap();
|
||||||
|
assert!(args.help);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_double_dash_flag_errors_even_with_positional() {
|
fn unknown_double_dash_flag_errors_even_with_positional() {
|
||||||
// Make sure the path-vs-flag distinction is robust:
|
// Make sure the path-vs-flag distinction is robust:
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use rdbms_playground::cli::Args;
|
use rdbms_playground::cli::{Args, HELP_TEXT};
|
||||||
use rdbms_playground::{logging, runtime};
|
use rdbms_playground::{logging, runtime};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -8,10 +8,16 @@ fn main() -> ExitCode {
|
|||||||
Ok(args) => args,
|
Ok(args) => args,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("rdbms-playground: {e}");
|
eprintln!("rdbms-playground: {e}");
|
||||||
|
eprintln!("\n{HELP_TEXT}");
|
||||||
return ExitCode::from(2);
|
return ExitCode::from(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if args.help {
|
||||||
|
print!("{HELP_TEXT}");
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(e) = logging::init(args.log_path.as_deref()) {
|
if let Err(e) = logging::init(args.log_path.as_deref()) {
|
||||||
eprintln!("rdbms-playground: failed to initialise logging: {e:#}");
|
eprintln!("rdbms-playground: failed to initialise logging: {e:#}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
|
|||||||
@@ -346,6 +346,37 @@ impl Project {
|
|||||||
self.kind
|
self.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is this an auto-named temp project that the user has
|
||||||
|
/// not modified?
|
||||||
|
///
|
||||||
|
/// Used to clean up the inevitable accumulation of
|
||||||
|
/// auto-named temp directories left behind when the user
|
||||||
|
/// launches the app, immediately loads another project
|
||||||
|
/// (or quits without doing anything), and never returns
|
||||||
|
/// to the temp.
|
||||||
|
///
|
||||||
|
/// "Unmodified" is defined as: kind is Temp AND
|
||||||
|
/// `project.yaml` lists no tables and no relationships.
|
||||||
|
/// The user-visible schema is what counts — show queries
|
||||||
|
/// only append to history.log and don't trip this check.
|
||||||
|
/// Errors reading or parsing the YAML default to "not
|
||||||
|
/// unmodified" (false), so a corrupted project is never
|
||||||
|
/// auto-deleted.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_unmodified_temp(&self) -> bool {
|
||||||
|
if !matches!(self.kind, ProjectKind::Temp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let yaml_path = self.path.join(PROJECT_YAML);
|
||||||
|
let Ok(body) = fs::read_to_string(&yaml_path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(snapshot) = crate::persistence::parse_schema(&body) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
snapshot.tables.is_empty() && snapshot.relationships.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Path to the SQLite database for this project. Always
|
/// Path to the SQLite database for this project. Always
|
||||||
/// `<project>/playground.db`.
|
/// `<project>/playground.db`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
+82
-9
@@ -64,15 +64,25 @@ pub async fn run(args: Args) -> Result<()> {
|
|||||||
let db_existed = db_path.exists();
|
let db_existed = db_path.exists();
|
||||||
let database = Database::open_with_persistence(db_path.as_path(), persistence)
|
let database = Database::open_with_persistence(db_path.as_path(), persistence)
|
||||||
.context("open database")?;
|
.context("open database")?;
|
||||||
if !db_existed
|
let mut initial_events: Vec<AppEvent> = Vec::new();
|
||||||
&& let Err(e) = database
|
if !db_existed {
|
||||||
.rebuild_from_text(project_path.clone(), None)
|
match database.rebuild_from_text(project_path.clone(), None).await {
|
||||||
.await
|
Ok(()) => {
|
||||||
{
|
// Surface the silent rebuild as a system note
|
||||||
// The terminal is still in cooked mode here (we haven't
|
// so the user sees that the .db was
|
||||||
// entered the alternate screen yet), so writing to
|
// reconstructed rather than wondering whether
|
||||||
// stderr lands directly in the user's shell. Drop the
|
// anything happened.
|
||||||
// project to release the lock first.
|
let summary = summarize_project(&project_path).unwrap_or_else(|_| {
|
||||||
|
"rebuilt playground.db from project.yaml + data/".to_string()
|
||||||
|
});
|
||||||
|
initial_events.push(AppEvent::RebuildSucceeded { summary });
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// The terminal is still in cooked mode here
|
||||||
|
// (we haven't entered the alternate screen
|
||||||
|
// yet), so writing to stderr lands directly
|
||||||
|
// in the user's shell. Drop the project to
|
||||||
|
// release the lock first.
|
||||||
drop(project);
|
drop(project);
|
||||||
if matches!(
|
if matches!(
|
||||||
e,
|
e,
|
||||||
@@ -83,6 +93,8 @@ pub async fn run(args: Args) -> Result<()> {
|
|||||||
}
|
}
|
||||||
return Err(anyhow::anyhow!(e.friendly_message())).context("rebuild from text");
|
return Err(anyhow::anyhow!(e.friendly_message())).context("rebuild from text");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut terminal = setup_terminal().context("setup terminal")?;
|
let mut terminal = setup_terminal().context("setup terminal")?;
|
||||||
let result = run_loop(
|
let result = run_loop(
|
||||||
@@ -95,6 +107,7 @@ pub async fn run(args: Args) -> Result<()> {
|
|||||||
},
|
},
|
||||||
display_name,
|
display_name,
|
||||||
project_is_temp,
|
project_is_temp,
|
||||||
|
initial_events,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = teardown_terminal(&mut terminal) {
|
if let Err(e) = teardown_terminal(&mut terminal) {
|
||||||
@@ -149,6 +162,7 @@ async fn run_loop(
|
|||||||
mut session: Session,
|
mut session: Session,
|
||||||
project_display_name: String,
|
project_display_name: String,
|
||||||
project_is_temp: bool,
|
project_is_temp: bool,
|
||||||
|
initial_events: Vec<AppEvent>,
|
||||||
) -> Result<Option<String>> {
|
) -> Result<Option<String>> {
|
||||||
let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(EVENT_CHANNEL_CAPACITY);
|
let (event_tx, mut event_rx) = mpsc::channel::<AppEvent>(EVENT_CHANNEL_CAPACITY);
|
||||||
let reader_handle = spawn_event_reader(event_tx.clone());
|
let reader_handle = spawn_event_reader(event_tx.clone());
|
||||||
@@ -157,6 +171,14 @@ async fn run_loop(
|
|||||||
app.project_name = Some(project_display_name);
|
app.project_name = Some(project_display_name);
|
||||||
app.project_is_temp = project_is_temp;
|
app.project_is_temp = project_is_temp;
|
||||||
|
|
||||||
|
// Send any startup events (e.g., the system-message form
|
||||||
|
// of "rebuilt from text on missing .db") so they're
|
||||||
|
// dispatched through the normal event path and end up in
|
||||||
|
// the output panel before the user types anything.
|
||||||
|
for event in initial_events {
|
||||||
|
let _ = event_tx.send(event).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Seed the table list with whatever the database currently
|
// Seed the table list with whatever the database currently
|
||||||
// shows. For a fresh in-memory DB this is empty, but doing
|
// shows. For a fresh in-memory DB this is empty, but doing
|
||||||
// it explicitly means file-backed databases (track 2) will
|
// it explicitly means file-backed databases (track 2) will
|
||||||
@@ -251,6 +273,31 @@ async fn run_loop(
|
|||||||
|
|
||||||
let _ = tokio::time::timeout(SHUTDOWN_GRACE, reader_handle).await;
|
let _ = tokio::time::timeout(SHUTDOWN_GRACE, reader_handle).await;
|
||||||
|
|
||||||
|
// Auto-delete the active project on quit if it's an
|
||||||
|
// unmodified temp — same rule as on project switch (see
|
||||||
|
// perform_switch). Captures the path first, drops the
|
||||||
|
// project (releasing the lock), then removes the dir.
|
||||||
|
let cleanup_on_quit: Option<std::path::PathBuf> = session
|
||||||
|
.project
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.is_unmodified_temp().then(|| p.path().to_path_buf()));
|
||||||
|
let _ = session.database.take();
|
||||||
|
let _ = session.project.take();
|
||||||
|
if let Some(stale) = cleanup_on_quit {
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&stale) {
|
||||||
|
tracing::warn!(
|
||||||
|
path = %stale.display(),
|
||||||
|
error = %e,
|
||||||
|
"could not clean up unmodified temp project on quit",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
path = %stale.display(),
|
||||||
|
"cleaned up unmodified temp project on quit",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!("event loop exited");
|
info!("event loop exited");
|
||||||
Ok(app.fatal_message.clone())
|
Ok(app.fatal_message.clone())
|
||||||
}
|
}
|
||||||
@@ -343,6 +390,15 @@ async fn perform_switch(
|
|||||||
copy_project(&src, dst).map_err(|e| e.to_string())?;
|
copy_project(&src, dst).map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture cleanup info from the OUTGOING project before
|
||||||
|
// we drop it: if it was an unmodified empty temp, we
|
||||||
|
// delete its directory after the switch so the data dir
|
||||||
|
// doesn't accumulate empty scratch projects.
|
||||||
|
let outgoing_cleanup_path: Option<std::path::PathBuf> =
|
||||||
|
session.project.as_ref().and_then(|p| {
|
||||||
|
p.is_unmodified_temp().then(|| p.path().to_path_buf())
|
||||||
|
});
|
||||||
|
|
||||||
// Drop current project + database BEFORE opening the new
|
// Drop current project + database BEFORE opening the new
|
||||||
// ones, releasing the old lock and stopping the old
|
// ones, releasing the old lock and stopping the old
|
||||||
// worker. Required for the "load my own current project"
|
// worker. Required for the "load my own current project"
|
||||||
@@ -351,6 +407,23 @@ async fn perform_switch(
|
|||||||
let _ = session.database.take();
|
let _ = session.database.take();
|
||||||
let _ = session.project.take();
|
let _ = session.project.take();
|
||||||
|
|
||||||
|
// The outgoing project's lock is now released; it's
|
||||||
|
// safe to remove its directory if it was unmodified.
|
||||||
|
if let Some(stale) = outgoing_cleanup_path {
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&stale) {
|
||||||
|
tracing::warn!(
|
||||||
|
path = %stale.display(),
|
||||||
|
error = %e,
|
||||||
|
"could not clean up unmodified temp project",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
path = %stale.display(),
|
||||||
|
"cleaned up unmodified temp project on switch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Open the destination project.
|
// Open the destination project.
|
||||||
let new_project = match &req {
|
let new_project = match &req {
|
||||||
SwitchRequest::Load { .. } | SwitchRequest::SaveAs { .. } => {
|
SwitchRequest::Load { .. } | SwitchRequest::SaveAs { .. } => {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ use rdbms_playground::app::{
|
|||||||
PathEntryPurpose,
|
PathEntryPurpose,
|
||||||
};
|
};
|
||||||
use rdbms_playground::event::AppEvent;
|
use rdbms_playground::event::AppEvent;
|
||||||
|
use rdbms_playground::db::Database;
|
||||||
|
use rdbms_playground::dsl::{ColumnSpec, Type};
|
||||||
|
use rdbms_playground::persistence::Persistence;
|
||||||
use rdbms_playground::project::{self, Project, ProjectKind, copy_project};
|
use rdbms_playground::project::{self, Project, ProjectKind, copy_project};
|
||||||
|
|
||||||
const fn key(code: KeyCode) -> AppEvent {
|
const fn key(code: KeyCode) -> AppEvent {
|
||||||
@@ -42,6 +45,26 @@ fn tempdir() -> tempfile::TempDir {
|
|||||||
tempfile::tempdir().expect("create tempdir")
|
tempfile::tempdir().expect("create tempdir")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_command_lists_supported_commands() {
|
||||||
|
let mut app = App::new();
|
||||||
|
type_str(&mut app, "help");
|
||||||
|
let actions = submit(&mut app);
|
||||||
|
assert!(actions.is_empty());
|
||||||
|
let body = app
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.text.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
for keyword in ["quit", "rebuild", "save", "load", "new", "create table"] {
|
||||||
|
assert!(
|
||||||
|
body.contains(keyword),
|
||||||
|
"help output missing `{keyword}`:\n{body}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn save_on_temp_opens_path_entry_modal() {
|
fn save_on_temp_opens_path_entry_modal() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
@@ -308,6 +331,62 @@ fn project_kind_recovered_from_dirname_on_open() {
|
|||||||
assert_eq!(opened_named.display_name(), "My Project");
|
assert_eq!(opened_named.display_name(), "My Project");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fresh_temp_is_unmodified() {
|
||||||
|
let data = tempdir();
|
||||||
|
let project = project::open_or_create(None, Some(data.path())).unwrap();
|
||||||
|
assert!(project.is_unmodified_temp());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temp_with_a_table_is_no_longer_unmodified() {
|
||||||
|
let data = tempdir();
|
||||||
|
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();
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
rt.block_on(async {
|
||||||
|
db.create_table(
|
||||||
|
"T".to_string(),
|
||||||
|
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
|
||||||
|
vec!["id".to_string()],
|
||||||
|
Some("create".to_string()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
drop(db);
|
||||||
|
drop(project);
|
||||||
|
|
||||||
|
let reopened = Project::open(&path).unwrap();
|
||||||
|
assert!(
|
||||||
|
!reopened.is_unmodified_temp(),
|
||||||
|
"a temp with a table should not be considered unmodified",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn named_project_is_never_unmodified_temp() {
|
||||||
|
let data = tempdir();
|
||||||
|
let temp = project::open_or_create(None, Some(data.path())).unwrap();
|
||||||
|
let temp_path = temp.path().to_path_buf();
|
||||||
|
drop(temp);
|
||||||
|
|
||||||
|
let named = data.path().join("MyOrders");
|
||||||
|
copy_project(&temp_path, &named).unwrap();
|
||||||
|
let opened = Project::open(&named).unwrap();
|
||||||
|
// Even though the schema is empty, kind is Named.
|
||||||
|
assert_eq!(opened.kind(), ProjectKind::Named);
|
||||||
|
assert!(!opened.is_unmodified_temp());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_projects_sorts_by_mtime() {
|
fn list_projects_sorts_by_mtime() {
|
||||||
let data = tempdir();
|
let data = tempdir();
|
||||||
|
|||||||
Reference in New Issue
Block a user