diff --git a/src/bin/cache/main.rs b/src/bin/cache/main.rs new file mode 100644 index 0000000..74c6308 --- /dev/null +++ b/src/bin/cache/main.rs @@ -0,0 +1,42 @@ +use std::process; + +use clap::Parser; +use colored::Colorize; + +use sdkman_cli_native::constants::VAR_DIR; +use sdkman_cli_native::helpers::{infer_sdkman_dir, read_file_content}; + +#[derive(Parser, Debug)] +#[command( + bin_name = "sdk cache", + about = "sdk subcommand to validate the SDKMAN candidates cache" +)] +struct Args; + +fn main() { + let sdkman_dir = infer_sdkman_dir(); + let cache_path = sdkman_dir.join(VAR_DIR).join("candidates"); + + if !cache_path.exists() || !cache_path.is_file() { + print_corrupt_cache_message(); + process::exit(1); + } + + match read_file_content(cache_path) { + Some(_) => {} + None => { + print_corrupt_cache_message(); + process::exit(1); + } + } +} + +fn print_corrupt_cache_message() { + eprintln!( + "{}", + "WARNING: Cache is corrupt. SDKMAN cannot be used until updated.".red() + ); + println!(); + println!(" $ sdk update"); + println!(); +} diff --git a/src/bin/config/main.rs b/src/bin/config/main.rs new file mode 100644 index 0000000..64a598d --- /dev/null +++ b/src/bin/config/main.rs @@ -0,0 +1,61 @@ +use std::env; +use std::process::Command; + +use clap::Parser; +use colored::Colorize; + +use sdkman_cli_native::helpers::infer_sdkman_dir; + +#[derive(Parser, Debug)] +#[command( + bin_name = "sdk config", + about = "sdk subcommand to edit the SDKMAN configuration file" +)] +struct Args; + +fn main() { + let sdkman_dir = infer_sdkman_dir(); + let config_path = sdkman_dir.join("etc").join("config"); + + let editor = env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + + let mut parts = editor.split_whitespace(); + let cmd = match parts.next() { + Some(c) => c, + None => { + eprintln!("{}", "No default editor configured.".red()); + println!( + "{}", + "Please set the default editor with the EDITOR environment variable.".yellow() + ); + std::process::exit(1); + } + }; + let extra_args: Vec<&str> = parts.collect(); + + if which_exists(cmd) { + let mut command = Command::new(cmd); + command.args(&extra_args); + command.arg(&config_path); + let status = command.status().expect("failed to execute editor"); + std::process::exit(status.code().unwrap_or(1)); + } else { + eprintln!( + "{}", + format!("Editor '{}' not found. Please set EDITOR to a valid editor.", cmd).red() + ); + println!( + "{}", + "Please set the default editor with the EDITOR environment variable.".yellow() + ); + std::process::exit(1); + } +} + +fn which_exists(cmd: &str) -> bool { + Command::new("which") + .arg(cmd) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} diff --git a/src/bin/flush/main.rs b/src/bin/flush/main.rs new file mode 100644 index 0000000..c5719ee --- /dev/null +++ b/src/bin/flush/main.rs @@ -0,0 +1,113 @@ +use std::fs; +use std::path::PathBuf; +use std::process; + +use clap::Parser; +use colored::Colorize; + +use sdkman_cli_native::constants::{TMP_DIR, VAR_DIR}; +use sdkman_cli_native::helpers::infer_sdkman_dir; + +#[derive(Parser, Debug)] +#[command( + bin_name = "sdk flush", + about = "sdk subcommand to flush temporary files and metadata" +)] +struct Args { + #[arg(required(false))] + qualifier: Option, +} + +fn main() { + let args = Args::parse(); + let sdkman_dir = infer_sdkman_dir(); + + match args.qualifier.as_deref() { + Some("version") => flush_version(sdkman_dir), + Some("tmp") | Some("temp") => cleanup_folder(sdkman_dir, TMP_DIR), + Some("metadata") => cleanup_folder(sdkman_dir, &format!("{}/metadata", VAR_DIR)), + _ => { + cleanup_folder(sdkman_dir.clone(), TMP_DIR); + cleanup_folder(sdkman_dir, &format!("{}/metadata", VAR_DIR)); + } + } +} + +fn flush_version(sdkman_dir: PathBuf) { + let version_file = sdkman_dir.join(VAR_DIR).join("version"); + if version_file.exists() { + fs::remove_file(&version_file).expect("could not remove version file"); + println!("{}", "Version file has been flushed.".green()); + } +} + +fn cleanup_folder(sdkman_dir: PathBuf, folder: &str) { + let cleanup_dir = sdkman_dir.join(folder); + + if !cleanup_dir.exists() { + fs::create_dir_all(&cleanup_dir).unwrap_or_else(|e| { + eprintln!("could not create directory {}: {}", folder, e); + process::exit(1); + }); + println!( + "{}", + format!("0 archive(s) flushed, freeing 0B for {}.", folder).green() + ); + return; + } + + let count = fs::read_dir(&cleanup_dir) + .map(|entries| entries.count()) + .unwrap_or(0); + + let disk_usage = get_disk_usage(&cleanup_dir); + + fs::remove_dir_all(&cleanup_dir).unwrap_or_else(|e| { + eprintln!("could not remove directory {}: {}", folder, e); + process::exit(1); + }); + + fs::create_dir_all(&cleanup_dir).unwrap_or_else(|e| { + eprintln!("could not recreate directory {}: {}", folder, e); + process::exit(1); + }); + + println!( + "{}", + format!( + "{} archive(s) flushed, freeing {} for {}.", + count, disk_usage, folder + ) + .green() + ); +} + +fn get_disk_usage(path: &PathBuf) -> String { + let total: u64 = fs::read_dir(path) + .ok() + .map(|entries| { + entries + .filter_map(|e| e.ok()) + .filter_map(|e| e.metadata().ok()) + .map(|m| m.len()) + .sum() + }) + .unwrap_or(0); + format_size(total) +} + +fn format_size(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.1}G", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.1}M", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.1}K", bytes as f64 / KB as f64) + } else { + format!("{}B", bytes) + } +} diff --git a/tests/cache.rs b/tests/cache.rs new file mode 100644 index 0000000..7f6b1fa --- /dev/null +++ b/tests/cache.rs @@ -0,0 +1,89 @@ +#[cfg(test)] +use std::env; +use std::fs; +use std::process::Command; + +use assert_cmd::prelude::*; +use predicates::prelude::*; +use serial_test::serial; +use support::{prepare_sdkman_dir, write_file, VirtualEnv}; + +mod support; + +#[test] +#[serial] +fn should_pass_with_valid_cache() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![support::TestCandidate { + name: "java", + versions: vec!["17.0.0-tem"], + current_version: "17.0.0-tem", + }], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + Command::new(assert_cmd::cargo::cargo_bin!("cache")) + .assert() + .success(); + + Ok(()) +} + +#[test] +#[serial] +fn should_fail_with_empty_cache() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + let cache_path = sdkman_dir.path().join("var/candidates"); + fs::write(&cache_path, "")?; + + Command::new(assert_cmd::cargo::cargo_bin!("cache")) + .assert() + .failure() + .stderr(predicate::str::contains("Cache is corrupt")) + .stdout(predicate::str::contains("sdk update")); + + Ok(()) +} + +#[test] +#[serial] +fn should_fail_with_missing_cache() -> Result<(), Box> { + let sdkman_dir = prepare_sdkman_dir(); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + Command::new(assert_cmd::cargo::cargo_bin!("cache")) + .assert() + .failure() + .stderr(predicate::str::contains("Cache is corrupt")); + + Ok(()) +} + +#[test] +#[serial] +fn should_fail_with_whitespace_only_cache() -> Result<(), Box> { + let sdkman_dir = prepare_sdkman_dir(); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + write_file( + sdkman_dir.path(), + std::path::Path::new("var"), + "candidates", + " \n ".to_string(), + ); + + Command::new(assert_cmd::cargo::cargo_bin!("cache")) + .assert() + .failure() + .stderr(predicate::str::contains("Cache is corrupt")); + + Ok(()) +} diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 0000000..4ff8e17 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,59 @@ +#[cfg(test)] +use std::env; +use std::process::Command; + +use assert_cmd::prelude::*; +use predicates::prelude::*; +use serial_test::serial; +use support::{VirtualEnv, write_file}; + +mod support; + +fn setup_with_config() -> tempfile::TempDir { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + // Create the config file that the editor will open + write_file( + sdkman_dir.path(), + std::path::Path::new("etc"), + "config", + "sdkman_auto_answer=false\n".to_string(), + ); + + sdkman_dir +} + +#[test] +#[serial] +fn should_fail_when_editor_not_found() -> Result<(), Box> { + let _sdkman_dir = setup_with_config(); + env::set_var("EDITOR", "nonexistent_editor_xyz"); + + Command::new(assert_cmd::cargo::cargo_bin!("config")) + .assert() + .failure() + .stderr(predicate::str::contains("not found")); + + Ok(()) +} + +#[test] +#[serial] +fn should_open_config_with_cat_as_editor() -> Result<(), Box> { + let _sdkman_dir = setup_with_config(); + env::set_var("EDITOR", "cat"); + + let assert = Command::new(assert_cmd::cargo::cargo_bin!("config")) + .assert() + .success(); + + // cat prints the config file contents to stdout + assert.stdout(predicate::str::contains("sdkman_auto_answer=false")); + + Ok(()) +} diff --git a/tests/flush.rs b/tests/flush.rs new file mode 100644 index 0000000..535421d --- /dev/null +++ b/tests/flush.rs @@ -0,0 +1,149 @@ +#[cfg(test)] +use std::env; +use std::fs; +use std::process::Command; + +use assert_cmd::prelude::*; +use predicates::prelude::*; +use serial_test::serial; +use support::{write_file, VirtualEnv}; + +mod support; + +fn create_tmp_archive(sdkman_dir: &tempfile::TempDir, name: &str) { + write_file( + sdkman_dir.path(), + std::path::Path::new("tmp"), + name, + "fake archive content".to_string(), + ); +} + +fn create_metadata_file(sdkman_dir: &tempfile::TempDir, name: &str) { + write_file( + sdkman_dir.path(), + std::path::Path::new("var/metadata"), + name, + "fake metadata content".to_string(), + ); +} + +#[test] +#[serial] +fn should_flush_tmp_and_metadata_by_default() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + create_tmp_archive(&sdkman_dir, "java-17.0.0-tem.zip"); + create_metadata_file(&sdkman_dir, "java-metadata.json"); + + Command::new(assert_cmd::cargo::cargo_bin!("flush")) + .assert() + .success() + .stdout(predicate::str::contains("archive(s) flushed")) + .stdout(predicate::str::contains("tmp")) + .stdout(predicate::str::contains("var/metadata")); + + assert!(sdkman_dir.path().join("tmp").exists()); + assert!(sdkman_dir.path().join("var/metadata").exists()); + assert!(fs::read_dir(sdkman_dir.path().join("tmp"))?.next().is_none()); + assert!(fs::read_dir(sdkman_dir.path().join("var/metadata"))?.next().is_none()); + + Ok(()) +} + +#[test] +#[serial] +fn should_flush_tmp_only() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + create_tmp_archive(&sdkman_dir, "java-17.0.0-tem.zip"); + + Command::new(assert_cmd::cargo::cargo_bin!("flush")) + .arg("tmp") + .assert() + .success() + .stdout(predicate::str::contains("archive(s) flushed")) + .stdout(predicate::str::contains("tmp")); + + assert!(sdkman_dir.path().join("tmp").exists()); + assert!(fs::read_dir(sdkman_dir.path().join("tmp"))?.next().is_none()); + + Ok(()) +} + +#[test] +#[serial] +fn should_flush_metadata_only() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + create_metadata_file(&sdkman_dir, "java-metadata.json"); + + Command::new(assert_cmd::cargo::cargo_bin!("flush")) + .arg("metadata") + .assert() + .success() + .stdout(predicate::str::contains("archive(s) flushed")) + .stdout(predicate::str::contains("var/metadata")); + + assert!(sdkman_dir.path().join("var/metadata").exists()); + assert!(fs::read_dir(sdkman_dir.path().join("var/metadata"))?.next().is_none()); + + Ok(()) +} + +#[test] +#[serial] +fn should_flush_version_file() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + assert!(sdkman_dir.path().join("var/version").exists()); + + Command::new(assert_cmd::cargo::cargo_bin!("flush")) + .arg("version") + .assert() + .success() + .stdout(predicate::str::contains("Version file has been flushed")); + + assert!(!sdkman_dir.path().join("var/version").exists()); + + Ok(()) +} + +#[test] +#[serial] +fn should_handle_empty_tmp_dir() -> Result<(), Box> { + let sdkman_dir = support::virtual_env(VirtualEnv { + cli_version: "5.0.0".to_string(), + native_version: "0.1.0".to_string(), + candidates: vec![], + }); + env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str()); + + Command::new(assert_cmd::cargo::cargo_bin!("flush")) + .arg("tmp") + .assert() + .success() + .stdout(predicate::str::contains("0 archive(s) flushed")); + + Ok(()) +}