1use sklears_core::prelude::SklearsError;
7use std::collections::HashMap;
8use std::fmt;
9
10#[derive(Debug, Clone)]
12pub enum PipelineError {
13 ConfigurationError {
15 message: String,
16 suggestions: Vec<String>,
17 context: ErrorContext,
18 },
19 DataCompatibilityError {
21 expected: DataShape,
22 actual: DataShape,
23 stage: String,
24 suggestions: Vec<String>,
25 },
26 StructureError {
28 error_type: StructureErrorType,
29 affected_components: Vec<String>,
30 suggestions: Vec<String>,
31 },
32 PerformanceWarning {
34 warning_type: PerformanceWarningType,
35 impact_level: ImpactLevel,
36 suggestions: Vec<String>,
37 metrics: Option<PerformanceMetrics>,
38 },
39 ResourceError {
41 resource_type: ResourceType,
42 limit: f64,
43 current: f64,
44 component: String,
45 suggestions: Vec<String>,
46 },
47 TypeSafetyError {
49 violation_type: TypeViolationType,
50 expected_type: String,
51 actual_type: String,
52 stage: String,
53 suggestions: Vec<String>,
54 },
55}
56
57#[derive(Debug, Clone)]
59pub struct ErrorContext {
60 pub pipeline_stage: String,
61 pub component_name: String,
62 pub input_shape: Option<(usize, usize)>,
63 pub parameters: HashMap<String, String>,
64 pub stack_trace: Vec<String>,
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct DataShape {
70 pub samples: usize,
71 pub features: usize,
72 pub data_type: String,
73 pub missing_values: bool,
74}
75
76#[derive(Debug, Clone)]
78pub enum StructureErrorType {
79 CyclicDependency,
81 MissingComponent,
83 InvalidConnection,
85 DanglingNode,
87 InconsistentFlow,
89}
90
91#[derive(Debug, Clone)]
93pub enum PerformanceWarningType {
94 MemoryUsage,
96 ComputationalComplexity,
98 NetworkBottleneck,
100 CacheInefficiency,
102 SuboptimalConfiguration,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
108pub enum ImpactLevel {
109 Low,
111 Medium,
113 High,
115 Critical,
117}
118
119#[derive(Debug, Clone)]
121pub enum ResourceType {
122 Memory,
124 CPU,
126 GPU,
128 Disk,
130 NetworkBandwidth,
132}
133
134#[derive(Debug, Clone)]
136pub struct PerformanceMetrics {
137 pub execution_time_ms: f64,
138 pub memory_usage_mb: f64,
139 pub cpu_utilization: f64,
140 pub cache_hit_ratio: f64,
141}
142
143#[derive(Debug, Clone)]
145pub enum TypeViolationType {
146 IncompatibleInputType,
148 MismatchedOutputType,
150 UnsupportedTransformation,
152 InvalidParameterType,
154}
155
156pub struct EnhancedErrorBuilder {
158 error_type: Option<PipelineError>,
159 suggestions: Vec<String>,
160 context: Option<ErrorContext>,
161}
162
163impl Default for EnhancedErrorBuilder {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl EnhancedErrorBuilder {
170 #[must_use]
171 pub fn new() -> Self {
172 Self {
173 error_type: None,
174 suggestions: Vec::new(),
175 context: None,
176 }
177 }
178
179 #[must_use]
181 pub fn configuration_error(mut self, message: &str) -> Self {
182 let suggestions = self.generate_configuration_suggestions(message);
183 self.error_type = Some(PipelineError::ConfigurationError {
184 message: message.to_string(),
185 suggestions,
186 context: self.context.clone().unwrap_or_default(),
187 });
188 self
189 }
190
191 #[must_use]
193 pub fn data_compatibility_error(
194 mut self,
195 expected: DataShape,
196 actual: DataShape,
197 stage: &str,
198 ) -> Self {
199 let suggestions = self.generate_compatibility_suggestions(&expected, &actual, stage);
200 self.error_type = Some(PipelineError::DataCompatibilityError {
201 expected,
202 actual,
203 stage: stage.to_string(),
204 suggestions,
205 });
206 self
207 }
208
209 #[must_use]
211 pub fn structure_error(
212 mut self,
213 error_type: StructureErrorType,
214 affected_components: Vec<String>,
215 ) -> Self {
216 let suggestions = self.generate_structure_suggestions(&error_type, &affected_components);
217 self.error_type = Some(PipelineError::StructureError {
218 error_type,
219 affected_components,
220 suggestions,
221 });
222 self
223 }
224
225 #[must_use]
227 pub fn performance_warning(
228 mut self,
229 warning_type: PerformanceWarningType,
230 impact_level: ImpactLevel,
231 metrics: Option<PerformanceMetrics>,
232 ) -> Self {
233 let suggestions =
234 self.generate_performance_suggestions(&warning_type, &impact_level, &metrics);
235 self.error_type = Some(PipelineError::PerformanceWarning {
236 warning_type,
237 impact_level,
238 suggestions,
239 metrics,
240 });
241 self
242 }
243
244 #[must_use]
246 pub fn resource_error(
247 mut self,
248 resource_type: ResourceType,
249 limit: f64,
250 current: f64,
251 component: &str,
252 ) -> Self {
253 let suggestions =
254 self.generate_resource_suggestions(&resource_type, limit, current, component);
255 self.error_type = Some(PipelineError::ResourceError {
256 resource_type,
257 limit,
258 current,
259 component: component.to_string(),
260 suggestions,
261 });
262 self
263 }
264
265 #[must_use]
267 pub fn type_safety_error(
268 mut self,
269 violation_type: TypeViolationType,
270 expected_type: &str,
271 actual_type: &str,
272 stage: &str,
273 ) -> Self {
274 let suggestions = self.generate_type_safety_suggestions(
275 &violation_type,
276 expected_type,
277 actual_type,
278 stage,
279 );
280 self.error_type = Some(PipelineError::TypeSafetyError {
281 violation_type,
282 expected_type: expected_type.to_string(),
283 actual_type: actual_type.to_string(),
284 stage: stage.to_string(),
285 suggestions,
286 });
287 self
288 }
289
290 #[must_use]
292 pub fn suggestion(mut self, suggestion: &str) -> Self {
293 self.suggestions.push(suggestion.to_string());
294 self
295 }
296
297 #[must_use]
299 pub fn context(mut self, context: ErrorContext) -> Self {
300 self.context = Some(context);
301 self
302 }
303
304 #[must_use]
306 pub fn build(self) -> PipelineError {
307 if let Some(mut error) = self.error_type {
309 match &mut error {
311 PipelineError::ConfigurationError { suggestions, .. } => {
312 suggestions.extend(self.suggestions);
313 }
314 PipelineError::DataCompatibilityError { suggestions, .. } => {
315 suggestions.extend(self.suggestions);
316 }
317 PipelineError::StructureError { suggestions, .. } => {
318 suggestions.extend(self.suggestions);
319 }
320 PipelineError::PerformanceWarning { suggestions, .. } => {
321 suggestions.extend(self.suggestions);
322 }
323 PipelineError::ResourceError { suggestions, .. } => {
324 suggestions.extend(self.suggestions);
325 }
326 PipelineError::TypeSafetyError { suggestions, .. } => {
327 suggestions.extend(self.suggestions);
328 }
329 }
330 if let PipelineError::ConfigurationError { context, .. } = &mut error {
332 if let Some(new_context) = self.context {
333 *context = new_context;
334 }
335 }
336 error
337 } else {
338 PipelineError::ConfigurationError {
340 message: "Unknown pipeline error".to_string(),
341 suggestions: self.suggestions,
342 context: self.context.unwrap_or_default(),
343 }
344 }
345 }
346
347 fn generate_configuration_suggestions(&self, message: &str) -> Vec<String> {
349 let mut suggestions = Vec::new();
350
351 if message.contains("parameter") {
352 suggestions.push("Check parameter names and types in the documentation".to_string());
353 suggestions.push("Use the builder pattern to set parameters safely".to_string());
354 suggestions.push("Validate parameter ranges before setting".to_string());
355 }
356
357 if message.contains("missing") {
358 suggestions
359 .push("Ensure all required components are added to the pipeline".to_string());
360 suggestions.push("Check the pipeline construction order".to_string());
361 }
362
363 if message.contains("incompatible") {
364 suggestions
365 .push("Verify component compatibility using the validation tools".to_string());
366 suggestions
367 .push("Consider adding adapter components between incompatible stages".to_string());
368 }
369
370 suggestions
371 }
372
373 fn generate_compatibility_suggestions(
375 &self,
376 expected: &DataShape,
377 actual: &DataShape,
378 stage: &str,
379 ) -> Vec<String> {
380 let mut suggestions = Vec::new();
381
382 if expected.features != actual.features {
383 suggestions.push(format!(
384 "Feature count mismatch in '{}': expected {}, got {}",
385 stage, expected.features, actual.features
386 ));
387 suggestions.push(
388 "Consider adding feature selection or expansion before this stage".to_string(),
389 );
390 suggestions.push(
391 "Check if previous pipeline stages modified the feature count unexpectedly"
392 .to_string(),
393 );
394 }
395
396 if expected.samples != actual.samples {
397 suggestions.push(format!(
398 "Sample count mismatch in '{}': expected {}, got {}",
399 stage, expected.samples, actual.samples
400 ));
401 suggestions.push("Verify data splitting and sampling operations".to_string());
402 }
403
404 if expected.data_type != actual.data_type {
405 suggestions.push(format!(
406 "Data type mismatch in '{}': expected {}, got {}",
407 stage, expected.data_type, actual.data_type
408 ));
409 suggestions.push("Add type conversion transformers before this stage".to_string());
410 }
411
412 if actual.missing_values && !expected.missing_values {
413 suggestions.push(
414 "Handle missing values using imputation or removal before this stage".to_string(),
415 );
416 suggestions
417 .push("Consider using robust algorithms that handle missing data".to_string());
418 }
419
420 suggestions
421 }
422
423 fn generate_structure_suggestions(
425 &self,
426 error_type: &StructureErrorType,
427 affected_components: &[String],
428 ) -> Vec<String> {
429 let mut suggestions = Vec::new();
430
431 match error_type {
432 StructureErrorType::CyclicDependency => {
433 suggestions.push("Remove circular dependencies between components".to_string());
434 suggestions
435 .push("Use topological sorting to validate pipeline structure".to_string());
436 suggestions.push(format!(
437 "Affected components: {}",
438 affected_components.join(", ")
439 ));
440 }
441 StructureErrorType::MissingComponent => {
442 suggestions.push("Add the missing component to the pipeline".to_string());
443 suggestions.push("Check component names for typos".to_string());
444 suggestions
445 .push("Verify component registration in the pipeline builder".to_string());
446 }
447 StructureErrorType::InvalidConnection => {
448 suggestions.push("Check connection compatibility between components".to_string());
449 suggestions.push("Ensure output types match input requirements".to_string());
450 suggestions.push("Consider adding adapter components".to_string());
451 }
452 StructureErrorType::DanglingNode => {
453 suggestions.push("Connect all nodes to the main pipeline flow".to_string());
454 suggestions.push("Remove unused components or connect them properly".to_string());
455 }
456 StructureErrorType::InconsistentFlow => {
457 suggestions.push("Review the entire pipeline flow for consistency".to_string());
458 suggestions.push("Use pipeline validation tools before execution".to_string());
459 }
460 }
461
462 suggestions
463 }
464
465 fn generate_performance_suggestions(
467 &self,
468 warning_type: &PerformanceWarningType,
469 impact_level: &ImpactLevel,
470 metrics: &Option<PerformanceMetrics>,
471 ) -> Vec<String> {
472 let mut suggestions = Vec::new();
473
474 match warning_type {
475 PerformanceWarningType::MemoryUsage => {
476 suggestions
477 .push("Consider using streaming processing for large datasets".to_string());
478 suggestions.push("Enable memory-efficient pipeline execution".to_string());
479 suggestions.push("Use data chunking to reduce memory footprint".to_string());
480
481 if let Some(metrics) = metrics {
482 if metrics.memory_usage_mb > 1000.0 {
483 suggestions.push(
484 "Memory usage exceeds 1GB - consider distributed processing"
485 .to_string(),
486 );
487 }
488 }
489 }
490 PerformanceWarningType::ComputationalComplexity => {
491 suggestions.push("Use parallel processing where possible".to_string());
492 suggestions.push("Consider simpler algorithms for large datasets".to_string());
493 suggestions.push("Enable SIMD optimizations if available".to_string());
494 }
495 PerformanceWarningType::NetworkBottleneck => {
496 suggestions.push("Implement result caching to reduce network calls".to_string());
497 suggestions.push("Use connection pooling for external services".to_string());
498 suggestions.push("Consider local processing alternatives".to_string());
499 }
500 PerformanceWarningType::CacheInefficiency => {
501 suggestions.push("Optimize cache configuration and size".to_string());
502 suggestions.push("Review cache key generation strategy".to_string());
503 suggestions.push("Consider different cache eviction policies".to_string());
504
505 if let Some(metrics) = metrics {
506 if metrics.cache_hit_ratio < 0.5 {
507 suggestions.push(format!(
508 "Low cache hit ratio ({:.1}%) - review caching strategy",
509 metrics.cache_hit_ratio * 100.0
510 ));
511 }
512 }
513 }
514 PerformanceWarningType::SuboptimalConfiguration => {
515 suggestions
516 .push("Run hyperparameter optimization to find better settings".to_string());
517 suggestions.push("Use AutoML tools for automatic configuration tuning".to_string());
518 suggestions.push("Profile different configuration options".to_string());
519 }
520 }
521
522 match impact_level {
523 ImpactLevel::Critical => {
524 suggestions.insert(
525 0,
526 "🚨 CRITICAL: This issue requires immediate attention".to_string(),
527 );
528 }
529 ImpactLevel::High => {
530 suggestions.insert(0, "⚠️ HIGH PRIORITY: Address this issue soon".to_string());
531 }
532 _ => {}
533 }
534
535 suggestions
536 }
537
538 fn generate_resource_suggestions(
540 &self,
541 resource_type: &ResourceType,
542 limit: f64,
543 current: f64,
544 component: &str,
545 ) -> Vec<String> {
546 let mut suggestions = Vec::new();
547 let utilization = (current / limit) * 100.0;
548
549 suggestions.push(format!(
550 "Resource utilization: {utilization:.1}% ({current:.2}/{limit:.2}) in component '{component}'"
551 ));
552
553 match resource_type {
554 ResourceType::Memory => {
555 suggestions.push("Reduce batch size to lower memory usage".to_string());
556 suggestions.push("Enable streaming processing mode".to_string());
557 suggestions.push("Use memory-mapped files for large datasets".to_string());
558 if utilization > 90.0 {
559 suggestions.push(
560 "Consider upgrading system memory or using distributed processing"
561 .to_string(),
562 );
563 }
564 }
565 ResourceType::CPU => {
566 suggestions.push("Reduce computational complexity of algorithms".to_string());
567 suggestions.push("Enable parallel processing across multiple cores".to_string());
568 suggestions.push("Use approximate algorithms for faster computation".to_string());
569 }
570 ResourceType::GPU => {
571 suggestions.push("Optimize GPU memory allocation and transfers".to_string());
572 suggestions
573 .push("Use mixed precision training to reduce GPU memory usage".to_string());
574 suggestions.push("Consider model parallelism for large models".to_string());
575 }
576 ResourceType::Disk => {
577 suggestions.push("Enable data compression to reduce disk usage".to_string());
578 suggestions.push("Use temporary file cleanup strategies".to_string());
579 suggestions.push("Consider cloud storage for large datasets".to_string());
580 }
581 ResourceType::NetworkBandwidth => {
582 suggestions.push("Implement data compression for network transfers".to_string());
583 suggestions.push("Use local caching to reduce network usage".to_string());
584 suggestions.push("Consider edge processing to minimize data movement".to_string());
585 }
586 }
587
588 suggestions
589 }
590
591 fn generate_type_safety_suggestions(
593 &self,
594 violation_type: &TypeViolationType,
595 expected_type: &str,
596 actual_type: &str,
597 stage: &str,
598 ) -> Vec<String> {
599 let mut suggestions = Vec::new();
600
601 suggestions.push(format!(
602 "Type mismatch in '{stage}': expected '{expected_type}', got '{actual_type}'"
603 ));
604
605 match violation_type {
606 TypeViolationType::IncompatibleInputType => {
607 suggestions.push("Add type conversion transformer before this stage".to_string());
608 suggestions
609 .push("Check the output type of the previous pipeline stage".to_string());
610 suggestions.push("Use type adapters for incompatible formats".to_string());
611 }
612 TypeViolationType::MismatchedOutputType => {
613 suggestions.push("Verify the component's output type specification".to_string());
614 suggestions.push("Use type casting or conversion as the final step".to_string());
615 suggestions.push("Check if the component configuration is correct".to_string());
616 }
617 TypeViolationType::UnsupportedTransformation => {
618 suggestions.push(
619 "Use a different transformation that supports this data type".to_string(),
620 );
621 suggestions.push("Preprocess the data to a supported type".to_string());
622 suggestions.push("Consider using a custom transformer".to_string());
623 }
624 TypeViolationType::InvalidParameterType => {
625 suggestions.push("Check parameter type requirements in documentation".to_string());
626 suggestions.push("Use proper type conversion for parameter values".to_string());
627 suggestions
628 .push("Validate parameter types before pipeline construction".to_string());
629 }
630 }
631
632 if expected_type.contains("float") && actual_type.contains("int") {
634 suggestions
635 .push("Convert integer values to float using .mapv(|x| x as f64)".to_string());
636 } else if expected_type.contains("int") && actual_type.contains("float") {
637 suggestions
638 .push("Convert float values to integer (with rounding if needed)".to_string());
639 }
640
641 suggestions
642 }
643}
644
645impl Default for ErrorContext {
646 fn default() -> Self {
647 Self {
648 pipeline_stage: "unknown".to_string(),
649 component_name: "unknown".to_string(),
650 input_shape: None,
651 parameters: HashMap::new(),
652 stack_trace: Vec::new(),
653 }
654 }
655}
656
657impl fmt::Display for PipelineError {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 match self {
660 PipelineError::ConfigurationError {
661 message,
662 suggestions,
663 context,
664 } => {
665 writeln!(f, "🔧 Configuration Error: {message}")?;
666 writeln!(
667 f,
668 " Component: {} (Stage: {})",
669 context.component_name, context.pipeline_stage
670 )?;
671 if !suggestions.is_empty() {
672 writeln!(f, "💡 Suggestions:")?;
673 for suggestion in suggestions {
674 writeln!(f, " • {suggestion}")?;
675 }
676 }
677 }
678 PipelineError::DataCompatibilityError {
679 expected,
680 actual,
681 stage,
682 suggestions,
683 } => {
684 writeln!(f, "📊 Data Compatibility Error in stage '{stage}':")?;
685 writeln!(
686 f,
687 " Expected: {} samples × {} features ({})",
688 expected.samples, expected.features, expected.data_type
689 )?;
690 writeln!(
691 f,
692 " Actual: {} samples × {} features ({})",
693 actual.samples, actual.features, actual.data_type
694 )?;
695 if !suggestions.is_empty() {
696 writeln!(f, "💡 Suggestions:")?;
697 for suggestion in suggestions {
698 writeln!(f, " • {suggestion}")?;
699 }
700 }
701 }
702 PipelineError::StructureError {
703 error_type,
704 affected_components,
705 suggestions,
706 } => {
707 writeln!(f, "🏗️ Pipeline Structure Error: {error_type:?}")?;
708 writeln!(
709 f,
710 " Affected components: {}",
711 affected_components.join(", ")
712 )?;
713 if !suggestions.is_empty() {
714 writeln!(f, "💡 Suggestions:")?;
715 for suggestion in suggestions {
716 writeln!(f, " • {suggestion}")?;
717 }
718 }
719 }
720 PipelineError::PerformanceWarning {
721 warning_type,
722 impact_level,
723 suggestions,
724 metrics,
725 } => {
726 writeln!(
727 f,
728 "⚡ Performance Warning: {warning_type:?} (Impact: {impact_level:?})"
729 )?;
730 if let Some(metrics) = metrics {
731 writeln!(
732 f,
733 " Execution time: {:.2}ms, Memory: {:.1}MB, CPU: {:.1}%",
734 metrics.execution_time_ms, metrics.memory_usage_mb, metrics.cpu_utilization
735 )?;
736 }
737 if !suggestions.is_empty() {
738 writeln!(f, "💡 Suggestions:")?;
739 for suggestion in suggestions {
740 writeln!(f, " • {suggestion}")?;
741 }
742 }
743 }
744 PipelineError::ResourceError {
745 resource_type,
746 limit,
747 current,
748 component,
749 suggestions,
750 } => {
751 let utilization = (current / limit) * 100.0;
752 writeln!(
753 f,
754 "🔋 Resource Error: {resource_type:?} constraint violated in '{component}'"
755 )?;
756 writeln!(f, " Usage: {current:.2}/{limit:.2} ({utilization:.1}%)")?;
757 if !suggestions.is_empty() {
758 writeln!(f, "💡 Suggestions:")?;
759 for suggestion in suggestions {
760 writeln!(f, " • {suggestion}")?;
761 }
762 }
763 }
764 PipelineError::TypeSafetyError {
765 violation_type,
766 expected_type,
767 actual_type,
768 stage,
769 suggestions,
770 } => {
771 writeln!(
772 f,
773 "🛡️ Type Safety Error: {violation_type:?} in stage '{stage}'"
774 )?;
775 writeln!(f, " Expected: {expected_type}, Got: {actual_type}")?;
776 if !suggestions.is_empty() {
777 writeln!(f, "💡 Suggestions:")?;
778 for suggestion in suggestions {
779 writeln!(f, " • {suggestion}")?;
780 }
781 }
782 }
783 }
784 Ok(())
785 }
786}
787
788impl From<PipelineError> for SklearsError {
789 fn from(error: PipelineError) -> Self {
790 SklearsError::InvalidInput(error.to_string())
791 }
792}
793
794impl PipelineError {
796 #[must_use]
798 pub fn configuration(message: &str) -> Self {
799 EnhancedErrorBuilder::new()
800 .configuration_error(message)
801 .build()
802 }
803
804 #[must_use]
806 pub fn data_compatibility(expected: DataShape, actual: DataShape, stage: &str) -> Self {
807 EnhancedErrorBuilder::new()
808 .data_compatibility_error(expected, actual, stage)
809 .build()
810 }
811
812 #[must_use]
814 pub fn performance_warning(
815 warning_type: PerformanceWarningType,
816 impact_level: ImpactLevel,
817 metrics: Option<PerformanceMetrics>,
818 ) -> Self {
819 EnhancedErrorBuilder::new()
820 .performance_warning(warning_type, impact_level, metrics)
821 .build()
822 }
823}
824
825#[allow(non_snake_case)]
826#[cfg(test)]
827mod tests {
828 use super::*;
829
830 #[test]
831 fn test_configuration_error_creation() {
832 let error = PipelineError::configuration("Invalid parameter 'learning_rate'");
833
834 let error_string = error.to_string();
835 assert!(error_string.contains("Configuration Error"));
836 assert!(error_string.contains("Invalid parameter"));
837 assert!(error_string.contains("💡 Suggestions:"));
838 }
839
840 #[test]
841 fn test_data_compatibility_error() {
842 let expected = DataShape {
843 samples: 100,
844 features: 10,
845 data_type: "float64".to_string(),
846 missing_values: false,
847 };
848 let actual = DataShape {
849 samples: 100,
850 features: 8,
851 data_type: "float64".to_string(),
852 missing_values: true,
853 };
854
855 let error = PipelineError::data_compatibility(expected, actual, "feature_selection");
856 let error_string = error.to_string();
857
858 assert!(error_string.contains("Data Compatibility Error"));
859 assert!(error_string.contains("feature_selection"));
860 assert!(error_string.contains("10 features"));
861 assert!(error_string.contains("8 features"));
862 }
863
864 #[test]
865 fn test_enhanced_error_builder() {
866 let context = ErrorContext {
867 pipeline_stage: "preprocessing".to_string(),
868 component_name: "scaler".to_string(),
869 input_shape: Some((100, 5)),
870 parameters: HashMap::new(),
871 stack_trace: Vec::new(),
872 };
873
874 let error = EnhancedErrorBuilder::new()
875 .configuration_error("Missing required parameter")
876 .suggestion("Check the documentation for required parameters")
877 .context(context)
878 .build();
879
880 let error_string = error.to_string();
881 println!("Error string: {}", error_string);
882 assert!(error_string.contains("Configuration Error"));
883 assert!(error_string.contains("Stage: preprocessing"));
884 assert!(error_string.contains("scaler"));
885 }
886}