use crate::error::Result;
use crate::storage::{InstalledJdk, JdkRepository};
use log::{debug, warn};
use std::path::{Path, PathBuf};
pub struct PostUninstallChecker<'a> {
repository: &'a JdkRepository<'a>,
}
impl<'a> PostUninstallChecker<'a> {
pub fn new(repository: &'a JdkRepository<'a>) -> Self {
Self { repository }
}
pub fn validate_removal(&self, removed_jdk: &InstalledJdk) -> Result<PostUninstallReport> {
debug!(
"Validating removal of {}@{}",
removed_jdk.distribution, removed_jdk.version
);
let mut report = PostUninstallReport {
jdk_completely_removed: false,
orphaned_metadata_files: Vec::new(),
shim_functionality_intact: false,
remaining_jdks: Vec::new(),
suggested_actions: Vec::new(),
};
report.jdk_completely_removed = self.verify_complete_removal(&removed_jdk.path)?;
report.orphaned_metadata_files = self.check_orphaned_metadata(&removed_jdk.path)?;
report.remaining_jdks = self.repository.list_installed_jdks()?;
report.shim_functionality_intact = self.validate_shim_functionality()?;
report.suggested_actions = self.generate_suggested_actions(&report);
Ok(report)
}
fn verify_complete_removal(&self, jdk_path: &Path) -> Result<bool> {
debug!("Verifying complete removal of {}", jdk_path.display());
if jdk_path.exists() {
warn!(
"JDK directory still exists after removal: {}",
jdk_path.display()
);
return Ok(false);
}
if let Some(parent) = jdk_path.parent() {
if parent.exists() {
for entry in std::fs::read_dir(parent)? {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.starts_with('.') && file_name.ends_with(".removing") {
warn!("Found temporary removal file: {}", path.display());
return Ok(false);
}
}
}
}
}
Ok(true)
}
fn check_orphaned_metadata(&self, jdk_path: &Path) -> Result<Vec<PathBuf>> {
debug!(
"Checking for orphaned metadata files related to {}",
jdk_path.display()
);
let mut orphaned_files = Vec::new();
if jdk_path.exists() {
let meta_file = jdk_path.join(".meta.json");
if meta_file.exists() {
orphaned_files.push(meta_file);
}
}
if let Some(parent) = jdk_path.parent() {
if parent.exists() {
for entry in std::fs::read_dir(parent)? {
let entry = entry?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if file_name.ends_with(".meta.json") && path.is_file() {
if let Some(jdk_name) = jdk_path.file_name().and_then(|n| n.to_str()) {
if file_name.contains(jdk_name) {
orphaned_files.push(path);
}
}
}
}
}
}
}
Ok(orphaned_files)
}
fn validate_shim_functionality(&self) -> Result<bool> {
debug!("Validating shim functionality");
let remaining_jdks = self.repository.list_installed_jdks()?;
if remaining_jdks.is_empty() {
debug!("No JDKs remain - shim functionality will be limited");
return Ok(false); }
Ok(true)
}
fn generate_suggested_actions(&self, report: &PostUninstallReport) -> Vec<String> {
let mut actions = Vec::new();
if !report.jdk_completely_removed {
actions.push("Run 'kopi doctor' to check for issues with JDK removal".to_string());
actions.push("Consider manually removing any remaining JDK files".to_string());
}
if !report.orphaned_metadata_files.is_empty() {
actions.push("Clean up orphaned metadata files to free disk space".to_string());
}
if !report.shim_functionality_intact {
if report.remaining_jdks.is_empty() {
actions.push("Consider installing a JDK to restore full functionality".to_string());
actions.push("Run 'kopi list --remote' to see available JDKs".to_string());
} else {
actions.push("Run 'kopi doctor' to diagnose shim issues".to_string());
}
}
if report.remaining_jdks.is_empty() {
actions
.push("All JDKs have been removed. Your shell PATH may need updating.".to_string());
}
actions
}
pub fn cleanup_orphaned_metadata(&self, report: &PostUninstallReport) -> Result<usize> {
debug!(
"Cleaning up {} orphaned metadata files",
report.orphaned_metadata_files.len()
);
let mut cleaned_count = 0;
for file_path in &report.orphaned_metadata_files {
match std::fs::remove_file(file_path) {
Ok(()) => {
debug!("Removed orphaned metadata file: {}", file_path.display());
cleaned_count += 1;
}
Err(e) => {
warn!(
"Failed to remove orphaned metadata file {}: {}",
file_path.display(),
e
);
}
}
}
Ok(cleaned_count)
}
}
#[derive(Debug)]
pub struct PostUninstallReport {
pub jdk_completely_removed: bool,
pub orphaned_metadata_files: Vec<PathBuf>,
pub shim_functionality_intact: bool,
pub remaining_jdks: Vec<InstalledJdk>,
pub suggested_actions: Vec<String>,
}
impl PostUninstallReport {
pub fn is_successful(&self) -> bool {
self.jdk_completely_removed && self.orphaned_metadata_files.is_empty()
}
pub fn get_summary(&self) -> String {
if self.is_successful() {
if self.remaining_jdks.is_empty() {
"✓ JDK removed successfully. No JDKs remain installed.".to_string()
} else {
format!(
"✓ JDK removed successfully. {} JDK{} remain installed.",
self.remaining_jdks.len(),
if self.remaining_jdks.len() == 1 {
""
} else {
"s"
}
)
}
} else {
let mut issues = Vec::new();
if !self.jdk_completely_removed {
issues.push("incomplete removal");
}
if !self.orphaned_metadata_files.is_empty() {
issues.push("orphaned metadata files");
}
format!("âš JDK removal completed with issues: {}", issues.join(", "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::KopiConfig;
use crate::version::Version;
use std::fs;
use std::str::FromStr;
use tempfile::TempDir;
fn create_test_setup() -> (TempDir, KopiConfig) {
let temp_dir = TempDir::new().unwrap();
let config = KopiConfig::new(temp_dir.path().to_path_buf()).unwrap();
fs::create_dir_all(config.jdks_dir().unwrap()).unwrap();
(temp_dir, config)
}
fn create_mock_jdk(config: &KopiConfig, distribution: &str, version: &str) -> InstalledJdk {
let jdk_path = config
.jdks_dir()
.unwrap()
.join(format!("{distribution}-{version}"));
fs::create_dir_all(&jdk_path).unwrap();
fs::write(jdk_path.join("release"), "JAVA_VERSION=\"21\"").unwrap();
fs::create_dir_all(jdk_path.join("bin")).unwrap();
fs::write(jdk_path.join("bin/java"), "#!/bin/sh\necho mock java").unwrap();
InstalledJdk {
distribution: distribution.to_string(),
version: Version::from_str(version).unwrap(),
path: jdk_path,
}
}
#[test]
fn test_verify_complete_removal_success() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let jdk = create_mock_jdk(&config, "temurin", "21.0.1");
fs::remove_dir_all(&jdk.path).unwrap();
let result = checker.verify_complete_removal(&jdk.path).unwrap();
assert!(result);
}
#[test]
fn test_verify_complete_removal_failure() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let jdk = create_mock_jdk(&config, "temurin", "21.0.1");
let result = checker.verify_complete_removal(&jdk.path).unwrap();
assert!(!result);
}
#[test]
fn test_check_orphaned_metadata() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let jdk = create_mock_jdk(&config, "temurin", "21.0.1");
let meta_file = jdk.path.join(".meta.json");
fs::write(&meta_file, "{}").unwrap();
let orphaned = checker.check_orphaned_metadata(&jdk.path).unwrap();
assert_eq!(orphaned.len(), 1);
assert_eq!(orphaned[0], meta_file);
}
#[test]
fn test_validate_shim_functionality() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let result = checker.validate_shim_functionality().unwrap();
assert!(!result);
create_mock_jdk(&config, "temurin", "21.0.1");
let result = checker.validate_shim_functionality().unwrap();
assert!(result);
}
#[test]
fn test_validate_removal_report() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let jdk = create_mock_jdk(&config, "temurin", "21.0.1");
create_mock_jdk(&config, "corretto", "17.0.9");
fs::remove_dir_all(&jdk.path).unwrap();
let report = checker.validate_removal(&jdk).unwrap();
assert!(report.jdk_completely_removed);
assert!(report.orphaned_metadata_files.is_empty());
assert!(report.shim_functionality_intact);
assert_eq!(report.remaining_jdks.len(), 1);
assert!(report.suggested_actions.is_empty());
}
#[test]
fn test_cleanup_orphaned_metadata() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let jdk = create_mock_jdk(&config, "temurin", "21.0.1");
let meta_file = jdk.path.join(".meta.json");
fs::write(&meta_file, "{}").unwrap();
let report = PostUninstallReport {
jdk_completely_removed: true,
orphaned_metadata_files: vec![meta_file.clone()],
shim_functionality_intact: true,
remaining_jdks: Vec::new(),
suggested_actions: Vec::new(),
};
let cleaned = checker.cleanup_orphaned_metadata(&report).unwrap();
assert_eq!(cleaned, 1);
assert!(!meta_file.exists());
}
#[test]
fn test_post_uninstall_report_summary() {
let report = PostUninstallReport {
jdk_completely_removed: true,
orphaned_metadata_files: Vec::new(),
shim_functionality_intact: true,
remaining_jdks: Vec::new(),
suggested_actions: Vec::new(),
};
assert!(report.is_successful());
assert!(report.get_summary().contains("No JDKs remain installed"));
let report_with_remaining = PostUninstallReport {
jdk_completely_removed: true,
orphaned_metadata_files: Vec::new(),
shim_functionality_intact: true,
remaining_jdks: vec![InstalledJdk {
distribution: "temurin".to_string(),
version: Version::from_str("21.0.1").unwrap(),
path: "/test/path".into(),
}],
suggested_actions: Vec::new(),
};
assert!(report_with_remaining.is_successful());
assert!(report_with_remaining.get_summary().contains("1 JDK remain"));
}
#[test]
fn test_generate_suggested_actions() {
let (_temp_dir, config) = create_test_setup();
let repository = JdkRepository::new(&config);
let checker = PostUninstallChecker::new(&repository);
let report = PostUninstallReport {
jdk_completely_removed: false,
orphaned_metadata_files: vec!["/test/meta.json".into()],
shim_functionality_intact: false,
remaining_jdks: Vec::new(),
suggested_actions: Vec::new(),
};
let actions = checker.generate_suggested_actions(&report);
assert!(!actions.is_empty());
assert!(actions.iter().any(|a| a.contains("kopi doctor")));
assert!(actions.iter().any(|a| a.contains("metadata files")));
assert!(actions.iter().any(|a| a.contains("installing a JDK")));
}
}