use super::chart_prepare_tests::{open, pump};
use crate::app::jobs::{Job, Outcome};
use crate::app::modals::filter_modal::{FilterOperator, FilterStatement, LogicalOperator};
use crate::*;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
const GONE: &str = "A file was removed or replaced after the dataset was opened: \
part-0.parquet. Reopen the dataset to read the current files.";
fn write_csv(path: &Path, rows: i64, keys: &[&str]) {
let mut body = String::from("id,key,val\n");
for id in 0..rows {
let key = keys[id as usize % keys.len()];
body.push_str(&format!("{id},{key},{}\n", id * 10));
}
std::fs::write(path, body).unwrap();
}
fn opened_app(
rows: i64,
keys: &[&str],
) -> (
App,
mpsc::Receiver<AppEvent>,
mpsc::Sender<AppEvent>,
tempfile::TempDir,
PathBuf,
) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.csv");
write_csv(&path, rows, keys);
let (tx, rx) = mpsc::channel();
let mut app = App::new(tx.clone(), crate::tests::test_runtime());
let config = crate::config::ConfigManager::with_dir(dir.path().join("config"));
app.views.manager = crate::view::ViewManager::new(&config).unwrap().into();
open(&mut app, &rx, &tx, path.clone());
(app, rx, tx, dir, path)
}
fn settle(
app: &mut App,
rx: &mpsc::Receiver<AppEvent>,
tx: &mpsc::Sender<AppEvent>,
event: AppEvent,
) {
if let Some(next) = app.event(event) {
tx.send(next).unwrap();
}
pump(app, rx, tx, |a| {
a.data_table_state.is_some() && !a.is_busy()
});
}
fn key(code: KeyCode) -> AppEvent {
AppEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn read_fails(app: &mut App, message: &str) {
let started = app.job_for_tests(Job::SampleRows, Some("Reading"));
let ticket = started.ticket();
started.end(Outcome::Failed {
message: message.to_string(),
panicked: false,
});
app.event(AppEvent::JobEnded(ticket));
}
fn ids(app: &App) -> Vec<i64> {
let state = app.data_table_state.as_ref().unwrap();
let df = state.lf().clone().collect().unwrap();
df.column("id")
.unwrap()
.i64()
.unwrap()
.into_no_null_iter()
.collect()
}
fn not_k2() -> FilterStatement {
FilterStatement {
columns: Vec::new(),
column: "key".to_string(),
operator: FilterOperator::NotEq,
value: "k2".to_string(),
logical_op: LogicalOperator::And,
}
}
#[test]
fn a_gone_file_offers_a_reopen_that_keeps_the_query_filters_sort_and_view() {
let (mut app, rx, tx, _dir, path) = opened_app(6, &["k0", "k1", "k2"]);
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::QQuery("select where id > 0".to_string())),
);
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::Filter(vec![not_k2()])),
);
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::Sort(vec!["val".to_string()], vec![true])),
);
assert_eq!(ids(&app), [4, 3, 1]);
let view = app
.create_view_from_current_state("place".to_string(), None, Default::default())
.unwrap();
app.views.active_id = Some(view.id.clone());
read_fails(&mut app, GONE);
assert!(app.confirmation_modal.active, "the reopen is offered");
assert_eq!(app.confirmation_modal.message, GONE);
assert_eq!(
(
app.confirmation_modal.yes_label,
app.confirmation_modal.no_label
),
("Reopen", "Close")
);
assert_eq!(app.error_message(), None);
write_csv(&path, 9, &["k0", "k1", "k2"]);
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert!(!app.confirmation_modal.active);
assert_eq!(app.error_message(), None);
assert_eq!(ids(&app), [7, 6, 4, 3, 1]);
let state = app.data_table_state.as_ref().unwrap();
assert_eq!(state.get_active_query(), "select where id > 0");
assert_eq!(state.get_filters().len(), 1);
assert_eq!(state.get_sort_columns(), ["val"]);
assert_eq!(app.views.active_id.as_deref(), Some(view.id.as_str()));
let stored = app.views.manager.get_view_by_id(&view.id).unwrap();
assert_eq!(stored.usage_count, 0, "a reopen is not a use");
}
#[test]
fn a_reopen_drills_into_the_same_group_again() {
let (mut app, rx, tx, _dir, path) = opened_app(6, &["k0", "k1", "k2"]);
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::QQuery("select n: count id by key".to_string())),
);
app.data_table_state
.as_mut()
.unwrap()
.table_state
.select(Some(1));
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
let key_of = |app: &App| {
app.data_table_state
.as_ref()
.unwrap()
.drilled_group_key()
.map(|(columns, values)| (columns.to_vec(), values.to_vec()))
};
let drilled = key_of(&app).expect("drilled into a group");
let group = drilled.1[0].clone();
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::Sort(vec!["id".to_string()], vec![true])),
);
let before = ids(&app);
assert_eq!(before.len(), 2);
read_fails(&mut app, GONE);
assert!(app.confirmation_modal.active);
write_csv(&path, 12, &["k9", "k0", "k1", "k2"]);
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert_eq!(app.error_message(), None);
assert_eq!(key_of(&app), Some(drilled.clone()), "the same group");
let state = app.data_table_state.as_ref().unwrap();
assert_eq!(
state.view_sort_columns(),
["id"],
"sorted inside it as before"
);
let after = ids(&app);
assert_eq!(after.len(), 3, "its rows as the files hold them now");
assert!(after.is_sorted_by(|a, b| a >= b), "{after:?}");
read_fails(&mut app, GONE);
write_csv(&path, 6, &["k9"]);
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
let state = app.data_table_state.as_ref().unwrap();
assert!(!state.is_drilled_down());
assert!(state.is_grouped());
assert_eq!(
app.flash_message(),
Some(format!("No rows with key = {group} in the current files").as_str())
);
}
#[test]
fn close_leaves_the_dataset_as_it_was() {
let (mut app, rx, tx, _dir, _path) = opened_app(6, &["k0", "k1"]);
settle(
&mut app,
&rx,
&tx,
AppEvent::Applied(Applied::QQuery("select where id > 2".to_string())),
);
read_fails(&mut app, GONE);
app.event(key(KeyCode::Right));
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert!(!app.confirmation_modal.active);
assert!(!app.modal_showing());
assert!(app.nothing_loading(), "nothing reopens");
assert_eq!(ids(&app), [3, 4, 5]);
}
#[test]
fn with_nothing_to_reopen_the_error_is_shown() {
let (tx, _rx) = mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let df = polars::prelude::df!("id" => [1i64, 2]).unwrap();
let options = OpenOptions::default();
let state =
crate::table::DataTableState::from_lazyframe(polars::prelude::IntoLazy::lazy(df), &options)
.unwrap();
app.install_for_tests(state, None, &options, None);
assert!(app.source.opened.is_none());
read_fails(&mut app, GONE);
assert!(!app.confirmation_modal.active);
assert_eq!(app.error_message(), Some(GONE));
}
#[test]
fn a_value_drill_follows_its_columns_type_or_is_said_gone() {
use polars::prelude::AnyValue;
let (mut app, rx, tx, _dir, path) = opened_app(6, &["k0", "k1", "k2"]);
let state = app.data_table_state.as_mut().unwrap();
state
.deferred(|s| s.drill_into_value("val", AnyValue::Int64(30)))
.unwrap();
app.spawn_async_collect(App::LOADING_BUFFER);
pump(&mut app, &rx, &tx, |a| !a.is_busy());
assert_eq!(ids(&app), [3]);
read_fails(&mut app, GONE);
std::fs::write(&path, "id,key,val\n3,k0,30\n4,k1,x\n7,k1,30\n").unwrap();
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert_eq!(app.error_message(), None);
assert!(app.data_table_state.as_ref().unwrap().is_drilled_down());
assert_eq!(ids(&app), [3, 7]);
let (mut app, rx, tx, _dir, path) = opened_app(6, &["k0", "k1", "k2"]);
let state = app.data_table_state.as_mut().unwrap();
state
.deferred(|s| s.drill_into_value("key", AnyValue::StringOwned("k1".into())))
.unwrap();
app.spawn_async_collect(App::LOADING_BUFFER);
pump(&mut app, &rx, &tx, |a| !a.is_busy());
assert_eq!(ids(&app), [1, 4]);
read_fails(&mut app, GONE);
std::fs::write(&path, "id,key,val\n0,1,0\n1,2,10\n").unwrap();
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert_eq!(app.error_message(), None);
assert!(!app.data_table_state.as_ref().unwrap().is_drilled_down());
assert_eq!(
app.flash_message(),
Some("No rows with key = k1 in the current files")
);
assert_eq!(ids(&app), [0, 1], "the view reads");
}
#[test]
fn reopen_closes_what_the_question_was_asked_over() {
let (mut app, rx, tx, _dir, _path) = opened_app(6, &["k0", "k1"]);
app.open_overlay(Overlay::PivotMelt);
read_fails(&mut app, GONE);
settle(&mut app, &rx, &tx, key(KeyCode::Enter));
assert_eq!(app.overlay, Overlay::None);
assert_eq!(app.error_message(), None);
assert_eq!(ids(&app), [0, 1, 2, 3, 4, 5]);
}
fn job_fails(app: &mut App, job: Job, message: &str) {
let started = app.job_for_tests(job, Some("Reading"));
let ticket = started.ticket();
started.end(Outcome::Failed {
message: message.to_string(),
panicked: false,
});
app.event(AppEvent::JobEnded(ticket));
}
fn close_leaves_nothing(
app: &mut App,
rx: &mpsc::Receiver<AppEvent>,
tx: &mpsc::Sender<AppEvent>,
name: &str,
) {
assert!(
app.confirmation_modal.active,
"{name}: the reopen is offered"
);
app.event(key(KeyCode::Right));
settle(app, rx, tx, key(KeyCode::Enter));
assert!(!app.confirmation_modal.active, "{name}");
assert_eq!(app.error_message(), None, "{name}: no error left behind");
assert_eq!(app.flash_message(), None, "{name}: nor a flash");
assert!(app.nothing_loading(), "{name}: nothing reopens");
}
#[test]
fn close_reveals_no_second_error() {
let jobs = [
Job::SampleRows,
Job::Analysis(Default::default()),
Job::Pivot,
Job::Copy,
Job::DrillRow,
Job::Export,
];
for job in jobs {
let name = format!("{:?}", job.kind());
let (mut app, rx, tx, _dir, _path) = opened_app(4, &["k0"]);
job_fails(&mut app, job, GONE);
close_leaves_nothing(&mut app, &rx, &tx, &name);
}
let (mut app, rx, tx, _dir, _path) = opened_app(4, &["k0"]);
let view = app
.create_view_from_current_state("v".to_string(), None, Default::default())
.unwrap();
let applying = crate::view::view_apply::Applying::Asked;
job_fails(&mut app, Job::ViewPivot(Box::new((view, applying))), GONE);
assert_eq!(
app.confirmation_modal.message,
format!("Error applying view: {GONE}")
);
close_leaves_nothing(&mut app, &rx, &tx, "view");
}
#[test]
fn a_job_not_reading_the_dataset_is_not_asked_about() {
let (mut app, _rx, _tx, _dir, _path) = opened_app(4, &["k0"]);
let job = Job::ReshapePreview { epoch: 0, token: 0 };
assert!(!job.reads_dataset());
job_fails(&mut app, job, GONE);
assert!(!app.confirmation_modal.active);
}