nextest_runner/config/core/
paths.rs1use super::NextestConfig;
7use crate::errors::ConfigPathsCaptureError;
8use camino::{Utf8Path, Utf8PathBuf};
9use camino_anchored::{
10 AbsUtf8PathBuf, AnchoredPath, CurrentDirError, DisplayPath, PathAnchor, RelUtf8PathBuf,
11 ResolvePathError,
12};
13use std::{
14 hash::{Hash, Hasher},
15 sync::Arc,
16};
17use thiserror::Error;
18
19#[derive(Clone, Debug)]
21pub struct InvocationDir(PathAnchor);
22
23impl InvocationDir {
24 pub fn capture() -> Result<Self, CurrentDirError> {
26 PathAnchor::current_dir().map(Self)
27 }
28
29 pub fn new(directory: AbsUtf8PathBuf) -> Self {
31 Self(PathAnchor::new(directory))
32 }
33
34 pub fn resolve_input(&self, path: &Utf8Path) -> Result<ConfigPath, ConfigPathResolveError> {
36 let resolved = self
37 .0
38 .resolve_input(path)
39 .map_err(|error| ConfigPathResolveError::new(path, error))?;
40 Ok(ConfigPath(Arc::new(resolved)))
41 }
42
43 fn resolve_absolute(&self, path: AbsUtf8PathBuf) -> ConfigPath {
44 ConfigPath(Arc::new(self.0.resolve_absolute(path)))
45 }
46}
47
48#[derive(Clone, Debug)]
51pub struct WorkspaceRoot(AbsUtf8PathBuf);
52
53impl WorkspaceRoot {
54 pub fn new(path: AbsUtf8PathBuf) -> Self {
56 Self(path)
57 }
58
59 pub fn as_path(&self) -> &Utf8Path {
61 self.0.as_path()
62 }
63}
64
65#[derive(Clone, Debug)]
67pub struct ConfigPaths {
68 invocation: InvocationDir,
69 workspace_root: WorkspaceRoot,
70}
71
72impl ConfigPaths {
73 pub fn capture(
75 workspace_root: impl Into<Utf8PathBuf>,
76 ) -> Result<Self, ConfigPathsCaptureError> {
77 let invocation = InvocationDir::capture().map_err(ConfigPathsCaptureError::CurrentDir)?;
78 let workspace_root = invocation
79 .0
80 .resolve_input(workspace_root.into())
81 .map_err(ConfigPathsCaptureError::WorkspaceRoot)?;
82 Ok(Self::new(
83 invocation,
84 WorkspaceRoot::new(workspace_root.into_absolute()),
85 ))
86 }
87
88 pub fn new(invocation: InvocationDir, workspace_root: WorkspaceRoot) -> Self {
90 Self {
91 invocation,
92 workspace_root,
93 }
94 }
95
96 pub fn workspace_root(&self) -> &WorkspaceRoot {
98 &self.workspace_root
99 }
100
101 pub fn resolve_input(&self, path: &Utf8Path) -> Result<ConfigPath, ConfigPathResolveError> {
103 self.invocation.resolve_input(path)
104 }
105
106 pub fn repository_config(&self, relative: &RelUtf8PathBuf) -> ConfigPath {
108 self.invocation
109 .resolve_absolute(self.workspace_root.0.join(relative))
110 }
111
112 pub fn shared_config(&self) -> ConfigPath {
114 self.repository_config(
115 &RelUtf8PathBuf::new(NextestConfig::CONFIG_PATH)
116 .expect("the shared config path is relative"),
117 )
118 }
119}
120
121#[derive(Clone, Debug)]
124pub struct ConfigPath(Arc<AnchoredPath>);
125
126impl ConfigPath {
127 pub fn absolute_path(&self) -> &Utf8Path {
129 self.0.absolute().as_path()
130 }
131
132 pub fn display(&self) -> DisplayPath<'_> {
134 self.0.display()
135 }
136}
137
138impl PartialEq for ConfigPath {
139 fn eq(&self, other: &Self) -> bool {
140 self.0.absolute() == other.0.absolute()
141 }
142}
143
144impl Eq for ConfigPath {}
145
146impl Hash for ConfigPath {
147 fn hash<H: Hasher>(&self, state: &mut H) {
148 self.0.absolute().hash(state);
149 }
150}
151
152#[derive(Debug, Error)]
154#[error("could not resolve configuration path `{path}`")]
155pub struct ConfigPathResolveError {
156 pub path: Utf8PathBuf,
158 #[source]
160 pub error: ResolvePathError,
161}
162
163impl ConfigPathResolveError {
164 fn new(path: impl Into<Utf8PathBuf>, error: ResolvePathError) -> Self {
165 Self {
166 path: path.into(),
167 error,
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::{
176 config::core::{ConfigFileSelection, VersionOnlyConfig},
177 errors::ConfigParseErrorKind,
178 };
179 use camino_tempfile::tempdir;
180 use std::fs;
181
182 #[test]
183 fn invocation_and_workspace_are_independent() {
184 let temp = tempdir().unwrap();
185 let workspace = temp.path().join("workspace");
186 for cwd in [
187 workspace.clone(),
188 workspace.join("member"),
189 temp.path().to_owned(),
190 ] {
191 let paths = ConfigPaths::new(
192 InvocationDir::new(AbsUtf8PathBuf::new(cwd.clone()).unwrap()),
193 WorkspaceRoot::new(AbsUtf8PathBuf::new(workspace.clone()).unwrap()),
194 );
195 let repository =
196 paths.repository_config(&RelUtf8PathBuf::new(".config/nextest.toml").unwrap());
197 assert_eq!(
198 repository.absolute_path(),
199 workspace.join(".config/nextest.toml")
200 );
201 let expected = repository
202 .absolute_path()
203 .strip_prefix(&cwd)
204 .unwrap_or(repository.absolute_path());
205 assert_eq!(repository.display().to_string(), expected.as_str());
206 let explicit = paths.resolve_input(Utf8Path::new("./custom.toml")).unwrap();
207 assert_eq!(explicit.absolute_path(), cwd.join("./custom.toml"));
208 assert_eq!(explicit.display().to_string(), "./custom.toml");
209 }
210 }
211
212 #[test]
213 fn parse_error_displays_invocation_relative_path() {
214 let temp = tempdir().unwrap();
215 let invocation = temp.path().join("invocation");
216 let workspace = temp.path().join("workspace");
217 fs::create_dir(&invocation).expect("created the invocation directory");
218 fs::write(invocation.join("custom.toml"), "nextest-version = [")
219 .expect("wrote the malformed config");
220 let paths = ConfigPaths::new(
221 InvocationDir::new(AbsUtf8PathBuf::new(invocation.clone()).unwrap()),
222 WorkspaceRoot::new(AbsUtf8PathBuf::new(workspace).unwrap()),
223 );
224
225 let error = VersionOnlyConfig::from_sources_with_paths(
226 &paths,
227 ConfigFileSelection::new(Some(Utf8Path::new("custom.toml"))),
228 &[][..],
229 )
230 .expect_err("the malformed config is rejected");
231
232 assert_eq!(error.config_file(), invocation.join("custom.toml"));
233 assert_eq!(error.display_config_file().to_string(), "custom.toml");
234 match error.kind() {
235 ConfigParseErrorKind::TomlParseError(_) => {}
236 other => panic!("expected a TOML parse error, found {other:?}"),
237 }
238 }
239}