use std::path::Path;
mod shared;
pub use shared::{buffer_lines, buffer_text, ensure_sample_data, sample_data_dir, test_runtime};
use crate::cache::isolate_cache;
pub(crate) mod fixtures;
#[cfg(feature = "cloud")]
mod cloud_recent_facts {
use crate::formats::dataset_files::DatasetFile;
use crate::formats::schema_union::FileFooter;
use crate::home::discover::EntryKind;
use polars::prelude::{DataType, Field, Schema};
use std::sync::Arc;
fn file(key: &str, size: u64, stamp: u64) -> DatasetFile {
DatasetFile {
key: key.to_string(),
size,
stamp,
etag: None,
}
}
fn facts_of_open(
full: &str,
files: &[DatasetFile],
read: &[usize],
footers: &[Option<FileFooter>],
) -> Option<(std::path::PathBuf, crate::cache::DatasetFacts)> {
static RUNTIME: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
let runtime = RUNTIME.get_or_init(|| tokio::runtime::Runtime::new().unwrap());
let source = crate::formats::dataset_files::StoreFiles::new(
full,
String::new(),
None,
Arc::new(object_store::memory::InMemory::new()),
Default::default(),
runtime.handle(),
);
crate::formats::dataset_files::facts_of(Arc::new(source), files.to_vec(), read, footers)
}
fn footer(rows: &[usize]) -> Option<FileFooter> {
let schema = Schema::from_iter([
Field::new("id".into(), DataType::Int64),
Field::new("amount".into(), DataType::Float64),
]);
Some(FileFooter {
schema: Arc::new(schema),
row_group_rows: rows.to_vec(),
row_group_bytes: rows.iter().map(|r| r * 8).collect(),
file_bytes: 0,
column_bytes: Vec::new(),
})
}
#[test]
fn an_open_records_what_the_home_screen_will_show() {
let files = vec![
file("sales/year=2024/part-0.parquet", 1000, 10),
file("sales/year=2025/part-0.parquet", 2000, 20),
];
let read = vec![0, 1];
let footers = vec![footer(&[5, 7]), footer(&[8])];
let (path, facts) =
facts_of_open("s3://bucket/sales/", &files, &read, &footers).expect("facts");
assert_eq!(path, std::path::PathBuf::from("s3://bucket/sales/"));
assert_eq!(facts.rows, Some(20));
assert_eq!(facts.cols, Some(3), "the partition column counts");
assert!(!facts.cols_sampled);
assert_eq!(facts.kind, Some(EntryKind::Hive));
assert_eq!(facts.holds.formats, vec![("parquet".to_string(), 2)]);
assert_eq!(facts.size, 3000);
assert_eq!(facts.mtime, 20);
assert_eq!(
facts.classified_by,
crate::home::discover::CLASSIFIER_VERSION
);
assert!(facts.columns.iter().any(|c| c == "amount"));
let flat = vec![file("sales/a.parquet", 1, 1), file("sales/b.parquet", 1, 1)];
let (_, facts) = facts_of_open(
"s3://bucket/sales/",
&flat,
&[0, 1],
&[footer(&[1]), footer(&[1])],
)
.unwrap();
assert_eq!(facts.kind, Some(EntryKind::MultiFile));
let one = vec![file("sales/a.parquet", 1, 1)];
let (_, facts) =
facts_of_open("s3://bucket/sales/a.parquet", &one, &[0], &[footer(&[4])]).unwrap();
assert_eq!(facts.kind, Some(EntryKind::File));
assert!(facts.holds.is_empty());
assert_eq!(facts.rows, Some(4));
}
#[test]
fn a_prefix_without_its_slash_is_still_a_directory() {
let files = vec![file("sales/a.parquet", 1, 1), file("sales/b.parquet", 1, 1)];
let (_, facts) = facts_of_open(
"s3://bucket/sales",
&files,
&[0, 1],
&[footer(&[1]), footer(&[1])],
)
.unwrap();
assert_eq!(facts.kind, Some(EntryKind::MultiFile));
let one = vec![file("sales/a.parquet", 1, 1)];
let (_, facts) =
facts_of_open("s3://bucket/sales/a.parquet", &one, &[0], &[footer(&[1])]).unwrap();
assert_eq!(facts.kind, Some(EntryKind::File));
}
#[test]
fn a_sample_never_replaces_a_whole_record() {
let whole = crate::cache::DatasetFacts {
rows: Some(20),
..Default::default()
};
let sample = crate::cache::DatasetFacts {
rows: None,
cols_sampled: true,
..Default::default()
};
assert!(crate::formats::dataset_files::facts_worth_recording(
None, &sample
));
assert!(crate::formats::dataset_files::facts_worth_recording(
Some(&sample),
&sample
));
assert!(crate::formats::dataset_files::facts_worth_recording(
Some(&sample),
&whole
));
assert!(crate::formats::dataset_files::facts_worth_recording(
Some(&whole),
&whole
));
assert!(!crate::formats::dataset_files::facts_worth_recording(
Some(&whole),
&sample
));
}
#[test]
fn one_object_is_recorded_from_its_footer() {
let tmp = tempfile::tempdir().unwrap();
let cache = crate::cache::CacheManager::with_dir(tmp.path().to_path_buf());
let schema = Schema::from_iter([Field::new("id".into(), DataType::Int64)]);
let footer = crate::cloud::cloud_hive::FileFooter {
schema: Arc::new(schema),
row_group_rows: vec![3, 4],
row_group_bytes: Vec::new(),
file_bytes: 0,
column_bytes: Vec::new(),
};
crate::App::record_cloud_object_facts(Some(&cache), "s3://bucket/x.parquet", &footer);
let known = cache.load_dataset_facts();
let facts = known
.get(std::path::Path::new("s3://bucket/x.parquet"))
.expect("recorded");
assert_eq!(facts.rows, Some(7));
assert_eq!(facts.cols, Some(1));
assert_eq!(facts.kind, Some(EntryKind::File));
assert_eq!(facts.size, 0);
assert!(
facts.mtime > 0,
"dated, so the index does not evict it first"
);
}
#[test]
fn a_sampled_read_records_columns_but_no_row_count() {
let files: Vec<DatasetFile> = (0..5)
.map(|i| file(&format!("x/p{i}.parquet"), 10, 1))
.collect();
let (_, facts) = facts_of_open(
"s3://bucket/x/",
&files,
&[0, 4],
&[footer(&[1]), footer(&[1])],
)
.unwrap();
assert_eq!(facts.rows, None);
assert_eq!(facts.cols, Some(2));
assert!(facts.cols_sampled);
}
}
pub fn work_pending(app: &crate::App) -> bool {
app.is_busy() || app.row_count_pending() || app.rows_in_flight().is_some()
}
pub(crate) fn worker_dies_once(
dies: fn(&crate::Job) -> bool,
) -> Option<crate::app::jobs::WorkerDies> {
let mut died = false;
Some(Box::new(move |job| {
if died || !dies(job) {
return false;
}
died = true;
true
}))
}
pub(crate) fn worker_waits_once(
which: fn(&crate::Job) -> bool,
) -> (
Option<crate::app::jobs::WorkerWaits>,
std::sync::mpsc::Sender<()>,
) {
let (release, gate) = std::sync::mpsc::channel();
let mut gate = Some(gate);
let waits: crate::app::jobs::WorkerWaits =
Box::new(move |job| if which(job) { gate.take() } else { None });
(Some(waits), release)
}
#[cfg(feature = "cloud")]
#[test]
fn one_object_store_serves_the_footer_the_probe_and_the_scan() {
let cloud = crate::config::CloudConfig {
s3_endpoint_url: Some("http://127.0.0.1:1".to_string()),
s3_region: Some("us-east-1".to_string()),
s3_access_key_id: Some("testing".to_string()),
s3_secret_access_key: Some("testing".to_string()),
..Default::default()
};
let rt = test_runtime();
let path = std::path::Path::new("s3://bucket/obj.parquet");
let (url, _, store) = super::App::cloud_store_for(path, &cloud, &rt).expect("a store");
assert_eq!(url, "s3://bucket/obj.parquet");
let (_, _, again) = super::App::cloud_store_for(path, &cloud, &rt).expect("the same store");
assert!(
std::sync::Arc::ptr_eq(&store, &again),
"built once, served twice"
);
let (_, _, probe) =
super::App::cloud_store_for(std::path::Path::new("s3://bucket/"), &cloud, &rt)
.expect("the bucket's store");
assert!(
std::sync::Arc::ptr_eq(&store, &probe),
"the probe shares it"
);
}
#[cfg(feature = "cloud")]
#[test]
fn a_runtime_shut_down_under_a_waiting_thread_does_not_panic_it() {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("tokio runtime");
let handle = rt.handle().clone();
let (started_tx, started_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
super::wait_on_runtime(&handle, async move {
let _ = started_tx.send(());
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
})
});
started_rx.recv().expect("future started");
rt.shutdown_background();
let outcome = waiter.join().expect("the waiting thread must not panic");
assert!(outcome.is_none());
}
#[test]
fn end_pressed_while_the_footers_are_coming_jumps_when_they_land() {
use crate::table::{DataTableState, FootersFound, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![100],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let mut state = DataTableState::from_schema_and_lazyframe(
dataset_of(rows()).schema.clone(),
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: None,
dataset: dataset_of(rows()),
lf: rows(),
file_rows: vec![100],
files: vec!["one".to_string()],
row_groups: vec![vec![100]],
remote: Some(crate::table::RemoteRead {
urls: vec!["one".to_string()],
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
}),
})
})),
..Default::default()
});
state.visible_rows = 10;
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(
app.counting.len_count_inflight.is_none(),
"no second pass over the footers already being read"
);
let reported = loop {
let event = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the pass reports back");
if matches!(event, AppEvent::JobEnded(ticket) if ticket.kind() == crate::JobKind::FootersJoin)
{
break event;
}
};
let _ = app.handle(reported);
while let Ok(event) = rx.try_recv() {
let _ = app.handle(event);
}
let state = app.data_table_state.as_ref().unwrap();
assert_eq!(state.num_rows_if_valid(), Some(100), "counted by the pass");
assert!(
state.start_row() > 0,
"and the end is where the view went, rather than the key being swallowed"
);
assert_ne!(
app.status_message.as_deref(),
Some("Counting rows to find the end..."),
"with nothing left saying it is counting rows that have been counted"
);
assert!(
app.counting.end_when_the_footers_land.is_none(),
"and the key is spent, not left waiting on the next dataset"
);
}
#[test]
fn a_sampled_dataset_shows_an_estimate_until_it_is_counted() {
use crate::formats::schema_union::RowEstimate;
use crate::render::footer::Total;
use crate::render::main_view::MainViewContent;
use crate::table::{DataTableState, FootersFound, RemoteFiles, RemoteRead};
use crate::{App, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::{Arc, Mutex, mpsc};
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![40],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(3, &[0], &[Some(footer)])
};
let urls = || vec!["a".to_string(), "b".to_string(), "c".to_string()];
let (go, gate) = mpsc::channel::<()>();
let gate = Arc::new(Mutex::new(gate));
let counter: crate::table::FileCounter = Arc::new(move |progress| {
let pass = progress.pass(3);
pass.advance();
let _ = gate.lock().unwrap().recv();
if progress.is_cancelled() {
return Err("cancelled".to_string());
}
pass.advance();
pass.advance();
Ok(vec![vec![40], vec![30], vec![30]])
});
let joined = counter.clone();
let mut state = DataTableState::from_schema_and_lazyframe(
dataset_of(rows()).schema.clone(),
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(urls()),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: counter,
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: Some(RowEstimate {
rows: 120,
sampled: 2,
files: 3,
}),
dataset: dataset_of(rows()),
lf: rows(),
file_rows: Vec::new(),
files: urls(),
row_groups: Vec::new(),
remote: Some(RemoteRead {
urls: urls(),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: joined.clone(),
}),
})
})),
..Default::default()
});
state.visible_rows = 10;
let (tx, rx) = mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.app_config.read.exact_count_files = 2;
app.install_for_tests(state, None, &OpenOptions::default(), None);
let pump = |app: &mut App, done: &dyn Fn(&App) -> bool| {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !done(app) {
assert!(std::time::Instant::now() < deadline, "never got there");
if let Ok(event) = rx.recv_timeout(std::time::Duration::from_millis(50)) {
let mut next = app.event(event);
while let Some(event) = next {
next = app.event(event);
}
}
}
};
let total = |app: &App| {
app.footer(MainViewContent::Datatable, false)
.position
.map(|p| p.total)
};
pump(&mut app, &|app| {
app.data_table_state
.as_ref()
.is_some_and(|s| s.footers_pending().is_none())
});
assert_eq!(total(&app), Some(Total::Estimated(120)));
assert!(
app.counting.len_count_inflight.is_none(),
"too many files to count unasked"
);
let key = |app: &mut App, code: KeyCode| {
let _ = app.key(&KeyEvent::new(code, KeyModifiers::NONE));
};
key(&mut app, KeyCode::Char('i'));
key(&mut app, KeyCode::Char('c'));
key(&mut app, KeyCode::Esc);
pump(&mut app, &|app| app.footers_counted().is_some());
let line = app
.footer_progress_line(MainViewContent::Datatable)
.expect("a progress line");
assert!(line.stoppable);
assert_eq!(line.counts[0].noun, "files");
assert_eq!((line.counts[0].done, line.counts[0].total), (1, Some(3)));
key(&mut app, KeyCode::Esc);
go.send(()).unwrap();
pump(&mut app, &|app| app.counting.len_count_inflight.is_none());
assert_eq!(
total(&app),
Some(Total::Estimated(120)),
"stopped at the estimate"
);
key(&mut app, KeyCode::Char('i'));
key(&mut app, KeyCode::Char('c'));
key(&mut app, KeyCode::Esc);
go.send(()).unwrap();
pump(&mut app, &|app| {
app.data_table_state
.as_ref()
.is_some_and(|s| s.is_num_rows_valid())
});
assert_eq!(total(&app), Some(Total::Known(100)));
assert!(app.row_estimate().is_none());
}
#[test]
fn row_numbers_over_a_sort_fall_back_to_the_view_in_a_store() {
use crate::table::{DataTableState, OpenFacts};
use polars::prelude::*;
let frame = || df!("v" => [3i64, 1, 2]).unwrap().lazy();
let open = |remote: bool| {
let mut state = DataTableState::from_lazyframe(frame(), &crate::OpenOptions::default())
.unwrap()
.with_open(OpenFacts {
remote_source: remote,
..Default::default()
});
state.sort_by(vec!["v".to_string()], vec![false]);
state
};
let mut local = open(false);
assert!(local.toggle_row_numbers(), "numbered over the sort");
assert!(local.carries_source_rows());
assert!(!local.row_numbers_count_the_view());
let mut remote = open(true);
assert!(
!remote.toggle_row_numbers(),
"no row index under a store's filters"
);
assert!(remote.row_numbers());
assert!(!remote.carries_source_rows());
assert!(remote.row_numbers_count_the_view());
assert_eq!(
remote.row_numbers_from(0, 3),
[1, 2, 3],
"the view's places"
);
}
#[test]
fn a_key_that_acts_while_busy_reads_nothing() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || {
df!(
"a" => (0..100i64).collect::<Vec<_>>(),
"b" => (0..100i64).collect::<Vec<_>>(),
"c" => (0..100i64).collect::<Vec<_>>(),
"d" => (0..100i64).collect::<Vec<_>>(),
)
.unwrap()
.lazy()
};
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut candidates: Vec<KeyCode> = (b'a'..=b'z')
.chain(b'A'..=b'Z')
.map(|c| KeyCode::Char(c as char))
.collect();
candidates.extend((1..=12).map(KeyCode::F));
candidates.extend("[]{}#,<>=".chars().map(KeyCode::Char));
candidates.extend([
KeyCode::Left,
KeyCode::Right,
KeyCode::Up,
KeyCode::Down,
KeyCode::Home,
KeyCode::End,
KeyCode::PageUp,
KeyCode::PageDown,
KeyCode::Enter,
KeyCode::Esc,
KeyCode::Tab,
KeyCode::Backspace,
]);
let staged = || {
let mut state = DataTableState::from_schema_and_lazyframe(
schema.clone(),
rows(),
&OpenOptions::default(),
None,
)
.unwrap();
state.visible_rows = 10;
state.visible_termcols = 1;
assert!(state.count_landed(state.len_generation(), 100, None));
state.collect();
state.scroll_right();
state.invalidate_num_rows();
state
};
let keys = candidates
.into_iter()
.map(|code| KeyEvent::new(code, KeyModifiers::NONE))
.chain(
[KeyCode::Left, KeyCode::Right].map(|code| KeyEvent::new(code, KeyModifiers::SHIFT)),
);
let mut admitted = 0;
for key in keys {
let code = key.code;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(staged(), None, &OpenOptions::default(), None);
if !app.key_acts_while_busy(&key) {
continue;
}
admitted += 1;
assert_eq!(
app.data_table_state
.as_ref()
.and_then(|s| s.num_rows_if_valid()),
None,
"{code:?}: the staged open should start without a count"
);
if let Some(next) = app.key(&key) {
let _ = app.handle(next);
}
let area = ratatui::layout::Rect::new(0, 0, 12, 12);
ratatui::widgets::Widget::render(&mut app, area, &mut ratatui::buffer::Buffer::empty(area));
assert_eq!(
app.data_table_state
.as_ref()
.and_then(|s| s.num_rows_if_valid()),
None,
"{code:?} acts while busy, so it must not count the rows"
);
}
assert!(
admitted >= 12,
"the classifier should admit the view keys; it admitted {admitted}"
);
}
#[test]
fn end_pressed_at_one_dataset_does_not_move_the_next() {
use crate::table::{DataTableState, FootersFound, RemoteFiles};
use crate::{App, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![100],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let staged = move || {
let mut state = DataTableState::from_schema_and_lazyframe(
dataset_of(rows()).schema.clone(),
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: None,
dataset: dataset_of(rows()),
lf: rows(),
file_rows: vec![100],
files: vec!["one".to_string()],
row_groups: vec![vec![100]],
remote: Some(crate::table::RemoteRead {
urls: vec!["one".to_string()],
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
}),
})
})),
..Default::default()
});
state.visible_rows = 10;
state
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(staged(), None, &OpenOptions::default(), None);
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(
app.counting.end_when_the_footers_land.is_some(),
"the key is waiting on this dataset's footers"
);
app.install_for_tests(staged(), None, &OpenOptions::default(), None);
assert!(
app.counting.end_when_the_footers_land.is_none(),
"and does not take the key with them"
);
assert_eq!(
app.data_table_state.as_ref().unwrap().start_row(),
0,
"the directory they opened is where they left it, at the top"
);
}
#[test]
fn a_new_dataset_keeps_nothing_the_last_one_left() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use polars::prelude::*;
let frame = || {
DataTableState::from_lazyframe(
df!("id" => [1i64, 2, 3]).unwrap().lazy(),
&OpenOptions::default(),
)
.unwrap()
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(
frame(),
Some(std::path::PathBuf::from("first.csv")),
&OpenOptions::default(),
None,
);
assert!(app.source.original_file_format.is_some());
assert!(app.source.original_file_delimiter.is_some());
app.source.opened_from_home = true;
app.analysis_modal.quality.findings.column = Some("id".to_string());
app.analysis_modal.quality.evidence_read =
Some(crate::analysis::analysis_modal::EvidenceRead {
rows: crate::analysis::quality_report::EvidenceRows::Duplicates,
label: "Data Quality / Duplicate rows".to_string(),
sample: None,
scope: crate::analysis::data_quality::QualityScope::CurrentView,
summary: Vec::new(),
});
app.sample.paths.push((
"first.csv".to_string(),
crate::analysis::table_sample::DrawPath::Reservoir,
));
app.views.active_id = Some("a view".to_string());
app.install_for_tests(frame(), None, &OpenOptions::default(), None);
assert_eq!(
app.analysis_modal.quality.findings,
crate::analysis::quality_report::FindingsView::default()
);
assert!(app.analysis_modal.quality.evidence_read.is_none());
assert!(app.prompt.query_running.is_none());
assert!(app.sample.paths.is_empty());
assert!(app.views.active_id.is_none());
assert!(app.info.file_facts.is_none());
assert!(app.info.codebook.is_none());
assert!(app.info.catalog_entry.is_none());
assert!(
app.source.opened.is_none(),
"a frame handed over has no path"
);
assert!(app.source.original_file_format.is_none());
assert!(app.source.original_file_delimiter.is_none());
assert!(
app.source.opened_from_home,
"q still goes home: a later open does not take home out of the stack"
);
}
#[test]
fn a_staged_open_does_not_leave_a_count_running_that_never_ran() {
use crate::table::{DataTableState, FootersFound, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let narrow = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let wide = || {
df!("id" => (0..100i64).collect::<Vec<_>>(), "oops" => vec!["a"; 100])
.unwrap()
.lazy()
};
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![100],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let mut state = DataTableState::from_schema_and_lazyframe(
dataset_of(narrow()).schema.clone(),
narrow(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(narrow())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: None,
dataset: dataset_of(wide()),
lf: wide(),
file_rows: vec![100],
files: vec!["one".to_string()],
row_groups: vec![vec![100]],
remote: Some(crate::table::RemoteRead {
urls: vec!["one".to_string()],
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(wide())),
count: Arc::new(|_| Ok(vec![vec![100]])),
}),
})
})),
..Default::default()
});
state.visible_rows = 10;
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
app.spawn_async_collect(App::LOADING_BUFFER);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
loop {
let event = rx
.recv_timeout(deadline.saturating_duration_since(std::time::Instant::now()))
.expect("the pass reports back");
let is_the_pass = matches!(event, AppEvent::JobEnded(ticket) if ticket.kind() == crate::JobKind::FootersJoin);
let _ = app.handle(event);
if is_the_pass {
break;
}
}
assert_eq!(
app.data_table_state.as_ref().unwrap().num_rows_if_valid(),
Some(100),
"the pass brought the count with it"
);
assert!(
app.counting.len_count_inflight.is_none(),
"so nothing is still counting, and the row count is a number rather than a \
spinner for the rest of the session"
);
}
#[test]
fn a_head_that_cannot_be_read_names_the_file_opened() {
use crate::OpenOptions;
use std::io::Write;
let source = tempfile::tempdir().unwrap();
let scratch = tempfile::tempdir().unwrap();
let plain = source.path().join("rows.csv");
std::fs::write(&plain, "id\n1\n").unwrap();
let gz = source.path().join("rows.csv.gz");
let mut encoder = flate2::write::GzEncoder::new(
std::fs::File::create(&gz).unwrap(),
flate2::Compression::default(),
);
encoder.write_all(b"id\n1\n").unwrap();
encoder.finish().unwrap();
for in_memory in [false, true] {
let options = OpenOptions {
header_rows: vec![5],
temp_dir: Some(scratch.path().to_path_buf()),
decompress_in_memory: in_memory,
..OpenOptions::default()
};
for file in [&plain, &gz] {
let Err(e) = crate::formats::readers::csv::read_delimited(
file,
b',',
&options,
&Default::default(),
) else {
panic!("{} has no line 5", file.display());
};
let said = crate::error_display::user_message_from_report(&e, None);
assert!(said.contains("rows.csv"), "{said}");
assert!(
!said.contains(&scratch.path().display().to_string()),
"{said}"
);
}
}
}
#[test]
fn a_failed_read_names_the_file_opened_not_its_temp_copy() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
let source = tempfile::tempdir().unwrap();
let scratch = tempfile::tempdir().unwrap();
let gz = source.path().join("rows.csv.gz");
let mut encoder = flate2::write::GzEncoder::new(
std::fs::File::create(&gz).unwrap(),
flate2::Compression::default(),
);
encoder.write_all(b"id\n1\n2\n").unwrap();
encoder.finish().unwrap();
let options = OpenOptions {
temp_dir: Some(scratch.path().to_path_buf()),
..OpenOptions::default()
};
let read =
crate::formats::readers::csv::read_delimited(&gz, b',', &options, &Default::default())
.unwrap();
let state = DataTableState::from_read(read, &options).unwrap();
let copy = state.temp_files()[0].to_path_buf();
assert!(copy.starts_with(scratch.path()));
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, Some(PathBuf::from(&gz)), &options, None);
let reason = format!("bad row\nIt stopped at {}.", copy.display());
app.rows_failed(true, true, &reason, None);
assert_eq!(
app.error_message(),
Some(format!("bad row\nIt stopped at {}.", gz.display()).as_str())
);
}
#[test]
fn a_pass_that_cannot_read_the_footers_stops_the_dataset_waiting_for_it() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64]).unwrap().lazy();
let mut lf = frame();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state =
DataTableState::from_schema_and_lazyframe(schema, frame(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(|_progress| None)),
..Default::default()
});
app.install_for_tests(state, None, &OpenOptions::default(), None);
let reported = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("a pass that failed still says so");
assert!(
matches!(reported, AppEvent::JobEnded(ticket) if ticket.kind() == crate::JobKind::FootersJoin),
"and says it the same way a pass that succeeded does"
);
let _ = app.handle(reported);
assert!(
app.data_table_state
.as_ref()
.unwrap()
.footers_pending()
.is_none(),
"the dataset is not left waiting for footers that are not coming"
);
}
#[test]
fn a_count_not_yet_taken_is_not_printed_as_the_total() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use polars::prelude::*;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
use std::sync::Arc;
let rows = || df!("id" => (0..70i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(|_| None)),
..Default::default()
});
state.set_provisional_rows(70);
assert!(
state.counts_itself_later(),
"the fixture is a dataset whose count is still coming"
);
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
app.busy = false;
let area = Rect::new(0, 0, 100, 24);
let mut buf = Buffer::empty(area);
(&mut app).render(area, &mut buf);
let bar: String = (0..area.width)
.map(|x| buf[(x, area.height - 1)].symbol().to_string())
.collect();
assert!(
!bar.contains("70 rows"),
"a partial is not a total: {bar:?}"
);
assert!(app.something_is_spinning());
}
#[test]
fn a_pass_that_brings_no_count_still_leaves_rows_on_screen() {
use crate::table::{DataTableState, FootersFound, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let wider = || {
df!("id" => (0..100i64).collect::<Vec<_>>(), "oops" => vec!["a"; 100])
.unwrap()
.lazy()
};
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![100],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let mut state = DataTableState::from_schema_and_lazyframe(
dataset_of(rows()).schema.clone(),
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: None,
dataset: dataset_of(wider()),
lf: wider(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
})
})),
..Default::default()
});
state.visible_rows = 10;
let (tx, rx) = std::sync::mpsc::channel();
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let mut app = App::new(tx, runtime.handle().clone());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
let reported = loop {
let event = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the pass reports back");
if matches!(event, AppEvent::JobEnded(ticket) if ticket.kind() == crate::JobKind::FootersJoin)
{
break event;
}
};
let _ = app.handle(reported);
assert!(
app.rows_in_flight().is_some(),
"the join dropped the buffer, so something has to read it back"
);
}
#[test]
fn end_on_a_sorted_dataset_still_reading_its_footers_waits_for_the_pass() {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(|_| None)),
..Default::default()
});
state.visible_rows = 10;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
state.deferred(|s| s.sort(vec!["id".to_string()], false));
assert!(
state.scan_is_the_root(),
"a sort is rebuilt over whatever the root becomes, so the join lands under it"
);
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(
app.counting.end_when_the_footers_land,
Some(app.dataset_generation),
"so End waits for the pass rather than starting a count the join will orphan"
);
assert!(
app.counting.end_after_count.is_none(),
"and nothing is left waiting on a count that will never be matched"
);
}
#[test]
fn a_count_the_join_orphaned_does_not_strand_end_or_speak_for_a_later_one() {
use crate::table::{DataTableState, FootersFound, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let wide = || {
df!("id" => (0..100i64).collect::<Vec<_>>(), "extra" => vec!["a"; 100])
.unwrap()
.lazy()
};
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![100],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(move |_| {
Some(FootersFound {
estimate: None,
dataset: dataset_of(wide()),
lf: wide(),
file_rows: vec![100],
files: vec!["one".to_string()],
row_groups: vec![vec![100]],
remote: Some(crate::table::RemoteRead {
urls: vec!["one".to_string()],
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(wide())),
count: Arc::new(|_| Ok(vec![vec![100]])),
}),
})
})),
..Default::default()
});
state.visible_rows = 10;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
state.deferred(|s| s.query("select doubled: id * 2".to_string()));
let orphaned = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(
app.counting.end_after_count,
Some(orphaned),
"the jump is waiting on the query's own count"
);
let live = app.dataset_generation;
let found = app
.data_table_state
.as_ref()
.and_then(|state| state.footers_pending())
.and_then(|pass| pass(&app.counting.footer_progress));
let _ = app.footers_joined(live, found);
assert!(
app.counting.footers_held.is_some(),
"held rather than joined, because a query is the root"
);
let state = app.data_table_state.as_mut().unwrap();
state.deferred(|s| s.query(String::new()));
let _ = app.handle(AppEvent::Update);
let joined = app.data_table_state.as_ref().unwrap().len_generation();
assert_ne!(
joined, orphaned,
"the join took a fresh generation past the count that was already running"
);
let before = app.data_table_state.as_ref().unwrap().start_row();
let next = app.event(AppEvent::BackgroundLenReady {
len_generation: orphaned,
num_rows: 100,
file_row_groups: None,
});
assert_eq!(
app.counting.end_after_count, None,
"the jump is not left waiting on a generation nothing will ever match"
);
assert_ne!(
app.status_message.as_deref(),
Some(App::COUNTING_FOR_END),
"and the line does not go on saying it is counting for an end nobody awaits"
);
let mut follow = next;
while let Some(event) = follow {
follow = app.event(event);
}
assert_eq!(
app.data_table_state.as_ref().unwrap().start_row(),
before,
"and the view stays where it is rather than moving on a stale answer"
);
}
#[test]
fn a_count_that_failed_for_another_frame_does_not_answer_for_this_end() {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
..Default::default()
});
state.visible_rows = 10;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let live = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(
app.counting.end_after_count,
Some(live),
"the jump is waiting on this frame's count"
);
app.status_message = None;
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: live.wrapping_sub(1),
});
assert_eq!(
app.counting.end_after_count,
Some(live),
"the End is still waiting on its own count, which has not failed"
);
assert_eq!(
app.status_message, None,
"and nothing is said about a count the user is not waiting on"
);
}
fn uncounted_remote_app() -> (crate::App, std::sync::mpsc::Receiver<crate::AppEvent>) {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = frame();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, frame(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(frame())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
..Default::default()
});
state.visible_rows = 10;
state.set_provisional_rows(40);
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
(app, rx)
}
fn footer_text(app: &mut crate::App) -> String {
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
let area = Rect::new(0, 0, 120, 24);
let mut buf = Buffer::empty(area);
app.render(area, &mut buf);
(0..area.width)
.map(|x| buf[(x, area.height - 1)].symbol().to_string())
.collect()
}
#[test]
fn the_bar_says_question_mark_when_the_count_failed() {
use crate::AppEvent;
let (mut app, _rx) = uncounted_remote_app();
let live = app.data_table_state.as_ref().unwrap().len_generation();
assert!(
!footer_text(&mut app).contains("/ ?"),
"nothing has failed yet"
);
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: live,
});
let bar = footer_text(&mut app);
assert!(
bar.contains("/ ?"),
"the count failed, so the total is unknown: {bar:?}"
);
}
#[test]
fn a_dead_frames_failed_count_leaves_this_frames_question_mark_alone() {
use crate::AppEvent;
let (mut app, _rx) = uncounted_remote_app();
let live = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: live,
});
assert!(
footer_text(&mut app).contains("/ ?"),
"this frame's count failed"
);
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: live.wrapping_sub(1),
});
let bar = footer_text(&mut app);
assert!(
bar.contains("/ ?"),
"a stranger's failure says nothing about this frame: {bar:?}"
);
}
#[cfg(test)]
fn a_look_is_out(
app: &mut crate::App,
path: &std::path::Path,
jump: bool,
) -> crate::app::jobs::Started {
let look = crate::Job::Classify(crate::app::jobs::Classify {
path: path.to_path_buf(),
browsing: app.home.browsing.clone(),
jump,
});
app.job_for_tests(look, Some(crate::App::LOOKING))
}
#[cfg(test)]
fn a_look_waits(app: &crate::App) -> Option<std::path::PathBuf> {
match app
.jobs
.current(|job| matches!(job, crate::Job::Classify(_)))
{
Some((_, crate::Job::Classify(look))) => Some(look.path.clone()),
_ => None,
}
}
#[cfg(test)]
fn the_look_answers(
app: &mut crate::App,
look: crate::app::jobs::Started,
found: Option<crate::home::discover::EntryKind>,
) -> Option<crate::AppEvent> {
let ticket = look.ticket();
look.end(crate::Outcome::answered(crate::Answer::Kind(found)));
app.event(crate::AppEvent::JobEnded(ticket))
}
#[test]
fn a_classify_answer_nobody_is_waiting_for_leaves_the_right_busy_behind() {
use crate::{App, InputMode};
type MovedOn = fn(&mut App);
let cases: Vec<(&str, MovedOn, bool)> = vec![
(
"they went back to the data",
|app: &mut App| app.input_mode = InputMode::Normal,
false,
),
(
"the browse moved under it",
|app: &mut App| app.home.browsing = Some(std::path::PathBuf::from("/elsewhere")),
false,
),
(
"an open took the generation",
|app: &mut App| {
app.jobs.advance();
app.busy = true;
},
true,
),
];
for (what, moved_on, busy_after) in cases {
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.enter_home();
let path = std::path::PathBuf::from("/mnt/share/orders");
let look = a_look_is_out(&mut app, &path, false);
moved_on(&mut app);
let moved_to = app.home.browsing.clone();
let follow = the_look_answers(
&mut app,
look,
Some(crate::home::discover::EntryKind::MultiFile),
);
assert!(follow.is_none(), "nothing was opened when {what}");
assert_eq!(
app.home.browsing, moved_to,
"and it did not browse into the answer's path when {what}"
);
assert_eq!(
app.is_busy(),
busy_after,
"busy after {what}: an answer puts down the busy it was holding, and \
only that one"
);
assert!(
a_look_waits(&app).is_none(),
"and the look is no longer outstanding when {what}"
);
}
}
#[test]
fn a_newer_look_replaces_an_older_one() {
use crate::{App, AppEvent};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.enter_home();
let first = std::path::PathBuf::from("/mnt/share/aaa");
let look = a_look_is_out(&mut app, &first, false);
let second = std::path::PathBuf::from("/mnt/share/bbb");
let _ = app.event(AppEvent::ClassifyThenOpen {
path: second.clone(),
jump: false,
});
assert_eq!(
a_look_waits(&app),
Some(second.clone()),
"the newer look is the one being waited on"
);
let follow = the_look_answers(
&mut app,
look,
Some(crate::home::discover::EntryKind::MultiFile),
);
assert!(follow.is_none(), "the stale answer opened nothing");
assert_eq!(
a_look_waits(&app),
Some(second),
"and did not cancel the look that replaced it"
);
assert!(app.is_busy(), "nor put down its busy state");
}
#[test]
fn going_home_does_not_wait_on_a_look_that_may_never_answer() {
use crate::App;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.enter_home();
let _look = a_look_is_out(&mut app, std::path::Path::new("/mnt/gone/orders"), false);
app.home.status = Some("Looking at orders...".to_string());
app.enter_home();
assert!(
!app.is_busy(),
"the keyboard is not waiting on a dead share"
);
assert!(a_look_waits(&app).is_none(), "and the look is put down");
assert_eq!(app.home.status, None, "with its line");
}
#[test]
fn a_typed_path_that_is_not_there_comes_back_to_the_prompt() {
use crate::App;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.enter_home();
let path = std::path::PathBuf::from("/mnt/share/nope");
let look = a_look_is_out(&mut app, &path, true);
let follow = the_look_answers(&mut app, look, None);
assert!(follow.is_none());
assert!(
app.home
.status
.as_deref()
.is_some_and(|s| s.contains("No such path")),
"it says so: {:?}",
app.home.status
);
assert!(app.home.path_input_active, "and the prompt is back");
assert_eq!(
app.home.path_input,
path.display().to_string(),
"with the path still in it"
);
}
#[test]
fn a_parked_end_does_not_put_its_message_on_the_home_screen() {
use crate::AppEvent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let (mut app, _rx) = uncounted_remote_app();
let waiting = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(footer_text(&mut app).contains("Counting rows"), "parked");
app.enter_home();
let bar = footer_text(&mut app);
assert!(
!bar.contains("Counting rows"),
"the home bar is the home screen's: {bar:?}"
);
assert!(
bar.contains("keys"),
"and it still has its keys rather than a sentence: {bar:?}"
);
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: waiting,
});
let bar = footer_text(&mut app);
assert!(
!bar.contains("Could not count the rows"),
"an error about a dataset they have left is not the home screen's news: \
{bar:?}"
);
}
#[test]
fn a_parked_end_does_not_put_its_message_on_the_next_dataset() {
use crate::{OpenOptions, table::DataTableState};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let (mut app, _rx) = uncounted_remote_app();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(footer_text(&mut app).contains("Counting rows"), "parked");
app.enter_home();
let rows = || df!("id" => &[1i64, 2, 3]).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let next =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap();
app.install_for_tests(next, None, &OpenOptions::default(), None);
app.busy = false;
let bar = footer_text(&mut app);
assert!(
!bar.contains("Counting rows"),
"the new dataset's bar is not the old one's: {bar:?}"
);
}
#[test]
fn a_parked_end_does_not_put_its_message_on_the_chart_view() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let (mut app, _rx) = uncounted_remote_app();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(footer_text(&mut app).contains("Counting rows"), "parked");
app.overlay = crate::Overlay::Chart;
let bar = footer_text(&mut app);
assert!(
app.status_message.is_some(),
"the End is still waiting, and the field still says so"
);
assert!(
!bar.contains("Counting rows"),
"but the chart's bar is the chart's: {bar:?}"
);
}
#[test]
fn a_failed_count_for_a_frame_that_is_gone_retires_its_end_quietly() {
use crate::AppEvent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let (mut app, _rx) = uncounted_remote_app();
let waiting = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(app.counting.end_after_count, Some(waiting));
assert_eq!(
app.status_message.as_deref(),
Some(crate::App::COUNTING_FOR_END),
"the status says the count is running"
);
let state = app.data_table_state.as_mut().unwrap();
state.deferred(|s| s.query("select doubled: id * 2".to_string()));
assert_ne!(
app.data_table_state.as_ref().unwrap().len_generation(),
waiting,
"the frame the count belongs to is gone"
);
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: waiting,
});
assert_eq!(
app.counting.end_after_count, None,
"the End it belonged to is retired"
);
let bar = footer_text(&mut app);
assert!(
!bar.contains("Counting rows"),
"the status it put up comes down: {bar:?}"
);
assert!(
!bar.contains("Could not count the rows"),
"and does not become an error about a frame the user is no longer looking \
at: {bar:?}"
);
}
#[test]
fn the_bar_says_it_is_counting_for_an_end_and_says_when_that_failed() {
use crate::AppEvent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let (mut app, _rx) = uncounted_remote_app();
let waiting = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert!(
!app.is_busy(),
"the jump parked rather than blocking the keyboard"
);
let bar = footer_text(&mut app);
assert!(
bar.contains("Counting rows"),
"and the line says why the view has not moved: {bar:?}"
);
let _ = app.handle(AppEvent::BackgroundLenFailed {
len_generation: waiting,
});
let bar = footer_text(&mut app);
assert!(
bar.contains("Could not count the rows"),
"and says so when the count it was waiting on fails: {bar:?}"
);
}
#[test]
fn an_end_pressed_on_the_dataset_they_left_does_not_move_the_next_one() {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let remote_state = |n: i64| {
let rows = move || df!("id" => (0..n).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state = DataTableState::from_schema_and_lazyframe(
schema,
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(move |_| Ok(vec![vec![n as usize]])),
offsets: None,
}),
..Default::default()
});
state.visible_rows = 10;
state
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(remote_state(100), None, &OpenOptions::default(), None);
let theirs = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(
app.counting.end_after_count,
Some(theirs),
"the jump is waiting on that directory's count"
);
app.install_for_tests(remote_state(500), None, &OpenOptions::default(), None);
assert_eq!(
app.counting.end_after_count, None,
"the key they pressed in the directory they left does not come with them"
);
let mut follow = app.event(AppEvent::BackgroundLenReady {
len_generation: theirs,
num_rows: 100,
file_row_groups: None,
});
while let Some(event) = follow {
follow = app.event(event);
}
let next = app.data_table_state.as_ref().unwrap().len_generation();
assert_ne!(
app.counting.end_after_count,
Some(next),
"and the directory on screen has not inherited it"
);
let mut follow = app.event(AppEvent::BackgroundLenReady {
len_generation: next,
num_rows: 500,
file_row_groups: None,
});
while let Some(event) = follow {
follow = app.event(event);
}
assert_eq!(
app.data_table_state.as_ref().unwrap().start_row(),
0,
"the directory they are looking at stays where they left it, at the top"
);
}
fn recv(rx: &std::sync::mpsc::Receiver<crate::AppEvent>) -> crate::AppEvent {
rx.recv_timeout(std::time::Duration::from_secs(30))
.expect("the job reports back")
}
#[test]
fn a_job_holds_the_generation_until_its_answer_is_handled() {
use crate::{App, AppEvent, Job, JobKind};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
assert!(!app.work_a_bump_would_strand(), "nothing is running yet");
let (go, wait) = std::sync::mpsc::channel::<()>();
let ticket = app.spawn_job(Job::QualityReport, Some("Working..."), move |_| {
wait.recv().ok();
Ok(crate::Answer::QualityReportWritten(
std::path::PathBuf::from("report.json"),
))
});
assert_eq!(ticket.kind(), JobKind::QualityReport);
assert!(
app.work_a_bump_would_strand(),
"the job holds the generation"
);
assert!(app.is_busy());
go.send(()).unwrap();
let ended = recv(&rx);
assert!(matches!(ended, AppEvent::JobEnded(t) if t == ticket));
assert!(
app.work_a_bump_would_strand(),
"an answer not yet handled still holds it"
);
let _ = app.handle(ended);
assert!(
!app.work_a_bump_would_strand(),
"and handling it lets go, with nothing left to arrive"
);
assert!(!app.is_busy());
assert!(
rx.recv_timeout(std::time::Duration::from_millis(50))
.is_err(),
"no second event releases anything"
);
}
#[test]
fn an_answer_under_another_job_is_dropped() {
use crate::app::jobs::{Answer, Job, Outcome};
use crate::{App, AppEvent};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let copy = app.job_for_tests(Job::Copy, Some("Copying..."));
let ticket = copy.ticket();
copy.end(Outcome::answered(Answer::Exported(
std::path::PathBuf::from("out.csv"),
)));
assert!(app.event(AppEvent::JobEnded(ticket)).is_none());
assert_eq!(app.flash_message(), None, "no export is reported");
assert!(!app.is_busy());
assert_eq!(app.status_message, None);
}
#[test]
fn a_job_puts_down_the_keys_and_the_line_it_held() {
use crate::{Answer, App, AppEvent, Job, OpenOptions, Outcome};
use polars::prelude::IntoLazy;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let end = |app: &mut App, job: crate::app::jobs::Started, outcome: Outcome| {
let ticket = job.ticket();
job.end(outcome);
app.event(AppEvent::JobEnded(ticket))
};
let written = || Answer::QualityReportWritten(std::path::PathBuf::from("r.json"));
let report = app.job_for_tests(Job::QualityReport, Some("Writing the report..."));
assert!(app.is_busy(), "keys wait on it");
assert_eq!(app.status_message.as_deref(), Some("Writing the report..."));
assert!(end(&mut app, report, Outcome::answered(written())).is_none());
assert!(!app.is_busy(), "and come back with its answer");
assert_eq!(app.status_message, None, "with its line");
let report = app.job_for_tests(Job::QualityReport, Some("Writing the report..."));
app.status_message = Some(App::COUNTING_FOR_END.to_string());
end(
&mut app,
report,
Outcome::Failed {
message: "disk full".to_string(),
panicked: false,
},
);
assert!(!app.is_busy());
assert_eq!(
app.status_message.as_deref(),
Some(App::COUNTING_FOR_END),
"a line that is not the job's stays"
);
let load = app.open_for_tests("a.csv");
let scan = app.job_for_tests(Job::Load(load), Some("Scanning input..."));
let next = end(
&mut app,
scan,
Outcome::answered(Answer::Load(Box::new(
crate::loading::LoadAnswer::Scanned {
lf: Box::new(polars::df!("a" => [1i32]).unwrap().lazy()),
path: None,
options: OpenOptions::default(),
},
))),
);
assert!(
next.is_none(),
"the next phase starts in the answer's own step"
);
assert!(app.is_busy(), "the open's wait goes on into its next phase");
assert_eq!(
app.status_message.as_deref(),
Some("Reading schema..."),
"under the next phase's line"
);
}
#[test]
fn a_job_leaves_the_line_another_job_still_shows() {
use crate::{Answer, App, AppEvent, Job, Outcome};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let first = app.job_for_tests(Job::QualityReport, Some("Writing the report..."));
let second = app.job_for_tests(Job::QualityReport, Some("Writing the report..."));
let ticket = first.ticket();
first.end(Outcome::answered(Answer::QualityReportWritten(
std::path::PathBuf::from("a.json"),
)));
app.event(AppEvent::JobEnded(ticket));
assert!(app.is_busy(), "the second still holds the keys");
assert_eq!(
app.status_message.as_deref(),
Some("Writing the report..."),
"and its line"
);
drop(second);
}
#[test]
fn going_home_takes_down_the_line_of_the_rows_it_stops_waiting_on() {
use crate::{App, InflightCollect, Job};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.loading.first_rows_for_tests();
let read = app.job_for_tests(
Job::Rows(InflightCollect::for_tests(0, 100)),
Some(App::LOADING_BUFFER),
);
assert!(app.is_busy());
app.enter_home();
assert!(!app.is_busy(), "nobody waits on the rows");
assert_eq!(
app.status_message, None,
"and the bar says nothing about them"
);
assert!(app.rows_in_flight().is_some(), "though they still land");
drop(read);
}
#[test]
fn a_cancelled_read_waits_out_its_worker_and_a_replaced_one_does_not() {
use crate::{App, AppEvent, Job};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let sample = app.job_for_tests(Job::SampleRows, Some("Reading the sample..."));
app.cancel_analysis();
assert!(!app.is_busy());
assert!(
app.cancelled_analysis_running().is_some(),
"the cancelled read is still going"
);
drop(sample);
app.event(recv(&rx));
assert!(app.cancelled_analysis_running().is_none(), "until it ends");
let run = app.job_for_tests(
Job::Analysis(crate::app::jobs::AnalysisRun::default()),
Some("Running analysis..."),
);
app.jobs.advance();
assert!(
app.cancelled_analysis_running().is_none(),
"replaced, not cancelled"
);
drop(run);
let _ = app.handle(recv(&rx));
assert!(!matches!(rx.try_recv(), Ok(AppEvent::JobEnded(_))));
}
#[test]
fn a_superseded_scan_does_not_continue_the_load() {
use crate::loading::LoadAnswer;
use crate::{Answer, App, AppEvent, Job, OpenOptions, Outcome};
use polars::prelude::IntoLazy;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let scanned = || {
Answer::Load(Box::new(LoadAnswer::Scanned {
lf: Box::new(polars::df!("a" => [1i32]).unwrap().lazy()),
path: Some(std::path::PathBuf::from("whatever.parquet")),
options: OpenOptions::default(),
}))
};
let first = app.open_for_tests("first.parquet");
let scan = app.job_for_tests(Job::Load(first), Some("Scanning input..."));
let second = app.open_for_tests("second.parquet");
assert_ne!(first, second);
let ticket = scan.ticket();
scan.end(Outcome::answered(scanned()));
assert!(app.event(AppEvent::JobEnded(ticket)).is_none());
assert!(
app.jobs
.current(|job| matches!(job, Job::Load(_)))
.is_none(),
"a superseded scan must not continue the load pipeline"
);
assert_eq!(
app.load_shown()
.map(|(phase, _, path, _)| (phase.to_string(), path.map(Path::to_path_buf))),
Some((
"Scanning input".to_string(),
Some(std::path::PathBuf::from("second.parquet"))
)),
"nor change what the screen says about the open that replaced it"
);
let state = crate::table::DataTableState::from_lazyframe(
polars::df!("a" => [1i32]).unwrap().lazy(),
&OpenOptions::default(),
)
.unwrap();
let read = Answer::Load(Box::new(LoadAnswer::SchemaRead {
state: Box::new(state),
path: Some(std::path::PathBuf::from("first.parquet")),
options: OpenOptions::default(),
debug_label: None,
}));
assert!(app.answer_for_tests(Job::Load(first), read).is_none());
assert!(app.data_table_state.is_none(), "nothing was installed");
assert!(
app.awaiting_dataset(),
"the second open is still on its way"
);
assert!(app.answer_for_tests(Job::Load(second), scanned()).is_none());
assert!(
app.jobs
.current(|job| matches!(job, Job::Load(_)))
.is_some(),
"the schema is read next"
);
assert_eq!(
app.load_shown().map(|shown| shown.0),
Some("Reading schema")
);
}
#[test]
fn stale_analysis_answers_are_ignored() {
use crate::analysis::data_quality::{DataQualityResults, QualityPrecision};
use crate::analysis::statistics::AnalysisResults;
use crate::{Answer, App, AppEvent, Job, Outcome};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let results = || AnalysisResults {
column_statistics: vec![],
total_rows: 999_999,
sample_size: None,
per_value: None,
correlation_matrix: None,
distribution_analyses: vec![],
};
let answers = vec![
Answer::Analysis(|modal, r| modal.describe_results = Some(r), results()),
Answer::Analysis(|modal, r| modal.distribution_results = Some(r), results()),
Answer::Analysis(
crate::analysis::analysis_modal::AnalysisModal::install_correlations,
results(),
),
Answer::DataQuality {
results: Box::new(DataQualityResults {
total_rows: Some(999_999),
evaluated_rows: 1,
precision: QualityPrecision::Sampled,
columns: vec![],
observations: vec![],
segments: vec![],
temporal: vec![],
identity: None,
category_variants: vec![],
shared_nulls: vec![],
source_files: None,
per_value: None,
footers_read: None,
reads: None,
examples: vec![],
unsampled_segments: vec![],
intent: None,
source: None,
derived: Default::default(),
}),
kept: None,
plan: Box::default(),
},
];
app.overlay = crate::Overlay::Analysis;
app.analysis_modal.selected_tool =
Some(crate::analysis::analysis_modal::AnalysisTool::DataQuality);
let runs: Vec<_> = answers
.iter()
.map(|_| {
app.job_for_tests(
Job::Analysis(crate::app::jobs::AnalysisRun::default()),
Some("Running analysis..."),
)
})
.collect();
app.jobs.advance();
for (run, answer) in runs.into_iter().zip(answers) {
let ticket = run.ticket();
run.end(Outcome::answered(answer));
app.event(AppEvent::JobEnded(ticket));
}
let modal = &app.analysis_modal;
assert!(modal.describe_results.is_none());
assert!(modal.distribution_results.is_none());
assert!(modal.correlation_results.is_none());
assert!(modal.quality.results.is_none());
}
#[test]
fn a_cancelled_job_does_not_hold_the_generation() {
use crate::{App, AppEvent, Job};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let old = app.job_for_tests(
Job::Analysis(crate::app::jobs::AnalysisRun::default()),
Some("Running analysis..."),
);
app.jobs.advance();
assert!(
!app.work_a_bump_would_strand(),
"the abandoned worker is not waited on"
);
assert!(app.cancelled_work_running(), "though it is still running");
let current = app.job_for_tests(
Job::Analysis(crate::app::jobs::AnalysisRun::default()),
Some("Running analysis..."),
);
assert!(app.work_a_bump_would_strand());
drop(old);
let _ = app.handle(recv(&rx));
assert!(
app.work_a_bump_would_strand(),
"the current job still holds"
);
assert!(!app.cancelled_work_running());
drop(current);
let ended = recv(&rx);
assert!(matches!(ended, AppEvent::JobEnded(_)));
let _ = app.handle(ended);
assert!(!app.work_a_bump_would_strand());
}
#[test]
fn a_panicking_worker_ends_its_job() {
use crate::{App, AppEvent, Job, JobKind};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let generation = app.task_generation();
app.overlay = crate::Overlay::PivotMelt;
let ticket = app.spawn_job(
Job::Pivot,
Some(App::COMPUTING_PIVOT),
|_| -> std::result::Result<crate::Answer, String> { panic!("worker died") },
);
assert!(app.work_a_bump_would_strand());
assert!(app.is_busy());
assert!(app.pivot_computing(), "the form waits on it");
let failed = recv(&rx);
assert!(
matches!(&failed, AppEvent::JobEnded(t) if *t == ticket && t.kind() == JobKind::Pivot),
"the panic ends the job it stopped"
);
let _ = app.handle(failed);
assert!(!app.is_busy(), "the spinner comes down");
assert!(app.status_message.is_none());
assert!(app.error_modal.active, "and the user is told");
assert!(
app.error_modal.message.contains("worker died"),
"{}",
app.error_modal.message
);
assert!(!app.pivot_computing(), "the pivot is no longer waited on");
assert!(
!app.work_a_bump_would_strand(),
"the generation is free again rather than held forever"
);
assert_eq!(app.task_generation(), generation, "and nothing bumped it");
assert!(
rx.recv_timeout(std::time::Duration::from_millis(50))
.is_err()
);
}
fn describe(event: &crate::AppEvent) -> String {
use crate::AppEvent;
match event {
AppEvent::JobEnded(ticket) => format!("JobEnded({:?})", ticket.kind()),
AppEvent::BackgroundLenReady { num_rows, .. } => {
format!("BackgroundLenReady({num_rows})")
}
AppEvent::BackgroundLenFailed { .. } => "BackgroundLenFailed".to_string(),
_ => "another event".to_string(),
}
}
#[test]
fn an_error_and_a_panic_end_a_job_the_same_way() {
use crate::{App, AppEvent, Job};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
for (what, dies) in [("returns an error", false), ("panics", true)] {
app.error_modal.hide();
app.spawn_job(
Job::QualityReport,
Some("Writing the report..."),
move |_| {
assert!(!dies, "worker died");
Err::<crate::Answer, _>("disk full".to_string())
},
);
let ended = recv(&rx);
assert!(
matches!(ended, AppEvent::JobEnded(_)),
"a job that {what}: {}",
describe(&ended)
);
let _ = app.handle(ended);
assert!(
rx.recv_timeout(std::time::Duration::from_millis(50))
.is_err(),
"a job that {what} ends once"
);
assert!(!app.is_busy(), "a job that {what} is over");
assert!(app.error_modal.active, "a job that {what} says why");
assert!(
app.error_modal
.message
.contains(if dies { "worker died" } else { "disk full" }),
"a job that {what}: {}",
app.error_modal.message
);
assert!(!app.work_a_bump_would_strand());
}
}
#[test]
fn a_failure_leaves_other_work_alone() {
use crate::chart::chart_export::ChartExportFormat;
use crate::{AnalysisProgress, App, AppEvent, InflightCollect, Job, Outcome};
use std::path::PathBuf;
let gone = crate::loading::LoadId::for_tests(u64::MAX);
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let fail = |app: &mut App, job: crate::app::jobs::Started| {
let ticket = job.ticket();
job.end(Outcome::Failed {
message: "not this one".to_string(),
panicked: false,
});
app.event(AppEvent::JobEnded(ticket));
};
let untouched = |app: &App, what: &str| {
assert!(app.is_busy(), "{what}: still busy");
assert!(!app.error_modal.active, "{what}: no error shown");
assert!(
app.jobs
.current(|job| matches!(job, Job::Analysis(_)))
.is_some(),
"{what}: the analysis is still waited on"
);
};
let analysis = || Job::Analysis(crate::app::jobs::AnalysisRun::default());
let kinds = [
analysis(),
Job::SampleRows,
Job::Load(gone),
Job::OpenNamed(gone),
Job::Rows(InflightCollect::for_tests(0, 100)),
Job::Pivot,
Job::DrillRow,
Job::InspectRow { frame: 0, row: 0 },
Job::InspectJson { token: 0 },
Job::Export,
Job::Copy,
Job::QualityReport,
];
let passed: Vec<_> = kinds
.iter()
.map(|job| app.job_for_tests(job.clone(), Some("Working...")))
.collect();
app.jobs.advance();
let running = app.job_for_tests(analysis(), Some("Running analysis..."));
app.analysis_modal.computing = Some(AnalysisProgress::new("Running analysis"));
let ahead = app.job_for_tests(Job::Rows(InflightCollect::for_tests(0, 100)), None);
fail(&mut app, ahead);
untouched(&app, "a load-ahead");
assert!(app.analysis_modal.computing.is_some());
assert!(
app.rows_in_flight().is_none(),
"the load-ahead's record goes"
);
for (job, started) in kinds.iter().zip(passed) {
fail(&mut app, started);
untouched(&app, &format!("{job:?} from a passed generation"));
}
let look = |path: &str| {
Job::Classify(crate::app::jobs::Classify {
path: PathBuf::from(path),
browsing: None,
jump: false,
})
};
let older = app.job_for_tests(look("/older"), None);
app.jobs.supersede(|job| matches!(job, Job::Classify(_)));
let _newer = app.job_for_tests(look("/newer"), None);
fail(&mut app, older);
untouched(&app, "an older look");
let older = app.job_for_tests(
Job::LookAtDirectory {
load: gone,
path: PathBuf::from("/older"),
},
None,
);
app.jobs
.supersede(|job| matches!(job, Job::LookAtDirectory { .. }));
fail(&mut app, older);
untouched(&app, "an older look at a directory");
let older = app.job_for_tests(
Job::ChartExport {
path: PathBuf::from("/tmp/old.png"),
format: ChartExportFormat::Png,
},
None,
);
app.jobs
.supersede(|job| matches!(job, Job::ChartExport { .. }));
fail(&mut app, older);
untouched(&app, "an older chart export");
assert_ne!(app.overlay, crate::Overlay::ChartExport);
let load = app.open_for_tests("gone.csv");
let open = app.job_for_tests(Job::Load(load), None);
app.abandon_load();
fail(&mut app, open);
untouched(&app, "an abandoned open");
fail(&mut app, running);
assert!(!app.is_busy());
assert!(app.analysis_modal.computing.is_none());
assert!(app.error_modal.active);
}
#[test]
fn a_count_answers_however_its_worker_ends() {
use crate::{App, AppEvent, DataTableState, LenCount, OpenOptions, OwedCount};
use polars::prelude::IntoLazy;
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx.clone(), crate::tests::test_runtime());
let lf = polars::df!("a" => [1i32, 2, 3]).unwrap().lazy();
let state = DataTableState::from_lazyframe(lf, &OpenOptions::default()).unwrap();
let generation = state.len_generation();
let owed = || OwedCount::new(LenCount::for_state(&state), tx.clone());
let answer = |app: &mut App, said: fn(&AppEvent) -> bool| {
app.counting.len_count_inflight = Some(generation);
let event = recv(&rx);
assert!(said(&event), "{}", describe(&event));
app.event(event);
assert_eq!(
app.counting.len_count_inflight, None,
"the count is no longer waited on"
);
assert!(rx.try_recv().is_err(), "once");
};
owed().answer(|_| panic!("count died"));
answer(&mut app, |e| {
matches!(e, AppEvent::BackgroundLenFailed { .. })
});
drop(owed());
answer(&mut app, |e| {
matches!(e, AppEvent::BackgroundLenFailed { .. })
});
owed().answer(LenCount::run);
answer(&mut app, |e| {
matches!(e, AppEvent::BackgroundLenReady { num_rows: 3, .. })
});
}
#[test]
fn an_open_holds_the_generation_before_the_errands_behind_it() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("next.csv");
std::fs::write(&path, "name,age\nada,36\n").expect("write csv");
let rows = || df!("id" => &[1i64, 2, 3]).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap();
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
app.owe_rows_for_tests("Loading buffer...");
assert!(
!app.work_a_bump_would_strand(),
"nothing holds the generation: the errand would go in on the next event"
);
let out = app
.handle(AppEvent::Open(vec![path], OpenOptions::default()))
.expect("the open is not a key");
assert!(out.is_none(), "the open started its scan itself");
assert!(
app.work_a_bump_would_strand(),
"and the scan holds the generation"
);
assert!(
app.rows_owed(),
"the collect is still owed rather than run: running it here would bump the \
generation the open has just taken for its scan"
);
}
#[cfg(feature = "http")]
#[test]
fn a_download_waiting_on_the_user_holds_the_generation() {
use crate::loading::{LoadAnswer, PendingDownload};
use crate::{Answer, App, Job, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let url = "https://example.invalid/data.parquet";
let load = app.open_for_tests(url);
assert!(!app.work_a_bump_would_strand(), "nothing is running yet");
let pending = PendingDownload::Http {
url: url.to_string(),
size: Some(1024),
options: OpenOptions::default(),
};
let _ = app.answer_for_tests(
Job::Load(load),
Answer::Load(Box::new(LoadAnswer::Sized(pending))),
);
assert!(app.confirmation_modal.active, "the user is being asked");
assert!(
app.work_a_bump_would_strand(),
"and the generation is held for as long as they take to answer"
);
let _ = app.key(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(
!app.work_a_bump_would_strand(),
"nothing waits on it once the download is declined"
);
}
#[cfg(feature = "http")]
#[test]
fn the_size_probe_reads_a_compressing_server() {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!(
"http://{}/flights.csv",
listener.local_addr().expect("bound")
);
std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut request = Vec::new();
let mut chunk = [0u8; 1024];
while !request.windows(4).any(|w| w == b"\r\n\r\n") {
match stream.read(&mut chunk) {
Ok(0) | Err(_) => break,
Ok(n) => request.extend_from_slice(&chunk[..n]),
}
}
let asks_for_gzip = String::from_utf8_lossy(&request)
.to_ascii_lowercase()
.lines()
.any(|l| l.starts_with("accept-encoding:") && l.contains("gzip"));
let headers = if asks_for_gzip {
"Content-Encoding: gzip\r\nContent-Length: 9404410"
} else {
"Content-Length: 33206996"
};
let _ = write!(
stream,
"HTTP/1.1 200 OK\r\n{headers}\r\nConnection: close\r\n\r\n"
);
});
assert_eq!(
crate::App::fetch_remote_size_http(&url).expect("the file is there"),
Some(33_206_996),
"the file's own length, not the compressed one and not none"
);
}
#[cfg(feature = "http")]
#[test]
fn the_download_confirmation_is_not_busy() {
use crate::{App, AppEvent, OpenOptions};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
fn screen(app: &mut App) -> String {
let area = Rect::new(0, 0, 120, 24);
let mut buf = Buffer::empty(area);
app.render(area, &mut buf);
buf.content().iter().map(|c| c.symbol()).collect()
}
let spinning = |text: &str| {
crate::glyphs::get()
.spinner
.iter()
.any(|frame| text.contains(frame))
};
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}/flights.parquet", listener.local_addr().unwrap());
let url = url.as_str();
std::thread::spawn(move || {
use std::io::{Read, Write};
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut head = Vec::new();
let mut byte = [0u8; 1];
while !head.ends_with(b"\r\n\r\n") && stream.read(&mut byte).unwrap_or(0) == 1 {
head.push(byte[0]);
}
let _ = write!(
stream,
"HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
);
}
});
let mut next = Some(AppEvent::Open(
vec![std::path::PathBuf::from(url)],
OpenOptions::default(),
));
while let Some(event) = next {
next = app.handle(event).expect("no keys here");
}
assert!(app.is_busy(), "the probe is running");
let bar = footer_text(&mut app);
assert!(bar.contains("Checking size"), "the probe is named: {bar}");
assert!(!bar.contains("Scanning"), "nothing is scanned yet: {bar}");
let answered = rx
.recv_timeout(std::time::Duration::from_secs(60))
.expect("the probe answers");
assert!(matches!(answered, AppEvent::JobEnded(_)));
let _ = app.handle(answered);
assert!(app.awaiting_open_confirmation(), "the user is being asked");
assert!(!app.is_busy(), "and nothing is running while they decide");
let bar = footer_text(&mut app);
assert!(!bar.contains("..."), "nothing said to be running: {bar}");
assert!(
!bar.contains("Checking") && !bar.contains("Scanning"),
"and no phase: {bar}"
);
assert!(!app.something_is_spinning(), "the run loop turns nothing");
if crate::glyphs::active_is_unicode() {
let text = screen(&mut app);
assert!(!spinning(&text), "no spinner anywhere: {text}");
}
}
struct TestClipboard(Option<usize>);
impl crate::clipboard::Destination for TestClipboard {
fn write(&mut self, _: crate::clipboard::Payload) -> Result<(), String> {
Ok(())
}
fn describe(&self) -> &'static str {
"test"
}
fn accepts(&self) -> crate::clipboard::Accepts {
crate::clipboard::Accepts {
html: self.0.is_none(),
base64_limit: self.0,
}
}
}
fn uncapped_clipboard(app: &mut crate::App) {
app.set_clipboard_destination(Box::new(TestClipboard(None)));
}
#[test]
fn a_capped_table_copy_asks_only_past_what_the_cap_could_hold() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let copy = |footer_width: Option<usize>, limit: usize| {
let rows = || {
df!("id" => &[1i64, 2, 3], "blob" => &[b"a".as_slice(), b"b", b"c"])
.unwrap()
.lazy()
};
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let state = DataTableState::from_schema_and_lazyframe(
schema,
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
column_bytes: footer_width
.map(|width| vec![("blob".to_string(), width)])
.unwrap_or_default(),
..Default::default()
});
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
assert!(state.count_landed(state.len_generation(), 3, None));
app.set_clipboard_destination(Box::new(TestClipboard(Some(limit))));
app.copy_modal.scope = crate::app::modals::copy_modal::CopyScope::Table;
let next = app.perform_copy();
(app, next)
};
let (app, next) = copy(Some(3 * 1024 * 1024), 100 * 1024);
assert!(!app.confirmation_modal.active);
assert!(matches!(
next,
Some(AppEvent::Applied(crate::Applied::CopyTable { .. }))
));
let (app, next) = copy(None, 100 * 1024);
assert!(!app.confirmation_modal.active);
assert!(matches!(
next,
Some(AppEvent::Applied(crate::Applied::CopyTable { .. }))
));
let (app, next) = copy(Some(3 * 1024 * 1024), 64 * 1024 * 1024);
assert!(app.confirmation_modal.active && next.is_none());
let (app, next) = copy(None, 64 * 1024 * 1024);
assert!(app.confirmation_modal.active && next.is_none());
}
#[test]
fn a_table_copy_with_no_size_yet_asks_first() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => &[1i64, 2, 3]).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap();
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
state.invalidate_num_rows();
assert!(state.estimated_copy_bytes().is_none());
uncapped_clipboard(&mut app);
app.copy_modal.scope = crate::app::modals::copy_modal::CopyScope::Table;
let _ = app.perform_copy();
assert!(
app.confirmation_modal.active,
"an unknown size asks; it never collects unprompted"
);
assert!(matches!(
app.confirmation_modal.asking,
Some(crate::app::feedback::Confirm::Copy(..))
));
}
#[test]
fn a_table_copy_counts_binary_at_its_base64_size() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let copy = |footer_width: Option<usize>| {
let rows = || {
df!("id" => &[1i64, 2, 3], "blob" => &[b"a".as_slice(), b"b", b"c"])
.unwrap()
.lazy()
};
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let state = DataTableState::from_schema_and_lazyframe(
schema,
rows(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
column_bytes: footer_width
.map(|width| vec![("blob".to_string(), width)])
.unwrap_or_default(),
..Default::default()
});
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
assert!(state.count_landed(state.len_generation(), 3, None));
uncapped_clipboard(&mut app);
app.copy_modal.scope = crate::app::modals::copy_modal::CopyScope::Table;
let next = app.perform_copy();
(app, next)
};
let (app, next) = copy(Some(3 * 1024 * 1024));
assert!(app.confirmation_modal.active, "large blobs ask first");
assert!(
matches!(
app.confirmation_modal.asking,
Some(crate::app::feedback::Confirm::Copy(..))
) && next.is_none()
);
let (app, next) = copy(Some(100));
assert!(!app.confirmation_modal.active, "small blobs copy");
assert!(matches!(
next,
Some(AppEvent::Applied(crate::Applied::CopyTable { .. }))
));
let (app, next) = copy(None);
assert!(app.confirmation_modal.active, "unmeasured blobs ask");
assert!(
matches!(
app.confirmation_modal.asking,
Some(crate::app::feedback::Confirm::Copy(..))
) && next.is_none()
);
}
#[test]
fn a_local_directorys_footers_size_its_binary_columns() {
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
let copy = |blob: usize| {
let dir = tempfile::tempdir().unwrap();
for (file, rows) in [(0u8, 2u8), (1, 1)] {
let blobs: Vec<Vec<u8>> = (0..rows).map(|i| vec![file * 10 + i; blob]).collect();
let mut df = df!(
"id" => (0..rows as i64).collect::<Vec<_>>(),
"blob" => blobs.iter().map(Vec::as_slice).collect::<Vec<_>>(),
)
.unwrap();
let f = std::fs::File::create(dir.path().join(format!("{file}.parquet"))).unwrap();
ParquetWriter::new(f).finish(&mut df).unwrap();
}
let options = OpenOptions {
hive: true,
..OpenOptions::default()
};
let state =
App::schema_state_from_local_hive(Some(dir.path()), &options, &Default::default())
.map(|(state, facts)| state.with_open(facts))
.expect("the local footer route");
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &options, None);
let state = app.data_table_state.as_mut().unwrap();
assert!(state.count_landed(state.len_generation(), 3, None));
uncapped_clipboard(&mut app);
app.copy_modal.scope = crate::app::modals::copy_modal::CopyScope::Table;
let next = app.perform_copy();
(app, next)
};
let (app, next) = copy(3 * 1024 * 1024);
assert!(next.is_none());
let message = &app.confirmation_modal.message;
assert!(message.starts_with("This copies about 12"), "{message}");
let (app, next) = copy(100);
assert!(!app.confirmation_modal.active, "small blobs copy");
assert!(matches!(
next,
Some(AppEvent::Applied(crate::Applied::CopyTable { .. }))
));
}
#[test]
fn a_confirmation_keeps_its_keys_in_its_own_footer() {
use crate::App;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("rows.csv");
std::fs::write(&path, "id\n1\n2\n3\n").unwrap();
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx.clone(), crate::tests::test_runtime());
chart_prepare_tests::open(&mut app, &rx, &tx, path);
let _ = app.key(&KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE));
assert!(
footer_text(&mut app).contains("+/- Filter"),
"the column's keys"
);
app.data_table_state.as_mut().unwrap().invalidate_num_rows();
uncapped_clipboard(&mut app);
app.copy_modal.scope = crate::app::modals::copy_modal::CopyScope::Table;
let _ = app.perform_copy();
assert!(app.confirmation_modal.active, "an unknown size asks");
let bar = footer_text(&mut app);
assert!(!bar.contains("Filter"), "not the table's: {bar}");
}
#[test]
fn a_count_landing_during_a_load_does_not_bump_the_generation() {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, AppEvent, OpenOptions};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
..Default::default()
});
state.visible_rows = 10;
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let waiting = app.data_table_state.as_ref().unwrap().len_generation();
let _ = app.key(&KeyEvent::new(KeyCode::End, KeyModifiers::NONE));
assert_eq!(app.counting.end_after_count, Some(waiting));
let lease = app.hold_the_generation();
let opening = app.task_generation();
let mut follow = app.event(AppEvent::BackgroundLenReady {
len_generation: waiting,
num_rows: 100,
file_row_groups: None,
});
while let Some(event) = follow {
follow = app.event(event);
}
assert_eq!(
app.task_generation(),
opening,
"the open is still waiting on the generation the jump would have bumped"
);
assert!(
app.rows_owed(),
"and the jump's collect is owed rather than dropped"
);
drop(lease);
let _ = app.handle(AppEvent::Update);
assert!(
!app.rows_owed(),
"the collect the jump asked for runs once nothing is waiting"
);
assert_ne!(
app.task_generation(),
opening,
"and it is what bumps the generation, now that it is safe to"
);
}
#[test]
fn a_dataset_owed_a_re_read_does_not_print_its_partial_as_the_total() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use polars::prelude::*;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::widgets::Widget;
use std::sync::Arc;
let rows = || df!("id" => (0..70i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(|_| None)),
..Default::default()
});
state.set_provisional_rows(70);
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
app.busy = false;
let bar_says_seventy = |app: &mut App| {
let area = Rect::new(0, 0, 100, 24);
let mut buf = Buffer::empty(area);
(&mut *app).render(area, &mut buf);
(0..area.width)
.map(|x| buf[(x, area.height - 1)].symbol().to_string())
.collect::<String>()
.contains("70 rows")
};
assert!(
!bar_says_seventy(&mut app),
"while the pass is out the dataset says its count is coming"
);
app.export_progress = Some(crate::ExportProgress::new(
std::path::Path::new("/tmp/out.csv"),
"Collecting",
));
let _lease = app.hold_the_generation();
let live = app.dataset_generation;
let _ = app.footers_joined(live, None);
assert!(
app.counting.reread_owed.is_some(),
"the fixture is a dataset owed a re-read it cannot have yet"
);
app.busy = false;
assert!(
!bar_says_seventy(&mut app),
"and it goes on saying so while the count it is owed waits its turn"
);
}
#[test]
fn a_query_over_a_dataset_still_reading_its_footers_is_counted() {
use crate::table::{DataTableState, RemoteFiles};
use crate::{App, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let rows = || df!("id" => (0..100i64).collect::<Vec<_>>()).unwrap().lazy();
let mut lf = rows();
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let mut state =
DataTableState::from_schema_and_lazyframe(schema, rows(), &OpenOptions::default(), None)
.unwrap()
.with_open(crate::table::OpenFacts {
remote_source: true,
remote_files: Some(RemoteFiles {
urls: Arc::new(vec!["one".to_string()]),
scan: Arc::new(move |_u: &[String], _t: &[PlSmallStr]| Ok(rows())),
count: Arc::new(|_| Ok(vec![vec![100]])),
offsets: None,
}),
footers_pending: Some(Arc::new(|_| None)),
..Default::default()
});
state.visible_rows = 10;
assert!(
state.counts_itself_later(),
"the pass is bringing this dataset's count"
);
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state, None, &OpenOptions::default(), None);
let state = app.data_table_state.as_mut().unwrap();
state.deferred(|s| s.query("select doubled: id * 2".to_string()));
assert!(
!state.counts_itself_later(),
"which is not the count the pass is bringing, and nothing else will take it"
);
app.spawn_async_collect(App::LOADING_BUFFER);
assert!(
app.counting.len_count_inflight.is_some(),
"so it is taken, rather than the row count spinning while the query is open"
);
}
#[test]
fn a_pass_from_the_dataset_before_this_one_joins_nothing_to_it() {
use crate::table::DataTableState;
use crate::{App, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let state_of = |lf: LazyFrame| {
DataTableState::from_schema_and_lazyframe(
dataset_of(lf.clone()).schema.clone(),
lf,
&OpenOptions::default(),
None,
)
.unwrap()
};
let first = || df!("id" => &[1i64]).unwrap().lazy();
let its_columns = || df!("id" => &[1i64], "oops" => &["a"]).unwrap().lazy();
let second = || df!("other" => &[2i64]).unwrap().lazy();
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state = state_of(first()).with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(move |_progress| {
Some(crate::table::FootersFound {
estimate: None,
dataset: dataset_of(its_columns()),
lf: its_columns(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
})
})),
..Default::default()
});
app.install_for_tests(state, None, &OpenOptions::default(), None);
let reported = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the first dataset's pass reports back");
app.install_for_tests(state_of(second()), None, &OpenOptions::default(), None);
let _ = app.handle(reported);
assert_eq!(
app.data_table_state.as_ref().unwrap().get_column_order(),
["other"],
"the directory on screen does not gain a column from the directory before it"
);
assert!(
app.counting.footers_held.is_none(),
"and they are not kept waiting for a dataset that is gone"
);
}
#[test]
fn a_pass_that_failed_waits_for_work_already_asked_for() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64]).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap();
app.install_for_tests(state, None, &OpenOptions::default(), None);
app.export_progress = Some(crate::ExportProgress::new(
std::path::Path::new("/tmp/out.csv"),
"Collecting",
));
let lease = app.hold_the_generation();
let waiting_on = app.task_generation();
let live = app.dataset_generation;
let _ = app.footers_joined(live, None);
assert_eq!(
app.task_generation(),
waiting_on,
"the export is still waiting on the answer this app would have thrown away"
);
assert_eq!(
app.counting.reread_owed,
Some(live),
"and the re-read the dataset is owed is remembered, not dropped"
);
app.export_progress = None;
drop(lease);
let _ = app.handle(AppEvent::Update);
let _ = app.handle(AppEvent::Update);
assert!(
app.task_generation() != waiting_on,
"the dataset gets the collect it was owed once nothing is waiting on the \
generation — without it, it never counts itself at all"
);
assert!(
app.counting.reread_owed.is_none(),
"and the errand is done rather than run again on every event"
);
}
#[test]
fn columns_arriving_during_work_already_asked_for_wait_for_it() {
use crate::table::{DataTableState, FootersFound};
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64]).unwrap().lazy();
let wider = || df!("id" => &[1i64], "oops" => &["a"]).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap();
app.install_for_tests(state, None, &OpenOptions::default(), None);
type Underway = (
Option<crate::app::jobs::Hold>,
Option<crate::app::jobs::Started>,
);
type Start = fn(&mut App) -> Underway;
let under_way: Vec<(&str, Start)> = vec![
("leased background work", |app: &mut App| {
(Some(app.hold_the_generation()), None)
}),
("a chart", |app: &mut App| {
let mut modal = crate::chart::chart_modal::ChartModal::new();
modal.spec.encoding.x.field = Some("id".to_string());
let prep = crate::app::jobs::ChartPrep {
dataset: None,
request: crate::ChartRequest::from_modal(&modal).expect("an x range"),
cancel: Default::default(),
};
(
None,
Some(app.job_for_tests(crate::app::jobs::Job::ChartPrepare(Box::new(prep)), None)),
)
}),
];
let put_away = |app: &mut App, (lease, job): Underway| {
drop(lease);
if let Some(job) = job {
let ticket = job.ticket();
job.end(crate::app::jobs::Outcome::Failed {
message: "put away".to_string(),
panicked: false,
});
app.jobs.end(ticket);
}
let _ = app.handle(AppEvent::Update);
};
for (what, start) in under_way {
let lease = start(&mut app);
let waiting_on = app.task_generation();
app.counting.footers_held = Some((
app.dataset_generation,
FootersFound {
estimate: None,
dataset: dataset_of(wider()),
lf: wider(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
},
));
let _ = app.handle(AppEvent::Update);
assert_eq!(
app.task_generation(),
waiting_on,
"{what} is still waiting on the answer this app would have thrown away"
);
assert!(
app.counting.footers_held.is_some(),
"and the columns wait their turn behind {what}"
);
put_away(&mut app, lease);
}
let _ = app.handle(AppEvent::Update);
assert_eq!(
app.data_table_state
.as_ref()
.unwrap()
.get_column_order()
.last()
.map(String::as_str),
Some("oops"),
"once nothing is waiting on an answer, the columns join"
);
}
#[test]
fn columns_held_for_one_dataset_are_not_given_to_the_next() {
use crate::table::{DataTableState, FootersFound};
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let first = || df!("id" => &[1i64]).unwrap().lazy();
let its_columns = || df!("id" => &[1i64], "oops" => &["a"]).unwrap().lazy();
let second = || df!("other" => &[2i64]).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let state_of = |lf: LazyFrame| {
DataTableState::from_schema_and_lazyframe(
dataset_of(lf.clone()).schema.clone(),
lf,
&OpenOptions::default(),
None,
)
.unwrap()
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state_of(first()), None, &OpenOptions::default(), None);
app.data_table_state
.as_mut()
.unwrap()
.query("select doubled: id * 2".to_string());
app.counting.footers_held = Some((
app.dataset_generation,
FootersFound {
estimate: None,
dataset: dataset_of(its_columns()),
lf: its_columns(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
},
));
let _ = app.handle(AppEvent::Update);
assert!(
app.counting.footers_held.is_some(),
"waiting, as they should be"
);
app.install_for_tests(state_of(second()), None, &OpenOptions::default(), None);
let _ = app.handle(AppEvent::Update);
assert_eq!(
app.data_table_state.as_ref().unwrap().get_column_order(),
["other"],
"the directory now on screen is not given the last one's columns"
);
assert!(
app.counting.footers_held.is_none(),
"and they are let go rather than waiting on for a third dataset"
);
}
#[test]
fn a_late_event_from_an_old_pass_does_not_throw_away_the_live_answer() {
use crate::table::{DataTableState, FootersFound};
use crate::{App, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64]).unwrap().lazy();
let wider = || df!("id" => &[1i64], "oops" => &["a"]).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let found = || FootersFound {
estimate: None,
dataset: dataset_of(wider()),
lf: wider(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
};
let last_column = |app: &App| {
app.data_table_state
.as_ref()
.unwrap()
.get_column_order()
.last()
.cloned()
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap();
app.install_for_tests(state, None, &OpenOptions::default(), None);
let live = app.dataset_generation;
let _ = app.footers_joined(live.wrapping_sub(1), Some(found()));
assert_eq!(
last_column(&app).as_deref(),
Some("id"),
"the older answer changes nothing"
);
let _ = app.footers_joined(live, Some(found()));
assert_eq!(last_column(&app).as_deref(), Some("oops"));
}
#[test]
fn a_footer_answer_survives_an_open_that_fails() {
use crate::table::{DataTableState, FootersFound};
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64]).unwrap().lazy();
let wider = || df!("id" => &[1i64], "oops" => &["a"]).unwrap().lazy();
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![1],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let found = FootersFound {
estimate: None,
dataset: dataset_of(wider()),
lf: wider(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
};
let found = std::sync::Mutex::new(Some(found));
let (tx, rx) = std::sync::mpsc::channel();
let mut app = App::new(tx.clone(), crate::tests::test_runtime());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(move |_progress| found.lock().unwrap().take())),
..Default::default()
});
app.install_for_tests(state, None, &OpenOptions::default(), None);
let answered = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the pass answers");
assert!(
matches!(answered, AppEvent::JobEnded(ticket) if ticket.kind() == crate::JobKind::FootersJoin)
);
let dir = tempfile::tempdir().unwrap();
let broken = dir.path().join("broken.parquet");
std::fs::write(&broken, b"not parquet").unwrap();
let mut next = Some(AppEvent::Open(vec![broken], OpenOptions::default()));
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while app.error_message().is_none() {
assert!(
std::time::Instant::now() < deadline,
"the open never failed"
);
let event = match next.take() {
Some(event) => event,
None => match rx.recv_timeout(std::time::Duration::from_millis(50)) {
Ok(event) => event,
Err(_) => continue,
},
};
next = app.event(event);
}
let _ = app.handle(answered);
assert_eq!(
app.data_table_state
.as_ref()
.unwrap()
.get_column_order()
.last()
.map(String::as_str),
Some("oops"),
"the dataset still on screen gets its columns"
);
}
#[test]
fn a_journal_reread_for_a_replaced_dataset_is_dropped() {
use crate::app::jobs::{Answer, Job, Outcome};
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
let state = || {
let lf = df!("id" => &[1i64]).unwrap().lazy();
let mut probe = lf.clone();
let schema = probe.collect_schema().unwrap();
DataTableState::from_schema_and_lazyframe(schema, lf, &OpenOptions::default(), None)
.unwrap()
};
let detail = || {
Answer::JournalDescribed(Box::new(crate::formats::text_formats::Detail {
tab: "Journal",
..Default::default()
}))
};
let tab = |app: &App| {
app.data_table_state
.as_ref()
.unwrap()
.format_detail()
.map(|detail| detail.tab)
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
app.install_for_tests(state(), None, &OpenOptions::default(), None);
let dataset = app.dataset_generation;
let started = app.job_for_tests(Job::JournalDetail { dataset }, None);
app.install_for_tests(state(), None, &OpenOptions::default(), None);
let ticket = started.ticket();
started.end(Outcome::answered(detail()));
let _ = app.handle(AppEvent::JobEnded(ticket));
assert_eq!(tab(&app), None, "the dataset on screen keeps its own tab");
let dataset = app.dataset_generation;
let started = app.job_for_tests(Job::JournalDetail { dataset }, None);
let ticket = started.ticket();
started.end(Outcome::answered(detail()));
let _ = app.handle(AppEvent::JobEnded(ticket));
assert_eq!(tab(&app), Some("Journal"));
}
#[test]
fn columns_arriving_under_a_query_wait_rather_than_break_it() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64, 2], "v" => &[10i64, 20]).unwrap().lazy();
let wider = || {
df!("id" => &[1i64, 2], "v" => &[10i64, 20], "oops" => &["a", "b"])
.unwrap()
.lazy()
};
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![2],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let (tx, _rx) = std::sync::mpsc::channel();
let mut app = App::new(tx, crate::tests::test_runtime());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap();
app.install_for_tests(state, None, &OpenOptions::default(), None);
let table = app.data_table_state.as_mut().unwrap();
table.query("select doubled: v * 2".to_string());
assert!(
table.error().is_none(),
"the query runs: {:?}",
table.error()
);
let asked = table.get_column_order().to_vec();
let generation = app.dataset_generation;
app.counting.footers_held = Some((
generation,
crate::table::FootersFound {
estimate: None,
dataset: dataset_of(wider()),
lf: wider(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
},
));
let _ = app.handle(AppEvent::Update);
let table = app.data_table_state.as_ref().unwrap();
assert_eq!(
table.get_column_order(),
asked.as_slice(),
"the query's own columns are still what is on screen"
);
assert!(
table.error().is_none(),
"and it has not been broken out from under: {:?}",
table.error()
);
assert!(
app.counting.footers_held.is_some(),
"the columns are kept, not thrown away"
);
app.data_table_state.as_mut().unwrap().query(String::new());
let _ = app.handle(AppEvent::Update);
assert_eq!(
app.data_table_state
.as_ref()
.unwrap()
.get_column_order()
.last()
.map(String::as_str),
Some("oops"),
"the columns join once the view is back on the data"
);
assert!(
app.counting.footers_held.is_none(),
"with nothing left waiting"
);
}
#[test]
fn a_staged_open_joins_what_its_footers_found() {
use crate::table::DataTableState;
use crate::{App, AppEvent, OpenOptions};
use polars::prelude::*;
use std::sync::Arc;
let frame = || df!("id" => &[1i64, 2], "v" => &[10i64, 20]).unwrap().lazy();
let wider = || {
df!("id" => &[1i64, 2], "v" => &[10i64, 20], "oops" => &["a", "b"])
.unwrap()
.lazy()
};
let dataset_of = |lf: LazyFrame| {
let mut lf = lf;
let schema = Arc::new((*lf.collect_schema().unwrap()).clone());
let footer = crate::formats::schema_union::FileFooter {
schema,
row_group_rows: vec![2],
file_bytes: 0,
row_group_bytes: Vec::new(),
column_bytes: Vec::new(),
};
crate::formats::schema_union::union_sampled(1, &[0], &[Some(footer)])
};
let reads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let counted = {
let reads = reads.clone();
move || {
let reads = reads.clone();
wider().with_column(col("id").map(
move |s| {
reads.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(s)
},
|_schema: &Schema, field: &Field| Ok(field.clone()),
))
}
};
let (tx, rx) = std::sync::mpsc::channel();
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
let mut app = App::new(tx, runtime.handle().clone());
let state = DataTableState::from_schema_and_lazyframe(
dataset_of(frame()).schema.clone(),
frame(),
&OpenOptions::default(),
None,
)
.unwrap()
.with_open(crate::table::OpenFacts {
footers_pending: Some(Arc::new(move |_progress| {
Some(crate::table::FootersFound {
estimate: None,
dataset: dataset_of(counted()),
lf: counted(),
file_rows: Vec::new(),
files: Vec::new(),
row_groups: Vec::new(),
remote: None,
})
})),
..Default::default()
});
app.install_for_tests(state, None, &OpenOptions::default(), None);
assert!(
!app.is_busy(),
"the dataset is on screen and must keep working while the rest are read"
);
let joined = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("the pass reports back");
let AppEvent::JobEnded(ticket) = joined else {
panic!("expected the footers to be reported, got another event");
};
assert_eq!(ticket.kind(), crate::JobKind::FootersJoin);
app.abandon_load();
app.jobs.worker_dies = crate::tests::worker_dies_once(|job| matches!(job, crate::Job::Rows(_)));
let _ = app.handle(AppEvent::JobEnded(ticket));
assert!(
app.rows_in_flight().is_some(),
"the rows on screen were read through the narrow frame and are read again"
);
assert_eq!(
app.data_table_state
.as_ref()
.unwrap()
.get_column_order()
.last()
.map(String::as_str),
Some("oops"),
"the column the pass found joins the dataset on screen"
);
assert_eq!(
reads.load(std::sync::atomic::Ordering::Relaxed),
0,
"and nothing was read to do it: this runs on the thread drawing the frame, \
and against a bucket the read it would do is a `len()` over every file"
);
}
#[test]
fn test_active_query_settings_only_one_set() {
use super::active_query_settings;
let (q, sql, fuzzy) = active_query_settings("", "", "");
assert!(q.is_none() && sql.is_none() && fuzzy.is_none());
let (q, sql, fuzzy) = active_query_settings("select a", "SELECT 1", "foo");
assert!(q.is_none() && sql.as_deref() == Some("SELECT 1") && fuzzy.is_none());
let (q, sql, fuzzy) = active_query_settings("select a", "", "foo bar");
assert!(q.is_none() && sql.is_none() && fuzzy.as_deref() == Some("foo bar"));
let (q, sql, fuzzy) = active_query_settings(" select a ", "", "");
assert!(q.as_deref() == Some("select a") && sql.is_none() && fuzzy.is_none());
}
mod export_format_tests;
#[cfg(test)]
mod table_sample_tests;
mod quality_memory_tests;
mod probe_slot_tests;
#[cfg(feature = "cloud")]
mod cloud_row_tests;
mod home_worker_panic_tests;
mod classify_batch_tests;
mod quality_sample_tests;
mod chart_prepare_tests;
mod chart_golden_tests;
mod view_rollback_tests;
#[cfg(feature = "sql")]
mod view_matching_tests;
#[cfg(feature = "cloud")]
mod peek_answer_tests;
#[cfg(feature = "cloud")]
mod cloud_csv_prefix_tests;
mod feedback_ladder_tests;
mod sort_filter_sync_tests;
mod background_read_tests;
mod read_mode_tests;
mod spec_source_tests;
mod file_facts_tests;
#[cfg(unix)]
mod startup_reads_tests;
mod inspector_tests;
mod inspector_layout_tests;
#[cfg(feature = "sql")]
mod doc_queries_tests;
mod codebook_tests;
mod csv_inference_tests;
mod text_input_flows;
mod confirm_tests;