use std::fs;
use std::path::Path;
use serde_json::Value;
use tempfile::TempDir;
use tldr_cli::commands::remaining::types::{
ArchChangeType, ArchDiffSummary, ArchLevelChange, ChangeType, DiffGranularity, DiffReport,
FileLevelChange, ImportEdge, ImportGraphSummary, ModuleLevelChange,
};
use tldr_cli::commands::remaining::diff::DiffArgs;
fn create_test_dir(files: &[(&str, &str)]) -> TempDir {
let dir = TempDir::new().expect("failed to create temp dir");
for (path, content) in files {
let full_path = dir.path().join(path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).expect("failed to create parent dirs");
}
fs::write(&full_path, content).expect("failed to write test file");
}
dir
}
fn run_diff(dir_a: &Path, dir_b: &Path, granularity: DiffGranularity) -> DiffReport {
let args = DiffArgs {
file_a: dir_a.to_path_buf(),
file_b: dir_b.to_path_buf(),
granularity,
semantic_only: false,
output: None,
};
args.run_to_report().expect("diff should succeed")
}
const PYTHON_UTILS: &str = r#"
def helper_one(x):
return x + 1
def helper_two(x, y):
return x * y
"#;
const PYTHON_UTILS_MODIFIED: &str = r#"
def helper_one(x, offset=0):
return x + 1 + offset
def helper_two(x, y):
return x * y
"#;
const PYTHON_MODELS: &str = r#"
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def greet(self):
return f"Hello, {self.name}"
"#;
const PYTHON_MAIN_IMPORTS_UTILS: &str = r#"
from utils import helper_one, helper_two
def main():
result = helper_one(10)
product = helper_two(3, 4)
return result + product
"#;
const PYTHON_MAIN_IMPORTS_BOTH: &str = r#"
from utils import helper_one, helper_two
from models import User
def main():
result = helper_one(10)
product = helper_two(3, 4)
user = User("Alice", "alice@example.com")
return result + product
"#;
const PYTHON_API_HANDLER: &str = r#"
from core.engine import process
from utils.helpers import validate
def handle_request(request):
data = validate(request)
return process(data)
def handle_health():
return {"status": "ok"}
"#;
const PYTHON_CORE_ENGINE: &str = r#"
from utils.helpers import transform
def process(data):
transformed = transform(data)
return {"result": transformed}
def analyze(data):
return {"analysis": len(data)}
"#;
const PYTHON_UTILS_HELPERS: &str = r#"
def validate(data):
if not data:
raise ValueError("empty data")
return data
def transform(data):
return [x * 2 for x in data]
def format_output(data):
return str(data)
"#;
#[test]
fn test_file_level_identical_dirs() {
let files = &[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
];
let dir_a = create_test_dir(files);
let dir_b = create_test_dir(files);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert!(report.identical, "Identical directories should produce identical=true");
assert_eq!(report.granularity, DiffGranularity::File);
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let non_identical: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type != ChangeType::Insert
&& fc.change_type != ChangeType::Delete
&& fc.change_type != ChangeType::Update)
.collect();
let modifications: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Update
|| fc.change_type == ChangeType::Insert
|| fc.change_type == ChangeType::Delete)
.collect();
assert!(
modifications.is_empty(),
"Identical dirs should have no modifications, got {} changes",
modifications.len()
);
}
#[test]
fn test_file_level_added_file() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert!(!report.identical, "Directories with an added file should not be identical");
assert_eq!(report.granularity, DiffGranularity::File);
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let added: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Insert)
.collect();
assert_eq!(added.len(), 1, "Should detect exactly one added file");
assert_eq!(added[0].relative_path, "models.py");
assert!(added[0].new_fingerprint.is_some(), "Added file should have new_fingerprint");
assert!(added[0].old_fingerprint.is_none(), "Added file should not have old_fingerprint");
}
#[test]
fn test_file_level_removed_file() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert!(!report.identical, "Directories with a removed file should not be identical");
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let removed: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Delete)
.collect();
assert_eq!(removed.len(), 1, "Should detect exactly one removed file");
assert_eq!(removed[0].relative_path, "models.py");
assert!(removed[0].old_fingerprint.is_some(), "Removed file should have old_fingerprint");
assert!(removed[0].new_fingerprint.is_none(), "Removed file should not have new_fingerprint");
}
#[test]
fn test_file_level_modified_file() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS_MODIFIED),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert!(!report.identical, "Modified file should make dirs non-identical");
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let modified: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Update)
.collect();
assert_eq!(modified.len(), 1, "Should detect exactly one modified file");
assert_eq!(modified[0].relative_path, "utils.py");
let old_fp = modified[0].old_fingerprint.expect("Modified file should have old_fingerprint");
let new_fp = modified[0].new_fingerprint.expect("Modified file should have new_fingerprint");
assert_ne!(old_fp, new_fp, "Fingerprints should differ for modified files");
let sig_changes = modified[0]
.signature_changes
.as_ref()
.expect("Modified file should have signature_changes");
assert!(
!sig_changes.is_empty(),
"Should report which signatures changed"
);
assert!(
sig_changes.iter().any(|s| s.contains("helper_one")),
"Signature changes should mention helper_one, got: {:?}",
sig_changes
);
}
#[test]
fn test_file_level_multiple_changes() {
let config_py = "CONFIG_KEY = 'value'\n";
let routes_py = r#"
def get_routes():
return ["/home", "/about"]
"#;
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("config.py", config_py),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS_MODIFIED),
("models.py", PYTHON_MODELS),
("routes.py", routes_py),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert!(!report.identical);
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let added_count = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Insert)
.count();
let removed_count = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Delete)
.count();
let modified_count = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Update)
.count();
assert_eq!(added_count, 1, "Should have 1 added file (routes.py)");
assert_eq!(removed_count, 1, "Should have 1 removed file (config.py)");
assert_eq!(modified_count, 1, "Should have 1 modified file (utils.py)");
let added_paths: Vec<&str> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Insert)
.map(|fc| fc.relative_path.as_str())
.collect();
assert!(added_paths.contains(&"routes.py"));
let removed_paths: Vec<&str> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Delete)
.map(|fc| fc.relative_path.as_str())
.collect();
assert!(removed_paths.contains(&"config.py"));
let modified_paths: Vec<&str> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Update)
.map(|fc| fc.relative_path.as_str())
.collect();
assert!(modified_paths.contains(&"utils.py"));
}
#[test]
fn test_file_level_nested_dirs() {
let dir_a = create_test_dir(&[
("src/utils.py", PYTHON_UTILS),
("src/models.py", PYTHON_MODELS),
]);
let dir_b = create_test_dir(&[
("src/utils.py", PYTHON_UTILS_MODIFIED),
("src/models.py", PYTHON_MODELS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
let file_changes = report.file_changes.expect("L6 report should have file_changes");
let modified: Vec<&FileLevelChange> = file_changes
.iter()
.filter(|fc| fc.change_type == ChangeType::Update)
.collect();
assert_eq!(modified.len(), 1);
assert!(
modified[0].relative_path.contains("src/utils.py")
|| modified[0].relative_path.contains("src\\utils.py"),
"Relative path should include subdirectory: got {}",
modified[0].relative_path
);
}
#[test]
fn test_module_level_import_added() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical);
assert_eq!(report.granularity, DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.py"))
.expect("main.py should appear in module_changes");
assert!(
!main_change.imports_added.is_empty(),
"main.py should have imports_added"
);
let has_models_import = main_change.imports_added.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_import,
"imports_added should include an edge to 'models', got: {:?}",
main_change.imports_added
);
assert!(
main_change.imports_removed.is_empty(),
"main.py should have no imports_removed"
);
}
#[test]
fn test_module_level_import_removed() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.py"))
.expect("main.py should appear in module_changes");
assert!(
!main_change.imports_removed.is_empty(),
"main.py should have imports_removed"
);
let has_models_removal = main_change.imports_removed.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_removal,
"imports_removed should include an edge to 'models', got: {:?}",
main_change.imports_removed
);
assert!(
main_change.imports_added.is_empty(),
"main.py should have no imports_added"
);
}
#[test]
fn test_module_level_new_module() {
let routes_with_import = r#"
from utils import helper_one
def get_routes():
val = helper_one(42)
return ["/home", "/about", val]
"#;
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
("routes.py", routes_with_import),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes");
let routes_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("routes.py"))
.expect("routes.py should appear in module_changes");
assert_eq!(
routes_change.change_type,
ChangeType::Insert,
"New module should be classified as Insert"
);
assert!(
!routes_change.imports_added.is_empty(),
"New module should have its imports listed in imports_added"
);
let imports_utils = routes_change.imports_added.iter().any(|edge| {
edge.target_module.contains("utils")
});
assert!(
imports_utils,
"routes.py imports from utils, should show in imports_added"
);
}
#[test]
fn test_module_level_summary() {
let routes_with_import = r#"
from utils import helper_one
def get_routes():
return [helper_one(1)]
"#;
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
("routes.py", routes_with_import),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let summary = report
.import_graph_summary
.expect("L7 report should have import_graph_summary");
assert!(
summary.total_edges_b > summary.total_edges_a,
"dir_b should have more import edges than dir_a: {} vs {}",
summary.total_edges_b,
summary.total_edges_a
);
assert!(
summary.edges_added >= 2,
"At least 2 edges should be added (models import + routes import), got {}",
summary.edges_added
);
assert_eq!(
summary.edges_removed, 0,
"No edges should be removed"
);
assert!(
summary.modules_with_import_changes >= 1,
"At least 1 module (main.py) should have import changes, got {}",
summary.modules_with_import_changes
);
}
#[test]
fn test_module_level_with_file_change() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS_MODIFIED), ("main.py", PYTHON_MAIN_IMPORTS_BOTH), ]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.py"))
.expect("main.py should appear in module_changes");
let file_change = main_change
.file_change
.as_ref()
.expect("Module with structural changes should have file_change");
assert_eq!(file_change.change_type, ChangeType::Update);
}
#[test]
fn test_arch_level_stable() {
let files = &[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
];
let dir_a = create_test_dir(files);
let dir_b = create_test_dir(files);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(report.identical, "Identical projects should be architecturally identical");
assert_eq!(report.granularity, DiffGranularity::Architecture);
let summary = report
.arch_summary
.expect("L8 report should have arch_summary");
assert!(
(summary.stability_score - 1.0).abs() < f64::EPSILON,
"Identical projects should have stability_score = 1.0, got {}",
summary.stability_score
);
assert_eq!(summary.layer_migrations, 0);
assert_eq!(summary.directories_added, 0);
assert_eq!(summary.directories_removed, 0);
assert_eq!(summary.cycles_introduced, 0);
assert_eq!(summary.cycles_resolved, 0);
let arch_changes = report.arch_changes.unwrap_or_default();
assert!(
arch_changes.is_empty(),
"Identical projects should have no arch changes"
);
}
#[test]
fn test_arch_level_new_directory() {
let middleware_py = r#"
from api.handler import handle_request
from utils.helpers import validate
def auth_middleware(request):
validate(request)
return handle_request(request)
def logging_middleware(request):
print(f"Request: {request}")
return handle_request(request)
"#;
let dir_a = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let dir_b = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
("middleware/auth.py", middleware_py),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(!report.identical);
let arch_changes = report
.arch_changes
.expect("L8 report should have arch_changes");
let added_dirs: Vec<&ArchLevelChange> = arch_changes
.iter()
.filter(|ac| matches!(ac.change_type, ArchChangeType::Added))
.collect();
assert!(
!added_dirs.is_empty(),
"Should detect at least one added directory"
);
let has_middleware = added_dirs
.iter()
.any(|ac| ac.directory.contains("middleware"));
assert!(
has_middleware,
"Added directories should include 'middleware', got: {:?}",
added_dirs.iter().map(|ac| &ac.directory).collect::<Vec<_>>()
);
let summary = report
.arch_summary
.expect("L8 report should have arch_summary");
assert!(
summary.directories_added >= 1,
"Should count at least 1 added directory, got {}",
summary.directories_added
);
assert!(
summary.stability_score < 1.0,
"Adding a directory should lower stability_score from 1.0, got {}",
summary.stability_score
);
}
#[test]
fn test_arch_level_removed_directory() {
let standalone_api = r#"
def handle_request(request):
return {"result": request}
def handle_health():
return {"status": "ok"}
"#;
let standalone_core = r#"
def process(data):
return {"result": data}
def analyze(data):
return {"analysis": len(data)}
"#;
let dir_a = create_test_dir(&[
("api/handler.py", standalone_api),
("core/engine.py", standalone_core),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let dir_b = create_test_dir(&[
("api/handler.py", standalone_api),
("core/engine.py", standalone_core),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(!report.identical);
let arch_changes = report
.arch_changes
.expect("L8 report should have arch_changes");
let removed_dirs: Vec<&ArchLevelChange> = arch_changes
.iter()
.filter(|ac| matches!(ac.change_type, ArchChangeType::Removed))
.collect();
assert!(
!removed_dirs.is_empty(),
"Should detect at least one removed directory"
);
let has_utils = removed_dirs
.iter()
.any(|ac| ac.directory.contains("utils"));
assert!(
has_utils,
"Removed directories should include 'utils', got: {:?}",
removed_dirs.iter().map(|ac| &ac.directory).collect::<Vec<_>>()
);
let summary = report
.arch_summary
.expect("L8 report should have arch_summary");
assert!(
summary.directories_removed >= 1,
"Should count at least 1 removed directory, got {}",
summary.directories_removed
);
}
#[test]
fn test_arch_summary_scores() {
let services_py = r#"
from core.engine import process
def serve(request):
return process(request)
def background_task(data):
return {"processed": data}
"#;
let dir_a = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let dir_b = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("services/worker.py", services_py),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(!report.identical);
let summary = report
.arch_summary
.expect("L8 report should have arch_summary");
assert!(
summary.directories_added >= 1,
"Expected at least 1 added directory (services), got {}",
summary.directories_added
);
assert!(
summary.directories_removed >= 1,
"Expected at least 1 removed directory (utils), got {}",
summary.directories_removed
);
assert!(
summary.stability_score < 1.0,
"Stability should be < 1.0 with directory changes, got {}",
summary.stability_score
);
assert!(
summary.stability_score >= 0.0,
"Stability should be non-negative, got {}",
summary.stability_score
);
let arch_changes = report
.arch_changes
.expect("Should have arch_changes");
assert!(
arch_changes.len() >= 2,
"Should have at least 2 arch changes (added + removed), got {}",
arch_changes.len()
);
}
#[test]
fn test_granularity_field_in_report() {
let files = &[("utils.py", PYTHON_UTILS)];
let dir_a = create_test_dir(files);
let dir_b = create_test_dir(files);
let report_l6 = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
assert_eq!(report_l6.granularity, DiffGranularity::File);
let report_l7 = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert_eq!(report_l7.granularity, DiffGranularity::Module);
let report_l8 = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert_eq!(report_l8.granularity, DiffGranularity::Architecture);
}
#[test]
fn test_directory_input_validation() {
let dir = create_test_dir(&[("utils.py", PYTHON_UTILS)]);
let file_path = dir.path().join("utils.py");
for granularity in &[
DiffGranularity::File,
DiffGranularity::Module,
DiffGranularity::Architecture,
] {
let args = DiffArgs {
file_a: file_path.clone(),
file_b: file_path.clone(),
granularity: *granularity,
semantic_only: false,
output: None,
};
let result = args.run_to_report();
assert!(
result.is_err(),
"Passing files to {:?} granularity should produce an error",
granularity
);
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("director") || err_msg.contains("Director"),
"Error should mention directories, got: {}",
err_msg
);
}
}
#[test]
fn test_diff_report_json_roundtrip_l6() {
let dir_a = create_test_dir(&[("utils.py", PYTHON_UTILS)]);
let dir_b = create_test_dir(&[("utils.py", PYTHON_UTILS_MODIFIED)]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::File);
let json_str = serde_json::to_string_pretty(&report)
.expect("DiffReport should serialize to JSON");
let value: Value = serde_json::from_str(&json_str).expect("JSON should parse");
assert!(value.get("granularity").is_some(), "JSON should contain granularity field");
assert!(value.get("file_changes").is_some(), "JSON should contain file_changes field");
let roundtrip: DiffReport =
serde_json::from_str(&json_str).expect("JSON should deserialize back to DiffReport");
assert_eq!(roundtrip.granularity, DiffGranularity::File);
assert!(roundtrip.file_changes.is_some());
}
#[test]
fn test_diff_report_json_roundtrip_l7() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
("models.py", PYTHON_MODELS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let json_str = serde_json::to_string_pretty(&report)
.expect("DiffReport should serialize to JSON");
let value: Value = serde_json::from_str(&json_str).expect("JSON should parse");
assert!(
value.get("module_changes").is_some(),
"L7 JSON should contain module_changes"
);
assert!(
value.get("import_graph_summary").is_some(),
"L7 JSON should contain import_graph_summary"
);
}
#[test]
fn test_diff_report_json_roundtrip_l8() {
let dir_a = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let dir_b = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
let json_str = serde_json::to_string_pretty(&report)
.expect("DiffReport should serialize to JSON");
let value: Value = serde_json::from_str(&json_str).expect("JSON should parse");
assert!(
value.get("arch_summary").is_some(),
"L8 JSON should contain arch_summary"
);
assert_eq!(
value.get("granularity").and_then(|v| v.as_str()),
Some("architecture"),
"granularity should be 'architecture'"
);
}
const TS_UTILS: &str = r#"
export function helperOne(x: number): number {
return x + 1;
}
export function helperTwo(x: number, y: number): number {
return x * y;
}
"#;
const TS_MODELS: &str = r#"
export interface User {
name: string;
email: string;
}
export function createUser(name: string, email: string): User {
return { name, email };
}
"#;
const TS_MAIN_IMPORTS_UTILS: &str = r#"
import { helperOne, helperTwo } from './utils';
function main(): number {
const result = helperOne(10);
const product = helperTwo(3, 4);
return result + product;
}
"#;
const TS_MAIN_IMPORTS_BOTH: &str = r#"
import { helperOne, helperTwo } from './utils';
import { createUser } from './models';
function main(): number {
const result = helperOne(10);
const product = helperTwo(3, 4);
const user = createUser("Alice", "alice@example.com");
return result + product;
}
"#;
const GO_UTILS: &str = r#"
package utils
func HelperOne(x int) int {
return x + 1
}
func HelperTwo(x, y int) int {
return x * y
}
"#;
const GO_MODELS: &str = r#"
package models
type User struct {
Name string
Email string
}
func NewUser(name, email string) User {
return User{Name: name, Email: email}
}
"#;
const GO_MAIN_IMPORTS_UTILS: &str = r#"
package main
import "myapp/utils"
func main() {
result := utils.HelperOne(10)
_ = result
}
"#;
const GO_MAIN_IMPORTS_BOTH: &str = r#"
package main
import (
"myapp/utils"
"myapp/models"
)
func main() {
result := utils.HelperOne(10)
user := models.NewUser("Alice", "alice@example.com")
_ = result
_ = user
}
"#;
const RS_UTILS: &str = r#"
pub fn helper_one(x: i32) -> i32 {
x + 1
}
pub fn helper_two(x: i32, y: i32) -> i32 {
x * y
}
"#;
const RS_MODELS: &str = r#"
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: String, email: String) -> Self {
User { name, email }
}
}
"#;
const RS_MAIN_IMPORTS_UTILS: &str = r#"
use crate::utils::{helper_one, helper_two};
fn main() {
let result = helper_one(10);
let product = helper_two(3, 4);
println!("{}", result + product);
}
"#;
const RS_MAIN_IMPORTS_BOTH: &str = r#"
use crate::utils::{helper_one, helper_two};
use crate::models::User;
fn main() {
let result = helper_one(10);
let product = helper_two(3, 4);
let user = User::new("Alice".into(), "alice@example.com".into());
println!("{}", result + product);
}
"#;
#[test]
fn test_module_level_typescript_import_added() {
let dir_a = create_test_dir(&[
("utils.ts", TS_UTILS),
("models.ts", TS_MODELS),
("main.ts", TS_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.ts", TS_UTILS),
("models.ts", TS_MODELS),
("main.ts", TS_MAIN_IMPORTS_BOTH),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical, "TypeScript import addition should produce non-identical report");
assert_eq!(report.granularity, DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes for TypeScript");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.ts"))
.expect("main.ts should appear in module_changes");
assert!(
!main_change.imports_added.is_empty(),
"main.ts should have imports_added for the new models import"
);
let has_models_import = main_change.imports_added.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_import,
"imports_added should include an edge to 'models', got: {:?}",
main_change.imports_added
);
}
#[test]
fn test_module_level_typescript_import_removed() {
let dir_a = create_test_dir(&[
("utils.ts", TS_UTILS),
("models.ts", TS_MODELS),
("main.ts", TS_MAIN_IMPORTS_BOTH),
]);
let dir_b = create_test_dir(&[
("utils.ts", TS_UTILS),
("models.ts", TS_MODELS),
("main.ts", TS_MAIN_IMPORTS_UTILS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let module_changes = report
.module_changes
.expect("L7 report should have module_changes");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.ts"))
.expect("main.ts should appear in module_changes");
assert!(
!main_change.imports_removed.is_empty(),
"main.ts should have imports_removed for the models import"
);
let has_models_removal = main_change.imports_removed.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_removal,
"imports_removed should include an edge to 'models', got: {:?}",
main_change.imports_removed
);
}
#[test]
fn test_module_level_go_import_added() {
let dir_a = create_test_dir(&[
("utils/utils.go", GO_UTILS),
("models/models.go", GO_MODELS),
("main.go", GO_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils/utils.go", GO_UTILS),
("models/models.go", GO_MODELS),
("main.go", GO_MAIN_IMPORTS_BOTH),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical, "Go import addition should produce non-identical report");
let module_changes = report
.module_changes
.expect("L7 report should have module_changes for Go");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.go"))
.expect("main.go should appear in module_changes");
assert!(
!main_change.imports_added.is_empty(),
"main.go should have imports_added for the new models import"
);
let has_models_import = main_change.imports_added.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_import,
"imports_added should include an edge to 'models', got: {:?}",
main_change.imports_added
);
}
#[test]
fn test_module_level_rust_import_added() {
let dir_a = create_test_dir(&[
("utils.rs", RS_UTILS),
("models.rs", RS_MODELS),
("main.rs", RS_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.rs", RS_UTILS),
("models.rs", RS_MODELS),
("main.rs", RS_MAIN_IMPORTS_BOTH),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical, "Rust import addition should produce non-identical report");
let module_changes = report
.module_changes
.expect("L7 report should have module_changes for Rust");
let main_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("main.rs"))
.expect("main.rs should appear in module_changes");
assert!(
!main_change.imports_added.is_empty(),
"main.rs should have imports_added for the new models use"
);
let has_models_import = main_change.imports_added.iter().any(|edge| {
edge.target_module.contains("models")
});
assert!(
has_models_import,
"imports_added should include an edge to 'models', got: {:?}",
main_change.imports_added
);
}
#[test]
fn test_module_level_mixed_languages() {
let dir_a = create_test_dir(&[
("backend/app.py", PYTHON_MAIN_IMPORTS_UTILS),
("backend/utils.py", PYTHON_UTILS),
("frontend/app.ts", TS_MAIN_IMPORTS_UTILS),
("frontend/utils.ts", TS_UTILS),
]);
let dir_b = create_test_dir(&[
("backend/app.py", PYTHON_MAIN_IMPORTS_BOTH),
("backend/utils.py", PYTHON_UTILS),
("backend/models.py", PYTHON_MODELS),
("frontend/app.ts", TS_MAIN_IMPORTS_BOTH),
("frontend/utils.ts", TS_UTILS),
("frontend/models.ts", TS_MODELS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical, "Mixed-language import changes should be non-identical");
let module_changes = report
.module_changes
.expect("L7 report should have module_changes for mixed languages");
let py_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("app.py"));
assert!(
py_change.is_some(),
"Python app.py should appear in module_changes"
);
let ts_change = module_changes
.iter()
.find(|mc| mc.module_path.contains("app.ts"));
assert!(
ts_change.is_some(),
"TypeScript app.ts should appear in module_changes"
);
}
#[test]
fn test_module_level_no_crash_on_unsupported_language() {
let dir_a = create_test_dir(&[
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
("utils.py", PYTHON_UTILS),
]);
let dir_b = create_test_dir(&[
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
assert!(!report.identical);
}
#[test]
fn test_module_level_import_graph_summary_multilang() {
let dir_a = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("main.py", PYTHON_MAIN_IMPORTS_UTILS),
("utils.ts", TS_UTILS),
("main.ts", TS_MAIN_IMPORTS_UTILS),
]);
let dir_b = create_test_dir(&[
("utils.py", PYTHON_UTILS),
("models.py", PYTHON_MODELS),
("main.py", PYTHON_MAIN_IMPORTS_BOTH),
("utils.ts", TS_UTILS),
("models.ts", TS_MODELS),
("main.ts", TS_MAIN_IMPORTS_BOTH),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Module);
let summary = report
.import_graph_summary
.expect("L7 report should have import_graph_summary");
assert!(
summary.total_edges_b > summary.total_edges_a,
"dir_b should have more import edges than dir_a: {} vs {}",
summary.total_edges_b,
summary.total_edges_a
);
assert!(
summary.edges_added > 0,
"Should have added edges, got 0"
);
}
#[test]
fn test_arch_level_with_typescript_project() {
let dir_a = create_test_dir(&[
("api/routes.ts", TS_UTILS),
("core/models.ts", TS_MODELS),
("utils/helpers.ts", TS_UTILS),
]);
let dir_b = create_test_dir(&[
("api/routes.ts", TS_UTILS),
("core/models.ts", TS_MODELS),
("utils/helpers.ts", TS_UTILS),
("services/auth.ts", TS_UTILS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(!report.identical, "Adding a service directory should be non-identical");
let arch_changes = report
.arch_changes
.expect("L8 report should have arch_changes");
let services_change = arch_changes
.iter()
.find(|ac| ac.directory == "services")
.expect("services directory should appear in arch_changes");
assert_eq!(services_change.change_type, ArchChangeType::Added);
assert_eq!(
services_change.new_layer.as_deref(),
Some("service"),
"services/ should be classified as 'service' layer"
);
}
#[test]
fn test_arch_level_mixed_language_project() {
let dir_a = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("api/routes.ts", TS_UTILS),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let dir_b = create_test_dir(&[
("api/handler.py", PYTHON_API_HANDLER),
("api/routes.ts", TS_UTILS),
("core/engine.py", PYTHON_CORE_ENGINE),
("utils/helpers.py", PYTHON_UTILS_HELPERS),
]);
let report = run_diff(dir_a.path(), dir_b.path(), DiffGranularity::Architecture);
assert!(report.identical, "Identical mixed-language dirs should be identical");
let summary = report.arch_summary.expect("L8 should have arch_summary");
assert_eq!(summary.stability_score, 1.0, "Identical dirs should have stability_score 1.0");
}