1use std::fmt;
35
36pub const BENCH_NAME_MAX_LEN: usize = 64;
37pub const BENCH_NAME_PATTERN: &str = r"^[a-z0-9_.\-/]+$";
38
39#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
40pub enum ValidationError {
41 #[error("bench name must not be empty")]
42 Empty,
43
44 #[error("bench name {name:?} exceeds maximum length of {max_len} characters")]
45 TooLong { name: String, max_len: usize },
46
47 #[error(
48 "bench name {name:?} contains invalid characters; \
49 allowed: lowercase alphanumeric, dots, underscores, hyphens, slashes"
50 )]
51 InvalidCharacters { name: String },
52
53 #[error(
54 "bench name {name:?} contains an empty path segment \
55 (leading, trailing, or consecutive slashes are forbidden)"
56 )]
57 EmptySegment { name: String },
58
59 #[error(
60 "bench name {name:?} contains a {segment:?} path segment (path traversal is forbidden)"
61 )]
62 PathTraversal { name: String, segment: String },
63}
64
65impl ValidationError {
66 pub fn name(&self) -> &str {
67 match self {
68 ValidationError::Empty => "",
69 ValidationError::TooLong { name, .. } => name,
70 ValidationError::InvalidCharacters { name } => name,
71 ValidationError::EmptySegment { name } => name,
72 ValidationError::PathTraversal { name, .. } => name,
73 }
74 }
75}
76
77pub fn validate_bench_name(name: &str) -> std::result::Result<(), ValidationError> {
78 if name.is_empty() {
79 return Err(ValidationError::Empty);
80 }
81 if name.len() > BENCH_NAME_MAX_LEN {
82 return Err(ValidationError::TooLong {
83 name: name.to_string(),
84 max_len: BENCH_NAME_MAX_LEN,
85 });
86 }
87 if !name.chars().all(|c| {
88 c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '.' || c == '/' || c == '-'
89 }) {
90 return Err(ValidationError::InvalidCharacters {
91 name: name.to_string(),
92 });
93 }
94 for segment in name.split('/') {
95 if segment.is_empty() {
96 return Err(ValidationError::EmptySegment {
97 name: name.to_string(),
98 });
99 }
100 if segment == "." || segment == ".." {
101 return Err(ValidationError::PathTraversal {
102 name: name.to_string(),
103 segment: segment.to_string(),
104 });
105 }
106 }
107 Ok(())
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
111pub enum StatsError {
112 #[error("no samples to summarize")]
113 NoSamples,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
117pub enum PairedError {
118 #[error("no samples to summarize")]
119 NoSamples,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
123pub enum AdapterError {
124 #[error("command argv must not be empty")]
125 EmptyArgv,
126
127 #[error("command timed out")]
128 Timeout,
129
130 #[error("timeout is not supported on this platform")]
131 TimeoutUnsupported,
132
133 #[error("{0}")]
134 Other(String),
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
138pub enum ConfigValidationError {
139 #[error("bench name validation: {0}")]
140 BenchName(String),
141
142 #[error("config validation: {0}")]
143 ConfigFile(String),
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
147pub enum IoError {
148 #[error("baseline resolve: {0}")]
149 BaselineResolve(String),
150
151 #[error("write artifacts: {0}")]
152 ArtifactWrite(String),
153
154 #[error("run command: {0}")]
155 RunCommand(String),
156
157 #[error("IO error: {0}")]
158 Other(String),
159}
160
161#[derive(Debug, thiserror::Error)]
162pub enum PerfgateError {
163 #[error(transparent)]
164 Validation(#[from] ValidationError),
165
166 #[error(transparent)]
167 Stats(#[from] StatsError),
168
169 #[error(transparent)]
170 Adapter(#[from] AdapterError),
171
172 #[error(transparent)]
173 Config(#[from] ConfigValidationError),
174
175 #[error(transparent)]
176 Io(#[from] IoError),
177
178 #[error(transparent)]
179 Paired(#[from] PairedError),
180}
181
182impl From<std::io::Error> for PerfgateError {
183 fn from(err: std::io::Error) -> Self {
184 PerfgateError::Io(IoError::Other(err.to_string()))
185 }
186}
187
188impl From<std::io::Error> for IoError {
189 fn from(err: std::io::Error) -> Self {
190 IoError::Other(err.to_string())
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195pub enum ErrorCategory {
196 Validation,
197 Stats,
198 Adapter,
199 Config,
200 Io,
201 Paired,
202}
203
204impl PerfgateError {
205 pub fn category(&self) -> ErrorCategory {
206 match self {
207 PerfgateError::Validation(_) => ErrorCategory::Validation,
208 PerfgateError::Stats(_) => ErrorCategory::Stats,
209 PerfgateError::Adapter(_) => ErrorCategory::Adapter,
210 PerfgateError::Config(_) => ErrorCategory::Config,
211 PerfgateError::Io(_) => ErrorCategory::Io,
212 PerfgateError::Paired(_) => ErrorCategory::Paired,
213 }
214 }
215
216 pub fn is_recoverable(&self) -> bool {
217 match self {
218 PerfgateError::Validation(_) => false,
219 PerfgateError::Stats(StatsError::NoSamples) => false,
220 PerfgateError::Adapter(AdapterError::EmptyArgv) => false,
221 PerfgateError::Adapter(AdapterError::Timeout) => true,
222 PerfgateError::Adapter(AdapterError::TimeoutUnsupported) => false,
223 PerfgateError::Adapter(AdapterError::Other(_)) => true,
224 PerfgateError::Config(_) => false,
225 PerfgateError::Io(_) => true,
226 PerfgateError::Paired(PairedError::NoSamples) => false,
227 }
228 }
229
230 pub fn exit_code(&self) -> i32 {
231 match self {
232 PerfgateError::Validation(_) => 1,
233 PerfgateError::Stats(_) => 1,
234 PerfgateError::Adapter(AdapterError::Timeout) => 1,
235 PerfgateError::Adapter(AdapterError::EmptyArgv) => 1,
236 PerfgateError::Adapter(AdapterError::TimeoutUnsupported) => 1,
237 PerfgateError::Adapter(AdapterError::Other(_)) => 1,
238 PerfgateError::Config(_) => 1,
239 PerfgateError::Io(_) => 1,
240 PerfgateError::Paired(_) => 1,
241 }
242 }
243}
244
245impl ErrorCategory {
246 pub fn as_str(&self) -> &'static str {
247 match self {
248 ErrorCategory::Validation => "validation",
249 ErrorCategory::Stats => "stats",
250 ErrorCategory::Adapter => "adapter",
251 ErrorCategory::Config => "config",
252 ErrorCategory::Io => "io",
253 ErrorCategory::Paired => "paired",
254 }
255 }
256}
257
258impl fmt::Display for ErrorCategory {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 write!(f, "{}", self.as_str())
261 }
262}
263
264pub type Result<T> = std::result::Result<T, PerfgateError>;
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn validation_error_empty() {
272 let err = ValidationError::Empty;
273 assert!(err.to_string().contains("empty"));
274 }
275
276 #[test]
277 fn validation_error_too_long() {
278 let err = ValidationError::TooLong {
279 name: "test".to_string(),
280 max_len: 64,
281 };
282 assert!(err.to_string().contains("exceeds maximum length"));
283 }
284
285 #[test]
286 fn validation_error_invalid_chars() {
287 let err = ValidationError::InvalidCharacters {
288 name: "TEST".to_string(),
289 };
290 assert!(err.to_string().contains("invalid characters"));
291 }
292
293 #[test]
294 fn validation_error_path_traversal() {
295 let err = ValidationError::PathTraversal {
296 name: "../test".to_string(),
297 segment: "..".to_string(),
298 };
299 assert!(err.to_string().contains("path traversal"));
300 }
301
302 #[test]
303 fn adapter_error_empty_argv() {
304 let err = AdapterError::EmptyArgv;
305 assert!(err.to_string().contains("argv"));
306 }
307
308 #[test]
309 fn adapter_error_timeout() {
310 let err = AdapterError::Timeout;
311 assert!(err.to_string().contains("timed out"));
312 }
313
314 #[test]
315 fn adapter_error_timeout_unsupported() {
316 let err = AdapterError::TimeoutUnsupported;
317 assert!(err.to_string().contains("not supported"));
318 }
319
320 #[test]
321 fn adapter_error_other() {
322 let err = AdapterError::Other("something went wrong".to_string());
323 assert!(err.to_string().contains("something went wrong"));
324 }
325
326 #[test]
327 fn config_validation_error_bench_name() {
328 let err = ConfigValidationError::BenchName("invalid name".to_string());
329 assert!(err.to_string().contains("bench name"));
330 }
331
332 #[test]
333 fn config_validation_error_config_file() {
334 let err = ConfigValidationError::ConfigFile("missing field".to_string());
335 assert!(err.to_string().contains("config"));
336 }
337
338 #[test]
339 fn io_error_baseline_resolve() {
340 let err = IoError::BaselineResolve("file not found".to_string());
341 assert!(err.to_string().contains("baseline resolve"));
342 }
343
344 #[test]
345 fn io_error_artifact_write() {
346 let err = IoError::ArtifactWrite("permission denied".to_string());
347 assert!(err.to_string().contains("write artifacts"));
348 }
349
350 #[test]
351 fn io_error_run_command() {
352 let err = IoError::RunCommand("spawn failed".to_string());
353 assert!(err.to_string().contains("run command"));
354 }
355
356 #[test]
357 fn perfgate_error_from_validation() {
358 let err: PerfgateError = ValidationError::Empty.into();
359 assert!(matches!(
360 err,
361 PerfgateError::Validation(ValidationError::Empty)
362 ));
363 assert_eq!(err.category(), ErrorCategory::Validation);
364 }
365
366 #[test]
367 fn perfgate_error_from_stats() {
368 let err: PerfgateError = StatsError::NoSamples.into();
369 assert!(matches!(err, PerfgateError::Stats(StatsError::NoSamples)));
370 assert_eq!(err.category(), ErrorCategory::Stats);
371 }
372
373 #[test]
374 fn perfgate_error_from_adapter() {
375 let err: PerfgateError = AdapterError::Timeout.into();
376 assert!(matches!(err, PerfgateError::Adapter(AdapterError::Timeout)));
377 assert_eq!(err.category(), ErrorCategory::Adapter);
378 }
379
380 #[test]
381 fn perfgate_error_from_config() {
382 let err: PerfgateError = ConfigValidationError::BenchName("test".to_string()).into();
383 assert!(matches!(
384 err,
385 PerfgateError::Config(ConfigValidationError::BenchName(_))
386 ));
387 assert_eq!(err.category(), ErrorCategory::Config);
388 }
389
390 #[test]
391 fn perfgate_error_from_io() {
392 let err: PerfgateError = IoError::BaselineResolve("test".to_string()).into();
393 assert!(matches!(
394 err,
395 PerfgateError::Io(IoError::BaselineResolve(_))
396 ));
397 assert_eq!(err.category(), ErrorCategory::Io);
398 }
399
400 #[test]
401 fn perfgate_error_from_paired() {
402 let err: PerfgateError = PairedError::NoSamples.into();
403 assert!(matches!(err, PerfgateError::Paired(PairedError::NoSamples)));
404 assert_eq!(err.category(), ErrorCategory::Paired);
405 }
406
407 #[test]
408 fn error_category_display() {
409 assert_eq!(ErrorCategory::Validation.to_string(), "validation");
410 assert_eq!(ErrorCategory::Stats.to_string(), "stats");
411 assert_eq!(ErrorCategory::Adapter.to_string(), "adapter");
412 assert_eq!(ErrorCategory::Config.to_string(), "config");
413 assert_eq!(ErrorCategory::Io.to_string(), "io");
414 assert_eq!(ErrorCategory::Paired.to_string(), "paired");
415 }
416
417 #[test]
418 fn is_recoverable_timeout() {
419 let err = PerfgateError::Adapter(AdapterError::Timeout);
420 assert!(err.is_recoverable());
421 }
422
423 #[test]
424 fn is_not_recoverable_validation() {
425 let err = PerfgateError::Validation(ValidationError::Empty);
426 assert!(!err.is_recoverable());
427 }
428
429 #[test]
430 fn is_not_recoverable_empty_argv() {
431 let err = PerfgateError::Adapter(AdapterError::EmptyArgv);
432 assert!(!err.is_recoverable());
433 }
434
435 #[test]
436 fn exit_code_always_positive() {
437 let errors: Vec<PerfgateError> = vec![
438 ValidationError::Empty.into(),
439 StatsError::NoSamples.into(),
440 AdapterError::Timeout.into(),
441 ConfigValidationError::BenchName("test".to_string()).into(),
442 IoError::Other("test".to_string()).into(),
443 PairedError::NoSamples.into(),
444 ];
445
446 for err in errors {
447 assert!(err.exit_code() > 0);
448 }
449 }
450
451 #[test]
452 fn from_std_io_error() {
453 let std_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
454 let err: PerfgateError = std_err.into();
455 assert!(matches!(err, PerfgateError::Io(IoError::Other(_))));
456 }
457
458 #[test]
459 fn result_type_alias() {
460 fn might_fail() -> Result<String> {
461 Err(PerfgateError::Validation(ValidationError::Empty))
462 }
463 let result = might_fail();
464 assert!(result.is_err());
465 }
466
467 #[test]
468 fn validate_bench_name_valid() {
469 assert!(validate_bench_name("my-bench").is_ok());
470 assert!(validate_bench_name("bench_a").is_ok());
471 assert!(validate_bench_name("path/to/bench").is_ok());
472 assert!(validate_bench_name("bench.v2").is_ok());
473 assert!(validate_bench_name("a").is_ok());
474 assert!(validate_bench_name("123").is_ok());
475 }
476
477 #[test]
478 fn validate_bench_name_invalid() {
479 assert!(validate_bench_name("bench|name").is_err());
480 assert!(validate_bench_name("").is_err());
481 assert!(validate_bench_name("bench name").is_err());
482 assert!(validate_bench_name("bench@name").is_err());
483 }
484
485 #[test]
486 fn validate_bench_name_path_traversal() {
487 assert!(validate_bench_name("../bench").is_err());
488 assert!(validate_bench_name("bench/../x").is_err());
489 assert!(validate_bench_name("./bench").is_err());
490 assert!(validate_bench_name("bench/.").is_err());
491 }
492
493 #[test]
494 fn validate_bench_name_empty_segments() {
495 assert!(validate_bench_name("/bench").is_err());
496 assert!(validate_bench_name("bench/").is_err());
497 assert!(validate_bench_name("bench//x").is_err());
498 assert!(validate_bench_name("/").is_err());
499 }
500
501 #[test]
502 fn validate_bench_name_length_cap() {
503 let name_64 = "a".repeat(BENCH_NAME_MAX_LEN);
504 assert!(validate_bench_name(&name_64).is_ok());
505
506 let name_65 = "a".repeat(BENCH_NAME_MAX_LEN + 1);
507 assert!(validate_bench_name(&name_65).is_err());
508 }
509
510 #[test]
511 fn validate_bench_name_case() {
512 assert!(validate_bench_name("MyBench").is_err());
513 assert!(validate_bench_name("BENCH").is_err());
514 assert!(validate_bench_name("benchA").is_err());
515 }
516
517 #[test]
518 fn validation_error_name_accessor() {
519 let err = ValidationError::TooLong {
520 name: "test".to_string(),
521 max_len: 64,
522 };
523 assert_eq!(err.name(), "test");
524
525 let err = ValidationError::Empty;
526 assert_eq!(err.name(), "");
527 }
528
529 #[test]
530 fn validation_error_empty_segment() {
531 let err = ValidationError::EmptySegment {
532 name: "bench//x".to_string(),
533 };
534 let msg = err.to_string();
535 assert!(msg.contains("empty path segment"));
536 assert!(msg.contains("bench//x"));
537 }
538
539 #[test]
540 fn stats_error_no_samples_display() {
541 let err = StatsError::NoSamples;
542 assert_eq!(err.to_string(), "no samples to summarize");
543 }
544
545 #[test]
546 fn paired_error_no_samples_display() {
547 let err = PairedError::NoSamples;
548 assert_eq!(err.to_string(), "no samples to summarize");
549 }
550
551 #[test]
552 fn io_error_other_display() {
553 let err = IoError::Other("disk full".to_string());
554 assert!(err.to_string().contains("disk full"));
555 }
556
557 #[test]
558 fn perfgate_error_transparent_display_forwards() {
559 let inner = ValidationError::InvalidCharacters {
560 name: "MY_BENCH".to_string(),
561 };
562 let outer: PerfgateError = inner.clone().into();
563 assert_eq!(outer.to_string(), inner.to_string());
564 }
565
566 #[test]
567 fn perfgate_error_transparent_display_stats() {
568 let inner = StatsError::NoSamples;
569 let outer: PerfgateError = inner.clone().into();
570 assert_eq!(outer.to_string(), inner.to_string());
571 }
572
573 #[test]
574 fn perfgate_error_transparent_display_io() {
575 let inner = IoError::BaselineResolve("baselines/bench.json".to_string());
576 let outer: PerfgateError = inner.clone().into();
577 assert_eq!(outer.to_string(), inner.to_string());
578 assert!(outer.to_string().contains("baselines/bench.json"));
579 }
580
581 #[test]
582 fn validation_display_contains_bench_name() {
583 let err = ValidationError::TooLong {
584 name: "my-long-bench-name".to_string(),
585 max_len: 64,
586 };
587 assert!(err.to_string().contains("my-long-bench-name"));
588 assert!(err.to_string().contains("64"));
589
590 let err = ValidationError::InvalidCharacters {
591 name: "BAD_NAME".to_string(),
592 };
593 assert!(err.to_string().contains("BAD_NAME"));
594
595 let err = ValidationError::PathTraversal {
596 name: "foo/../bar".to_string(),
597 segment: "..".to_string(),
598 };
599 assert!(err.to_string().contains("foo/../bar"));
600 assert!(err.to_string().contains(".."));
601 }
602
603 #[test]
604 fn io_error_contains_file_path() {
605 let err = IoError::BaselineResolve("baselines/perf.json not found".to_string());
606 assert!(err.to_string().contains("baselines/perf.json"));
607
608 let err = IoError::ArtifactWrite("artifacts/perfgate/run.json".to_string());
609 assert!(err.to_string().contains("artifacts/perfgate/run.json"));
610
611 let err = IoError::RunCommand("failed to spawn /usr/bin/echo".to_string());
612 assert!(err.to_string().contains("/usr/bin/echo"));
613 }
614
615 #[test]
616 fn exit_code_is_always_one() {
617 let errors: Vec<PerfgateError> = vec![
618 ValidationError::Empty.into(),
619 ValidationError::TooLong {
620 name: "x".into(),
621 max_len: 64,
622 }
623 .into(),
624 ValidationError::InvalidCharacters { name: "X".into() }.into(),
625 ValidationError::EmptySegment { name: "/x".into() }.into(),
626 ValidationError::PathTraversal {
627 name: "..".into(),
628 segment: "..".into(),
629 }
630 .into(),
631 StatsError::NoSamples.into(),
632 AdapterError::EmptyArgv.into(),
633 AdapterError::Timeout.into(),
634 AdapterError::TimeoutUnsupported.into(),
635 AdapterError::Other("err".into()).into(),
636 ConfigValidationError::BenchName("b".into()).into(),
637 ConfigValidationError::ConfigFile("c".into()).into(),
638 IoError::BaselineResolve("r".into()).into(),
639 IoError::ArtifactWrite("w".into()).into(),
640 IoError::RunCommand("r".into()).into(),
641 IoError::Other("o".into()).into(),
642 PairedError::NoSamples.into(),
643 ];
644 for err in &errors {
645 assert_eq!(err.exit_code(), 1, "exit_code for {:?}", err);
646 }
647 }
648
649 #[test]
650 fn error_category_as_str_matches_display() {
651 let categories = [
652 ErrorCategory::Validation,
653 ErrorCategory::Stats,
654 ErrorCategory::Adapter,
655 ErrorCategory::Config,
656 ErrorCategory::Io,
657 ErrorCategory::Paired,
658 ];
659 for cat in &categories {
660 assert_eq!(cat.as_str(), cat.to_string());
661 }
662 }
663
664 #[test]
665 fn from_std_io_error_to_io_error() {
666 let std_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
667 let err: IoError = std_err.into();
668 assert!(matches!(err, IoError::Other(_)));
669 assert!(err.to_string().contains("access denied"));
670 }
671
672 #[test]
673 fn is_recoverable_io_errors() {
674 let cases = vec![
675 PerfgateError::Io(IoError::BaselineResolve("x".into())),
676 PerfgateError::Io(IoError::ArtifactWrite("x".into())),
677 PerfgateError::Io(IoError::RunCommand("x".into())),
678 PerfgateError::Io(IoError::Other("x".into())),
679 ];
680 for err in cases {
681 assert!(
682 err.is_recoverable(),
683 "IO errors should be recoverable: {:?}",
684 err
685 );
686 }
687 }
688
689 #[test]
690 fn is_not_recoverable_config_errors() {
691 let err = PerfgateError::Config(ConfigValidationError::BenchName("x".into()));
692 assert!(!err.is_recoverable());
693 let err = PerfgateError::Config(ConfigValidationError::ConfigFile("x".into()));
694 assert!(!err.is_recoverable());
695 }
696
697 #[test]
698 fn is_recoverable_adapter_other() {
699 let err = PerfgateError::Adapter(AdapterError::Other("transient".into()));
700 assert!(err.is_recoverable());
701 }
702
703 #[test]
704 fn is_not_recoverable_timeout_unsupported() {
705 let err = PerfgateError::Adapter(AdapterError::TimeoutUnsupported);
706 assert!(!err.is_recoverable());
707 }
708
709 #[test]
710 fn is_not_recoverable_paired_no_samples() {
711 let err = PerfgateError::Paired(PairedError::NoSamples);
712 assert!(!err.is_recoverable());
713 }
714}
715
716#[cfg(test)]
717mod property_tests {
718 use super::*;
719 use proptest::prelude::*;
720
721 fn error_message_strategy() -> impl Strategy<Value = String> {
722 "[a-zA-Z0-9 ]{1,50}"
723 }
724
725 proptest! {
726 #[test]
727 fn prop_adapter_error_other_preserves_message(msg in error_message_strategy()) {
728 let err = AdapterError::Other(msg.clone());
729 let displayed = err.to_string();
730 prop_assert!(displayed.contains(&msg));
731 }
732
733 #[test]
734 fn prop_io_error_other_preserves_message(msg in error_message_strategy()) {
735 let err = IoError::Other(msg.clone());
736 let displayed = err.to_string();
737 prop_assert!(displayed.contains(&msg));
738 }
739
740 #[test]
741 fn prop_config_error_preserves_message(msg in error_message_strategy()) {
742 let err = ConfigValidationError::BenchName(msg.clone());
743 let displayed = err.to_string();
744 prop_assert!(displayed.contains(&msg));
745 }
746
747 #[test]
748 fn prop_error_category_consistent(
749 msg in error_message_strategy()
750 ) {
751 let errors: Vec<PerfgateError> = vec![
752 PerfgateError::Validation(ValidationError::Empty),
753 PerfgateError::Stats(StatsError::NoSamples),
754 PerfgateError::Adapter(AdapterError::Other(msg.clone())),
755 PerfgateError::Config(ConfigValidationError::ConfigFile(msg.clone())),
756 PerfgateError::Io(IoError::Other(msg)),
757 PerfgateError::Paired(PairedError::NoSamples),
758 ];
759
760 for err in errors {
761 let category = err.category();
762 let displayed = category.to_string();
763 prop_assert!(!displayed.is_empty());
764 prop_assert!(err.exit_code() > 0);
765 }
766 }
767
768 #[test]
769 fn prop_validate_bench_name_valid_chars(
770 name in "[a-z0-9_.\\-/]{1,64}"
771 ) {
772 let result = validate_bench_name(&name);
773 if !name.contains("..") && !name.contains("./") && !name.starts_with('/') && !name.ends_with('/') && !name.contains("//") {
774 let has_invalid = name.split('/').any(|s| s == "." || s == ".." || s.is_empty());
775 if !has_invalid {
776 prop_assert!(result.is_ok(), "name '{}' should be valid", name);
777 }
778 }
779 }
780 }
781}