1use std::{
4 collections::BTreeMap,
5 ffi::OsStr,
6 fmt::Display,
7 path::{Path, PathBuf},
8 str::FromStr,
9 sync::{Arc, OnceLock},
10};
11
12use anyhow::{Result, anyhow, bail};
13use cj_path_util::{path_util::AppendToPath, unix::polyfill::add_extension};
14use derive_more::From;
15use kstring::KString;
16
17use crate::{
18 clone, ctx,
19 git::GitHash,
20 info,
21 run::{
22 config::JobTemplate,
23 env_vars::AllowableCustomEnvVar,
24 key::{
25 BenchmarkingJobParameters, CustomParameters, ExtendPath, RunParameters,
26 UncheckedCustomParameters,
27 },
28 },
29 serde_types::{
30 allowed_env_var::AllowedEnvVar, date_and_time::DateTimeWithOffset,
31 proper_dirname::ProperDirname, proper_filename::ProperFilename,
32 },
33 utillib::{
34 arc::CloneArc, into_arc_path::IntoArcPath, invert::Invert, path_is_top::PathIsTop,
35 type_name_short::type_name_short,
36 },
37};
38
39#[derive(Debug)]
52pub struct ParametersDir {
53 base_path: Arc<Path>,
54 target_name: ProperDirname,
55 custom_parameters: CheckedOrUncheckedCustomParameters,
56 path_cache: OnceLock<Arc<Path>>,
57}
58
59#[derive(Debug, Clone)]
62pub struct KeyDir {
63 parent: Arc<ParametersDir>,
64 commit_id: GitHash,
65 path_cache: OnceLock<Arc<Path>>,
66}
67
68#[derive(Debug, Clone)]
71pub struct RunDir {
72 parent: Arc<KeyDir>,
73 timestamp: DateTimeWithOffset,
74 path_cache: OnceLock<Arc<Path>>,
75}
76
77#[derive(Debug, derive_more::From)]
79pub enum OutputSubdir {
80 ParametersDir(Arc<ParametersDir>),
81 KeyDir(Arc<KeyDir>),
82 RunDir(Arc<RunDir>),
83}
84
85pub trait ToPath {
88 fn to_path(&self) -> &Arc<Path>;
91}
92
93pub trait SubDirs: ToPath {
94 type Target;
95
96 fn append_subdir_str(self: Arc<Self>, file_name: &str) -> Result<Self::Target>;
97
98 fn sub_dirs(self: &Arc<Self>) -> Result<impl Iterator<Item = Result<Self::Target>>> {
101 let dir_path = self.to_path().to_owned();
102 Ok(std::fs::read_dir(&dir_path)
103 .map_err(ctx!("opening dir {dir_path:?}"))?
104 .map(|entry| -> Result<Option<Self::Target>> {
105 let entry: std::fs::DirEntry = entry?;
106 let ft = entry.file_type()?;
107 if ft.is_dir() {
108 if let Some(file_name) = entry.file_name().to_str() {
109 match self.clone_arc().append_subdir_str(&file_name) {
110 Ok(v) => Ok(Some(v)),
111 Err(e) => {
112 info!("ignoring subdir that does not parse: {e:#}");
113 Ok(None)
114 }
115 }
116 } else {
117 info!(
118 "ignoring path with file name that doesn't decode as string: {:?}",
119 entry.path()
120 );
121 Ok(None)
122 }
123 } else {
124 Ok(None)
125 }
126 })
127 .filter_map({
128 move |r| {
129 r.map_err(ctx!(
130 "getting {} listing for dir {dir_path:?}",
131 type_name_short::<Self>()
132 ))
133 .transpose()
134 }
135 }))
136 }
137}
138
139pub trait ReplaceBasePath {
140 fn replace_base_path(&self, base_path: Arc<Path>) -> Self;
141}
142
143fn parse_filename<T: FromStr>(file_name: &OsStr) -> Result<T>
146where
147 T::Err: Display,
148{
149 if let Ok(file_name_str) = file_name.to_owned().into_string() {
150 T::from_str(&file_name_str).map_err(|e| {
151 anyhow!(
152 "dir name {file_name_str:?} does not parse as {}: {e:#}",
153 type_name_short::<T>()
154 )
155 })
156 } else {
157 let lossy1 = file_name.to_string_lossy();
158 let lossy: &str = lossy1.as_ref();
159 bail!("can't decode dir name to string: {lossy:?}");
160 }
161}
162
163fn parse_path_filename<T: FromStr>(path: &Path) -> Result<(T, &Path)>
165where
166 T::Err: Display,
167{
168 let file_name = path
169 .file_name()
170 .ok_or_else(|| anyhow!("path is missing a file name"))?;
171 let dir = path
172 .parent()
173 .ok_or_else(|| anyhow!("path is missing a parent dir"))?;
174 match parse_filename(file_name) {
175 Ok(val) => Ok((val, dir)),
176 Err(e) => Err(e),
177 }
178}
179
180#[derive(Debug, Clone, From)]
188pub enum CheckedOrUncheckedCustomParameters {
189 UncheckedCustomParameters(#[from] Arc<UncheckedCustomParameters>),
190 CustomParameters(#[from] Arc<CustomParameters>),
191}
192
193impl CheckedOrUncheckedCustomParameters {
194 pub fn extend_path(&self, path: PathBuf) -> PathBuf {
195 match self {
196 CheckedOrUncheckedCustomParameters::UncheckedCustomParameters(v) => v.extend_path(path),
197 CheckedOrUncheckedCustomParameters::CustomParameters(v) => v.extend_path(path),
198 }
199 }
200
201 pub fn get(&self, key: &AllowedEnvVar<AllowableCustomEnvVar>) -> Option<&str> {
202 match self {
203 CheckedOrUncheckedCustomParameters::UncheckedCustomParameters(v) => {
204 v.btree_map().get(key).map(AsRef::as_ref)
205 }
206 CheckedOrUncheckedCustomParameters::CustomParameters(v) => {
207 v.btree_map().get(key).map(AsRef::as_ref)
208 }
209 }
210 }
211}
212
213impl Display for CheckedOrUncheckedCustomParameters {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 match self {
216 CheckedOrUncheckedCustomParameters::UncheckedCustomParameters(v) => v.fmt(f),
217 CheckedOrUncheckedCustomParameters::CustomParameters(v) => v.fmt(f),
218 }
219 }
220}
221
222impl PartialEq for ParametersDir {
223 fn eq(&self, other: &Self) -> bool {
224 self.to_path().eq(other.to_path())
225 }
226}
227impl Eq for ParametersDir {}
228
229impl PartialOrd for ParametersDir {
230 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
231 self.to_path().partial_cmp(other.to_path())
232 }
233}
234
235impl Ord for ParametersDir {
236 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
237 self.to_path().cmp(other.to_path())
238 }
239}
240
241impl ToPath for ParametersDir {
242 fn to_path(&self) -> &Arc<Path> {
243 let Self {
244 base_path,
245 target_name,
246 custom_parameters,
247 path_cache,
248 } = self;
249 path_cache.get_or_init(|| {
250 custom_parameters
251 .extend_path(base_path.append(target_name.as_str()))
252 .into()
253 })
254 }
255}
256
257impl SubDirs for ParametersDir {
258 type Target = KeyDir;
259
260 fn append_subdir_str(self: Arc<Self>, file_name: &str) -> Result<Self::Target> {
261 let commit_id = parse_filename(file_name.as_ref())?;
262 Ok(KeyDir {
263 parent: self,
264 commit_id,
265 path_cache: Default::default(),
266 })
267 }
268}
269
270impl TryFrom<Arc<Path>> for ParametersDir {
271 type Error = anyhow::Error;
272
273 fn try_from(path: Arc<Path>) -> std::result::Result<Self, Self::Error> {
274 let target_name;
275 let custom_env_vars;
276 let base_path;
277 {
278 let mut current_path = &*path;
279 let mut current_vars = BTreeMap::new();
280 loop {
281 if let Some(dir_name) = current_path.file_name() {
282 let dir_name_str = dir_name.to_str().ok_or_else(|| {
283 anyhow!(
284 "directory segment can't be decoded as string: {:?} in {:?}",
285 dir_name.to_string_lossy().as_ref(),
286 path
287 )
288 })?;
289 if let Some((var_name, val)) = dir_name_str.split_once('=') {
290 let key = AllowedEnvVar::from_str(var_name)?;
291 let val = KString::from_ref(val);
292 current_vars.insert(key, val);
293
294 if let Some(parent) = current_path.parent() {
295 if parent.is_top() {
296 bail!(
297 "parsing {} {:?}: missing target segment left of the var segments",
298 type_name_short::<Self>(),
299 path
300 );
301 }
302 current_path = parent;
303 } else {
304 unreachable!("because file_name() above already failed, right?")
305 }
306 } else {
307 target_name = ProperDirname::from_str(dir_name_str).map_err(|msg| {
308 anyhow!("not a proper directory name: {dir_name_str:?}: {msg}")
309 })?;
310 custom_env_vars = current_vars;
311 if let Some(parent) = current_path.parent() {
312 base_path = parent.into();
313 } else {
314 bail!("path is missing a base_dir part (1): {path:?}")
316 }
317 break;
318 }
319 } else {
320 if current_path.is_top() {
321 bail!("path is missing a target or base_dir part: {path:?}")
322 }
323 bail!("path {path:?} contains a '..' or '.' part: {current_path:?}")
324 }
325 }
326 }
327 Ok(Self {
328 base_path,
329 target_name,
330 custom_parameters: CheckedOrUncheckedCustomParameters::UncheckedCustomParameters(
331 Arc::new(UncheckedCustomParameters::from(custom_env_vars)),
332 ),
333 path_cache: path.into(),
334 })
335 }
336}
337
338impl ReplaceBasePath for ParametersDir {
339 fn replace_base_path(&self, base_path: Arc<Path>) -> Self {
340 let Self {
341 base_path: _,
342 target_name,
343 custom_parameters,
344 path_cache: _,
345 } = self;
346 clone!(target_name);
347 clone!(custom_parameters);
348 Self {
349 base_path,
350 target_name,
351 custom_parameters,
352 path_cache: Default::default(),
353 }
354 }
355}
356
357impl JobTemplate {
360 pub fn to_parameters_dir(&self, base_path: Arc<Path>) -> ParametersDir {
361 ParametersDir::from_job_template(base_path, self)
362 }
363}
364
365impl ParametersDir {
366 pub fn base_path(&self) -> &Arc<Path> {
367 &self.base_path
368 }
369 pub fn target_name(&self) -> &ProperDirname {
370 &self.target_name
371 }
372 pub fn custom_parameters(&self) -> &CheckedOrUncheckedCustomParameters {
373 &self.custom_parameters
374 }
375
376 pub fn from_job_template(base_path: Arc<Path>, job_template: &JobTemplate) -> Self {
377 let JobTemplate {
378 priority: _,
379 initial_boost: _,
380 command,
381 custom_parameters,
382 } = job_template;
383 let target_name = command.target_name.clone();
384 let custom_parameters = custom_parameters.clone_arc().into();
385 Self {
386 base_path,
387 target_name,
388 custom_parameters,
389 path_cache: Default::default(),
390 }
391 }
392}
393
394impl TryFrom<Arc<Path>> for KeyDir {
395 type Error = anyhow::Error;
396
397 fn try_from(path: Arc<Path>) -> std::result::Result<Self, Self::Error> {
398 let (commit_id, parent_dir) = parse_path_filename(&path)?;
399 let parent = ParametersDir::try_from(parent_dir.into_arc_path())?.into();
400 Ok(Self {
401 parent,
402 commit_id,
403 path_cache: path.into(),
404 })
405 }
406}
407
408impl ReplaceBasePath for KeyDir {
409 fn replace_base_path(&self, base_path: Arc<Path>) -> Self {
410 let Self {
411 parent,
412 commit_id,
413 path_cache: _,
414 } = self;
415 let parent = parent.replace_base_path(base_path).into();
416 clone!(commit_id);
417 Self {
418 parent,
419 commit_id,
420 path_cache: Default::default(),
421 }
422 }
423}
424
425impl ToPath for KeyDir {
426 fn to_path(&self) -> &Arc<Path> {
427 let Self {
428 parent,
429 commit_id,
430 path_cache,
431 } = self;
432 path_cache.get_or_init(|| parent.to_path().append(commit_id.to_string()).into())
433 }
434}
435
436impl SubDirs for KeyDir {
437 type Target = RunDir;
438
439 fn append_subdir_str(self: Arc<Self>, file_name: &str) -> Result<Self::Target> {
440 Ok(self.append_subdir(parse_filename(file_name.as_ref())?))
441 }
442}
443
444impl KeyDir {
445 pub fn from_base_target_params(
446 output_base_dir: Arc<Path>,
447 target_name: ProperDirname,
448 RunParameters {
449 commit_id,
450 custom_parameters,
451 }: &RunParameters,
452 ) -> Arc<Self> {
453 let parent = Arc::new(ParametersDir {
454 target_name,
455 custom_parameters: CheckedOrUncheckedCustomParameters::CustomParameters(
456 custom_parameters.clone_arc(),
457 ),
458 base_path: output_base_dir,
459 path_cache: Default::default(),
460 });
461 let commit_id = commit_id.clone();
462 Arc::new(KeyDir {
463 commit_id,
464 parent,
465 path_cache: Default::default(),
466 })
467 }
468
469 pub fn from_benchmarking_job_parameters(
470 output_base_dir: Arc<Path>,
471 benchmarking_job_parameters: &BenchmarkingJobParameters,
472 ) -> Arc<Self> {
473 let BenchmarkingJobParameters {
474 run_parameters,
475 command,
476 } = benchmarking_job_parameters;
477 Self::from_base_target_params(output_base_dir, command.target_name.clone(), run_parameters)
478 }
479
480 pub fn append_subdir(self: Arc<Self>, dir_name: DateTimeWithOffset) -> RunDir {
481 RunDir {
482 parent: self,
483 timestamp: dir_name,
484 path_cache: Default::default(),
485 }
486 }
487
488 pub fn parent(&self) -> &Arc<ParametersDir> {
489 &self.parent
490 }
491 pub fn commit_id(&self) -> &GitHash {
492 &self.commit_id
493 }
494}
495
496impl TryFrom<Arc<Path>> for RunDir {
497 type Error = anyhow::Error;
498
499 fn try_from(path: Arc<Path>) -> std::result::Result<Self, Self::Error> {
500 let (timestamp, parent_path) = parse_path_filename(&path)?;
501 let parent = KeyDir::try_from(parent_path.into_arc_path())?.into();
502 Ok(Self {
503 parent,
504 timestamp,
505 path_cache: path.into(),
506 })
507 }
508}
509
510impl ReplaceBasePath for RunDir {
511 fn replace_base_path(&self, base_path: Arc<Path>) -> Self {
512 let Self {
513 parent,
514 timestamp,
515 path_cache: _,
516 } = self;
517 let parent = parent.replace_base_path(base_path).into();
518 clone!(timestamp);
519 Self {
520 parent,
521 timestamp,
522 path_cache: Default::default(),
523 }
524 }
525}
526
527impl ToPath for RunDir {
528 fn to_path(&self) -> &Arc<Path> {
529 let Self {
530 parent,
531 timestamp,
532 path_cache,
533 } = self;
534 path_cache.get_or_init(|| parent.to_path().append(timestamp.to_string()).into())
535 }
536}
537
538impl RunDir {
539 pub fn parent(&self) -> &Arc<KeyDir> {
540 &self.parent
541 }
542 pub fn timestamp(&self) -> &DateTimeWithOffset {
543 &self.timestamp
544 }
545
546 pub fn evobench_log_path(&self) -> PathBuf {
548 self.to_path().append("evobench.log.zstd")
549 }
550
551 pub fn evobench_log_uncompressed_path(&self) -> PathBuf {
556 add_extension(self.evobench_log_path(), "uncompressed")
557 .expect("evobench_log_path has filename")
558 }
559
560 pub fn bench_output_log_path(&self) -> PathBuf {
564 self.to_path().append("bench_output.log.zstd")
565 }
566
567 pub fn standard_log_path(&self) -> PathBuf {
570 self.to_path().append("standard.log.zstd")
571 }
572
573 pub fn append(&self, file_name: &ProperFilename) -> PathBuf {
576 self.to_path().append(file_name.as_str())
577 }
578
579 pub fn append_str(&self, file_name: &str) -> Result<PathBuf> {
584 let proper = ProperFilename::from_str(file_name)
585 .map_err(|msg| anyhow!("not a proper file name ({msg}): {file_name:?}"))?;
586 Ok(self.append(&proper))
587 }
588}
589
590macro_rules! def_output_subdir_from {
592 { $t:tt } => {
593 impl From<$t> for OutputSubdir {
594 fn from(value: $t) -> Self {
595 Self::$t(Arc::new(value))
596 }
597 }
598 }
599}
600def_output_subdir_from!(ParametersDir);
601def_output_subdir_from!(KeyDir);
602def_output_subdir_from!(RunDir);
603
604impl OutputSubdir {
605 pub fn replace_base_path(self, path: Arc<Path>) -> Self {
606 match self {
607 OutputSubdir::ParametersDir(v) => v.replace_base_path(path).into(),
608 OutputSubdir::KeyDir(v) => v.replace_base_path(path).into(),
609 OutputSubdir::RunDir(v) => v.replace_base_path(path).into(),
610 }
611 }
612
613 pub fn to_path(&self) -> &Arc<Path> {
614 match self {
615 OutputSubdir::ParametersDir(v) => v.to_path(),
616 OutputSubdir::KeyDir(v) => v.to_path(),
617 OutputSubdir::RunDir(v) => v.to_path(),
618 }
619 }
620
621 pub fn type_name(&self) -> &'static str {
622 match self {
623 OutputSubdir::ParametersDir(_) => "ParametersDir",
624 OutputSubdir::KeyDir(_) => "KeyDir",
625 OutputSubdir::RunDir(_) => "RunDir",
626 }
627 }
628}
629
630impl TryFrom<Arc<Path>> for OutputSubdir {
632 type Error = anyhow::Error;
633
634 fn try_from(path: Arc<Path>) -> std::result::Result<Self, Self::Error> {
635 (|| -> Result<anyhow::Error, OutputSubdir> {
640 let e1 = RunDir::try_from(path.clone_arc()).invert()?;
641 let e2 = KeyDir::try_from(path.clone_arc()).invert()?;
642 let e3 = ParametersDir::try_from(path.clone_arc()).invert()?;
643 Ok(anyhow!(
644 "can't parse path {path:?}\n\
645 - as RunDir: {e1:#}\n\
646 - as KeyDir: {e2:#}\n\
647 - as ParametersDir: {e3:#}"
648 ))
649 })()
650 .invert()
651 }
652}
653
654impl ToPath for OutputSubdir {
655 fn to_path(&self) -> &Arc<Path> {
656 match self {
657 OutputSubdir::ParametersDir(v) => v.to_path(),
658 OutputSubdir::KeyDir(v) => v.to_path(),
659 OutputSubdir::RunDir(v) => v.to_path(),
660 }
661 }
662}
663
664impl SubDirs for OutputSubdir {
665 type Target = OutputSubdir;
666
667 fn append_subdir_str(self: Arc<Self>, file_name: &str) -> Result<Self::Target> {
668 Ok(match &*self {
669 OutputSubdir::ParametersDir(v) => v.clone_arc().append_subdir_str(file_name)?.into(),
670 OutputSubdir::KeyDir(v) => v.clone_arc().append_subdir_str(file_name)?.into(),
671 OutputSubdir::RunDir(_) => bail!("can't get subdirs for RunDir instances"),
676 })
677 }
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn t_parameters_dir() {
686 let path = "/home/evobench/silo-benchmark-outputs/api/CONCURRENCY=120/DATASET=SC2open\
687 /RANDOMIZED=1/REPEAT=1/SORTED=0"
688 .into_arc_path();
689 let d = ParametersDir::try_from(path.clone_arc()).unwrap();
690 assert_eq!(
691 d.base_path(),
692 &"/home/evobench/silo-benchmark-outputs".into_arc_path()
693 );
694 assert_eq!(d.target_name().as_str(), "api");
695
696 let p = |name: &str| -> AllowedEnvVar<AllowableCustomEnvVar> { name.parse().unwrap() };
697 assert_eq!(d.custom_parameters().get(&p("CONCURRENCY")), Some("120"));
698 assert_eq!(d.custom_parameters().get(&p("DATASET")), Some("SC2open"));
699 assert_eq!(d.custom_parameters().get(&p("SORTED")), Some("0"));
700
701 assert_eq!(d.to_path(), &path);
702
703 let new_base_path = "foo".into_arc_path();
704 let new_path = "foo/api/CONCURRENCY=120/DATASET=SC2open/RANDOMIZED=1/REPEAT=1/SORTED=0"
705 .into_arc_path();
706 let d2 = d.replace_base_path(new_base_path.clone_arc());
707 assert_eq!(d2.base_path(), &new_base_path);
708 assert_eq!(d2.to_path(), &new_path);
709 }
710
711 #[test]
712 fn t_output_subdir() -> Result<(), String> {
713 let t1 = |s: &str| -> Result<OutputSubdir> {
714 let dir = OutputSubdir::try_from(s.into_arc_path())?;
715 Ok(dir.replace_base_path("BASE".into_arc_path()))
716 };
717 let t2 = |s: &str| -> Result<PathBuf> {
718 let dir = t1(s)?;
719 Ok(dir.to_path().to_path_buf())
720 };
721 let t = |s: &str| -> Result<String, String> {
722 match t2(s) {
723 Ok(p) => Ok(p.to_string_lossy().to_string()),
724 Err(e) => Err(e.to_string()),
725 }
726 };
727
728 assert_eq!(&t("/foo=1//bar=2/fa")?, "BASE/fa");
729 assert_eq!(&t("api/foo=1//bar=2/")?, "BASE/api/bar=2/foo=1");
730 assert_eq!(&t("um/api/foo=1/bar=2/")?, "BASE/api/bar=2/foo=1");
731 assert_eq!(&t("/api/foo=1/bar=2/")?, "BASE/api/bar=2/foo=1");
732 assert_eq!(&t("um/api/foo=1/./bar=2/")?, "BASE/api/bar=2/foo=1");
734 assert_eq!(
736 &t("um/api/foo=1/baz=3/../bar=2/").err().unwrap(),
737 "can't parse path \"um/api/foo=1/baz=3/../bar=2/\"\n- as RunDir: dir name \"bar=2\" does not parse as DateTimeWithOffset: input contains invalid characters\n- as KeyDir: dir name \"bar=2\" does not parse as GitHash: not a git hash of 40 hex bytes: \"bar=2\"\n- as ParametersDir: path \"um/api/foo=1/baz=3/../bar=2/\" contains a '..' or '.' part: \"um/api/foo=1/baz=3/..\""
738 );
739 assert_eq!(
740 &t("api/foo=1/bar=2/09193b52688a964956b3fae0f52eeae471adc027")?,
741 "BASE/api/bar=2/foo=1/09193b52688a964956b3fae0f52eeae471adc027"
742 );
743 assert_eq!(
744 &t("api/foo=1/bar=2/09193b52688a964956b3fae0f52eeae471adc027/\
745 2026-02-02T11:26:48.563793486+00:00")?,
746 "BASE/api/bar=2/foo=1/09193b52688a964956b3fae0f52eeae471adc027/\
747 2026-02-02T11:26:48.563793486+00:00"
748 );
749 assert_eq!(
756 &t("/foo=1//bar=2/09193b52688a964956b3fae0f52eeae471adc027")?,
757 "BASE/09193b52688a964956b3fae0f52eeae471adc027"
758 );
759 assert_eq!(
762 &t("/foo=1//bar=2/09193b52688a964956b3fae0f52eeae471adc027/\
763 2026-02-02T11:26:48.563793486+00:00")
764 .err()
765 .unwrap(),
766 "can't parse path \"/foo=1//bar=2/09193b52688a964956b3fae0f52eeae471adc027/2026-02-02T11:26:48.563793486+00:00\"\n- as RunDir: parsing ParametersDir \"/foo=1//bar=2\": missing target segment left of the var segments\n- as KeyDir: dir name \"2026-02-02T11:26:48.563793486+00:00\" does not parse as GitHash: not a git hash of 40 hex bytes: \"2026-02-02T11:26:48.563793486+00:00\"\n- as ParametersDir: not a proper directory name: \"2026-02-02T11:26:48.563793486+00:00\": a file name (not path), must not contain '/', '\\n', '\\0', and must not be \".\", \"..\", the empty string, or longer than 255 bytes, and not have a file extension"
767 );
768 assert_eq!(
771 &t(".").err().unwrap(),
772 "can't parse path \".\"\n- as RunDir: path is missing a file name\n- as KeyDir: path is missing a file name\n- as ParametersDir: path \".\" contains a '..' or '.' part: \".\""
773 );
774 assert_eq!(
775 &t("./foo=1").err().unwrap(),
776 "can't parse path \"./foo=1\"\n- as RunDir: dir name \"foo=1\" does not parse as DateTimeWithOffset: input contains invalid characters\n- as KeyDir: dir name \"foo=1\" does not parse as GitHash: not a git hash of 40 hex bytes: \"foo=1\"\n- as ParametersDir: path \"./foo=1\" contains a '..' or '.' part: \".\""
777 );
778 assert_eq!(&t("./a/foo=1")?, "BASE/a/foo=1");
779 assert_eq!(&t("./a/./foo=1")?, "BASE/a/foo=1");
780 assert_eq!(
782 &t("./../.").err().unwrap(),
783 "can't parse path \"./../.\"\n- as RunDir: path is missing a file name\n- as KeyDir: path is missing a file name\n- as ParametersDir: path \"./../.\" contains a '..' or '.' part: \"./../.\""
784 );
785 assert_eq!(&t("a/.././b")?, "BASE/b");
786 assert_eq!(&t("a/../b/.")?, "BASE/b");
787 Ok(())
788 }
789}