use std::io::Write;
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tldr_cli::commands::remaining::types::{
ASTChange, ChangeType, DiffGranularity, DiffReport, DiffSummary, NodeKind,
};
use tldr_cli::commands::remaining::diff::DiffArgs;
fn write_temp_py(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".py").unwrap();
write!(f, "{}", content).unwrap();
f.flush().unwrap();
f
}
fn write_temp_ts(content: &str) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(".ts").unwrap();
write!(f, "{}", content).unwrap();
f.flush().unwrap();
f
}
fn run_l3_diff(file_a: &NamedTempFile, file_b: &NamedTempFile) -> DiffReport {
let args = DiffArgs {
file_a: file_a.path().to_path_buf(),
file_b: file_b.path().to_path_buf(),
granularity: DiffGranularity::Statement,
semantic_only: false,
output: None,
};
args.run_to_report().expect("L3 statement-level diff should succeed")
}
fn find_change<'a>(
changes: &'a [ASTChange],
name: &str,
change_type: ChangeType,
) -> Option<&'a ASTChange> {
changes
.iter()
.find(|c| c.change_type == change_type && c.name.as_deref() == Some(name))
}
fn find_child<'a>(
parent: &'a ASTChange,
change_type: ChangeType,
node_kind: NodeKind,
) -> Option<&'a ASTChange> {
parent.children.as_ref().and_then(|children| {
children
.iter()
.find(|c| c.change_type == change_type && c.node_kind == node_kind)
})
}
fn count_children(parent: &ASTChange, change_type: ChangeType) -> usize {
parent
.children
.as_ref()
.map(|ch| ch.iter().filter(|c| c.change_type == change_type).count())
.unwrap_or(0)
}
fn count_all_children(parent: &ASTChange) -> usize {
parent.children.as_ref().map(|ch| ch.len()).unwrap_or(0)
}
#[test]
fn test_statement_identical() {
let source = r#"
def compute(x, y):
result = x + y
if result > 10:
result = 10
return result
def helper():
return 42
"#;
let file_a = write_temp_py(source);
let file_b = write_temp_py(source);
let report = run_l3_diff(&file_a, &file_b);
assert!(report.identical, "Identical files should produce identical=true");
assert!(
report.changes.is_empty(),
"Identical files should have zero changes, got {}",
report.changes.len()
);
assert_eq!(
report.granularity,
DiffGranularity::Statement,
"Report granularity should be Statement"
);
}
#[test]
fn test_statement_added() {
let source_a = r#"
def compute(x, y):
result = x + y
return result
"#;
let source_b = r#"
def compute(x, y):
result = x + y
if result < 0:
result = 0
return result
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let compute_change = find_change(&report.changes, "compute", ChangeType::Update)
.expect("Should have Update change for 'compute'");
assert!(
compute_change.children.is_some(),
"Function update should have statement-level children"
);
let children = compute_change.children.as_ref().unwrap();
assert!(
!children.is_empty(),
"Should have at least one statement-level change"
);
let has_insert = children
.iter()
.any(|c| c.change_type == ChangeType::Insert && c.node_kind == NodeKind::Statement);
assert!(
has_insert,
"Should have a statement Insert for the new if block"
);
}
#[test]
fn test_statement_removed() {
let source_a = r#"
def compute(x, y):
result = x + y
if result < 0:
result = 0
return result
"#;
let source_b = r#"
def compute(x, y):
result = x + y
return result
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let compute_change = find_change(&report.changes, "compute", ChangeType::Update)
.expect("Should have Update change for 'compute'");
assert!(
compute_change.children.is_some(),
"Function update should have statement-level children"
);
let has_delete = compute_change
.children
.as_ref()
.unwrap()
.iter()
.any(|c| c.change_type == ChangeType::Delete && c.node_kind == NodeKind::Statement);
assert!(
has_delete,
"Should have a statement Delete for the removed if block"
);
}
#[test]
fn test_statement_modified() {
let source_a = r#"
def compute(x):
result = x * 2
return result
"#;
let source_b = r#"
def compute(x):
result = x * 3
return result
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let compute_change = find_change(&report.changes, "compute", ChangeType::Update)
.expect("Should have Update change for 'compute'");
assert!(
compute_change.children.is_some(),
"Function update should have statement-level children"
);
let has_update = compute_change
.children
.as_ref()
.unwrap()
.iter()
.any(|c| c.change_type == ChangeType::Update && c.node_kind == NodeKind::Statement);
assert!(
has_update,
"Should have a statement Update for the modified assignment"
);
}
#[test]
fn test_statement_reordered() {
let source_a = r#"
def process(items):
items.sort()
items.reverse()
return items
"#;
let source_b = r#"
def process(items):
items.reverse()
items.sort()
return items
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let process_change = find_change(&report.changes, "process", ChangeType::Update)
.expect("Should have Update change for 'process'");
assert!(
process_change.children.is_some(),
"Function update should have statement-level children"
);
let children = process_change.children.as_ref().unwrap();
assert!(
!children.is_empty(),
"Should detect statement reordering as changes"
);
}
#[test]
fn test_nested_if_changed() {
let source_a = r#"
def check(x):
if x > 0:
print("positive")
return True
return False
"#;
let source_b = r#"
def check(x):
if x > 0:
print("non-negative")
return True
return False
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let check_change = find_change(&report.changes, "check", ChangeType::Update)
.expect("Should have Update change for 'check'");
assert!(
check_change.children.is_some(),
"Function update should have statement-level children"
);
let children = check_change.children.as_ref().unwrap();
assert!(
!children.is_empty(),
"Should detect changes within nested if block"
);
}
#[test]
fn test_unmatched_function() {
let source_a = r#"
def existing():
return 42
"#;
let source_b = r#"
def existing():
return 42
def brand_new(x):
if x > 0:
return x
return 0
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let existing_change = find_change(&report.changes, "existing", ChangeType::Update);
assert!(
existing_change.is_none(),
"Identical function 'existing' should have no Update change"
);
let new_fn = find_change(&report.changes, "brand_new", ChangeType::Insert)
.expect("Should have Insert for new function 'brand_new'");
assert_eq!(
new_fn.node_kind,
NodeKind::Function,
"New function should be reported as NodeKind::Function, not Statement"
);
assert!(
new_fn.children.is_none() || new_fn.children.as_ref().unwrap().is_empty(),
"Function-level Insert should not have statement children"
);
}
#[test]
fn test_multiple_functions() {
let source_a = r#"
def unchanged():
return 42
def modified(x):
result = x + 1
return result
"#;
let source_b = r#"
def unchanged():
return 42
def modified(x):
result = x + 1
if result > 100:
result = 100
return result
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let unchanged = find_change(&report.changes, "unchanged", ChangeType::Update);
assert!(
unchanged.is_none(),
"'unchanged' function should not have any changes"
);
let modified_change = find_change(&report.changes, "modified", ChangeType::Update)
.expect("Should have Update change for 'modified'");
assert!(
modified_change.children.is_some(),
"Modified function update should have statement-level children"
);
let children = modified_change.children.as_ref().unwrap();
assert!(
!children.is_empty(),
"Modified function should have statement-level changes"
);
}
#[test]
fn test_large_function_fallback() {
let mut lines_a = vec!["def big_func():".to_string()];
let mut lines_b = vec!["def big_func():".to_string()];
for i in 0..210 {
lines_a.push(format!(" x_{} = {}", i, i));
if i >= 205 {
lines_b.push(format!(" x_{} = {}", i, i + 1000));
} else {
lines_b.push(format!(" x_{} = {}", i, i));
}
}
lines_a.push(" return x_0".to_string());
lines_b.push(" return x_0".to_string());
let source_a = lines_a.join("\n");
let source_b = lines_b.join("\n");
let file_a = write_temp_py(&source_a);
let file_b = write_temp_py(&source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
let big_fn = find_change(&report.changes, "big_func", ChangeType::Update)
.expect("Should have Update for 'big_func'");
assert_eq!(
big_fn.node_kind,
NodeKind::Function,
"big_func should be reported as a function-level update"
);
}
#[test]
fn test_statement_typescript() {
let source_a = r#"
function calculate(x: number): number {
const result = x * 2;
return result;
}
"#;
let source_b = r#"
function calculate(x: number): number {
const result = x * 2;
if (result > 100) {
return 100;
}
return result;
}
"#;
let file_a = write_temp_ts(source_a);
let file_b = write_temp_ts(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical, "Files should not be identical");
assert_eq!(report.granularity, DiffGranularity::Statement);
let calc_change = find_change(&report.changes, "calculate", ChangeType::Update)
.expect("Should have Update change for 'calculate'");
assert!(
calc_change.children.is_some() && !calc_change.children.as_ref().unwrap().is_empty(),
"TypeScript function update should have statement-level children"
);
}
#[test]
fn test_function_deleted() {
let source_a = r#"
def keep_me():
return 1
def remove_me():
x = 42
return x
"#;
let source_b = r#"
def keep_me():
return 1
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
assert!(!report.identical);
let del = find_change(&report.changes, "remove_me", ChangeType::Delete)
.expect("Should have Delete for removed function");
assert_eq!(
del.node_kind,
NodeKind::Function,
"Removed function should be NodeKind::Function"
);
assert!(
del.children.is_none() || del.children.as_ref().unwrap().is_empty(),
"Function-level Delete should not have statement children"
);
}
#[test]
fn test_statement_summary() {
let source_a = r#"
def func_a():
return 1
def func_b(x):
result = x + 1
return result
"#;
let source_b = r#"
def func_a():
return 1
def func_b(x):
result = x + 1
if result > 10:
result = 10
return result
"#;
let file_a = write_temp_py(source_a);
let file_b = write_temp_py(source_b);
let report = run_l3_diff(&file_a, &file_b);
let summary = report.summary.expect("Report should have summary");
assert!(
summary.total_changes >= 1,
"Should have at least 1 function-level change"
);
assert!(
summary.updates >= 1,
"Should have at least 1 update (for func_b)"
);
}