Skip to main content

pipeline_service/execution/
matrix.rs

1// Matrix Strategy Expansion
2// Expands matrix strategies into concrete job instances
3
4use crate::parser::models::{MatrixStrategy, Strategy, Value};
5
6use std::collections::HashMap;
7
8/// A single matrix instance (one combination of matrix values)
9#[derive(Debug, Clone)]
10pub struct MatrixInstance {
11    /// Name of this instance (combination of values)
12    pub name: String,
13    /// Variable values for this instance
14    pub variables: HashMap<String, Value>,
15}
16
17/// Matrix expander for job strategies
18pub struct MatrixExpander;
19
20impl MatrixExpander {
21    /// Expand a strategy into matrix instances
22    pub fn expand(strategy: &Strategy) -> Vec<MatrixInstance> {
23        let mut instances = Vec::new();
24
25        // Handle matrix strategy
26        if let Some(matrix) = &strategy.matrix {
27            instances = Self::expand_matrix(matrix);
28        }
29
30        // Handle parallel strategy (creates N identical instances)
31        if let Some(parallel) = strategy.parallel {
32            if instances.is_empty() {
33                // No matrix, just parallel copies
34                instances = Self::expand_parallel(parallel);
35            }
36            // If we have both matrix and parallel, the matrix takes precedence
37            // and parallel just limits concurrency (handled by executor)
38        }
39
40        // Apply maxParallel limit info (stored for executor to use)
41        // The actual limiting happens during execution
42
43        instances
44    }
45
46    /// Expand an inline matrix into instances
47    fn expand_matrix(matrix: &MatrixStrategy) -> Vec<MatrixInstance> {
48        match matrix {
49            MatrixStrategy::Inline(config) => {
50                // Each top-level key is an instance name, values are the variables
51                config
52                    .iter()
53                    .map(|(name, vars)| MatrixInstance {
54                        name: name.clone(),
55                        variables: vars
56                            .iter()
57                            .map(|(k, v)| (k.clone(), Self::yaml_to_value(v)))
58                            .collect(),
59                    })
60                    .collect()
61            }
62            MatrixStrategy::Expression(_expr) => {
63                // Expression-based matrices need to be evaluated at runtime
64                // For now, return empty and let the executor handle it
65                Vec::new()
66            }
67        }
68    }
69
70    /// Expand a parallel count into instances
71    fn expand_parallel(count: u32) -> Vec<MatrixInstance> {
72        (0..count)
73            .map(|i| {
74                let mut variables = HashMap::new();
75                variables.insert(
76                    "System.JobPositionInPhase".to_string(),
77                    Value::Number((i + 1) as f64),
78                );
79                variables.insert(
80                    "System.TotalJobsInPhase".to_string(),
81                    Value::Number(count as f64),
82                );
83                MatrixInstance {
84                    name: format!("Job {}", i + 1),
85                    variables,
86                }
87            })
88            .collect()
89    }
90
91    /// Convert serde_yaml::Value to our Value type
92    fn yaml_to_value(yaml: &serde_yaml::Value) -> Value {
93        match yaml {
94            serde_yaml::Value::Null => Value::Null,
95            serde_yaml::Value::Bool(b) => Value::Bool(*b),
96            serde_yaml::Value::Number(n) => {
97                Value::Number(n.as_f64().unwrap_or(n.as_i64().unwrap_or(0) as f64))
98            }
99            serde_yaml::Value::String(s) => Value::String(s.clone()),
100            serde_yaml::Value::Sequence(seq) => {
101                Value::Array(seq.iter().map(Self::yaml_to_value).collect())
102            }
103            serde_yaml::Value::Mapping(map) => Value::Object(
104                map.iter()
105                    .filter_map(|(k, v)| {
106                        k.as_str()
107                            .map(|key| (key.to_string(), Self::yaml_to_value(v)))
108                    })
109                    .collect(),
110            ),
111            serde_yaml::Value::Tagged(_) => Value::Null, // Not supported
112        }
113    }
114
115    /// Get the maximum parallel limit from a strategy
116    pub fn max_parallel(strategy: &Strategy) -> Option<u32> {
117        strategy.max_parallel
118    }
119
120    /// Check if a strategy has matrix expansion
121    pub fn has_matrix(strategy: &Strategy) -> bool {
122        strategy.matrix.is_some()
123    }
124
125    /// Check if a strategy has parallel expansion
126    pub fn has_parallel(strategy: &Strategy) -> bool {
127        strategy.parallel.is_some()
128    }
129}
130
131/// Builder for creating matrix configurations programmatically
132pub struct MatrixBuilder {
133    instances: HashMap<String, HashMap<String, Value>>,
134}
135
136impl MatrixBuilder {
137    pub fn new() -> Self {
138        Self {
139            instances: HashMap::new(),
140        }
141    }
142
143    /// Add an instance with given name and variables
144    pub fn add_instance(
145        mut self,
146        name: impl Into<String>,
147        variables: HashMap<String, Value>,
148    ) -> Self {
149        self.instances.insert(name.into(), variables);
150        self
151    }
152
153    /// Add an instance with a single variable
154    pub fn add_simple(
155        mut self,
156        instance_name: impl Into<String>,
157        var_name: impl Into<String>,
158        var_value: impl Into<Value>,
159    ) -> Self {
160        let mut vars = HashMap::new();
161        vars.insert(var_name.into(), var_value.into());
162        self.instances.insert(instance_name.into(), vars);
163        self
164    }
165
166    /// Build into MatrixInstance list
167    pub fn build(self) -> Vec<MatrixInstance> {
168        self.instances
169            .into_iter()
170            .map(|(name, variables)| MatrixInstance { name, variables })
171            .collect()
172    }
173}
174
175impl Default for MatrixBuilder {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_expand_inline_matrix() {
187        let mut config = HashMap::new();
188
189        let mut linux_vars = HashMap::new();
190        linux_vars.insert(
191            "vmImage".to_string(),
192            serde_yaml::Value::String("ubuntu-latest".to_string()),
193        );
194        linux_vars.insert(
195            "platform".to_string(),
196            serde_yaml::Value::String("linux".to_string()),
197        );
198        config.insert("linux".to_string(), linux_vars);
199
200        let mut windows_vars = HashMap::new();
201        windows_vars.insert(
202            "vmImage".to_string(),
203            serde_yaml::Value::String("windows-latest".to_string()),
204        );
205        windows_vars.insert(
206            "platform".to_string(),
207            serde_yaml::Value::String("windows".to_string()),
208        );
209        config.insert("windows".to_string(), windows_vars);
210
211        let strategy = Strategy {
212            matrix: Some(MatrixStrategy::Inline(config)),
213            parallel: None,
214            max_parallel: Some(2),
215            run_once: None,
216            rolling: None,
217            canary: None,
218        };
219
220        let instances = MatrixExpander::expand(&strategy);
221
222        assert_eq!(instances.len(), 2);
223
224        // Check that we have both instances (order may vary)
225        let names: Vec<_> = instances.iter().map(|i| i.name.as_str()).collect();
226        assert!(names.contains(&"linux"));
227        assert!(names.contains(&"windows"));
228
229        // Check variables
230        let linux = instances.iter().find(|i| i.name == "linux").unwrap();
231        assert_eq!(
232            linux.variables.get("vmImage"),
233            Some(&Value::String("ubuntu-latest".to_string()))
234        );
235        assert_eq!(
236            linux.variables.get("platform"),
237            Some(&Value::String("linux".to_string()))
238        );
239    }
240
241    #[test]
242    fn test_expand_parallel() {
243        let strategy = Strategy {
244            matrix: None,
245            parallel: Some(4),
246            max_parallel: None,
247            run_once: None,
248            rolling: None,
249            canary: None,
250        };
251
252        let instances = MatrixExpander::expand(&strategy);
253
254        assert_eq!(instances.len(), 4);
255
256        for (i, instance) in instances.iter().enumerate() {
257            assert_eq!(instance.name, format!("Job {}", i + 1));
258            assert_eq!(
259                instance.variables.get("System.JobPositionInPhase"),
260                Some(&Value::Number((i + 1) as f64))
261            );
262            assert_eq!(
263                instance.variables.get("System.TotalJobsInPhase"),
264                Some(&Value::Number(4.0))
265            );
266        }
267    }
268
269    #[test]
270    fn test_matrix_builder() {
271        let instances = MatrixBuilder::new()
272            .add_simple("debug", "configuration", "Debug")
273            .add_simple("release", "configuration", "Release")
274            .build();
275
276        assert_eq!(instances.len(), 2);
277
278        let names: Vec<_> = instances.iter().map(|i| i.name.as_str()).collect();
279        assert!(names.contains(&"debug"));
280        assert!(names.contains(&"release"));
281    }
282
283    #[test]
284    fn test_max_parallel() {
285        let strategy = Strategy {
286            matrix: None,
287            parallel: Some(10),
288            max_parallel: Some(3),
289            run_once: None,
290            rolling: None,
291            canary: None,
292        };
293
294        assert_eq!(MatrixExpander::max_parallel(&strategy), Some(3));
295    }
296
297    #[test]
298    fn test_no_matrix_strategy() {
299        let strategy = Strategy {
300            matrix: None,
301            parallel: None,
302            max_parallel: None,
303            run_once: None,
304            rolling: None,
305            canary: None,
306        };
307
308        let instances = MatrixExpander::expand(&strategy);
309        assert!(instances.is_empty());
310    }
311
312    #[test]
313    fn test_yaml_value_conversion() {
314        let yaml_string = serde_yaml::Value::String("test".to_string());
315        assert_eq!(
316            MatrixExpander::yaml_to_value(&yaml_string),
317            Value::String("test".to_string())
318        );
319
320        let yaml_number = serde_yaml::Value::Number(serde_yaml::Number::from(42));
321        assert_eq!(
322            MatrixExpander::yaml_to_value(&yaml_number),
323            Value::Number(42.0)
324        );
325
326        let yaml_bool = serde_yaml::Value::Bool(true);
327        assert_eq!(MatrixExpander::yaml_to_value(&yaml_bool), Value::Bool(true));
328
329        let yaml_null = serde_yaml::Value::Null;
330        assert_eq!(MatrixExpander::yaml_to_value(&yaml_null), Value::Null);
331    }
332}