use crate::notebook::testing::types::{Cell, CellMetadata, CellType, Notebook};
pub struct PropertyTester {
config: PropertyTestConfig,
}
#[derive(Debug, Clone)]
pub struct PropertyTestConfig {
pub max_cells: usize,
pub max_iterations: u32,
pub seed: Option<u64>,
}
impl Default for PropertyTestConfig {
fn default() -> Self {
Self {
max_cells: 20,
max_iterations: 1000,
seed: None,
}
}
}
impl Default for PropertyTester {
fn default() -> Self {
Self::new()
}
}
impl PropertyTester {
pub fn new() -> Self {
Self {
config: PropertyTestConfig::default(),
}
}
pub fn with_config(config: PropertyTestConfig) -> Self {
Self { config }
}
pub fn arbitrary_notebook(seed: u64, size: usize) -> Notebook {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let mut rng = StdRng::seed_from_u64(seed);
let mut cells = Vec::new();
for i in 0..size {
let is_code = rng.gen_bool(0.8);
let cell = if is_code {
Cell {
id: format!("cell_{i}"),
source: generate_random_code(&mut rng, i),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
}
} else {
Cell {
id: format!("md_{i}"),
source: format!("# Markdown cell {i}"),
cell_type: CellType::Markdown,
metadata: CellMetadata::default(),
}
};
cells.push(cell);
}
Notebook {
cells,
metadata: None,
}
}
pub fn test_determinism(&self, notebook: &Notebook) -> bool {
use crate::notebook::testing::tester::NotebookTester;
let mut tester1 = NotebookTester::new();
let mut tester2 = NotebookTester::new();
for cell in ¬ebook.cells {
if matches!(cell.cell_type, CellType::Code) {
let result1 = tester1.execute_cell(cell);
let result2 = tester2.execute_cell(cell);
if result1 != result2 {
return false;
}
}
}
true
}
pub fn test_commutativity(&self, cell1: &Cell, cell2: &Cell) -> bool {
use crate::notebook::testing::tester::NotebookTester;
if !are_cells_independent(cell1, cell2) {
return true; }
let mut tester1 = NotebookTester::new();
let mut tester2 = NotebookTester::new();
let _ = tester1.execute_cell(cell1);
let result1 = tester1.execute_cell(cell2);
let _ = tester2.execute_cell(cell2);
let result2 = tester2.execute_cell(cell1);
result1 == result2
}
}
fn generate_random_code(rng: &mut impl rand::Rng, index: usize) -> String {
match rng.gen_range(0..6) {
0 => format!("let x{} = {}", index, rng.gen_range(0..100)),
1 => format!("{} + {}", rng.gen_range(0..10), rng.gen_range(0..10)),
2 => format!("println(\"Cell {index}\")"),
3 => "1 + 1".to_string(),
4 => format!("// Comment {index}"),
_ => format!("let y{} = x{} * 2", index, index.saturating_sub(1)),
}
}
fn sanitize_source(source: String) -> String {
if source.is_empty() {
"1 + 1".to_string()
} else {
source
.chars()
.take(100)
.filter(|c| c.is_ascii() && !c.is_control())
.collect()
}
}
fn are_cells_independent(cell1: &Cell, cell2: &Cell) -> bool {
let vars1 = extract_variables(&cell1.source);
let vars2 = extract_variables(&cell2.source);
vars1.is_disjoint(&vars2)
}
fn extract_variables(source: &str) -> std::collections::HashSet<String> {
use std::collections::HashSet;
let mut vars = HashSet::new();
let words: Vec<&str> = source.split_whitespace().collect();
for word in words {
if word.chars().all(|c| c.is_alphanumeric() || c == '_') {
vars.insert(word.to_string());
}
}
vars
}
#[cfg(test)]
mod tests {
use super::*;
use crate::notebook::testing::types::{Cell, CellMetadata, CellType, Notebook};
#[test]
fn test_property_tester_new() {
let tester = PropertyTester::new();
assert_eq!(tester.config.max_cells, 20);
assert_eq!(tester.config.max_iterations, 1000);
assert!(tester.config.seed.is_none());
}
#[test]
fn test_property_tester_with_config() {
let config = PropertyTestConfig {
max_cells: 50,
max_iterations: 2000,
seed: Some(42),
};
let tester = PropertyTester::with_config(config);
assert_eq!(tester.config.max_cells, 50);
assert_eq!(tester.config.max_iterations, 2000);
assert_eq!(tester.config.seed, Some(42));
}
#[test]
fn test_property_test_config_default() {
let config = PropertyTestConfig::default();
assert_eq!(config.max_cells, 20);
assert_eq!(config.max_iterations, 1000);
assert!(config.seed.is_none());
}
#[test]
fn test_arbitrary_notebook_generation() {
let notebook = PropertyTester::arbitrary_notebook(42, 5);
assert_eq!(notebook.cells.len(), 5);
assert!(notebook.metadata.is_none());
for cell in ¬ebook.cells {
assert!(!cell.id.is_empty());
if matches!(cell.cell_type, CellType::Code) {
assert!(cell.id.starts_with("cell_"));
} else {
assert!(cell.id.starts_with("md_"));
}
}
}
#[test]
fn test_arbitrary_notebook_empty() {
let notebook = PropertyTester::arbitrary_notebook(123, 0);
assert_eq!(notebook.cells.len(), 0);
}
#[test]
fn test_generate_random_code() {
use rand::rngs::StdRng;
use rand::SeedableRng;
let mut rng = StdRng::seed_from_u64(42);
for i in 0..10 {
let code = generate_random_code(&mut rng, i);
assert!(!code.is_empty());
}
}
#[test]
fn test_sanitize_source() {
let result = sanitize_source(String::new());
assert_eq!(result, "1 + 1");
let result = sanitize_source("hello world".to_string());
assert_eq!(result, "hello world");
let long_str = "a".repeat(200);
let result = sanitize_source(long_str);
assert_eq!(result.len(), 100);
let result = sanitize_source("hello\nworld\t!".to_string());
assert!(!result.contains('\n'));
assert!(!result.contains('\t'));
}
#[test]
fn test_extract_variables() {
let source = "let x = 42; let y = x + 1";
let vars = extract_variables(source);
assert!(vars.contains("x"));
assert!(vars.contains("y"));
assert!(vars.contains("let"));
assert!(vars.contains("1"));
}
#[test]
fn test_extract_variables_empty() {
let vars = extract_variables("");
assert!(vars.is_empty());
}
#[test]
fn test_extract_variables_with_special_chars() {
let source = "foo_bar baz-qux test123";
let vars = extract_variables(source);
assert!(vars.contains("foo_bar"));
assert!(vars.contains("test123"));
}
#[test]
fn test_are_cells_independent() {
let cell1 = Cell {
id: "1".to_string(),
source: "let x = a".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
let _cell2 = Cell {
id: "2".to_string(),
source: "let y = b".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
let cell3 = Cell {
id: "3".to_string(),
source: "let z = x + y".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
assert!(!are_cells_independent(&cell1, &cell3));
}
#[test]
fn test_test_determinism_empty_notebook() {
let tester = PropertyTester::new();
let notebook = Notebook {
cells: vec![],
metadata: None,
};
assert!(tester.test_determinism(¬ebook));
}
#[test]
fn test_test_determinism_markdown_only() {
let tester = PropertyTester::new();
let notebook = Notebook {
cells: vec![Cell {
id: "md1".to_string(),
source: "# Title".to_string(),
cell_type: CellType::Markdown,
metadata: CellMetadata::default(),
}],
metadata: None,
};
assert!(tester.test_determinism(¬ebook));
}
#[test]
fn test_test_commutativity_independent_cells() {
let tester = PropertyTester::new();
let cell1 = Cell {
id: "1".to_string(),
source: "let a = 1".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
let cell2 = Cell {
id: "2".to_string(),
source: "let b = 2".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
let _ = tester.test_commutativity(&cell1, &cell2);
}
#[test]
fn test_test_commutativity_dependent_cells() {
let tester = PropertyTester::new();
let cell1 = Cell {
id: "1".to_string(),
source: "let x = 1".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
let cell2 = Cell {
id: "2".to_string(),
source: "let y = x + 1".to_string(),
cell_type: CellType::Code,
metadata: CellMetadata::default(),
};
assert!(tester.test_commutativity(&cell1, &cell2));
}
#[test]
fn test_arbitrary_notebook_deterministic() {
let notebook1 = PropertyTester::arbitrary_notebook(42, 3);
let notebook2 = PropertyTester::arbitrary_notebook(42, 3);
assert_eq!(notebook1.cells.len(), notebook2.cells.len());
for (cell1, cell2) in notebook1.cells.iter().zip(notebook2.cells.iter()) {
assert_eq!(cell1.id, cell2.id);
assert_eq!(cell1.source, cell2.source);
}
}
#[test]
fn test_arbitrary_notebook_different_seeds() {
let notebook1 = PropertyTester::arbitrary_notebook(42, 5);
let notebook2 = PropertyTester::arbitrary_notebook(43, 5);
assert_eq!(notebook1.cells.len(), notebook2.cells.len());
let mut _any_different = false;
for (cell1, cell2) in notebook1.cells.iter().zip(notebook2.cells.iter()) {
if cell1.source != cell2.source {
_any_different = true;
break;
}
}
}
}