use std::io::Write;
use tempfile::NamedTempFile;
use tldr_cli::commands::remaining::diff::DiffArgs;
use tldr_cli::commands::remaining::types::{
ASTChange, ChangeType, DiffGranularity, DiffReport, NodeKind,
};
fn write_temp(content: &str, suffix: &str) -> NamedTempFile {
let mut f = NamedTempFile::with_suffix(suffix).unwrap();
write!(f, "{}", content).unwrap();
f.flush().unwrap();
f
}
fn run_l2_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::Expression,
semantic_only: false,
output: None,
};
args.run_to_report().expect("L2 diff should succeed")
}
#[allow(dead_code)]
fn assert_has_change(report: &DiffReport, change_type: ChangeType, context: &str) {
let found = report.changes.iter().any(|c| c.change_type == change_type);
assert!(
found,
"{}: expected at least one {:?} change, but found none. Changes: {:?}",
context,
change_type,
report
.changes
.iter()
.map(|c| format!(
"{:?}:{:?}:{}",
c.change_type,
c.node_kind,
c.name.as_deref().unwrap_or("<none>")
))
.collect::<Vec<_>>()
);
}
#[allow(dead_code)]
fn assert_has_change_with_kind(
report: &DiffReport,
change_type: ChangeType,
node_kind: NodeKind,
context: &str,
) {
let found = report
.changes
.iter()
.any(|c| c.change_type == change_type && c.node_kind == node_kind);
assert!(
found,
"{}: expected {:?} change with {:?} kind, but found none. Changes: {:?}",
context,
change_type,
node_kind,
report
.changes
.iter()
.map(|c| format!(
"{:?}:{:?}:{}",
c.change_type,
c.node_kind,
c.name.as_deref().unwrap_or("<none>")
))
.collect::<Vec<_>>()
);
}
#[allow(dead_code)]
fn count_changes(report: &DiffReport, change_type: ChangeType) -> usize {
report
.changes
.iter()
.filter(|c| c.change_type == change_type)
.count()
}
fn count_top_level_changes(report: &DiffReport) -> usize {
report.changes.len()
}
#[allow(dead_code)]
fn find_change(report: &DiffReport, change_type: ChangeType) -> Option<&ASTChange> {
report.changes.iter().find(|c| c.change_type == change_type)
}
fn flatten_changes(report: &DiffReport) -> Vec<&ASTChange> {
let mut result = Vec::new();
for change in &report.changes {
result.push(change);
if let Some(children) = &change.children {
for child in children {
result.push(child);
}
}
}
result
}
fn format_changes(report: &DiffReport) -> String {
report
.changes
.iter()
.map(|c| {
let children_info = if let Some(children) = &c.children {
format!(
" [children: {}]",
children
.iter()
.map(|ch| format!(
"{:?}:{}",
ch.change_type,
ch.name.as_deref().unwrap_or("<none>")
))
.collect::<Vec<_>>()
.join(", ")
)
} else {
String::new()
};
format!(
"{:?}:{:?}:{}{}",
c.change_type,
c.node_kind,
c.name.as_deref().unwrap_or("<none>"),
children_info
)
})
.collect::<Vec<_>>()
.join("; ")
}
mod core_expression {
use super::*;
#[test]
fn identical_files_expression_level() {
let content = "def foo(x):\n return x + 1\n\ndef bar(y):\n return y * 2\n";
let a = write_temp(content, ".py");
let b = write_temp(content, ".py");
let report = run_l2_diff(&a, &b);
assert!(
report.identical,
"Identical files should produce identical=true. Changes: {}",
format_changes(&report)
);
assert!(
report.changes.is_empty(),
"Identical files should produce no changes. Changes: {}",
format_changes(&report)
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
}
#[test]
fn single_expression_change() {
let a = write_temp("def foo(x):\n return x + 1\n", ".py");
let b = write_temp("def foo(x):\n return x + 2\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect at least one expression-level change"
);
let all_changes = flatten_changes(&report);
let has_update_or_insert_delete = all_changes.iter().any(|c| {
c.change_type == ChangeType::Update
|| c.change_type == ChangeType::Insert
|| c.change_type == ChangeType::Delete
});
assert!(
has_update_or_insert_delete,
"Should have Update, Insert, or Delete changes for the expression. Changes: {}",
format_changes(&report)
);
}
#[test]
fn expression_insert() {
let a = write_temp("def foo(x):\n y = x + 1\n return y\n", ".py");
let b = write_temp(
"def foo(x):\n y = x + 1\n z = y * 2\n return z\n",
".py",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with added expression should not be identical"
);
let all_changes = flatten_changes(&report);
let has_insert = all_changes
.iter()
.any(|c| c.change_type == ChangeType::Insert);
assert!(
has_insert,
"Should detect Insert for added expression. Changes: {}",
format_changes(&report)
);
}
#[test]
fn expression_delete() {
let a = write_temp(
"def foo(x):\n y = x + 1\n z = y * 2\n return z\n",
".py",
);
let b = write_temp("def foo(x):\n y = x + 1\n return y\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with removed expression should not be identical"
);
let all_changes = flatten_changes(&report);
let has_delete = all_changes
.iter()
.any(|c| c.change_type == ChangeType::Delete);
assert!(
has_delete,
"Should detect Delete for removed expression. Changes: {}",
format_changes(&report)
);
}
#[test]
fn nested_expression_change() {
let a = write_temp("def foo(x):\n return (x + 1) * (x - 1)\n", ".py");
let b = write_temp("def foo(x):\n return (x + 2) * (x - 1)\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with nested expression change should not be identical"
);
assert!(
!report.changes.is_empty(),
"Should detect expression-level change for nested expression. Changes: {}",
format_changes(&report)
);
let total = count_top_level_changes(&report);
assert!(
total <= 3,
"Nested expression change should produce at most 3 top-level changes (got {}). \
L2 groups tokens into expressions. Changes: {}",
total,
format_changes(&report)
);
}
#[test]
fn multiple_expression_changes() {
let a = write_temp(
"def foo(x):\n a = x + 1\n b = x * 2\n c = x - 3\n return a + b + c\n",
".py",
);
let b = write_temp(
"def foo(x):\n a = x + 10\n b = x * 20\n c = x - 3\n return a + b + c\n",
".py",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with multiple expression changes should not be identical"
);
let all_changes = flatten_changes(&report);
let change_count = all_changes
.iter()
.filter(|c| c.change_type != ChangeType::Format)
.count();
assert!(
change_count >= 2,
"Should detect at least 2 expression-level changes (got {}). Changes: {}",
change_count,
format_changes(&report)
);
}
}
mod expression_vs_token {
use super::*;
#[test]
fn groups_tokens_into_expressions() {
let a = write_temp("def foo():\n return x + 1\n", ".py");
let b = write_temp("def foo():\n return y + 2\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with expression change should not be identical"
);
let top_level_semantic = report
.changes
.iter()
.filter(|c| c.change_type != ChangeType::Format)
.count();
assert!(
top_level_semantic <= 2,
"L2 should group token changes within an expression. Got {} top-level changes \
(expected <= 2). If this were L1, each token change would be separate. Changes: {}",
top_level_semantic,
format_changes(&report)
);
}
#[test]
fn separates_different_expressions() {
let a = write_temp("x = 1\ny = 2\n", ".py");
let b = write_temp("x = 10\ny = 20\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with two expression changes should not be identical"
);
let all_changes = flatten_changes(&report);
let semantic_changes = all_changes
.iter()
.filter(|c| c.change_type != ChangeType::Format)
.count();
assert!(
semantic_changes >= 2,
"Two different expressions changed should produce at least 2 changes (got {}). \
Changes: {}",
semantic_changes,
format_changes(&report)
);
}
#[test]
fn function_call_arg_change() {
let a = write_temp("def main():\n result = foo(x, y)\n", ".py");
let b = write_temp("def main():\n result = foo(x, z)\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Files with call arg change should not be identical"
);
assert!(
!report.changes.is_empty(),
"Should detect expression-level change for call arg. Changes: {}",
format_changes(&report)
);
let top_level = count_top_level_changes(&report);
assert!(
top_level <= 3,
"Call arg change should be grouped in expression (got {} top-level, expected <= 3). \
Changes: {}",
top_level,
format_changes(&report)
);
}
}
mod language_specific {
use super::*;
#[test]
fn python_expression_diff() {
let a = write_temp(
"def check(x):\n result = x if x > 0 else -x\n return result\n",
".py",
);
let b = write_temp(
"def check(x):\n result = x if x > 10 else -x\n return result\n",
".py",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Python conditional expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Python conditional. Changes: {}",
format_changes(&report)
);
}
#[test]
fn rust_expression_diff() {
let a = write_temp(
r#"fn classify(x: i32) -> &'static str {
match x {
0 => "zero",
1 => "one",
_ => "other",
}
}
"#,
".rs",
);
let b = write_temp(
r#"fn classify(x: i32) -> &'static str {
match x {
0 => "zero",
1 => "one",
2 => "two",
_ => "other",
}
}
"#,
".rs",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Rust match arm change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
let all_changes = flatten_changes(&report);
let has_change = all_changes
.iter()
.any(|c| c.change_type == ChangeType::Insert || c.change_type == ChangeType::Update);
assert!(
has_change,
"Should detect Insert or Update for added Rust match arm. Changes: {}",
format_changes(&report)
);
}
#[test]
fn typescript_expression_diff() {
let a = write_temp(
"function check(x: number): number {\n return x > 0 ? x : -x;\n}\n",
".ts",
);
let b = write_temp(
"function check(x: number): number {\n return x > 10 ? x : -x;\n}\n",
".ts",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"TypeScript ternary change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in TypeScript ternary. Changes: {}",
format_changes(&report)
);
}
}
mod edge_cases {
use super::*;
#[test]
fn empty_expression() {
let a = write_temp("def foo():\n pass\n", ".py");
let b = write_temp("def foo():\n return 1\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Empty body to non-empty body should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect changes from pass to return. Changes: {}",
format_changes(&report)
);
}
#[test]
fn deeply_nested_expressions() {
let a = write_temp("def foo(x):\n return ((((x + 1))))\n", ".py");
let b = write_temp("def foo(x):\n return ((((x + 2))))\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Deeply nested expression change should not be identical"
);
assert!(
!report.changes.is_empty(),
"Should detect deeply nested expression change. Changes: {}",
format_changes(&report)
);
let total = count_top_level_changes(&report);
assert!(
total <= 3,
"Deeply nested change should produce at most 3 top-level changes (got {}). \
L2 should not report each nesting level separately. Changes: {}",
total,
format_changes(&report)
);
}
#[test]
fn granularity_is_expression() {
let a = write_temp("x = 1\n", ".py");
let b = write_temp("x = 2\n", ".py");
let report = run_l2_diff(&a, &b);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"DiffReport granularity must be Expression for L2 diff. Got: {:?}",
report.granularity
);
}
}
mod children {
use super::*;
#[test]
fn update_has_children() {
let a = write_temp("def foo(x):\n return x + 1\n", ".py");
let b = write_temp("def foo(x):\n return x + 2\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(!report.identical, "Files should not be identical");
let updates: Vec<&ASTChange> = report
.changes
.iter()
.filter(|c| c.change_type == ChangeType::Update)
.collect();
if !updates.is_empty() {
let has_children = updates.iter().any(|u| {
u.children
.as_ref()
.map(|ch| !ch.is_empty())
.unwrap_or(false)
});
assert!(
has_children,
"Expression-level Update changes should have children containing \
token-level changes. Updates: {:?}",
updates
.iter()
.map(|u| format!(
"name={}, children={:?}",
u.name.as_deref().unwrap_or("<none>"),
u.children.as_ref().map(|c| c.len())
))
.collect::<Vec<_>>()
);
}
assert!(!report.changes.is_empty(), "Should have changes detected");
}
#[test]
fn insert_no_children() {
let a = write_temp("def foo(x):\n return x\n", ".py");
let b = write_temp("def foo(x):\n y = x * 2\n return y\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(!report.identical, "Files should not be identical");
let inserts: Vec<&ASTChange> = report
.changes
.iter()
.filter(|c| c.change_type == ChangeType::Insert)
.collect();
for ins in &inserts {
let child_count = ins.children.as_ref().map(|c| c.len()).unwrap_or(0);
assert_eq!(
child_count,
0,
"Expression-level Insert should not have children (got {}). \
Name: {}",
child_count,
ins.name.as_deref().unwrap_or("<none>")
);
}
}
}
mod summary {
use super::*;
#[test]
fn summary_counts_accurate() {
let a = write_temp("x = 1\ny = 2\n", ".py");
let b = write_temp("x = 10\ny = 2\nz = 3\n", ".py");
let report = run_l2_diff(&a, &b);
assert!(!report.identical, "Files should not be identical");
let summary = report
.summary
.as_ref()
.expect("L2 report should have a summary");
assert!(
summary.total_changes > 0,
"Summary should have non-zero total_changes. Summary: {:?}",
summary
);
let actual_count = report.changes.len() as u32;
assert_eq!(
summary.total_changes, actual_count,
"Summary total_changes ({}) should match actual changes count ({}). Summary: {:?}",
summary.total_changes, actual_count, summary
);
}
}
mod multilang_expression {
use super::*;
#[test]
fn javascript_arrow_expression() {
let a = write_temp(
"const add = (x, y) => x + y;\nconst sub = (x, y) => x - y;\n",
".js",
);
let b = write_temp(
"const add = (x, y) => x + y + 1;\nconst sub = (x, y) => x - y;\n",
".js",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"JavaScript arrow expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in JS arrow function. Changes: {}",
format_changes(&report)
);
}
#[test]
fn go_expression_diff() {
let a = write_temp(
"package main\n\nfunc foo(x int) int {\n\treturn x + 1\n}\n",
".go",
);
let b = write_temp(
"package main\n\nfunc foo(x int) int {\n\treturn x + 2\n}\n",
".go",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Go expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Go. Changes: {}",
format_changes(&report)
);
}
#[test]
fn java_expression_diff() {
let a = write_temp(
"public class Main {\n public static int foo(int x) {\n return Math.max(x, 0);\n }\n}\n",
".java",
);
let b = write_temp(
"public class Main {\n public static int foo(int x) {\n return Math.max(x, 10);\n }\n}\n",
".java",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Java expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Java method call. Changes: {}",
format_changes(&report)
);
}
#[test]
fn ruby_expression_diff() {
let a = write_temp("def foo(x)\n x + 1\nend\n", ".rb");
let b = write_temp("def foo(x)\n x + 2\nend\n", ".rb");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Ruby expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Ruby. Changes: {}",
format_changes(&report)
);
}
#[test]
fn c_expression_diff() {
let a = write_temp("int foo(int x) {\n return x + 1;\n}\n", ".c");
let b = write_temp("int foo(int x) {\n return x + 2;\n}\n", ".c");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"C expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in C. Changes: {}",
format_changes(&report)
);
}
#[test]
fn cpp_expression_diff() {
let a = write_temp(
"#include <iostream>\nint main() {\n std::cout << \"hello\";\n return 0;\n}\n",
".cpp",
);
let b = write_temp(
"#include <iostream>\nint main() {\n std::cout << \"hello\";\n return 1;\n}\n",
".cpp",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"C++ expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in C++. Changes: {}",
format_changes(&report)
);
}
#[test]
fn kotlin_expression_diff() {
let a = write_temp("fun main() {\n val x = 1\n println(x)\n}\n", ".kt");
let b = write_temp("fun main() {\n val x = 2\n println(x)\n}\n", ".kt");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Kotlin expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Kotlin. Changes: {}",
format_changes(&report)
);
}
#[test]
fn swift_expression_diff() {
let a = write_temp(
"func compute() -> Int {\n let x = 1\n return x + 1\n}\n",
".swift",
);
let b = write_temp(
"func compute() -> Int {\n let x = 1\n return x + 2\n}\n",
".swift",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Swift expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Swift. Changes: {}",
format_changes(&report)
);
}
#[test]
fn csharp_expression_diff() {
let a = write_temp(
"using System;\nclass Program {\n static void Main() {\n Console.WriteLine(42);\n }\n}\n",
".cs",
);
let b = write_temp(
"using System;\nclass Program {\n static void Main() {\n Console.WriteLine(99);\n }\n}\n",
".cs",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"C# expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in C#. Changes: {}",
format_changes(&report)
);
}
#[test]
fn scala_expression_diff() {
let a = write_temp(
"object Main {\n def compute(x: Int): Int = {\n x + 1\n }\n}\n",
".scala",
);
let b = write_temp(
"object Main {\n def compute(x: Int): Int = {\n x + 2\n }\n}\n",
".scala",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Scala expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Scala. Changes: {}",
format_changes(&report)
);
}
#[test]
fn php_expression_diff() {
let a = write_temp(
"<?php\nfunction compute($x) {\n return $x + 1;\n}\n",
".php",
);
let b = write_temp(
"<?php\nfunction compute($x) {\n return $x + 2;\n}\n",
".php",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"PHP expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in PHP. Changes: {}",
format_changes(&report)
);
}
#[test]
fn lua_expression_diff() {
let a = write_temp("function compute(x)\n return x + 1\nend\n", ".lua");
let b = write_temp("function compute(x)\n return x + 2\nend\n", ".lua");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Lua expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Lua. Changes: {}",
format_changes(&report)
);
}
#[test]
fn luau_expression_diff() {
let a = write_temp(
"local function compute(x: number): number\n return x + 1\nend\n",
".luau",
);
let b = write_temp(
"local function compute(x: number): number\n return x + 2\nend\n",
".luau",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Luau expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Luau. Changes: {}",
format_changes(&report)
);
}
#[test]
fn elixir_expression_diff() {
let a = write_temp(
"defmodule Math do\n def add(x) do\n x + 1\n end\nend\n",
".ex",
);
let b = write_temp(
"defmodule Math do\n def add(x) do\n x + 2\n end\nend\n",
".ex",
);
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"Elixir expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in Elixir. Changes: {}",
format_changes(&report)
);
}
#[test]
fn ocaml_expression_diff() {
let a = write_temp("let compute x =\n x + 1\n", ".ml");
let b = write_temp("let compute x =\n x + 2\n", ".ml");
let report = run_l2_diff(&a, &b);
assert!(
!report.identical,
"OCaml expression change should not be identical"
);
assert_eq!(
report.granularity,
DiffGranularity::Expression,
"Granularity should be Expression"
);
assert!(
!report.changes.is_empty(),
"Should detect expression change in OCaml. Changes: {}",
format_changes(&report)
);
}
}