use crate::dependency_collector::collect_deprecated_from_dependencies_with_paths;
use crate::migrate_ruff::migrate_file;
use crate::type_introspection_context::TypeIntrospectionContext;
use crate::TypeIntrospectionMethod;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_module(dir: &std::path::Path, rel_path: &str, content: &str) -> PathBuf {
let full_path = dir.join(rel_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&full_path, content).unwrap();
full_path
}
#[test]
fn test_dulwich_porcelain_migration() {
let temp_dir = TempDir::new().unwrap();
create_module(temp_dir.path(), "dulwich/__init__.py", "");
create_module(
temp_dir.path(),
"dulwich/repo.py",
r#"from dissolve import replace_me
class BaseRepo:
@replace_me
def stage(self, fs_paths):
"""Deprecated stage method."""
return self.get_worktree().stage(fs_paths)
def get_worktree(self):
return WorkTree()
class Repo(BaseRepo):
def __init__(self, path):
self.path = path
class WorkTree:
def stage(self, fs_paths):
pass
"#,
);
create_module(
temp_dir.path(),
"dulwich/errors.py",
"class NotGitRepository(Exception): pass\n",
);
let porcelain_source = r#"from .repo import BaseRepo, Repo
from .errors import NotGitRepository
def open_repo_closing(path="."):
"""Open a repository that will auto-close."""
return Repo(path)
def add(repo=".", paths=None):
"""Add files to repository."""
if paths is None:
paths = []
with open_repo_closing(repo) as r:
# This should be migrated
r.stage(paths)
def add_multiple(repo=".", file_list=None):
"""Add multiple files."""
with open_repo_closing(repo) as r:
for f in file_list or []:
r.stage([f]) # This should also be migrated
def simple_test():
"""Simple test without context manager."""
repo = Repo(".")
repo.stage(["file.txt"]) # This should definitely be migrated
"#;
let porcelain_path = create_module(temp_dir.path(), "dulwich/porcelain.py", porcelain_source);
let additional_paths = vec![temp_dir.path().to_string_lossy().to_string()];
let dep_result = collect_deprecated_from_dependencies_with_paths(
porcelain_source,
"dulwich.porcelain",
5,
&additional_paths,
)
.unwrap();
println!(
"Found replacements: {:?}",
dep_result.replacements.keys().collect::<Vec<_>>()
);
assert!(dep_result
.replacements
.keys()
.any(|k| k.contains("stage") && (k.contains("BaseRepo") || k.contains("Repo"))));
let mut type_context = TypeIntrospectionContext::new_with_workspace(
TypeIntrospectionMethod::PyrightLsp,
Some(temp_dir.path().to_str().unwrap()),
)
.unwrap();
type_context
.open_file(&temp_dir.path().join("dulwich/__init__.py"), "")
.unwrap();
type_context
.open_file(
&temp_dir.path().join("dulwich/repo.py"),
&std::fs::read_to_string(temp_dir.path().join("dulwich/repo.py")).unwrap(),
)
.unwrap();
let result = migrate_file(
porcelain_source,
"dulwich.porcelain",
&porcelain_path,
&mut type_context,
dep_result.replacements,
dep_result.inheritance_map,
)
.unwrap();
type_context.shutdown().unwrap();
println!("Migrated result:\n{}", result);
if result.contains("repo.get_worktree().stage([\"file.txt\"])") {
println!("Simple case works - issue is with context manager type inference");
} else {
println!("Even simple case doesn't work - more fundamental issue");
}
assert!(
result.contains("repo.get_worktree().stage([\"file.txt\"])"),
"Simple direct call should be migrated"
);
assert!(result.contains("with open_repo_closing(repo) as r:"));
assert!(result.contains("from .repo import BaseRepo, Repo"));
}
#[test]
fn test_dulwich_nested_context_managers() {
let temp_dir = TempDir::new().unwrap();
create_module(temp_dir.path(), "testpkg/__init__.py", "");
create_module(
temp_dir.path(),
"testpkg/base.py",
r#"
from dissolve import replace_me
class Resource:
@replace_me()
def old_method(self, x):
return self.new_method(x * 2)
def new_method(self, x):
return x
class Manager:
def __enter__(self):
return Resource()
def __exit__(self, *args):
pass
"#,
);
let source = r#"
from .base import Manager
def process():
with Manager() as outer:
with Manager() as inner:
result1 = outer.old_method(5)
result2 = inner.old_method(10)
return result1 + result2
"#;
let file_path = create_module(temp_dir.path(), "testpkg/usage.py", source);
let additional_paths = vec![temp_dir.path().to_string_lossy().to_string()];
let dep_result = collect_deprecated_from_dependencies_with_paths(
source,
"testpkg.usage",
5,
&additional_paths,
)
.unwrap();
let mut type_context = TypeIntrospectionContext::new_with_workspace(
TypeIntrospectionMethod::PyrightLsp,
Some(temp_dir.path().to_str().unwrap()),
)
.unwrap();
let result = migrate_file(
source,
"testpkg.usage",
&file_path,
&mut type_context,
dep_result.replacements,
dep_result.inheritance_map,
)
.unwrap();
type_context.shutdown().unwrap();
println!("Nested context manager result:\n{}", result);
println!("Test completed - context manager type inference is a known limitation");
}
#[test]
fn test_dulwich_with_type_annotations() {
let temp_dir = TempDir::new().unwrap();
create_module(temp_dir.path(), "repo_pkg/__init__.py", "");
create_module(
temp_dir.path(),
"repo_pkg/types.py",
r#"
from typing import Union, Optional, List
from dissolve import replace_me
class Repository:
@replace_me()
def commit(self, message: str, author: Optional[str] = None) -> str:
return self.create_commit(message=message, author=author)
def create_commit(self, message: str, author: Optional[str] = None) -> str:
return f"commit: {message}"
def get_repo(path: Union[str, bytes]) -> Repository:
return Repository()
"#,
);
let source = r#"
from typing import List, Optional
from .types import get_repo, Repository
def make_commits(repo_path: str, messages: List[str], author: Optional[str] = None) -> List[str]:
repo = get_repo(repo_path)
results = []
for msg in messages:
# This should be migrated with proper type handling
commit_id = repo.commit(msg, author)
results.append(commit_id)
return results
"#;
let file_path = create_module(temp_dir.path(), "repo_pkg/operations.py", source);
let additional_paths = vec![temp_dir.path().to_string_lossy().to_string()];
let dep_result = collect_deprecated_from_dependencies_with_paths(
source,
"repo_pkg.operations",
5,
&additional_paths,
)
.unwrap();
let replacement_keys: Vec<String> = dep_result.replacements.keys().cloned().collect();
println!("Replacements found: {:?}", replacement_keys);
let mut type_context = TypeIntrospectionContext::new_with_workspace(
TypeIntrospectionMethod::PyrightLsp,
Some(temp_dir.path().to_str().unwrap()),
)
.unwrap();
let result = migrate_file(
source,
"repo_pkg.operations",
&file_path,
&mut type_context,
dep_result.replacements,
dep_result.inheritance_map,
)
.unwrap();
type_context.shutdown().unwrap();
println!("Migration result:\n{}", result);
assert!(
!result.contains("repo.commit(msg, author)"),
"Failed to migrate repo.commit(msg, author)"
);
assert!(result.contains("repo.create_commit(message=msg, author=author)"));
assert!(result.contains("repo_path: str"));
assert!(result.contains("messages: List[str]"));
assert!(result.contains("author: Optional[str] = None"));
}