1mod commands;
31mod completer;
32mod prompt;
33mod session;
34
35use std::io::IsTerminal;
36
37pub use commands::{CommandParser, ReplCommand};
38pub use completer::SchemaAwareCompleter;
39pub use prompt::{AndonPrompt, HealthStatus};
40#[cfg(feature = "repl")]
41use reedline::{Reedline, Signal};
42pub use session::{DisplayConfig, ReplSession};
43
44use crate::Result;
45
46#[cfg(feature = "repl")]
52pub fn run() -> Result<()> {
53 if std::io::stdin().is_terminal() {
55 run_interactive()
56 } else {
57 run_non_interactive()
58 }
59}
60
61#[cfg(feature = "repl")]
63fn run_interactive() -> Result<()> {
64 let mut session = ReplSession::new();
65 let mut line_editor = create_editor(&session)?;
66 let prompt = AndonPrompt::new();
67
68 println!(
69 "alimentar {} - Interactive Data Explorer",
70 env!("CARGO_PKG_VERSION")
71 );
72 println!("Type 'help' for commands, 'quit' to exit\n");
73
74 loop {
75 let sig = line_editor.read_line(&prompt);
76 match sig {
77 Ok(Signal::Success(line)) => {
78 if dispatch_line(&line, &mut session, &line_editor) {
79 break;
80 }
81 }
82 Ok(Signal::CtrlC) => {
83 println!("^C");
84 }
85 Ok(Signal::CtrlD) => {
86 println!("\nGoodbye!");
87 break;
88 }
89 Err(e) => {
90 eprintln!("Input error: {e}");
91 break;
92 }
93 }
94 }
95
96 Ok(())
97}
98
99#[cfg(feature = "repl")]
101fn dispatch_line(line: &str, session: &mut ReplSession, editor: &Reedline) -> bool {
102 let trimmed = line.trim();
103 if trimmed.is_empty() {
104 return false;
105 }
106
107 session.add_history(trimmed);
108
109 match CommandParser::parse(trimmed) {
110 Ok(cmd) => {
111 if matches!(cmd, ReplCommand::Quit) {
112 println!("Goodbye!");
113 return true;
114 }
115 if let Err(e) = session.execute(cmd) {
116 eprintln!("Error: {e}");
117 }
118 }
119 Err(e) => eprintln!("Parse error: {e}"),
120 }
121
122 update_completer(editor, session);
123 false
124}
125
126#[cfg(feature = "repl")]
128#[allow(clippy::unnecessary_wraps)] fn run_non_interactive() -> Result<()> {
130 use std::io::BufRead;
131
132 let mut session = ReplSession::new();
133
134 println!(
135 "alimentar {} - Interactive Data Explorer",
136 env!("CARGO_PKG_VERSION")
137 );
138 println!("Type 'help' for commands, 'quit' to exit\n");
139
140 let stdin = std::io::stdin();
141 for line in stdin.lock().lines() {
142 let Ok(line) = line else {
143 println!("Goodbye!");
144 break;
145 };
146
147 let trimmed = line.trim();
148 if trimmed.is_empty() {
149 continue;
150 }
151
152 session.add_history(trimmed);
153
154 match CommandParser::parse(trimmed) {
155 Ok(cmd) => {
156 if matches!(cmd, ReplCommand::Quit) {
157 println!("Goodbye!");
158 break;
159 }
160 if let Err(e) = session.execute(cmd) {
161 eprintln!("Error: {e}");
162 }
163 }
164 Err(e) => eprintln!("Parse error: {e}"),
165 }
166 }
167
168 if std::io::stdin().lock().lines().next().is_none() {
170 println!("Goodbye!");
171 }
172
173 Ok(())
174}
175
176#[cfg(feature = "repl")]
177fn create_editor(session: &ReplSession) -> Result<Reedline> {
178 use reedline::{FileBackedHistory, Reedline};
179
180 let history_path = dirs_home().join(".alimentar_history");
181 let history = FileBackedHistory::with_file(1000, history_path)
182 .map_err(|e| crate::Error::io_no_path(std::io::Error::other(e.to_string())))?;
183
184 let completer = Box::new(SchemaAwareCompleter::new(session));
185
186 let editor = Reedline::create()
187 .with_history(Box::new(history))
188 .with_completer(completer);
189
190 Ok(editor)
191}
192
193#[cfg(feature = "repl")]
194fn update_completer(editor: &Reedline, session: &ReplSession) {
195 let completer = Box::new(SchemaAwareCompleter::new(session));
196 let _ = (editor, completer);
199}
200
201fn dirs_home() -> std::path::PathBuf {
202 std::env::var("HOME")
203 .map(std::path::PathBuf::from)
204 .unwrap_or_else(|_| std::path::PathBuf::from("."))
205}
206
207#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::ArrowDataset;
215
216 #[test]
221 fn test_session_creation() {
222 let session = ReplSession::new();
223 assert!(session.active_dataset().is_none());
224 assert!(session.datasets().is_empty());
225 assert!(session.history().is_empty());
226 }
227
228 #[test]
229 fn test_session_load_dataset() {
230 let mut session = ReplSession::new();
231
232 let dataset = create_test_dataset();
234 session.load_dataset("test", dataset);
235
236 assert!(session.datasets().contains(&"test".to_string()));
237 assert!(session.active_dataset().is_some());
238 }
239
240 #[test]
241 fn test_session_switch_dataset() {
242 let mut session = ReplSession::new();
243
244 session.load_dataset("data1", create_test_dataset());
246 session.load_dataset("data2", create_test_dataset());
247
248 assert_eq!(session.active_name(), Some("data2".to_string()));
249
250 session.use_dataset("data1").unwrap();
251 assert_eq!(session.active_name(), Some("data1".to_string()));
252 }
253
254 #[test]
255 fn test_session_history_tracking() {
256 let mut session = ReplSession::new();
257
258 session.add_history("load data.parquet");
259 session.add_history("info");
260 session.add_history("head 10");
261
262 assert_eq!(session.history().len(), 3);
263 assert_eq!(session.history()[0], "load data.parquet");
264 }
265
266 #[test]
267 fn test_session_history_export() {
268 let mut session = ReplSession::new();
269
270 session.add_history("load data.parquet");
271 session.add_history("quality check");
272 session.add_history("convert csv");
273
274 let script = session.export_history();
275
276 assert!(script.contains("# alimentar session export"));
277 assert!(script.contains("alimentar"));
278 assert!(script.contains("load data.parquet"));
279 }
280
281 #[test]
282 fn test_session_quality_cache() {
283 let mut session = ReplSession::new();
284 session.load_dataset("test", create_test_dataset());
285
286 assert!(session.quality_cache().is_some());
288 }
289
290 #[test]
295 fn test_parse_load_command() {
296 let cmd = CommandParser::parse("load data.parquet").unwrap();
297 assert!(matches!(cmd, ReplCommand::Load { path } if path == "data.parquet"));
298 }
299
300 #[test]
301 fn test_parse_info_command() {
302 let cmd = CommandParser::parse("info").unwrap();
303 assert!(matches!(cmd, ReplCommand::Info));
304 }
305
306 #[test]
307 fn test_parse_head_command_default() {
308 let cmd = CommandParser::parse("head").unwrap();
309 assert!(matches!(cmd, ReplCommand::Head { n: 10 }));
310 }
311
312 #[test]
313 fn test_parse_head_command_with_count() {
314 let cmd = CommandParser::parse("head 25").unwrap();
315 assert!(matches!(cmd, ReplCommand::Head { n: 25 }));
316 }
317
318 #[test]
319 fn test_parse_schema_command() {
320 let cmd = CommandParser::parse("schema").unwrap();
321 assert!(matches!(cmd, ReplCommand::Schema));
322 }
323
324 #[test]
325 fn test_parse_quality_check_command() {
326 let cmd = CommandParser::parse("quality check").unwrap();
327 assert!(matches!(cmd, ReplCommand::QualityCheck));
328 }
329
330 #[test]
331 fn test_parse_quality_score_command() {
332 let cmd = CommandParser::parse("quality score").unwrap();
333 assert!(matches!(
334 cmd,
335 ReplCommand::QualityScore {
336 suggest: false,
337 json: false,
338 badge: false
339 }
340 ));
341 }
342
343 #[test]
344 fn test_parse_quality_score_with_flags() {
345 let cmd = CommandParser::parse("quality score --suggest --json").unwrap();
346 assert!(matches!(
347 cmd,
348 ReplCommand::QualityScore {
349 suggest: true,
350 json: true,
351 badge: false
352 }
353 ));
354 }
355
356 #[test]
357 fn test_parse_drift_detect_command() {
358 let cmd = CommandParser::parse("drift detect baseline.parquet").unwrap();
359 assert!(
360 matches!(cmd, ReplCommand::DriftDetect { reference } if reference == "baseline.parquet")
361 );
362 }
363
364 #[test]
365 fn test_parse_convert_command() {
366 let cmd = CommandParser::parse("convert csv").unwrap();
367 assert!(matches!(cmd, ReplCommand::Convert { format } if format == "csv"));
368 }
369
370 #[test]
371 fn test_parse_datasets_command() {
372 let cmd = CommandParser::parse("datasets").unwrap();
373 assert!(matches!(cmd, ReplCommand::Datasets));
374 }
375
376 #[test]
377 fn test_parse_use_command() {
378 let cmd = CommandParser::parse("use my_data").unwrap();
379 assert!(matches!(cmd, ReplCommand::Use { name } if name == "my_data"));
380 }
381
382 #[test]
383 fn test_parse_history_command() {
384 let cmd = CommandParser::parse("history").unwrap();
385 assert!(matches!(cmd, ReplCommand::History { export: false }));
386 }
387
388 #[test]
389 fn test_parse_history_export_command() {
390 let cmd = CommandParser::parse("history --export").unwrap();
391 assert!(matches!(cmd, ReplCommand::History { export: true }));
392 }
393
394 #[test]
395 fn test_parse_help_command() {
396 let cmd = CommandParser::parse("help").unwrap();
397 assert!(matches!(cmd, ReplCommand::Help { topic: None }));
398 }
399
400 #[test]
401 fn test_parse_help_with_topic() {
402 let cmd = CommandParser::parse("help quality").unwrap();
403 assert!(matches!(cmd, ReplCommand::Help { topic: Some(t) } if t == "quality"));
404 }
405
406 #[test]
407 fn test_parse_question_mark_help() {
408 let cmd = CommandParser::parse("?").unwrap();
409 assert!(matches!(cmd, ReplCommand::Help { topic: None }));
410 }
411
412 #[test]
413 fn test_parse_quit_command() {
414 let cmd = CommandParser::parse("quit").unwrap();
415 assert!(matches!(cmd, ReplCommand::Quit));
416 }
417
418 #[test]
419 fn test_parse_exit_command() {
420 let cmd = CommandParser::parse("exit").unwrap();
421 assert!(matches!(cmd, ReplCommand::Quit));
422 }
423
424 #[test]
425 fn test_parse_invalid_command() {
426 let result = CommandParser::parse("invalid_command_xyz");
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn test_parse_empty_command() {
432 let result = CommandParser::parse("");
433 assert!(result.is_err());
434 }
435
436 #[test]
441 fn test_completer_command_suggestions() {
442 let session = ReplSession::new();
443 let completer = SchemaAwareCompleter::new(&session);
444
445 let suggestions = completer.complete("lo");
446 assert!(suggestions.iter().any(|s| s == "load"));
447 }
448
449 #[test]
450 fn test_completer_subcommand_suggestions() {
451 let session = ReplSession::new();
452 let completer = SchemaAwareCompleter::new(&session);
453
454 let suggestions = completer.complete("quality ");
455 assert!(suggestions.iter().any(|s| s == "check"));
456 assert!(suggestions.iter().any(|s| s == "score"));
457 }
458
459 #[test]
460 fn test_completer_column_suggestions() {
461 let mut session = ReplSession::new();
462 session.load_dataset("test", create_test_dataset());
463
464 let completer = SchemaAwareCompleter::new(&session);
465
466 let suggestions = completer.complete("select ");
468 assert!(!suggestions.is_empty());
470 }
471
472 #[test]
473 fn test_completer_dataset_suggestions() {
474 let mut session = ReplSession::new();
475 session.load_dataset("train", create_test_dataset());
476 session.load_dataset("test", create_test_dataset());
477
478 let completer = SchemaAwareCompleter::new(&session);
479
480 let suggestions = completer.complete("use ");
481 assert!(suggestions.iter().any(|s| s == "train"));
482 assert!(suggestions.iter().any(|s| s == "test"));
483 }
484
485 #[test]
490 fn test_prompt_no_dataset() {
491 let session = ReplSession::new();
492 let prompt_str = AndonPrompt::render(&session);
493
494 assert!(prompt_str.contains("alimentar"));
495 assert!(!prompt_str.contains('[')); }
497
498 #[test]
499 fn test_prompt_healthy_dataset() {
500 let mut session = ReplSession::new();
501 session.load_dataset("data", create_test_dataset());
502 let prompt_str = AndonPrompt::render(&session);
505
506 assert!(prompt_str.contains("data"));
507 assert!(prompt_str.contains('A') || prompt_str.contains("rows"));
508 }
509
510 #[test]
511 fn test_health_status_from_grade() {
512 assert_eq!(HealthStatus::from_grade('A'), HealthStatus::Healthy);
513 assert_eq!(HealthStatus::from_grade('B'), HealthStatus::Healthy);
514 assert_eq!(HealthStatus::from_grade('C'), HealthStatus::Warning);
515 assert_eq!(HealthStatus::from_grade('D'), HealthStatus::Warning);
516 assert_eq!(HealthStatus::from_grade('F'), HealthStatus::Critical);
517 }
518
519 #[test]
524 fn test_parse_export_quality_json() {
525 let cmd = CommandParser::parse("export quality --json").unwrap();
526 assert!(matches!(cmd, ReplCommand::Export { what, json: true } if what == "quality"));
527 }
528
529 #[test]
530 fn test_parse_validate_schema() {
531 let cmd = CommandParser::parse("validate --schema spec.yaml").unwrap();
532 assert!(matches!(cmd, ReplCommand::Validate { schema } if schema == "spec.yaml"));
533 }
534
535 #[test]
540 fn test_display_config_defaults() {
541 let config = DisplayConfig::default();
542
543 assert_eq!(config.max_rows, 10);
544 assert_eq!(config.max_column_width, 50);
545 assert!(config.color_output);
546 }
547
548 #[test]
549 fn test_display_config_builder() {
550 let config = DisplayConfig::default()
551 .with_max_rows(20)
552 .with_max_column_width(100)
553 .with_color(false);
554
555 assert_eq!(config.max_rows, 20);
556 assert_eq!(config.max_column_width, 100);
557 assert!(!config.color_output);
558 }
559
560 fn create_test_dataset() -> ArrowDataset {
565 use std::sync::Arc;
566
567 use arrow::{
568 array::{Int32Array, StringArray},
569 datatypes::{DataType, Field, Schema},
570 record_batch::RecordBatch,
571 };
572
573 let schema = Schema::new(vec![
574 Field::new("id", DataType::Int32, false),
575 Field::new("name", DataType::Utf8, true),
576 Field::new("score", DataType::Int32, true),
577 ]);
578
579 let id_array = Int32Array::from(vec![1, 2, 3]);
580 let name_array = StringArray::from(vec![Some("Alice"), Some("Bob"), Some("Charlie")]);
581 let score_array = Int32Array::from(vec![Some(85), Some(92), Some(78)]);
582
583 let batch = RecordBatch::try_new(
584 Arc::new(schema),
585 vec![
586 Arc::new(id_array),
587 Arc::new(name_array),
588 Arc::new(score_array),
589 ],
590 )
591 .unwrap();
592
593 ArrowDataset::from_batch(batch).unwrap()
594 }
595
596 #[test]
601 fn test_health_status_indicator_all_variants() {
602 assert_eq!(HealthStatus::Healthy.indicator(), "");
603 assert_eq!(HealthStatus::Warning.indicator(), "!");
604 assert_eq!(HealthStatus::Critical.indicator(), "!!");
605 assert_eq!(HealthStatus::None.indicator(), "");
606 }
607
608 #[test]
609 fn test_health_status_from_grade_unknown() {
610 assert_eq!(HealthStatus::from_grade('X'), HealthStatus::None);
611 assert_eq!(HealthStatus::from_grade('Z'), HealthStatus::None);
612 assert_eq!(HealthStatus::from_grade(' '), HealthStatus::None);
613 }
614
615 #[cfg(feature = "repl")]
616 #[test]
617 fn test_health_status_color_all_variants() {
618 use nu_ansi_term::Color;
619
620 assert_eq!(HealthStatus::Healthy.color(), Color::Green);
621 assert_eq!(HealthStatus::Warning.color(), Color::Yellow);
622 assert_eq!(HealthStatus::Critical.color(), Color::Red);
623 assert_eq!(HealthStatus::None.color(), Color::Default);
624 }
625
626 #[test]
627 fn test_andon_prompt_new_and_default() {
628 let prompt1 = AndonPrompt::new();
629 let prompt2 = AndonPrompt::default();
630 let session = ReplSession::new();
632 let render1 = AndonPrompt::render(&session);
633 let _ = (prompt1, prompt2); assert!(render1.contains("alimentar"));
635 }
636
637 #[test]
638 fn test_andon_prompt_render_with_grade_indicator() {
639 let mut session = ReplSession::new();
640 session.load_dataset("test_data", create_test_dataset());
641
642 let prompt_str = AndonPrompt::render(&session);
643 assert!(prompt_str.contains("test_data"));
645 assert!(prompt_str.contains("rows"));
646 }
647
648 #[cfg(feature = "repl")]
649 #[test]
650 fn test_andon_prompt_render_colored() {
651 let mut session = ReplSession::new();
652 session.load_dataset("colored_test", create_test_dataset());
653
654 let prompt_str = AndonPrompt::render_colored(&session);
655 assert!(prompt_str.contains("alimentar"));
657 assert!(prompt_str.contains("colored_test"));
658 }
659
660 #[cfg(feature = "repl")]
661 #[test]
662 fn test_andon_prompt_render_colored_no_dataset() {
663 let session = ReplSession::new();
664 let prompt_str = AndonPrompt::render_colored(&session);
665 assert!(prompt_str.contains("alimentar"));
666 }
669
670 #[cfg(feature = "repl")]
671 #[test]
672 fn test_andon_prompt_trait_methods() {
673 use reedline::Prompt;
674
675 let prompt = AndonPrompt::new();
676
677 assert_eq!(prompt.render_prompt_left().as_ref(), "alimentar > ");
678 assert_eq!(prompt.render_prompt_right().as_ref(), "");
679 assert_eq!(prompt.render_prompt_multiline_indicator().as_ref(), "... ");
680 }
681
682 #[test]
687 fn test_session_use_dataset_not_found() {
688 let mut session = ReplSession::new();
689 session.load_dataset("data1", create_test_dataset());
690
691 let result = session.use_dataset("nonexistent");
692 assert!(result.is_err());
693 assert!(result.unwrap_err().to_string().contains("not found"));
694 }
695
696 #[test]
697 fn test_session_active_row_count() {
698 let mut session = ReplSession::new();
699 assert!(session.active_row_count().is_none());
700
701 session.load_dataset("test", create_test_dataset());
702 assert_eq!(session.active_row_count(), Some(3));
703 }
704
705 #[test]
706 fn test_session_active_grade() {
707 let mut session = ReplSession::new();
708 assert!(session.active_grade().is_none());
709
710 session.load_dataset("test", create_test_dataset());
711 assert!(session.active_grade().is_some());
712 }
713
714 #[test]
715 fn test_session_column_names() {
716 let mut session = ReplSession::new();
717 assert!(session.column_names().is_empty());
718
719 session.load_dataset("test", create_test_dataset());
720 let columns = session.column_names();
721 assert!(columns.contains(&"id".to_string()));
722 assert!(columns.contains(&"name".to_string()));
723 assert!(columns.contains(&"score".to_string()));
724 }
725
726 #[test]
727 fn test_session_export_history_with_active_dataset() {
728 let mut session = ReplSession::new();
729 session.load_dataset("data.parquet", create_test_dataset());
730
731 session.add_history("info");
732 session.add_history("head 5");
733 session.add_history("quality check");
734
735 let script = session.export_history();
736 assert!(script.contains("#!/usr/bin/env bash"));
737 assert!(script.contains("alimentar session export"));
738 assert!(script.contains("info"));
740 }
741
742 #[test]
743 fn test_session_config_access() {
744 let session = ReplSession::new();
745 assert_eq!(session.config.max_rows, 10);
746 assert_eq!(session.config.max_column_width, 50);
747 assert!(session.config.color_output);
748 }
749
750 #[test]
751 fn test_session_default_implementation() {
752 let session1 = ReplSession::new();
753 let session2 = ReplSession::default();
754
755 assert_eq!(session1.datasets().len(), session2.datasets().len());
756 assert_eq!(session1.history().len(), session2.history().len());
757 }
758
759 #[test]
764 fn test_completer_empty_input() {
765 let session = ReplSession::new();
766 let completer = SchemaAwareCompleter::new(&session);
767
768 let suggestions = completer.complete("");
769 assert!(suggestions.len() > 5);
771 assert!(suggestions.contains(&"load".to_string()));
772 assert!(suggestions.contains(&"info".to_string()));
773 assert!(suggestions.contains(&"quit".to_string()));
774 }
775
776 #[test]
777 fn test_completer_convert_suggestions() {
778 let session = ReplSession::new();
779 let completer = SchemaAwareCompleter::new(&session);
780
781 let suggestions = completer.complete("convert ");
782 assert!(suggestions.contains(&"csv".to_string()));
783 assert!(suggestions.contains(&"parquet".to_string()));
784 assert!(suggestions.contains(&"json".to_string()));
785 }
786
787 #[test]
788 fn test_completer_convert_partial() {
789 let session = ReplSession::new();
790 let completer = SchemaAwareCompleter::new(&session);
791
792 let suggestions = completer.complete("convert p");
793 assert!(suggestions.contains(&"parquet".to_string()));
794 assert!(!suggestions.contains(&"csv".to_string()));
795 }
796
797 #[test]
798 fn test_completer_help_suggestions() {
799 let session = ReplSession::new();
800 let completer = SchemaAwareCompleter::new(&session);
801
802 let suggestions = completer.complete("help ");
803 assert!(suggestions.contains(&"quality".to_string()));
804 assert!(suggestions.contains(&"drift".to_string()));
805 assert!(suggestions.contains(&"export".to_string()));
806 }
807
808 #[test]
809 fn test_completer_help_partial() {
810 let session = ReplSession::new();
811 let completer = SchemaAwareCompleter::new(&session);
812
813 let suggestions = completer.complete("help q");
814 assert!(suggestions.contains(&"quality".to_string()));
815 assert!(!suggestions.contains(&"drift".to_string()));
816 }
817
818 #[test]
819 fn test_completer_drift_subcommands() {
820 let session = ReplSession::new();
821 let completer = SchemaAwareCompleter::new(&session);
822
823 let suggestions = completer.complete("drift ");
824 assert!(suggestions.contains(&"detect".to_string()));
825 }
826
827 #[test]
828 fn test_completer_load_no_suggestions() {
829 let session = ReplSession::new();
830 let completer = SchemaAwareCompleter::new(&session);
831
832 let suggestions = completer.complete("load ");
834 assert!(suggestions.is_empty());
835 }
836
837 #[test]
838 fn test_completer_select_column_suggestions() {
839 let mut session = ReplSession::new();
840 session.load_dataset("test", create_test_dataset());
841
842 let completer = SchemaAwareCompleter::new(&session);
843
844 let suggestions = completer.complete("select ");
845 assert!(suggestions.contains(&"id".to_string()));
846 assert!(suggestions.contains(&"name".to_string()));
847 assert!(suggestions.contains(&"score".to_string()));
848 }
849
850 #[test]
851 fn test_completer_drop_column_suggestions() {
852 let mut session = ReplSession::new();
853 session.load_dataset("test", create_test_dataset());
854
855 let completer = SchemaAwareCompleter::new(&session);
856
857 let suggestions = completer.complete("drop ");
858 assert!(suggestions.contains(&"id".to_string()));
859 }
860
861 #[test]
862 fn test_completer_update_columns() {
863 let mut session = ReplSession::new();
864 let mut completer = SchemaAwareCompleter::new(&session);
865
866 let suggestions = completer.complete("select ");
868 assert!(suggestions.is_empty());
869
870 session.load_dataset("test", create_test_dataset());
872 completer.update_columns(&session);
873
874 let suggestions = completer.complete("select ");
875 assert!(!suggestions.is_empty());
876 }
877
878 #[test]
879 fn test_completer_update_datasets() {
880 let mut session = ReplSession::new();
881 let mut completer = SchemaAwareCompleter::new(&session);
882
883 let suggestions = completer.complete("use ");
885 assert!(suggestions.is_empty());
886
887 session.load_dataset("mydata", create_test_dataset());
889 completer.update_datasets(&session);
890
891 let suggestions = completer.complete("use ");
892 assert!(suggestions.contains(&"mydata".to_string()));
893 }
894
895 #[test]
896 fn test_completer_unknown_command() {
897 let session = ReplSession::new();
898 let completer = SchemaAwareCompleter::new(&session);
899
900 let suggestions = completer.complete("unknowncmd ");
902 assert!(suggestions.is_empty());
903 }
904
905 #[test]
906 fn test_completer_partial_command_filtering() {
907 let session = ReplSession::new();
908 let completer = SchemaAwareCompleter::new(&session);
909
910 let suggestions = completer.complete("he");
911 assert!(suggestions.contains(&"head".to_string()));
912 assert!(suggestions.contains(&"help".to_string()));
913 assert!(!suggestions.contains(&"load".to_string()));
914 }
915
916 #[test]
917 fn test_completer_quality_partial_subcommand() {
918 let session = ReplSession::new();
919 let completer = SchemaAwareCompleter::new(&session);
920
921 let suggestions = completer.complete("quality c");
922 assert!(suggestions.contains(&"check".to_string()));
923 assert!(!suggestions.contains(&"score".to_string()));
924 }
925
926 #[test]
927 fn test_completer_use_with_partial_name() {
928 let mut session = ReplSession::new();
929 session.load_dataset("training_data", create_test_dataset());
930 session.load_dataset("test_data", create_test_dataset());
931
932 let completer = SchemaAwareCompleter::new(&session);
933
934 let suggestions = completer.complete("use tr");
935 assert!(suggestions.contains(&"training_data".to_string()));
936 assert!(!suggestions.contains(&"test_data".to_string()));
937 }
938
939 #[test]
944 fn test_command_names_returns_all() {
945 let names = CommandParser::command_names();
946 assert!(names.contains(&"load"));
947 assert!(names.contains(&"info"));
948 assert!(names.contains(&"head"));
949 assert!(names.contains(&"schema"));
950 assert!(names.contains(&"quality"));
951 assert!(names.contains(&"drift"));
952 assert!(names.contains(&"convert"));
953 assert!(names.contains(&"datasets"));
954 assert!(names.contains(&"use"));
955 assert!(names.contains(&"history"));
956 assert!(names.contains(&"help"));
957 assert!(names.contains(&"export"));
958 assert!(names.contains(&"validate"));
959 assert!(names.contains(&"quit"));
960 assert!(names.contains(&"exit"));
961 }
962
963 #[test]
964 fn test_subcommands_quality() {
965 let subs = CommandParser::subcommands("quality");
966 assert!(subs.contains(&"check"));
967 assert!(subs.contains(&"score"));
968 }
969
970 #[test]
971 fn test_subcommands_drift() {
972 let subs = CommandParser::subcommands("drift");
973 assert!(subs.contains(&"detect"));
974 }
975
976 #[test]
977 fn test_subcommands_unknown() {
978 let subs = CommandParser::subcommands("unknown");
979 assert!(subs.is_empty());
980 }
981
982 #[test]
983 fn test_flags_quality_score() {
984 let flags = CommandParser::flags("quality", Some("score"));
985 assert!(flags.contains(&"--suggest"));
986 assert!(flags.contains(&"--json"));
987 assert!(flags.contains(&"--badge"));
988 }
989
990 #[test]
991 fn test_flags_export() {
992 let flags = CommandParser::flags("export", None);
993 assert!(flags.contains(&"--json"));
994 }
995
996 #[test]
997 fn test_flags_validate() {
998 let flags = CommandParser::flags("validate", None);
999 assert!(flags.contains(&"--schema"));
1000 }
1001
1002 #[test]
1003 fn test_flags_history() {
1004 let flags = CommandParser::flags("history", None);
1005 assert!(flags.contains(&"--export"));
1006 }
1007
1008 #[test]
1009 fn test_flags_unknown() {
1010 let flags = CommandParser::flags("unknown", None);
1011 assert!(flags.is_empty());
1012 }
1013
1014 #[test]
1015 fn test_parse_quality_score_badge() {
1016 let cmd = CommandParser::parse("quality score --badge").unwrap();
1017 assert!(matches!(cmd, ReplCommand::QualityScore { badge: true, .. }));
1018 }
1019
1020 #[test]
1021 fn test_parse_history_shorthand() {
1022 let cmd = CommandParser::parse("history -e").unwrap();
1023 assert!(matches!(cmd, ReplCommand::History { export: true }));
1024 }
1025
1026 #[test]
1027 fn test_parse_validate_shorthand() {
1028 let cmd = CommandParser::parse("validate -s schema.yaml").unwrap();
1029 assert!(matches!(cmd, ReplCommand::Validate { schema } if schema == "schema.yaml"));
1030 }
1031
1032 #[test]
1033 fn test_parse_case_insensitive() {
1034 let cmd1 = CommandParser::parse("LOAD file.csv").unwrap();
1035 let cmd2 = CommandParser::parse("Load FILE.CSV").unwrap();
1036 assert!(matches!(cmd1, ReplCommand::Load { .. }));
1037 assert!(matches!(cmd2, ReplCommand::Load { .. }));
1038 }
1039
1040 #[test]
1041 fn test_parse_head_invalid_number() {
1042 let result = CommandParser::parse("head abc");
1044 assert!(result.is_err());
1045 assert!(result.unwrap_err().to_string().contains("Invalid number"));
1046 }
1047
1048 #[test]
1049 fn test_parse_load_missing_path() {
1050 let result = CommandParser::parse("load");
1051 assert!(result.is_err());
1052 }
1053
1054 #[test]
1055 fn test_parse_use_missing_name() {
1056 let result = CommandParser::parse("use");
1057 assert!(result.is_err());
1058 }
1059
1060 #[test]
1061 fn test_parse_convert_missing_format() {
1062 let result = CommandParser::parse("convert");
1063 assert!(result.is_err());
1064 }
1065
1066 #[test]
1067 fn test_parse_drift_missing_reference() {
1068 let result = CommandParser::parse("drift detect");
1069 assert!(result.is_err());
1070 }
1071
1072 #[test]
1073 fn test_parse_quality_invalid_subcommand() {
1074 let result = CommandParser::parse("quality invalid");
1075 assert!(result.is_err());
1076 }
1077
1078 #[test]
1079 fn test_parse_quality_missing_subcommand() {
1080 let result = CommandParser::parse("quality");
1081 assert!(result.is_err());
1082 }
1083
1084 #[test]
1085 fn test_parse_drift_invalid_subcommand() {
1086 let result = CommandParser::parse("drift invalid");
1087 assert!(result.is_err());
1088 }
1089
1090 #[test]
1091 fn test_parse_validate_missing_schema() {
1092 let result = CommandParser::parse("validate --schema");
1093 assert!(result.is_err());
1094 }
1095
1096 #[test]
1097 fn test_parse_export_missing_what() {
1098 let result = CommandParser::parse("export");
1099 assert!(result.is_err());
1100 }
1101}