| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use crate::output::{ |
| noop::NoopDirectoryWriter, ArtifactType, DirectoryArtifactType, DynArtifact, |
| DynDirectoryArtifact, EntityId, EntityInfo, ReportedOutcome, Reporter, Timestamp, |
| }; |
| use crate::trace::duration; |
| use async_trait::async_trait; |
| use fuchsia_async as fasync; |
| use log::error; |
| use parking_lot::Mutex; |
| use std::{ |
| collections::HashMap, |
| io::{Error, Write}, |
| sync::{atomic::AtomicU32, Arc}, |
| time::Duration, |
| }; |
| |
| /// Duration after which to emit an excessive duration log. |
| const EXCESSIVE_DURATION: Duration = Duration::from_secs(60); |
| |
| /// A handle around an inner writer. This serves as a "multiplexing" writer that |
| /// writes bytes from multiple sources into a single serial destination, typically |
| /// to stdout. |
| /// Output sent to a handle is buffered until a newline is encountered, then the |
| /// buffered output is written to the inner writer. |
| /// The handle also supports prepending a prefix to the start of each buffer. This |
| /// helps preserve existing behavior where prefixes are added to the start of stdout |
| /// and log lines to help a developer understand what produced some output. |
| struct ShellWriterHandle<W: 'static + Write + Send + Sync> { |
| inner: Arc<Mutex<ShellWriterHandleInner<W>>>, |
| buffer: Vec<u8>, |
| /// Prefix, if any, to prepend to output before writing to the inner writer. |
| prefix: Option<Vec<u8>>, |
| handle_id: u32, |
| } |
| |
| impl<W: 'static + Write + Send + Sync> ShellWriterHandle<W> { |
| const NEWLINE_BYTE: u8 = b'\n'; |
| const BUFFER_CAPACITY: usize = 1024; |
| |
| /// Create a new handle to a wrapped writer. |
| fn new_handle(inner: Arc<Mutex<ShellWriterHandleInner<W>>>, prefix: Option<String>) -> Self { |
| let mut lock = inner.lock(); |
| let handle_id = lock.num_handles; |
| lock.num_handles += 1; |
| drop(lock); |
| Self { |
| inner, |
| buffer: Vec::with_capacity(Self::BUFFER_CAPACITY), |
| prefix: prefix.map(String::into_bytes), |
| handle_id, |
| } |
| } |
| |
| /// Write a full line to the inner writer. |
| fn write_bufs(writer: &mut W, bufs: &[&[u8]]) -> Result<(), Error> { |
| for buf in bufs { |
| writer.write_all(buf)?; |
| } |
| Ok(()) |
| } |
| } |
| |
| /// Inner mutable state for |ShellWriterHandle|. |
| struct ShellWriterHandleInner<W: 'static + Write + Send + Sync> { |
| /// The writer to which all content is passed. |
| writer: W, |
| /// The id of the last handle that wrote to the writer, used to conditionally |
| /// output a prefix only when the handle writing to the output changes. |
| last_writer_id: Option<u32>, |
| /// The number of handles that have been created. Used to assign ids to handles. |
| num_handles: u32, |
| } |
| |
| impl<W: 'static + Write + Send + Sync> ShellWriterHandleInner<W> { |
| fn new(writer: W) -> Self { |
| Self { writer, last_writer_id: None, num_handles: 0 } |
| } |
| } |
| |
| /// A handle to a writer contained in a |ShellWriterHandle|. This is exposed for testing |
| /// purposes. |
| pub struct ShellWriterView<W: 'static + Write + Send + Sync>(Arc<Mutex<ShellWriterHandleInner<W>>>); |
| |
| impl<W: 'static + Write + Send + Sync> ShellWriterView<W> { |
| pub fn lock(&self) -> parking_lot::MappedMutexGuard<'_, W> { |
| parking_lot::MutexGuard::map(self.0.lock(), |handle_inner| &mut handle_inner.writer) |
| } |
| } |
| |
| impl<W: 'static + Write + Send + Sync> Write for ShellWriterHandle<W> { |
| fn write(&mut self, buf: &[u8]) -> Result<usize, Error> { |
| duration!("shell_write"); |
| // find the last newline in the buffer. In case multiple lines are written as once, |
| // we should write once to the inner writer and add our prefix only once. This helps |
| // avoid spamming the output with prefixes in case many lines are present. |
| let newline_pos = buf |
| .iter() |
| .rev() |
| .position(|byte| *byte == Self::NEWLINE_BYTE) |
| .map(|pos_from_end| buf.len() - pos_from_end - 1); |
| // In case we'd exceed the buffer, just wrte everything, but append a newline to avoid |
| // interspersing. |
| let (final_byte_pos, append_newline) = match newline_pos { |
| // no newline, pushing all to buffer would exceed capacity |
| None if self.buffer.len() + buf.len() > Self::BUFFER_CAPACITY => (buf.len() - 1, true), |
| None => { |
| self.buffer.extend_from_slice(buf); |
| return Ok(buf.len()); |
| } |
| // newline exists, but the rest of buf would exceed capacity. |
| Some(pos) if buf.len() - pos > Self::BUFFER_CAPACITY => (buf.len() - 1, true), |
| Some(pos) => (pos, false), |
| }; |
| |
| let mut inner = self.inner.lock(); |
| let last_writer_id = inner.last_writer_id.replace(self.handle_id); |
| |
| let mut bufs_to_write = vec![]; |
| if let Some(prefix) = self.prefix.as_ref() { |
| if last_writer_id != Some(self.handle_id) { |
| bufs_to_write.push(prefix.as_slice()); |
| } |
| } |
| if !self.buffer.is_empty() { |
| bufs_to_write.push(self.buffer.as_slice()); |
| } |
| bufs_to_write.push(&buf[..final_byte_pos + 1]); |
| if append_newline { |
| bufs_to_write.push(&[Self::NEWLINE_BYTE]); |
| } |
| |
| Self::write_bufs(&mut inner.writer, bufs_to_write.as_slice())?; |
| |
| self.buffer.clear(); |
| self.buffer.extend_from_slice(&buf[final_byte_pos + 1..]); |
| Ok(buf.len()) |
| } |
| |
| fn flush(&mut self) -> Result<(), Error> { |
| let mut inner = self.inner.lock(); |
| let last_writer_id = inner.last_writer_id.replace(self.handle_id); |
| if !self.buffer.is_empty() { |
| self.buffer.push(Self::NEWLINE_BYTE); |
| let mut bufs_to_write = vec![]; |
| if let Some(prefix) = self.prefix.as_ref() { |
| if last_writer_id != Some(self.handle_id) { |
| bufs_to_write.push(prefix.as_slice()); |
| } |
| } |
| bufs_to_write.push(self.buffer.as_slice()); |
| |
| Self::write_bufs(&mut inner.writer, bufs_to_write.as_slice())?; |
| self.buffer.clear(); |
| } |
| inner.writer.flush() |
| } |
| } |
| |
| impl<W: 'static + Write + Send + Sync> std::ops::Drop for ShellWriterHandle<W> { |
| fn drop(&mut self) { |
| let _ = self.flush(); |
| } |
| } |
| |
| /// A reporter that outputs results and artifacts to a single stream, usually stdout. |
| /// This reporter is intended to provide "live" updates to a developer watching while |
| /// tests are executed. |
| pub struct ShellReporter<W: 'static + Write + Send + Sync> { |
| /// Arc around the writer and state, used to dispense more handles. |
| inner: Arc<Mutex<ShellWriterHandleInner<W>>>, |
| /// Map containing known information about each entity. |
| entity_state_map: Mutex<HashMap<EntityId, EntityState>>, |
| /// Number of completed suites, used to output |
| completed_suites: AtomicU32, |
| } |
| |
| /// All known state needed by a |ShellReporter| to display results. |
| struct EntityState { |
| name: String, |
| excessive_duration_task: Option<fasync::Task<()>>, |
| children: Vec<EntityId>, |
| restricted_logs: Option<ShellWriterView<Vec<u8>>>, |
| run_state: EntityRunningState, |
| } |
| |
| enum EntityRunningState { |
| NotRunning, |
| Started, |
| Finished(ReportedOutcome), |
| } |
| |
| impl EntityState { |
| fn new<S: Into<String>>(name: S) -> Self { |
| Self { |
| name: name.into(), |
| excessive_duration_task: None, |
| children: vec![], |
| restricted_logs: None, |
| run_state: EntityRunningState::NotRunning, |
| } |
| } |
| |
| fn name(&self) -> &str { |
| &self.name |
| } |
| } |
| |
| impl ShellReporter<Vec<u8>> { |
| pub fn new_expose_writer_for_test() -> (Self, ShellWriterView<Vec<u8>>) { |
| let inner = Arc::new(Mutex::new(ShellWriterHandleInner::new(vec![]))); |
| let mut entity_state_map = HashMap::new(); |
| entity_state_map.insert(EntityId::TestRun, EntityState::new("TEST RUN")); |
| ( |
| Self { |
| inner: inner.clone(), |
| entity_state_map: Mutex::new(entity_state_map), |
| completed_suites: AtomicU32::new(0), |
| }, |
| ShellWriterView(inner), |
| ) |
| } |
| } |
| |
| impl<W: 'static + Write + Send + Sync> ShellReporter<W> { |
| pub fn new(inner: W) -> Self { |
| let inner = Arc::new(Mutex::new(ShellWriterHandleInner::new(inner))); |
| let mut entity_state_map = HashMap::new(); |
| entity_state_map.insert(EntityId::TestRun, EntityState::new("TEST RUN")); |
| Self { |
| inner, |
| entity_state_map: Mutex::new(entity_state_map), |
| completed_suites: AtomicU32::new(0), |
| } |
| } |
| |
| fn new_writer_handle(&self, prefix: Option<String>) -> Box<DynArtifact> { |
| Box::new(ShellWriterHandle::new_handle(Arc::clone(&self.inner), prefix)) |
| } |
| |
| fn num_known_suites(entity_map: &HashMap<EntityId, EntityState>) -> usize { |
| entity_map.keys().filter(|id| matches!(id, EntityId::Suite(_))).count() |
| } |
| } |
| |
| #[async_trait] |
| impl<W: 'static + Write + Send + Sync> Reporter for ShellReporter<W> { |
| async fn new_entity(&self, entity: &EntityId, name: &str) -> Result<(), Error> { |
| let mut map = self.entity_state_map.lock(); |
| map.insert(entity.clone(), EntityState::new(name)); |
| if let EntityId::Case { suite, .. } = entity { |
| map.get_mut(&EntityId::Suite(*suite)).unwrap().children.push(entity.clone()); |
| } |
| Ok(()) |
| } |
| |
| async fn set_entity_info(&self, _entity: &EntityId, _info: &EntityInfo) {} |
| |
| async fn entity_started(&self, entity: &EntityId, _: Timestamp) -> Result<(), Error> { |
| let mut writer = self.new_writer_handle(None); |
| let mut entity_map_lock = self.entity_state_map.lock(); |
| let mut entity_entry = entity_map_lock.get_mut(entity).unwrap(); |
| entity_entry.run_state = EntityRunningState::Started; |
| let name = entity_entry.name().to_string(); |
| match entity { |
| EntityId::TestRun => (), |
| EntityId::Suite(_) => writeln!(writer, "Running test '{}'", name)?, |
| EntityId::Case { .. } => { |
| writeln!(writer, "[RUNNING]\t{}", name)?; |
| entity_entry.excessive_duration_task = Some(fasync::Task::spawn(async move { |
| fasync::Timer::new(EXCESSIVE_DURATION).await; |
| writeln!( |
| writer, |
| "[duration - {}]:\tStill running after {:?} seconds", |
| name, |
| EXCESSIVE_DURATION.as_secs() |
| ) |
| .unwrap_or_else(|e| error!("Failed to write: {:?}", e)); |
| })); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn entity_stopped( |
| &self, |
| entity: &EntityId, |
| outcome: &ReportedOutcome, |
| _: Timestamp, |
| ) -> Result<(), Error> { |
| let mut writer = self.new_writer_handle(None); |
| let mut entity_map_lock = self.entity_state_map.lock(); |
| entity_map_lock.get_mut(entity).unwrap().run_state = EntityRunningState::Finished(*outcome); |
| let entity_entry = entity_map_lock.get_mut(entity).unwrap(); |
| let name = entity_entry.name().to_string(); |
| |
| // cancel any tasks for reporting excessive duration |
| let _ = entity_entry.excessive_duration_task.take(); |
| match entity { |
| EntityId::TestRun => (), |
| EntityId::Suite(_) => (), |
| EntityId::Case { .. } => { |
| // We don't list error result as it indicates the test didn't finish. |
| if *outcome != ReportedOutcome::Error { |
| writeln!(writer, "[{}]\t{}", outcome, name)?; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn entity_finished(&self, entity: &EntityId) -> Result<(), Error> { |
| let mut writer = self.new_writer_handle(None); |
| let entity_map_lock = self.entity_state_map.lock(); |
| let entity_entry = entity_map_lock.get(entity).unwrap(); |
| let name = entity_entry.name().to_string(); |
| let outcome = match &entity_entry.run_state { |
| EntityRunningState::Finished(outcome) => *outcome, |
| _ => ReportedOutcome::Inconclusive, |
| }; |
| let children: Vec<_> = entity_entry.children.iter().cloned().collect(); |
| match entity { |
| EntityId::TestRun => (), |
| EntityId::Suite(_) => { |
| if matches!(entity_entry.run_state, EntityRunningState::NotRunning) { |
| // no need to output a report if the test wasn't even started. |
| return Ok(()); |
| } |
| |
| let cases: Vec<_> = |
| children.iter().map(|child| entity_map_lock.get(child).unwrap()).collect(); |
| let executed: Vec<_> = cases |
| .iter() |
| .filter(|case| match &case.run_state { |
| EntityRunningState::Finished(_) | EntityRunningState::Started => true, |
| EntityRunningState::NotRunning => false, |
| }) |
| .collect(); |
| let mut failed: Vec<_> = cases |
| .iter() |
| .filter(|case| { |
| matches!( |
| &case.run_state, |
| EntityRunningState::Finished( |
| ReportedOutcome::Failed | ReportedOutcome::Timedout |
| ) |
| ) |
| }) |
| .map(|case| case.name()) |
| .collect(); |
| failed.sort(); |
| let mut not_finished: Vec<_> = cases |
| .iter() |
| .filter(|case| { |
| matches!( |
| &case.run_state, |
| EntityRunningState::Started |
| | EntityRunningState::Finished(ReportedOutcome::Error) |
| ) |
| }) |
| .map(|case| case.name()) |
| .collect(); |
| not_finished.sort(); |
| let num_passed = cases |
| .iter() |
| .filter(|case| { |
| matches!( |
| &case.run_state, |
| EntityRunningState::Finished(ReportedOutcome::Passed) |
| ) |
| }) |
| .count(); |
| |
| let suite_number = |
| self.completed_suites.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| if suite_number > 0 { |
| writeln!( |
| writer, |
| "\nTest run count {}/{}", |
| suite_number + 1, |
| Self::num_known_suites(&*entity_map_lock), |
| )?; |
| } |
| writeln!(writer)?; |
| if !failed.is_empty() { |
| writeln!(writer, "Failed tests: {}", failed.join(", "))?; |
| } |
| if !not_finished.is_empty() { |
| writeln!(writer, "\nThe following test(s) never completed:")?; |
| for t in not_finished { |
| writeln!(writer, "{}", t)?; |
| } |
| } |
| writeln!(writer, "{} out of {} tests passed...", num_passed, executed.len())?; |
| |
| if let Some(restricted_logs) = &entity_entry.restricted_logs { |
| writeln!(writer, "\nTest {} produced unexpected high-severity logs:", &name)?; |
| writeln!(writer, "----------------xxxxx----------------")?; |
| writer.write_all(restricted_logs.lock().as_slice())?; |
| writeln!(writer, "\n----------------xxxxx----------------")?; |
| writeln!(writer, "Failing this test. See: https://fuchsia.dev/fuchsia-src/development/diagnostics/test_and_logs#restricting_log_severity\n")?; |
| } |
| match outcome { |
| ReportedOutcome::Cancelled => { |
| writeln!(writer, "{} was cancelled before completion.", &name)? |
| } |
| ReportedOutcome::DidNotFinish => { |
| writeln!(writer, "{} did not complete successfully.", &name)? |
| } |
| other => writeln!(writer, "{} completed with result: {}", &name, other)?, |
| } |
| } |
| EntityId::Case { .. } => (), |
| } |
| Ok(()) |
| } |
| |
| async fn new_artifact( |
| &self, |
| entity: &EntityId, |
| artifact_type: &ArtifactType, |
| ) -> Result<Box<DynArtifact>, Error> { |
| let mut lock = self.entity_state_map.lock(); |
| let mut entity = lock.get_mut(entity).unwrap(); |
| let name = entity.name(); |
| |
| Ok(match artifact_type { |
| ArtifactType::Stdout => self.new_writer_handle(format!("[stdout - {}]\n", name).into()), |
| ArtifactType::Stderr => self.new_writer_handle(format!("[stderr - {}]\n", name).into()), |
| ArtifactType::Syslog => self.new_writer_handle(None), |
| ArtifactType::RestrictedLog => { |
| // Restricted logs are saved for reporting when the entity completes. |
| let log_buffer = Arc::new(Mutex::new(ShellWriterHandleInner::new(vec![]))); |
| entity.restricted_logs = Some(ShellWriterView(log_buffer.clone())); |
| Box::new(ShellWriterHandle::new_handle(log_buffer, None)) |
| } |
| }) |
| } |
| |
| async fn new_directory_artifact( |
| &self, |
| _entity: &EntityId, |
| _artifact_type: &DirectoryArtifactType, |
| _component_moniker: Option<String>, |
| ) -> Result<Box<DynDirectoryArtifact>, Error> { |
| // For the purpose of live reporting we don't display anything from a directory. |
| Ok(Box::new(NoopDirectoryWriter)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use crate::output::{CaseId, RunReporter, SuiteId}; |
| use std::io::ErrorKind; |
| |
| fn create_writer_inner_and_view( |
| ) -> (Arc<Mutex<ShellWriterHandleInner<Vec<u8>>>>, ShellWriterView<Vec<u8>>) { |
| let inner = Arc::new(Mutex::new(ShellWriterHandleInner::new(vec![]))); |
| (inner.clone(), ShellWriterView(inner)) |
| } |
| |
| #[fuchsia::test] |
| fn single_handle() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut write_handle = ShellWriterHandle::new_handle(handle_inner, None); |
| |
| assert_eq!(write_handle.write(b"hello world").unwrap(), b"hello world".len(),); |
| assert!(output.lock().is_empty()); |
| |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len(),); |
| assert_eq!(output.lock().as_slice(), b"hello world\n"); |
| |
| assert_eq!(write_handle.write(b"flushed output").unwrap(), b"flushed output".len(),); |
| write_handle.flush().unwrap(); |
| assert_eq!(output.lock().as_slice(), b"hello world\nflushed output\n"); |
| } |
| |
| #[fuchsia::test] |
| fn single_handle_with_prefix() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut write_handle = |
| ShellWriterHandle::new_handle(handle_inner, Some("[prefix] ".to_string())); |
| |
| assert_eq!(write_handle.write(b"hello world").unwrap(), b"hello world".len(),); |
| assert!(output.lock().is_empty()); |
| |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len(),); |
| assert_eq!(output.lock().as_slice(), b"[prefix] hello world\n"); |
| |
| assert_eq!(write_handle.write(b"flushed output").unwrap(), b"flushed output".len(),); |
| write_handle.flush().unwrap(); |
| assert_eq!(output.lock().as_slice(), b"[prefix] hello world\nflushed output\n"); |
| } |
| |
| #[fuchsia::test] |
| fn single_handle_multiple_line() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut write_handle = ShellWriterHandle::new_handle(handle_inner, None); |
| const WRITE_BYTES: &[u8] = b"This is a \nmultiline output \nwithout newline termination"; |
| assert_eq!(write_handle.write(WRITE_BYTES).unwrap(), WRITE_BYTES.len(),); |
| assert_eq!(output.lock().as_slice(), b"This is a \nmultiline output \n"); |
| write_handle.flush().unwrap(); |
| assert_eq!( |
| output.lock().as_slice(), |
| b"This is a \nmultiline output \nwithout newline termination\n" |
| ); |
| output.lock().clear(); |
| |
| const TERMINATED_BYTES: &[u8] = b"This is \nnewline terminated \noutput\n"; |
| assert_eq!(write_handle.write(TERMINATED_BYTES).unwrap(), TERMINATED_BYTES.len(),); |
| assert_eq!(output.lock().as_slice(), b"This is \nnewline terminated \noutput\n"); |
| } |
| |
| #[fuchsia::test] |
| fn single_handle_exceed_buffer_in_single_write() { |
| const CAPACITY: usize = ShellWriterHandle::<Vec<u8>>::BUFFER_CAPACITY; |
| // each case consists of a sequence of pairs, where each pair is a string to write, and |
| // the expected output after writing the string. |
| let cases = vec![ |
| ( |
| "exceed in one write", |
| vec![("a".repeat(CAPACITY + 1), format!("{}\n", "a".repeat(CAPACITY + 1)))], |
| ), |
| ( |
| "exceed on second write", |
| vec![ |
| ("a".to_string(), "".to_string()), |
| ("a".repeat(CAPACITY + 1), format!("{}\n", "a".repeat(CAPACITY + 2))), |
| ], |
| ), |
| ( |
| "exceed in one write, with newline", |
| vec![( |
| format!("\n{}", "a".repeat(CAPACITY + 1)), |
| format!("\n{}\n", "a".repeat(CAPACITY + 1)), |
| )], |
| ), |
| ( |
| "exceed in two writes, with newline", |
| vec![ |
| ("a".to_string(), "".to_string()), |
| ( |
| format!("\n{}", "a".repeat(CAPACITY + 1)), |
| format!("a\n{}\n", "a".repeat(CAPACITY + 1)), |
| ), |
| ], |
| ), |
| ]; |
| |
| for (case_name, writes) in cases.into_iter() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut write_handle = ShellWriterHandle::new_handle(handle_inner, None); |
| for (write_no, (to_write, expected)) in writes.into_iter().enumerate() { |
| assert_eq!( |
| write_handle.write(to_write.as_bytes()).unwrap(), |
| to_write.as_bytes().len(), |
| "Got wrong number of bytes written for write {:?} in case {}", |
| write_no, |
| case_name |
| ); |
| assert_eq!( |
| String::from_utf8(output.lock().clone()).unwrap(), |
| expected, |
| "Buffer contains unexpected contents after write {:?} in case {}", |
| write_no, |
| case_name |
| ) |
| } |
| } |
| } |
| |
| #[fuchsia::test] |
| fn single_handle_with_prefix_multiple_line() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut write_handle = |
| ShellWriterHandle::new_handle(handle_inner, Some("[prefix] ".to_string())); |
| const WRITE_BYTES: &[u8] = b"This is a \nmultiline output \nwithout newline termination"; |
| assert_eq!(write_handle.write(WRITE_BYTES).unwrap(), WRITE_BYTES.len(),); |
| // Note we 'chunk' output in each write to avoid spamming the prefix, so the second |
| // line won't contain the prefix. |
| assert_eq!(output.lock().as_slice(), b"[prefix] This is a \nmultiline output \n"); |
| write_handle.flush().unwrap(); |
| assert_eq!( |
| output.lock().as_slice(), |
| "[prefix] This is a \nmultiline output \nwithout newline termination\n".as_bytes() |
| ); |
| |
| const TERMINATED_BYTES: &[u8] = b"This is \nnewline terminated \noutput\n"; |
| assert_eq!(write_handle.write(TERMINATED_BYTES).unwrap(), TERMINATED_BYTES.len(),); |
| assert_eq!( |
| output.lock().as_slice(), |
| b"[prefix] This is a \nmultiline output \n\ |
| without newline termination\nThis is \nnewline terminated \noutput\n" |
| ); |
| } |
| |
| #[fuchsia::test] |
| fn multiple_handles() { |
| let (handle_inner, output) = create_writer_inner_and_view(); |
| let mut handle_1 = |
| ShellWriterHandle::new_handle(handle_inner.clone(), Some("[1] ".to_string())); |
| let mut handle_2 = ShellWriterHandle::new_handle(handle_inner, Some("[2] ".to_string())); |
| |
| write!(handle_1, "hi from 1").unwrap(); |
| write!(handle_2, "hi from 2").unwrap(); |
| assert!(output.lock().is_empty()); |
| write!(handle_1, "\n").unwrap(); |
| assert_eq!(output.lock().as_slice(), "[1] hi from 1\n".as_bytes()); |
| write!(handle_2, "\n").unwrap(); |
| assert_eq!(output.lock().as_slice(), "[1] hi from 1\n[2] hi from 2\n".as_bytes()); |
| } |
| |
| // The following tests verify behavior of the shell writer when the inner writer |
| // exhibits some allowed edge cases. |
| |
| #[fuchsia::test] |
| fn output_written_when_inner_writer_writes_partial_buffer() { |
| /// A writer that writes at most 3 bytes at a time. |
| struct PartialOutputWriter(Vec<u8>); |
| impl Write for PartialOutputWriter { |
| fn write(&mut self, buf: &[u8]) -> Result<usize, Error> { |
| if buf.len() >= 3 { |
| self.0.write(&buf[..3]) |
| } else { |
| self.0.write(buf) |
| } |
| } |
| |
| fn flush(&mut self) -> Result<(), Error> { |
| self.0.flush() |
| } |
| } |
| |
| let inner = Arc::new(Mutex::new(ShellWriterHandleInner::new(PartialOutputWriter(vec![])))); |
| let output = ShellWriterView(inner.clone()); |
| let mut write_handle = |
| ShellWriterHandle::new_handle(inner.clone(), Some("[prefix] ".to_string())); |
| assert_eq!(write_handle.write(b"hello").unwrap(), b"hello".len()); |
| assert!(output.lock().0.is_empty()); |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().0.as_slice(), b"[prefix] hello\n"); |
| |
| let mut write_handle_2 = |
| ShellWriterHandle::new_handle(inner, Some("[prefix2] ".to_string())); |
| |
| assert_eq!(write_handle.write(b"hello").unwrap(), b"hello".len()); |
| assert_eq!(write_handle_2.write(b"world").unwrap(), b"world".len()); |
| assert_eq!(output.lock().0.as_slice(), b"[prefix] hello\n"); |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().0.as_slice(), b"[prefix] hello\nhello\n"); |
| assert_eq!(write_handle_2.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().0.as_slice(), b"[prefix] hello\nhello\n[prefix2] world\n"); |
| } |
| |
| #[fuchsia::test] |
| fn output_written_when_inner_writer_returns_interrupted() { |
| /// A writer that returns interrupted on the first write attempt |
| struct InterruptWriter { |
| buf: Vec<u8>, |
| returned_interrupt: bool, |
| } |
| impl Write for InterruptWriter { |
| fn write(&mut self, buf: &[u8]) -> Result<usize, Error> { |
| if !self.returned_interrupt { |
| self.returned_interrupt = true; |
| Err(ErrorKind::Interrupted.into()) |
| } else { |
| self.buf.write(buf) |
| } |
| } |
| |
| fn flush(&mut self) -> Result<(), Error> { |
| self.buf.flush() |
| } |
| } |
| |
| let inner = Arc::new(Mutex::new(ShellWriterHandleInner::new(InterruptWriter { |
| buf: vec![], |
| returned_interrupt: false, |
| }))); |
| let output = ShellWriterView(inner.clone()); |
| let mut write_handle = |
| ShellWriterHandle::new_handle(inner.clone(), Some("[prefix] ".to_string())); |
| assert_eq!(write_handle.write(b"hello").unwrap(), b"hello".len()); |
| assert!(output.lock().buf.is_empty()); |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().buf.as_slice(), b"[prefix] hello\n"); |
| |
| let mut write_handle_2 = |
| ShellWriterHandle::new_handle(inner.clone(), Some("[prefix2] ".to_string())); |
| |
| assert_eq!(write_handle.write(b"hello").unwrap(), b"hello".len()); |
| assert_eq!(write_handle_2.write(b"world").unwrap(), b"world".len()); |
| assert_eq!(output.lock().buf.as_slice(), b"[prefix] hello\n"); |
| assert_eq!(write_handle.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().buf.as_slice(), b"[prefix] hello\nhello\n"); |
| assert_eq!(write_handle_2.write(b"\n").unwrap(), b"\n".len()); |
| assert_eq!(output.lock().buf.as_slice(), b"[prefix] hello\nhello\n[prefix2] world\n"); |
| } |
| |
| #[fuchsia::test] |
| async fn report_case_events() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| let case_1_reporter = |
| suite_reporter.new_case("case-1", &CaseId(0)).await.expect("create case"); |
| let case_2_reporter = |
| suite_reporter.new_case("case-2", &CaseId(1)).await.expect("create case"); |
| |
| case_1_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| expected.push_str("[RUNNING]\tcase-1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_2_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| expected.push_str("[RUNNING]\tcase-2\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_1_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| expected.push_str("[PASSED]\tcase-1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_2_reporter |
| .stopped(&ReportedOutcome::Failed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| expected.push_str("[FAILED]\tcase-2\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_1_reporter.finished().await.expect("finish case"); |
| case_2_reporter.finished().await.expect("finish case"); |
| suite_reporter |
| .stopped(&ReportedOutcome::Failed, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| suite_reporter.finished().await.expect("finish suite"); |
| |
| expected.push_str("\n"); |
| expected.push_str("Failed tests: case-2\n"); |
| expected.push_str("1 out of 2 tests passed...\n"); |
| expected.push_str("test-suite completed with result: FAILED\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn syslog_artifacts() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| let mut syslog_writer = |
| suite_reporter.new_artifact(&ArtifactType::Syslog).await.expect("create syslog"); |
| |
| writeln!(syslog_writer, "[log] test syslog").expect("write to syslog"); |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[log] test syslog\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected); |
| |
| suite_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| writeln!(syslog_writer, "[log] more test syslog").expect("write to syslog"); |
| expected.push_str("[log] more test syslog\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected); |
| |
| suite_reporter.finished().await.expect("finish suite"); |
| expected.push_str("\n"); |
| expected.push_str("0 out of 0 tests passed...\n"); |
| expected.push_str("test-suite completed with result: PASSED\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn report_retricted_logs() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| |
| let case_reporter = |
| suite_reporter.new_case("case-0", &CaseId(0)).await.expect("create case"); |
| case_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| case_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| case_reporter.finished().await.expect("finish case"); |
| |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[RUNNING]\tcase-0\n"); |
| expected.push_str("[PASSED]\tcase-0\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| suite_reporter |
| .stopped(&ReportedOutcome::Failed, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| let mut restricted_log = suite_reporter |
| .new_artifact(&ArtifactType::RestrictedLog) |
| .await |
| .expect("create restricted log"); |
| write!(restricted_log, "suite restricted log").expect("write to restricted log"); |
| drop(restricted_log); |
| |
| suite_reporter.finished().await.expect("finish suite"); |
| expected.push_str("\n"); |
| expected.push_str("1 out of 1 tests passed...\n"); |
| expected.push_str("\nTest test-suite produced unexpected high-severity logs:\n"); |
| expected.push_str("----------------xxxxx----------------\n"); |
| expected.push_str("suite restricted log\n\n"); |
| expected.push_str("----------------xxxxx----------------\n"); |
| expected.push_str("Failing this test. See: https://fuchsia.dev/fuchsia-src/development/diagnostics/test_and_logs#restricting_log_severity\n"); |
| expected.push_str("\ntest-suite completed with result: FAILED\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn stdout_artifacts() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| |
| let case_0_reporter = |
| suite_reporter.new_case("case-0", &CaseId(0)).await.expect("create case"); |
| let case_1_reporter = |
| suite_reporter.new_case("case-1", &CaseId(1)).await.expect("create case"); |
| case_0_reporter.started(Timestamp::Unknown).await.expect("start case"); |
| case_1_reporter.started(Timestamp::Unknown).await.expect("start case"); |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[RUNNING]\tcase-0\n[RUNNING]\tcase-1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| let mut case_0_stdout = |
| case_0_reporter.new_artifact(&ArtifactType::Stdout).await.expect("create artifact"); |
| let mut case_1_stdout = |
| case_1_reporter.new_artifact(&ArtifactType::Stdout).await.expect("create artifact"); |
| |
| writeln!(case_0_stdout, "stdout from case 0").expect("write to stdout"); |
| writeln!(case_1_stdout, "stdout from case 1").expect("write to stdout"); |
| |
| expected.push_str("[stdout - case-0]\nstdout from case 0\n"); |
| expected.push_str("[stdout - case-1]\nstdout from case 1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_0_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| case_1_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| expected.push_str("[PASSED]\tcase-0\n"); |
| expected.push_str("[PASSED]\tcase-1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| case_0_reporter.finished().await.expect("finish case"); |
| case_1_reporter.finished().await.expect("finish case"); |
| suite_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| |
| suite_reporter.finished().await.expect("finish suite"); |
| expected.push_str("\n"); |
| expected.push_str("2 out of 2 tests passed...\n"); |
| expected.push_str("test-suite completed with result: PASSED\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn report_unfinished() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("suite started"); |
| |
| let case_reporter = |
| suite_reporter.new_case("case-0", &CaseId(0)).await.expect("create case"); |
| case_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| case_reporter |
| .stopped(&ReportedOutcome::Passed, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| case_reporter.finished().await.expect("finish case"); |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[RUNNING]\tcase-0\n[PASSED]\tcase-0\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| // add a case that finishes with ERROR |
| let unfinished_case_1 = |
| suite_reporter.new_case("case-1", &CaseId(1)).await.expect("create case"); |
| unfinished_case_1.started(Timestamp::Unknown).await.expect("case started"); |
| unfinished_case_1 |
| .stopped(&ReportedOutcome::Error, Timestamp::Unknown) |
| .await |
| .expect("stop case"); |
| unfinished_case_1.finished().await.expect("finish case"); |
| expected.push_str("[RUNNING]\tcase-1\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| // add a case that doesn't finish at all |
| let unfinished_case_2 = |
| suite_reporter.new_case("case-2", &CaseId(2)).await.expect("create case"); |
| unfinished_case_2.started(Timestamp::Unknown).await.expect("case started"); |
| unfinished_case_2.finished().await.expect("finish case"); |
| expected.push_str("[RUNNING]\tcase-2\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| |
| suite_reporter |
| .stopped(&ReportedOutcome::Failed, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| suite_reporter.finished().await.expect("finish suite"); |
| expected.push_str("\n"); |
| expected.push_str("\nThe following test(s) never completed:\n"); |
| expected.push_str("case-1\n"); |
| expected.push_str("case-2\n"); |
| expected.push_str("1 out of 3 tests passed...\n"); |
| expected.push_str("test-suite completed with result: FAILED\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn report_cancelled_suite() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("suite started"); |
| |
| let case_reporter = |
| suite_reporter.new_case("case", &CaseId(0)).await.expect("create new case"); |
| case_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| case_reporter.finished().await.expect("case finished"); |
| suite_reporter |
| .stopped(&ReportedOutcome::Cancelled, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| suite_reporter.finished().await.expect("case finished"); |
| |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[RUNNING]\tcase\n"); |
| expected.push_str("\n"); |
| expected.push_str("\nThe following test(s) never completed:\n"); |
| expected.push_str("case\n"); |
| expected.push_str("0 out of 1 tests passed...\n"); |
| expected.push_str("test-suite was cancelled before completion.\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| |
| #[fuchsia::test] |
| async fn report_suite_did_not_finish() { |
| let (shell_reporter, output) = ShellReporter::new_expose_writer_for_test(); |
| let run_reporter = RunReporter::new_for_test(shell_reporter); |
| let suite_reporter = |
| run_reporter.new_suite("test-suite", &SuiteId(0)).await.expect("create suite"); |
| suite_reporter.started(Timestamp::Unknown).await.expect("suite started"); |
| |
| let case_reporter = |
| suite_reporter.new_case("case", &CaseId(0)).await.expect("create new case"); |
| case_reporter.started(Timestamp::Unknown).await.expect("case started"); |
| case_reporter.finished().await.expect("case finished"); |
| suite_reporter |
| .stopped(&ReportedOutcome::DidNotFinish, Timestamp::Unknown) |
| .await |
| .expect("stop suite"); |
| suite_reporter.finished().await.expect("case finished"); |
| |
| let mut expected = "Running test 'test-suite'\n".to_string(); |
| expected.push_str("[RUNNING]\tcase\n"); |
| expected.push_str("\n"); |
| expected.push_str("\nThe following test(s) never completed:\n"); |
| expected.push_str("case\n"); |
| expected.push_str("0 out of 1 tests passed...\n"); |
| expected.push_str("test-suite did not complete successfully.\n"); |
| assert_eq!(String::from_utf8(output.lock().clone()).unwrap(), expected,); |
| } |
| } |