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