Skip to content

Migrate current command to the native CLI #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ assemble:
- input: 'target/{{ osPlatformReplaced }}/release'
output: libexec
includes:
- 'current{.exe}'
- 'default{.exe,}'
- 'help{.exe,}'
- 'home{.exe,}'
Expand Down
100 changes: 100 additions & 0 deletions src/bin/current/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::fs;
use std::path::PathBuf;
use std::process;

use clap::Parser;
use colored::Colorize;

use sdkman_cli_native::constants::{CANDIDATES_DIR, CURRENT_DIR};
use sdkman_cli_native::helpers::{infer_sdkman_dir, known_candidates, validate_candidate};

#[derive(Parser, Debug)]
#[command(
bin_name = "sdk current",
about = "sdk subcommand to display the current version in use for one or all candidates"
)]
struct Args {
#[arg(required(false))]
candidate: Option<String>,
}

fn main() {
let args = Args::parse();
let sdkman_dir = infer_sdkman_dir();
let all_candidates = known_candidates(sdkman_dir.to_owned());

match args.candidate {
Some(candidate) => {
// Show current version for a specific candidate
let candidate = validate_candidate(all_candidates, &candidate);
let current_version = get_current_version(sdkman_dir.to_owned(), &candidate);
match current_version {
Some(version) => println!("Using {} version {}", candidate.bold(), version.bold()),
None => {
eprintln!("No current version of {} configured.", candidate.bold());
process::exit(1);
}
}
}
None => {
// Show current version for all candidates
let mut found_any = false;
let mut candidates_with_versions = Vec::new();

// Collect all candidates with their versions first
for candidate in all_candidates {
let current_version = get_current_version(sdkman_dir.to_owned(), candidate);
if let Some(version) = current_version {
candidates_with_versions.push((candidate, version));
found_any = true;
}
}

if found_any {
// Print header
println!("{}", "Current versions in use:".bold());

// Print all candidate versions
for (candidate, version) in candidates_with_versions {
println!("{} {}", candidate, version);
}
} else {
eprintln!("No candidates are in use.");
process::exit(0);
}
}
}
}

fn get_current_version(base_dir: PathBuf, candidate: &str) -> Option<String> {
// First check if the candidate is installed
let candidate_dir = base_dir.join(CANDIDATES_DIR).join(candidate);
if !candidate_dir.exists() || !candidate_dir.is_dir() {
return None;
}

// Check for current symlink
let current_link = candidate_dir.join(CURRENT_DIR);
if !current_link.exists() {
return None;
}

// Get the symlink target (which should be the version)
if let Ok(target) = fs::read_link(&current_link) {
// Extract the version from the path
return target
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string());
}

// If this is not a symlink but a directory (fallback case)
if current_link.is_dir() {
return current_link
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string());
}

None
}
193 changes: 193 additions & 0 deletions tests/current.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#[cfg(test)]
use std::env;
use std::path::Path;
use std::process::Command;

use assert_cmd::prelude::*;
use predicates::prelude::*;
use serial_test::serial;
use support::{TestCandidate, VirtualEnv};

mod support;

#[test]
#[serial]
fn should_show_current_version_for_specific_candidate() -> Result<(), Box<dyn std::error::Error>> {
let name = "java";
let current_version = "11.0.15-tem";
let versions = vec!["11.0.15-tem", "17.0.3-tem"];

let env = VirtualEnv {
cli_version: "5.0.0".to_string(),
native_version: "0.1.0".to_string(),
candidates: vec![TestCandidate {
name,
versions: versions.clone(),
current_version,
}],
};

let sdkman_dir = support::virtual_env(env);
env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str());

let expected_output = format!("Using {} version {}", name, current_version);
let contains_expected = predicate::str::contains(expected_output);

Command::cargo_bin("current")?
.arg(name)
.assert()
.success()
.stdout(contains_expected)
.code(0);

Ok(())
}

#[test]
#[serial]
fn should_show_current_versions_for_all_candidates() -> Result<(), Box<dyn std::error::Error>> {
// Define multiple candidates with their versions
let java_name = "java";
let java_current_version = "11.0.15-tem";
let java_versions = vec!["11.0.15-tem", "17.0.3-tem"];

let kotlin_name = "kotlin";
let kotlin_current_version = "1.7.22";
let kotlin_versions = vec!["1.6.21", "1.7.22"];

let env = VirtualEnv {
cli_version: "5.0.0".to_string(),
native_version: "0.1.0".to_string(),
candidates: vec![
TestCandidate {
name: java_name,
versions: java_versions.clone(),
current_version: java_current_version,
},
TestCandidate {
name: kotlin_name,
versions: kotlin_versions.clone(),
current_version: kotlin_current_version,
},
],
};

let sdkman_dir = support::virtual_env(env);
env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str());

// Expected output patterns for the simple format (candidate version)
let expected_java_output = format!("{} {}", java_name, java_current_version);
let expected_kotlin_output = format!("{} {}", kotlin_name, kotlin_current_version);

// Check for both expected outputs
let contains_java_output = predicate::str::contains(expected_java_output);
let contains_kotlin_output = predicate::str::contains(expected_kotlin_output);

Command::cargo_bin("current")?
.assert()
.success()
.stdout(contains_java_output.and(contains_kotlin_output))
.code(0);

Ok(())
}

#[test]
#[serial]
fn should_show_error_for_non_existent_candidate() -> Result<(), Box<dyn std::error::Error>> {
let invalid_name = "invalid";

// Create a simple environment with an empty candidates file
let env = VirtualEnv {
cli_version: "5.0.0".to_string(),
native_version: "0.1.0".to_string(),
candidates: vec![],
};

let sdkman_dir = support::virtual_env(env);

// Write at least one valid candidate to avoid empty candidates list error
support::write_file(
sdkman_dir.path(),
Path::new("var"),
"candidates",
"java".to_string(),
);

env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str());

let contains_error = predicate::str::contains(invalid_name);

Command::cargo_bin("current")?
.arg(invalid_name)
.assert()
.failure()
.stderr(contains_error)
.code(1);

Ok(())
}

#[test]
#[serial]
fn should_show_error_for_candidate_with_no_current_version(
) -> Result<(), Box<dyn std::error::Error>> {
// Create a candidate entry in candidates file, but no directory structure
let sdkman_dir = support::prepare_sdkman_dir();

// Write candidates file with a candidate
let candidate_name = "kotlin";
support::write_file(
sdkman_dir.path(),
Path::new("var"),
"candidates",
candidate_name.to_string(),
);

// Create candidate directory but no current symlink
let candidate_dir = Path::new("candidates").join(candidate_name);
std::fs::create_dir_all(sdkman_dir.path().join(&candidate_dir))
.expect("Failed to create candidate directory");

env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str());

let contains_error = predicate::str::contains("No current version of");

Command::cargo_bin("current")?
.arg(candidate_name)
.assert()
.failure()
.stderr(contains_error)
.code(1);

Ok(())
}

#[test]
#[serial]
fn should_show_message_when_no_candidates_in_use() -> Result<(), Box<dyn std::error::Error>> {
// Create empty candidates file, but ensure it has at least one character (e.g., "kotlin")
// to avoid causing a panic in the known_candidates function
let sdkman_dir = support::prepare_sdkman_dir();
support::write_file(
sdkman_dir.path(),
Path::new("var"),
"candidates",
"kotlin".to_string(),
);

// Create candidates dir structure but without current symlinks
std::fs::create_dir_all(sdkman_dir.path().join("candidates/kotlin"))
.expect("Failed to create candidate directory");

env::set_var("SDKMAN_DIR", sdkman_dir.path().as_os_str());

let contains_message = predicate::str::contains("No candidates are in use");

Command::cargo_bin("current")?
.assert()
.stderr(contains_message)
.code(0);

Ok(())
}
33 changes: 15 additions & 18 deletions tests/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ mod support;
#[test]
#[serial]
fn should_set_an_installed_version_as_default() -> Result<(), Box<dyn std::error::Error>> {
let candidate = TestCandidate {
name: "scala",
versions: vec!["0.0.1", "0.0.2"],
current_version: "0.0.1",
};
let env = VirtualEnv {
cli_version: "0.0.1".to_string(),
native_version: "0.0.1".to_string(),
candidate: Some(candidate),
candidates: vec![TestCandidate {
name: "scala",
versions: vec!["0.0.1", "0.0.2"],
current_version: "0.0.1",
}],
};

let sdkman_dir = support::virtual_env(env);
Expand Down Expand Up @@ -50,15 +49,14 @@ fn should_set_an_installed_version_as_default() -> Result<(), Box<dyn std::error
#[test]
#[serial]
fn should_reset_the_current_default_version_as_default() -> Result<(), Box<dyn std::error::Error>> {
let candidate = TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
};
let env = VirtualEnv {
cli_version: "0.0.1".to_string(),
native_version: "0.0.1".to_string(),
candidate: Some(candidate),
candidates: vec![TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
}],
};

let sdkman_dir = support::virtual_env(env);
Expand Down Expand Up @@ -90,15 +88,14 @@ fn should_reset_the_current_default_version_as_default() -> Result<(), Box<dyn s
#[test]
#[serial]
fn should_not_set_an_uninstalled_version_as_default() -> Result<(), Box<dyn std::error::Error>> {
let candidate = TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
};
let env = VirtualEnv {
cli_version: "0.0.1".to_string(),
native_version: "0.0.1".to_string(),
candidate: Some(candidate),
candidates: vec![TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
}],
};

let sdkman_dir = support::virtual_env(env);
Expand Down
11 changes: 5 additions & 6 deletions tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ mod support;
#[test]
#[serial]
fn should_fail_if_candidate_is_unknown() -> Result<(), Box<dyn std::error::Error>> {
let candidate = TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
};
let env = VirtualEnv {
cli_version: "0.0.1".to_string(),
native_version: "0.0.1".to_string(),
candidate: Some(candidate),
candidates: vec![TestCandidate {
name: "scala",
versions: vec!["0.0.1"],
current_version: "0.0.1",
}],
};

let sdkman_dir = support::virtual_env(env);
Expand Down
Loading
Loading