run_test_suite_lib/output/
mod.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 fidl_fuchsia_test_manager as ftest_manager;
6use std::borrow::Borrow;
7use std::io::{Error, Write};
8use std::marker::PhantomData;
9use std::path::Path;
10use test_list::TestTag;
11
12mod directory;
13mod directory_with_stdout;
14mod line;
15mod memory;
16mod mux;
17mod noop;
18mod shell;
19
20pub use directory::{DirectoryReporter, SchemaVersion};
21pub use directory_with_stdout::DirectoryWithStdoutReporter;
22use line::AnsiFilterReporter;
23pub use memory::{InMemoryArtifact, InMemoryDirectoryWriter, InMemoryReporter};
24pub use mux::MultiplexedReporter;
25pub use noop::NoopReporter;
26pub use shell::{ShellReporter, ShellWriterView};
27
28pub(crate) type DynArtifact = dyn 'static + Write + Send + Sync;
29pub(crate) type DynDirectoryArtifact = dyn 'static + DirectoryWrite + Send + Sync;
30pub(crate) type DynReporter = dyn 'static + Reporter + Send + Sync;
31
32pub struct EntityReporter<E, T: Borrow<DynReporter>> {
33    reporter: T,
34    entity: EntityId,
35    _entity_type: std::marker::PhantomData<E>,
36}
37
38/// A reporter for structured results scoped to a test run.
39/// To report results and artifacts for a test run, a user should create a `RunReporter`,
40/// and create child `SuiteReporter`s and `CaseReporter`s beneath it to report results and
41/// artifacts scoped to suites and cases. When the run is finished, the user should call
42/// `record` to ensure the results are persisted.
43///
44/// `RunReporter`, `SuiteReporter`, and `CaseReporter` are wrappers around `Reporter`
45/// implementations that statically ensure that some assumptions made by `Reporter` implemtnations
46/// are upheld (for example, that an entity marked finished at most once).
47pub type RunReporter = EntityReporter<(), Box<DynReporter>>;
48/// A reporter for structured results scoped to a test suite. Note this may not outlive
49/// the `RunReporter` from which it is created.
50pub type SuiteReporter<'a> = EntityReporter<SuiteId, &'a DynReporter>;
51
52/// A reporter for structured results scoped to a single test case. Note this may not outlive
53/// the `SuiteReporter` from which it is created.
54pub type CaseReporter<'a> = EntityReporter<CaseId, &'a DynReporter>;
55
56impl<E, T: Borrow<DynReporter>> EntityReporter<E, T> {
57    /// Create a new artifact scoped to the suite.
58    pub fn new_artifact(&self, artifact_type: &ArtifactType) -> Result<Box<DynArtifact>, Error> {
59        self.reporter.borrow().new_artifact(&self.entity, artifact_type)
60    }
61
62    /// Create a new directory artifact scoped to the suite.
63    pub fn new_directory_artifact(
64        &self,
65        artifact_type: &DirectoryArtifactType,
66        component_moniker: Option<String>,
67    ) -> Result<Box<DynDirectoryArtifact>, Error> {
68        self.reporter.borrow().new_directory_artifact(
69            &self.entity,
70            artifact_type,
71            component_moniker,
72        )
73    }
74
75    /// Record that the suite has started.
76    pub fn started(&self, timestamp: Timestamp) -> Result<(), Error> {
77        self.reporter.borrow().entity_started(&self.entity, timestamp)
78    }
79
80    /// Record the outcome of the suite.
81    pub fn stopped(&self, outcome: &ReportedOutcome, timestamp: Timestamp) -> Result<(), Error> {
82        self.reporter.borrow().entity_stopped(&self.entity, outcome, timestamp)
83    }
84    /// Finalize and persist the test run.
85    pub fn finished(self) -> Result<(), Error> {
86        self.reporter.borrow().entity_finished(&self.entity)
87    }
88}
89
90impl RunReporter {
91    /// Create a `RunReporter`, where stdout, stderr, syslog, and restricted log artifact types
92    /// are sanitized for ANSI escape sequences before being passed along to |reporter|.
93    pub fn new_ansi_filtered<R: 'static + Reporter + Send + Sync>(reporter: R) -> Self {
94        Self::new(AnsiFilterReporter::new(reporter))
95    }
96
97    /// Create a `RunReporter`.
98    pub fn new<R: 'static + Reporter + Send + Sync>(reporter: R) -> Self {
99        let reporter = Box::new(reporter);
100        Self { reporter, entity: EntityId::TestRun, _entity_type: PhantomData }
101    }
102
103    /// Set the number of expected suites for the run.
104    pub fn set_expected_suites(&self, expected_suites: u32) {
105        self.reporter.set_entity_info(
106            &self.entity,
107            &EntityInfo { expected_children: Some(expected_suites), ..EntityInfo::default() },
108        )
109    }
110
111    /// Record a new suite under the test run.
112    pub fn new_suite(&self, url: &str, suite_id: &SuiteId) -> Result<SuiteReporter<'_>, Error> {
113        let entity = EntityId::Suite(*suite_id);
114        self.reporter.new_entity(&entity, url)?;
115        Ok(SuiteReporter { reporter: self.reporter.borrow(), entity, _entity_type: PhantomData })
116    }
117}
118
119impl<'a> SuiteReporter<'a> {
120    /// Record a new suite under the suite.
121    pub fn new_case(&self, name: &str, case_id: &CaseId) -> Result<CaseReporter<'_>, Error> {
122        let suite = match self.entity.clone() {
123            EntityId::Suite(suite) => suite,
124            _ => panic!("Suite reporter should contain a suite"),
125        };
126        let entity = EntityId::Case { suite, case: *case_id };
127        self.reporter.new_entity(&entity, name)?;
128        Ok(CaseReporter { reporter: self.reporter, entity, _entity_type: PhantomData })
129    }
130
131    /// Set the tags for this suite.
132    pub fn set_tags(&self, tags: Vec<TestTag>) {
133        self.reporter.set_entity_info(
134            &self.entity,
135            &EntityInfo { tags: Some(tags), ..EntityInfo::default() },
136        );
137    }
138}
139
140/// An enumeration of different known artifact types.
141#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
142pub enum ArtifactType {
143    Stdout,
144    Stderr,
145    Syslog,
146    RestrictedLog,
147}
148
149/// An enumeration of different known artifact types consisting of multiple files.
150#[derive(Clone, Copy, Debug)]
151pub enum DirectoryArtifactType {
152    /// An arbitrary set of custom files stored by the test in a directory.
153    Custom,
154    /// A collection of debug files. For example: coverage and profiling.
155    Debug,
156}
157
158/// Common outcome type for test results, suites, and test cases.
159#[derive(Clone, Copy, PartialEq, Debug)]
160pub enum ReportedOutcome {
161    Passed,
162    Failed,
163    Inconclusive,
164    Timedout,
165    Error,
166    Skipped,
167    Cancelled,
168    DidNotFinish,
169}
170
171impl std::fmt::Display for ReportedOutcome {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        let repr = match self {
174            Self::Passed => "PASSED",
175            Self::Failed => "FAILED",
176            Self::Inconclusive => "INCONCLUSIVE",
177            Self::Timedout => "TIMED_OUT",
178            Self::Error => "ERROR",
179            Self::Skipped => "SKIPPED",
180            Self::Cancelled => "CANCELLED",
181            Self::DidNotFinish => "DID_NOT_FINISH",
182        };
183        write!(f, "{}", repr)
184    }
185}
186
187impl From<ftest_manager::CaseStatus> for ReportedOutcome {
188    fn from(status: ftest_manager::CaseStatus) -> Self {
189        match status {
190            ftest_manager::CaseStatus::Passed => Self::Passed,
191            ftest_manager::CaseStatus::Failed => Self::Failed,
192            ftest_manager::CaseStatus::TimedOut => Self::Timedout,
193            ftest_manager::CaseStatus::Skipped => Self::Skipped,
194            // Test case 'Error' indicates the test failed to report a result, not internal error.
195            ftest_manager::CaseStatus::Error => Self::DidNotFinish,
196            ftest_manager::CaseStatusUnknown!() => {
197                panic!("unrecognized case status");
198            }
199        }
200    }
201}
202
203impl From<crate::Outcome> for ReportedOutcome {
204    fn from(outcome: crate::Outcome) -> Self {
205        match outcome {
206            crate::Outcome::Passed => Self::Passed,
207            crate::Outcome::Failed => Self::Failed,
208            crate::Outcome::Inconclusive => Self::Inconclusive,
209            crate::Outcome::Cancelled => Self::Cancelled,
210            crate::Outcome::DidNotFinish => Self::DidNotFinish,
211            crate::Outcome::Timedout => Self::Timedout,
212            crate::Outcome::Error { .. } => Self::Error,
213        }
214    }
215}
216
217impl Into<test_output_directory::ArtifactType> for ArtifactType {
218    fn into(self) -> test_output_directory::ArtifactType {
219        match self {
220            Self::Stdout => test_output_directory::ArtifactType::Stdout,
221            Self::Stderr => test_output_directory::ArtifactType::Stderr,
222            Self::Syslog => test_output_directory::ArtifactType::Syslog,
223            Self::RestrictedLog => test_output_directory::ArtifactType::RestrictedLog,
224        }
225    }
226}
227
228impl Into<test_output_directory::ArtifactType> for DirectoryArtifactType {
229    fn into(self) -> test_output_directory::ArtifactType {
230        match self {
231            Self::Custom => test_output_directory::ArtifactType::Custom,
232            Self::Debug => test_output_directory::ArtifactType::Debug,
233        }
234    }
235}
236
237#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
238pub struct SuiteId(pub u32);
239#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
240pub struct CaseId(pub u32);
241
242#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
243pub enum EntityId {
244    TestRun,
245    Suite(SuiteId),
246    Case { suite: SuiteId, case: CaseId },
247}
248
249#[derive(Default)]
250pub struct EntityInfo {
251    pub expected_children: Option<u32>,
252    pub tags: Option<Vec<TestTag>>,
253}
254
255/// A trait for structs that report test results.
256/// Implementations of `Reporter` serve as the backend powering `RunReporter`.
257///
258/// As with `std::io::Write`, `Reporter` implementations are intended to be composable.
259/// An implementation of `Reporter` may contain other `Reporter` implementors and delegate
260/// calls to them.
261pub trait Reporter: Send + Sync {
262    /// Record a new test suite or test case. This should be called once per entity.
263    fn new_entity(&self, entity: &EntityId, name: &str) -> Result<(), Error>;
264
265    /// Add additional info for an entity.
266    fn set_entity_info(&self, entity: &EntityId, info: &EntityInfo);
267
268    /// Record that a test run, suite or case has started.
269    fn entity_started(&self, entity: &EntityId, timestamp: Timestamp) -> Result<(), Error>;
270
271    /// Record that a test run, suite, or case has stopped.
272    fn entity_stopped(
273        &self,
274        entity: &EntityId,
275        outcome: &ReportedOutcome,
276        timestamp: Timestamp,
277    ) -> Result<(), Error>;
278
279    /// Record that a test run, suite, or case has stopped. After this method is called for
280    /// an entity, no additional events or artifacts may be added to the entity.
281    /// Implementations of `Reporter` may assume that `entity_finished` will be called no more
282    /// than once for any entity.
283    fn entity_finished(&self, entity: &EntityId) -> Result<(), Error>;
284
285    /// Create a new artifact scoped to the referenced entity.
286    fn new_artifact(
287        &self,
288        entity: &EntityId,
289        artifact_type: &ArtifactType,
290    ) -> Result<Box<DynArtifact>, Error>;
291
292    /// Create a new artifact consisting of multiple files.
293    fn new_directory_artifact(
294        &self,
295        entity: &EntityId,
296        artifact_type: &DirectoryArtifactType,
297        component_moniker: Option<String>,
298    ) -> Result<Box<DynDirectoryArtifact>, Error>;
299}
300
301/// A trait for writing artifacts that consist of multiple files organized in a directory.
302pub trait DirectoryWrite {
303    /// Create a new file within the directory. `path` must be a relative path with no parent
304    /// segments.
305    fn new_file(&self, path: &Path) -> Result<Box<DynArtifact>, Error>;
306}
307
308/// A wrapper around Fuchsia's representation of time.
309/// This is added as fuchsia-zircon is not available on host.
310#[derive(Clone, Copy)]
311pub struct ZxTime(i64);
312
313impl ZxTime {
314    pub const fn from_nanos(nanos: i64) -> Self {
315        ZxTime(nanos)
316    }
317
318    pub fn checked_sub(&self, rhs: Self) -> Option<std::time::Duration> {
319        let nanos = self.0 - rhs.0;
320        if nanos < 0 {
321            None
322        } else {
323            Some(std::time::Duration::from_nanos(nanos as u64))
324        }
325    }
326}
327
328#[derive(Clone, Copy)]
329pub enum Timestamp {
330    Unknown,
331    Given(ZxTime),
332}
333
334impl Timestamp {
335    pub fn from_nanos(nanos: Option<i64>) -> Self {
336        match nanos {
337            None => Self::Unknown,
338            Some(n) => Self::Given(ZxTime::from_nanos(n)),
339        }
340    }
341}