use crate::notebook::testing::tester::NotebookTester;
use crate::notebook::testing::types::{Cell, CellOutput, CellType, Notebook};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct DifferentialConfig {
pub performance_threshold_ms: u64,
pub track_performance: bool,
}
impl Default for DifferentialConfig {
fn default() -> Self {
Self {
performance_threshold_ms: 100,
track_performance: true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DifferentialResult {
pub cell_id: String,
pub reference_output: CellOutput,
pub candidate_output: CellOutput,
pub divergence: DivergenceType,
pub reference_time: Duration,
pub candidate_time: Duration,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DivergenceType {
None,
OutputMismatch,
TypeMismatch,
PerformanceRegression,
BothFailed,
}
pub struct DifferentialTester {
reference: NotebookTester,
candidate: NotebookTester,
config: DifferentialConfig,
}
impl Default for DifferentialTester {
fn default() -> Self {
Self::new()
}
}
impl DifferentialTester {
pub fn new() -> Self {
Self {
reference: NotebookTester::new(),
candidate: NotebookTester::new(),
config: DifferentialConfig::default(),
}
}
pub fn with_config(config: DifferentialConfig) -> Self {
Self {
reference: NotebookTester::new(),
candidate: NotebookTester::new(),
config,
}
}
pub fn compare(&mut self, notebook: &Notebook) -> Vec<DifferentialResult> {
let mut results = Vec::new();
for cell in ¬ebook.cells {
if matches!(cell.cell_type, CellType::Markdown) {
continue;
}
let result = self.compare_cell(cell);
if !matches!(result.divergence, DivergenceType::None) {
results.push(result);
}
}
results
}
fn compare_cell(&mut self, cell: &Cell) -> DifferentialResult {
let ref_start = Instant::now();
let ref_output = self
.reference
.execute_cell(cell)
.unwrap_or_else(CellOutput::Error);
let ref_time = ref_start.elapsed();
let cand_start = Instant::now();
let cand_output = self
.candidate
.execute_cell(cell)
.unwrap_or_else(CellOutput::Error);
let cand_time = cand_start.elapsed();
let divergence = self.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
DifferentialResult {
cell_id: cell.id.clone(),
reference_output: ref_output,
candidate_output: cand_output,
divergence,
reference_time: ref_time,
candidate_time: cand_time,
}
}
fn classify_divergence(
&self,
ref_output: &CellOutput,
cand_output: &CellOutput,
ref_time: Duration,
cand_time: Duration,
) -> DivergenceType {
if ref_output != cand_output {
match (ref_output, cand_output) {
(CellOutput::Error(_), CellOutput::Error(_)) => DivergenceType::BothFailed,
(CellOutput::Value(_), CellOutput::Value(_))
| (CellOutput::DataFrame(_), CellOutput::DataFrame(_)) => {
DivergenceType::OutputMismatch
}
_ => DivergenceType::TypeMismatch,
}
} else if self.config.track_performance {
let threshold = Duration::from_millis(self.config.performance_threshold_ms);
if cand_time > ref_time + threshold && cand_time > ref_time * 2 {
DivergenceType::PerformanceRegression
} else {
DivergenceType::None
}
} else {
DivergenceType::None
}
}
pub fn generate_report(&self, results: &[DifferentialResult]) -> String {
let mut report = String::new();
report.push_str("=== Differential Testing Report ===\n\n");
if results.is_empty() {
report.push_str("No divergences found. Implementations are consistent.\n");
} else {
report.push_str(&format!("Found {} divergences:\n\n", results.len()));
for (i, result) in results.iter().enumerate() {
report.push_str(&format!("{}. Cell '{}'\n", i + 1, result.cell_id));
report.push_str(&format!(" Divergence: {:?}\n", result.divergence));
if self.config.track_performance {
report.push_str(&format!(
" Performance: ref={:?}, candidate={:?}\n",
result.reference_time, result.candidate_time
));
}
if !matches!(result.divergence, DivergenceType::PerformanceRegression) {
report.push_str(&format!(
" Reference: {:?}\n Candidate: {:?}\n",
result.reference_output, result.candidate_output
));
}
report.push('\n');
}
}
report
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::notebook::testing::types::CellMetadata;
#[test]
fn test_differential_config_default() {
let config = DifferentialConfig::default();
assert_eq!(config.performance_threshold_ms, 100);
assert!(config.track_performance);
}
#[test]
fn test_differential_config_custom() {
let config = DifferentialConfig {
performance_threshold_ms: 500,
track_performance: false,
};
assert_eq!(config.performance_threshold_ms, 500);
assert!(!config.track_performance);
}
#[test]
fn test_differential_config_clone() {
let config = DifferentialConfig {
performance_threshold_ms: 250,
track_performance: true,
};
let cloned = config;
assert_eq!(cloned.performance_threshold_ms, 250);
assert!(cloned.track_performance);
}
#[test]
fn test_divergence_type_enum_variants() {
let variants = [
DivergenceType::None,
DivergenceType::OutputMismatch,
DivergenceType::TypeMismatch,
DivergenceType::PerformanceRegression,
DivergenceType::BothFailed,
];
assert_eq!(variants.len(), 5);
}
#[test]
fn test_divergence_type_debug_format() {
assert_eq!(format!("{:?}", DivergenceType::None), "None");
assert_eq!(
format!("{:?}", DivergenceType::OutputMismatch),
"OutputMismatch"
);
assert_eq!(
format!("{:?}", DivergenceType::TypeMismatch),
"TypeMismatch"
);
assert_eq!(
format!("{:?}", DivergenceType::PerformanceRegression),
"PerformanceRegression"
);
assert_eq!(format!("{:?}", DivergenceType::BothFailed), "BothFailed");
}
#[test]
fn test_divergence_type_partial_eq() {
assert_eq!(DivergenceType::None, DivergenceType::None);
assert_eq!(
DivergenceType::OutputMismatch,
DivergenceType::OutputMismatch
);
assert_ne!(DivergenceType::None, DivergenceType::OutputMismatch);
assert_ne!(DivergenceType::TypeMismatch, DivergenceType::BothFailed);
}
#[test]
fn test_differential_result_creation() {
let result = DifferentialResult {
cell_id: "test_cell".to_string(),
reference_output: CellOutput::Value("ref_value".to_string()),
candidate_output: CellOutput::Value("cand_value".to_string()),
divergence: DivergenceType::OutputMismatch,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(75),
};
assert_eq!(result.cell_id, "test_cell");
assert!(matches!(result.reference_output, CellOutput::Value(_)));
assert!(matches!(result.candidate_output, CellOutput::Value(_)));
assert_eq!(result.divergence, DivergenceType::OutputMismatch);
assert_eq!(result.reference_time, Duration::from_millis(50));
assert_eq!(result.candidate_time, Duration::from_millis(75));
}
#[test]
fn test_differential_result_clone() {
let result = DifferentialResult {
cell_id: "clone_test".to_string(),
reference_output: CellOutput::Error("error".to_string()),
candidate_output: CellOutput::None,
divergence: DivergenceType::TypeMismatch,
reference_time: Duration::from_millis(100),
candidate_time: Duration::from_millis(200),
};
let cloned = result;
assert_eq!(cloned.cell_id, "clone_test");
assert_eq!(cloned.divergence, DivergenceType::TypeMismatch);
}
#[test]
fn test_differential_result_partial_eq() {
let result1 = DifferentialResult {
cell_id: "test".to_string(),
reference_output: CellOutput::Value("same".to_string()),
candidate_output: CellOutput::Value("same".to_string()),
divergence: DivergenceType::None,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(50),
};
let result2 = DifferentialResult {
cell_id: "test".to_string(),
reference_output: CellOutput::Value("same".to_string()),
candidate_output: CellOutput::Value("same".to_string()),
divergence: DivergenceType::None,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(50),
};
assert_eq!(result1, result2);
}
#[test]
fn test_differential_tester_new() {
let tester = DifferentialTester::new();
assert_eq!(tester.config.performance_threshold_ms, 100);
assert!(tester.config.track_performance);
}
#[test]
fn test_differential_tester_default() {
let tester = DifferentialTester::default();
assert_eq!(tester.config.performance_threshold_ms, 100);
assert!(tester.config.track_performance);
}
#[test]
fn test_differential_tester_with_config() {
let config = DifferentialConfig {
performance_threshold_ms: 300,
track_performance: false,
};
let tester = DifferentialTester::with_config(config);
assert_eq!(tester.config.performance_threshold_ms, 300);
assert!(!tester.config.track_performance);
}
#[test]
fn test_compare_empty_notebook() {
let mut tester = DifferentialTester::new();
let notebook = Notebook {
cells: vec![],
metadata: None,
};
let results = tester.compare(¬ebook);
assert!(results.is_empty());
}
#[test]
fn test_compare_markdown_cells_skipped() {
let mut tester = DifferentialTester::new();
let markdown_cell = Cell {
id: "md1".to_string(),
source: "# Header".to_string(),
cell_type: CellType::Markdown,
metadata: CellMetadata { test: None },
};
let notebook = Notebook {
cells: vec![markdown_cell],
metadata: None,
};
let results = tester.compare(¬ebook);
assert!(results.is_empty());
}
#[test]
fn test_classify_divergence_identical_outputs() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Value("same".to_string());
let cand_output = CellOutput::Value("same".to_string());
let ref_time = Duration::from_millis(50);
let cand_time = Duration::from_millis(55);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::None);
}
#[test]
fn test_classify_divergence_output_mismatch_values() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Value("reference".to_string());
let cand_output = CellOutput::Value("candidate".to_string());
let ref_time = Duration::from_millis(50);
let cand_time = Duration::from_millis(50);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::OutputMismatch);
}
#[test]
fn test_classify_divergence_type_mismatch() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Value("value".to_string());
let cand_output = CellOutput::Error("error".to_string());
let ref_time = Duration::from_millis(50);
let cand_time = Duration::from_millis(50);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::TypeMismatch);
}
#[test]
fn test_classify_divergence_both_failed() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Error("ref error".to_string());
let cand_output = CellOutput::Error("cand error".to_string());
let ref_time = Duration::from_millis(50);
let cand_time = Duration::from_millis(50);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::BothFailed);
}
#[test]
fn test_classify_divergence_performance_regression() {
let mut config = DifferentialConfig::default();
config.performance_threshold_ms = 50;
let tester = DifferentialTester::with_config(config);
let ref_output = CellOutput::Value("same".to_string());
let cand_output = CellOutput::Value("same".to_string());
let ref_time = Duration::from_millis(100);
let cand_time = Duration::from_millis(300);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::PerformanceRegression);
}
#[test]
fn test_classify_divergence_no_performance_regression_under_threshold() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Value("same".to_string());
let cand_output = CellOutput::Value("same".to_string());
let ref_time = Duration::from_millis(100);
let cand_time = Duration::from_millis(150);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::None);
}
#[test]
fn test_classify_divergence_performance_tracking_disabled() {
let config = DifferentialConfig {
performance_threshold_ms: 50,
track_performance: false,
};
let tester = DifferentialTester::with_config(config);
let ref_output = CellOutput::Value("same".to_string());
let cand_output = CellOutput::Value("same".to_string());
let ref_time = Duration::from_millis(100);
let cand_time = Duration::from_millis(1000);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::None);
}
#[test]
fn test_generate_report_no_divergences() {
let tester = DifferentialTester::new();
let results = vec![];
let report = tester.generate_report(&results);
assert!(report.contains("=== Differential Testing Report ==="));
assert!(report.contains("No divergences found"));
assert!(report.contains("Implementations are consistent"));
}
#[test]
fn test_generate_report_with_divergences() {
let tester = DifferentialTester::new();
let result = DifferentialResult {
cell_id: "cell1".to_string(),
reference_output: CellOutput::Value("ref".to_string()),
candidate_output: CellOutput::Value("cand".to_string()),
divergence: DivergenceType::OutputMismatch,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(75),
};
let results = vec![result];
let report = tester.generate_report(&results);
assert!(report.contains("=== Differential Testing Report ==="));
assert!(report.contains("Found 1 divergences"));
assert!(report.contains("Cell 'cell1'"));
assert!(report.contains("OutputMismatch"));
assert!(report.contains("Performance: ref="));
assert!(report.contains("Reference: Value(\"ref\")"));
assert!(report.contains("Candidate: Value(\"cand\")"));
}
#[test]
fn test_generate_report_performance_regression_only() {
let tester = DifferentialTester::new();
let result = DifferentialResult {
cell_id: "slow_cell".to_string(),
reference_output: CellOutput::Value("same".to_string()),
candidate_output: CellOutput::Value("same".to_string()),
divergence: DivergenceType::PerformanceRegression,
reference_time: Duration::from_millis(100),
candidate_time: Duration::from_millis(500),
};
let results = vec![result];
let report = tester.generate_report(&results);
assert!(report.contains("PerformanceRegression"));
assert!(report.contains("Performance: ref="));
assert!(!report.contains("Reference: Value")); }
#[test]
fn test_generate_report_multiple_divergences() {
let tester = DifferentialTester::new();
let results = vec![
DifferentialResult {
cell_id: "cell1".to_string(),
reference_output: CellOutput::Value("ref1".to_string()),
candidate_output: CellOutput::Value("cand1".to_string()),
divergence: DivergenceType::OutputMismatch,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(60),
},
DifferentialResult {
cell_id: "cell2".to_string(),
reference_output: CellOutput::Value("ok".to_string()),
candidate_output: CellOutput::Error("fail".to_string()),
divergence: DivergenceType::TypeMismatch,
reference_time: Duration::from_millis(30),
candidate_time: Duration::from_millis(40),
},
];
let report = tester.generate_report(&results);
assert!(report.contains("Found 2 divergences"));
assert!(report.contains("1. Cell 'cell1'"));
assert!(report.contains("2. Cell 'cell2'"));
assert!(report.contains("OutputMismatch"));
assert!(report.contains("TypeMismatch"));
}
#[test]
fn test_generate_report_no_performance_tracking() {
let config = DifferentialConfig {
performance_threshold_ms: 100,
track_performance: false,
};
let tester = DifferentialTester::with_config(config);
let result = DifferentialResult {
cell_id: "cell1".to_string(),
reference_output: CellOutput::Value("ref".to_string()),
candidate_output: CellOutput::Value("cand".to_string()),
divergence: DivergenceType::OutputMismatch,
reference_time: Duration::from_millis(50),
candidate_time: Duration::from_millis(75),
};
let results = vec![result];
let report = tester.generate_report(&results);
assert!(report.contains("OutputMismatch"));
assert!(!report.contains("Performance:")); }
#[test]
fn test_large_threshold_no_regression() {
let config = DifferentialConfig {
performance_threshold_ms: 1000, track_performance: true,
};
let tester = DifferentialTester::with_config(config);
let ref_output = CellOutput::Value("same".to_string());
let cand_output = CellOutput::Value("same".to_string());
let ref_time = Duration::from_millis(100);
let cand_time = Duration::from_millis(300);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::None);
}
#[test]
fn test_edge_case_zero_times() {
let tester = DifferentialTester::new();
let ref_output = CellOutput::Value("fast".to_string());
let cand_output = CellOutput::Value("fast".to_string());
let ref_time = Duration::from_millis(0);
let cand_time = Duration::from_millis(0);
let divergence = tester.classify_divergence(&ref_output, &cand_output, ref_time, cand_time);
assert_eq!(divergence, DivergenceType::None);
}
}