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