run_test_suite_lib/output/
directory.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::output::{
6    ArtifactType, DirectoryArtifactType, DirectoryWrite, DynArtifact, DynDirectoryArtifact,
7    EntityId, EntityInfo, ReportedOutcome, Reporter, Timestamp, ZxTime,
8};
9use anyhow::format_err;
10use fuchsia_sync::Mutex;
11use std::borrow::Cow;
12use std::collections::HashMap;
13use std::fs::{DirBuilder, File};
14use std::io::{BufWriter, Error, ErrorKind};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU32, Ordering};
17use test_list::TestTag;
18use test_output_directory as directory;
19
20pub use directory::SchemaVersion;
21
22const STDOUT_FILE: &str = "stdout.txt";
23const STDERR_FILE: &str = "stderr.txt";
24const SYSLOG_FILE: &str = "syslog.txt";
25const REPORT_FILE: &str = "report.txt";
26const RESTRICTED_LOG_FILE: &str = "restricted_logs.txt";
27const CUSTOM_ARTIFACT_DIRECTORY: &str = "custom";
28const DEBUG_ARTIFACT_DIRECTORY: &str = "debug";
29
30const SAVE_AFTER_SUITE_COUNT: u32 = 10;
31
32/// A reporter that saves results and artifacts to disk in the Fuchsia test output format.
33pub struct DirectoryReporter {
34    /// Root directory in which to place results.
35    output_directory: directory::OutputDirectoryBuilder,
36    /// A mapping from ID to every test run, test suite, and test case. The test run entry
37    /// is always contained in ID TEST_RUN_ID. Entries are added as new test cases and suites
38    /// are found, and removed once they have been persisted.
39    entries: Mutex<HashMap<EntityId, EntityEntry>>,
40    /// Atomic counter used to generate unique names for custom artifact directories.
41    name_counter: AtomicU32,
42    /// Counter for number of completed suites. Used to save complete results after
43    /// SAVE_AFTER_SUITE_COUNT suites complete.
44    suites_finished_counter: AtomicU32,
45}
46
47/// In-memory representation of either a test run, test suite, or test case.
48struct EntityEntry {
49    common: directory::CommonResult,
50    /// A list of the children of an entity referenced by their id. Unused for a test case.
51    children: Vec<EntityId>,
52    /// Timer used to measure durations as the difference between monotonic timestamps on
53    /// start and stop events.
54    timer: MonotonicTimer,
55    tags: Option<Vec<TestTag>>,
56}
57
58enum MonotonicTimer {
59    Unknown,
60    /// Entity has started but not stopped.
61    Started {
62        /// Monotonic start timestamp reported by the target.
63        mono_start_time: ZxTime,
64    },
65    /// Entity has completed running.
66    Stopped,
67}
68
69impl DirectoryReporter {
70    /// Create a new `DirectoryReporter` that places results in the given `root` directory.
71    pub fn new(root: PathBuf, schema_version: SchemaVersion) -> Result<Self, Error> {
72        let output_directory = directory::OutputDirectoryBuilder::new(root, schema_version)?;
73
74        let mut entries = HashMap::new();
75        entries.insert(
76            EntityId::TestRun,
77            EntityEntry {
78                common: directory::CommonResult {
79                    name: "".to_string(),
80                    artifact_dir: output_directory.new_artifact_dir()?,
81                    outcome: directory::Outcome::NotStarted.into(),
82                    start_time: None,
83                    duration_milliseconds: None,
84                },
85                children: vec![],
86                timer: MonotonicTimer::Unknown,
87                tags: None,
88            },
89        );
90        let new_self = Self {
91            output_directory,
92            entries: Mutex::new(entries),
93            name_counter: AtomicU32::new(0),
94            suites_finished_counter: AtomicU32::new(1),
95        };
96        new_self.persist_summary()?;
97        Ok(new_self)
98    }
99
100    pub(super) fn add_report(&self, entity: &EntityId) -> Result<BufWriter<File>, Error> {
101        self.new_artifact_inner(entity, directory::ArtifactType::Report)
102    }
103
104    fn persist_summary(&self) -> Result<(), Error> {
105        let entry_lock = self.entries.lock();
106        let run_entry =
107            entry_lock.get(&EntityId::TestRun).expect("Run entry should always be present");
108
109        let mut run_result =
110            directory::TestRunResult { common: Cow::Borrowed(&run_entry.common), suites: vec![] };
111
112        for suite_entity_id in run_entry.children.iter() {
113            let suite_entry =
114                entry_lock.get(suite_entity_id).expect("Nonexistant suite referenced");
115            let mut suite_result = directory::SuiteResult {
116                common: Cow::Borrowed(&suite_entry.common),
117                cases: vec![],
118                tags: suite_entry.tags.as_ref().map(Cow::Borrowed).unwrap_or(Cow::Owned(vec![])),
119            };
120            for case_entity_id in suite_entry.children.iter() {
121                suite_result.cases.push(directory::TestCaseResult {
122                    common: Cow::Borrowed(
123                        &entry_lock
124                            .get(case_entity_id)
125                            .expect("Nonexistant case referenced")
126                            .common,
127                    ),
128                })
129            }
130
131            run_result.suites.push(suite_result);
132        }
133
134        self.output_directory.save_summary(&run_result)
135    }
136
137    fn new_artifact_inner(
138        &self,
139        entity: &EntityId,
140        artifact_type: directory::ArtifactType,
141    ) -> Result<BufWriter<File>, Error> {
142        let mut lock = self.entries.lock();
143        let entry = lock
144            .get_mut(entity)
145            .expect("Attempting to create an artifact for an entity that does not exist");
146        let file = entry
147            .common
148            .artifact_dir
149            .new_artifact(artifact_type, filename_for_type(&artifact_type))?;
150        Ok(BufWriter::new(file))
151    }
152}
153
154impl Reporter for DirectoryReporter {
155    fn new_entity(&self, entity: &EntityId, name: &str) -> Result<(), Error> {
156        let mut entries = self.entries.lock();
157        let parent_id = match entity {
158            EntityId::TestRun => panic!("Cannot create new test run"),
159            EntityId::Suite => EntityId::TestRun,
160            EntityId::Case { .. } => EntityId::Suite,
161        };
162        let parent = entries
163            .get_mut(&parent_id)
164            .expect("Attempting to create a child for an entity that does not exist");
165        parent.children.push(*entity);
166        entries.insert(
167            *entity,
168            EntityEntry {
169                common: directory::CommonResult {
170                    name: name.to_string(),
171                    artifact_dir: self.output_directory.new_artifact_dir()?,
172                    outcome: directory::Outcome::NotStarted.into(),
173                    start_time: None,
174                    duration_milliseconds: None,
175                },
176                children: vec![],
177                timer: MonotonicTimer::Unknown,
178                tags: None,
179            },
180        );
181
182        Ok(())
183    }
184
185    fn set_entity_info(&self, entity: &EntityId, info: &EntityInfo) {
186        let mut entries = self.entries.lock();
187        let entry = entries.get_mut(entity).expect("Setting info for entity that does not exist");
188        entry.tags = info.tags.clone();
189    }
190
191    fn entity_started(&self, entity: &EntityId, timestamp: Timestamp) -> Result<(), Error> {
192        let mut entries = self.entries.lock();
193        let entry =
194            entries.get_mut(entity).expect("Outcome reported for an entity that does not exist");
195        entry.common.start_time = Some(
196            std::time::SystemTime::now()
197                .duration_since(std::time::SystemTime::UNIX_EPOCH)
198                .unwrap()
199                .as_millis() as u64,
200        );
201        entry.common.outcome = directory::Outcome::Inconclusive.into();
202        match (&entry.timer, timestamp) {
203            (MonotonicTimer::Unknown, Timestamp::Given(mono_start_time)) => {
204                entry.timer = MonotonicTimer::Started { mono_start_time };
205            }
206            _ => (),
207        }
208        Ok(())
209    }
210
211    fn entity_stopped(
212        &self,
213        entity: &EntityId,
214        outcome: &ReportedOutcome,
215        timestamp: Timestamp,
216    ) -> Result<(), Error> {
217        let mut entries = self.entries.lock();
218        let entry =
219            entries.get_mut(entity).expect("Outcome reported for an entity that does not exist");
220        entry.common.outcome = into_serializable_outcome(*outcome).into();
221
222        if let (MonotonicTimer::Started { mono_start_time }, Timestamp::Given(mono_end_time)) =
223            (&entry.timer, timestamp)
224        {
225            entry.common.duration_milliseconds = Some(
226                mono_end_time
227                    .checked_sub(*mono_start_time)
228                    .expect("end time must be after start time")
229                    .as_millis() as u64,
230            );
231            entry.timer = MonotonicTimer::Stopped;
232        }
233        Ok(())
234    }
235
236    /// Finalize and persist the outcome and artifacts for an entity. This should only be
237    /// called once per entity.
238    fn entity_finished(&self, entity: &EntityId) -> Result<(), Error> {
239        match entity {
240            EntityId::TestRun => self.persist_summary()?,
241            EntityId::Suite => {
242                let num_saved_suites = self.suites_finished_counter.fetch_add(1, Ordering::Relaxed);
243                if num_saved_suites % SAVE_AFTER_SUITE_COUNT == 0 {
244                    self.persist_summary()?;
245                }
246            }
247            // Cases are saved as part of suites.
248            EntityId::Case { .. } => (),
249        }
250        Ok(())
251    }
252
253    fn new_artifact(
254        &self,
255        entity: &EntityId,
256        artifact_type: &ArtifactType,
257    ) -> Result<Box<DynArtifact>, Error> {
258        let file = self.new_artifact_inner(entity, (*artifact_type).into())?;
259        Ok(Box::new(file))
260    }
261
262    fn new_directory_artifact(
263        &self,
264        entity: &EntityId,
265        artifact_type: &DirectoryArtifactType,
266        component_moniker: Option<String>,
267    ) -> Result<Box<DynDirectoryArtifact>, Error> {
268        let mut lock = self.entries.lock();
269        let entry = lock
270            .get_mut(entity)
271            .expect("Attempting to create an artifact for an entity that does not exist");
272        let name = format!(
273            "{}-{}",
274            prefix_for_directory_type(artifact_type),
275            self.name_counter.fetch_add(1, Ordering::Relaxed),
276        );
277        let subdir = entry.common.artifact_dir.new_directory_artifact(
278            directory::ArtifactMetadata {
279                artifact_type: directory::MaybeUnknown::Known((*artifact_type).into()),
280                component_moniker,
281            },
282            name,
283        )?;
284
285        Ok(Box::new(DirectoryDirectoryWriter { path: subdir }))
286    }
287}
288
289/// A |DirectoryWrite| implementation that creates files in a set directory.
290struct DirectoryDirectoryWriter {
291    path: PathBuf,
292}
293
294impl DirectoryWrite for DirectoryDirectoryWriter {
295    fn new_file(&self, path: &Path) -> Result<Box<DynArtifact>, Error> {
296        let new_path = self.path.join(path);
297        // The path must be relative to the parent directory (no absolute paths) and cannot have
298        // any parent components (which can escape the parent directory).
299        if !new_path.starts_with(&self.path)
300            || new_path.components().any(|c| match c {
301                std::path::Component::ParentDir => true,
302                _ => false,
303            })
304        {
305            return Err(Error::new(
306                ErrorKind::Other,
307                format_err!(
308                    "Path {:?} results in destination {:?} that may be outside of {:?}",
309                    path,
310                    new_path,
311                    self.path
312                ),
313            ));
314        }
315        if let Some(parent) = new_path.parent() {
316            if !parent.exists() {
317                DirBuilder::new().recursive(true).create(&parent)?;
318            }
319        }
320
321        let file = BufWriter::new(File::create(new_path)?);
322        Ok(Box::new(file))
323    }
324}
325
326fn prefix_for_directory_type(artifact_type: &DirectoryArtifactType) -> &'static str {
327    match artifact_type {
328        DirectoryArtifactType::Custom => CUSTOM_ARTIFACT_DIRECTORY,
329        DirectoryArtifactType::Debug => DEBUG_ARTIFACT_DIRECTORY,
330    }
331}
332
333fn filename_for_type(artifact_type: &directory::ArtifactType) -> &'static str {
334    match artifact_type {
335        directory::ArtifactType::Stdout => STDOUT_FILE,
336        directory::ArtifactType::Stderr => STDERR_FILE,
337        directory::ArtifactType::Syslog => SYSLOG_FILE,
338        directory::ArtifactType::RestrictedLog => RESTRICTED_LOG_FILE,
339        directory::ArtifactType::Report => REPORT_FILE,
340        directory::ArtifactType::Custom => unreachable!("Custom artifact is not a file"),
341        directory::ArtifactType::Debug => {
342            unreachable!("Debug artifacts must be placed in a directory")
343        }
344    }
345}
346fn into_serializable_outcome(outcome: ReportedOutcome) -> directory::Outcome {
347    match outcome {
348        ReportedOutcome::Passed => directory::Outcome::Passed,
349        ReportedOutcome::Failed => directory::Outcome::Failed,
350        ReportedOutcome::Inconclusive => directory::Outcome::Inconclusive,
351        ReportedOutcome::Timedout => directory::Outcome::Timedout,
352        ReportedOutcome::Error => directory::Outcome::Error,
353        ReportedOutcome::Skipped => directory::Outcome::Skipped,
354        ReportedOutcome::Cancelled => directory::Outcome::Inconclusive,
355        ReportedOutcome::DidNotFinish => directory::Outcome::Inconclusive,
356    }
357}
358
359#[cfg(test)]
360mod test {
361    use super::*;
362    use crate::output::{CaseId, RunReporter};
363    use fixture::fixture;
364    use tempfile::tempdir;
365    use test_output_directory::testing::{
366        assert_run_result, ExpectedDirectory, ExpectedSuite, ExpectedTestCase, ExpectedTestRun,
367    };
368
369    fn version_variants<F>(_name: &str, test_fn: F)
370    where
371        F: Fn(SchemaVersion),
372    {
373        for schema in SchemaVersion::all_variants() {
374            test_fn(schema);
375        }
376    }
377
378    #[fixture(version_variants)]
379    #[fuchsia::test]
380    fn no_artifacts(version: SchemaVersion) {
381        let dir = tempdir().expect("create temp directory");
382        const CASE_TIMES: [(ZxTime, ZxTime); 3] = [
383            (ZxTime::from_nanos(0x1100000), ZxTime::from_nanos(0x2100000)),
384            (ZxTime::from_nanos(0x1200000), ZxTime::from_nanos(0x2200000)),
385            (ZxTime::from_nanos(0x1300000), ZxTime::from_nanos(0x2300000)),
386        ];
387        const SUITE_TIMES: (ZxTime, ZxTime) =
388            (ZxTime::from_nanos(0x1000000), ZxTime::from_nanos(0x2400000));
389
390        let run_reporter = RunReporter::new(
391            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
392        );
393        let suite_reporter = run_reporter.new_suite("suite").expect("create suite reporter");
394        suite_reporter.started(Timestamp::Given(SUITE_TIMES.0)).expect("start suite");
395        for case_no in 0..3 {
396            let case_reporter = suite_reporter
397                .new_case(&format!("case-{:?}", case_no), &CaseId(case_no))
398                .expect("create suite reporter");
399            case_reporter
400                .started(Timestamp::Given(CASE_TIMES[case_no as usize].0))
401                .expect("start case");
402            case_reporter.stopped(&ReportedOutcome::Passed, Timestamp::Unknown).expect("stop case");
403        }
404        suite_reporter
405            .stopped(&ReportedOutcome::Failed, Timestamp::Unknown)
406            .expect("set suite outcome");
407        suite_reporter.finished().expect("record suite");
408        run_reporter
409            .stopped(&ReportedOutcome::Timedout, Timestamp::Unknown)
410            .expect("set run outcome");
411        run_reporter.finished().expect("record run");
412
413        assert_run_result(
414            dir.path(),
415            &ExpectedTestRun::new(directory::Outcome::Timedout).with_suite(
416                ExpectedSuite::new("suite", directory::Outcome::Failed)
417                    .with_case(ExpectedTestCase::new("case-0", directory::Outcome::Passed))
418                    .with_case(ExpectedTestCase::new("case-1", directory::Outcome::Passed))
419                    .with_case(ExpectedTestCase::new("case-2", directory::Outcome::Passed)),
420            ),
421        );
422    }
423
424    #[fixture(version_variants)]
425    #[fuchsia::test]
426    fn artifacts_per_entity(version: SchemaVersion) {
427        let dir = tempdir().expect("create temp directory");
428        let run_reporter = RunReporter::new(
429            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
430        );
431        let suite_reporter = run_reporter.new_suite("suite").expect("create new suite");
432        run_reporter.started(Timestamp::Unknown).expect("start run");
433        for case_no in 0..3 {
434            let case_reporter = suite_reporter
435                .new_case(&format!("case-1-{:?}", case_no), &CaseId(case_no))
436                .expect("create new case");
437            case_reporter.started(Timestamp::Unknown).expect("start case");
438            let mut artifact =
439                case_reporter.new_artifact(&ArtifactType::Stdout).expect("create case artifact");
440            writeln!(artifact, "stdout from case {:?}", case_no).expect("write to artifact");
441            case_reporter
442                .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
443                .expect("report case outcome");
444        }
445
446        let mut suite_artifact =
447            suite_reporter.new_artifact(&ArtifactType::Stdout).expect("create suite artifact");
448        writeln!(suite_artifact, "stdout from suite").expect("write to artifact");
449        suite_reporter.started(Timestamp::Unknown).expect("start suite");
450        suite_reporter
451            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
452            .expect("report suite outcome");
453        suite_reporter.finished().expect("record suite");
454        drop(suite_artifact); // want to flush contents
455
456        let mut run_artifact =
457            run_reporter.new_artifact(&ArtifactType::Stdout).expect("create run artifact");
458        writeln!(run_artifact, "stdout from run").expect("write to artifact");
459        run_reporter
460            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
461            .expect("record run outcome");
462        run_reporter.finished().expect("record run");
463        drop(run_artifact); // want to flush contents
464
465        assert_run_result(
466            dir.path(),
467            &ExpectedTestRun::new(directory::Outcome::Passed)
468                .with_artifact(
469                    directory::ArtifactType::Stdout,
470                    STDOUT_FILE.into(),
471                    "stdout from run\n",
472                )
473                .with_suite(
474                    ExpectedSuite::new("suite", directory::Outcome::Passed)
475                        .with_case(
476                            ExpectedTestCase::new("case-1-0", directory::Outcome::Passed)
477                                .with_artifact(
478                                    directory::ArtifactType::Stdout,
479                                    STDOUT_FILE.into(),
480                                    "stdout from case 0\n",
481                                ),
482                        )
483                        .with_case(
484                            ExpectedTestCase::new("case-1-1", directory::Outcome::Passed)
485                                .with_artifact(
486                                    directory::ArtifactType::Stdout,
487                                    STDOUT_FILE.into(),
488                                    "stdout from case 1\n",
489                                ),
490                        )
491                        .with_case(
492                            ExpectedTestCase::new("case-1-2", directory::Outcome::Passed)
493                                .with_artifact(
494                                    directory::ArtifactType::Stdout,
495                                    STDOUT_FILE.into(),
496                                    "stdout from case 2\n",
497                                ),
498                        )
499                        .with_artifact(
500                            directory::ArtifactType::Stdout,
501                            STDOUT_FILE.into(),
502                            "stdout from suite\n",
503                        ),
504                ),
505        );
506    }
507
508    #[fixture(version_variants)]
509    #[fuchsia::test]
510    fn empty_directory_artifacts(version: SchemaVersion) {
511        let dir = tempdir().expect("create temp directory");
512
513        let run_reporter = RunReporter::new(
514            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
515        );
516        run_reporter.started(Timestamp::Unknown).expect("start run");
517        let _run_directory_artifact = run_reporter
518            .new_directory_artifact(&DirectoryArtifactType::Custom, None)
519            .expect("Create run directory artifact");
520
521        let suite_reporter = run_reporter.new_suite("suite").expect("create new suite");
522        suite_reporter.started(Timestamp::Unknown).expect("start suite");
523        let _suite_directory_artifact = suite_reporter
524            .new_directory_artifact(&DirectoryArtifactType::Custom, Some("suite-moniker".into()))
525            .expect("create suite directory artifact");
526
527        let case_reporter =
528            suite_reporter.new_case("case-1-1", &CaseId(1)).expect("create new case");
529        case_reporter.started(Timestamp::Unknown).expect("start case");
530        let _case_directory_artifact = case_reporter
531            .new_directory_artifact(&DirectoryArtifactType::Custom, None)
532            .expect("create suite directory artifact");
533        case_reporter
534            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
535            .expect("report case outcome");
536        case_reporter.finished().expect("Case finished");
537
538        suite_reporter
539            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
540            .expect("report suite outcome");
541        suite_reporter.finished().expect("record suite");
542
543        run_reporter
544            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
545            .expect("record run outcome");
546        run_reporter.finished().expect("record run");
547
548        assert_run_result(
549            dir.path(),
550            &ExpectedTestRun::new(directory::Outcome::Passed)
551                .with_directory_artifact(
552                    directory::ArtifactType::Custom,
553                    Option::<&str>::None,
554                    ExpectedDirectory::new(),
555                )
556                .with_suite(
557                    ExpectedSuite::new("suite", directory::Outcome::Passed)
558                        .with_directory_artifact(
559                            directory::ArtifactMetadata {
560                                artifact_type: directory::ArtifactType::Custom.into(),
561                                component_moniker: Some("suite-moniker".into()),
562                            },
563                            Option::<&str>::None,
564                            ExpectedDirectory::new(),
565                        )
566                        .with_case(
567                            ExpectedTestCase::new("case-1-1", directory::Outcome::Passed)
568                                .with_directory_artifact(
569                                    directory::ArtifactType::Custom,
570                                    Option::<&str>::None,
571                                    ExpectedDirectory::new(),
572                                ),
573                        ),
574                ),
575        );
576    }
577
578    #[fixture(version_variants)]
579    #[fuchsia::test]
580    fn directory_artifacts(version: SchemaVersion) {
581        let dir = tempdir().expect("create temp directory");
582
583        let run_reporter = RunReporter::new(
584            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
585        );
586        run_reporter.started(Timestamp::Unknown).expect("start run");
587        let run_directory_artifact = run_reporter
588            .new_directory_artifact(&DirectoryArtifactType::Custom, None)
589            .expect("Create run directory artifact");
590        let mut run_artifact_file = run_directory_artifact
591            .new_file("run-artifact".as_ref())
592            .expect("Create file in run directory artifact");
593        writeln!(run_artifact_file, "run artifact content").expect("write to run artifact");
594        drop(run_artifact_file); // force flushing
595
596        let suite_reporter = run_reporter.new_suite("suite").expect("create new suite");
597        suite_reporter.started(Timestamp::Unknown).expect("start suite");
598        let suite_directory_artifact = suite_reporter
599            .new_directory_artifact(&DirectoryArtifactType::Custom, Some("suite-moniker".into()))
600            .expect("create suite directory artifact");
601        let mut suite_artifact_file = suite_directory_artifact
602            .new_file("suite-artifact".as_ref())
603            .expect("Create file in suite directory artifact");
604        writeln!(suite_artifact_file, "suite artifact content").expect("write to suite artifact");
605        drop(suite_artifact_file); // force flushing
606
607        let case_reporter =
608            suite_reporter.new_case("case-1-1", &CaseId(1)).expect("create new case");
609        case_reporter.started(Timestamp::Unknown).expect("start case");
610        let case_directory_artifact = case_reporter
611            .new_directory_artifact(&DirectoryArtifactType::Custom, None)
612            .expect("create suite directory artifact");
613        let mut case_artifact_file = case_directory_artifact
614            .new_file("case-artifact".as_ref())
615            .expect("Create file in case directory artifact");
616        writeln!(case_artifact_file, "case artifact content").expect("write to case artifact");
617        drop(case_artifact_file); // force flushing
618        case_reporter
619            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
620            .expect("report case outcome");
621        case_reporter.finished().expect("Case finished");
622
623        suite_reporter
624            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
625            .expect("report suite outcome");
626        suite_reporter.finished().expect("record suite");
627
628        run_reporter
629            .stopped(&ReportedOutcome::Passed, Timestamp::Unknown)
630            .expect("record run outcome");
631        run_reporter.finished().expect("record run");
632
633        assert_run_result(
634            dir.path(),
635            &ExpectedTestRun::new(directory::Outcome::Passed)
636                .with_directory_artifact(
637                    directory::ArtifactType::Custom,
638                    Option::<&str>::None,
639                    ExpectedDirectory::new().with_file("run-artifact", "run artifact content\n"),
640                )
641                .with_suite(
642                    ExpectedSuite::new("suite", directory::Outcome::Passed)
643                        .with_directory_artifact(
644                            directory::ArtifactMetadata {
645                                artifact_type: directory::ArtifactType::Custom.into(),
646                                component_moniker: Some("suite-moniker".into()),
647                            },
648                            Option::<&str>::None,
649                            ExpectedDirectory::new()
650                                .with_file("suite-artifact", "suite artifact content\n"),
651                        )
652                        .with_case(
653                            ExpectedTestCase::new("case-1-1", directory::Outcome::Passed)
654                                .with_directory_artifact(
655                                    directory::ArtifactType::Custom,
656                                    Option::<&str>::None,
657                                    ExpectedDirectory::new()
658                                        .with_file("case-artifact", "case artifact content\n"),
659                                ),
660                        ),
661                ),
662        );
663    }
664
665    #[fixture(version_variants)]
666    #[fuchsia::test]
667    fn ensure_paths_cannot_escape_directory(version: SchemaVersion) {
668        let dir = tempdir().expect("create temp directory");
669
670        let run_reporter = RunReporter::new(
671            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
672        );
673
674        let directory = run_reporter
675            .new_directory_artifact(&DirectoryArtifactType::Custom, None)
676            .expect("make custom directory");
677        assert!(directory.new_file("file.txt".as_ref()).is_ok());
678
679        assert!(directory.new_file("this/is/a/file/path.txt".as_ref()).is_ok());
680
681        assert!(directory.new_file("../file.txt".as_ref()).is_err());
682        assert!(directory.new_file("/file.txt".as_ref()).is_err());
683        assert!(directory.new_file("../this/is/a/file/path.txt".as_ref()).is_err());
684        assert!(directory.new_file("/this/is/a/file/path.txt".as_ref()).is_err());
685        assert!(directory.new_file("/../file.txt".as_ref()).is_err());
686        assert!(directory.new_file("fail/../../file.txt".as_ref()).is_err());
687    }
688
689    #[fixture(version_variants)]
690    #[fuchsia::test]
691    fn early_finish_ok(version: SchemaVersion) {
692        // This test verifies that a suite is saved if finished() is called before an outcome is
693        // reported. This could happen if some error causes test execution to terminate early.
694        let dir = tempdir().expect("create temp directory");
695        let run_reporter = RunReporter::new(
696            DirectoryReporter::new(dir.path().to_path_buf(), version).expect("create run reporter"),
697        );
698
699        run_reporter.started(Timestamp::Unknown).expect("start test run");
700
701        // Add a suite and case that start, but don't stop.
702        let suite_reporter = run_reporter.new_suite("suite").expect("create new suite");
703        suite_reporter.started(Timestamp::Unknown).expect("start suite");
704        let case_reporter = suite_reporter.new_case("case", &CaseId(0)).expect("create case");
705        case_reporter.started(Timestamp::Unknown).expect("start case");
706        // finish run without reporting result
707        case_reporter.finished().expect("finish case");
708        suite_reporter.finished().expect("finish suite");
709
710        // Add a suite that doesn't start.
711        let no_start_suite_reporter =
712            run_reporter.new_suite("no-start-suite").expect("create new suite");
713        no_start_suite_reporter.finished().expect("finish suite");
714
715        run_reporter.finished().expect("finish test run");
716
717        assert_run_result(
718            dir.path(),
719            &ExpectedTestRun::new(directory::Outcome::Inconclusive)
720                .with_suite(
721                    ExpectedSuite::new("suite", directory::Outcome::Inconclusive)
722                        .with_case(ExpectedTestCase::new("case", directory::Outcome::Inconclusive)),
723                )
724                .with_suite(ExpectedSuite::new("no-start-suite", directory::Outcome::NotStarted)),
725        );
726    }
727}