1mod macros;
6pub mod testing;
7mod v1;
8
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use std::collections::hash_map::Iter;
12use std::collections::HashMap;
13use std::fs::{DirBuilder, File};
14use std::io::Error;
15use std::path::{Path, PathBuf};
16use test_list::TestTag;
17
18pub const RUN_SUMMARY_NAME: &str = "run_summary.json";
20pub const RUN_NAME: &str = "run";
21
22enumerable_enum! {
23 #[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
25 SchemaVersion {
26 V1,
27 }
28}
29
30enumerable_enum! {
31 #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy)]
33 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
34 Outcome {
35 NotStarted,
36 Passed,
37 Failed,
38 Inconclusive,
39 Timedout,
40 Error,
41 Skipped,
42 }
43}
44
45enumerable_enum! {
46 #[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Copy, Hash)]
48 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
49 ArtifactType {
50 Syslog,
51 RestrictedLog,
53 Stdout,
54 Stderr,
55 Custom,
57 Report,
59 Debug,
61 }
62}
63
64#[derive(PartialEq, Eq, Debug, Clone)]
67pub struct ArtifactSubDirectory {
68 version: SchemaVersion,
69 root: PathBuf,
70 artifacts: HashMap<PathBuf, ArtifactMetadata>,
71}
72
73#[derive(PartialEq, Eq, Debug, Clone)]
75pub struct CommonResult {
76 pub name: String,
77 pub artifact_dir: ArtifactSubDirectory,
78 pub outcome: MaybeUnknown<Outcome>,
79 pub start_time: Option<u64>,
81 pub duration_milliseconds: Option<u64>,
82}
83
84#[derive(PartialEq, Eq, Debug, Clone)]
88pub struct TestRunResult<'a> {
89 pub common: Cow<'a, CommonResult>,
90 pub suites: Vec<SuiteResult<'a>>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct SuiteResult<'a> {
98 pub common: Cow<'a, CommonResult>,
99 pub cases: Vec<TestCaseResult<'a>>,
100 pub tags: Cow<'a, Vec<TestTag>>,
101}
102
103#[derive(PartialEq, Eq, Debug, Clone)]
105pub struct TestCaseResult<'a> {
106 pub common: Cow<'a, CommonResult>,
107}
108
109impl TestRunResult<'static> {
110 pub fn from_dir(root: &Path) -> Result<Self, Error> {
111 v1::parse_from_directory(root)
112 }
113}
114
115#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone, Hash)]
117pub struct ArtifactMetadata {
118 pub artifact_type: MaybeUnknown<ArtifactType>,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub component_moniker: Option<String>,
123}
124
125#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Hash, Clone)]
126#[serde(untagged)]
127pub enum MaybeUnknown<T> {
128 Known(T),
129 Unknown(String),
130}
131
132impl<T> From<T> for MaybeUnknown<T> {
133 fn from(other: T) -> Self {
134 Self::Known(other)
135 }
136}
137
138impl From<ArtifactType> for ArtifactMetadata {
139 fn from(other: ArtifactType) -> Self {
140 Self { artifact_type: MaybeUnknown::Known(other), component_moniker: None }
141 }
142}
143
144pub struct OutputDirectoryBuilder {
166 version: SchemaVersion,
167 root: PathBuf,
168 creation_instant: std::time::Instant,
172}
173
174impl OutputDirectoryBuilder {
175 pub fn new(dir: impl Into<PathBuf>, version: SchemaVersion) -> Result<Self, Error> {
177 let root = dir.into();
178 ensure_directory_exists(&root)?;
179 Ok(Self { version, root, creation_instant: std::time::Instant::now() })
180 }
181
182 pub fn new_artifact_dir(&self) -> Result<ArtifactSubDirectory, Error> {
187 match self.version {
188 SchemaVersion::V1 => {
189 let subdir_root =
190 self.root.join(format!("{:?}", self.creation_instant.elapsed().as_nanos()));
191 Ok(ArtifactSubDirectory {
192 version: self.version,
193 root: subdir_root,
194 artifacts: HashMap::new(),
195 })
196 }
197 }
198 }
199
200 pub fn save_summary<'a, 'b>(&'a self, result: &'a TestRunResult<'b>) -> Result<(), Error> {
202 match self.version {
203 SchemaVersion::V1 => v1::save_summary(self.root.as_path(), result),
204 }
205 }
206
207 pub fn path(&self) -> &Path {
209 self.root.as_path()
210 }
211}
212
213impl ArtifactSubDirectory {
214 pub fn new_artifact(
216 &mut self,
217 metadata: impl Into<ArtifactMetadata>,
218 name: impl AsRef<Path>,
219 ) -> Result<File, Error> {
220 ensure_directory_exists(self.root.as_path())?;
221 match self.version {
222 SchemaVersion::V1 => {
223 self.artifacts.insert(name.as_ref().to_path_buf(), metadata.into());
225 File::create(self.root.join(name))
226 }
227 }
228 }
229
230 pub fn new_directory_artifact(
232 &mut self,
233 metadata: impl Into<ArtifactMetadata>,
234 name: impl AsRef<Path>,
235 ) -> Result<PathBuf, Error> {
236 match self.version {
237 SchemaVersion::V1 => {
238 let subdir = self.root.join(name.as_ref());
240 ensure_directory_exists(subdir.as_path())?;
241 self.artifacts.insert(name.as_ref().to_path_buf(), metadata.into());
242 Ok(subdir)
243 }
244 }
245 }
246
247 pub fn path_to_artifact(&self, name: impl AsRef<Path>) -> Option<PathBuf> {
249 match self.version {
250 SchemaVersion::V1 => match self.artifacts.contains_key(name.as_ref()) {
251 true => Some(self.root.join(name.as_ref())),
252 false => None,
253 },
254 }
255 }
256
257 pub fn contents(&self) -> Vec<PathBuf> {
260 self.artifacts.keys().cloned().collect()
261 }
262
263 pub fn artifact_iter(&self) -> Iter<'_, PathBuf, ArtifactMetadata> {
267 self.artifacts.iter()
268 }
269}
270
271fn ensure_directory_exists(dir: &Path) -> Result<(), Error> {
272 match dir.exists() {
273 true => Ok(()),
274 false => DirBuilder::new().recursive(true).create(&dir),
275 }
276}
277
278#[cfg(test)]
279mod test {
280 use super::*;
281 use std::io::Write;
282 use tempfile::tempdir;
283
284 fn validate_against_schema(version: SchemaVersion, root: &Path) {
285 match version {
286 SchemaVersion::V1 => v1::validate_against_schema(root),
287 }
288 }
289
290 fn round_trip_test_all_versions<F>(produce_run_fn: F)
292 where
293 F: Fn(&OutputDirectoryBuilder) -> TestRunResult<'static>,
294 {
295 for version in SchemaVersion::all_variants() {
296 let dir = tempdir().expect("Create dir");
297 let output_dir = OutputDirectoryBuilder::new(dir.path(), version).expect("create dir");
298
299 let run_result = produce_run_fn(&output_dir);
300 output_dir.save_summary(&run_result).expect("save summary");
301
302 validate_against_schema(version, dir.path());
303
304 let parsed = TestRunResult::from_dir(dir.path()).expect("parse output directory");
305 assert_eq!(run_result, parsed, "version: {:?}", version);
306 }
307 }
308
309 #[test]
310 fn minimal() {
311 round_trip_test_all_versions(|dir_builder| TestRunResult {
312 common: Cow::Owned(CommonResult {
313 name: RUN_NAME.to_string(),
314 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
315 outcome: Outcome::Passed.into(),
316 start_time: None,
317 duration_milliseconds: None,
318 }),
319 suites: vec![],
320 });
321 let _ = Outcome::all_variants();
322 }
323
324 #[test]
325 fn artifact_types() {
326 for artifact_type in ArtifactType::all_variants() {
327 round_trip_test_all_versions(|dir_builder| {
328 let mut run_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
329 let mut run_artifact =
330 run_artifact_dir.new_artifact(artifact_type, "a.txt").expect("create artifact");
331 write!(run_artifact, "run contents").unwrap();
332
333 let mut suite_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
334 let mut suite_artifact = suite_artifact_dir
335 .new_artifact(artifact_type, "a.txt")
336 .expect("create artifact");
337 write!(suite_artifact, "suite contents").unwrap();
338
339 let mut case_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
340 let mut case_artifact = case_artifact_dir
341 .new_artifact(artifact_type, "a.txt")
342 .expect("create artifact");
343 write!(case_artifact, "case contents").unwrap();
344
345 TestRunResult {
346 common: Cow::Owned(CommonResult {
347 name: RUN_NAME.to_string(),
348 artifact_dir: run_artifact_dir,
349 outcome: Outcome::Passed.into(),
350 start_time: None,
351 duration_milliseconds: None,
352 }),
353 suites: vec![SuiteResult {
354 common: Cow::Owned(CommonResult {
355 name: "suite".to_string(),
356 artifact_dir: suite_artifact_dir,
357 outcome: Outcome::Passed.into(),
358 start_time: None,
359 duration_milliseconds: None,
360 }),
361 tags: Cow::Owned(vec![]),
362 cases: vec![TestCaseResult {
363 common: Cow::Owned(CommonResult {
364 name: "case".to_string(),
365 artifact_dir: case_artifact_dir,
366 outcome: Outcome::Passed.into(),
367 start_time: None,
368 duration_milliseconds: None,
369 }),
370 }],
371 }],
372 }
373 });
374 }
375 }
376
377 #[test]
378 fn artifact_types_moniker_specified() {
379 for artifact_type in ArtifactType::all_variants() {
380 round_trip_test_all_versions(|dir_builder| {
381 let mut run_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
382 let mut run_artifact = run_artifact_dir
383 .new_artifact(
384 ArtifactMetadata {
385 artifact_type: artifact_type.into(),
386 component_moniker: Some("moniker".to_string()),
387 },
388 "a.txt",
389 )
390 .expect("create artifact");
391 write!(run_artifact, "run contents").unwrap();
392
393 let mut suite_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
394 let mut suite_artifact = suite_artifact_dir
395 .new_artifact(
396 ArtifactMetadata {
397 artifact_type: artifact_type.into(),
398 component_moniker: Some("moniker".to_string()),
399 },
400 "a.txt",
401 )
402 .expect("create artifact");
403 write!(suite_artifact, "suite contents").unwrap();
404
405 let mut case_artifact_dir = dir_builder.new_artifact_dir().expect("new dir");
406 let mut case_artifact = case_artifact_dir
407 .new_artifact(
408 ArtifactMetadata {
409 artifact_type: artifact_type.into(),
410 component_moniker: Some("moniker".to_string()),
411 },
412 "a.txt",
413 )
414 .expect("create artifact");
415 write!(case_artifact, "case contents").unwrap();
416
417 TestRunResult {
418 common: Cow::Owned(CommonResult {
419 name: RUN_NAME.to_string(),
420 artifact_dir: run_artifact_dir,
421 outcome: Outcome::Passed.into(),
422 start_time: None,
423 duration_milliseconds: None,
424 }),
425 suites: vec![SuiteResult {
426 common: Cow::Owned(CommonResult {
427 name: "suite".to_string(),
428 artifact_dir: suite_artifact_dir,
429 outcome: Outcome::Passed.into(),
430 start_time: None,
431 duration_milliseconds: None,
432 }),
433 tags: Cow::Owned(vec![]),
434 cases: vec![TestCaseResult {
435 common: Cow::Owned(CommonResult {
436 name: "case".to_string(),
437 artifact_dir: case_artifact_dir,
438 outcome: Outcome::Passed.into(),
439 start_time: None,
440 duration_milliseconds: None,
441 }),
442 }],
443 }],
444 }
445 });
446 }
447 }
448
449 #[test]
450 fn outcome_types() {
451 for outcome_type in Outcome::all_variants() {
452 round_trip_test_all_versions(|dir_builder| TestRunResult {
453 common: Cow::Owned(CommonResult {
454 name: RUN_NAME.to_string(),
455 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
456 outcome: outcome_type.into(),
457 start_time: None,
458 duration_milliseconds: None,
459 }),
460 suites: vec![SuiteResult {
461 common: Cow::Owned(CommonResult {
462 name: "suite".to_string(),
463 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
464 outcome: outcome_type.into(),
465 start_time: None,
466 duration_milliseconds: None,
467 }),
468 tags: Cow::Owned(vec![]),
469 cases: vec![TestCaseResult {
470 common: Cow::Owned(CommonResult {
471 name: "case".to_string(),
472 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
473 outcome: outcome_type.into(),
474 start_time: None,
475 duration_milliseconds: None,
476 }),
477 }],
478 }],
479 });
480 }
481 }
482
483 #[test]
484 fn timing_specified() {
485 round_trip_test_all_versions(|dir_builder| TestRunResult {
486 common: Cow::Owned(CommonResult {
487 name: RUN_NAME.to_string(),
488 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
489 outcome: Outcome::Passed.into(),
490 start_time: Some(1),
491 duration_milliseconds: Some(2),
492 }),
493 suites: vec![SuiteResult {
494 common: Cow::Owned(CommonResult {
495 name: "suite".to_string(),
496 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
497 outcome: Outcome::Passed.into(),
498 start_time: Some(3),
499 duration_milliseconds: Some(4),
500 }),
501 tags: Cow::Owned(vec![]),
502 cases: vec![TestCaseResult {
503 common: Cow::Owned(CommonResult {
504 name: "case".to_string(),
505 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
506 outcome: Outcome::Passed.into(),
507 start_time: Some(5),
508 duration_milliseconds: Some(6),
509 }),
510 }],
511 }],
512 });
513 }
514
515 #[test]
516 fn tags_specified() {
517 round_trip_test_all_versions(|dir_builder| TestRunResult {
518 common: Cow::Owned(CommonResult {
519 name: RUN_NAME.to_string(),
520 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
521 outcome: Outcome::Passed.into(),
522 start_time: Some(1),
523 duration_milliseconds: Some(2),
524 }),
525 suites: vec![SuiteResult {
526 common: Cow::Owned(CommonResult {
527 name: "suite".to_string(),
528 artifact_dir: dir_builder.new_artifact_dir().expect("new dir"),
529 outcome: Outcome::Passed.into(),
530 start_time: Some(3),
531 duration_milliseconds: Some(4),
532 }),
533 tags: Cow::Owned(vec![
534 TestTag { key: "hermetic".to_string(), value: "false".to_string() },
535 TestTag { key: "realm".to_string(), value: "system".to_string() },
536 ]),
537 cases: vec![],
538 }],
539 });
540 }
541}