pipeline_service/execution/
matrix.rs1use crate::parser::models::{MatrixStrategy, Strategy, Value};
5
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct MatrixInstance {
11 pub name: String,
13 pub variables: HashMap<String, Value>,
15}
16
17pub struct MatrixExpander;
19
20impl MatrixExpander {
21 pub fn expand(strategy: &Strategy) -> Vec<MatrixInstance> {
23 let mut instances = Vec::new();
24
25 if let Some(matrix) = &strategy.matrix {
27 instances = Self::expand_matrix(matrix);
28 }
29
30 if let Some(parallel) = strategy.parallel {
32 if instances.is_empty() {
33 instances = Self::expand_parallel(parallel);
35 }
36 }
39
40 instances
44 }
45
46 fn expand_matrix(matrix: &MatrixStrategy) -> Vec<MatrixInstance> {
48 match matrix {
49 MatrixStrategy::Inline(config) => {
50 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 Vec::new()
66 }
67 }
68 }
69
70 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 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, }
113 }
114
115 pub fn max_parallel(strategy: &Strategy) -> Option<u32> {
117 strategy.max_parallel
118 }
119
120 pub fn has_matrix(strategy: &Strategy) -> bool {
122 strategy.matrix.is_some()
123 }
124
125 pub fn has_parallel(strategy: &Strategy) -> bool {
127 strategy.parallel.is_some()
128 }
129}
130
131pub 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 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 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 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 let names: Vec<_> = instances.iter().map(|i| i.name.as_str()).collect();
226 assert!(names.contains(&"linux"));
227 assert!(names.contains(&"windows"));
228
229 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}