1use serde::{Deserialize, Serialize};
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ShardConfig {
13 pub current: u32,
15 pub total: u32,
17}
18
19impl ShardConfig {
20 #[must_use]
26 pub fn new(current: u32, total: u32) -> Self {
27 assert!(total > 0, "Total shards must be greater than 0");
28 assert!(
29 current > 0,
30 "Current shard must be 1-based (greater than 0)"
31 );
32 assert!(
33 current <= total,
34 "Current shard ({current}) cannot exceed total ({total})"
35 );
36
37 Self { current, total }
38 }
39
40 pub fn parse(s: &str) -> Result<Self, ShardParseError> {
46 let parts: Vec<&str> = s.split('/').collect();
47 if parts.len() != 2 {
48 return Err(ShardParseError::InvalidFormat(s.to_string()));
49 }
50
51 let current = parts[0]
52 .parse::<u32>()
53 .map_err(|_| ShardParseError::InvalidNumber(parts[0].to_string()))?;
54 let total = parts[1]
55 .parse::<u32>()
56 .map_err(|_| ShardParseError::InvalidNumber(parts[1].to_string()))?;
57
58 if total == 0 {
59 return Err(ShardParseError::ZeroTotal);
60 }
61 if current == 0 {
62 return Err(ShardParseError::ZeroCurrent);
63 }
64 if current > total {
65 return Err(ShardParseError::CurrentExceedsTotal { current, total });
66 }
67
68 Ok(Self { current, total })
69 }
70
71 #[must_use]
75 pub fn should_run_index(&self, test_index: usize) -> bool {
76 (test_index % self.total as usize) + 1 == self.current as usize
77 }
78
79 #[must_use]
83 pub fn should_run_name(&self, test_name: &str) -> bool {
84 let hash = Self::hash_test_name(test_name);
85 (hash % self.total as u64) + 1 == self.current as u64
86 }
87
88 fn hash_test_name(name: &str) -> u64 {
90 let mut hasher = DefaultHasher::new();
91 name.hash(&mut hasher);
92 hasher.finish()
93 }
94
95 #[must_use]
97 pub fn filter_tests<'a>(&self, tests: &'a [&str]) -> Vec<&'a str> {
98 tests
99 .iter()
100 .filter(|name| self.should_run_name(name))
101 .copied()
102 .collect()
103 }
104
105 #[must_use]
107 pub fn filter_by_index<T: Clone>(&self, items: &[T]) -> Vec<T> {
108 items
109 .iter()
110 .enumerate()
111 .filter(|(idx, _)| self.should_run_index(*idx))
112 .map(|(_, item)| item.clone())
113 .collect()
114 }
115
116 #[must_use]
118 pub fn estimated_count(&self, total_tests: usize) -> usize {
119 let base = total_tests / self.total as usize;
120 let remainder = total_tests % self.total as usize;
121 if self.current as usize <= remainder {
122 base + 1
123 } else {
124 base
125 }
126 }
127
128 #[must_use]
130 pub fn validate_coverage(total_tests: usize, total_shards: u32) -> bool {
131 let mut covered = vec![false; total_tests];
132
133 for shard in 1..=total_shards {
134 let config = ShardConfig::new(shard, total_shards);
135 for (idx, is_covered) in covered.iter_mut().enumerate() {
136 if config.should_run_index(idx) {
137 if *is_covered {
138 return false; }
140 *is_covered = true;
141 }
142 }
143 }
144
145 covered.iter().all(|&c| c)
146 }
147}
148
149impl Default for ShardConfig {
150 fn default() -> Self {
151 Self {
152 current: 1,
153 total: 1,
154 }
155 }
156}
157
158impl std::fmt::Display for ShardConfig {
159 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 write!(f, "{}/{}", self.current, self.total)
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum ShardParseError {
167 InvalidFormat(String),
169 InvalidNumber(String),
171 ZeroTotal,
173 ZeroCurrent,
175 CurrentExceedsTotal {
177 current: u32,
179 total: u32,
181 },
182}
183
184impl std::fmt::Display for ShardParseError {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Self::InvalidFormat(s) => {
188 write!(
189 f,
190 "Invalid shard format '{s}', expected 'N/M' (e.g., '1/4')"
191 )
192 }
193 Self::InvalidNumber(s) => write!(f, "Invalid number in shard spec: '{s}'"),
194 Self::ZeroTotal => write!(f, "Total shards cannot be zero"),
195 Self::ZeroCurrent => write!(f, "Current shard must be 1-based (cannot be 0)"),
196 Self::CurrentExceedsTotal { current, total } => {
197 write!(f, "Current shard ({current}) exceeds total ({total})")
198 }
199 }
200 }
201}
202
203impl std::error::Error for ShardParseError {}
204
205#[derive(Debug, Clone)]
207pub struct ShardedRunner {
208 config: ShardConfig,
209 test_names: Vec<String>,
210}
211
212impl ShardedRunner {
213 #[must_use]
215 pub fn new(config: ShardConfig) -> Self {
216 Self {
217 config,
218 test_names: Vec::new(),
219 }
220 }
221
222 pub fn add_tests(&mut self, tests: impl IntoIterator<Item = impl Into<String>>) {
224 for test in tests {
225 self.test_names.push(test.into());
226 }
227 }
228
229 #[must_use]
231 pub fn assigned_tests(&self) -> Vec<&str> {
232 self.test_names
233 .iter()
234 .filter(|name| self.config.should_run_name(name))
235 .map(String::as_str)
236 .collect()
237 }
238
239 #[must_use]
241 pub fn config(&self) -> ShardConfig {
242 self.config
243 }
244
245 #[must_use]
247 pub fn total_tests(&self) -> usize {
248 self.test_names.len()
249 }
250
251 #[must_use]
253 pub fn assigned_count(&self) -> usize {
254 self.assigned_tests().len()
255 }
256}
257
258#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct ShardReport {
261 pub shard: Option<ShardConfig>,
263 pub tests_run: usize,
265 pub tests_passed: usize,
267 pub tests_failed: usize,
269 pub tests_skipped: usize,
271 pub duration_ms: u64,
273 pub failed_tests: Vec<String>,
275}
276
277impl ShardReport {
278 #[must_use]
280 pub fn new(shard: ShardConfig) -> Self {
281 Self {
282 shard: Some(shard),
283 ..Default::default()
284 }
285 }
286
287 #[must_use]
289 pub fn is_success(&self) -> bool {
290 self.tests_failed == 0
291 }
292
293 #[must_use]
295 pub fn merge(reports: &[ShardReport]) -> Self {
296 let mut merged = Self::default();
297
298 for report in reports {
299 merged.tests_run += report.tests_run;
300 merged.tests_passed += report.tests_passed;
301 merged.tests_failed += report.tests_failed;
302 merged.tests_skipped += report.tests_skipped;
303 merged.duration_ms = merged.duration_ms.max(report.duration_ms);
304 merged.failed_tests.extend(report.failed_tests.clone());
305 }
306
307 merged
308 }
309
310 #[must_use]
312 pub fn to_json(&self) -> String {
313 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
314 }
315}
316
317#[cfg(test)]
318#[allow(
319 clippy::unwrap_used,
320 clippy::expect_used,
321 clippy::needless_range_loop,
322 clippy::field_reassign_with_default
323)]
324mod tests {
325 use super::*;
326
327 #[test]
332 fn h0_shard_01_config_new() {
333 let config = ShardConfig::new(1, 4);
334 assert_eq!(config.current, 1);
335 assert_eq!(config.total, 4);
336 }
337
338 #[test]
339 fn h0_shard_02_config_display() {
340 let config = ShardConfig::new(2, 5);
341 assert_eq!(format!("{config}"), "2/5");
342 }
343
344 #[test]
345 #[should_panic(expected = "Total shards must be greater than 0")]
346 fn h0_shard_03_config_zero_total_panics() {
347 let _ = ShardConfig::new(1, 0);
348 }
349
350 #[test]
351 #[should_panic(expected = "Current shard must be 1-based")]
352 fn h0_shard_04_config_zero_current_panics() {
353 let _ = ShardConfig::new(0, 4);
354 }
355
356 #[test]
357 #[should_panic(expected = "cannot exceed total")]
358 fn h0_shard_05_config_current_exceeds_total_panics() {
359 let _ = ShardConfig::new(5, 4);
360 }
361
362 #[test]
367 fn h0_shard_06_parse_valid() {
368 let config = ShardConfig::parse("2/4").unwrap();
369 assert_eq!(config.current, 2);
370 assert_eq!(config.total, 4);
371 }
372
373 #[test]
374 fn h0_shard_07_parse_invalid_format() {
375 let err = ShardConfig::parse("2-4").unwrap_err();
376 assert!(matches!(err, ShardParseError::InvalidFormat(_)));
377 }
378
379 #[test]
380 fn h0_shard_08_parse_invalid_number() {
381 let err = ShardConfig::parse("abc/4").unwrap_err();
382 assert!(matches!(err, ShardParseError::InvalidNumber(_)));
383 }
384
385 #[test]
386 fn h0_shard_09_parse_zero_total() {
387 let err = ShardConfig::parse("1/0").unwrap_err();
388 assert!(matches!(err, ShardParseError::ZeroTotal));
389 }
390
391 #[test]
392 fn h0_shard_10_parse_zero_current() {
393 let err = ShardConfig::parse("0/4").unwrap_err();
394 assert!(matches!(err, ShardParseError::ZeroCurrent));
395 }
396
397 #[test]
398 fn h0_shard_11_parse_current_exceeds_total() {
399 let err = ShardConfig::parse("5/4").unwrap_err();
400 assert!(matches!(err, ShardParseError::CurrentExceedsTotal { .. }));
401 }
402
403 #[test]
408 fn h0_shard_12_should_run_index_shard1of4() {
409 let config = ShardConfig::new(1, 4);
410 assert!(config.should_run_index(0)); assert!(!config.should_run_index(1)); assert!(!config.should_run_index(2)); assert!(!config.should_run_index(3)); assert!(config.should_run_index(4)); }
416
417 #[test]
418 fn h0_shard_13_should_run_index_shard2of4() {
419 let config = ShardConfig::new(2, 4);
420 assert!(!config.should_run_index(0));
421 assert!(config.should_run_index(1));
422 assert!(!config.should_run_index(2));
423 assert!(!config.should_run_index(3));
424 assert!(!config.should_run_index(4));
425 assert!(config.should_run_index(5));
426 }
427
428 #[test]
429 fn h0_shard_14_all_shards_cover_all_tests() {
430 let mut covered = [false; 10];
432
433 for shard in 1..=4 {
434 let config = ShardConfig::new(shard, 4);
435 for idx in 0..10 {
436 if config.should_run_index(idx) {
437 assert!(!covered[idx], "Test {idx} covered twice");
438 covered[idx] = true;
439 }
440 }
441 }
442
443 assert!(covered.iter().all(|&c| c), "All tests must be covered");
444 }
445
446 #[test]
451 fn h0_shard_15_should_run_name_deterministic() {
452 let config = ShardConfig::new(1, 4);
453 let result1 = config.should_run_name("test_foo");
454 let result2 = config.should_run_name("test_foo");
455 assert_eq!(result1, result2, "Same name should give same result");
456 }
457
458 #[test]
459 fn h0_shard_16_filter_tests_by_name() {
460 let config = ShardConfig::new(1, 2);
461 let tests = vec!["test_a", "test_b", "test_c", "test_d"];
462 let filtered = config.filter_tests(&tests);
463
464 assert!(!filtered.is_empty());
466 assert!(filtered.len() <= tests.len());
467 }
468
469 #[test]
470 fn h0_shard_17_all_shards_cover_all_names() {
471 let tests = vec!["test_1", "test_2", "test_3", "test_4", "test_5"];
472 let mut covered = vec![false; tests.len()];
473
474 for shard in 1..=3 {
475 let config = ShardConfig::new(shard, 3);
476 for (idx, test) in tests.iter().enumerate() {
477 if config.should_run_name(test) {
478 covered[idx] = true;
479 }
480 }
481 }
482
483 assert!(covered.iter().all(|&c| c), "All tests must be covered");
484 }
485
486 #[test]
491 fn h0_shard_18_filter_by_index() {
492 let config = ShardConfig::new(1, 2);
493 let items = vec!["a", "b", "c", "d"];
494 let filtered = config.filter_by_index(&items);
495
496 assert_eq!(filtered, vec!["a", "c"]);
498 }
499
500 #[test]
501 fn h0_shard_19_filter_by_index_shard2() {
502 let config = ShardConfig::new(2, 2);
503 let items = vec!["a", "b", "c", "d"];
504 let filtered = config.filter_by_index(&items);
505
506 assert_eq!(filtered, vec!["b", "d"]);
508 }
509
510 #[test]
515 fn h0_shard_20_estimated_count_even() {
516 let config = ShardConfig::new(1, 4);
517 assert_eq!(config.estimated_count(100), 25);
518 }
519
520 #[test]
521 fn h0_shard_21_estimated_count_uneven() {
522 let config1 = ShardConfig::new(1, 3);
524 let config2 = ShardConfig::new(2, 3);
525 let config3 = ShardConfig::new(3, 3);
526
527 let total =
528 config1.estimated_count(10) + config2.estimated_count(10) + config3.estimated_count(10);
529 assert_eq!(total, 10);
530 }
531
532 #[test]
537 fn h0_shard_22_validate_coverage_success() {
538 assert!(ShardConfig::validate_coverage(100, 4));
539 assert!(ShardConfig::validate_coverage(10, 3));
540 assert!(ShardConfig::validate_coverage(7, 7));
541 }
542
543 #[test]
548 fn h0_shard_23_runner_new() {
549 let config = ShardConfig::new(1, 4);
550 let runner = ShardedRunner::new(config);
551 assert_eq!(runner.config(), config);
552 assert_eq!(runner.total_tests(), 0);
553 }
554
555 #[test]
556 fn h0_shard_24_runner_add_tests() {
557 let config = ShardConfig::new(1, 2);
558 let mut runner = ShardedRunner::new(config);
559 runner.add_tests(vec!["test_a", "test_b", "test_c"]);
560
561 assert_eq!(runner.total_tests(), 3);
562 assert!(runner.assigned_count() > 0);
563 }
564
565 #[test]
566 fn h0_shard_25_runner_assigned_tests() {
567 let config = ShardConfig::new(1, 2);
568 let mut runner = ShardedRunner::new(config);
569 runner.add_tests(vec!["test_a", "test_b", "test_c", "test_d"]);
570
571 let assigned = runner.assigned_tests();
572 assert!(!assigned.is_empty());
573 assert!(assigned.len() <= 4);
574 }
575
576 #[test]
581 fn h0_shard_26_report_new() {
582 let config = ShardConfig::new(1, 4);
583 let report = ShardReport::new(config);
584 assert_eq!(report.shard, Some(config));
585 assert_eq!(report.tests_run, 0);
586 }
587
588 #[test]
589 fn h0_shard_27_report_is_success() {
590 let mut report = ShardReport::default();
591 report.tests_passed = 10;
592 report.tests_failed = 0;
593 assert!(report.is_success());
594
595 report.tests_failed = 1;
596 assert!(!report.is_success());
597 }
598
599 #[test]
600 fn h0_shard_28_report_merge() {
601 let mut r1 = ShardReport::default();
602 r1.tests_run = 10;
603 r1.tests_passed = 9;
604 r1.tests_failed = 1;
605 r1.duration_ms = 1000;
606
607 let mut r2 = ShardReport::default();
608 r2.tests_run = 10;
609 r2.tests_passed = 10;
610 r2.tests_failed = 0;
611 r2.duration_ms = 500;
612
613 let merged = ShardReport::merge(&[r1, r2]);
614 assert_eq!(merged.tests_run, 20);
615 assert_eq!(merged.tests_passed, 19);
616 assert_eq!(merged.tests_failed, 1);
617 assert_eq!(merged.duration_ms, 1000); }
619
620 #[test]
621 fn h0_shard_29_report_to_json() {
622 let report = ShardReport::new(ShardConfig::new(1, 2));
623 let json = report.to_json();
624 assert!(json.contains("tests_run"));
625 assert!(json.contains("shard"));
626 }
627
628 #[test]
633 fn h0_shard_30_default_config() {
634 let config = ShardConfig::default();
635 assert_eq!(config.current, 1);
636 assert_eq!(config.total, 1);
637 assert!(config.should_run_index(0));
639 assert!(config.should_run_index(100));
640 }
641
642 #[test]
647 fn h0_shard_31_parse_invalid_total_number() {
648 let err = ShardConfig::parse("1/abc").unwrap_err();
650 assert!(matches!(err, ShardParseError::InvalidNumber(_)));
651 if let ShardParseError::InvalidNumber(s) = &err {
652 assert_eq!(s, "abc");
653 }
654 }
655
656 #[test]
657 fn h0_shard_32_parse_too_many_slashes() {
658 let err = ShardConfig::parse("1/2/3").unwrap_err();
659 assert!(matches!(err, ShardParseError::InvalidFormat(_)));
660 }
661
662 #[test]
663 fn h0_shard_33_parse_single_number() {
664 let err = ShardConfig::parse("5").unwrap_err();
665 assert!(matches!(err, ShardParseError::InvalidFormat(_)));
666 }
667
668 #[test]
669 fn h0_shard_34_error_display_invalid_format() {
670 let err = ShardParseError::InvalidFormat("bad".to_string());
671 let msg = format!("{err}");
672 assert!(msg.contains("Invalid shard format"));
673 assert!(msg.contains("bad"));
674 assert!(msg.contains("N/M"));
675 }
676
677 #[test]
678 fn h0_shard_35_error_display_invalid_number() {
679 let err = ShardParseError::InvalidNumber("xyz".to_string());
680 let msg = format!("{err}");
681 assert!(msg.contains("Invalid number"));
682 assert!(msg.contains("xyz"));
683 }
684
685 #[test]
686 fn h0_shard_36_error_display_zero_total() {
687 let err = ShardParseError::ZeroTotal;
688 let msg = format!("{err}");
689 assert!(msg.contains("Total shards cannot be zero"));
690 }
691
692 #[test]
693 fn h0_shard_37_error_display_zero_current() {
694 let err = ShardParseError::ZeroCurrent;
695 let msg = format!("{err}");
696 assert!(msg.contains("must be 1-based"));
697 }
698
699 #[test]
700 fn h0_shard_38_error_display_current_exceeds_total() {
701 let err = ShardParseError::CurrentExceedsTotal {
702 current: 10,
703 total: 5,
704 };
705 let msg = format!("{err}");
706 assert!(msg.contains("10"));
707 assert!(msg.contains('5'));
708 assert!(msg.contains("exceeds"));
709 }
710
711 #[test]
712 fn h0_shard_39_error_is_std_error() {
713 let err: Box<dyn std::error::Error> =
714 Box::new(ShardParseError::InvalidFormat("test".to_string()));
715 assert!(!err.to_string().is_empty());
717 }
718
719 #[test]
720 fn h0_shard_40_validate_coverage_with_zero_tests() {
721 assert!(ShardConfig::validate_coverage(0, 4));
723 }
724
725 #[test]
726 fn h0_shard_41_validate_coverage_single_shard() {
727 assert!(ShardConfig::validate_coverage(100, 1));
728 }
729
730 #[test]
731 fn h0_shard_42_report_merge_with_failed_tests() {
732 let mut r1 = ShardReport::default();
733 r1.tests_run = 5;
734 r1.tests_passed = 4;
735 r1.tests_failed = 1;
736 r1.failed_tests = vec!["test_a".to_string(), "test_b".to_string()];
737 r1.duration_ms = 100;
738
739 let mut r2 = ShardReport::default();
740 r2.tests_run = 5;
741 r2.tests_passed = 3;
742 r2.tests_failed = 2;
743 r2.failed_tests = vec!["test_c".to_string()];
744 r2.duration_ms = 200;
745
746 let merged = ShardReport::merge(&[r1, r2]);
747 assert_eq!(merged.tests_run, 10);
748 assert_eq!(merged.tests_passed, 7);
749 assert_eq!(merged.tests_failed, 3);
750 assert_eq!(merged.failed_tests.len(), 3);
751 assert!(merged.failed_tests.contains(&"test_a".to_string()));
752 assert!(merged.failed_tests.contains(&"test_b".to_string()));
753 assert!(merged.failed_tests.contains(&"test_c".to_string()));
754 assert_eq!(merged.duration_ms, 200); }
756
757 #[test]
758 fn h0_shard_43_report_merge_empty() {
759 let merged = ShardReport::merge(&[]);
760 assert_eq!(merged.tests_run, 0);
761 assert_eq!(merged.tests_passed, 0);
762 assert_eq!(merged.tests_failed, 0);
763 assert!(merged.is_success()); }
765
766 #[test]
767 fn h0_shard_44_report_with_skipped_tests() {
768 let mut report = ShardReport::default();
769 report.tests_run = 10;
770 report.tests_passed = 8;
771 report.tests_failed = 0;
772 report.tests_skipped = 2;
773
774 assert!(report.is_success());
775 assert_eq!(report.tests_skipped, 2);
776 }
777
778 #[test]
779 fn h0_shard_45_report_merge_skipped() {
780 let mut r1 = ShardReport::default();
781 r1.tests_skipped = 3;
782
783 let mut r2 = ShardReport::default();
784 r2.tests_skipped = 2;
785
786 let merged = ShardReport::merge(&[r1, r2]);
787 assert_eq!(merged.tests_skipped, 5);
788 }
789
790 #[test]
791 fn h0_shard_46_estimated_count_more_shards_than_tests() {
792 let config1 = ShardConfig::new(1, 10);
794 let config2 = ShardConfig::new(4, 10);
795 let config10 = ShardConfig::new(10, 10);
796
797 assert_eq!(config1.estimated_count(3), 1);
799 assert_eq!(config2.estimated_count(3), 0);
800 assert_eq!(config10.estimated_count(3), 0);
801 }
802
803 #[test]
804 fn h0_shard_47_filter_tests_empty_list() {
805 let config = ShardConfig::new(1, 2);
806 let tests: Vec<&str> = vec![];
807 let filtered = config.filter_tests(&tests);
808 assert!(filtered.is_empty());
809 }
810
811 #[test]
812 fn h0_shard_48_filter_by_index_empty() {
813 let config = ShardConfig::new(1, 2);
814 let items: Vec<i32> = vec![];
815 let filtered = config.filter_by_index(&items);
816 assert!(filtered.is_empty());
817 }
818
819 #[test]
820 fn h0_shard_49_runner_add_tests_from_strings() {
821 let config = ShardConfig::new(1, 2);
822 let mut runner = ShardedRunner::new(config);
823
824 runner.add_tests(vec!["test_1".to_string(), "test_2".to_string()]);
826 assert_eq!(runner.total_tests(), 2);
827 }
828
829 #[test]
830 fn h0_shard_50_runner_multiple_add_calls() {
831 let config = ShardConfig::new(1, 2);
832 let mut runner = ShardedRunner::new(config);
833
834 runner.add_tests(vec!["test_a", "test_b"]);
835 runner.add_tests(vec!["test_c"]);
836
837 assert_eq!(runner.total_tests(), 3);
838 }
839
840 #[test]
841 fn h0_shard_51_config_clone_and_eq() {
842 let config1 = ShardConfig::new(2, 4);
843 let config2 = config1;
844 assert_eq!(config1, config2);
845
846 let config3 = ShardConfig::new(3, 4);
847 assert_ne!(config1, config3);
848 }
849
850 #[test]
851 fn h0_shard_52_hash_test_name_consistency() {
852 let config = ShardConfig::new(1, 100);
854
855 let mut results = Vec::new();
856 for _ in 0..10 {
857 results.push(config.should_run_name("consistent_test_name"));
858 }
859
860 assert!(results.iter().all(|&r| r == results[0]));
862 }
863
864 #[test]
865 fn h0_shard_53_different_names_different_distribution() {
866 let tests = vec![
868 "test_alpha",
869 "test_beta",
870 "test_gamma",
871 "test_delta",
872 "test_epsilon",
873 "test_zeta",
874 "test_eta",
875 "test_theta",
876 ];
877
878 let mut shard_counts = [0usize; 4];
880 for test in &tests {
881 for shard in 1..=4 {
882 let config = ShardConfig::new(shard, 4);
883 if config.should_run_name(test) {
884 shard_counts[shard as usize - 1] += 1;
885 break;
886 }
887 }
888 }
889
890 let total: usize = shard_counts.iter().sum();
892 assert_eq!(total, tests.len());
893 }
894
895 #[test]
896 fn h0_shard_54_report_json_contains_all_fields() {
897 let config = ShardConfig::new(1, 2);
898 let mut report = ShardReport::new(config);
899 report.tests_run = 10;
900 report.tests_passed = 8;
901 report.tests_failed = 2;
902 report.tests_skipped = 0;
903 report.duration_ms = 1234;
904 report.failed_tests = vec!["fail_1".to_string(), "fail_2".to_string()];
905
906 let json = report.to_json();
907 assert!(json.contains("\"tests_run\": 10"));
908 assert!(json.contains("\"tests_passed\": 8"));
909 assert!(json.contains("\"tests_failed\": 2"));
910 assert!(json.contains("\"tests_skipped\": 0"));
911 assert!(json.contains("\"duration_ms\": 1234"));
912 assert!(json.contains("fail_1"));
913 assert!(json.contains("fail_2"));
914 }
915
916 #[test]
917 fn h0_shard_55_validate_coverage_large_test_count() {
918 assert!(ShardConfig::validate_coverage(1000, 16));
920 assert!(ShardConfig::validate_coverage(999, 16));
921 }
922
923 #[test]
924 fn h0_shard_56_shard_config_debug() {
925 let config = ShardConfig::new(3, 7);
926 let debug = format!("{config:?}");
927 assert!(debug.contains("ShardConfig"));
928 assert!(debug.contains("current: 3"));
929 assert!(debug.contains("total: 7"));
930 }
931
932 #[test]
933 fn h0_shard_57_shard_parse_error_clone() {
934 let err1 = ShardParseError::InvalidFormat("test".to_string());
935 let err2 = err1.clone();
936 assert_eq!(err1, err2);
937 }
938
939 #[test]
940 fn h0_shard_58_sharded_runner_debug() {
941 let config = ShardConfig::new(1, 2);
942 let runner = ShardedRunner::new(config);
943 let debug = format!("{runner:?}");
944 assert!(debug.contains("ShardedRunner"));
945 }
946
947 #[test]
948 fn h0_shard_59_sharded_runner_clone() {
949 let config = ShardConfig::new(1, 2);
950 let mut runner = ShardedRunner::new(config);
951 runner.add_tests(vec!["test_a", "test_b"]);
952
953 let cloned = runner.clone();
954 assert_eq!(runner.total_tests(), cloned.total_tests());
955 assert_eq!(runner.config(), cloned.config());
956 }
957
958 #[test]
959 fn h0_shard_60_shard_report_clone() {
960 let config = ShardConfig::new(1, 2);
961 let mut report = ShardReport::new(config);
962 report.tests_run = 5;
963 report.failed_tests = vec!["fail".to_string()];
964
965 let cloned = report.clone();
966 assert_eq!(report.tests_run, cloned.tests_run);
967 assert_eq!(report.failed_tests, cloned.failed_tests);
968 }
969}