Skip to main content

jugar_probar/
shard.rs

1//! Test Sharding for Distributed Execution (Feature G.5)
2//!
3//! Enables parallel test execution across multiple machines in CI/CD pipelines.
4//! Implements deterministic test distribution for reproducible sharding.
5
6use serde::{Deserialize, Serialize};
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9
10/// Shard configuration for distributed test execution
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ShardConfig {
13    /// Current shard index (1-based)
14    pub current: u32,
15    /// Total number of shards
16    pub total: u32,
17}
18
19impl ShardConfig {
20    /// Create a new shard configuration
21    ///
22    /// # Panics
23    ///
24    /// Panics if `current` is 0, greater than `total`, or `total` is 0.
25    #[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    /// Parse shard config from CLI string format "N/M"
41    ///
42    /// # Errors
43    ///
44    /// Returns error if format is invalid
45    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    /// Check if a test at given index should run on this shard
72    ///
73    /// Uses modulo distribution for even test distribution.
74    #[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    /// Check if a test with given name should run on this shard
80    ///
81    /// Uses deterministic hash-based distribution for stable assignment.
82    #[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    /// Compute deterministic hash for test name
89    fn hash_test_name(name: &str) -> u64 {
90        let mut hasher = DefaultHasher::new();
91        name.hash(&mut hasher);
92        hasher.finish()
93    }
94
95    /// Filter a list of tests to only those that should run on this shard
96    #[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    /// Filter tests by index
106    #[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    /// Get estimated test count for this shard
117    #[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    /// Validate that all shards together cover all tests exactly once
129    #[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; // Double coverage
139                    }
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/// Errors that can occur when parsing shard configuration
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum ShardParseError {
167    /// Invalid format (expected "N/M")
168    InvalidFormat(String),
169    /// Invalid number in shard spec
170    InvalidNumber(String),
171    /// Total shards cannot be zero
172    ZeroTotal,
173    /// Current shard cannot be zero (1-based)
174    ZeroCurrent,
175    /// Current shard exceeds total
176    CurrentExceedsTotal {
177        /// Current shard number
178        current: u32,
179        /// Total shard count
180        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/// Sharded test runner
206#[derive(Debug, Clone)]
207pub struct ShardedRunner {
208    config: ShardConfig,
209    test_names: Vec<String>,
210}
211
212impl ShardedRunner {
213    /// Create a new sharded runner
214    #[must_use]
215    pub fn new(config: ShardConfig) -> Self {
216        Self {
217            config,
218            test_names: Vec::new(),
219        }
220    }
221
222    /// Add tests to the runner
223    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    /// Get tests assigned to this shard
230    #[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    /// Get shard configuration
240    #[must_use]
241    pub fn config(&self) -> ShardConfig {
242        self.config
243    }
244
245    /// Get total test count
246    #[must_use]
247    pub fn total_tests(&self) -> usize {
248        self.test_names.len()
249    }
250
251    /// Get assigned test count
252    #[must_use]
253    pub fn assigned_count(&self) -> usize {
254        self.assigned_tests().len()
255    }
256}
257
258/// Report for merged shard results
259#[derive(Debug, Clone, Default, Serialize, Deserialize)]
260pub struct ShardReport {
261    /// Shard configuration used
262    pub shard: Option<ShardConfig>,
263    /// Number of tests run
264    pub tests_run: usize,
265    /// Number of tests passed
266    pub tests_passed: usize,
267    /// Number of tests failed
268    pub tests_failed: usize,
269    /// Number of tests skipped
270    pub tests_skipped: usize,
271    /// Duration in milliseconds
272    pub duration_ms: u64,
273    /// Failed test names
274    pub failed_tests: Vec<String>,
275}
276
277impl ShardReport {
278    /// Create a new empty report
279    #[must_use]
280    pub fn new(shard: ShardConfig) -> Self {
281        Self {
282            shard: Some(shard),
283            ..Default::default()
284        }
285    }
286
287    /// Check if all tests passed
288    #[must_use]
289    pub fn is_success(&self) -> bool {
290        self.tests_failed == 0
291    }
292
293    /// Merge multiple shard reports
294    #[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    /// Export to JSON
311    #[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    // =========================================================================
328    // H₀-SHARD-01: ShardConfig creation
329    // =========================================================================
330
331    #[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    // =========================================================================
363    // H₀-SHARD-06: Parse from string
364    // =========================================================================
365
366    #[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    // =========================================================================
404    // H₀-SHARD-12: Test distribution by index
405    // =========================================================================
406
407    #[test]
408    fn h0_shard_12_should_run_index_shard1of4() {
409        let config = ShardConfig::new(1, 4);
410        assert!(config.should_run_index(0)); // 0 % 4 = 0, +1 = 1
411        assert!(!config.should_run_index(1)); // 1 % 4 = 1, +1 = 2
412        assert!(!config.should_run_index(2)); // 2 % 4 = 2, +1 = 3
413        assert!(!config.should_run_index(3)); // 3 % 4 = 3, +1 = 4
414        assert!(config.should_run_index(4)); // 4 % 4 = 0, +1 = 1
415    }
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        // 10 tests distributed across 4 shards
431        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    // =========================================================================
447    // H₀-SHARD-15: Test distribution by name (hash-based)
448    // =========================================================================
449
450    #[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        // Should get roughly half the tests
465        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    // =========================================================================
487    // H₀-SHARD-18: Filter by index
488    // =========================================================================
489
490    #[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        // Shard 1 of 2 gets indices 0, 2
497        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        // Shard 2 of 2 gets indices 1, 3
507        assert_eq!(filtered, vec!["b", "d"]);
508    }
509
510    // =========================================================================
511    // H₀-SHARD-20: Estimated count
512    // =========================================================================
513
514    #[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        // 10 tests / 3 shards = 3 each + 1 remainder
523        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    // =========================================================================
533    // H₀-SHARD-22: Validate coverage
534    // =========================================================================
535
536    #[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    // =========================================================================
544    // H₀-SHARD-23: ShardedRunner
545    // =========================================================================
546
547    #[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    // =========================================================================
577    // H₀-SHARD-26: ShardReport
578    // =========================================================================
579
580    #[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); // Max duration
618    }
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    // =========================================================================
629    // H₀-SHARD-30: Default config
630    // =========================================================================
631
632    #[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        // Default should run all tests
638        assert!(config.should_run_index(0));
639        assert!(config.should_run_index(100));
640    }
641
642    // =========================================================================
643    // Additional coverage tests for edge cases
644    // =========================================================================
645
646    #[test]
647    fn h0_shard_31_parse_invalid_total_number() {
648        // Test parsing with invalid second number (total)
649        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        // Verify it implements std::error::Error
716        assert!(!err.to_string().is_empty());
717    }
718
719    #[test]
720    fn h0_shard_40_validate_coverage_with_zero_tests() {
721        // Edge case: zero tests should be valid (trivially covered)
722        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); // max of 100 and 200
755    }
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()); // No failures = success
764    }
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        // 3 tests across 10 shards
793        let config1 = ShardConfig::new(1, 10);
794        let config2 = ShardConfig::new(4, 10);
795        let config10 = ShardConfig::new(10, 10);
796
797        // Shards 1-3 get 1 test each, shards 4-10 get 0
798        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        // Test with owned Strings
825        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        // Verify same name always produces same hash distribution
853        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        // All results should be identical
861        assert!(results.iter().all(|&r| r == results[0]));
862    }
863
864    #[test]
865    fn h0_shard_53_different_names_different_distribution() {
866        // Different names should (eventually) go to different shards
867        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        // With 4 shards, different tests should distribute
879        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        // Each test goes to exactly one shard
891        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        // Verify coverage validation with larger numbers
919        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}