1pub use perfgate_error::BENCH_NAME_MAX_LEN;
40
41pub use perfgate_error::BENCH_NAME_PATTERN;
56
57pub use perfgate_error::ValidationError;
83
84pub use perfgate_error::validate_bench_name;
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn valid_names_basic() {
142 assert!(validate_bench_name("my-bench").is_ok());
143 assert!(validate_bench_name("bench_a").is_ok());
144 assert!(validate_bench_name("path/to/bench").is_ok());
145 assert!(validate_bench_name("bench.v2").is_ok());
146 assert!(validate_bench_name("a").is_ok());
147 assert!(validate_bench_name("123").is_ok());
148 }
149
150 #[test]
151 fn valid_names_with_dots() {
152 assert!(validate_bench_name("bench.v1").is_ok());
153 assert!(validate_bench_name("v1.2.3").is_ok());
154 assert!(validate_bench_name("bench.test.final").is_ok());
155 }
156
157 #[test]
158 fn valid_names_with_hyphens() {
159 assert!(validate_bench_name("my-bench-name").is_ok());
160 assert!(validate_bench_name("bench-v1-final").is_ok());
161 }
162
163 #[test]
164 fn valid_names_with_underscores() {
165 assert!(validate_bench_name("bench_name").is_ok());
166 assert!(validate_bench_name("my_bench_v2").is_ok());
167 }
168
169 #[test]
170 fn valid_names_with_slashes() {
171 assert!(validate_bench_name("path/to/bench").is_ok());
172 assert!(validate_bench_name("a/b/c").is_ok());
173 assert!(validate_bench_name("category/subcategory/bench").is_ok());
174 }
175
176 #[test]
177 fn valid_names_mixed_chars() {
178 assert!(validate_bench_name("my_bench-v1.2").is_ok());
179 assert!(validate_bench_name("path/to-bench_v2").is_ok());
180 assert!(validate_bench_name("a1-b2_c3.d4/e5").is_ok());
181 }
182
183 #[test]
184 fn valid_names_single_char() {
185 assert!(validate_bench_name("a").is_ok());
186 assert!(validate_bench_name("z").is_ok());
187 assert!(validate_bench_name("0").is_ok());
188 assert!(validate_bench_name("9").is_ok());
189 }
190
191 #[test]
192 fn valid_names_all_digits() {
193 assert!(validate_bench_name("12345").is_ok());
194 assert!(validate_bench_name("0").is_ok());
195 }
196
197 #[test]
198 fn invalid_empty() {
199 assert!(matches!(
200 validate_bench_name(""),
201 Err(ValidationError::Empty)
202 ));
203 }
204
205 #[test]
206 fn invalid_uppercase() {
207 assert!(matches!(
208 validate_bench_name("MyBench"),
209 Err(ValidationError::InvalidCharacters { .. })
210 ));
211 assert!(matches!(
212 validate_bench_name("BENCH"),
213 Err(ValidationError::InvalidCharacters { .. })
214 ));
215 assert!(matches!(
216 validate_bench_name("benchA"),
217 Err(ValidationError::InvalidCharacters { .. })
218 ));
219 assert!(matches!(
220 validate_bench_name("Bench"),
221 Err(ValidationError::InvalidCharacters { .. })
222 ));
223 }
224
225 #[test]
226 fn invalid_special_characters() {
227 assert!(matches!(
228 validate_bench_name("bench|name"),
229 Err(ValidationError::InvalidCharacters { .. })
230 ));
231 assert!(matches!(
232 validate_bench_name("bench name"),
233 Err(ValidationError::InvalidCharacters { .. })
234 ));
235 assert!(matches!(
236 validate_bench_name("bench@name"),
237 Err(ValidationError::InvalidCharacters { .. })
238 ));
239 assert!(matches!(
240 validate_bench_name("bench#name"),
241 Err(ValidationError::InvalidCharacters { .. })
242 ));
243 assert!(matches!(
244 validate_bench_name("bench$name"),
245 Err(ValidationError::InvalidCharacters { .. })
246 ));
247 assert!(matches!(
248 validate_bench_name("bench%name"),
249 Err(ValidationError::InvalidCharacters { .. })
250 ));
251 assert!(matches!(
252 validate_bench_name("bench!name"),
253 Err(ValidationError::InvalidCharacters { .. })
254 ));
255 }
256
257 #[test]
258 fn invalid_path_traversal() {
259 assert!(matches!(
260 validate_bench_name("../bench"),
261 Err(ValidationError::PathTraversal { .. })
262 ));
263 assert!(matches!(
264 validate_bench_name("bench/../x"),
265 Err(ValidationError::PathTraversal { .. })
266 ));
267 assert!(matches!(
268 validate_bench_name("./bench"),
269 Err(ValidationError::PathTraversal { .. })
270 ));
271 assert!(matches!(
272 validate_bench_name("bench/."),
273 Err(ValidationError::PathTraversal { .. })
274 ));
275 assert!(matches!(
276 validate_bench_name(".."),
277 Err(ValidationError::PathTraversal { .. })
278 ));
279 assert!(matches!(
280 validate_bench_name("."),
281 Err(ValidationError::PathTraversal { .. })
282 ));
283 }
284
285 #[test]
286 fn invalid_empty_segments() {
287 assert!(matches!(
288 validate_bench_name("/bench"),
289 Err(ValidationError::EmptySegment { .. })
290 ));
291 assert!(matches!(
292 validate_bench_name("bench/"),
293 Err(ValidationError::EmptySegment { .. })
294 ));
295 assert!(matches!(
296 validate_bench_name("bench//x"),
297 Err(ValidationError::EmptySegment { .. })
298 ));
299 assert!(matches!(
300 validate_bench_name("/"),
301 Err(ValidationError::EmptySegment { .. })
302 ));
303 assert!(matches!(
304 validate_bench_name("a//b"),
305 Err(ValidationError::EmptySegment { .. })
306 ));
307 assert!(matches!(
308 validate_bench_name("//"),
309 Err(ValidationError::EmptySegment { .. })
310 ));
311 }
312
313 #[test]
314 fn invalid_too_long() {
315 let name_64 = "a".repeat(BENCH_NAME_MAX_LEN);
316 assert!(validate_bench_name(&name_64).is_ok());
317
318 let name_65 = "a".repeat(BENCH_NAME_MAX_LEN + 1);
319 let result = validate_bench_name(&name_65);
320 assert!(matches!(result, Err(ValidationError::TooLong { .. })));
321 if let Err(ValidationError::TooLong { max_len, .. }) = result {
322 assert_eq!(max_len, BENCH_NAME_MAX_LEN);
323 }
324 }
325
326 #[test]
327 fn error_name_accessor() {
328 let err = validate_bench_name("INVALID").unwrap_err();
329 assert_eq!(err.name(), "INVALID");
330
331 let err = validate_bench_name("").unwrap_err();
332 assert_eq!(err.name(), "");
333
334 let err = validate_bench_name(&"x".repeat(100)).unwrap_err();
335 assert!(err.name().starts_with('x'));
336 }
337
338 #[test]
339 fn error_display() {
340 let err = ValidationError::Empty;
341 assert!(err.to_string().contains("must not be empty"));
342
343 let err = ValidationError::TooLong {
344 name: "test".to_string(),
345 max_len: 64,
346 };
347 assert!(err.to_string().contains("exceeds maximum length"));
348
349 let err = ValidationError::InvalidCharacters {
350 name: "TEST".to_string(),
351 };
352 assert!(err.to_string().contains("invalid characters"));
353
354 let err = ValidationError::EmptySegment {
355 name: "/test".to_string(),
356 };
357 assert!(err.to_string().contains("empty path segment"));
358
359 let err = ValidationError::PathTraversal {
360 name: "../test".to_string(),
361 segment: "..".to_string(),
362 };
363 assert!(err.to_string().contains("path traversal"));
364 }
365
366 #[test]
369 fn boundary_exact_max_len() {
370 let name = "a".repeat(BENCH_NAME_MAX_LEN);
371 assert!(validate_bench_name(&name).is_ok());
372 }
373
374 #[test]
375 fn boundary_one_over_max_len() {
376 let name = "a".repeat(BENCH_NAME_MAX_LEN + 1);
377 assert!(matches!(
378 validate_bench_name(&name),
379 Err(ValidationError::TooLong { max_len, .. }) if max_len == BENCH_NAME_MAX_LEN
380 ));
381 }
382
383 #[test]
384 fn boundary_one_under_max_len() {
385 let name = "a".repeat(BENCH_NAME_MAX_LEN - 1);
386 assert!(validate_bench_name(&name).is_ok());
387 }
388
389 #[test]
390 fn boundary_single_char_all_valid() {
391 for c in b'a'..=b'z' {
392 assert!(validate_bench_name(&String::from(c as char)).is_ok());
393 }
394 for c in b'0'..=b'9' {
395 assert!(validate_bench_name(&String::from(c as char)).is_ok());
396 }
397 assert!(validate_bench_name("_").is_ok());
398 assert!(validate_bench_name("-").is_ok());
399 }
400
401 #[test]
402 fn boundary_single_dot_is_path_traversal() {
403 assert!(matches!(
404 validate_bench_name("."),
405 Err(ValidationError::PathTraversal { .. })
406 ));
407 }
408
409 #[test]
410 fn boundary_double_dot_is_path_traversal() {
411 assert!(matches!(
412 validate_bench_name(".."),
413 Err(ValidationError::PathTraversal { .. })
414 ));
415 }
416
417 #[test]
418 fn boundary_single_slash_is_empty_segment() {
419 assert!(matches!(
420 validate_bench_name("/"),
421 Err(ValidationError::EmptySegment { .. })
422 ));
423 }
424
425 #[test]
426 fn boundary_max_len_with_slashes() {
427 let segment = "ab";
429 let sep = "/";
430 let seg_with_sep = segment.len() + sep.len(); let count = BENCH_NAME_MAX_LEN / seg_with_sep; let remainder = BENCH_NAME_MAX_LEN - (count * seg_with_sep);
433 let mut name: String = (0..count).map(|_| format!("{segment}/")).collect();
434 name.push_str(&"a".repeat(remainder));
435 assert_eq!(name.len(), BENCH_NAME_MAX_LEN);
436 assert!(validate_bench_name(&name).is_ok());
437 }
438
439 #[test]
442 fn unicode_emoji_rejected() {
443 assert!(matches!(
444 validate_bench_name("bench-🚀"),
445 Err(ValidationError::InvalidCharacters { .. })
446 ));
447 assert!(matches!(
448 validate_bench_name("🔥"),
449 Err(ValidationError::InvalidCharacters { .. })
450 ));
451 assert!(matches!(
452 validate_bench_name("a😀b"),
453 Err(ValidationError::InvalidCharacters { .. })
454 ));
455 }
456
457 #[test]
458 fn unicode_cjk_rejected() {
459 assert!(matches!(
460 validate_bench_name("ベンチ"),
461 Err(ValidationError::InvalidCharacters { .. })
462 ));
463 assert!(matches!(
464 validate_bench_name("bench-测试"),
465 Err(ValidationError::InvalidCharacters { .. })
466 ));
467 assert!(matches!(
468 validate_bench_name("벤치마크"),
469 Err(ValidationError::InvalidCharacters { .. })
470 ));
471 }
472
473 #[test]
474 fn unicode_rtl_rejected() {
475 assert!(matches!(
476 validate_bench_name("مقعد"),
477 Err(ValidationError::InvalidCharacters { .. })
478 ));
479 assert!(matches!(
480 validate_bench_name("bench-בדיקה"),
481 Err(ValidationError::InvalidCharacters { .. })
482 ));
483 }
484
485 #[test]
486 fn unicode_accented_rejected() {
487 assert!(matches!(
488 validate_bench_name("café"),
489 Err(ValidationError::InvalidCharacters { .. })
490 ));
491 assert!(matches!(
492 validate_bench_name("naïve"),
493 Err(ValidationError::InvalidCharacters { .. })
494 ));
495 assert!(matches!(
496 validate_bench_name("über"),
497 Err(ValidationError::InvalidCharacters { .. })
498 ));
499 }
500
501 #[test]
502 fn unicode_zero_width_and_bom_rejected() {
503 assert!(matches!(
505 validate_bench_name("bench\u{200B}name"),
506 Err(ValidationError::InvalidCharacters { .. })
507 ));
508 assert!(matches!(
510 validate_bench_name("\u{FEFF}bench"),
511 Err(ValidationError::InvalidCharacters { .. })
512 ));
513 }
514
515 #[test]
516 fn unicode_multibyte_length_check() {
517 let name: String = "🔥".repeat(16);
519 assert_eq!(name.len(), 64);
520 assert!(matches!(
521 validate_bench_name(&name),
522 Err(ValidationError::InvalidCharacters { .. })
523 ));
524 }
525
526 #[test]
529 fn empty_string_returns_empty_error() {
530 assert!(matches!(
531 validate_bench_name(""),
532 Err(ValidationError::Empty)
533 ));
534 }
535
536 #[test]
537 fn whitespace_only_rejected() {
538 assert!(matches!(
539 validate_bench_name(" "),
540 Err(ValidationError::InvalidCharacters { .. })
541 ));
542 assert!(matches!(
543 validate_bench_name(" "),
544 Err(ValidationError::InvalidCharacters { .. })
545 ));
546 assert!(matches!(
547 validate_bench_name("\t"),
548 Err(ValidationError::InvalidCharacters { .. })
549 ));
550 assert!(matches!(
551 validate_bench_name("\n"),
552 Err(ValidationError::InvalidCharacters { .. })
553 ));
554 assert!(matches!(
555 validate_bench_name("\r\n"),
556 Err(ValidationError::InvalidCharacters { .. })
557 ));
558 }
559
560 #[test]
561 fn null_byte_rejected() {
562 assert!(matches!(
563 validate_bench_name("\0"),
564 Err(ValidationError::InvalidCharacters { .. })
565 ));
566 assert!(matches!(
567 validate_bench_name("bench\0name"),
568 Err(ValidationError::InvalidCharacters { .. })
569 ));
570 }
571
572 #[test]
575 fn hyphen_prefixed_names_are_valid() {
576 assert!(validate_bench_name("-1").is_ok());
578 assert!(validate_bench_name("-bench").is_ok());
579 assert!(validate_bench_name("--double").is_ok());
580 }
581
582 #[test]
583 fn control_characters_rejected() {
584 for c in 0x00u8..=0x1F {
585 let name = format!("bench{}name", c as char);
586 assert!(
587 validate_bench_name(&name).is_err(),
588 "control char 0x{c:02x} should be rejected"
589 );
590 }
591 assert!(matches!(
593 validate_bench_name("bench\x7Fname"),
594 Err(ValidationError::InvalidCharacters { .. })
595 ));
596 }
597
598 #[test]
599 fn backslash_rejected() {
600 assert!(matches!(
601 validate_bench_name("bench\\name"),
602 Err(ValidationError::InvalidCharacters { .. })
603 ));
604 assert!(matches!(
605 validate_bench_name("path\\to\\bench"),
606 Err(ValidationError::InvalidCharacters { .. })
607 ));
608 }
609
610 #[test]
611 fn path_traversal_in_middle_segment() {
612 assert!(matches!(
613 validate_bench_name("a/../b"),
614 Err(ValidationError::PathTraversal { .. })
615 ));
616 assert!(matches!(
617 validate_bench_name("a/./b"),
618 Err(ValidationError::PathTraversal { .. })
619 ));
620 assert!(matches!(
621 validate_bench_name("a/b/../c"),
622 Err(ValidationError::PathTraversal { .. })
623 ));
624 }
625
626 #[test]
627 fn triple_dot_segment_is_valid() {
628 assert!(validate_bench_name("...").is_ok());
630 assert!(validate_bench_name("a/.../b").is_ok());
631 }
632
633 #[test]
636 fn large_string_over_max_len() {
637 let name = "a".repeat(1000);
638 assert!(matches!(
639 validate_bench_name(&name),
640 Err(ValidationError::TooLong { .. })
641 ));
642 }
643
644 #[test]
645 fn large_string_way_over_max_len() {
646 let name = "b".repeat(100_000);
647 let result = validate_bench_name(&name);
648 assert!(
649 matches!(result, Err(ValidationError::TooLong { max_len, .. }) if max_len == BENCH_NAME_MAX_LEN)
650 );
651 }
652
653 #[test]
654 fn large_string_with_invalid_chars_over_max_len() {
655 let name = "X".repeat(BENCH_NAME_MAX_LEN + 1);
657 assert!(matches!(
658 validate_bench_name(&name),
659 Err(ValidationError::TooLong { .. })
660 ));
661 }
662
663 #[test]
664 fn large_number_of_segments() {
665 let segments: Vec<&str> = (0..32).map(|_| "a").collect();
667 let name = segments.join("/");
668 if name.len() <= BENCH_NAME_MAX_LEN {
669 assert!(validate_bench_name(&name).is_ok());
670 } else {
671 assert!(matches!(
672 validate_bench_name(&name),
673 Err(ValidationError::TooLong { .. })
674 ));
675 }
676 }
677
678 #[test]
679 fn large_segment_at_boundary() {
680 let name = "z".repeat(BENCH_NAME_MAX_LEN);
682 assert!(validate_bench_name(&name).is_ok());
683 }
684
685 #[test]
686 fn error_preserves_name_for_large_input() {
687 let name = "x".repeat(BENCH_NAME_MAX_LEN + 10);
688 if let Err(ValidationError::TooLong {
689 name: err_name,
690 max_len,
691 }) = validate_bench_name(&name)
692 {
693 assert_eq!(err_name, name);
694 assert_eq!(max_len, BENCH_NAME_MAX_LEN);
695 } else {
696 panic!("expected TooLong error");
697 }
698 }
699
700 #[test]
701 fn bench_name_max_len_constant_is_64() {
702 assert_eq!(BENCH_NAME_MAX_LEN, 64);
703 }
704
705 #[test]
706 fn bench_name_pattern_matches_expected() {
707 assert_eq!(BENCH_NAME_PATTERN, r"^[a-z0-9_.\-/]+$");
708 }
709}
710
711#[cfg(test)]
712mod property_tests {
713 use super::*;
714 use proptest::prelude::*;
715
716 prop_compose! {
717 fn valid_bench_char()(
718 c in any::<u8>()
719 .prop_map(|b| {
720 if b.is_ascii_lowercase() || b.is_ascii_digit() {
721 char::from(b)
722 } else {
723 ['_', '-'][(b as usize) % 2]
724 }
725 })
726 ) -> char {
727 c
728 }
729 }
730
731 prop_compose! {
732 fn valid_segment_char()(
733 c in any::<u8>()
734 .prop_map(|b| {
735 if b.is_ascii_lowercase() || b.is_ascii_digit() {
736 char::from(b)
737 } else {
738 ['_', '.', '-'][(b as usize) % 3]
739 }
740 })
741 ) -> char {
742 c
743 }
744 }
745
746 prop_compose! {
747 fn valid_segment()(s in proptest::collection::vec(valid_segment_char(), 1..10)) -> String {
748 let seg: String = s.into_iter().collect();
749 if seg == "." || seg == ".." {
750 "a".to_string()
751 } else {
752 seg
753 }
754 }
755 }
756
757 prop_compose! {
758 fn valid_bench_name()(
759 segments in proptest::collection::vec(valid_segment(), 1..5)
760 ) -> String {
761 segments.join("/")
762 }
763 }
764
765 fn is_invalid_chars_error(result: &std::result::Result<(), ValidationError>) -> bool {
766 matches!(result, Err(ValidationError::InvalidCharacters { .. }))
767 }
768
769 fn is_too_long_error(result: &std::result::Result<(), ValidationError>) -> bool {
770 matches!(result, Err(ValidationError::TooLong { .. }))
771 }
772
773 fn is_empty_error(result: &std::result::Result<(), ValidationError>) -> bool {
774 matches!(result, Err(ValidationError::Empty))
775 }
776
777 fn is_empty_segment_error(result: &std::result::Result<(), ValidationError>) -> bool {
778 matches!(result, Err(ValidationError::EmptySegment { .. }))
779 }
780
781 fn is_path_traversal_error(result: &std::result::Result<(), ValidationError>) -> bool {
782 matches!(result, Err(ValidationError::PathTraversal { .. }))
783 }
784
785 proptest! {
786 #[test]
787 fn valid_chars_produce_ok(name in valid_bench_name()) {
788 prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
789 prop_assert!(validate_bench_name(&name).is_ok());
790 }
791
792 #[test]
793 fn uppercase_always_fails(name in "[a-z0-9_\\-]{1,30}[A-Z][a-z0-9_\\-]{1,30}") {
794 prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
795 let result = validate_bench_name(&name);
796 prop_assert!(is_invalid_chars_error(&result),
797 "Expected InvalidCharacters error for name '{}' with uppercase, got {:?}", name, result);
798 }
799
800 #[test]
801 fn length_boundary(
802 len in BENCH_NAME_MAX_LEN.saturating_sub(1)..=BENCH_NAME_MAX_LEN.saturating_add(1)
803 ) {
804 let name: String = "a".repeat(len);
805 let result = validate_bench_name(&name);
806 if len <= BENCH_NAME_MAX_LEN && len > 0 {
807 prop_assert!(result.is_ok());
808 } else if len > BENCH_NAME_MAX_LEN {
809 prop_assert!(is_too_long_error(&result));
810 } else {
811 prop_assert!(is_empty_error(&result));
812 }
813 }
814
815 #[test]
816 fn empty_string_fails(name in "") {
817 let _ = name;
818 let result = validate_bench_name("");
819 prop_assert!(is_empty_error(&result));
820 }
821
822 #[test]
823 fn double_slash_fails(prefix in valid_segment(), suffix in valid_segment()) {
824 prop_assume!(prefix != "." && prefix != "..");
825 prop_assume!(suffix != "." && suffix != "..");
826 let name = format!("{prefix}//{suffix}");
827 let result = validate_bench_name(&name);
828 prop_assert!(is_empty_segment_error(&result));
829 }
830
831 #[test]
832 fn leading_slash_fails(name in valid_bench_name()) {
833 let name_with_leading = format!("/{name}");
834 let result = validate_bench_name(&name_with_leading);
835 prop_assert!(is_empty_segment_error(&result));
836 }
837
838 #[test]
839 fn trailing_slash_fails(name in valid_bench_name()) {
840 let name_with_trailing = format!("{name}/");
841 let result = validate_bench_name(&name_with_trailing);
842 prop_assert!(is_empty_segment_error(&result));
843 }
844
845 #[test]
846 fn dot_segment_fails(suffix in "[a-z0-9_-]+") {
847 let name = format!("./{suffix}");
848 prop_assume!(!suffix.is_empty());
849 let result = validate_bench_name(&name);
850 prop_assert!(is_path_traversal_error(&result));
851 }
852
853 #[test]
854 fn double_dot_segment_fails(suffix in "[a-z0-9_-]+") {
855 let name = format!("../{suffix}");
856 prop_assume!(!suffix.is_empty());
857 let result = validate_bench_name(&name);
858 prop_assert!(is_path_traversal_error(&result));
859 }
860
861 #[test]
862 fn valid_char_roundtrip(c in valid_bench_char()) {
863 let name: String = std::iter::repeat_n(c, 10).collect();
864 prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
865 prop_assert!(validate_bench_name(&name).is_ok());
866 }
867
868 #[test]
869 fn special_invalid_chars(c in any::<char>()) {
870 prop_assume!(!c.is_ascii_lowercase());
871 prop_assume!(!c.is_ascii_digit());
872 prop_assume!(c != '_');
873 prop_assume!(c != '.');
874 prop_assume!(c != '-');
875 prop_assume!(c != '/');
876 prop_assume!(c != '\0');
877
878 let name = format!("bench{}test", c);
879 let result = validate_bench_name(&name);
880 prop_assert!(is_invalid_chars_error(&result));
881 }
882 }
883}