1pub mod assertions;
5pub mod parser;
6pub mod reporter;
7pub mod runner;
8
9pub use assertions::{Assertion, AssertionResult};
11pub use parser::TestFileParser;
12pub use reporter::{ReportFormat, TestReporter};
13pub use runner::{TestResult, TestRunner, TestSuiteResult};
14
15use crate::parser::models::Value;
16
17use std::collections::HashMap;
18use std::path::PathBuf;
19
20use serde::de::{self, MapAccess, Visitor};
21use serde::{Deserialize, Deserializer, Serialize};
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct TestSuite {
30 #[serde(default)]
32 pub name: Option<String>,
33 pub tests: Vec<PipelineTest>,
35 #[serde(default)]
37 pub defaults: Option<TestDefaults>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct TestDefaults {
43 #[serde(default)]
45 pub variables: HashMap<String, String>,
46 #[serde(default)]
48 pub parameters: HashMap<String, serde_yaml::Value>,
49 #[serde(default)]
51 pub working_dir: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct PipelineTest {
57 pub name: String,
59 pub pipeline: PathBuf,
61 #[serde(default)]
63 pub variables: HashMap<String, String>,
64 #[serde(default)]
66 pub parameters: HashMap<String, serde_yaml::Value>,
67 #[serde(default)]
69 pub working_dir: Option<String>,
70 #[serde(default)]
72 pub assertions: Vec<AssertionDef>,
73}
74
75#[derive(Debug, Clone, Serialize)]
86pub enum AssertionDef {
87 StepSucceeded(String),
89
90 StepFailed(String),
92
93 StepSkipped(String),
95
96 JobSucceeded(String),
98
99 JobFailed(String),
101
102 JobSkipped(String),
104
105 StageSucceeded(String),
107
108 StageFailed(String),
110
111 StageSkipped(String),
113
114 StepOutputEquals(StepOutputAssertion),
116
117 StepOutputContains(StepOutputPatternAssertion),
119
120 StepRanBefore(OrderAssertion),
122
123 StepsRanInParallel(ParallelAssertion),
125
126 VariableEquals(VariableAssertion),
128
129 VariableContains(VariablePatternAssertion),
131
132 PipelineSucceeded,
134
135 PipelineFailed,
137}
138
139impl<'de> Deserialize<'de> for AssertionDef {
140 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141 where
142 D: Deserializer<'de>,
143 {
144 struct AssertionDefVisitor;
145
146 impl<'de> Visitor<'de> for AssertionDefVisitor {
147 type Value = AssertionDef;
148
149 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
150 formatter.write_str(
151 "a string like 'pipeline_succeeded' or a mapping like 'step_succeeded: Build'",
152 )
153 }
154
155 fn visit_str<E>(self, value: &str) -> Result<AssertionDef, E>
157 where
158 E: de::Error,
159 {
160 match value {
161 "pipeline_succeeded" => Ok(AssertionDef::PipelineSucceeded),
162 "pipeline_failed" => Ok(AssertionDef::PipelineFailed),
163 _ => Err(de::Error::unknown_variant(
164 value,
165 &["pipeline_succeeded", "pipeline_failed"],
166 )),
167 }
168 }
169
170 fn visit_map<M>(self, mut map: M) -> Result<AssertionDef, M::Error>
172 where
173 M: MapAccess<'de>,
174 {
175 let key: String = map
176 .next_key()?
177 .ok_or_else(|| de::Error::custom("expected assertion key"))?;
178
179 let result = match key.as_str() {
180 "step_succeeded" => {
181 let val: String = map.next_value()?;
182 Ok(AssertionDef::StepSucceeded(val))
183 }
184 "step_failed" => {
185 let val: String = map.next_value()?;
186 Ok(AssertionDef::StepFailed(val))
187 }
188 "step_skipped" => {
189 let val: String = map.next_value()?;
190 Ok(AssertionDef::StepSkipped(val))
191 }
192 "job_succeeded" => {
193 let val: String = map.next_value()?;
194 Ok(AssertionDef::JobSucceeded(val))
195 }
196 "job_failed" => {
197 let val: String = map.next_value()?;
198 Ok(AssertionDef::JobFailed(val))
199 }
200 "job_skipped" => {
201 let val: String = map.next_value()?;
202 Ok(AssertionDef::JobSkipped(val))
203 }
204 "stage_succeeded" => {
205 let val: String = map.next_value()?;
206 Ok(AssertionDef::StageSucceeded(val))
207 }
208 "stage_failed" => {
209 let val: String = map.next_value()?;
210 Ok(AssertionDef::StageFailed(val))
211 }
212 "stage_skipped" => {
213 let val: String = map.next_value()?;
214 Ok(AssertionDef::StageSkipped(val))
215 }
216 "step_output_equals" => {
217 let val: StepOutputAssertion = map.next_value()?;
218 Ok(AssertionDef::StepOutputEquals(val))
219 }
220 "step_output_contains" => {
221 let val: StepOutputPatternAssertion = map.next_value()?;
222 Ok(AssertionDef::StepOutputContains(val))
223 }
224 "step_ran_before" => {
225 let val: OrderAssertion = map.next_value()?;
226 Ok(AssertionDef::StepRanBefore(val))
227 }
228 "steps_ran_in_parallel" => {
229 let val: ParallelAssertion = map.next_value()?;
230 Ok(AssertionDef::StepsRanInParallel(val))
231 }
232 "variable_equals" => {
233 let val: VariableAssertion = map.next_value()?;
234 Ok(AssertionDef::VariableEquals(val))
235 }
236 "variable_contains" => {
237 let val: VariablePatternAssertion = map.next_value()?;
238 Ok(AssertionDef::VariableContains(val))
239 }
240 "pipeline_succeeded" => {
241 let _: serde_yaml::Value = map.next_value()?;
243 Ok(AssertionDef::PipelineSucceeded)
244 }
245 "pipeline_failed" => {
246 let _: serde_yaml::Value = map.next_value()?;
247 Ok(AssertionDef::PipelineFailed)
248 }
249 _ => Err(de::Error::unknown_field(
250 &key,
251 &[
252 "step_succeeded",
253 "step_failed",
254 "step_skipped",
255 "job_succeeded",
256 "job_failed",
257 "job_skipped",
258 "stage_succeeded",
259 "stage_failed",
260 "stage_skipped",
261 "step_output_equals",
262 "step_output_contains",
263 "step_ran_before",
264 "steps_ran_in_parallel",
265 "variable_equals",
266 "variable_contains",
267 "pipeline_succeeded",
268 "pipeline_failed",
269 ],
270 )),
271 };
272
273 result
274 }
275 }
276
277 deserializer.deserialize_any(AssertionDefVisitor)
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct StepOutputAssertion {
284 pub step: String,
286 pub output: String,
288 pub expected: serde_yaml::Value,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct StepOutputPatternAssertion {
295 pub step: String,
297 pub pattern: String,
299 #[serde(default)]
301 pub output: Option<String>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct OrderAssertion {
307 pub step: String,
309 pub before: String,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct ParallelAssertion {
316 pub steps: Vec<String>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct VariableAssertion {
323 pub name: String,
325 pub expected: serde_yaml::Value,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct VariablePatternAssertion {
332 pub name: String,
334 pub pattern: String,
336}
337
338impl AssertionDef {
343 pub fn to_assertion(&self) -> Assertion {
345 match self {
346 AssertionDef::StepSucceeded(name) => Assertion::StepSucceeded { step: name.clone() },
347 AssertionDef::StepFailed(name) => Assertion::StepFailed { step: name.clone() },
348 AssertionDef::StepSkipped(name) => Assertion::StepSkipped { step: name.clone() },
349 AssertionDef::JobSucceeded(name) => Assertion::JobSucceeded { job: name.clone() },
350 AssertionDef::JobFailed(name) => Assertion::JobFailed { job: name.clone() },
351 AssertionDef::JobSkipped(name) => Assertion::JobSkipped { job: name.clone() },
352 AssertionDef::StageSucceeded(name) => Assertion::StageSucceeded {
353 stage: name.clone(),
354 },
355 AssertionDef::StageFailed(name) => Assertion::StageFailed {
356 stage: name.clone(),
357 },
358 AssertionDef::StageSkipped(name) => Assertion::StageSkipped {
359 stage: name.clone(),
360 },
361 AssertionDef::StepOutputEquals(a) => Assertion::StepOutputEquals {
362 step: a.step.clone(),
363 output: a.output.clone(),
364 expected: yaml_to_value(&a.expected),
365 },
366 AssertionDef::StepOutputContains(a) => Assertion::StepOutputContains {
367 step: a.step.clone(),
368 pattern: a.pattern.clone(),
369 output: a.output.clone(),
370 },
371 AssertionDef::StepRanBefore(a) => Assertion::StepRanBefore {
372 step: a.step.clone(),
373 before: a.before.clone(),
374 },
375 AssertionDef::StepsRanInParallel(a) => Assertion::StepsRanInParallel {
376 steps: a.steps.clone(),
377 },
378 AssertionDef::VariableEquals(a) => Assertion::VariableEquals {
379 name: a.name.clone(),
380 expected: yaml_to_value(&a.expected),
381 },
382 AssertionDef::VariableContains(a) => Assertion::VariableContains {
383 name: a.name.clone(),
384 pattern: a.pattern.clone(),
385 },
386 AssertionDef::PipelineSucceeded => Assertion::PipelineSucceeded,
387 AssertionDef::PipelineFailed => Assertion::PipelineFailed,
388 }
389 }
390}
391
392fn yaml_to_value(v: &serde_yaml::Value) -> Value {
394 match v {
395 serde_yaml::Value::Null => Value::Null,
396 serde_yaml::Value::Bool(b) => Value::Bool(*b),
397 serde_yaml::Value::Number(n) => {
398 if let Some(i) = n.as_i64() {
399 Value::Number(i as f64)
400 } else if let Some(f) = n.as_f64() {
401 Value::Number(f)
402 } else {
403 Value::Null
404 }
405 }
406 serde_yaml::Value::String(s) => Value::String(s.clone()),
407 serde_yaml::Value::Sequence(seq) => Value::Array(seq.iter().map(yaml_to_value).collect()),
408 serde_yaml::Value::Mapping(map) => {
409 let mut obj = HashMap::new();
410 for (k, v) in map {
411 if let serde_yaml::Value::String(key) = k {
412 obj.insert(key.clone(), yaml_to_value(v));
413 }
414 }
415 Value::Object(obj)
416 }
417 serde_yaml::Value::Tagged(tagged) => yaml_to_value(&tagged.value),
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_assertion_def_to_assertion_step_succeeded() {
427 let def = AssertionDef::StepSucceeded("Build".to_string());
428 let assertion = def.to_assertion();
429 assert!(matches!(
430 assertion,
431 Assertion::StepSucceeded { step } if step == "Build"
432 ));
433 }
434
435 #[test]
436 fn test_assertion_def_to_assertion_variable_equals() {
437 let def = AssertionDef::VariableEquals(VariableAssertion {
438 name: "BUILD_CONFIG".to_string(),
439 expected: serde_yaml::Value::String("Release".to_string()),
440 });
441 let assertion = def.to_assertion();
442 assert!(matches!(
443 assertion,
444 Assertion::VariableEquals { name, expected }
445 if name == "BUILD_CONFIG" && expected == Value::String("Release".to_string())
446 ));
447 }
448
449 #[test]
450 fn test_yaml_to_value_primitives() {
451 assert_eq!(yaml_to_value(&serde_yaml::Value::Null), Value::Null);
452 assert_eq!(
453 yaml_to_value(&serde_yaml::Value::Bool(true)),
454 Value::Bool(true)
455 );
456 assert_eq!(
457 yaml_to_value(&serde_yaml::Value::String("hello".to_string())),
458 Value::String("hello".to_string())
459 );
460 }
461
462 #[test]
463 fn test_pipeline_test_deserialize() {
464 let yaml = r#"
465name: "Build test"
466pipeline: azure-pipelines.yml
467variables:
468 BUILD_CONFIG: Release
469assertions:
470 - step_succeeded: Build
471 - pipeline_succeeded
472"#;
473 let test: PipelineTest = serde_yaml::from_str(yaml).unwrap();
474 assert_eq!(test.name, "Build test");
475 assert_eq!(test.pipeline, PathBuf::from("azure-pipelines.yml"));
476 assert_eq!(test.variables.get("BUILD_CONFIG").unwrap(), "Release");
477 assert_eq!(test.assertions.len(), 2);
478 }
479
480 #[test]
481 fn test_test_suite_deserialize() {
482 let yaml = r#"
483tests:
484 - name: "Build stage runs correctly"
485 pipeline: azure-pipelines.yml
486 variables:
487 BUILD_CONFIG: Release
488 assertions:
489 - step_succeeded: Build
490 - step_output_contains:
491 step: Build
492 pattern: "Build succeeded"
493 - step_ran_before:
494 step: Test
495 before: Deploy
496
497 - name: "Deploy is skipped on PR"
498 pipeline: azure-pipelines.yml
499 variables:
500 BUILD_REASON: PullRequest
501 assertions:
502 - step_skipped: Deploy
503"#;
504 let suite: TestSuite = serde_yaml::from_str(yaml).unwrap();
505 assert_eq!(suite.tests.len(), 2);
506 assert_eq!(suite.tests[0].name, "Build stage runs correctly");
507 assert_eq!(suite.tests[0].assertions.len(), 3);
508 assert_eq!(suite.tests[1].name, "Deploy is skipped on PR");
509 assert_eq!(suite.tests[1].assertions.len(), 1);
510 }
511}