1use crate::cancel::{Cancelled, NamedFutureExt, OrCancel};
6use crate::connector::SuiteRunnerConnector;
7use crate::diagnostics::{self, LogDisplayConfiguration};
8use crate::outcome::{Outcome, RunTestSuiteError};
9use crate::output::{self, RunReporter, Timestamp};
10use crate::params::{RunParams, TestParams, TimeoutBehavior};
11use crate::running_suite::{RunningSuite, WaitForStartArgs, run_suite_and_collect_logs};
12use diagnostics_data::LogTextDisplayOptions;
13use flex_client::ProxyHasDomain;
14use flex_fuchsia_test_manager::{self as ftest_manager, SuiteRunnerProxy};
15use futures::prelude::*;
16use log::warn;
17use std::io::Write;
18use std::path::PathBuf;
19
20struct RunState<'a> {
21 run_params: &'a RunParams,
22 final_outcome: Option<Outcome>,
23 failed_suites: u32,
24 timeout_occurred: bool,
25 cancel_occurred: bool,
26 internal_error_occurred: bool,
27}
28
29impl<'a> RunState<'a> {
30 fn new(run_params: &'a RunParams) -> Self {
31 Self {
32 run_params,
33 final_outcome: None,
34 failed_suites: 0,
35 timeout_occurred: false,
36 cancel_occurred: false,
37 internal_error_occurred: false,
38 }
39 }
40
41 fn cancel_run(&mut self, final_outcome: Outcome) {
42 self.final_outcome = Some(final_outcome);
43 self.cancel_occurred = true;
44 }
45
46 fn record_next_outcome(&mut self, next_outcome: Outcome) {
47 if next_outcome != Outcome::Passed {
48 self.failed_suites += 1;
49 }
50 match &next_outcome {
51 Outcome::Timedout => self.timeout_occurred = true,
52 Outcome::Cancelled => self.cancel_occurred = true,
53 Outcome::Error { origin } if origin.is_internal_error() => {
54 self.internal_error_occurred = true;
55 }
56 Outcome::Passed
57 | Outcome::Failed
58 | Outcome::Inconclusive
59 | Outcome::DidNotFinish
60 | Outcome::Error { .. } => (),
61 }
62
63 self.final_outcome = match (self.final_outcome.take(), next_outcome) {
64 (None, first_outcome) => Some(first_outcome),
65 (Some(outcome), Outcome::Passed) => Some(outcome),
66 (Some(_), failing_outcome) => Some(failing_outcome),
67 };
68 }
69
70 fn should_stop_run(&mut self) -> bool {
71 let stop_due_to_timeout = self.run_params.timeout_behavior
72 == TimeoutBehavior::TerminateRemaining
73 && self.timeout_occurred;
74 let stop_due_to_failures = match self.run_params.stop_after_failures.as_ref() {
75 Some(threshold) => self.failed_suites >= threshold.get(),
76 None => false,
77 };
78 stop_due_to_timeout
79 || stop_due_to_failures
80 || self.cancel_occurred
81 || self.internal_error_occurred
82 }
83
84 fn final_outcome(self) -> Outcome {
85 self.final_outcome.unwrap_or(Outcome::Passed)
86 }
87}
88
89async fn run_test_chunk<'a, F: 'a + Future<Output = ()> + Unpin>(
93 runner_proxy: SuiteRunnerProxy,
94 test_params: TestParams,
95 run_state: &'a mut RunState<'_>,
96 run_params: &'a RunParams,
97 run_reporter: &'a RunReporter,
98 cancel_fut: F,
99) -> Result<(), RunTestSuiteError> {
100 let timeout = test_params
101 .timeout_seconds
102 .map(|seconds| std::time::Duration::from_secs(seconds.get() as u64));
103
104 let mut combined_log_interest = run_params.min_severity_logs.clone();
107 combined_log_interest.extend(test_params.min_severity_logs.iter().cloned());
108
109 let mut run_options = flex_fuchsia_test_manager::RunSuiteOptions {
110 max_concurrent_test_case_runs: test_params.parallel,
111 arguments: Some(test_params.test_args),
112 timeout: timeout.map(|duration| duration.as_nanos() as i64),
113 run_disabled_tests: Some(test_params.also_run_disabled_tests),
114 test_case_filters: test_params.test_filters,
115 break_on_failure: Some(test_params.break_on_failure),
116 logs_iterator_type: Some(
117 run_params.log_protocol.unwrap_or_else(diagnostics::get_logs_iterator_type),
118 ),
119 log_interest: Some(combined_log_interest),
120 no_exception_channel: Some(test_params.no_exception_channel),
121 ..Default::default()
122 };
123 let suite = run_reporter.new_suite(&test_params.test_url)?;
124 suite.set_tags(test_params.tags);
125 if let Some(realm) = test_params.realm.as_ref() {
126 run_options.realm_options = Some(flex_fuchsia_test_manager::RealmOptions {
127 realm: Some(realm.get_realm_client()?),
128 offers: Some(realm.offers()),
129 test_collection: Some(realm.collection().to_string()),
130 ..Default::default()
131 });
132 }
133 let (suite_controller, suite_server_end) =
134 runner_proxy.domain().create_proxy::<ftest_manager::SuiteControllerMarker>();
135 let suite_start_fut = RunningSuite::wait_for_start(WaitForStartArgs {
136 proxy: suite_controller,
137 max_severity_logs: test_params.max_severity_logs,
138 timeout,
139 timeout_grace: std::time::Duration::from_secs(run_params.timeout_grace_seconds as u64),
140 max_pipelined: None,
141 no_cases_equals_success: test_params.no_cases_equals_success,
142 });
143
144 runner_proxy.run(&test_params.test_url, run_options, suite_server_end)?;
145
146 let cancel_fut = cancel_fut.shared();
147
148 let handle_suite_fut = async move {
149 'block: {
151 let suite_stop_fut = cancel_fut.clone().map(|_| Outcome::Cancelled);
152
153 let running_suite =
154 match suite_start_fut.named("suite_start").or_cancelled(suite_stop_fut).await {
155 Ok(running_suite) => running_suite,
156 Err(Cancelled(final_outcome)) => {
157 run_state.cancel_run(final_outcome);
158 break 'block;
159 }
160 };
161
162 let log_display = LogDisplayConfiguration {
163 interest: run_params.min_severity_logs.clone(),
164 text_options: LogTextDisplayOptions {
165 show_full_moniker: run_params.show_full_moniker,
166 ..Default::default()
167 },
168 };
169
170 let result =
171 run_suite_and_collect_logs(running_suite, &suite, log_display, cancel_fut.clone())
172 .await;
173 let suite_outcome = result.unwrap_or_else(|err| Outcome::error(err));
174 suite.finished()?;
176 run_state.record_next_outcome(suite_outcome);
177 if run_state.should_stop_run() {
178 break 'block;
179 }
180 }
181 Result::<_, RunTestSuiteError>::Ok(())
182 };
183
184 handle_suite_fut.boxed_local().await.map_err(|e| e.into())
185}
186
187async fn run_tests<'a, F: 'a + Future<Output = ()> + Unpin>(
188 connector: impl SuiteRunnerConnector,
189 test_params: TestParams,
190 run_params: RunParams,
191 run_reporter: &'a RunReporter,
192 cancel_fut: F,
193) -> Result<Outcome, RunTestSuiteError> {
194 let mut run_state = RunState::new(&run_params);
195 let cancel_fut = cancel_fut.shared();
196 match run_state.should_stop_run() {
197 true => {
198 let suite_reporter = run_reporter.new_suite(&test_params.test_url)?;
201 suite_reporter.set_tags(test_params.tags);
202 suite_reporter.finished()?;
203 }
204 false => {
205 let runner_proxy = connector.connect().await?;
206 run_test_chunk(
207 runner_proxy,
208 test_params,
209 &mut run_state,
210 &run_params,
211 run_reporter,
212 cancel_fut.clone(),
213 )
214 .await?;
215 }
216 }
217
218 Ok(run_state.final_outcome())
219}
220
221pub async fn run_test_and_get_outcome<F>(
232 connector: impl SuiteRunnerConnector,
233 test_params: TestParams,
234 run_params: RunParams,
235 run_reporter: RunReporter,
236 cancel_fut: F,
237) -> Outcome
238where
239 F: Future<Output = ()>,
240{
241 match run_reporter.started(Timestamp::Unknown) {
242 Ok(()) => (),
243 Err(e) => return Outcome::error(e),
244 }
245 let test_outcome = match run_tests(
246 connector,
247 test_params,
248 run_params,
249 &run_reporter,
250 cancel_fut.boxed_local(),
251 )
252 .await
253 {
254 Ok(s) => s,
255 Err(e) => {
256 return Outcome::error(e);
257 }
258 };
259
260 let report_result = match run_reporter.stopped(&test_outcome.clone().into(), Timestamp::Unknown)
261 {
262 Ok(()) => run_reporter.finished(),
263 Err(e) => Err(e),
264 };
265 if let Err(e) = report_result {
266 warn!("Failed to record results: {:?}", e);
267 }
268
269 test_outcome
270}
271
272pub struct DirectoryReporterOptions {
273 pub root_path: PathBuf,
275}
276
277pub fn create_reporter<W: 'static + Write + Send + Sync>(
279 filter_ansi: bool,
280 dir: Option<DirectoryReporterOptions>,
281 writer: W,
282) -> Result<output::RunReporter, anyhow::Error> {
283 let stdout_reporter = output::ShellReporter::new(writer);
284 let dir_reporter = dir
285 .map(|dir| {
286 output::DirectoryWithStdoutReporter::new(dir.root_path, output::SchemaVersion::V1)
287 })
288 .transpose()?;
289 let reporter = match (dir_reporter, filter_ansi) {
290 (Some(dir_reporter), false) => output::RunReporter::new(output::MultiplexedReporter::new(
291 stdout_reporter,
292 dir_reporter,
293 )),
294 (Some(dir_reporter), true) => output::RunReporter::new_ansi_filtered(
295 output::MultiplexedReporter::new(stdout_reporter, dir_reporter),
296 ),
297 (None, false) => output::RunReporter::new(stdout_reporter),
298 (None, true) => output::RunReporter::new_ansi_filtered(stdout_reporter),
299 };
300 Ok(reporter)
301}
302
303#[cfg(test)]
304mod test {
305 use super::*;
306 use crate::connector::SingleRunConnector;
307 use crate::output::{EntityId, InMemoryReporter};
308 use assert_matches::assert_matches;
309 use fidl::endpoints::{Proxy, create_proxy_and_stream};
310 use flex_fuchsia_test_manager as ftest_manager;
311 use futures::future::join;
312 use futures::stream::futures_unordered::FuturesUnordered;
313 use maplit::hashmap;
314 use std::collections::HashMap;
315 #[cfg(target_os = "fuchsia")]
316 use {
317 flex_fuchsia_io as fio, futures::future::join3, vfs::file::vmo::read_only,
318 vfs::pseudo_directory, zx,
319 };
320
321 async fn fake_running_all_suites(
324 mut stream: ftest_manager::SuiteRunnerRequestStream,
325 mut suite_events: HashMap<&str, Vec<ftest_manager::Event>>,
326 ) {
327 let mut suite_streams = vec![];
328
329 if let Ok(Some(req)) = stream.try_next().await {
330 match req {
331 ftest_manager::SuiteRunnerRequest::Run { test_suite_url, controller, .. } => {
332 let events = suite_events
333 .remove(test_suite_url.as_str())
334 .expect("Got a request for an unexpected test URL");
335 suite_streams.push((controller.into_stream(), events));
336 }
337 ftest_manager::SuiteRunnerRequest::_UnknownMethod { ordinal, .. } => {
338 panic!("Not expecting unknown request: {}", ordinal)
339 }
340 }
341 }
342 assert!(suite_events.is_empty(), "Expected AddSuite to be called for all specified suites");
343
344 let mut suite_streams = suite_streams
346 .into_iter()
347 .map(|(mut stream, events)| {
348 async move {
349 let mut maybe_events = Some(events);
350 while let Ok(Some(req)) = stream.try_next().await {
351 match req {
352 ftest_manager::SuiteControllerRequest::WatchEvents {
353 responder,
354 ..
355 } => {
356 let send_events = maybe_events.take().unwrap_or(vec![]);
357 let _ = responder.send(Ok(send_events));
358 }
359 _ => {
360 }
362 }
363 }
364 }
365 })
366 .collect::<FuturesUnordered<_>>();
367
368 async move { while let Some(_) = suite_streams.next().await {} }.await;
369 }
370
371 struct ParamsForRunTests {
372 runner_proxy: ftest_manager::SuiteRunnerProxy,
373 test_params: TestParams,
374 run_reporter: RunReporter,
375 }
376
377 fn create_empty_suite_events() -> Vec<ftest_manager::Event> {
378 vec![
379 ftest_manager::Event {
380 timestamp: Some(1000),
381 details: Some(ftest_manager::EventDetails::SuiteStarted(
382 ftest_manager::SuiteStartedEventDetails { ..Default::default() },
383 )),
384 ..Default::default()
385 },
386 ftest_manager::Event {
387 timestamp: Some(2000),
388 details: Some(ftest_manager::EventDetails::SuiteStopped(
389 ftest_manager::SuiteStoppedEventDetails {
390 result: Some(ftest_manager::SuiteResult::Finished),
391 ..Default::default()
392 },
393 )),
394 ..Default::default()
395 },
396 ]
397 }
398
399 async fn call_run_tests(params: ParamsForRunTests) -> Outcome {
400 run_test_and_get_outcome(
401 SingleRunConnector::new(params.runner_proxy),
402 params.test_params,
403 RunParams {
404 timeout_behavior: TimeoutBehavior::Continue,
405 timeout_grace_seconds: 0,
406 stop_after_failures: None,
407 accumulate_debug_data: false,
408 log_protocol: None,
409 min_severity_logs: vec![],
410 show_full_moniker: false,
411 },
412 params.run_reporter,
413 futures::future::pending(),
414 )
415 .await
416 }
417
418 #[fuchsia::test]
419 async fn single_run_no_events() {
420 let (runner_proxy, suite_runner_stream) =
421 create_proxy_and_stream::<ftest_manager::SuiteRunnerMarker>();
422
423 let reporter = InMemoryReporter::new();
424 let run_reporter = RunReporter::new(reporter.clone());
425 let run_fut = call_run_tests(ParamsForRunTests {
426 runner_proxy,
427 test_params: TestParams {
428 test_url: "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm".to_string(),
429 ..TestParams::default()
430 },
431 run_reporter,
432 });
433 let fake_fut = fake_running_all_suites(
434 suite_runner_stream,
435 hashmap! {
436 "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm" => create_empty_suite_events()
437 },
438 );
439
440 assert_eq!(join(run_fut, fake_fut).await.0, Outcome::Passed,);
441
442 let reports = reporter.get_reports();
443 assert_eq!(2usize, reports.len());
444 assert!(reports[0].report.artifacts.is_empty());
445 assert!(reports[0].report.directories.is_empty());
446 assert!(reports[1].report.artifacts.is_empty());
447 assert!(reports[1].report.directories.is_empty());
448 }
449
450 #[cfg(target_os = "fuchsia")]
451 #[fuchsia::test]
452 async fn single_run_custom_directory() {
453 let (runner_proxy, suite_runner_stream) =
454 create_proxy_and_stream::<ftest_manager::SuiteRunnerMarker>();
455
456 let reporter = InMemoryReporter::new();
457 let run_reporter = RunReporter::new(reporter.clone());
458 let run_fut = call_run_tests(ParamsForRunTests {
459 runner_proxy,
460 test_params: TestParams {
461 test_url: "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm".to_string(),
462 ..TestParams::default()
463 },
464 run_reporter,
465 });
466
467 let dir = pseudo_directory! {
468 "test_file.txt" => read_only("Hello, World!"),
469 };
470
471 let directory_proxy = vfs::directory::serve(dir, fio::PERM_READABLE | fio::PERM_WRITABLE);
472
473 let directory_client =
474 fidl::endpoints::ClientEnd::new(directory_proxy.into_channel().unwrap().into());
475
476 let (_pair_1, pair_2) = zx::EventPair::create();
477
478 let events = vec![
479 ftest_manager::Event {
480 timestamp: Some(1000),
481 details: Some(ftest_manager::EventDetails::SuiteStarted(
482 ftest_manager::SuiteStartedEventDetails { ..Default::default() },
483 )),
484 ..Default::default()
485 },
486 ftest_manager::Event {
487 details: Some(ftest_manager::EventDetails::SuiteArtifactGenerated(
488 ftest_manager::SuiteArtifactGeneratedEventDetails {
489 artifact: Some(ftest_manager::Artifact::Custom(
490 ftest_manager::CustomArtifact {
491 directory_and_token: Some(ftest_manager::DirectoryAndToken {
492 directory: directory_client,
493 token: pair_2,
494 }),
495 ..Default::default()
496 },
497 )),
498 ..Default::default()
499 },
500 )),
501 ..Default::default()
502 },
503 ftest_manager::Event {
504 timestamp: Some(2000),
505 details: Some(ftest_manager::EventDetails::SuiteStopped(
506 ftest_manager::SuiteStoppedEventDetails {
507 result: Some(ftest_manager::SuiteResult::Finished),
508 ..Default::default()
509 },
510 )),
511 ..Default::default()
512 },
513 ];
514
515 let fake_fut = fake_running_all_suites(
516 suite_runner_stream,
517 hashmap! {
518 "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm" => events
519 },
520 );
521
522 assert_eq!(join(run_fut, fake_fut).await.0, Outcome::Passed,);
523
524 let reports = reporter.get_reports();
525 assert_eq!(2usize, reports.len());
526 let run = reports.iter().find(|e| e.id == EntityId::Suite).expect("find run report");
527 assert_eq!(1usize, run.report.directories.len());
528 let dir = &run.report.directories[0];
529 let files = dir.1.files.lock();
530 assert_eq!(1usize, files.len());
531 let (name, file) = &files[0];
532 assert_eq!(name.to_string_lossy(), "test_file.txt".to_string());
533 assert_eq!(file.get_contents(), b"Hello, World!");
534 }
535
536 #[fuchsia::test]
537 async fn record_output_after_internal_error() {
538 let (runner_proxy, suite_runner_stream) =
539 create_proxy_and_stream::<ftest_manager::SuiteRunnerMarker>();
540
541 let reporter = InMemoryReporter::new();
542 let run_reporter = RunReporter::new(reporter.clone());
543 let run_fut = call_run_tests(ParamsForRunTests {
544 runner_proxy,
545 test_params: TestParams {
546 test_url: "fuchsia-pkg://fuchsia.com/invalid#meta/invalid.cm".to_string(),
547 ..TestParams::default()
548 },
549 run_reporter,
550 });
551
552 let fake_fut = fake_running_all_suites(
553 suite_runner_stream,
554 hashmap! {
555 "fuchsia-pkg://fuchsia.com/invalid#meta/invalid.cm" => vec![
557 ftest_manager::Event {
558 timestamp: Some(1000),
559 details: Some(ftest_manager::EventDetails::SuiteStarted(ftest_manager::SuiteStartedEventDetails {..Default::default()})),
560 ..Default::default()
561 },
562 ftest_manager::Event {
563 timestamp: Some(2000),
564 details: Some(ftest_manager::EventDetails::SuiteStopped(ftest_manager::SuiteStoppedEventDetails {
565 result: Some(ftest_manager::SuiteResult::InternalError),
566 ..Default::default()
567 },
568 )),
569 ..Default::default()
570 },
571 ],
572 },
573 );
574
575 assert_matches!(join(run_fut, fake_fut).await.0, Outcome::Error { .. });
576
577 let reports = reporter.get_reports();
578 assert_eq!(2usize, reports.len());
579 let invalid_suite = reports
580 .iter()
581 .find(|e| e.report.name == "fuchsia-pkg://fuchsia.com/invalid#meta/invalid.cm")
582 .expect("find run report");
583 assert_eq!(invalid_suite.report.outcome, Some(output::ReportedOutcome::Error));
584 assert!(invalid_suite.report.is_finished);
585
586 let run = reports.iter().find(|e| e.id == EntityId::TestRun).expect("find run report");
588 assert_eq!(run.report.outcome, Some(output::ReportedOutcome::Error));
589 assert!(run.report.is_finished);
590 assert!(run.report.started_time.is_some());
591 }
592
593 #[cfg(target_os = "fuchsia")]
594 #[fuchsia::test]
595 async fn single_run_debug_data() {
596 let (runner_proxy, suite_runner_stream) =
597 create_proxy_and_stream::<ftest_manager::SuiteRunnerMarker>();
598
599 let reporter = InMemoryReporter::new();
600 let run_reporter = RunReporter::new(reporter.clone());
601 let run_fut = call_run_tests(ParamsForRunTests {
602 runner_proxy,
603 test_params: TestParams {
604 test_url: "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm".to_string(),
605 ..TestParams::default()
606 },
607 run_reporter,
608 });
609
610 let (debug_client, debug_service) =
611 fidl::endpoints::create_endpoints::<ftest_manager::DebugDataIteratorMarker>();
612 let debug_data_fut = async move {
613 let (client, server) = zx::Socket::create_stream();
614 let mut compressor = zstd::bulk::Compressor::new(0).unwrap();
615 let bytes = compressor.compress(b"Not a real profile").unwrap();
616 let _ = server.write(bytes.as_slice()).unwrap();
617 let mut service = debug_service.into_stream();
618 let mut data = vec![ftest_manager::DebugData {
619 name: Some("test_file.profraw".to_string()),
620 socket: Some(client.into()),
621 ..Default::default()
622 }];
623 drop(server);
624 while let Ok(Some(request)) = service.try_next().await {
625 match request {
626 ftest_manager::DebugDataIteratorRequest::GetNext { .. } => {
627 panic!("Not Implemented");
628 }
629 ftest_manager::DebugDataIteratorRequest::GetNextCompressed {
630 responder,
631 ..
632 } => {
633 let _ = responder.send(std::mem::take(&mut data));
634 }
635 }
636 }
637 };
638
639 let events = vec![
640 ftest_manager::Event {
641 timestamp: Some(1000),
642 details: Some(ftest_manager::EventDetails::SuiteStarted(
643 ftest_manager::SuiteStartedEventDetails { ..Default::default() },
644 )),
645 ..Default::default()
646 },
647 ftest_manager::Event {
648 details: Some(ftest_manager::EventDetails::SuiteArtifactGenerated(
649 ftest_manager::SuiteArtifactGeneratedEventDetails {
650 artifact: Some(ftest_manager::Artifact::DebugData(debug_client)),
651 ..Default::default()
652 },
653 )),
654 ..Default::default()
655 },
656 ftest_manager::Event {
657 timestamp: Some(2000),
658 details: Some(ftest_manager::EventDetails::SuiteStopped(
659 ftest_manager::SuiteStoppedEventDetails {
660 result: Some(ftest_manager::SuiteResult::Finished),
661 ..Default::default()
662 },
663 )),
664 ..Default::default()
665 },
666 ];
667
668 let fake_fut = fake_running_all_suites(
669 suite_runner_stream,
670 hashmap! {
671 "fuchsia-pkg://fuchsia.com/nothing#meta/nothing.cm" => events,
672 },
673 );
674
675 assert_eq!(join3(run_fut, debug_data_fut, fake_fut).await.0, Outcome::Passed);
676
677 let reports = reporter.get_reports();
678 assert_eq!(2usize, reports.len());
679 let run = reports.iter().find(|e| e.id == EntityId::Suite).expect("find run report");
680 assert_eq!(1usize, run.report.directories.len());
681 let dir = &run.report.directories[0];
682 let files = dir.1.files.lock();
683 assert_eq!(1usize, files.len());
684 let (name, file) = &files[0];
685 assert_eq!(name.to_string_lossy(), "test_file.profraw".to_string());
686 assert_eq!(file.get_contents(), b"Not a real profile");
687 }
688}