use std::error;
use std::path::{Path, PathBuf};
use toml::Value;
use crate::detection::WorkspaceContext;
use crate::pal::Filesystem;
pub(crate) fn validate_workspace_context(
target_path: &Path,
fs: &impl Filesystem,
) -> Result<WorkspaceContext, Box<dyn error::Error>> {
let current_dir = fs.current_dir()?;
let current_workspace_root =
find_workspace_root(¤t_dir, fs).map_err(|original_error| {
format!("Current directory is not within a Cargo workspace: {original_error}")
})?;
let resolved_target_path = if target_path.is_absolute() {
target_path.to_path_buf()
} else {
let relative_to_current = current_dir.join(target_path);
if fs.exists(&relative_to_current) {
relative_to_current
} else {
current_workspace_root.join(target_path)
}
};
let absolute_target_path = fs.canonicalize(&resolved_target_path).map_err(|error| {
format!(
"Target path '{}' does not exist or cannot be accessed: {error}",
target_path.display()
)
})?;
let target_workspace_root =
find_workspace_root(&absolute_target_path, fs).map_err(|original_error| {
format!("Target path is not within a Cargo workspace: {original_error}")
})?;
let current_workspace_normalized = normalize_path(¤t_workspace_root, fs);
let target_workspace_normalized = normalize_path(&target_workspace_root, fs);
if current_workspace_normalized != target_workspace_normalized {
return Err(format!(
"Current directory workspace ('{}') differs from target path workspace ('{}')",
current_workspace_normalized.display(),
target_workspace_normalized.display()
)
.into());
}
let absolute_target_path_normalized = normalize_path(&absolute_target_path, fs);
Ok(WorkspaceContext {
absolute_target_path: absolute_target_path_normalized,
workspace_root: target_workspace_normalized,
})
}
#[cfg_attr(test, mutants::skip)]
fn find_workspace_root(
start_path: &Path,
fs: &impl Filesystem,
) -> Result<PathBuf, Box<dyn error::Error>> {
let mut current_dir = start_path;
loop {
if fs.cargo_toml_exists(current_dir) {
let contents = fs.read_cargo_toml(current_dir)?;
let value: Value = toml::from_str(&contents)?;
if value.get("workspace").is_some() {
return Ok(fs
.canonicalize(current_dir)
.unwrap_or_else(|_| current_dir.to_path_buf()));
}
}
match current_dir.parent() {
Some(parent) => current_dir = parent,
None => break,
}
}
Err("Could not find workspace root".into())
}
fn normalize_path(path: &Path, fs: &impl Filesystem) -> PathBuf {
let canonical = fs.canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if let Some(path_str) = canonical.to_str()
&& let Some(stripped) = path_str.strip_prefix(r"\\?\")
{
return PathBuf::from(stripped);
}
canonical
}
#[cfg(all(test, not(miri)))]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::fs;
use std::path::Path;
use serial_test::serial;
use super::*;
use crate::pal::FilesystemFacade;
#[test]
#[serial] fn validate_workspace_context_nonexistent_file() {
let fs = FilesystemFacade::target();
let result = validate_workspace_context(Path::new("nonexistent/file.rs"), &fs);
assert!(
result.is_err(),
"Should return error for non-existent files"
);
assert!(result.unwrap_err().to_string().contains("does not exist"));
}
fn create_minimal_workspace_for_validation() -> tempfile::TempDir {
let temp_dir = tempfile::tempdir().unwrap();
let workspace_root = temp_dir.path();
fs::write(
workspace_root.join("Cargo.toml"),
r#"[workspace]
members = ["test_pkg"]
resolver = "2"
"#,
)
.unwrap();
let test_pkg = workspace_root.join("test_pkg");
fs::create_dir_all(test_pkg.join("src")).unwrap();
fs::write(
test_pkg.join("Cargo.toml"),
r#"[package]
name = "test_pkg"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
fs::write(test_pkg.join("src/lib.rs"), "// minimal lib\n").unwrap();
temp_dir
}
#[test]
#[serial] fn validate_workspace_context_from_workspace() {
let workspace = create_minimal_workspace_for_validation();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(workspace.path()).unwrap();
let target_file = Path::new("test_pkg/src/lib.rs");
let fs = FilesystemFacade::target();
let result = validate_workspace_context(target_file, &fs);
std::env::set_current_dir(original_dir).unwrap();
result.unwrap();
}
#[test]
#[serial] fn validate_workspace_context_from_temp_dir() {
let original_dir = std::env::current_dir().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let target_path = Path::new("nonexistent.rs");
let fs = FilesystemFacade::target();
let result = validate_workspace_context(target_path, &fs);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Current directory is not within a Cargo workspace")
);
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
#[serial] fn validate_workspace_context_different_workspaces() {
let temp_dir = tempfile::tempdir().unwrap();
let fake_workspace = temp_dir.path().join("fake_workspace");
fs::create_dir_all(&fake_workspace).unwrap();
fs::write(
fake_workspace.join("Cargo.toml"),
r#"
[workspace]
members = ["package1"]
"#,
)
.unwrap();
let fake_package = fake_workspace.join("package1");
fs::create_dir_all(&fake_package).unwrap();
fs::write(
fake_package.join("Cargo.toml"),
r#"
[package]
name = "fake_package"
version = "0.1.0"
"#,
)
.unwrap();
let other_workspace = temp_dir.path().join("other_workspace");
fs::create_dir_all(&other_workspace).unwrap();
fs::write(
other_workspace.join("Cargo.toml"),
r#"
[workspace]
members = ["other_package"]
"#,
)
.unwrap();
let other_package = other_workspace.join("other_package");
fs::create_dir_all(other_package.join("src")).unwrap();
fs::write(
other_package.join("Cargo.toml"),
r#"
[package]
name = "other_package"
version = "0.1.0"
"#,
)
.unwrap();
fs::write(other_package.join("src").join("lib.rs"), "// test file").unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&fake_workspace).unwrap();
let other_workspace_file = other_package.join("src").join("lib.rs");
let fs = FilesystemFacade::target();
let result = validate_workspace_context(&other_workspace_file, &fs);
result.unwrap_err();
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
#[serial] fn validate_workspace_context_relative_path_outside() {
let fs = FilesystemFacade::target();
let result =
validate_workspace_context(Path::new("../../../outside_workspace/file.rs"), &fs);
assert!(result.is_err(), "Expected error but validation succeeded!");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("does not exist")
|| error_msg.contains("is not within the current workspace"),
"Expected appropriate error message, got: {error_msg}"
);
}
}