1use crate::analysis::data_quality::QualityScope;
10use crate::analysis::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::home::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::EveryRow if self.view => "No sample".to_string(),
445 method => method.name().to_string(),
446 },
447 SampleField::By => match &self.draft.method {
448 SampleMethod::PerPartition { column } => column.clone(),
449 _ => String::new(),
450 },
451 SampleField::Seed => self.draft.seed.to_string(),
452 _ => String::new(),
453 }
454 }
455
456 pub fn partition_values_known(&self) -> Vec<String> {
459 let Some(column) = self.context.partition_columns.get(self.partition_column) else {
460 return Vec::new();
461 };
462 if let Some((_, values)) = self
463 .context
464 .partition_values
465 .iter()
466 .find(|(name, _)| name == column)
467 {
468 return values.clone();
469 }
470 let prefix = format!("{column}=");
471 let mut values: Vec<String> = Vec::new();
472 for file in &self.context.files {
473 for segment in file.split(['/', '\\']) {
474 if let Some(value) = segment.strip_prefix(&prefix)
475 && !values.iter().any(|known| known == value)
476 {
477 values.push(value.to_string());
478 }
479 }
480 }
481 values
482 }
483
484 fn check_partition_values(&self, column: &str, values: &str) -> Result<(), String> {
489 let known = self.partition_values_known();
490 if known.is_empty() {
491 return Ok(());
492 }
493 let holds = match known.as_slice() {
494 [only] => only.clone(),
495 [first, .., last] => format!("{first} {} {last}", crate::glyphs::get().ellipsis),
496 [] => String::new(),
497 };
498 if let Some((start, end)) = values.split_once("..") {
499 let numeric = known.iter().all(|value| value.parse::<f64>().is_ok());
500 let dated = known
501 .iter()
502 .all(|value| chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok());
503 let fits = |end: &str| {
504 let end = end.trim();
505 !end.is_empty()
506 && (!numeric || end.parse::<f64>().is_ok())
507 && (!dated || chrono::NaiveDate::parse_from_str(end, "%Y-%m-%d").is_ok())
508 };
509 if !fits(start) || !fits(end) {
510 return Err(format!(
511 "A {column} range runs between two of its values, like {}..{}; it holds {holds}",
512 known[0],
513 known[known.len() - 1]
514 ));
515 }
516 return Ok(());
517 }
518 for value in values.split(',').map(str::trim).filter(|v| !v.is_empty()) {
519 if value != "∅" && !known.iter().any(|known| known == value) {
520 return Err(format!(
521 "{column} has no partition {value:?}; it holds {holds}"
522 ));
523 }
524 }
525 Ok(())
526 }
527
528 pub fn no_sample(&self) -> bool {
530 self.view && self.draft.method == SampleMethod::EveryRow
531 }
532
533 pub fn rows_expected(&self) -> Option<usize> {
537 let asked = match self.draft.method {
538 SampleMethod::Spread | SampleMethod::FirstRows => self.typed_size()?,
539 SampleMethod::EveryRow => self.context.view_rows?,
540 SampleMethod::PerPartition { .. } => return None,
541 };
542 let there = match self.kind {
543 RowsKind::All => self.context.view_rows,
544 _ => None,
545 };
546 Some(there.map_or(asked, |rows| asked.min(rows)))
547 }
548
549 pub fn estimate(&self) -> Option<u64> {
551 let rows = self.rows_expected()? as u64;
552 let per_row = match self.kind {
553 RowsKind::All | RowsKind::Range => self.bytes_per_row?,
554 _ => self.source_bytes_per_row?,
555 };
556 Some(rows.saturating_mul(per_row as u64))
557 }
558
559 pub fn edited(&mut self) {
561 self.error = None;
562 self.anyway = false;
563 }
564
565 fn typed_size(&self) -> Option<usize> {
567 crate::analysis::sampling::parse_size(self.size.value()).ok()
568 }
569
570 pub fn files_chosen(&self) -> Vec<usize> {
572 self.files
573 .value()
574 .split(',')
575 .filter_map(|part| part.trim().parse().ok())
576 .collect()
577 }
578
579 pub fn finish(&self) -> Result<Sample, String> {
581 let scope = match self.kind {
582 RowsKind::All => QualityScope::CurrentView,
583 RowsKind::Source => QualityScope::WholeSource,
584 RowsKind::Range => {
585 let number = |input: &TextInput| {
586 input
587 .value()
588 .replace([',', '_', ' '], "")
589 .parse::<usize>()
590 .ok()
591 };
592 match (number(&self.range_from), number(&self.range_to)) {
593 (Some(from), Some(to)) if from >= 1 && to >= from => {
594 QualityScope::parse_command(&format!("rows {from}..{to}"))
595 .map_err(|e| e.to_string())?
596 }
597 _ => {
598 return Err(
599 "From row and To row are row numbers, From at least 1 and not past To"
600 .to_string(),
601 );
602 }
603 }
604 }
605 RowsKind::Files => {
606 let chosen = self.files_chosen();
607 let count = self.context.files.len();
608 if chosen.is_empty() {
609 return Err("Type the numbers of the files to read, like 1,3".to_string());
610 }
611 if let Some(file) = chosen.iter().find(|file| **file == 0 || **file > count) {
612 return Err(format!(
613 "There is no file {file}; the files are 1 to {count}"
614 ));
615 }
616 QualityScope::parse_command(&format!(
617 "files {}",
618 chosen
619 .iter()
620 .map(usize::to_string)
621 .collect::<Vec<_>>()
622 .join(",")
623 ))
624 .map_err(|e| e.to_string())?
625 }
626 RowsKind::Partitions => {
627 let column = self.choice(SampleField::PartitionColumn);
628 let values = self.partition_values.value().trim();
629 if values.is_empty() {
630 return Err(format!("Type the {column} values to read"));
631 }
632 self.check_partition_values(&column, values)?;
633 QualityScope::parse_command(&format!("partition {column}={values}"))
634 .map_err(|e| e.to_string())?
635 }
636 RowsKind::Time => {
637 let column = self.choice(SampleField::TimeColumn);
638 let (from, before) = (
639 self.time_from.value().trim(),
640 self.time_before.value().trim(),
641 );
642 QualityScope::parse_command(&format!("time {column}={from}..{before}")).map_err(
643 |_| {
644 "From and Before are dates (2024-01-31) or timestamps, From first"
645 .to_string()
646 },
647 )?
648 }
649 };
650 let mut sample = Sample {
651 scope,
652 ..self.draft.clone()
653 };
654 if self.fields().contains(&SampleField::Size) {
655 sample.rows = crate::analysis::sampling::parse_size(self.size.value())
656 .map_err(|e| e.to_string())?
657 .min(u32::MAX as usize);
658 }
659 if self.fields().contains(&SampleField::Seed) {
660 sample.seed = self
661 .seed
662 .value()
663 .replace([',', '_', ' '], "")
664 .parse()
665 .map_err(|_| "Random seed is a whole number, like 0 or 42".to_string())?;
666 }
667 Ok(sample)
668 }
669}
670
671const PER_VALUE_ROWS: usize = 1_000;
673
674pub fn new_seed() -> u64 {
677 std::time::SystemTime::now()
678 .duration_since(std::time::UNIX_EPOCH)
679 .unwrap_or_default()
680 .as_nanos() as u64
681 % 1_000_000
682}
683
684impl crate::app::form::Form for SampleForm {
685 type Field = SampleField;
686
687 fn fields(&self) -> Vec<(SampleField, crate::app::form::FieldKind)> {
688 use crate::app::form::FieldKind;
689 SampleForm::fields(self)
690 .into_iter()
691 .map(|field| {
692 let kind = if field.is_text() {
693 FieldKind::Text
694 } else {
695 FieldKind::Choice
696 };
697 (field, kind)
698 })
699 .collect()
700 }
701
702 fn focused(&self) -> SampleField {
703 self.field
704 }
705
706 fn set_focused(&mut self, field: SampleField) {
707 self.field = field;
708 if field == SampleField::Seed {
711 self.seed.select_all();
712 }
713 if field == SampleField::Size {
714 self.size.select_all();
715 }
716 self.sync_focus(true);
717 }
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 fn form() -> SampleForm {
725 SampleForm::new(
726 &Sample::default(),
727 SampleContext {
728 view_rows: Some(36_800_000),
729 filtered: false,
730 files: (2016..2019)
731 .map(|year| format!("/data/weather/year={year}/0.parquet"))
732 .collect(),
733 partition_columns: vec!["year".to_string()],
734 partition_values: Vec::new(),
735 time_columns: vec!["date".to_string()],
736 value_columns: vec!["station".to_string()],
737 },
738 &crate::config::Theme::from_config(&crate::config::ThemeConfig::default()).unwrap(),
739 )
740 }
741
742 #[test]
745 fn each_kind_of_rows_shows_only_its_inputs() {
746 let mut form = form();
747 assert_eq!(form.choice(SampleField::Rows), "All rows (36.8M)");
748 assert_eq!(form.fields()[1], SampleField::Method);
749 form.adjust(true);
750 assert_eq!(form.kind, RowsKind::Partitions);
751 assert_eq!(
752 form.fields()[1..3],
753 [SampleField::PartitionColumn, SampleField::PartitionValues]
754 );
755 assert_eq!(form.partition_values_known(), vec!["2016", "2017", "2018"]);
756 form.adjust(true);
757 assert_eq!(form.kind, RowsKind::Files);
758 form.adjust(true);
759 assert_eq!(form.kind, RowsKind::Range);
760 assert_eq!(
761 (form.range_from.value(), form.range_to.value()),
762 ("1", "36800000"),
763 "a range starts as the whole table"
764 );
765 form.adjust(false);
766 assert_eq!(form.kind, RowsKind::Files, "Left undoes Right");
767 assert!(
768 !form.kinds().contains(&RowsKind::Source),
769 "unfiltered is the same as all"
770 );
771 }
772
773 #[test]
774 fn method_steps_both_ways_and_rows_appear_with_it() {
775 let mut form = form();
776 form.field = SampleField::Method;
777 form.adjust(true);
778 assert_eq!(
779 form.draft.method,
780 SampleMethod::PerPartition {
781 column: "station".to_string()
782 }
783 );
784 assert_eq!(form.size.value(), "1,000");
785 assert!(form.fields().contains(&SampleField::By));
786 form.adjust(true);
787 assert_eq!(form.draft.method, SampleMethod::FirstRows);
788 assert!(
789 !form.fields().contains(&SampleField::Seed),
790 "the head has no seed"
791 );
792 form.adjust(true);
793 assert_eq!(form.draft.method, SampleMethod::EveryRow);
794 assert!(!form.fields().contains(&SampleField::Size));
795 form.adjust(false);
796 assert_eq!(form.draft.method, SampleMethod::FirstRows);
797 }
798
799 #[test]
802 fn the_files_row_keeps_its_paging_keys() {
803 use crate::app::form::{Form, FormKey};
804 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
805 let mut form = form();
806 while form.kind != RowsKind::Files {
807 form.adjust(true);
808 }
809 assert!(form.focus(SampleField::Files));
810 for code in [KeyCode::PageDown, KeyCode::PageUp] {
811 assert_eq!(
812 crate::app::form::key(&mut form, &KeyEvent::new(code, KeyModifiers::NONE)),
813 FormKey::Text(SampleField::Files)
814 );
815 }
816 }
817
818 #[test]
821 fn the_size_is_typed() {
822 let mut form = form();
823 assert_eq!(form.size.value(), "100,000");
824 while form.field != SampleField::Size {
825 crate::app::form::Form::focus_next(&mut form);
826 }
827 let key = |c| {
828 crossterm::event::KeyEvent::new(
829 crossterm::event::KeyCode::Char(c),
830 crossterm::event::KeyModifiers::NONE,
831 )
832 };
833 for c in "250k".chars() {
834 form.size.handle_key(&key(c), None);
835 }
836 assert_eq!(form.size.value(), "250k", "typing replaces the size");
837 assert_eq!(form.finish().unwrap().rows, 250_000);
838 form.size.set_value("2M");
839 assert_eq!(form.finish().unwrap().rows, 2_000_000);
840 form.size.set_value("lots");
841 assert!(form.finish().unwrap_err().contains("50k"));
842 form.size.set_value("0");
843 assert!(form.finish().is_err());
844 form.field = SampleField::Method;
846 while form.draft.method != SampleMethod::EveryRow {
847 form.adjust(true);
848 }
849 assert!(form.finish().is_ok());
850 }
851
852 #[test]
855 fn the_seed_is_typed() {
856 let mut form = form();
857 while form.field != SampleField::Seed {
858 crate::app::form::Form::focus_next(&mut form);
859 }
860 assert!(form.field.is_text());
861 let zero = crossterm::event::KeyEvent::new(
862 crossterm::event::KeyCode::Char('0'),
863 crossterm::event::KeyModifiers::NONE,
864 );
865 form.seed.handle_key(&zero, None);
866 assert_eq!(form.finish().unwrap().seed, 0);
867 form.seed.set_value("seven");
868 assert!(form.finish().unwrap_err().contains("whole number"));
869 }
870
871 #[test]
872 fn the_rows_described_become_a_scope() {
873 let mut form = form();
874 form.set_scope(&QualityScope::parse_command("partition year=2016..2017").unwrap());
875 assert_eq!(form.kind, RowsKind::Partitions);
876 assert_eq!(
877 form.finish().unwrap().scope,
878 QualityScope::SourcePartition {
879 column: "year".to_string(),
880 value: "2016..2017".to_string()
881 }
882 );
883 form.kind = RowsKind::Range;
884 form.range_from.set_value("2");
885 form.range_to.set_value("5,000");
886 assert_eq!(
887 form.finish().unwrap().scope,
888 QualityScope::ViewRows {
889 start: 2,
890 end: 5_000
891 }
892 );
893 form.kind = RowsKind::Files;
894 form.files.set_value("1, 4");
895 assert!(form.finish().unwrap_err().contains("no file 4"));
896 }
897
898 #[test]
900 fn partition_values_the_source_does_not_hold_are_refused() {
901 let mut form = form();
902 form.kind = RowsKind::Partitions;
903 for garbage in ["asdf", "2016,20x7", "a..b", "2016.."] {
904 form.partition_values.set_value(garbage);
905 let error = form.finish().unwrap_err();
906 assert!(error.contains("2016"), "{garbage}: {error}");
907 }
908 for fine in ["2017", "2016,2018", "2015..2030"] {
909 form.partition_values.set_value(fine);
910 assert!(form.finish().is_ok(), "{fine}");
911 }
912 }
913}