cli_testing_specialist/utils/
parallel.rs

1//! Parallel processing strategy selection module
2//!
3//! This module provides intelligent parallel processing strategy selection
4//! based on workload size and characteristics.
5
6use crate::types::TestCategory;
7
8/// Parallel processing strategy
9///
10/// Determines the level of parallelism for test generation:
11/// - Sequential: Single-threaded execution (small workloads)
12/// - CategoryLevel: Parallel execution per test category (medium workloads)
13/// - TestLevel: Maximum parallelism within categories (large workloads)
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ParallelStrategy {
16    /// Single-threaded execution
17    ///
18    /// Best for:
19    /// - Small workloads (1-2 categories, <20 total tests)
20    /// - Single category with few options
21    /// - Avoiding thread overhead
22    Sequential,
23
24    /// Parallel execution per test category
25    ///
26    /// Best for:
27    /// - Medium workloads (3-5 categories, 20-100 tests)
28    /// - Multiple independent categories
29    /// - Balanced CPU usage
30    CategoryLevel,
31
32    /// Maximum parallelism (both category and test level)
33    ///
34    /// Best for:
35    /// - Large workloads (6+ categories, 100+ tests)
36    /// - Complex CLI tools (kubectl, docker, git)
37    /// - High-end systems with many CPU cores
38    TestLevel,
39}
40
41/// Workload characteristics for strategy selection
42#[derive(Debug, Clone)]
43pub struct Workload {
44    /// Number of test categories to generate
45    pub num_categories: usize,
46
47    /// Estimated number of tests per category (average)
48    pub estimated_tests_per_category: usize,
49
50    /// Number of available CPU cores
51    pub num_cpus: usize,
52}
53
54impl Workload {
55    /// Create a new workload descriptor
56    pub fn new(
57        categories: &[TestCategory],
58        num_global_options: usize,
59        num_subcommands: usize,
60    ) -> Self {
61        let num_categories = categories.len();
62
63        // Estimate tests per category based on CLI complexity
64        // Basic formula: (global_options + subcommands) * category_multiplier
65        let estimated_tests_per_category =
66            estimate_tests_per_category(num_global_options, num_subcommands, num_categories);
67
68        let num_cpus = num_cpus::get();
69
70        Self {
71            num_categories,
72            estimated_tests_per_category,
73            num_cpus,
74        }
75    }
76
77    /// Calculate total estimated tests
78    pub fn total_estimated_tests(&self) -> usize {
79        self.num_categories * self.estimated_tests_per_category
80    }
81}
82
83/// Estimate tests per category based on CLI complexity
84fn estimate_tests_per_category(
85    num_global_options: usize,
86    num_subcommands: usize,
87    num_categories: usize,
88) -> usize {
89    if num_categories == 0 {
90        return 0;
91    }
92
93    // Different categories have different test multipliers
94    // Basic: ~1-2 tests per option
95    // Security: ~2-3 tests per option
96    // Path: ~1 test per path option
97    // Average: ~2 tests per option/subcommand
98
99    let complexity_score = num_global_options + num_subcommands;
100    let avg_tests_per_category = complexity_score.max(1) * 2 / num_categories.max(1);
101
102    // Clamp to reasonable range
103    avg_tests_per_category.clamp(5, 50)
104}
105
106/// Choose optimal parallel processing strategy
107///
108/// Decision algorithm:
109/// 1. Sequential: total_tests < 20 OR num_categories <= 1
110/// 2. CategoryLevel: total_tests < 100 OR num_cpus < 4
111/// 3. TestLevel: total_tests >= 100 AND num_cpus >= 4
112///
113/// # Examples
114///
115/// ```
116/// use cli_testing_specialist::utils::parallel::{choose_strategy, Workload, ParallelStrategy};
117/// use cli_testing_specialist::types::TestCategory;
118///
119/// let categories = vec![TestCategory::Basic, TestCategory::Security];
120/// let workload = Workload::new(&categories, 10, 5);
121/// let strategy = choose_strategy(&workload);
122///
123/// // Small workload -> Sequential or CategoryLevel
124/// assert!(matches!(strategy, ParallelStrategy::Sequential | ParallelStrategy::CategoryLevel));
125/// ```
126pub fn choose_strategy(workload: &Workload) -> ParallelStrategy {
127    let total_tests = workload.total_estimated_tests();
128
129    // Strategy 1: Sequential (small workloads)
130    if total_tests < 20 || workload.num_categories <= 1 {
131        log::debug!(
132            "Choosing Sequential strategy (total_tests={}, num_categories={})",
133            total_tests,
134            workload.num_categories
135        );
136        return ParallelStrategy::Sequential;
137    }
138
139    // Strategy 2: CategoryLevel (medium workloads or low CPU)
140    if total_tests < 100 || workload.num_cpus < 4 {
141        log::debug!(
142            "Choosing CategoryLevel strategy (total_tests={}, num_cpus={})",
143            total_tests,
144            workload.num_cpus
145        );
146        return ParallelStrategy::CategoryLevel;
147    }
148
149    // Strategy 3: TestLevel (large workloads and high CPU)
150    log::debug!(
151        "Choosing TestLevel strategy (total_tests={}, num_cpus={})",
152        total_tests,
153        workload.num_cpus
154    );
155    ParallelStrategy::TestLevel
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    fn create_test_workload(
163        num_categories: usize,
164        num_global_options: usize,
165        num_subcommands: usize,
166    ) -> Workload {
167        let categories: Vec<TestCategory> = (0..num_categories)
168            .map(|i| match i % 3 {
169                0 => TestCategory::Basic,
170                1 => TestCategory::Security,
171                _ => TestCategory::Help,
172            })
173            .collect();
174
175        Workload::new(&categories, num_global_options, num_subcommands)
176    }
177
178    #[test]
179    fn test_choose_strategy_sequential_small_workload() {
180        // 1 category, 5 options -> ~10 tests
181        let workload = create_test_workload(1, 5, 0);
182        assert_eq!(choose_strategy(&workload), ParallelStrategy::Sequential);
183    }
184
185    #[test]
186    fn test_choose_strategy_sequential_single_category() {
187        // 1 category, even with many options -> Sequential
188        let workload = create_test_workload(1, 50, 10);
189        assert_eq!(choose_strategy(&workload), ParallelStrategy::Sequential);
190    }
191
192    #[test]
193    fn test_choose_strategy_category_level_medium_workload() {
194        // 3 categories, 10 options -> ~60 tests
195        let workload = create_test_workload(3, 10, 5);
196        assert_eq!(choose_strategy(&workload), ParallelStrategy::CategoryLevel);
197    }
198
199    #[test]
200    fn test_choose_strategy_test_level_large_workload() {
201        // 6 categories, 30 options, 50 subcommands -> ~160 tests
202        let workload = create_test_workload(6, 30, 50);
203        let total_tests = workload.total_estimated_tests();
204
205        // Verify workload is large enough
206        assert!(
207            total_tests >= 100,
208            "Expected large workload (>=100 tests), got {}",
209            total_tests
210        );
211
212        let strategy = choose_strategy(&workload);
213
214        // Result depends on CPU count
215        if workload.num_cpus >= 4 {
216            assert_eq!(
217                strategy,
218                ParallelStrategy::TestLevel,
219                "Expected TestLevel with {} CPUs and {} tests",
220                workload.num_cpus,
221                total_tests
222            );
223        } else {
224            assert_eq!(
225                strategy,
226                ParallelStrategy::CategoryLevel,
227                "Expected CategoryLevel with {} CPUs and {} tests",
228                workload.num_cpus,
229                total_tests
230            );
231        }
232    }
233
234    #[test]
235    fn test_estimate_tests_per_category() {
236        // 10 options, 5 subcommands, 3 categories
237        // complexity_score = 15
238        // avg = 15 * 2 / 3 = 10
239        let result = estimate_tests_per_category(10, 5, 3);
240        assert_eq!(result, 10);
241    }
242
243    #[test]
244    fn test_estimate_tests_per_category_clamping() {
245        // Very small complexity -> clamp to 5
246        let result = estimate_tests_per_category(1, 0, 5);
247        assert_eq!(result, 5);
248
249        // Very large complexity -> clamp to 50
250        let result = estimate_tests_per_category(100, 50, 1);
251        assert_eq!(result, 50);
252    }
253
254    #[test]
255    fn test_workload_total_estimated_tests() {
256        let workload = create_test_workload(4, 10, 5);
257        let total = workload.total_estimated_tests();
258
259        // 4 categories * estimated_tests_per_category
260        assert!(total > 0);
261        assert!(total <= 200); // Sanity check
262    }
263}