1use crate::data_quality::QualityScope;
10use crate::sampling::{Sample, SampleMethod};
11use crate::widgets::text_input::TextInput;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum RowsKind {
16 All,
18 Source,
21 Partitions,
22 Files,
23 Range,
24 Time,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SampleField {
30 Rows,
31 PartitionColumn,
32 PartitionValues,
33 Files,
34 RangeFrom,
35 RangeTo,
36 TimeColumn,
37 TimeFrom,
38 TimeBefore,
39 Method,
40 By,
41 Size,
42 Seed,
43}
44
45impl SampleField {
46 pub fn label(self) -> &'static str {
47 match self {
48 Self::Rows => "Rows from:",
49 Self::PartitionColumn => "Partition:",
50 Self::PartitionValues => "Values:",
51 Self::Files => "Files:",
52 Self::RangeFrom => "From row:",
53 Self::RangeTo => "To row:",
54 Self::TimeColumn => "Column:",
55 Self::TimeFrom => "From:",
56 Self::TimeBefore => "Before:",
57 Self::Method => "Method:",
58 Self::By => "Per value of:",
59 Self::Size => "Sample size:",
60 Self::Seed => "Random seed:",
61 }
62 }
63
64 pub fn is_text(self) -> bool {
66 matches!(
67 self,
68 Self::PartitionValues
69 | Self::Files
70 | Self::RangeFrom
71 | Self::RangeTo
72 | Self::TimeFrom
73 | Self::TimeBefore
74 | Self::Size
75 | Self::Seed
76 )
77 }
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct SampleContext {
83 pub view_rows: Option<usize>,
85 pub filtered: bool,
87 pub files: Vec<String>,
89 pub partition_columns: Vec<String>,
90 pub partition_values: Vec<(String, Vec<String>)>,
93 pub time_columns: Vec<String>,
94 pub value_columns: Vec<String>,
96}
97
98pub struct SampleForm {
99 pub inline: bool,
102 pub view: bool,
105 pub bytes_per_row: Option<usize>,
108 pub source_bytes_per_row: Option<usize>,
111 pub anyway: bool,
114 pub draft: Sample,
115 pub kind: RowsKind,
116 pub field: SampleField,
117 pub context: SampleContext,
118 pub partition_column: usize,
119 pub partition_values: TextInput,
120 pub files: TextInput,
121 pub range_from: TextInput,
122 pub range_to: TextInput,
123 pub time_column: usize,
124 pub time_from: TextInput,
125 pub time_before: TextInput,
126 pub size: TextInput,
128 pub seed: TextInput,
131 pub error: Option<String>,
132 pub file_offset: usize,
134}
135
136impl SampleForm {
137 pub fn new(sample: &Sample, context: SampleContext, theme: &crate::config::Theme) -> Self {
138 let input = || TextInput::new().with_theme(theme);
139 let mut form = Self {
140 inline: false,
141 view: false,
142 bytes_per_row: None,
143 source_bytes_per_row: None,
144 anyway: false,
145 draft: sample.clone(),
146 kind: RowsKind::All,
147 field: SampleField::Rows,
148 context,
149 partition_column: 0,
150 partition_values: input(),
151 files: input(),
152 range_from: input(),
153 range_to: input(),
154 time_column: 0,
155 time_from: input(),
156 time_before: input(),
157 size: input(),
158 seed: input(),
159 error: None,
160 file_offset: 0,
161 };
162 form.set_scope(&sample.scope);
163 form.seed.suggest(sample.seed.to_string());
166 form.size.suggest(crate::numfmt::group_chrome(sample.rows));
168 form.sync_focus(true);
169 form
170 }
171
172 pub fn set_scope(&mut self, scope: &QualityScope) {
174 self.kind = match scope {
175 QualityScope::CurrentView => RowsKind::All,
176 QualityScope::WholeSource => RowsKind::Source,
177 QualityScope::FirstRows(end) => {
178 self.range_from.set_value("1");
179 self.range_to.set_value(end.to_string());
180 RowsKind::Range
181 }
182 QualityScope::ViewRows { start, end } => {
183 self.range_from.set_value(start.to_string());
184 self.range_to.set_value(end.to_string());
185 RowsKind::Range
186 }
187 QualityScope::SourceFiles(files) => {
188 self.files.set_value(
189 files
190 .iter()
191 .map(usize::to_string)
192 .collect::<Vec<_>>()
193 .join(","),
194 );
195 RowsKind::Files
196 }
197 QualityScope::SourcePartition { column, value } => {
198 if !self.context.partition_columns.contains(column) {
199 self.context.partition_columns.insert(0, column.clone());
200 }
201 self.partition_column = self
202 .context
203 .partition_columns
204 .iter()
205 .position(|name| name == column)
206 .unwrap_or(0);
207 self.partition_values.set_value(value);
208 RowsKind::Partitions
209 }
210 QualityScope::SourceTimeRange { column, start, end } => {
211 if !self.context.time_columns.contains(column) {
212 self.context.time_columns.insert(0, column.clone());
213 }
214 self.time_column = self
215 .context
216 .time_columns
217 .iter()
218 .position(|name| name == column)
219 .unwrap_or(0);
220 self.time_from.set_value(start);
221 self.time_before.set_value(end);
222 RowsKind::Time
223 }
224 };
225 }
226
227 pub fn kinds(&self) -> Vec<RowsKind> {
229 let mut kinds = vec![RowsKind::All];
230 if self.context.filtered {
231 kinds.push(RowsKind::Source);
232 }
233 if !self.context.partition_columns.is_empty() {
234 kinds.push(RowsKind::Partitions);
235 }
236 if self.context.files.len() > 1 {
237 kinds.push(RowsKind::Files);
238 }
239 kinds.push(RowsKind::Range);
240 if !self.context.time_columns.is_empty() {
241 kinds.push(RowsKind::Time);
242 }
243 if !kinds.contains(&self.kind) {
244 kinds.push(self.kind);
245 }
246 kinds
247 }
248
249 pub fn fields(&self) -> Vec<SampleField> {
251 if self.no_sample() {
253 return vec![SampleField::Method];
254 }
255 let mut fields = vec![SampleField::Rows];
256 fields.extend(match self.kind {
257 RowsKind::All | RowsKind::Source => vec![],
258 RowsKind::Partitions => {
259 vec![SampleField::PartitionColumn, SampleField::PartitionValues]
260 }
261 RowsKind::Files => vec![SampleField::Files],
262 RowsKind::Range => vec![SampleField::RangeFrom, SampleField::RangeTo],
263 RowsKind::Time => vec![
264 SampleField::TimeColumn,
265 SampleField::TimeFrom,
266 SampleField::TimeBefore,
267 ],
268 });
269 fields.push(SampleField::Method);
270 if matches!(self.draft.method, SampleMethod::PerPartition { .. }) {
271 fields.push(SampleField::By);
272 }
273 if self.draft.method != SampleMethod::EveryRow {
274 fields.push(SampleField::Size);
275 }
276 if matches!(
277 self.draft.method,
278 SampleMethod::Spread | SampleMethod::PerPartition { .. }
279 ) {
280 fields.push(SampleField::Seed);
281 }
282 fields
283 }
284
285 pub fn input(&self, field: SampleField) -> Option<&TextInput> {
286 Some(match field {
287 SampleField::PartitionValues => &self.partition_values,
288 SampleField::Files => &self.files,
289 SampleField::RangeFrom => &self.range_from,
290 SampleField::RangeTo => &self.range_to,
291 SampleField::TimeFrom => &self.time_from,
292 SampleField::TimeBefore => &self.time_before,
293 SampleField::Size => &self.size,
294 SampleField::Seed => &self.seed,
295 _ => return None,
296 })
297 }
298
299 pub fn input_mut(&mut self, field: SampleField) -> Option<&mut TextInput> {
300 Some(match field {
301 SampleField::PartitionValues => &mut self.partition_values,
302 SampleField::Files => &mut self.files,
303 SampleField::RangeFrom => &mut self.range_from,
304 SampleField::RangeTo => &mut self.range_to,
305 SampleField::TimeFrom => &mut self.time_from,
306 SampleField::TimeBefore => &mut self.time_before,
307 SampleField::Size => &mut self.size,
308 SampleField::Seed => &mut self.seed,
309 _ => return None,
310 })
311 }
312
313 pub fn sync_focus(&mut self, form_focused: bool) {
316 let field = self.field;
317 for candidate in [
318 SampleField::PartitionValues,
319 SampleField::Files,
320 SampleField::RangeFrom,
321 SampleField::RangeTo,
322 SampleField::TimeFrom,
323 SampleField::TimeBefore,
324 SampleField::Size,
325 SampleField::Seed,
326 ] {
327 if let Some(input) = self.input_mut(candidate) {
328 input.set_focused(form_focused && candidate == field);
329 }
330 }
331 }
332
333 pub fn adjust(&mut self, forward: bool) {
335 let step = |len: usize, at: usize| {
336 if forward {
337 (at + 1) % len
338 } else {
339 (at + len - 1) % len
340 }
341 };
342 match self.field {
343 SampleField::Rows => {
344 let kinds = self.kinds();
345 let at = kinds
346 .iter()
347 .position(|kind| *kind == self.kind)
348 .unwrap_or(0);
349 self.kind = kinds[step(kinds.len(), at)];
350 if self.kind == RowsKind::Range && self.range_to.value().is_empty() {
352 self.range_from.suggest("1");
353 if let Some(rows) = self.context.view_rows {
354 self.range_to.suggest(rows.to_string());
355 }
356 }
357 }
358 SampleField::PartitionColumn if !self.context.partition_columns.is_empty() => {
359 self.partition_column =
360 step(self.context.partition_columns.len(), self.partition_column);
361 }
362 SampleField::TimeColumn if !self.context.time_columns.is_empty() => {
363 self.time_column = step(self.context.time_columns.len(), self.time_column);
364 }
365 SampleField::Method => {
366 let by = match &self.draft.method {
367 SampleMethod::PerPartition { column } => Some(column.clone()),
368 _ => None,
369 }
370 .or_else(|| self.context.value_columns.first().cloned());
371 let mut methods = vec![SampleMethod::Spread];
372 if let Some(column) = by {
373 methods.push(SampleMethod::PerPartition { column });
374 }
375 methods.push(SampleMethod::FirstRows);
376 methods.push(SampleMethod::EveryRow);
377 let at = methods
378 .iter()
379 .position(|method| {
380 std::mem::discriminant(method) == std::mem::discriminant(&self.draft.method)
381 })
382 .unwrap_or(0);
383 self.draft.method = methods[step(methods.len(), at)].clone();
384 if matches!(self.draft.method, SampleMethod::PerPartition { .. })
387 && self.typed_size().unwrap_or(self.draft.rows) > PER_VALUE_ROWS
388 {
389 self.draft.rows = PER_VALUE_ROWS;
390 self.size
391 .suggest(crate::numfmt::group_chrome(PER_VALUE_ROWS));
392 }
393 }
394 SampleField::By => {
395 let columns = &self.context.value_columns;
396 if let SampleMethod::PerPartition { column } = &self.draft.method
397 && !columns.is_empty()
398 {
399 let at = columns.iter().position(|name| name == column).unwrap_or(0);
400 self.draft.method = SampleMethod::PerPartition {
401 column: columns[step(columns.len(), at)].clone(),
402 };
403 }
404 }
405 _ => {}
406 }
407 }
408
409 pub fn choice(&self, field: SampleField) -> String {
411 match field {
412 SampleField::Rows => match self.kind {
413 RowsKind::All => {
414 let count = self
415 .context
416 .view_rows
417 .map(|rows| format!(" ({})", crate::discover::format_rows(rows)))
418 .unwrap_or_default();
419 if self.context.filtered {
420 format!("All rows as filtered{count}")
421 } else {
422 format!("All rows{count}")
423 }
424 }
425 RowsKind::Source => "The source, unfiltered".to_string(),
426 RowsKind::Partitions => "Partitions".to_string(),
427 RowsKind::Files => "Files".to_string(),
428 RowsKind::Range => "Row range".to_string(),
429 RowsKind::Time => "Time range".to_string(),
430 },
431 SampleField::PartitionColumn => self
432 .context
433 .partition_columns
434 .get(self.partition_column)
435 .cloned()
436 .unwrap_or_default(),
437 SampleField::TimeColumn => self
438 .context
439 .time_columns
440 .get(self.time_column)
441 .cloned()
442 .unwrap_or_default(),
443 SampleField::Method => match &self.draft.method {
444 SampleMethod::Spread => "Random".to_string(),
445 SampleMethod::PerPartition { .. } => "Equal per value".to_string(),
446 SampleMethod::FirstRows => "First rows".to_string(),
447 SampleMethod::EveryRow if self.view => "No sample".to_string(),
448 SampleMethod::EveryRow => "Every row".to_string(),
449 },
450 SampleField::By => match &self.draft.method {
451 SampleMethod::PerPartition { column } => column.clone(),
452 _ => String::new(),
453 },
454 SampleField::Seed => self.draft.seed.to_string(),
455 _ => String::new(),
456 }
457 }
458
459 pub fn partition_values_known(&self) -> Vec<String> {
462 let Some(column) = self.context.partition_columns.get(self.partition_column) else {
463 return Vec::new();
464 };
465 if let Some((_, values)) = self
466 .context
467 .partition_values
468 .iter()
469 .find(|(name, _)| name == column)
470 {
471 return values.clone();
472 }
473 let prefix = format!("{column}=");
474 let mut values: Vec<String> = Vec::new();
475 for file in &self.context.files {
476 for segment in file.split(['/', '\\']) {
477 if let Some(value) = segment.strip_prefix(&prefix)
478 && !values.iter().any(|known| known == value)
479 {
480 values.push(value.to_string());
481 }
482 }
483 }
484 values
485 }
486
487 fn check_partition_values(&self, column: &str, values: &str) -> Result<(), String> {
492 let known = self.partition_values_known();
493 if known.is_empty() {
494 return Ok(());
495 }
496 let holds = match known.as_slice() {
497 [only] => only.clone(),
498 [first, .., last] => format!("{first} {} {last}", crate::glyphs::get().ellipsis),
499 [] => String::new(),
500 };
501 if let Some((start, end)) = values.split_once("..") {
502 let numeric = known.iter().all(|value| value.parse::<f64>().is_ok());
503 let dated = known
504 .iter()
505 .all(|value| chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok());
506 let fits = |end: &str| {
507 let end = end.trim();
508 !end.is_empty()
509 && (!numeric || end.parse::<f64>().is_ok())
510 && (!dated || chrono::NaiveDate::parse_from_str(end, "%Y-%m-%d").is_ok())
511 };
512 if !fits(start) || !fits(end) {
513 return Err(format!(
514 "A {column} range runs between two of its values, like {}..{}; it holds {holds}",
515 known[0],
516 known[known.len() - 1]
517 ));
518 }
519 return Ok(());
520 }
521 for value in values.split(',').map(str::trim).filter(|v| !v.is_empty()) {
522 if value != "∅" && !known.iter().any(|known| known == value) {
523 return Err(format!(
524 "{column} has no partition {value:?}; it holds {holds}"
525 ));
526 }
527 }
528 Ok(())
529 }
530
531 pub fn no_sample(&self) -> bool {
533 self.view && self.draft.method == SampleMethod::EveryRow
534 }
535
536 pub fn rows_expected(&self) -> Option<usize> {
540 let asked = match self.draft.method {
541 SampleMethod::Spread | SampleMethod::FirstRows => self.typed_size()?,
542 SampleMethod::EveryRow => self.context.view_rows?,
543 SampleMethod::PerPartition { .. } => return None,
544 };
545 let there = match self.kind {
546 RowsKind::All => self.context.view_rows,
547 _ => None,
548 };
549 Some(there.map_or(asked, |rows| asked.min(rows)))
550 }
551
552 pub fn estimate(&self) -> Option<u64> {
554 let rows = self.rows_expected()? as u64;
555 let per_row = match self.kind {
556 RowsKind::All | RowsKind::Range => self.bytes_per_row?,
557 _ => self.source_bytes_per_row?,
558 };
559 Some(rows.saturating_mul(per_row as u64))
560 }
561
562 pub fn edited(&mut self) {
564 self.error = None;
565 self.anyway = false;
566 }
567
568 fn typed_size(&self) -> Option<usize> {
570 crate::sampling::parse_size(self.size.value()).ok()
571 }
572
573 pub fn files_chosen(&self) -> Vec<usize> {
575 self.files
576 .value()
577 .split(',')
578 .filter_map(|part| part.trim().parse().ok())
579 .collect()
580 }
581
582 pub fn finish(&self) -> Result<Sample, String> {
584 let scope = match self.kind {
585 RowsKind::All => QualityScope::CurrentView,
586 RowsKind::Source => QualityScope::WholeSource,
587 RowsKind::Range => {
588 let number = |input: &TextInput| {
589 input
590 .value()
591 .replace([',', '_', ' '], "")
592 .parse::<usize>()
593 .ok()
594 };
595 match (number(&self.range_from), number(&self.range_to)) {
596 (Some(from), Some(to)) if from >= 1 && to >= from => {
597 QualityScope::parse_command(&format!("rows {from}..{to}"))
598 .map_err(|e| e.to_string())?
599 }
600 _ => {
601 return Err(
602 "From row and To row are row numbers, From at least 1 and not past To"
603 .to_string(),
604 );
605 }
606 }
607 }
608 RowsKind::Files => {
609 let chosen = self.files_chosen();
610 let count = self.context.files.len();
611 if chosen.is_empty() {
612 return Err("Type the numbers of the files to read, like 1,3".to_string());
613 }
614 if let Some(file) = chosen.iter().find(|file| **file == 0 || **file > count) {
615 return Err(format!(
616 "There is no file {file}; the files are 1 to {count}"
617 ));
618 }
619 QualityScope::parse_command(&format!(
620 "files {}",
621 chosen
622 .iter()
623 .map(usize::to_string)
624 .collect::<Vec<_>>()
625 .join(",")
626 ))
627 .map_err(|e| e.to_string())?
628 }
629 RowsKind::Partitions => {
630 let column = self.choice(SampleField::PartitionColumn);
631 let values = self.partition_values.value().trim();
632 if values.is_empty() {
633 return Err(format!("Type the {column} values to read"));
634 }
635 self.check_partition_values(&column, values)?;
636 QualityScope::parse_command(&format!("partition {column}={values}"))
637 .map_err(|e| e.to_string())?
638 }
639 RowsKind::Time => {
640 let column = self.choice(SampleField::TimeColumn);
641 let (from, before) = (
642 self.time_from.value().trim(),
643 self.time_before.value().trim(),
644 );
645 QualityScope::parse_command(&format!("time {column}={from}..{before}")).map_err(
646 |_| {
647 "From and Before are dates (2024-01-31) or timestamps, From first"
648 .to_string()
649 },
650 )?
651 }
652 };
653 let mut sample = Sample {
654 scope,
655 ..self.draft.clone()
656 };
657 if self.fields().contains(&SampleField::Size) {
658 sample.rows = crate::sampling::parse_size(self.size.value())
659 .map_err(|e| e.to_string())?
660 .min(u32::MAX as usize);
661 }
662 if self.fields().contains(&SampleField::Seed) {
663 sample.seed = self
664 .seed
665 .value()
666 .replace([',', '_', ' '], "")
667 .parse()
668 .map_err(|_| "Random seed is a whole number, like 0 or 42".to_string())?;
669 }
670 Ok(sample)
671 }
672}
673
674const PER_VALUE_ROWS: usize = 1_000;
676
677pub fn new_seed() -> u64 {
680 std::time::SystemTime::now()
681 .duration_since(std::time::UNIX_EPOCH)
682 .unwrap_or_default()
683 .as_nanos() as u64
684 % 1_000_000
685}
686
687impl crate::form::Form for SampleForm {
688 type Field = SampleField;
689
690 fn fields(&self) -> Vec<(SampleField, crate::form::FieldKind)> {
691 use crate::form::FieldKind;
692 SampleForm::fields(self)
693 .into_iter()
694 .map(|field| {
695 let kind = if field.is_text() {
696 FieldKind::Text
697 } else {
698 FieldKind::Choice
699 };
700 (field, kind)
701 })
702 .collect()
703 }
704
705 fn focused(&self) -> SampleField {
706 self.field
707 }
708
709 fn set_focused(&mut self, field: SampleField) {
710 self.field = field;
711 if field == SampleField::Seed {
714 self.seed.select_all();
715 }
716 if field == SampleField::Size {
717 self.size.select_all();
718 }
719 self.sync_focus(true);
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726
727 fn form() -> SampleForm {
728 SampleForm::new(
729 &Sample::default(),
730 SampleContext {
731 view_rows: Some(36_800_000),
732 filtered: false,
733 files: (2016..2019)
734 .map(|year| format!("/data/weather/year={year}/0.parquet"))
735 .collect(),
736 partition_columns: vec!["year".to_string()],
737 partition_values: Vec::new(),
738 time_columns: vec!["date".to_string()],
739 value_columns: vec!["station".to_string()],
740 },
741 &crate::config::Theme::from_config(&crate::config::ThemeConfig::default()).unwrap(),
742 )
743 }
744
745 #[test]
748 fn each_kind_of_rows_shows_only_its_inputs() {
749 let mut form = form();
750 assert_eq!(form.choice(SampleField::Rows), "All rows (36.8M)");
751 assert_eq!(form.fields()[1], SampleField::Method);
752 form.adjust(true);
753 assert_eq!(form.kind, RowsKind::Partitions);
754 assert_eq!(
755 form.fields()[1..3],
756 [SampleField::PartitionColumn, SampleField::PartitionValues]
757 );
758 assert_eq!(form.partition_values_known(), vec!["2016", "2017", "2018"]);
759 form.adjust(true);
760 assert_eq!(form.kind, RowsKind::Files);
761 form.adjust(true);
762 assert_eq!(form.kind, RowsKind::Range);
763 assert_eq!(
764 (form.range_from.value(), form.range_to.value()),
765 ("1", "36800000"),
766 "a range starts as the whole table"
767 );
768 form.adjust(false);
769 assert_eq!(form.kind, RowsKind::Files, "Left undoes Right");
770 assert!(
771 !form.kinds().contains(&RowsKind::Source),
772 "unfiltered is the same as all"
773 );
774 }
775
776 #[test]
777 fn method_steps_both_ways_and_rows_appear_with_it() {
778 let mut form = form();
779 form.field = SampleField::Method;
780 form.adjust(true);
781 assert_eq!(
782 form.draft.method,
783 SampleMethod::PerPartition {
784 column: "station".to_string()
785 }
786 );
787 assert_eq!(form.size.value(), "1,000");
788 assert!(form.fields().contains(&SampleField::By));
789 form.adjust(true);
790 assert_eq!(form.draft.method, SampleMethod::FirstRows);
791 assert!(
792 !form.fields().contains(&SampleField::Seed),
793 "the head has no seed"
794 );
795 form.adjust(true);
796 assert_eq!(form.draft.method, SampleMethod::EveryRow);
797 assert!(!form.fields().contains(&SampleField::Size));
798 form.adjust(false);
799 assert_eq!(form.draft.method, SampleMethod::FirstRows);
800 }
801
802 #[test]
805 fn the_files_row_keeps_its_paging_keys() {
806 use crate::form::{Form, FormKey};
807 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
808 let mut form = form();
809 while form.kind != RowsKind::Files {
810 form.adjust(true);
811 }
812 assert!(form.focus(SampleField::Files));
813 for code in [KeyCode::PageDown, KeyCode::PageUp] {
814 assert_eq!(
815 crate::form::key(&mut form, &KeyEvent::new(code, KeyModifiers::NONE)),
816 FormKey::Text(SampleField::Files)
817 );
818 }
819 }
820
821 #[test]
824 fn the_size_is_typed() {
825 let mut form = form();
826 assert_eq!(form.size.value(), "100,000");
827 while form.field != SampleField::Size {
828 crate::form::Form::focus_next(&mut form);
829 }
830 let key = |c| {
831 crossterm::event::KeyEvent::new(
832 crossterm::event::KeyCode::Char(c),
833 crossterm::event::KeyModifiers::NONE,
834 )
835 };
836 for c in "250k".chars() {
837 form.size.handle_key(&key(c), None);
838 }
839 assert_eq!(form.size.value(), "250k", "typing replaces the size");
840 assert_eq!(form.finish().unwrap().rows, 250_000);
841 form.size.set_value("2M");
842 assert_eq!(form.finish().unwrap().rows, 2_000_000);
843 form.size.set_value("lots");
844 assert!(form.finish().unwrap_err().contains("50k"));
845 form.size.set_value("0");
846 assert!(form.finish().is_err());
847 form.field = SampleField::Method;
849 while form.draft.method != SampleMethod::EveryRow {
850 form.adjust(true);
851 }
852 assert!(form.finish().is_ok());
853 }
854
855 #[test]
858 fn the_seed_is_typed() {
859 let mut form = form();
860 while form.field != SampleField::Seed {
861 crate::form::Form::focus_next(&mut form);
862 }
863 assert!(form.field.is_text());
864 let zero = crossterm::event::KeyEvent::new(
865 crossterm::event::KeyCode::Char('0'),
866 crossterm::event::KeyModifiers::NONE,
867 );
868 form.seed.handle_key(&zero, None);
869 assert_eq!(form.finish().unwrap().seed, 0);
870 form.seed.set_value("seven");
871 assert!(form.finish().unwrap_err().contains("whole number"));
872 }
873
874 #[test]
875 fn the_rows_described_become_a_scope() {
876 let mut form = form();
877 form.set_scope(&QualityScope::parse_command("partition year=2016..2017").unwrap());
878 assert_eq!(form.kind, RowsKind::Partitions);
879 assert_eq!(
880 form.finish().unwrap().scope,
881 QualityScope::SourcePartition {
882 column: "year".to_string(),
883 value: "2016..2017".to_string()
884 }
885 );
886 form.kind = RowsKind::Range;
887 form.range_from.set_value("2");
888 form.range_to.set_value("5,000");
889 assert_eq!(
890 form.finish().unwrap().scope,
891 QualityScope::ViewRows {
892 start: 2,
893 end: 5_000
894 }
895 );
896 form.kind = RowsKind::Files;
897 form.files.set_value("1, 4");
898 assert!(form.finish().unwrap_err().contains("no file 4"));
899 }
900
901 #[test]
903 fn partition_values_the_source_does_not_hold_are_refused() {
904 let mut form = form();
905 form.kind = RowsKind::Partitions;
906 for garbage in ["asdf", "2016,20x7", "a..b", "2016.."] {
907 form.partition_values.set_value(garbage);
908 let error = form.finish().unwrap_err();
909 assert!(error.contains("2016"), "{garbage}: {error}");
910 }
911 for fine in ["2017", "2016,2018", "2015..2030"] {
912 form.partition_values.set_value(fine);
913 assert!(form.finish().is_ok(), "{fine}");
914 }
915 }
916}