use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
if !dst.exists() {
fs::create_dir_all(dst)?;
}
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
fn run_dissolve_command(args: &[&str], cwd: Option<&std::path::Path>) -> (String, String, bool) {
let mut cmd = Command::new("cargo");
cmd.args(["run", "--"]);
cmd.args(args);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
let output = cmd.output().expect("Failed to execute dissolve command");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let success = output.status.success();
(stdout, stderr, success)
}
fn create_example_copy() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let example_src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("example");
let example_dst = temp_dir.path().join("example");
copy_dir_recursive(&example_src, &example_dst).expect("Failed to copy example directory");
(temp_dir, example_dst)
}
fn normalize_output(output: &str, temp_path: &std::path::Path) -> String {
let temp_str = temp_path.to_string_lossy();
let normalized = output.replace(&*temp_str, "<TEMP>");
let normalized = normalized.replace("\\", "/");
let mut lines: Vec<&str> = normalized.lines().collect();
if lines.len() > 5 && lines[0].contains("deprecated function") {
let mut summary_start = lines.len();
for (i, line) in lines.iter().enumerate() {
if line.contains("=== Summary ===") {
summary_start = i;
break;
}
}
if summary_start < lines.len() {
lines[..summary_start].sort();
}
}
lines.join("\n")
}
#[test]
fn test_dissolve_info_command() {
let (_temp_dir, example_path) = create_example_copy();
let (stdout, stderr, success) =
run_dissolve_command(&["info", &example_path.to_string_lossy()], None);
assert!(success, "dissolve info command failed. stderr: {}", stderr);
assert!(stdout.contains("deprecated function(s)"));
assert!(stdout.contains("=== Summary ==="));
assert!(stdout.contains("Total files analyzed:"));
assert!(stdout.contains("Total deprecated functions found:"));
assert!(stdout.contains("library/utils.py"));
assert!(stdout.contains("library/models.py"));
assert!(stdout.contains("library/async_ops.py"));
assert!(stdout.contains("library/config.py"));
assert!(stdout.contains("library/containers.py"));
assert!(stdout.contains("library/processors.py"));
assert!(stdout.contains("old_add"));
assert!(stdout.contains("old_multiply"));
assert!(stdout.contains("OldDataProcessor"));
assert!(stdout.contains("LegacyUser"));
let expected_info_output = normalize_output(&stdout, &example_path);
let expected_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("info_output.txt");
if let Some(parent) = expected_file.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&expected_file, &expected_info_output).ok();
println!("Info command output length: {} characters", stdout.len());
println!(
"Info command found {} files",
stdout.matches(".py:").count()
);
}
#[test]
fn test_dissolve_check_command() {
let (_temp_dir, example_path) = create_example_copy();
let (stdout, stderr, _success) =
run_dissolve_command(&["check", &example_path.to_string_lossy()], None);
assert!(
stdout.contains("@replace_me function(s) can be replaced")
|| stdout.contains("ERRORS found")
);
assert!(stdout.contains("library/utils.py"));
assert!(stdout.contains("library/models.py"));
println!("Check command output: {}", stdout);
if !stderr.is_empty() {
println!("Check command stderr: {}", stderr);
}
let expected_check_output = normalize_output(&stdout, &example_path);
let expected_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("check_output.txt");
if let Some(parent) = expected_file.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&expected_file, &expected_check_output).ok();
}
#[test]
fn test_dissolve_migrate_command() {
let (_temp_dir, example_path) = create_example_copy();
let consumer_path = example_path.join("consumer");
let (stdout, stderr, success) =
run_dissolve_command(&["migrate", &consumer_path.to_string_lossy()], None);
assert!(
success,
"dissolve migrate command failed. stderr: {}",
stderr
);
println!("Migrate command (dry run) output: {}", stdout);
let (stdout_write, stderr_write, success_write) = run_dissolve_command(
&["migrate", &consumer_path.to_string_lossy(), "--write"],
None,
);
assert!(
success_write,
"dissolve migrate --write command failed. stderr: {}",
stderr_write
);
println!("Migrate command (--write) output: {}", stdout_write);
let expected_file_dry = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("migrate_dry_output.txt");
let expected_file_write = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("migrate_write_output.txt");
if let Some(parent) = expected_file_dry.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&expected_file_dry, normalize_output(&stdout, &example_path)).ok();
fs::write(
&expected_file_write,
normalize_output(&stdout_write, &example_path),
)
.ok();
}
#[test]
fn test_dissolve_cleanup_command() {
let (_temp_dir, example_path) = create_example_copy();
let library_path = example_path.join("library");
let (stdout, stderr, _success) =
run_dissolve_command(&["cleanup", &library_path.to_string_lossy()], None);
println!("Cleanup command output: {}", stdout);
if !stderr.is_empty() {
println!("Cleanup command stderr: {}", stderr);
}
let expected_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("expected")
.join("cleanup_output.txt");
if let Some(parent) = expected_file.parent() {
fs::create_dir_all(parent).ok();
}
fs::write(&expected_file, normalize_output(&stdout, &example_path)).ok();
}
#[test]
fn test_full_workflow_integration() {
let (_temp_dir, example_path) = create_example_copy();
let (info_out, _, info_success) =
run_dissolve_command(&["info", &example_path.to_string_lossy()], None);
assert!(info_success);
assert!(info_out.contains("Total deprecated functions found:"));
let (check_out, _, _check_success) =
run_dissolve_command(&["check", &example_path.to_string_lossy()], None);
assert!(check_out.contains("@replace_me function(s)") || check_out.contains("ERRORS found"));
let consumer_path = example_path.join("consumer");
let (migrate_out, _, migrate_success) = run_dissolve_command(
&["migrate", &consumer_path.to_string_lossy(), "--write"],
None,
);
assert!(migrate_success);
let library_path = example_path.join("library");
let (cleanup_out, _, _cleanup_success) =
run_dissolve_command(&["cleanup", &library_path.to_string_lossy()], None);
println!("Full workflow completed successfully");
println!("Info found {} files", info_out.matches(".py:").count());
println!("Migration output: {}", migrate_out);
println!("Cleanup output: {}", cleanup_out);
}
#[test]
fn test_module_qualification_fix() {
use crate::core::{ConstructType, ParameterInfo, ReplaceInfo};
use crate::migrate_ruff::migrate_file;
use crate::tests::test_utils::TestContext;
use crate::type_introspection_context::TypeIntrospectionContext;
use crate::types::TypeIntrospectionMethod;
use std::collections::HashMap;
use std::path::Path;
let source = r#"
# Test source that uses deprecated function
from library import utils
result = utils.old_function(42, extra="test")
"#;
let mut replacements = HashMap::new();
let mut replace_info = ReplaceInfo::new(
"library.utils.old_function",
"new_function({value}, extra={extra})",
ConstructType::Function,
);
replace_info.parameters = vec![
ParameterInfo::from_name("value"),
ParameterInfo::from_name("extra"),
];
if let Ok(rustpython_ast::Mod::Module(module)) = rustpython_parser::parse(
"def old_function(value, extra): return new_function(value, extra=extra)",
rustpython_parser::Mode::Module,
"<test>",
) {
if let Some(rustpython_ast::Stmt::FunctionDef(func)) = module.body.first() {
if let Some(rustpython_ast::Stmt::Return(ret)) = func.body.first() {
replace_info.replacement_ast = ret.value.clone();
}
}
}
replacements.insert("library.utils.old_function".to_string(), replace_info);
let test_ctx = TestContext::new(source);
let mut type_context =
TypeIntrospectionContext::new(TypeIntrospectionMethod::PyrightLsp).unwrap();
let result = migrate_file(
source,
"test_module",
Path::new(&test_ctx.file_path),
&mut type_context,
replacements,
HashMap::new(),
)
.unwrap();
type_context.shutdown().unwrap();
assert!(
result.contains("library.new_function(42, extra=\"test\")"),
"Expected 'library.new_function(42, extra=\"test\")', got:\n{}",
result
);
println!("Module qualification fix test passed!");
}