1use std::{collections::HashMap, fmt::Write, path::Path, sync::Arc};
7
8use super::commands::ReplCommand;
9use crate::{
10 dataset::Dataset,
11 quality::{LetterGrade, QualityChecker, QualityScore},
12 ArrowDataset, Error, Result,
13};
14
15#[derive(Debug, Clone)]
17pub struct DisplayConfig {
18 pub max_rows: usize,
20 pub max_column_width: usize,
22 pub color_output: bool,
24}
25
26impl Default for DisplayConfig {
27 fn default() -> Self {
28 Self {
29 max_rows: 10,
30 max_column_width: 50,
31 color_output: true,
32 }
33 }
34}
35
36impl DisplayConfig {
37 #[must_use]
39 pub fn with_max_rows(mut self, rows: usize) -> Self {
40 self.max_rows = rows;
41 self
42 }
43
44 #[must_use]
46 pub fn with_max_column_width(mut self, width: usize) -> Self {
47 self.max_column_width = width;
48 self
49 }
50
51 #[must_use]
53 pub fn with_color(mut self, enabled: bool) -> Self {
54 self.color_output = enabled;
55 self
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct QualityCache {
62 pub score: QualityScore,
64 pub timestamp: std::time::Instant,
66}
67
68#[derive(Debug)]
73pub struct ReplSession {
74 datasets: HashMap<String, Arc<ArrowDataset>>,
76 active_name: Option<String>,
78 history: Vec<String>,
80 pub config: DisplayConfig,
82 quality_cache: Option<QualityCache>,
84}
85
86impl Default for ReplSession {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl ReplSession {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 datasets: HashMap::new(),
98 active_name: None,
99 history: Vec::new(),
100 config: DisplayConfig::default(),
101 quality_cache: None,
102 }
103 }
104
105 pub fn load_dataset(&mut self, name: &str, dataset: ArrowDataset) {
109 let score = self.compute_quality(&dataset);
111
112 let arc_dataset = Arc::new(dataset);
113 self.datasets.insert(name.to_string(), arc_dataset);
114 self.active_name = Some(name.to_string());
115
116 if let Some(score) = score {
117 self.quality_cache = Some(QualityCache {
118 score,
119 timestamp: std::time::Instant::now(),
120 });
121 }
122 }
123
124 fn compute_quality(&self, dataset: &ArrowDataset) -> Option<QualityScore> {
126 let checker = QualityChecker::new();
127 match checker.check(dataset) {
128 Ok(report) => {
129 let checklist = self.build_basic_checklist(&report);
131 Some(QualityScore::from_checklist(checklist))
132 }
133 Err(_) => None,
134 }
135 }
136
137 #[allow(clippy::unused_self)]
139 fn build_basic_checklist(
140 &self,
141 report: &crate::quality::QualityReport,
142 ) -> Vec<crate::quality::ChecklistItem> {
143 use crate::quality::{ChecklistItem, Severity};
144
145 let mut items = Vec::new();
146
147 items.push(ChecklistItem::new(
149 1,
150 "Schema is readable",
151 Severity::Critical,
152 true,
153 ));
154
155 let has_excessive_nulls = report.columns.values().any(|c| c.null_ratio > 0.5);
157 items.push(ChecklistItem::new(
158 2,
159 "No excessive null ratios (>50%)",
160 Severity::High,
161 !has_excessive_nulls,
162 ));
163
164 let has_high_duplicates = report.columns.values().any(|c| c.duplicate_ratio > 0.3);
166 items.push(ChecklistItem::new(
167 3,
168 "No high duplicate ratios (>30%)",
169 Severity::Medium,
170 !has_high_duplicates,
171 ));
172
173 items.push(ChecklistItem::new(
175 4,
176 "Dataset has rows",
177 Severity::Critical,
178 report.row_count > 0,
179 ));
180
181 items.push(ChecklistItem::new(
183 5,
184 "Dataset has columns",
185 Severity::Critical,
186 report.column_count > 0,
187 ));
188
189 items
190 }
191
192 pub fn use_dataset(&mut self, name: &str) -> Result<()> {
198 if self.datasets.contains_key(name) {
199 self.active_name = Some(name.to_string());
200
201 if let Some(dataset) = self.datasets.get(name) {
203 if let Some(score) = self.compute_quality(dataset) {
204 self.quality_cache = Some(QualityCache {
205 score,
206 timestamp: std::time::Instant::now(),
207 });
208 }
209 }
210
211 Ok(())
212 } else {
213 Err(Error::NotFound(format!("Dataset '{}' not found", name)))
214 }
215 }
216
217 #[must_use]
219 pub fn active_dataset(&self) -> Option<&Arc<ArrowDataset>> {
220 self.active_name.as_ref().and_then(|n| self.datasets.get(n))
221 }
222
223 #[must_use]
225 pub fn active_name(&self) -> Option<String> {
226 self.active_name.clone()
227 }
228
229 #[must_use]
231 pub fn active_grade(&self) -> Option<LetterGrade> {
232 self.quality_cache.as_ref().map(|c| c.score.grade)
233 }
234
235 #[must_use]
237 pub fn active_row_count(&self) -> Option<usize> {
238 self.active_dataset().map(|d| d.len())
239 }
240
241 #[must_use]
243 pub fn datasets(&self) -> Vec<String> {
244 self.datasets.keys().cloned().collect()
245 }
246
247 pub fn add_history(&mut self, command: &str) {
249 self.history.push(command.to_string());
250 }
251
252 #[must_use]
254 pub fn history(&self) -> &[String] {
255 &self.history
256 }
257
258 #[must_use]
260 pub fn quality_cache(&self) -> Option<&QualityCache> {
261 self.quality_cache.as_ref()
262 }
263
264 #[must_use]
266 pub fn export_history(&self) -> String {
267 let mut script = String::new();
268 script.push_str("#!/usr/bin/env bash\n");
269 script.push_str("# alimentar session export\n");
270 let _ = writeln!(script, "# Generated: {}", chrono_now());
271 script.push_str("# Reproducible session for Batuta pipeline integration\n\n");
272
273 for cmd in &self.history {
274 let batch_cmd = self.repl_to_batch(cmd);
276 let _ = writeln!(script, "alimentar {}", batch_cmd);
277 }
278
279 script
280 }
281
282 fn repl_to_batch(&self, cmd: &str) -> String {
284 let parts: Vec<&str> = cmd.split_whitespace().collect();
285 if parts.is_empty() {
286 return String::new();
287 }
288
289 match parts[0] {
290 "load" => {
291 cmd.to_string()
293 }
294 "info" | "schema" | "head" => {
295 if let Some(name) = &self.active_name {
297 format!("{} {}", cmd, name)
298 } else {
299 cmd.to_string()
300 }
301 }
302 "quality" => {
303 if parts.len() > 1 {
304 if let Some(name) = &self.active_name {
305 format!("{} {} {}", parts[0], parts[1], name)
306 } else {
307 cmd.to_string()
308 }
309 } else {
310 cmd.to_string()
311 }
312 }
313 _ => cmd.to_string(),
314 }
315 }
316
317 #[must_use]
319 pub fn column_names(&self) -> Vec<String> {
320 self.active_dataset()
321 .map(|d| {
322 d.schema()
323 .fields()
324 .iter()
325 .map(|f| f.name().clone())
326 .collect()
327 })
328 .unwrap_or_default()
329 }
330
331 pub fn execute(&mut self, cmd: ReplCommand) -> Result<()> {
337 match cmd {
338 ReplCommand::Load { path } => self.cmd_load(&path),
339 ReplCommand::Info => self.cmd_info(),
340 ReplCommand::Head { n } => self.cmd_head(n),
341 ReplCommand::Schema => self.cmd_schema(),
342 ReplCommand::QualityCheck => self.cmd_quality_check(),
343 ReplCommand::QualityScore {
344 suggest,
345 json,
346 badge,
347 } => self.cmd_quality_score(suggest, json, badge),
348 ReplCommand::DriftDetect { reference } => self.cmd_drift_detect(&reference),
349 ReplCommand::Convert { format } => self.cmd_convert(&format),
350 ReplCommand::Datasets => self.cmd_datasets(),
351 ReplCommand::Use { name } => self.use_dataset(&name),
352 ReplCommand::History { export } => self.cmd_history(export),
353 ReplCommand::Help { topic } => self.cmd_help(topic.as_deref()),
354 ReplCommand::Export { what, json } => self.cmd_export(&what, json),
355 ReplCommand::Validate { schema } => self.cmd_validate(&schema),
356 ReplCommand::Quit => Ok(()), }
358 }
359
360 fn cmd_load(&mut self, path: &str) -> Result<()> {
361 let dataset = load_dataset_from_path(path)?;
362 let name = Path::new(path)
363 .file_name()
364 .and_then(|n| n.to_str())
365 .unwrap_or("data")
366 .to_string();
367
368 self.load_dataset(&name, dataset);
369 println!(
370 "Loaded '{}' ({} rows)",
371 name,
372 self.active_row_count().unwrap_or(0)
373 );
374 Ok(())
375 }
376
377 fn cmd_info(&self) -> Result<()> {
378 let dataset = self.require_active()?;
379 println!(
380 "Dataset: {}",
381 self.active_name.as_deref().unwrap_or("unnamed")
382 );
383 println!("Rows: {}", dataset.len());
384 println!("Columns: {}", dataset.schema().fields().len());
385
386 if let Some(cache) = &self.quality_cache {
387 println!("Quality: {} ({:.1})", cache.score.grade, cache.score.score);
388 }
389
390 Ok(())
391 }
392
393 fn cmd_head(&self, n: usize) -> Result<()> {
394 use crate::transform::Transform;
395
396 let dataset = self.require_active()?;
397 let mut total_rows = 0;
398 let mut output_batches = Vec::new();
399
400 for batch in dataset.iter() {
401 if total_rows >= n {
402 break;
403 }
404 let rows_needed = n - total_rows;
405 let rows_to_take = rows_needed.min(batch.num_rows());
406
407 if rows_to_take > 0 {
408 let take_transform = crate::transform::Take::new(rows_to_take);
409 let limited = take_transform.apply(batch)?;
410 total_rows += limited.num_rows();
411 output_batches.push(limited);
412 }
413 }
414
415 println!(
416 "{}",
417 arrow::util::pretty::pretty_format_batches(&output_batches)?
418 );
419 Ok(())
420 }
421
422 fn cmd_schema(&self) -> Result<()> {
423 let dataset = self.require_active()?;
424 let schema = dataset.schema();
425
426 println!("Schema ({} columns):", schema.fields().len());
427 for field in schema.fields() {
428 let nullable = if field.is_nullable() {
429 "nullable"
430 } else {
431 "not null"
432 };
433 println!(" {}: {} ({})", field.name(), field.data_type(), nullable);
434 }
435
436 Ok(())
437 }
438
439 fn cmd_quality_check(&self) -> Result<()> {
440 let dataset = self.require_active()?;
441 let checker = QualityChecker::new();
442 let report = checker.check(dataset)?;
443
444 println!("Quality Check Results:");
445 println!(" Total rows: {}", report.row_count);
446 println!(" Total columns: {}", report.column_count);
447
448 for (name, col) in &report.columns {
449 println!("\n Column: {}", name);
450 println!(" Null ratio: {:.2}%", col.null_ratio * 100.0);
451 println!(" Duplicate ratio: {:.2}%", col.duplicate_ratio * 100.0);
452 }
453
454 Ok(())
455 }
456
457 fn cmd_quality_score(&self, suggest: bool, json: bool, badge: bool) -> Result<()> {
458 if let Some(cache) = &self.quality_cache {
459 if json {
460 println!("{}", cache.score.to_json());
461 } else if badge {
462 println!("{}", cache.score.badge_url());
463 } else {
464 println!(
465 "Quality Score: {} ({:.1}/100)",
466 cache.score.grade, cache.score.score
467 );
468 println!("Decision: {}", cache.score.grade.publication_decision());
469
470 if suggest {
471 let failed = cache.score.failed_items();
472 if !failed.is_empty() {
473 println!("\nSuggestions:");
474 for item in failed {
475 println!(" [{:?}] {}", item.severity, item.description);
476 if let Some(s) = &item.suggestion {
477 println!(" → {}", s);
478 }
479 }
480 }
481 }
482 }
483 Ok(())
484 } else {
485 Err(Error::NotFound("No quality data available".to_string()))
486 }
487 }
488
489 fn cmd_drift_detect(&self, reference: &str) -> Result<()> {
490 let dataset = self.require_active()?;
491 let ref_dataset = load_dataset_from_path(reference)?;
492
493 let detector = crate::DriftDetector::new(ref_dataset);
494 let report = detector.detect(dataset)?;
495
496 println!("Drift Detection Report:");
497 println!(" Columns analyzed: {}", report.column_scores.len());
498 println!(
499 " Drifted columns: {}",
500 report
501 .column_scores
502 .values()
503 .filter(|d| d.drift_detected)
504 .count()
505 );
506
507 for (name, drift) in &report.column_scores {
508 if drift.drift_detected {
509 println!(" {} [{:?}]: {:.2?}", name, drift.severity, drift.p_value);
510 }
511 }
512
513 Ok(())
514 }
515
516 fn cmd_convert(&self, format: &str) -> Result<()> {
517 let dataset = self.require_active()?;
518 let name = self.active_name.as_deref().unwrap_or("data");
519 let output = format!("{}.{}", name, format);
520
521 match format {
522 "csv" => dataset.to_csv(&output)?,
523 "parquet" => dataset.to_parquet(&output)?,
524 "json" => dataset.to_json(&output)?,
525 _ => return Err(Error::InvalidFormat(format!("Unknown format: {}", format))),
526 }
527
528 println!("Converted to {}", output);
529 Ok(())
530 }
531
532 #[allow(clippy::unnecessary_wraps)]
533 fn cmd_datasets(&self) -> Result<()> {
534 if self.datasets.is_empty() {
535 println!("No datasets loaded. Use 'load <file>' to load one.");
536 } else {
537 println!("Loaded datasets:");
538 for name in self.datasets.keys() {
539 let marker = if Some(name) == self.active_name.as_ref() {
540 "* "
541 } else {
542 " "
543 };
544 if let Some(ds) = self.datasets.get(name) {
545 println!("{}{} ({} rows)", marker, name, ds.len());
546 }
547 }
548 }
549 Ok(())
550 }
551
552 #[allow(clippy::unnecessary_wraps)]
553 fn cmd_history(&self, export: bool) -> Result<()> {
554 if export {
555 print!("{}", self.export_history());
556 } else {
557 for (i, cmd) in self.history.iter().enumerate() {
558 println!("{:4} {}", i + 1, cmd);
559 }
560 }
561 Ok(())
562 }
563
564 #[allow(clippy::unnecessary_wraps, clippy::unused_self)]
565 fn cmd_help(&self, topic: Option<&str>) -> Result<()> {
566 match topic {
567 None => {
568 println!("alimentar REPL Commands:");
569 println!();
570 println!("Data Loading:");
571 println!(" load <file> Load a dataset (parquet, csv, json)");
572 println!(" datasets List loaded datasets");
573 println!(" use <name> Switch active dataset");
574 println!();
575 println!("Data Inspection:");
576 println!(" info Show dataset metadata");
577 println!(" head [n] Show first n rows (default: 10)");
578 println!(" schema Display column schema");
579 println!();
580 println!("Quality (Andon):");
581 println!(" quality check Run quality checks");
582 println!(" quality score 100-point quality score");
583 println!(" --suggest Show improvement suggestions");
584 println!(" --json Output as JSON");
585 println!(" --badge Output shields.io badge URL");
586 println!();
587 println!("Analysis:");
588 println!(" drift detect <ref> Compare with reference dataset");
589 println!(" convert <format> Export to format (csv, parquet, json)");
590 println!();
591 println!("Pipeline (Batuta):");
592 println!(" export quality --json Export quality for PMAT");
593 println!(" validate --schema <f> Validate against schema spec");
594 println!();
595 println!("Session:");
596 println!(" history Show command history");
597 println!(" history --export Export as reproducible script");
598 println!(" help [topic] Show help (topics: quality, drift, export)");
599 println!(" quit, exit Exit REPL");
600 }
601 Some("quality") => {
602 println!("Quality Commands (Jidoka - Built-in Quality):");
603 println!();
604 println!("The quality system provides a 100-point scoring based on:");
605 println!(" - Critical (2x weight): Blocks publication");
606 println!(" - High (1.5x weight): Needs immediate attention");
607 println!(" - Medium (1x weight): Should fix before publish");
608 println!(" - Low (0.5x weight): Minor/informational");
609 println!();
610 println!("Letter Grades:");
611 println!(" A (95+): Publish immediately");
612 println!(" B (85-94): Publish with caveats");
613 println!(" C (70-84): Remediation required");
614 println!(" D (50-69): Major rework needed");
615 println!(" F (<50): Do not publish");
616 }
617 Some("drift") => {
618 println!("Drift Detection:");
619 println!();
620 println!("Compare your active dataset against a reference to detect:");
621 println!(" - Statistical distribution changes");
622 println!(" - Schema differences");
623 println!(" - Value range shifts");
624 println!();
625 println!("Usage: drift detect <reference.parquet>");
626 }
627 Some("export") => {
628 println!("Export Commands (Batuta Integration):");
629 println!();
630 println!(" export quality --json Quality metrics for PMAT");
631 println!(" validate --schema <f> Pre-transpilation validation");
632 println!(" history --export Reproducible session script");
633 }
634 Some(t) => {
635 println!("Unknown help topic: '{}'. Try: quality, drift, export", t);
636 }
637 }
638 Ok(())
639 }
640
641 fn cmd_export(&self, what: &str, json: bool) -> Result<()> {
642 match what {
643 "quality" => {
644 if let Some(cache) = &self.quality_cache {
645 if json {
646 println!("{}", cache.score.to_json());
647 } else {
648 println!("Quality: {} ({:.1})", cache.score.grade, cache.score.score);
649 }
650 Ok(())
651 } else {
652 Err(Error::NotFound("No quality data available".to_string()))
653 }
654 }
655 _ => Err(Error::InvalidFormat(format!(
656 "Unknown export type: {}",
657 what
658 ))),
659 }
660 }
661
662 fn cmd_validate(&self, schema_path: &str) -> Result<()> {
663 let _ = self.require_active()?;
664 if std::path::Path::new(schema_path).exists() {
666 println!(
667 "Validation against {} - Feature in development",
668 schema_path
669 );
670 Ok(())
671 } else {
672 Err(Error::NotFound(format!(
673 "Schema file not found: {}",
674 schema_path
675 )))
676 }
677 }
678
679 fn require_active(&self) -> Result<&Arc<ArrowDataset>> {
680 self.active_dataset().ok_or_else(|| {
681 Error::NotFound("No active dataset. Use 'load <file>' first.".to_string())
682 })
683 }
684}
685
686fn chrono_now() -> String {
688 use std::time::{SystemTime, UNIX_EPOCH};
689
690 let duration = SystemTime::now()
691 .duration_since(UNIX_EPOCH)
692 .unwrap_or_default();
693
694 format!("{}s since epoch", duration.as_secs())
695}
696
697fn load_dataset_from_path(path: &str) -> Result<ArrowDataset> {
699 let path = Path::new(path);
700 let extension = path
701 .extension()
702 .and_then(|e| e.to_str())
703 .unwrap_or("")
704 .to_lowercase();
705
706 match extension.as_str() {
707 "parquet" | "pq" => ArrowDataset::from_parquet(path),
708 "csv" => ArrowDataset::from_csv(path),
709 "json" | "jsonl" => ArrowDataset::from_json(path),
710 _ => Err(Error::unsupported_format(format!(
711 "Unknown file extension: .{}. Supported: parquet, csv, json",
712 extension
713 ))),
714 }
715}
716
717#[cfg(test)]
718mod tests {
719 use std::sync::Arc;
720
721 use arrow::{
722 array::{Float64Array, Int32Array, StringArray},
723 datatypes::{DataType, Field, Schema as ArrowSchema},
724 record_batch::RecordBatch,
725 };
726
727 use super::*;
728
729 fn create_test_dataset() -> ArrowDataset {
730 let schema = Arc::new(ArrowSchema::new(vec![
731 Field::new("id", DataType::Int32, false),
732 Field::new("name", DataType::Utf8, true),
733 Field::new("value", DataType::Float64, true),
734 ]));
735
736 let batch = RecordBatch::try_new(
737 schema.clone(),
738 vec![
739 Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])),
740 Arc::new(StringArray::from(vec![
741 Some("a"),
742 Some("b"),
743 None,
744 Some("d"),
745 Some("e"),
746 ])),
747 Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0])),
748 ],
749 )
750 .unwrap();
751
752 ArrowDataset::new(vec![batch]).unwrap()
753 }
754
755 #[test]
757 fn test_display_config_default() {
758 let config = DisplayConfig::default();
759 assert_eq!(config.max_rows, 10);
760 assert_eq!(config.max_column_width, 50);
761 assert!(config.color_output);
762 }
763
764 #[test]
765 fn test_display_config_with_max_rows() {
766 let config = DisplayConfig::default().with_max_rows(20);
767 assert_eq!(config.max_rows, 20);
768 }
769
770 #[test]
771 fn test_display_config_with_max_column_width() {
772 let config = DisplayConfig::default().with_max_column_width(100);
773 assert_eq!(config.max_column_width, 100);
774 }
775
776 #[test]
777 fn test_display_config_with_color() {
778 let config = DisplayConfig::default().with_color(false);
779 assert!(!config.color_output);
780 }
781
782 #[test]
783 fn test_display_config_chained() {
784 let config = DisplayConfig::default()
785 .with_max_rows(25)
786 .with_max_column_width(75)
787 .with_color(false);
788 assert_eq!(config.max_rows, 25);
789 assert_eq!(config.max_column_width, 75);
790 assert!(!config.color_output);
791 }
792
793 #[test]
795 fn test_session_new() {
796 let session = ReplSession::new();
797 assert!(session.datasets().is_empty());
798 assert!(session.active_name().is_none());
799 assert!(session.active_dataset().is_none());
800 assert!(session.history().is_empty());
801 }
802
803 #[test]
804 fn test_session_default() {
805 let session = ReplSession::default();
806 assert!(session.datasets().is_empty());
807 }
808
809 #[test]
810 fn test_session_load_dataset() {
811 let mut session = ReplSession::new();
812 let dataset = create_test_dataset();
813
814 session.load_dataset("test", dataset);
815
816 assert_eq!(session.datasets().len(), 1);
817 assert!(session.datasets().contains(&"test".to_string()));
818 assert_eq!(session.active_name(), Some("test".to_string()));
819 assert!(session.active_dataset().is_some());
820 }
821
822 #[test]
823 fn test_session_use_dataset_success() {
824 let mut session = ReplSession::new();
825 session.load_dataset("ds1", create_test_dataset());
826 session.load_dataset("ds2", create_test_dataset());
827
828 assert_eq!(session.active_name(), Some("ds2".to_string()));
829
830 session.use_dataset("ds1").unwrap();
831 assert_eq!(session.active_name(), Some("ds1".to_string()));
832 }
833
834 #[test]
835 fn test_session_use_dataset_not_found() {
836 let mut session = ReplSession::new();
837 let result = session.use_dataset("nonexistent");
838 assert!(result.is_err());
839 }
840
841 #[test]
842 fn test_session_active_row_count() {
843 let mut session = ReplSession::new();
844 assert!(session.active_row_count().is_none());
845
846 session.load_dataset("test", create_test_dataset());
847 assert_eq!(session.active_row_count(), Some(5));
848 }
849
850 #[test]
851 fn test_session_active_grade() {
852 let mut session = ReplSession::new();
853 assert!(session.active_grade().is_none());
854
855 session.load_dataset("test", create_test_dataset());
856 assert!(session.active_grade().is_some());
858 }
859
860 #[test]
861 fn test_session_column_names() {
862 let mut session = ReplSession::new();
863 assert!(session.column_names().is_empty());
864
865 session.load_dataset("test", create_test_dataset());
866 let cols = session.column_names();
867 assert_eq!(cols.len(), 3);
868 assert!(cols.contains(&"id".to_string()));
869 assert!(cols.contains(&"name".to_string()));
870 assert!(cols.contains(&"value".to_string()));
871 }
872
873 #[test]
874 fn test_session_history() {
875 let mut session = ReplSession::new();
876
877 session.add_history("load test.parquet");
878 session.add_history("head 5");
879 session.add_history("schema");
880
881 assert_eq!(session.history().len(), 3);
882 assert_eq!(session.history()[0], "load test.parquet");
883 assert_eq!(session.history()[1], "head 5");
884 assert_eq!(session.history()[2], "schema");
885 }
886
887 #[test]
888 fn test_session_quality_cache() {
889 let mut session = ReplSession::new();
890 assert!(session.quality_cache().is_none());
891
892 session.load_dataset("test", create_test_dataset());
893 assert!(session.quality_cache().is_some());
894 }
895
896 #[test]
897 fn test_session_export_history() {
898 let mut session = ReplSession::new();
899 session.add_history("load test.parquet");
900 session.add_history("head 5");
901
902 let script = session.export_history();
903 assert!(script.starts_with("#!/usr/bin/env bash"));
904 assert!(script.contains("alimentar load test.parquet"));
905 assert!(script.contains("alimentar head 5"));
906 }
907
908 #[test]
909 fn test_session_export_history_with_active_dataset() {
910 let mut session = ReplSession::new();
911 session.load_dataset("test.parquet", create_test_dataset());
912 session.add_history("info");
913 session.add_history("schema");
914 session.add_history("head");
915
916 let script = session.export_history();
917 assert!(script.contains("alimentar info test.parquet"));
918 assert!(script.contains("alimentar schema test.parquet"));
919 assert!(script.contains("alimentar head test.parquet"));
920 }
921
922 #[test]
923 fn test_session_repl_to_batch_empty() {
924 let session = ReplSession::new();
925 assert_eq!(session.repl_to_batch(""), "");
926 }
927
928 #[test]
929 fn test_session_repl_to_batch_load() {
930 let session = ReplSession::new();
931 assert_eq!(
932 session.repl_to_batch("load test.parquet"),
933 "load test.parquet"
934 );
935 }
936
937 #[test]
938 fn test_session_repl_to_batch_quality_with_args() {
939 let mut session = ReplSession::new();
940 session.load_dataset("data.parquet", create_test_dataset());
941
942 assert_eq!(
943 session.repl_to_batch("quality check"),
944 "quality check data.parquet"
945 );
946 }
947
948 #[test]
949 fn test_session_repl_to_batch_quality_no_active() {
950 let session = ReplSession::new();
951 assert_eq!(session.repl_to_batch("quality check"), "quality check");
952 }
953
954 #[test]
955 fn test_session_repl_to_batch_unknown_command() {
956 let session = ReplSession::new();
957 assert_eq!(session.repl_to_batch("custom cmd"), "custom cmd");
958 }
959
960 #[test]
962 fn test_chrono_now() {
963 let now = chrono_now();
964 assert!(now.contains("since epoch"));
965 assert!(now.chars().any(|c| c.is_ascii_digit()));
967 }
968
969 #[test]
971 fn test_load_dataset_unknown_extension() {
972 let result = load_dataset_from_path("test.xyz");
973 assert!(result.is_err());
974 let err = result.unwrap_err().to_string();
975 assert!(err.contains("Unknown file extension"));
976 }
977
978 #[test]
979 fn test_load_dataset_no_extension() {
980 let result = load_dataset_from_path("test");
981 assert!(result.is_err());
982 }
983
984 #[test]
986 fn test_execute_info() {
987 let mut session = ReplSession::new();
988 session.load_dataset("test", create_test_dataset());
989
990 let result = session.execute(ReplCommand::Info);
991 assert!(result.is_ok());
992 }
993
994 #[test]
995 fn test_execute_info_no_dataset() {
996 let mut session = ReplSession::new();
997 let result = session.execute(ReplCommand::Info);
998 assert!(result.is_err());
999 }
1000
1001 #[test]
1002 fn test_execute_schema() {
1003 let mut session = ReplSession::new();
1004 session.load_dataset("test", create_test_dataset());
1005
1006 let result = session.execute(ReplCommand::Schema);
1007 assert!(result.is_ok());
1008 }
1009
1010 #[test]
1011 fn test_execute_head() {
1012 let mut session = ReplSession::new();
1013 session.load_dataset("test", create_test_dataset());
1014
1015 let result = session.execute(ReplCommand::Head { n: 3 });
1016 assert!(result.is_ok());
1017 }
1018
1019 #[test]
1020 fn test_execute_head_no_dataset() {
1021 let mut session = ReplSession::new();
1022 let result = session.execute(ReplCommand::Head { n: 3 });
1023 assert!(result.is_err());
1024 }
1025
1026 #[test]
1027 fn test_execute_quality_check() {
1028 let mut session = ReplSession::new();
1029 session.load_dataset("test", create_test_dataset());
1030
1031 let result = session.execute(ReplCommand::QualityCheck);
1032 assert!(result.is_ok());
1033 }
1034
1035 #[test]
1036 fn test_execute_quality_score() {
1037 let mut session = ReplSession::new();
1038 session.load_dataset("test", create_test_dataset());
1039
1040 let result = session.execute(ReplCommand::QualityScore {
1041 suggest: false,
1042 json: false,
1043 badge: false,
1044 });
1045 assert!(result.is_ok());
1046 }
1047
1048 #[test]
1049 fn test_execute_quality_score_suggest() {
1050 let mut session = ReplSession::new();
1051 session.load_dataset("test", create_test_dataset());
1052
1053 let result = session.execute(ReplCommand::QualityScore {
1054 suggest: true,
1055 json: false,
1056 badge: false,
1057 });
1058 assert!(result.is_ok());
1059 }
1060
1061 #[test]
1062 fn test_execute_quality_score_json() {
1063 let mut session = ReplSession::new();
1064 session.load_dataset("test", create_test_dataset());
1065
1066 let result = session.execute(ReplCommand::QualityScore {
1067 suggest: false,
1068 json: true,
1069 badge: false,
1070 });
1071 assert!(result.is_ok());
1072 }
1073
1074 #[test]
1075 fn test_execute_quality_score_badge() {
1076 let mut session = ReplSession::new();
1077 session.load_dataset("test", create_test_dataset());
1078
1079 let result = session.execute(ReplCommand::QualityScore {
1080 suggest: false,
1081 json: false,
1082 badge: true,
1083 });
1084 assert!(result.is_ok());
1085 }
1086
1087 #[test]
1088 fn test_execute_quality_score_no_cache() {
1089 let mut session = ReplSession::new();
1090 let result = session.execute(ReplCommand::QualityScore {
1091 suggest: false,
1092 json: false,
1093 badge: false,
1094 });
1095 assert!(result.is_err());
1096 }
1097
1098 #[test]
1099 fn test_execute_datasets_empty() {
1100 let mut session = ReplSession::new();
1101 let result = session.execute(ReplCommand::Datasets);
1102 assert!(result.is_ok());
1103 }
1104
1105 #[test]
1106 fn test_execute_datasets_with_data() {
1107 let mut session = ReplSession::new();
1108 session.load_dataset("ds1", create_test_dataset());
1109 session.load_dataset("ds2", create_test_dataset());
1110
1111 let result = session.execute(ReplCommand::Datasets);
1112 assert!(result.is_ok());
1113 }
1114
1115 #[test]
1116 fn test_execute_history() {
1117 let mut session = ReplSession::new();
1118 session.add_history("test cmd");
1119
1120 let result = session.execute(ReplCommand::History { export: false });
1121 assert!(result.is_ok());
1122 }
1123
1124 #[test]
1125 fn test_execute_history_export() {
1126 let mut session = ReplSession::new();
1127 session.add_history("test cmd");
1128
1129 let result = session.execute(ReplCommand::History { export: true });
1130 assert!(result.is_ok());
1131 }
1132
1133 #[test]
1134 fn test_execute_help_none() {
1135 let mut session = ReplSession::new();
1136 let result = session.execute(ReplCommand::Help { topic: None });
1137 assert!(result.is_ok());
1138 }
1139
1140 #[test]
1141 fn test_execute_help_quality() {
1142 let mut session = ReplSession::new();
1143 let result = session.execute(ReplCommand::Help {
1144 topic: Some("quality".to_string()),
1145 });
1146 assert!(result.is_ok());
1147 }
1148
1149 #[test]
1150 fn test_execute_help_drift() {
1151 let mut session = ReplSession::new();
1152 let result = session.execute(ReplCommand::Help {
1153 topic: Some("drift".to_string()),
1154 });
1155 assert!(result.is_ok());
1156 }
1157
1158 #[test]
1159 fn test_execute_help_export() {
1160 let mut session = ReplSession::new();
1161 let result = session.execute(ReplCommand::Help {
1162 topic: Some("export".to_string()),
1163 });
1164 assert!(result.is_ok());
1165 }
1166
1167 #[test]
1168 fn test_execute_help_unknown() {
1169 let mut session = ReplSession::new();
1170 let result = session.execute(ReplCommand::Help {
1171 topic: Some("unknown".to_string()),
1172 });
1173 assert!(result.is_ok());
1174 }
1175
1176 #[test]
1177 fn test_execute_export_quality() {
1178 let mut session = ReplSession::new();
1179 session.load_dataset("test", create_test_dataset());
1180
1181 let result = session.execute(ReplCommand::Export {
1182 what: "quality".to_string(),
1183 json: false,
1184 });
1185 assert!(result.is_ok());
1186 }
1187
1188 #[test]
1189 fn test_execute_export_quality_json() {
1190 let mut session = ReplSession::new();
1191 session.load_dataset("test", create_test_dataset());
1192
1193 let result = session.execute(ReplCommand::Export {
1194 what: "quality".to_string(),
1195 json: true,
1196 });
1197 assert!(result.is_ok());
1198 }
1199
1200 #[test]
1201 fn test_execute_export_quality_no_cache() {
1202 let mut session = ReplSession::new();
1203 let result = session.execute(ReplCommand::Export {
1204 what: "quality".to_string(),
1205 json: false,
1206 });
1207 assert!(result.is_err());
1208 }
1209
1210 #[test]
1211 fn test_execute_export_unknown() {
1212 let mut session = ReplSession::new();
1213 session.load_dataset("test", create_test_dataset());
1214
1215 let result = session.execute(ReplCommand::Export {
1216 what: "unknown".to_string(),
1217 json: false,
1218 });
1219 assert!(result.is_err());
1220 }
1221
1222 #[test]
1223 fn test_execute_validate_file_not_found() {
1224 let mut session = ReplSession::new();
1225 session.load_dataset("test", create_test_dataset());
1226
1227 let result = session.execute(ReplCommand::Validate {
1228 schema: "nonexistent.json".to_string(),
1229 });
1230 assert!(result.is_err());
1231 }
1232
1233 #[test]
1234 fn test_execute_convert_invalid_format() {
1235 let mut session = ReplSession::new();
1236 session.load_dataset("test", create_test_dataset());
1237
1238 let result = session.execute(ReplCommand::Convert {
1239 format: "invalid".to_string(),
1240 });
1241 assert!(result.is_err());
1242 }
1243
1244 #[test]
1245 fn test_execute_quit() {
1246 let mut session = ReplSession::new();
1247 let result = session.execute(ReplCommand::Quit);
1248 assert!(result.is_ok());
1249 }
1250
1251 #[test]
1252 fn test_execute_use() {
1253 let mut session = ReplSession::new();
1254 session.load_dataset("test", create_test_dataset());
1255
1256 let result = session.execute(ReplCommand::Use {
1257 name: "test".to_string(),
1258 });
1259 assert!(result.is_ok());
1260 }
1261
1262 #[test]
1263 fn test_execute_use_not_found() {
1264 let mut session = ReplSession::new();
1265 let result = session.execute(ReplCommand::Use {
1266 name: "nonexistent".to_string(),
1267 });
1268 assert!(result.is_err());
1269 }
1270
1271 #[test]
1272 fn test_require_active_none() {
1273 let session = ReplSession::new();
1274 let result = session.require_active();
1275 assert!(result.is_err());
1276 }
1277
1278 #[test]
1279 fn test_require_active_some() {
1280 let mut session = ReplSession::new();
1281 session.load_dataset("test", create_test_dataset());
1282
1283 let result = session.require_active();
1284 assert!(result.is_ok());
1285 }
1286
1287 #[test]
1289 fn test_multiple_datasets() {
1290 let mut session = ReplSession::new();
1291 session.load_dataset("first", create_test_dataset());
1292 session.load_dataset("second", create_test_dataset());
1293 session.load_dataset("third", create_test_dataset());
1294
1295 assert_eq!(session.datasets().len(), 3);
1296 assert_eq!(session.active_name(), Some("third".to_string()));
1297
1298 session.use_dataset("first").unwrap();
1299 assert_eq!(session.active_name(), Some("first".to_string()));
1300 }
1301
1302 #[test]
1304 fn test_build_basic_checklist() {
1305 let session = ReplSession::new();
1306 let dataset = create_test_dataset();
1307 let checker = QualityChecker::new();
1308 let report = checker.check(&dataset).unwrap();
1309
1310 let checklist = session.build_basic_checklist(&report);
1311 assert_eq!(checklist.len(), 5);
1312 }
1313}