use super::NextestConfig;
use crate::errors::ConfigPathsCaptureError;
use camino::{Utf8Path, Utf8PathBuf};
use camino_anchored::{
AbsUtf8PathBuf, AnchoredPath, CurrentDirError, DisplayPath, PathAnchor, RelUtf8PathBuf,
ResolvePathError,
};
use std::{
hash::{Hash, Hasher},
sync::Arc,
};
use thiserror::Error;
#[derive(Clone, Debug)]
pub struct InvocationDir(PathAnchor);
impl InvocationDir {
pub fn capture() -> Result<Self, CurrentDirError> {
PathAnchor::current_dir().map(Self)
}
pub fn new(directory: AbsUtf8PathBuf) -> Self {
Self(PathAnchor::new(directory))
}
pub fn resolve_input(&self, path: &Utf8Path) -> Result<ConfigPath, ConfigPathResolveError> {
let resolved = self
.0
.resolve_input(path)
.map_err(|error| ConfigPathResolveError::new(path, error))?;
Ok(ConfigPath(Arc::new(resolved)))
}
fn resolve_absolute(&self, path: AbsUtf8PathBuf) -> ConfigPath {
ConfigPath(Arc::new(self.0.resolve_absolute(path)))
}
}
#[derive(Clone, Debug)]
pub struct WorkspaceRoot(AbsUtf8PathBuf);
impl WorkspaceRoot {
pub fn new(path: AbsUtf8PathBuf) -> Self {
Self(path)
}
pub fn as_path(&self) -> &Utf8Path {
self.0.as_path()
}
}
#[derive(Clone, Debug)]
pub struct ConfigPaths {
invocation: InvocationDir,
workspace_root: WorkspaceRoot,
}
impl ConfigPaths {
pub fn capture(
workspace_root: impl Into<Utf8PathBuf>,
) -> Result<Self, ConfigPathsCaptureError> {
let invocation = InvocationDir::capture().map_err(ConfigPathsCaptureError::CurrentDir)?;
let workspace_root = invocation
.0
.resolve_input(workspace_root.into())
.map_err(ConfigPathsCaptureError::WorkspaceRoot)?;
Ok(Self::new(
invocation,
WorkspaceRoot::new(workspace_root.into_absolute()),
))
}
pub fn new(invocation: InvocationDir, workspace_root: WorkspaceRoot) -> Self {
Self {
invocation,
workspace_root,
}
}
pub fn workspace_root(&self) -> &WorkspaceRoot {
&self.workspace_root
}
pub fn resolve_input(&self, path: &Utf8Path) -> Result<ConfigPath, ConfigPathResolveError> {
self.invocation.resolve_input(path)
}
pub fn repository_config(&self, relative: &RelUtf8PathBuf) -> ConfigPath {
self.invocation
.resolve_absolute(self.workspace_root.0.join(relative))
}
pub fn shared_config(&self) -> ConfigPath {
self.repository_config(
&RelUtf8PathBuf::new(NextestConfig::CONFIG_PATH)
.expect("the shared config path is relative"),
)
}
}
#[derive(Clone, Debug)]
pub struct ConfigPath(Arc<AnchoredPath>);
impl ConfigPath {
pub fn absolute_path(&self) -> &Utf8Path {
self.0.absolute().as_path()
}
pub fn display(&self) -> DisplayPath<'_> {
self.0.display()
}
}
impl PartialEq for ConfigPath {
fn eq(&self, other: &Self) -> bool {
self.0.absolute() == other.0.absolute()
}
}
impl Eq for ConfigPath {}
impl Hash for ConfigPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.absolute().hash(state);
}
}
#[derive(Debug, Error)]
#[error("could not resolve configuration path `{path}`")]
pub struct ConfigPathResolveError {
pub path: Utf8PathBuf,
#[source]
pub error: ResolvePathError,
}
impl ConfigPathResolveError {
fn new(path: impl Into<Utf8PathBuf>, error: ResolvePathError) -> Self {
Self {
path: path.into(),
error,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
config::core::{ConfigFileSelection, VersionOnlyConfig},
errors::ConfigParseErrorKind,
};
use camino_tempfile::tempdir;
use std::fs;
#[test]
fn invocation_and_workspace_are_independent() {
let temp = tempdir().unwrap();
let workspace = temp.path().join("workspace");
for cwd in [
workspace.clone(),
workspace.join("member"),
temp.path().to_owned(),
] {
let paths = ConfigPaths::new(
InvocationDir::new(AbsUtf8PathBuf::new(cwd.clone()).unwrap()),
WorkspaceRoot::new(AbsUtf8PathBuf::new(workspace.clone()).unwrap()),
);
let repository =
paths.repository_config(&RelUtf8PathBuf::new(".config/nextest.toml").unwrap());
assert_eq!(
repository.absolute_path(),
workspace.join(".config/nextest.toml")
);
let expected = repository
.absolute_path()
.strip_prefix(&cwd)
.unwrap_or(repository.absolute_path());
assert_eq!(repository.display().to_string(), expected.as_str());
let explicit = paths.resolve_input(Utf8Path::new("./custom.toml")).unwrap();
assert_eq!(explicit.absolute_path(), cwd.join("./custom.toml"));
assert_eq!(explicit.display().to_string(), "./custom.toml");
}
}
#[test]
fn parse_error_displays_invocation_relative_path() {
let temp = tempdir().unwrap();
let invocation = temp.path().join("invocation");
let workspace = temp.path().join("workspace");
fs::create_dir(&invocation).expect("created the invocation directory");
fs::write(invocation.join("custom.toml"), "nextest-version = [")
.expect("wrote the malformed config");
let paths = ConfigPaths::new(
InvocationDir::new(AbsUtf8PathBuf::new(invocation.clone()).unwrap()),
WorkspaceRoot::new(AbsUtf8PathBuf::new(workspace).unwrap()),
);
let error = VersionOnlyConfig::from_sources_with_paths(
&paths,
ConfigFileSelection::new(Some(Utf8Path::new("custom.toml"))),
&[][..],
)
.expect_err("the malformed config is rejected");
assert_eq!(error.config_file(), invocation.join("custom.toml"));
assert_eq!(error.display_config_file().to_string(), "custom.toml");
match error.kind() {
ConfigParseErrorKind::TomlParseError(_) => {}
other => panic!("expected a TOML parse error, found {other:?}"),
}
}
}