use crate::paths;
use super::super::types::{Check, CheckResult};
fn classify_version_entry(
id: &'static str,
name: &'static str,
entry: &str,
venvs_dir: Option<&std::path::Path>,
) -> CheckResult {
if entry.eq_ignore_ascii_case("system") {
return CheckResult::ok(id, name).with_details("system Python (no virtualenv)");
}
let env_exists = venvs_dir
.map(|dir| dir.join(entry).exists())
.unwrap_or(false);
if env_exists {
CheckResult::ok(id, name).with_details(format!("set to '{}'", entry))
} else {
CheckResult::error(id, name, format!("references non-existent env '{}'", entry))
.with_suggestion(format!("Run: scuv create {} <python-version>", entry))
}
}
fn resolve_local_version_file_for_doctor(dir: &std::path::Path) -> std::path::PathBuf {
let version_file = paths::local_version_file(dir);
if version_file.exists() {
version_file
} else {
let legacy = dir.join(paths::LEGACY_VERSION_FILE);
if legacy.exists() {
legacy
} else {
version_file
}
}
}
pub(super) struct VersionCheck;
impl Check for VersionCheck {
fn id(&self) -> &'static str {
"version"
}
fn name(&self) -> &'static str {
"version files"
}
fn run(&self) -> Vec<CheckResult> {
let mut results = Vec::new();
let venvs_dir = paths::virtualenvs_dir().ok();
if let Ok(global_file) = paths::global_version_file() {
if global_file.exists() {
match std::fs::read_to_string(&global_file) {
Ok(content) => {
let env_name = content.trim();
if !env_name.is_empty() {
results.push(classify_version_entry(
"version:global",
"global version",
env_name,
venvs_dir.as_deref(),
));
}
}
Err(_) => {
results.push(
CheckResult::warn(
"version:global",
"global version",
"could not read global version file",
)
.with_suggestion(format!("Check file: {}", global_file.display())),
);
}
}
}
}
let current_dir = match std::env::current_dir() {
Ok(dir) => dir,
Err(_) => return results,
};
let local_file = resolve_local_version_file_for_doctor(¤t_dir);
if local_file.exists() {
match std::fs::read_to_string(&local_file) {
Ok(content) => {
let env_name = content.trim();
if !env_name.is_empty() {
results.push(classify_version_entry(
"version:local",
"local version",
env_name,
venvs_dir.as_deref(),
));
}
}
Err(_) => {
results.push(
CheckResult::warn(
"version:local",
"local version",
"could not read local version file",
)
.with_suggestion(format!("Check file: {}", local_file.display())),
);
}
}
}
if results.is_empty() {
results.push(
CheckResult::ok(self.id(), self.name()).with_details("no version files configured"),
);
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::doctor::CheckStatus;
use crate::core::doctor::checks::test_support::TempDirCwdGuard;
use crate::test_utils::with_temp_scoop_home;
use serial_test::serial;
#[test]
fn classify_treats_system_as_valid_sentinel() {
let result = classify_version_entry("version:test", "test version", "system", None);
assert!(
result.is_ok(),
"system must classify as Ok, got {result:#?}"
);
assert!(
result
.details
.as_deref()
.is_some_and(|d| d.contains("system Python")),
"details should explain the sentinel, got {:?}",
result.details
);
}
#[test]
fn classify_is_case_insensitive_for_system_sentinel() {
for variant in ["SYSTEM", "System", "sYsTeM"] {
let result = classify_version_entry("version:test", "test version", variant, None);
assert!(
result.is_ok(),
"case-variant '{variant}' must classify as Ok"
);
}
}
#[test]
fn classify_existing_env_is_ok() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("myenv")).unwrap();
let result =
classify_version_entry("version:test", "test version", "myenv", Some(tmp.path()));
assert!(result.is_ok());
assert!(
result
.details
.as_deref()
.is_some_and(|d| d.contains("myenv"))
);
}
#[test]
fn classify_missing_env_is_error() {
let tmp = tempfile::tempdir().unwrap();
let result = classify_version_entry(
"version:test",
"test version",
"missingenv",
Some(tmp.path()),
);
assert!(result.is_error());
assert!(
result
.suggestion
.as_deref()
.is_some_and(|s| s.contains("scuv create"))
);
}
#[test]
#[serial]
fn version_check_treats_global_system_sentinel_as_ok() {
with_temp_scoop_home(|temp| {
std::fs::write(temp.path().join("version"), "system").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let global = results
.iter()
.find(|r| r.id == "version:global")
.expect("version:global must run when ~/.scoop/version exists");
assert!(
global.is_ok(),
"version:global must be Ok for system sentinel, got {global:#?}"
);
});
}
#[test]
#[serial]
fn version_check_treats_local_system_sentinel_as_ok() {
with_temp_scoop_home(|temp| {
let cwd_guard = TempDirCwdGuard::new();
std::fs::write(cwd_guard.path().join(".scuv-version"), "system").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let local = results
.iter()
.find(|r| r.id == "version:local")
.expect("version:local check should run when .scuv-version exists");
assert!(
local.is_ok(),
"version:local must be Ok for system sentinel, got {local:#?}"
);
});
}
#[test]
#[serial]
fn version_check_local_read_error_suggestion_names_version_file() {
with_temp_scoop_home(|temp| {
let cwd_guard = TempDirCwdGuard::new();
std::fs::create_dir(cwd_guard.path().join(paths::VERSION_FILE)).unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let local = results
.iter()
.find(|r| r.id == "version:local")
.expect("version:local check should run when the version-file path exists");
assert!(
local.is_warning(),
"expected a warning for an unreadable version file, got {local:#?}"
);
let suggestion = local.suggestion.as_deref().unwrap_or_default();
assert!(
suggestion.contains(paths::VERSION_FILE),
"suggestion should name the actual version-file path, got: {suggestion}"
);
});
}
#[test]
#[serial]
fn version_check_sees_legacy_only_local_version_file() {
with_temp_scoop_home(|temp| {
let cwd_guard = TempDirCwdGuard::new();
std::fs::write(cwd_guard.path().join(".scoop-version"), "ghost-env").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let local = results
.iter()
.find(|r| r.id == "version:local")
.expect("version:local must be reported from a legacy-only .scoop-version file");
assert!(
local.is_error(),
"references a non-existent env, so it should error, got {local:#?}"
);
assert!(
matches!(&local.status, CheckStatus::Error(msg) if msg.contains("ghost-env")),
"error should name the env read from the legacy file, got {local:#?}"
);
});
}
#[test]
#[serial]
fn version_check_prefers_new_file_over_legacy_when_both_present() {
with_temp_scoop_home(|temp| {
let cwd_guard = TempDirCwdGuard::new();
std::fs::write(cwd_guard.path().join(".scuv-version"), "newenv").unwrap();
std::fs::write(cwd_guard.path().join(".scoop-version"), "oldenv").unwrap();
std::fs::create_dir_all(temp.path().join("virtualenvs")).unwrap();
let results = VersionCheck.run();
let local = results
.iter()
.find(|r| r.id == "version:local")
.expect("version:local must run");
assert!(
matches!(&local.status, CheckStatus::Error(msg) if msg.contains("newenv")),
"new-named file must win when both are present, got {local:#?}"
);
});
}
#[test]
#[serial]
fn version_run_summary_reports_id_and_name_when_no_files() {
let tmp = tempfile::tempdir().unwrap();
let _cwd = TempDirCwdGuard::new(); let _g = crate::test_utils::env_guard(&[
(paths::SCUV_HOME_ENV, Some(tmp.path().to_str().unwrap())),
("SCUV_VERSION", None),
("SCOOP_VERSION", None),
]);
let results = VersionCheck.run();
let summary = results.iter().find(|r| r.id == "version");
assert!(
summary.is_some(),
"expected a 'version' summary result: {results:#?}"
);
assert_eq!(summary.unwrap().name, "version files");
}
}